a2a-exposed 0.0.0-stage → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/config.mjs ADDED
@@ -0,0 +1,95 @@
1
+ // Config storage: ~/.config/a2a-exposed/config.env (chmod 600) + peers.json.
2
+ // Environment variables always override values from config.env.
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ /** <base>/a2a-exposed (base: $XDG_CONFIG_HOME or ~/.config). */
8
+ export function defaultConfigDir(base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")) {
9
+ return path.join(base, "a2a-exposed");
10
+ }
11
+
12
+ export const CONFIG_DIR = process.env.A2A_CONFIG_DIR || defaultConfigDir();
13
+ export const CONFIG_FILE = path.join(CONFIG_DIR, "config.env");
14
+ export const PEERS_FILE = path.join(CONFIG_DIR, "peers.json");
15
+
16
+ export function parseEnv(text) {
17
+ const out = {};
18
+ for (const raw of text.split(/\r?\n/)) {
19
+ const line = raw.trim();
20
+ if (!line || line.startsWith("#")) continue;
21
+ const m = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
22
+ if (!m) continue;
23
+ let v = m[2];
24
+ if (v.startsWith('"')) {
25
+ try { v = JSON.parse(v); } catch { v = v.slice(1, -1); }
26
+ } else if (v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1);
27
+ out[m[1]] = v;
28
+ }
29
+ return out;
30
+ }
31
+
32
+ export function serializeEnv(obj) {
33
+ const lines = ["# a2a-exposed config (contains secrets: keep chmod 600, never commit)"];
34
+ for (const [k, v] of Object.entries(obj)) {
35
+ if (v === undefined || v === null) continue;
36
+ const s = String(v);
37
+ lines.push(`${k}=${/^[A-Za-z0-9_./:@+,-]*$/.test(s) ? s : JSON.stringify(s)}`);
38
+ }
39
+ return lines.join("\n") + "\n";
40
+ }
41
+
42
+ let cache = null;
43
+ export function fileConfig() {
44
+ if (cache) return cache;
45
+ try { cache = parseEnv(fs.readFileSync(CONFIG_FILE, "utf8")); } catch { cache = {}; }
46
+ return cache;
47
+ }
48
+
49
+ /** Value from the environment, else config.env, else default. */
50
+ export function get(key, dflt = "") {
51
+ const e = process.env[key];
52
+ if (e !== undefined && e !== "") return e;
53
+ const f = fileConfig()[key];
54
+ return f !== undefined && f !== "" ? f : dflt;
55
+ }
56
+
57
+ function ensureDir() {
58
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
59
+ }
60
+
61
+ function writePrivate(file, data) {
62
+ ensureDir();
63
+ const tmp = `${file}.${process.pid}.tmp`;
64
+ fs.writeFileSync(tmp, data, { mode: 0o600 });
65
+ fs.chmodSync(tmp, 0o600);
66
+ fs.renameSync(tmp, file);
67
+ }
68
+
69
+ /** Merge updates into config.env (undefined values are left alone, null removes the key). */
70
+ export function saveConfig(updates) {
71
+ const cur = { ...fileConfig() };
72
+ for (const [k, v] of Object.entries(updates)) {
73
+ if (v === undefined) continue;
74
+ if (v === null) delete cur[k];
75
+ else cur[k] = String(v);
76
+ }
77
+ writePrivate(CONFIG_FILE, serializeEnv(cur));
78
+ cache = cur;
79
+ }
80
+
81
+ export function loadPeers() {
82
+ try { return JSON.parse(fs.readFileSync(PEERS_FILE, "utf8")) || {}; } catch { return {}; }
83
+ }
84
+
85
+ export function savePeers(peers) {
86
+ writePrivate(PEERS_FILE, JSON.stringify(peers, null, 2) + "\n");
87
+ }
88
+
89
+ /** Set (non-empty) both in the environment and in config.env, with different values: get() returns the environment's. */
90
+ export function envOverridesFile(key) {
91
+ const e = process.env[key], f = fileConfig()[key];
92
+ return !!e && !!f && e !== f;
93
+ }
94
+
95
+ export const peerTokenVar = (alias) => `PEER_${alias.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_TOKEN`;