audit-logger: the assigned-module manifest (ADR 0048) #2
@@ -3,6 +3,8 @@
|
||||
// missing subtitles, searches providers for them, and records what it downloaded. This client
|
||||
// talks its /api surface (keyed by an X-API-KEY header); bazarr's tools and events import it.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface WantedSubtitle {
|
||||
kind: "episode" | "movie";
|
||||
title: string; // series + episode, or movie title
|
||||
@@ -34,6 +36,13 @@ export interface HistoryEntry {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class BazarrClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -47,8 +56,9 @@ export class BazarrClient {
|
||||
/** Build from the module's resolved environment. Bazarr's API is keyed; without URL and key
|
||||
* there is nothing to talk to, so this throws rather than run half-configured. */
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): BazarrClient {
|
||||
const url = env.MESH_BAZARR_URL;
|
||||
const apiKey = env.MESH_BAZARR_API_KEY;
|
||||
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;
|
||||
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);
|
||||
|
||||
@@ -80,6 +80,24 @@
|
||||
"/services/media/anime:/anime",
|
||||
"/services/media/downloads:/downloads"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-bazarr",
|
||||
"image": "mesh-runtime-bazarr@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/bazarr/broker:/run/secrets/broker: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_CONFIG_FILE": "/run/config/config.json",
|
||||
"MESH_BAZARR_CONFIG_DIR": "/var/lib/bazarr/config"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+12
-2
@@ -3,6 +3,8 @@
|
||||
// gitea. Both this module's tools and its events entrypoint import it, and nothing outside gitea
|
||||
// does.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
/** A repository, trimmed to what the mesh cares about. */
|
||||
export interface GiteaRepo {
|
||||
full_name: string;
|
||||
@@ -42,6 +44,13 @@ export interface GiteaLabel {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class GiteaClient {
|
||||
readonly baseUrl: string;
|
||||
private cachedUsername: string | null = null;
|
||||
@@ -60,8 +69,9 @@ export class GiteaClient {
|
||||
* call to make, so this throws rather than hand back a client that fails on first use.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): GiteaClient {
|
||||
const url = env.MESH_GITEA_URL ?? env.GITEA_URL ?? `http://127.0.0.1:${env.GITEA_PORT ?? "3000"}`;
|
||||
const token = env.MESH_GITEA_TOKEN ?? env.GITEA_TOKEN;
|
||||
const cfg = meshConfig(env.MESH_GITEA_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_GITEA_URL ?? env.GITEA_URL ?? `http://127.0.0.1:${env.GITEA_PORT ?? "3000"}`;
|
||||
const token = cfg.token ?? env.MESH_GITEA_TOKEN ?? env.GITEA_TOKEN;
|
||||
if (!token) throw new Error("no Gitea token — set MESH_GITEA_TOKEN");
|
||||
return new GiteaClient(url, token);
|
||||
}
|
||||
|
||||
@@ -88,6 +88,30 @@
|
||||
"volumes": [
|
||||
"/services/gitea/gitea:/data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/gitea/config.json",
|
||||
"mode": "0600",
|
||||
"content": "{}\n",
|
||||
"merge": "json"
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-gitea",
|
||||
"image": "mesh-runtime-gitea@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/gitea/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/gitea/config.json:/run/config/config.json:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_GITEA_URL": "http://127.0.0.1:3000",
|
||||
"MESH_GITEA_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// the shared hal sdk, where a change here rebuilt everything; here it rebuilds only grafana. Both
|
||||
// this module's tools and its events entrypoint import it, and nothing outside grafana does.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface GrafanaHealth {
|
||||
database: string;
|
||||
version: string;
|
||||
@@ -35,6 +37,13 @@ export interface GrafanaAlert {
|
||||
activeAt?: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class GrafanaClient {
|
||||
readonly baseUrl: string;
|
||||
private readonly authHeader: string;
|
||||
@@ -51,12 +60,13 @@ export class GrafanaClient {
|
||||
* Throws when neither is configured — the module then contributes nothing rather than failing.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): GrafanaClient {
|
||||
const url = env.MESH_GRAFANA_URL ?? `http://127.0.0.1:${env.GRAFANA_PORT ?? "3000"}`;
|
||||
const token = env.MESH_GRAFANA_TOKEN;
|
||||
const cfg = meshConfig(env.MESH_GRAFANA_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_GRAFANA_URL ?? `http://127.0.0.1:${env.GRAFANA_PORT ?? "3000"}`;
|
||||
const token = cfg.token ?? env.MESH_GRAFANA_TOKEN;
|
||||
if (token) return new GrafanaClient(url, `Bearer ${token}`);
|
||||
const password = env.MESH_GRAFANA_PASSWORD;
|
||||
const password = cfg.password ?? env.MESH_GRAFANA_PASSWORD;
|
||||
if (password) {
|
||||
const user = env.MESH_GRAFANA_USER ?? "admin";
|
||||
const user = cfg.user ?? env.MESH_GRAFANA_USER ?? "admin";
|
||||
return new GrafanaClient(url, `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`);
|
||||
}
|
||||
throw new Error("no Grafana auth — set MESH_GRAFANA_TOKEN or MESH_GRAFANA_PASSWORD");
|
||||
|
||||
@@ -60,6 +60,30 @@
|
||||
"volumes": [
|
||||
"/services/grafana/data:/var/lib/grafana"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/grafana/config.json",
|
||||
"mode": "0600",
|
||||
"content": "{}\n",
|
||||
"merge": "json"
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-grafana",
|
||||
"image": "mesh-runtime-grafana@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/grafana/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/grafana/config.json:/run/config/config.json:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_GRAFANA_URL": "http://127.0.0.1:3000",
|
||||
"MESH_GRAFANA_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// ADR 0044). Both this module's tools and its events entrypoint import it, and nothing outside
|
||||
// home-assistant does. Talks to the HA REST API (/api) with a long-lived access token.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface HAEntityState {
|
||||
entity_id: string;
|
||||
state: string;
|
||||
@@ -18,6 +20,13 @@ export interface HAConfig {
|
||||
state?: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class HomeAssistantClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -34,8 +43,9 @@ export class HomeAssistantClient {
|
||||
* every API call is Bearer-authenticated and there is nowhere to discover it from.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): HomeAssistantClient {
|
||||
const url = env.MESH_HOMEASSISTANT_URL ?? `http://127.0.0.1:${env.HOMEASSISTANT_PORT ?? "8123"}`;
|
||||
const token = env.MESH_HOMEASSISTANT_TOKEN;
|
||||
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;
|
||||
if (!token) throw new Error("no Home Assistant token — set MESH_HOMEASSISTANT_TOKEN");
|
||||
return new HomeAssistantClient(url, token);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"volumes": [
|
||||
"/services/home-assistant/config:/config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-home-assistant",
|
||||
"image": "mesh-runtime-home-assistant@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/home-assistant/broker:/run/secrets/broker: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_CONFIG_FILE": "/run/config/config.json",
|
||||
"MESH_HOMEASSISTANT_CONFIG_DIR": "/var/lib/home-assistant/config"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// endpoint reports the live mountpoints and their listener counts — the one thing worth watching, and
|
||||
// the basis for both the status tool and the stream started/stopped events.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface IcecastMount {
|
||||
/** The mountpoint path, e.g. "/stream.mp3", derived from the source's listen URL. */
|
||||
mount: string;
|
||||
@@ -33,6 +35,13 @@ interface RawSource {
|
||||
server_type?: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class IcecastClient {
|
||||
readonly baseUrl: string;
|
||||
private readonly authHeader?: string;
|
||||
@@ -46,8 +55,9 @@ export class IcecastClient {
|
||||
}
|
||||
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): IcecastClient {
|
||||
const url = env.MESH_ICECAST_URL ?? `http://127.0.0.1:${env.ICECAST_PORT ?? "8000"}`;
|
||||
return new IcecastClient(url, env.MESH_ICECAST_ADMIN_USER, env.MESH_ICECAST_ADMIN_PASSWORD);
|
||||
const cfg = meshConfig(env.MESH_ICECAST_CONFIG_FILE);
|
||||
const url = cfg.url ?? (env.MESH_ICECAST_URL ?? `http://127.0.0.1:${env.ICECAST_PORT ?? "8000"}`);
|
||||
return new IcecastClient(url, cfg.user ?? env.MESH_ICECAST_ADMIN_USER, cfg.password ?? env.MESH_ICECAST_ADMIN_PASSWORD);
|
||||
}
|
||||
|
||||
async getStatus(): Promise<IcecastStatus> {
|
||||
|
||||
@@ -53,6 +53,30 @@
|
||||
"ports": [
|
||||
"8000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/icecast/config.json",
|
||||
"mode": "0600",
|
||||
"content": "{}\n",
|
||||
"merge": "json"
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-icecast",
|
||||
"image": "mesh-runtime-icecast@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/icecast/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/icecast/config.json:/run/config/config.json:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_ICECAST_URL": "http://127.0.0.1:8000",
|
||||
"MESH_ICECAST_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// The InfluxDB API client — influxdb's own code, living in the module (novox/hq ADR 0044). Only
|
||||
// this module's tools import it. Talks to the InfluxDB 2.x HTTP API (/api/v2) with a token.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface InfluxHealth {
|
||||
name?: string;
|
||||
status?: string;
|
||||
@@ -15,6 +17,13 @@ export interface InfluxBucket {
|
||||
retentionSeconds?: number;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class InfluxDBClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -32,10 +41,11 @@ export class InfluxDBClient {
|
||||
* is token-authenticated. The org scopes bucket listing and queries.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): InfluxDBClient {
|
||||
const url = env.MESH_INFLUXDB_URL ?? `http://127.0.0.1:${env.INFLUXDB_PORT ?? "8086"}`;
|
||||
const token = env.MESH_INFLUXDB_TOKEN;
|
||||
const cfg = meshConfig(env.MESH_INFLUXDB_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_INFLUXDB_URL ?? `http://127.0.0.1:${env.INFLUXDB_PORT ?? "8086"}`;
|
||||
const token = cfg.token ?? env.MESH_INFLUXDB_TOKEN;
|
||||
if (!token) throw new Error("no InfluxDB token — set MESH_INFLUXDB_TOKEN");
|
||||
const org = env.MESH_INFLUXDB_ORG ?? "mesh";
|
||||
const org = cfg.org ?? env.MESH_INFLUXDB_ORG ?? "mesh";
|
||||
return new InfluxDBClient(url, token, org);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
],
|
||||
"own-secrets": {
|
||||
"admin": "/var/lib/influxdb-module/admin.secret",
|
||||
"admin-token": "/var/lib/influxdb-module/admin-token.secret"
|
||||
"admin-token": "/var/lib/influxdb-module/admin-token.secret",
|
||||
"broker": "/var/lib/mesh/influxdb/broker"
|
||||
},
|
||||
"listens": [
|
||||
{
|
||||
@@ -17,6 +18,12 @@
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "mesh-state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mesh/influxdb",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
@@ -59,6 +66,24 @@
|
||||
"/services/influxdb/data:/var/lib/influxdb2",
|
||||
"/services/influxdb/config:/etc/influxdb2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-influxdb",
|
||||
"image": "mesh-runtime-influxdb@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/influxdb/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/influxdb/config.json:/run/config/config.json:ro",
|
||||
"/services/influxdb/config:/var/lib/influxdb/config:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_INFLUXDB_URL": "http://127.0.0.1:8086",
|
||||
"MESH_INFLUXDB_CONFIG_FILE": "/run/config/config.json",
|
||||
"MESH_INFLUXDB_CONFIG_DIR": "/var/lib/influxdb/config"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// an indexer proxy: it normalises many torrent trackers behind one Torznab surface. This client
|
||||
// talks its /api/v2.0 REST API, and only jackett's tools import it.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface JackettIndexer {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -22,6 +24,13 @@ export interface JackettResult {
|
||||
link?: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class JackettClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -38,8 +47,9 @@ export class JackettClient {
|
||||
* module contributes no tools rather than failing half-configured.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): JackettClient {
|
||||
const url = env.MESH_JACKETT_URL;
|
||||
const apiKey = env.MESH_JACKETT_API_KEY;
|
||||
const cfg = meshConfig(env.MESH_JACKETT_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_JACKETT_URL;
|
||||
const apiKey = cfg.apiKey ?? env.MESH_JACKETT_API_KEY;
|
||||
if (!url) throw new Error("no Jackett URL — set MESH_JACKETT_URL");
|
||||
if (!apiKey) throw new Error("no Jackett API key — set MESH_JACKETT_API_KEY");
|
||||
return new JackettClient(url, apiKey);
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "mesh-state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mesh/jackett",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "directory",
|
||||
@@ -36,6 +42,27 @@
|
||||
"volumes": [
|
||||
"/services/jackett/config:/config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-jackett",
|
||||
"image": "mesh-runtime-jackett@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/jackett/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/jackett/config.json:/run/config/config.json:ro",
|
||||
"/services/jackett/config:/var/lib/jackett/config:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_JACKETT_URL": "http://127.0.0.1:9117",
|
||||
"MESH_JACKETT_CONFIG_FILE": "/run/config/config.json",
|
||||
"MESH_JACKETT_CONFIG_DIR": "/var/lib/jackett/config"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"own-secrets": {
|
||||
"broker": "/var/lib/mesh/jackett/broker"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
// it rebuilds only keycloak. Both this module's tools and its events entrypoint import it, and
|
||||
// nothing outside keycloak does.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class KeycloakClient {
|
||||
readonly baseUrl: string;
|
||||
readonly defaultRealm: string;
|
||||
@@ -28,11 +37,12 @@ export class KeycloakClient {
|
||||
* here lets the tool runtime expose no keycloak tools rather than tools that always error.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): KeycloakClient {
|
||||
const url = env.MESH_KEYCLOAK_URL ?? `http://127.0.0.1:${env.KEYCLOAK_PORT ?? "8080"}`;
|
||||
const adminUser = env.MESH_KEYCLOAK_ADMIN ?? env.KEYCLOAK_ADMIN ?? "admin";
|
||||
const adminPass = env.MESH_KEYCLOAK_PASSWORD ?? env.KEYCLOAK_ADMIN_PASSWORD;
|
||||
const cfg = meshConfig(env.MESH_KEYCLOAK_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_KEYCLOAK_URL ?? `http://127.0.0.1:${env.KEYCLOAK_PORT ?? "8080"}`;
|
||||
const adminUser = cfg.user ?? env.MESH_KEYCLOAK_ADMIN ?? env.KEYCLOAK_ADMIN ?? "admin";
|
||||
const adminPass = cfg.password ?? env.MESH_KEYCLOAK_PASSWORD ?? env.KEYCLOAK_ADMIN_PASSWORD;
|
||||
if (!adminPass) throw new Error("no Keycloak admin password — set MESH_KEYCLOAK_PASSWORD");
|
||||
const realm = env.MESH_KEYCLOAK_REALM ?? "master";
|
||||
const realm = cfg.realm ?? env.MESH_KEYCLOAK_REALM ?? "master";
|
||||
return new KeycloakClient(url, adminUser, adminPass, realm);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,30 @@
|
||||
"ports": [
|
||||
"8080"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/keycloak/config.json",
|
||||
"mode": "0600",
|
||||
"content": "{}\n",
|
||||
"merge": "json"
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-keycloak",
|
||||
"image": "mesh-runtime-keycloak@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/keycloak/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/keycloak/config.json:/run/config/config.json:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_KEYCLOAK_URL": "http://127.0.0.1:8080",
|
||||
"MESH_KEYCLOAK_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+12
-3
@@ -9,6 +9,7 @@
|
||||
// falls back to `doveadm` inside the imap container, the operation the HTTP surface cannot serve.
|
||||
|
||||
import { execFile } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const run = promisify(execFile);
|
||||
@@ -44,6 +45,13 @@ export interface MailMessage {
|
||||
// The fields we ask doveadm for, once — kept together so read and search stay identical in shape.
|
||||
const FETCH_FIELDS = "date.received hdr.subject hdr.from body.snippet";
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class MailuClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -62,12 +70,13 @@ export class MailuClient {
|
||||
* client with neither would only fail later, one call at a time, so it fails here instead.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): MailuClient {
|
||||
const url = env.MESH_MAILU_URL;
|
||||
const apiKey = env.MESH_MAILU_API_KEY;
|
||||
const cfg = meshConfig(env.MESH_MAILU_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_MAILU_URL;
|
||||
const apiKey = cfg.apiKey ?? env.MESH_MAILU_API_KEY;
|
||||
if (!url || !apiKey) {
|
||||
throw new Error("Mailu is not configured — set MESH_MAILU_URL and MESH_MAILU_API_KEY");
|
||||
}
|
||||
const imapContainer = env.MESH_MAILU_IMAP_CONTAINER ?? "mailu-imap";
|
||||
const imapContainer = cfg.container ?? env.MESH_MAILU_IMAP_CONTAINER ?? "mailu-imap";
|
||||
return new MailuClient(url, apiKey, imapContainer);
|
||||
}
|
||||
|
||||
|
||||
@@ -282,6 +282,31 @@
|
||||
"/services/mailu/data/certs:/certs",
|
||||
"/services/mailu/data/overrides/nginx:/overrides:ro"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/mailu/config.json",
|
||||
"mode": "0600",
|
||||
"content": "{}\n",
|
||||
"merge": "json"
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-mailu",
|
||||
"image": "mesh-runtime-mailu@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/mailu/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/mailu/config.json:/run/config/config.json:ro",
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_MAILU_URL": "http://127.0.0.1:80",
|
||||
"MESH_MAILU_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// because occ has no version-stable "list every share" across the releases we run.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface NextcloudUser {
|
||||
uid: string;
|
||||
@@ -24,6 +25,13 @@ export interface NextcloudShare {
|
||||
owner: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class NextcloudClient {
|
||||
constructor(
|
||||
private readonly container: string,
|
||||
@@ -40,10 +48,11 @@ export class NextcloudClient {
|
||||
* throws without it, and the module then contributes nothing rather than failing.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): NextcloudClient {
|
||||
const container = env.MESH_NEXTCLOUD_CONTAINER ?? "nextcloud";
|
||||
const ocsUrl = env.MESH_NEXTCLOUD_URL ?? `http://127.0.0.1:${env.NEXTCLOUD_PORT ?? "80"}`;
|
||||
const adminUser = env.MESH_NEXTCLOUD_ADMIN_USER ?? "admin";
|
||||
const adminPassword = env.MESH_NEXTCLOUD_ADMIN_PASSWORD;
|
||||
const cfg = meshConfig(env.MESH_NEXTCLOUD_CONFIG_FILE);
|
||||
const container = cfg.container ?? env.MESH_NEXTCLOUD_CONTAINER ?? "nextcloud";
|
||||
const ocsUrl = cfg.url ?? env.MESH_NEXTCLOUD_URL ?? `http://127.0.0.1:${env.NEXTCLOUD_PORT ?? "80"}`;
|
||||
const adminUser = cfg.user ?? env.MESH_NEXTCLOUD_ADMIN_USER ?? "admin";
|
||||
const adminPassword = cfg.password ?? env.MESH_NEXTCLOUD_ADMIN_PASSWORD;
|
||||
if (!adminPassword) throw new Error("no Nextcloud admin password — set MESH_NEXTCLOUD_ADMIN_PASSWORD");
|
||||
return new NextcloudClient(container, ocsUrl.replace(/\/$/, ""), adminUser, adminPassword);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,31 @@
|
||||
"volumes": [
|
||||
"/services/nextcloud/html:/var/www/html"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/nextcloud/config.json",
|
||||
"mode": "0600",
|
||||
"content": "{}\n",
|
||||
"merge": "json"
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-nextcloud",
|
||||
"image": "mesh-runtime-nextcloud@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/nextcloud/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/nextcloud/config.json:/run/config/config.json:ro",
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_NEXTCLOUD_URL": "http://127.0.0.1:80",
|
||||
"MESH_NEXTCLOUD_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// configuration, GET /nodes for installed node modules. A default install has no auth; when
|
||||
// adminAuth is on, a bearer token (minted at /auth/token) is required.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface NodeRedFlow {
|
||||
/** The tab (flow) node id. */
|
||||
id: string;
|
||||
@@ -18,6 +20,13 @@ export interface NodeRedNodeModule {
|
||||
types: string[];
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class NodeRedClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -35,9 +44,10 @@ export class NodeRedClient {
|
||||
* a default install needs none.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): NodeRedClient {
|
||||
const url = env.MESH_NODERED_URL;
|
||||
const cfg = meshConfig(env.MESH_NODERED_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_NODERED_URL;
|
||||
if (!url) throw new Error("no Node-RED URL — set MESH_NODERED_URL");
|
||||
return new NodeRedClient(url, env.MESH_NODERED_TOKEN);
|
||||
return new NodeRedClient(url, cfg.token ?? env.MESH_NODERED_TOKEN);
|
||||
}
|
||||
|
||||
private headers(extra: Record<string, string> = {}): Record<string, string> {
|
||||
|
||||
@@ -46,6 +46,30 @@
|
||||
"volumes": [
|
||||
"/services/nodered/data:/data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/nodered/config.json",
|
||||
"mode": "0600",
|
||||
"content": "{}\n",
|
||||
"merge": "json"
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-nodered",
|
||||
"image": "mesh-runtime-nodered@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/nodered/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/nodered/config.json:/run/config/config.json:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_NODERED_URL": "http://127.0.0.1:1880",
|
||||
"MESH_NODERED_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// nzbget and nothing else. Both this module's tools and its events entrypoint import it, and
|
||||
// nothing outside nzbget does. NZBGet speaks JSON-RPC at /jsonrpc, behind HTTP Basic auth.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface NzbgetStatus {
|
||||
/** Bytes/sec — NZBGet reports it split across two 32-bit halves, rejoined here. */
|
||||
speedBytesPerSec: number;
|
||||
@@ -39,6 +41,13 @@ export interface NzbgetHistoryItem {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class NzbgetClient {
|
||||
readonly rpcUrl: string;
|
||||
private readonly auth: string;
|
||||
@@ -55,12 +64,13 @@ export class NzbgetClient {
|
||||
* as "exposes nothing"). The control username defaults to "nzbget", NZBGet's own default.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): NzbgetClient {
|
||||
const url = env.MESH_NZBGET_URL;
|
||||
const password = env.MESH_NZBGET_PASSWORD;
|
||||
const cfg = meshConfig(env.MESH_NZBGET_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_NZBGET_URL;
|
||||
const password = cfg.password ?? env.MESH_NZBGET_PASSWORD;
|
||||
if (!url || !password) {
|
||||
throw new Error("NZBGet not configured — set MESH_NZBGET_URL and MESH_NZBGET_PASSWORD");
|
||||
}
|
||||
const user = env.MESH_NZBGET_USER ?? "nzbget";
|
||||
const user = cfg.user ?? env.MESH_NZBGET_USER ?? "nzbget";
|
||||
return new NzbgetClient(url, user, password);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,24 @@
|
||||
"/services/nzbget/config:/config",
|
||||
"/services/media/downloads:/downloads"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-nzbget",
|
||||
"image": "mesh-runtime-nzbget@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/nzbget/broker:/run/secrets/broker: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_CONFIG_FILE": "/run/config/config.json",
|
||||
"MESH_NZBGET_CONFIG_DIR": "/var/lib/nzbget/config"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+12
-2
@@ -2,6 +2,8 @@
|
||||
// request front-end: viewers ask for movies and shows, and an operator approves them. This client
|
||||
// talks its /api/v1 REST API (keyed by an ApiKey header); ombi's tools and events import it.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface OmbiRequest {
|
||||
kind: "movie" | "tv";
|
||||
id: number;
|
||||
@@ -20,6 +22,13 @@ export interface RequestCounts {
|
||||
available: number;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class OmbiClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -33,8 +42,9 @@ export class OmbiClient {
|
||||
/** Build from the module's resolved environment. Ombi's API is keyed; without URL and key there
|
||||
* is nothing to talk to, so this throws rather than run half-configured. */
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): OmbiClient {
|
||||
const url = env.MESH_OMBI_URL;
|
||||
const apiKey = env.MESH_OMBI_API_KEY;
|
||||
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;
|
||||
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);
|
||||
|
||||
@@ -49,6 +49,24 @@
|
||||
"volumes": [
|
||||
"/services/ombi/config:/config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-ombi",
|
||||
"image": "mesh-runtime-ombi@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/ombi/broker:/run/secrets/broker: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_CONFIG_FILE": "/run/config/config.json",
|
||||
"MESH_OMBI_CONFIG_DIR": "/var/lib/ombi/config"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// by an API key sent as the `x-api-key` header). The client speaks only what the tools and the
|
||||
// item-added event need: server version and statistics, albums, and recent assets.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface PhotosServerInfo {
|
||||
version: string;
|
||||
photos?: number;
|
||||
@@ -24,6 +26,13 @@ export interface PhotosAsset {
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class PhotosClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -38,8 +47,9 @@ export class PhotosClient {
|
||||
* the API key is required, and without it the module contributes nothing rather than reaching an
|
||||
* unauthenticated endpoint. */
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): PhotosClient {
|
||||
const url = env.MESH_PHOTOS_URL ?? `http://127.0.0.1:${env.PHOTOS_PORT ?? "2283"}`;
|
||||
const key = env.MESH_PHOTOS_API_KEY;
|
||||
const cfg = meshConfig(env.MESH_PHOTOS_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_PHOTOS_URL ?? `http://127.0.0.1:${env.PHOTOS_PORT ?? "2283"}`;
|
||||
const key = cfg.apiKey ?? env.MESH_PHOTOS_API_KEY;
|
||||
if (!key) throw new Error("no photos API key — set MESH_PHOTOS_API_KEY");
|
||||
return new PhotosClient(url, key);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,25 @@
|
||||
"/etc/photos/store.json:/etc/photos/store.json:ro",
|
||||
"/etc/photos/store.secret:/etc/photos/store.secret:ro"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-photos",
|
||||
"image": "mesh-runtime-photos@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/photos/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/photos/config.json:/run/config/config.json:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_PHOTOS_URL": "http://127.0.0.1:2283",
|
||||
"MESH_PHOTOS_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
],
|
||||
"capabilities": [
|
||||
"container-runtime"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// which the host owns and emits — so this module reads Portainer's own resources (endpoints,
|
||||
// stacks, containers) and exposes them, and stops there.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface PortainerEndpoint {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -27,6 +29,13 @@ export interface PortainerContainer {
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class PortainerClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -44,8 +53,9 @@ export class PortainerClient {
|
||||
* exposes nothing rather than calling Portainer unauthenticated.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): PortainerClient {
|
||||
const url = env.MESH_PORTAINER_URL ?? `https://127.0.0.1:${env.PORTAINER_PORT ?? "9443"}`;
|
||||
const token = env.MESH_PORTAINER_TOKEN;
|
||||
const cfg = meshConfig(env.MESH_PORTAINER_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_PORTAINER_URL ?? `https://127.0.0.1:${env.PORTAINER_PORT ?? "9443"}`;
|
||||
const token = cfg.token ?? env.MESH_PORTAINER_TOKEN;
|
||||
if (!token) throw new Error("no Portainer token — set MESH_PORTAINER_TOKEN");
|
||||
return new PortainerClient(url, token);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "mesh-state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mesh/portainer",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data",
|
||||
"type": "directory",
|
||||
@@ -31,6 +37,33 @@
|
||||
"/services/portainer/data:/data",
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/portainer/config.json",
|
||||
"mode": "0600",
|
||||
"content": "{}\n",
|
||||
"merge": "json"
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-portainer",
|
||||
"image": "mesh-runtime-portainer@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/portainer/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/portainer/config.json:/run/config/config.json:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_PORTAINER_URL": "https://127.0.0.1:9443",
|
||||
"MESH_PORTAINER_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"own-secrets": {
|
||||
"broker": "/var/lib/mesh/portainer/broker"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// against CSRF by checking the Referer header. Node's fetch keeps no cookie jar, so the SID is
|
||||
// captured on login and carried by hand on every later call, with a single re-login on expiry.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface QbTransferInfo {
|
||||
dlSpeedBytesPerSec: number;
|
||||
upSpeedBytesPerSec: number;
|
||||
@@ -30,6 +32,13 @@ export interface QbTorrent {
|
||||
savePath: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class QbittorrentClient {
|
||||
readonly baseUrl: string;
|
||||
private sid: string | null = null;
|
||||
@@ -49,12 +58,13 @@ export class QbittorrentClient {
|
||||
* (the harness treats the throw as "exposes nothing"). The user defaults to "admin".
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): QbittorrentClient {
|
||||
const url = env.MESH_QBITTORRENT_URL;
|
||||
const password = env.MESH_QBITTORRENT_PASSWORD;
|
||||
const cfg = meshConfig(env.MESH_QBITTORRENT_CONFIG_FILE);
|
||||
const url = cfg.url ?? env.MESH_QBITTORRENT_URL;
|
||||
const password = cfg.password ?? env.MESH_QBITTORRENT_PASSWORD;
|
||||
if (!url || !password) {
|
||||
throw new Error("qBittorrent not configured — set MESH_QBITTORRENT_URL and MESH_QBITTORRENT_PASSWORD");
|
||||
}
|
||||
const user = env.MESH_QBITTORRENT_USER ?? "admin";
|
||||
const user = cfg.user ?? env.MESH_QBITTORRENT_USER ?? "admin";
|
||||
return new QbittorrentClient(url, user, password);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,24 @@
|
||||
"/services/qbittorrent/config:/config",
|
||||
"/services/media/downloads:/downloads"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-qbittorrent",
|
||||
"image": "mesh-runtime-qbittorrent@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/qbittorrent/broker:/run/secrets/broker: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_CONFIG_FILE": "/run/config/config.json",
|
||||
"MESH_QBITTORRENT_CONFIG_DIR": "/var/lib/qbittorrent/config"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// merged results. Its JSON API (`/search?q=...&format=json`) is what makes a `searxng_search` tool
|
||||
// useful; the client speaks only that. No credential — the instance is reached inside the mesh.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface SearxResult {
|
||||
title: string;
|
||||
url: string;
|
||||
@@ -26,6 +28,13 @@ export interface SearxOptions {
|
||||
pageno?: number;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class SearxngClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -36,7 +45,8 @@ export class SearxngClient {
|
||||
/** Build from the module's environment. No key: SearXNG's search API is open on the mesh, so a URL
|
||||
* is all it takes — defaulting to the container's own listen port. */
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): SearxngClient {
|
||||
const url = env.MESH_SEARXNG_URL ?? `http://127.0.0.1:${env.SEARXNG_PORT ?? "8080"}`;
|
||||
const cfg = meshConfig(env.MESH_SEARXNG_CONFIG_FILE);
|
||||
const url = cfg.url ?? (env.MESH_SEARXNG_URL ?? `http://127.0.0.1:${env.SEARXNG_PORT ?? "8080"}`);
|
||||
return new SearxngClient(url);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"container-runtime"
|
||||
],
|
||||
"own-secrets": {
|
||||
"secret": "/var/lib/searxng-module/secret.secret"
|
||||
"secret": "/var/lib/searxng-module/secret.secret",
|
||||
"broker": "/var/lib/mesh/searxng/broker"
|
||||
},
|
||||
"listens": [
|
||||
{
|
||||
@@ -16,6 +17,12 @@
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "mesh-state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mesh/searxng",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
@@ -64,6 +71,30 @@
|
||||
"ports": [
|
||||
"8080"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/searxng/config.json",
|
||||
"mode": "0600",
|
||||
"content": "{}\n",
|
||||
"merge": "json"
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-searxng",
|
||||
"image": "mesh-runtime-searxng@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/searxng/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/searxng/config.json:/run/config/config.json:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_SEARXNG_URL": "http://127.0.0.1:8080",
|
||||
"MESH_SEARXNG_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// { response: { result: "success" | "error", message, data } }. This client unwraps that envelope
|
||||
// and hands back only the data.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface TautulliSession {
|
||||
user: string;
|
||||
title: string;
|
||||
@@ -32,6 +34,13 @@ export interface TautulliHomeStat {
|
||||
rows: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class TautulliClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -48,8 +57,9 @@ export class TautulliClient {
|
||||
* Throws when no key is configured — the module then contributes nothing rather than failing.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): TautulliClient {
|
||||
const url = env.MESH_TAUTULLI_URL ?? `http://127.0.0.1:${env.TAUTULLI_PORT ?? "8181"}`;
|
||||
const apiKey = env.MESH_TAUTULLI_APIKEY;
|
||||
const cfg = meshConfig(env.MESH_TAUTULLI_CONFIG_FILE);
|
||||
const url = cfg.url ?? (env.MESH_TAUTULLI_URL ?? `http://127.0.0.1:${env.TAUTULLI_PORT ?? "8181"}`);
|
||||
const apiKey = cfg.apiKey ?? env.MESH_TAUTULLI_APIKEY;
|
||||
if (!apiKey) throw new Error("no Tautulli API key — set MESH_TAUTULLI_APIKEY");
|
||||
return new TautulliClient(url, apiKey);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,24 @@
|
||||
"volumes": [
|
||||
"/services/tautulli/config:/config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-tautulli",
|
||||
"image": "mesh-runtime-tautulli@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/tautulli/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/tautulli/config.json:/run/config/config.json:ro",
|
||||
"/services/tautulli/config:/var/lib/tautulli/config:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_TAUTULLI_URL": "http://127.0.0.1:8181",
|
||||
"MESH_TAUTULLI_CONFIG_FILE": "/run/config/config.json",
|
||||
"MESH_TAUTULLI_CONFIG_DIR": "/var/lib/tautulli/config"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// ADR 0044). Both this module's tools and its events entrypoint import it, and nothing outside
|
||||
// verdaccio does.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface VerdaccioPackage {
|
||||
name: string;
|
||||
version?: string;
|
||||
@@ -17,6 +19,13 @@ export interface PackageInfo {
|
||||
modified?: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class VerdaccioClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -34,9 +43,10 @@ export class VerdaccioClient {
|
||||
* port); an optional MESH_VERDACCIO_TOKEN authenticates. Throws when no URL is configured.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): VerdaccioClient {
|
||||
const url = env.MESH_VERDACCIO_URL ?? `http://127.0.0.1:${env.VERDACCIO_PORT ?? "4873"}`;
|
||||
const cfg = meshConfig(env.MESH_VERDACCIO_CONFIG_FILE);
|
||||
const url = cfg.url ?? (env.MESH_VERDACCIO_URL ?? `http://127.0.0.1:${env.VERDACCIO_PORT ?? "4873"}`);
|
||||
if (!url) throw new Error("no verdaccio URL — set MESH_VERDACCIO_URL");
|
||||
return new VerdaccioClient(url, env.MESH_VERDACCIO_TOKEN);
|
||||
return new VerdaccioClient(url, cfg.token ?? env.MESH_VERDACCIO_TOKEN);
|
||||
}
|
||||
|
||||
private async getJson<T>(path: string): Promise<T> {
|
||||
|
||||
@@ -58,6 +58,22 @@
|
||||
"/services/verdaccio/storage:/verdaccio/storage",
|
||||
"/services/verdaccio/conf:/verdaccio/conf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-verdaccio",
|
||||
"image": "mesh-runtime-verdaccio@sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/verdaccio/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/verdaccio/config.json:/run/config/config.json:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_VERDACCIO_URL": "http://127.0.0.1:4873",
|
||||
"MESH_VERDACCIO_CONFIG_FILE": "/run/config/config.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user