Protect local state file permissions (#825)
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
import { chmodSync, mkdtempSync, rmSync, statSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { loadOrCreateDaemonKeyPair } from "./daemon-keypair.js";
|
||||||
|
import { PRIVATE_FILE_MODE } from "./private-files.js";
|
||||||
|
|
||||||
|
const MODE_MASK = 0o777;
|
||||||
|
const PERMISSIVE_FILE_MODE = 0o644;
|
||||||
|
|
||||||
|
function createTempHome(): string {
|
||||||
|
return mkdtempSync(path.join(tmpdir(), "paseo-keypair-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeOf(filePath: string): number {
|
||||||
|
return statSync(filePath).mode & MODE_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.skipIf(process.platform === "win32")("daemon keypair file permissions", () => {
|
||||||
|
test("creates daemon-keypair.json with private permissions", async () => {
|
||||||
|
const home = createTempHome();
|
||||||
|
try {
|
||||||
|
await loadOrCreateDaemonKeyPair(home);
|
||||||
|
|
||||||
|
expect(modeOf(path.join(home, "daemon-keypair.json"))).toBe(PRIVATE_FILE_MODE);
|
||||||
|
} finally {
|
||||||
|
rmSync(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("repairs existing daemon-keypair.json permissions when loading", async () => {
|
||||||
|
const home = createTempHome();
|
||||||
|
const keypairPath = path.join(home, "daemon-keypair.json");
|
||||||
|
try {
|
||||||
|
const created = await loadOrCreateDaemonKeyPair(home);
|
||||||
|
chmodSync(keypairPath, PERMISSIVE_FILE_MODE);
|
||||||
|
|
||||||
|
const loaded = await loadOrCreateDaemonKeyPair(home);
|
||||||
|
|
||||||
|
expect(loaded.publicKeyB64).toBe(created.publicKeyB64);
|
||||||
|
expect(modeOf(keypairPath)).toBe(PRIVATE_FILE_MODE);
|
||||||
|
} finally {
|
||||||
|
rmSync(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type pino from "pino";
|
import type pino from "pino";
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
importSecretKey,
|
importSecretKey,
|
||||||
type KeyPair,
|
type KeyPair,
|
||||||
} from "@getpaseo/relay/e2ee";
|
} from "@getpaseo/relay/e2ee";
|
||||||
|
import { ensurePrivateFile, writePrivateFileSync } from "./private-files.js";
|
||||||
|
|
||||||
const KeyPairSchema = z.object({
|
const KeyPairSchema = z.object({
|
||||||
v: z.literal(2),
|
v: z.literal(2),
|
||||||
@@ -36,6 +37,7 @@ export async function loadOrCreateDaemonKeyPair(
|
|||||||
|
|
||||||
if (existsSync(filePath)) {
|
if (existsSync(filePath)) {
|
||||||
try {
|
try {
|
||||||
|
ensurePrivateFile(filePath);
|
||||||
const raw = readFileSync(filePath, "utf8");
|
const raw = readFileSync(filePath, "utf8");
|
||||||
const parsed = KeyPairSchema.parse(JSON.parse(raw));
|
const parsed = KeyPairSchema.parse(JSON.parse(raw));
|
||||||
|
|
||||||
@@ -60,7 +62,7 @@ export async function loadOrCreateDaemonKeyPair(
|
|||||||
secretKeyB64,
|
secretKeyB64,
|
||||||
};
|
};
|
||||||
|
|
||||||
writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", { mode: 0o600 });
|
writePrivateFileSync(filePath, JSON.stringify(payload, null, 2) + "\n");
|
||||||
log?.info({ filePath }, "Saved daemon keypair");
|
log?.info({ filePath }, "Saved daemon keypair");
|
||||||
|
|
||||||
return { keyPair, publicKeyB64 };
|
return { keyPair, publicKeyB64 };
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { mkdtempSync, rmSync, statSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { resolvePaseoHome } from "./paseo-home.js";
|
||||||
|
import { PRIVATE_DIRECTORY_MODE } from "./private-files.js";
|
||||||
|
|
||||||
|
const MODE_MASK = 0o777;
|
||||||
|
|
||||||
|
function modeOf(filePath: string): number {
|
||||||
|
return statSync(filePath).mode & MODE_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.skipIf(process.platform === "win32")("resolvePaseoHome permissions", () => {
|
||||||
|
test("creates PASEO_HOME with private permissions", () => {
|
||||||
|
const parent = mkdtempSync(path.join(tmpdir(), "paseo-home-parent-"));
|
||||||
|
const paseoHome = path.join(parent, "home");
|
||||||
|
try {
|
||||||
|
expect(resolvePaseoHome({ PASEO_HOME: paseoHome })).toBe(paseoHome);
|
||||||
|
expect(modeOf(paseoHome)).toBe(PRIVATE_DIRECTORY_MODE);
|
||||||
|
} finally {
|
||||||
|
rmSync(parent, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { mkdirSync } from "node:fs";
|
import { ensurePrivateDirectory } from "./private-files.js";
|
||||||
|
|
||||||
function expandHomeDir(input: string): string {
|
function expandHomeDir(input: string): string {
|
||||||
if (input.startsWith("~/")) {
|
if (input.startsWith("~/")) {
|
||||||
@@ -15,6 +15,6 @@ function expandHomeDir(input: string): string {
|
|||||||
export function resolvePaseoHome(env: NodeJS.ProcessEnv = process.env): string {
|
export function resolvePaseoHome(env: NodeJS.ProcessEnv = process.env): string {
|
||||||
const raw = env.PASEO_HOME ?? "~/.paseo";
|
const raw = env.PASEO_HOME ?? "~/.paseo";
|
||||||
const resolved = path.resolve(expandHomeDir(raw));
|
const resolved = path.resolve(expandHomeDir(raw));
|
||||||
mkdirSync(resolved, { recursive: true });
|
ensurePrivateDirectory(resolved);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
|
import { chmodSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import { PersistedConfigSchema } from "./persisted-config.js";
|
import {
|
||||||
|
loadPersistedConfig,
|
||||||
|
PersistedConfigSchema,
|
||||||
|
savePersistedConfig,
|
||||||
|
} from "./persisted-config.js";
|
||||||
|
import { PRIVATE_FILE_MODE } from "./private-files.js";
|
||||||
|
|
||||||
|
const MODE_MASK = 0o777;
|
||||||
|
const PERMISSIVE_FILE_MODE = 0o644;
|
||||||
|
|
||||||
|
function createTempHome(): string {
|
||||||
|
return mkdtempSync(path.join(tmpdir(), "paseo-config-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeOf(filePath: string): number {
|
||||||
|
return statSync(filePath).mode & MODE_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
describe("PersistedConfigSchema daemon auth config", () => {
|
describe("PersistedConfigSchema daemon auth config", () => {
|
||||||
test("accepts optional daemon password hash", () => {
|
test("accepts optional daemon password hash", () => {
|
||||||
@@ -465,3 +484,48 @@ describe("PersistedConfigSchema voice mode config", () => {
|
|||||||
expect(parsed.features?.voiceMode?.turnDetection?.provider).toBe("local");
|
expect(parsed.features?.voiceMode?.turnDetection?.provider).toBe("local");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe.skipIf(process.platform === "win32")("persisted config file permissions", () => {
|
||||||
|
test("initializes config.json with private permissions", () => {
|
||||||
|
const home = createTempHome();
|
||||||
|
try {
|
||||||
|
loadPersistedConfig(home);
|
||||||
|
|
||||||
|
expect(modeOf(path.join(home, "config.json"))).toBe(PRIVATE_FILE_MODE);
|
||||||
|
} finally {
|
||||||
|
rmSync(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("repairs permissive config.json permissions when loading", () => {
|
||||||
|
const home = createTempHome();
|
||||||
|
const configPath = path.join(home, "config.json");
|
||||||
|
try {
|
||||||
|
writeFileSync(configPath, "{}\n", { mode: PERMISSIVE_FILE_MODE });
|
||||||
|
chmodSync(configPath, PERMISSIVE_FILE_MODE);
|
||||||
|
|
||||||
|
loadPersistedConfig(home);
|
||||||
|
|
||||||
|
expect(modeOf(configPath)).toBe(PRIVATE_FILE_MODE);
|
||||||
|
} finally {
|
||||||
|
rmSync(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saves config.json with private permissions", () => {
|
||||||
|
const home = createTempHome();
|
||||||
|
try {
|
||||||
|
savePersistedConfig(home, {
|
||||||
|
providers: {
|
||||||
|
openai: {
|
||||||
|
apiKey: "secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(modeOf(path.join(home, "config.json"))).toBe(PRIVATE_FILE_MODE);
|
||||||
|
} finally {
|
||||||
|
rmSync(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ProviderOverrideSchema,
|
ProviderOverrideSchema,
|
||||||
} from "./agent/provider-launch-config.js";
|
} from "./agent/provider-launch-config.js";
|
||||||
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
||||||
|
import { ensurePrivateFile, writePrivateFileSync } from "./private-files.js";
|
||||||
|
|
||||||
export const LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
export const LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
||||||
export const LogFormatSchema = z.enum(["pretty", "json"]);
|
export const LogFormatSchema = z.enum(["pretty", "json"]);
|
||||||
@@ -369,8 +370,7 @@ export function loadPersistedConfig(paseoHome: string, logger?: LoggerLike): Per
|
|||||||
|
|
||||||
if (!existsSync(configPath)) {
|
if (!existsSync(configPath)) {
|
||||||
try {
|
try {
|
||||||
mkdirSync(path.dirname(configPath), { recursive: true });
|
writePrivateFileSync(configPath, JSON.stringify(DEFAULT_PERSISTED_CONFIG, null, 2) + "\n");
|
||||||
writeFileSync(configPath, JSON.stringify(DEFAULT_PERSISTED_CONFIG, null, 2) + "\n");
|
|
||||||
log?.info(`Initialized config file at ${configPath}`);
|
log?.info(`Initialized config file at ${configPath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
@@ -380,6 +380,7 @@ export function loadPersistedConfig(paseoHome: string, logger?: LoggerLike): Per
|
|||||||
|
|
||||||
let raw: string;
|
let raw: string;
|
||||||
try {
|
try {
|
||||||
|
ensurePrivateFile(configPath);
|
||||||
raw = readFileSync(configPath, "utf-8");
|
raw = readFileSync(configPath, "utf-8");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
@@ -424,7 +425,7 @@ export function savePersistedConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
writeFileSync(configPath, JSON.stringify(result.data, null, 2) + "\n");
|
writePrivateFileSync(configPath, JSON.stringify(result.data, null, 2) + "\n");
|
||||||
log?.info(`Saved to ${configPath}`);
|
log?.info(`Saved to ${configPath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export const PRIVATE_DIRECTORY_MODE = 0o700;
|
||||||
|
export const PRIVATE_FILE_MODE = 0o600;
|
||||||
|
|
||||||
|
function chmodBestEffort(targetPath: string, mode: number): void {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
chmodSync(targetPath, mode);
|
||||||
|
} catch {
|
||||||
|
// Keep startup resilient if the filesystem does not support POSIX modes.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensurePrivateDirectory(directoryPath: string): void {
|
||||||
|
mkdirSync(directoryPath, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
||||||
|
chmodBestEffort(directoryPath, PRIVATE_DIRECTORY_MODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensurePrivateFile(filePath: string): void {
|
||||||
|
chmodBestEffort(filePath, PRIVATE_FILE_MODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writePrivateFileSync(
|
||||||
|
filePath: string,
|
||||||
|
data: string | NodeJS.ArrayBufferView,
|
||||||
|
): void {
|
||||||
|
ensurePrivateDirectory(path.dirname(filePath));
|
||||||
|
writeFileSync(filePath, data, { mode: PRIVATE_FILE_MODE });
|
||||||
|
ensurePrivateFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writePrivateFileAtomicSync(
|
||||||
|
filePath: string,
|
||||||
|
data: string | NodeJS.ArrayBufferView,
|
||||||
|
): void {
|
||||||
|
ensurePrivateDirectory(path.dirname(filePath));
|
||||||
|
const tmpPath = path.join(
|
||||||
|
path.dirname(filePath),
|
||||||
|
`.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
writeFileSync(tmpPath, data, { mode: PRIVATE_FILE_MODE });
|
||||||
|
renameSync(tmpPath, filePath);
|
||||||
|
ensurePrivateFile(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
rmSync(tmpPath, { force: true });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { chmodSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import type pino from "pino";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { PRIVATE_FILE_MODE } from "../private-files.js";
|
||||||
|
import { PushTokenStore } from "./token-store.js";
|
||||||
|
|
||||||
|
const MODE_MASK = 0o777;
|
||||||
|
const PERMISSIVE_FILE_MODE = 0o644;
|
||||||
|
|
||||||
|
function createLogger(): pino.Logger {
|
||||||
|
const logger = {
|
||||||
|
child: () => logger,
|
||||||
|
debug: () => undefined,
|
||||||
|
info: () => undefined,
|
||||||
|
warn: () => undefined,
|
||||||
|
};
|
||||||
|
return logger as unknown as pino.Logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeOf(filePath: string): number {
|
||||||
|
return statSync(filePath).mode & MODE_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.skipIf(process.platform === "win32")("PushTokenStore file permissions", () => {
|
||||||
|
test("persists push tokens with private permissions", () => {
|
||||||
|
const home = mkdtempSync(path.join(tmpdir(), "paseo-push-tokens-"));
|
||||||
|
const tokenPath = path.join(home, "push-tokens.json");
|
||||||
|
try {
|
||||||
|
const store = new PushTokenStore(createLogger(), tokenPath);
|
||||||
|
|
||||||
|
store.addToken("ExponentPushToken[test]");
|
||||||
|
|
||||||
|
expect(modeOf(tokenPath)).toBe(PRIVATE_FILE_MODE);
|
||||||
|
} finally {
|
||||||
|
rmSync(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("repairs existing push token file permissions when loading", () => {
|
||||||
|
const home = mkdtempSync(path.join(tmpdir(), "paseo-push-tokens-"));
|
||||||
|
const tokenPath = path.join(home, "push-tokens.json");
|
||||||
|
try {
|
||||||
|
writeFileSync(tokenPath, JSON.stringify({ tokens: ["ExponentPushToken[test]"] }));
|
||||||
|
chmodSync(tokenPath, PERMISSIVE_FILE_MODE);
|
||||||
|
|
||||||
|
const store = new PushTokenStore(createLogger(), tokenPath);
|
||||||
|
|
||||||
|
expect(store.getAllTokens()).toEqual(["ExponentPushToken[test]"]);
|
||||||
|
expect(modeOf(tokenPath)).toBe(PRIVATE_FILE_MODE);
|
||||||
|
} finally {
|
||||||
|
rmSync(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type pino from "pino";
|
import type pino from "pino";
|
||||||
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { dirname } from "node:path";
|
|
||||||
|
import { ensurePrivateFile, writePrivateFileAtomicSync } from "../private-files.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store for Expo push tokens.
|
* Store for Expo push tokens.
|
||||||
@@ -46,6 +47,7 @@ export class PushTokenStore {
|
|||||||
if (!existsSync(this.filePath)) {
|
if (!existsSync(this.filePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ensurePrivateFile(this.filePath);
|
||||||
const raw = readFileSync(this.filePath, "utf-8");
|
const raw = readFileSync(this.filePath, "utf-8");
|
||||||
const parsed = JSON.parse(raw) as { tokens?: unknown };
|
const parsed = JSON.parse(raw) as { tokens?: unknown };
|
||||||
const tokens = Array.isArray(parsed.tokens)
|
const tokens = Array.isArray(parsed.tokens)
|
||||||
@@ -61,11 +63,8 @@ export class PushTokenStore {
|
|||||||
|
|
||||||
private persist(): void {
|
private persist(): void {
|
||||||
try {
|
try {
|
||||||
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
||||||
const tmpPath = `${this.filePath}.tmp`;
|
|
||||||
const payload = JSON.stringify({ tokens: Array.from(this.tokens) }, null, 2) + "\n";
|
const payload = JSON.stringify({ tokens: Array.from(this.tokens) }, null, 2) + "\n";
|
||||||
writeFileSync(tmpPath, payload);
|
writePrivateFileAtomicSync(this.filePath, payload);
|
||||||
renameSync(tmpPath, this.filePath);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error instanceof Error ? error : new Error(String(error));
|
const err = error instanceof Error ? error : new Error(String(error));
|
||||||
this.logger.warn({ err }, "Failed to persist push tokens");
|
this.logger.warn({ err }, "Failed to persist push tokens");
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
import { mkdtempSync, rmSync, existsSync, readFileSync } from "node:fs";
|
import {
|
||||||
|
chmodSync,
|
||||||
|
existsSync,
|
||||||
|
mkdtempSync,
|
||||||
|
readFileSync,
|
||||||
|
rmSync,
|
||||||
|
statSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { getOrCreateServerId } from "./server-id.js";
|
import { getOrCreateServerId } from "./server-id.js";
|
||||||
|
import { PRIVATE_FILE_MODE } from "./private-files.js";
|
||||||
|
|
||||||
|
const MODE_MASK = 0o777;
|
||||||
|
const PERMISSIVE_FILE_MODE = 0o644;
|
||||||
|
|
||||||
function tmpHome(): string {
|
function tmpHome(): string {
|
||||||
return mkdtempSync(path.join(tmpdir(), "paseo-server-id-"));
|
return mkdtempSync(path.join(tmpdir(), "paseo-server-id-"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function modeOf(filePath: string): number {
|
||||||
|
return statSync(filePath).mode & MODE_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
describe("getOrCreateServerId", () => {
|
describe("getOrCreateServerId", () => {
|
||||||
let home: string;
|
let home: string;
|
||||||
const originalEnv = process.env;
|
const originalEnv = process.env;
|
||||||
@@ -43,4 +59,31 @@ describe("getOrCreateServerId", () => {
|
|||||||
expect(existsSync(idPath)).toBe(true);
|
expect(existsSync(idPath)).toBe(true);
|
||||||
expect(readFileSync(idPath, "utf8").trim()).toBe("test-daemon-id");
|
expect(readFileSync(idPath, "utf8").trim()).toBe("test-daemon-id");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe.skipIf(process.platform === "win32")("file permissions", () => {
|
||||||
|
it("creates server-id with private permissions", () => {
|
||||||
|
getOrCreateServerId(home);
|
||||||
|
|
||||||
|
expect(modeOf(path.join(home, "server-id"))).toBe(PRIVATE_FILE_MODE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("repairs existing server-id permissions when loading", () => {
|
||||||
|
const idPath = path.join(home, "server-id");
|
||||||
|
writeFileSync(idPath, "srv_existing\n", { mode: PERMISSIVE_FILE_MODE });
|
||||||
|
chmodSync(idPath, PERMISSIVE_FILE_MODE);
|
||||||
|
|
||||||
|
expect(getOrCreateServerId(home)).toBe("srv_existing");
|
||||||
|
expect(modeOf(idPath)).toBe(PRIVATE_FILE_MODE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("repairs existing server-id permissions when using an env override", () => {
|
||||||
|
const idPath = path.join(home, "server-id");
|
||||||
|
process.env.PASEO_SERVER_ID = "test-daemon-id";
|
||||||
|
writeFileSync(idPath, "srv_existing\n", { mode: PERMISSIVE_FILE_MODE });
|
||||||
|
chmodSync(idPath, PERMISSIVE_FILE_MODE);
|
||||||
|
|
||||||
|
expect(getOrCreateServerId(home)).toBe("test-daemon-id");
|
||||||
|
expect(modeOf(idPath)).toBe(PRIVATE_FILE_MODE);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
|
|
||||||
|
import { ensurePrivateFile, writePrivateFileSync } from "./private-files.js";
|
||||||
|
|
||||||
interface LoggerLike {
|
interface LoggerLike {
|
||||||
child(bindings: Record<string, unknown>): LoggerLike;
|
child(bindings: Record<string, unknown>): LoggerLike;
|
||||||
info(...args: unknown[]): void;
|
info(...args: unknown[]): void;
|
||||||
@@ -47,17 +49,20 @@ export function getOrCreateServerId(
|
|||||||
// Persist the override for consistent identity across restarts.
|
// Persist the override for consistent identity across restarts.
|
||||||
if (!existsSync(serverIdPath)) {
|
if (!existsSync(serverIdPath)) {
|
||||||
try {
|
try {
|
||||||
writeFileSync(serverIdPath, `${envOverride}\n`, "utf8");
|
writePrivateFileSync(serverIdPath, `${envOverride}\n`);
|
||||||
log?.info({ serverId: envOverride }, "Persisted PASEO_SERVER_ID override");
|
log?.info({ serverId: envOverride }, "Persisted PASEO_SERVER_ID override");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log?.warn({ error }, "Failed to persist PASEO_SERVER_ID override");
|
log?.warn({ error }, "Failed to persist PASEO_SERVER_ID override");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ensurePrivateFile(serverIdPath);
|
||||||
}
|
}
|
||||||
return envOverride;
|
return envOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsSync(serverIdPath)) {
|
if (existsSync(serverIdPath)) {
|
||||||
try {
|
try {
|
||||||
|
ensurePrivateFile(serverIdPath);
|
||||||
const raw = readFileSync(serverIdPath, "utf8");
|
const raw = readFileSync(serverIdPath, "utf8");
|
||||||
const parsed = raw.trim();
|
const parsed = raw.trim();
|
||||||
if (parsed.length > 0) {
|
if (parsed.length > 0) {
|
||||||
@@ -70,7 +75,7 @@ export function getOrCreateServerId(
|
|||||||
|
|
||||||
const created = generateServerId();
|
const created = generateServerId();
|
||||||
try {
|
try {
|
||||||
writeFileSync(serverIdPath, `${created}\n`, "utf8");
|
writePrivateFileSync(serverIdPath, `${created}\n`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log?.warn({ error }, "Failed to persist serverId (continuing with in-memory id)");
|
log?.warn({ error }, "Failed to persist serverId (continuing with in-memory id)");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user