diff --git a/modules/bazarr/client.ts b/modules/bazarr/client.ts index c312938..cead9ba 100644 --- a/modules/bazarr/client.ts +++ b/modules/bazarr/client.ts @@ -43,6 +43,14 @@ function meshConfig(file?: string): Record { catch { return {}; } } +/** Read a secret the mesh mounted at a file path (an own-secret delivered by `secret accept`); + * absent or unreadable yields undefined so callers fall back rather than crash. */ +function readSecret(file?: string): string | undefined { + if (!file) return undefined; + try { return readFileSync(file, "utf8").trim(); } + catch { return undefined; } +} + export class BazarrClient { readonly baseUrl: string; @@ -58,7 +66,7 @@ export class BazarrClient { static fromEnv(env: NodeJS.ProcessEnv = process.env): BazarrClient { const cfg = meshConfig(env.MESH_BAZARR_CONFIG_FILE); const url = cfg.url ?? env.MESH_BAZARR_URL; - const apiKey = cfg.apiKey ?? env.MESH_BAZARR_API_KEY; + const apiKey = cfg.apiKey ?? readSecret(env.MESH_BAZARR_API_KEY_FILE) ?? env.MESH_BAZARR_API_KEY; if (!url) throw new Error("no Bazarr URL — set MESH_BAZARR_URL"); if (!apiKey) throw new Error("no Bazarr API key — set MESH_BAZARR_API_KEY"); return new BazarrClient(url, apiKey); diff --git a/modules/bazarr/module.json b/modules/bazarr/module.json index 9a4f684..6267f9b 100644 --- a/modules/bazarr/module.json +++ b/modules/bazarr/module.json @@ -8,7 +8,8 @@ "module.bazarr.subtitle.downloaded" ], "own-secrets": { - "broker": "/var/lib/mesh/bazarr/broker" + "broker": "/var/lib/mesh/bazarr/broker", + "api-key": "/var/lib/mesh/bazarr/api-key" }, "listens": [ { @@ -87,12 +88,14 @@ "network": "host", "volumes": [ "/var/lib/mesh/bazarr/broker:/run/secrets/broker:ro", + "/var/lib/mesh/bazarr/api-key:/run/secrets/api-key:ro", "/var/lib/mesh/bazarr/config.json:/run/config/config.json:ro", "/services/bazarr/config:/var/lib/bazarr/config:ro" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", "MESH_BAZARR_URL": "http://127.0.0.1:6767", + "MESH_BAZARR_API_KEY_FILE": "/run/secrets/api-key", "MESH_BAZARR_CONFIG_FILE": "/run/config/config.json", "MESH_BAZARR_CONFIG_DIR": "/var/lib/bazarr/config" }, diff --git a/modules/home-assistant/client.ts b/modules/home-assistant/client.ts index 525ec7e..62e8323 100644 --- a/modules/home-assistant/client.ts +++ b/modules/home-assistant/client.ts @@ -27,6 +27,14 @@ function meshConfig(file?: string): Record { catch { return {}; } } +/** Read a secret the mesh mounted at a file path (an own-secret delivered by `secret accept`); + * absent or unreadable yields undefined so callers fall back rather than crash. */ +function readSecret(file?: string): string | undefined { + if (!file) return undefined; + try { return readFileSync(file, "utf8").trim(); } + catch { return undefined; } +} + export class HomeAssistantClient { readonly baseUrl: string; @@ -45,7 +53,7 @@ export class HomeAssistantClient { static fromEnv(env: NodeJS.ProcessEnv = process.env): HomeAssistantClient { const cfg = meshConfig(env.MESH_HOMEASSISTANT_CONFIG_FILE); const url = cfg.url ?? env.MESH_HOMEASSISTANT_URL ?? `http://127.0.0.1:${env.HOMEASSISTANT_PORT ?? "8123"}`; - const token = cfg.token ?? env.MESH_HOMEASSISTANT_TOKEN; + const token = cfg.token ?? readSecret(env.MESH_HOMEASSISTANT_TOKEN_FILE) ?? env.MESH_HOMEASSISTANT_TOKEN; if (!token) throw new Error("no Home Assistant token — set MESH_HOMEASSISTANT_TOKEN"); return new HomeAssistantClient(url, token); } diff --git a/modules/home-assistant/module.json b/modules/home-assistant/module.json index 2f01f45..1b8fcbc 100644 --- a/modules/home-assistant/module.json +++ b/modules/home-assistant/module.json @@ -8,7 +8,8 @@ "module.home-assistant.state.changed" ], "own-secrets": { - "broker": "/var/lib/mesh/home-assistant/broker" + "broker": "/var/lib/mesh/home-assistant/broker", + "token": "/var/lib/mesh/home-assistant/token" }, "listens": [ { @@ -61,12 +62,14 @@ "network": "host", "volumes": [ "/var/lib/mesh/home-assistant/broker:/run/secrets/broker:ro", + "/var/lib/mesh/home-assistant/token:/run/secrets/token:ro", "/var/lib/mesh/home-assistant/config.json:/run/config/config.json:ro", "/services/home-assistant/config:/var/lib/home-assistant/config:ro" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", "MESH_HOMEASSISTANT_URL": "http://127.0.0.1:8123", + "MESH_HOMEASSISTANT_TOKEN_FILE": "/run/secrets/token", "MESH_HOMEASSISTANT_CONFIG_FILE": "/run/config/config.json", "MESH_HOMEASSISTANT_CONFIG_DIR": "/var/lib/home-assistant/config" }, diff --git a/modules/nzbget/client.ts b/modules/nzbget/client.ts index cd5f0c3..f4d82f4 100644 --- a/modules/nzbget/client.ts +++ b/modules/nzbget/client.ts @@ -48,6 +48,14 @@ function meshConfig(file?: string): Record { catch { return {}; } } +/** Read a secret the mesh mounted at a file path (an own-secret delivered by `secret accept`); + * absent or unreadable yields undefined so callers fall back rather than crash. */ +function readSecret(file?: string): string | undefined { + if (!file) return undefined; + try { return readFileSync(file, "utf8").trim(); } + catch { return undefined; } +} + export class NzbgetClient { readonly rpcUrl: string; private readonly auth: string; @@ -66,7 +74,7 @@ export class NzbgetClient { static fromEnv(env: NodeJS.ProcessEnv = process.env): NzbgetClient { const cfg = meshConfig(env.MESH_NZBGET_CONFIG_FILE); const url = cfg.url ?? env.MESH_NZBGET_URL; - const password = cfg.password ?? env.MESH_NZBGET_PASSWORD; + const password = cfg.password ?? readSecret(env.MESH_NZBGET_PASSWORD_FILE) ?? env.MESH_NZBGET_PASSWORD; if (!url || !password) { throw new Error("NZBGet not configured — set MESH_NZBGET_URL and MESH_NZBGET_PASSWORD"); } diff --git a/modules/nzbget/module.json b/modules/nzbget/module.json index e3dee8e..42b9cdf 100644 --- a/modules/nzbget/module.json +++ b/modules/nzbget/module.json @@ -10,7 +10,8 @@ ], "consumes": [], "own-secrets": { - "broker": "/var/lib/mesh/nzbget/broker" + "broker": "/var/lib/mesh/nzbget/broker", + "password": "/var/lib/mesh/nzbget/password" }, "listens": [ { @@ -74,12 +75,14 @@ "network": "host", "volumes": [ "/var/lib/mesh/nzbget/broker:/run/secrets/broker:ro", + "/var/lib/mesh/nzbget/password:/run/secrets/password:ro", "/var/lib/mesh/nzbget/config.json:/run/config/config.json:ro", "/services/nzbget/config:/var/lib/nzbget/config:ro" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", "MESH_NZBGET_URL": "http://127.0.0.1:6789", + "MESH_NZBGET_PASSWORD_FILE": "/run/secrets/password", "MESH_NZBGET_CONFIG_FILE": "/run/config/config.json", "MESH_NZBGET_CONFIG_DIR": "/var/lib/nzbget/config" }, diff --git a/modules/ombi/client.ts b/modules/ombi/client.ts index 8181140..6bfd82d 100644 --- a/modules/ombi/client.ts +++ b/modules/ombi/client.ts @@ -29,6 +29,14 @@ function meshConfig(file?: string): Record { catch { return {}; } } +/** Read a secret the mesh mounted at a file path (an own-secret delivered by `secret accept`); + * absent or unreadable yields undefined so callers fall back rather than crash. */ +function readSecret(file?: string): string | undefined { + if (!file) return undefined; + try { return readFileSync(file, "utf8").trim(); } + catch { return undefined; } +} + export class OmbiClient { readonly baseUrl: string; @@ -44,7 +52,7 @@ export class OmbiClient { static fromEnv(env: NodeJS.ProcessEnv = process.env): OmbiClient { const cfg = meshConfig(env.MESH_OMBI_CONFIG_FILE); const url = cfg.url ?? env.MESH_OMBI_URL; - const apiKey = cfg.apiKey ?? env.MESH_OMBI_API_KEY; + const apiKey = cfg.apiKey ?? readSecret(env.MESH_OMBI_API_KEY_FILE) ?? env.MESH_OMBI_API_KEY; if (!url) throw new Error("no Ombi URL — set MESH_OMBI_URL"); if (!apiKey) throw new Error("no Ombi API key — set MESH_OMBI_API_KEY"); return new OmbiClient(url, apiKey); diff --git a/modules/ombi/module.json b/modules/ombi/module.json index f16bea2..727355c 100644 --- a/modules/ombi/module.json +++ b/modules/ombi/module.json @@ -9,7 +9,8 @@ "module.ombi.request.approved" ], "own-secrets": { - "broker": "/var/lib/mesh/ombi/broker" + "broker": "/var/lib/mesh/ombi/broker", + "api-key": "/var/lib/mesh/ombi/api-key" }, "listens": [ { @@ -66,12 +67,14 @@ "network": "host", "volumes": [ "/var/lib/mesh/ombi/broker:/run/secrets/broker:ro", + "/var/lib/mesh/ombi/api-key:/run/secrets/api-key:ro", "/var/lib/mesh/ombi/config.json:/run/config/config.json:ro", "/services/ombi/config:/var/lib/ombi/config:ro" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", "MESH_OMBI_URL": "http://127.0.0.1:3579", + "MESH_OMBI_API_KEY_FILE": "/run/secrets/api-key", "MESH_OMBI_CONFIG_FILE": "/run/config/config.json", "MESH_OMBI_CONFIG_DIR": "/var/lib/ombi/config" }, diff --git a/modules/plex/client.ts b/modules/plex/client.ts index 8ff03bb..5aa7e6f 100644 --- a/modules/plex/client.ts +++ b/modules/plex/client.ts @@ -5,6 +5,17 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; +/** Read a secret the mesh mounted at a file path (an own-secret); absent or unreadable yields + * undefined, so callers can fall back rather than crash. */ +function readSecret(path: string | undefined): string | undefined { + if (!path) return undefined; + try { + return readFileSync(path, "utf8").trim(); + } catch { + return undefined; + } +} + export interface PlexLibrary { key: string; title: string; @@ -47,7 +58,9 @@ export class PlexClient { static fromEnv(env: NodeJS.ProcessEnv = process.env): PlexClient { const url = env.MESH_PLEX_URL ?? `http://127.0.0.1:${env.PLEX_PORT ?? "32400"}`; const dataDir = env.MESH_PLEX_DATA_DIR ?? "/var/lib/plex"; - const token = env.MESH_PLEX_TOKEN ?? PlexClient.detectToken(dataDir); + // The operator-provided token is an own-secret the mesh mounts at MESH_PLEX_TOKEN_FILE (delivered + // by `secret accept`); prefer it, fall back to a bare env var, then to discovery from the data dir. + const token = readSecret(env.MESH_PLEX_TOKEN_FILE) ?? env.MESH_PLEX_TOKEN ?? PlexClient.detectToken(dataDir); if (!token) throw new Error("no Plex token — set MESH_PLEX_TOKEN or make the data dir readable"); return new PlexClient(url, token); } diff --git a/modules/plex/module.json b/modules/plex/module.json index 665252f..c184c18 100644 --- a/modules/plex/module.json +++ b/modules/plex/module.json @@ -13,7 +13,8 @@ "module.*.download.completed" ], "own-secrets": { - "broker": "/var/lib/mesh/plex/broker" + "broker": "/var/lib/mesh/plex/broker", + "token": "/var/lib/mesh/plex/token" }, "listens": [ { @@ -95,11 +96,13 @@ "network": "host", "volumes": [ "/var/lib/mesh/plex/broker:/run/secrets/broker:ro", + "/var/lib/mesh/plex/token:/run/secrets/token:ro", "/services/plex/config:/var/lib/plex/config:ro" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", "MESH_PLEX_URL": "http://127.0.0.1:32400", + "MESH_PLEX_TOKEN_FILE": "/run/secrets/token", "MESH_PLEX_DATA_DIR": "/var/lib/plex" } } diff --git a/modules/qbittorrent/client.ts b/modules/qbittorrent/client.ts index 2bf9964..a5c9e80 100644 --- a/modules/qbittorrent/client.ts +++ b/modules/qbittorrent/client.ts @@ -39,6 +39,14 @@ function meshConfig(file?: string): Record { catch { return {}; } } +/** Read a secret the mesh mounted at a file path (an own-secret delivered by `secret accept`); + * absent or unreadable yields undefined so callers fall back rather than crash. */ +function readSecret(file?: string): string | undefined { + if (!file) return undefined; + try { return readFileSync(file, "utf8").trim(); } + catch { return undefined; } +} + export class QbittorrentClient { readonly baseUrl: string; private sid: string | null = null; @@ -60,7 +68,7 @@ export class QbittorrentClient { static fromEnv(env: NodeJS.ProcessEnv = process.env): QbittorrentClient { const cfg = meshConfig(env.MESH_QBITTORRENT_CONFIG_FILE); const url = cfg.url ?? env.MESH_QBITTORRENT_URL; - const password = cfg.password ?? env.MESH_QBITTORRENT_PASSWORD; + const password = cfg.password ?? readSecret(env.MESH_QBITTORRENT_PASSWORD_FILE) ?? env.MESH_QBITTORRENT_PASSWORD; if (!url || !password) { throw new Error("qBittorrent not configured — set MESH_QBITTORRENT_URL and MESH_QBITTORRENT_PASSWORD"); } diff --git a/modules/qbittorrent/module.json b/modules/qbittorrent/module.json index 56b0858..b13396c 100644 --- a/modules/qbittorrent/module.json +++ b/modules/qbittorrent/module.json @@ -10,7 +10,8 @@ ], "consumes": [], "own-secrets": { - "broker": "/var/lib/mesh/qbittorrent/broker" + "broker": "/var/lib/mesh/qbittorrent/broker", + "password": "/var/lib/mesh/qbittorrent/password" }, "listens": [ { @@ -74,12 +75,14 @@ "network": "host", "volumes": [ "/var/lib/mesh/qbittorrent/broker:/run/secrets/broker:ro", + "/var/lib/mesh/qbittorrent/password:/run/secrets/password:ro", "/var/lib/mesh/qbittorrent/config.json:/run/config/config.json:ro", "/services/qbittorrent/config:/var/lib/qbittorrent/config:ro" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", "MESH_QBITTORRENT_URL": "http://127.0.0.1:8080", + "MESH_QBITTORRENT_PASSWORD_FILE": "/run/secrets/password", "MESH_QBITTORRENT_CONFIG_FILE": "/run/config/config.json", "MESH_QBITTORRENT_CONFIG_DIR": "/var/lib/qbittorrent/config" },