@bitmacro/relay-agent 0.1.4 → 0.2.0-beta.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.
@@ -1488,8 +1488,8 @@ var Node = class _Node {
1488
1488
  this.#index = index;
1489
1489
  return;
1490
1490
  }
1491
- const [token2, ...restTokens] = tokens;
1492
- const pattern = token2 === "*" ? restTokens.length === 0 ? ["", "", ONLY_WILDCARD_REG_EXP_STR] : ["", "", LABEL_REG_EXP_STR] : token2 === "/*" ? ["", "", TAIL_WILDCARD_REG_EXP_STR] : token2.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
1491
+ const [token, ...restTokens] = tokens;
1492
+ const pattern = token === "*" ? restTokens.length === 0 ? ["", "", ONLY_WILDCARD_REG_EXP_STR] : ["", "", LABEL_REG_EXP_STR] : token === "/*" ? ["", "", TAIL_WILDCARD_REG_EXP_STR] : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
1493
1493
  let node;
1494
1494
  if (pattern) {
1495
1495
  const name = pattern[1];
@@ -1522,7 +1522,7 @@ var Node = class _Node {
1522
1522
  paramMap.push([name, node.#varIndex]);
1523
1523
  }
1524
1524
  } else {
1525
- node = this.#children[token2];
1525
+ node = this.#children[token];
1526
1526
  if (!node) {
1527
1527
  if (Object.keys(this.#children).some(
1528
1528
  (k) => k.length > 1 && k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR
@@ -1532,7 +1532,7 @@ var Node = class _Node {
1532
1532
  if (pathErrorCheckOnly) {
1533
1533
  return;
1534
1534
  }
1535
- node = this.#children[token2] = new _Node();
1535
+ node = this.#children[token] = new _Node();
1536
1536
  }
1537
1537
  }
1538
1538
  node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
@@ -2714,20 +2714,81 @@ var serve = (options, listeningListener) => {
2714
2714
  return server;
2715
2715
  };
2716
2716
 
2717
+ // src/config/relay-instances.ts
2718
+ var cachedInstances = null;
2719
+ function parseRelayInstances() {
2720
+ const raw2 = process.env.RELAY_INSTANCES;
2721
+ if (!raw2 || typeof raw2 !== "string") return null;
2722
+ try {
2723
+ const arr = JSON.parse(raw2);
2724
+ if (!Array.isArray(arr)) return null;
2725
+ const instances = [];
2726
+ const defaultWhitelist = process.env.WHITELIST_PATH ?? "/etc/strfry/whitelist.txt";
2727
+ for (const item of arr) {
2728
+ if (!item || typeof item !== "object") continue;
2729
+ const obj = item;
2730
+ const id = typeof obj.id === "string" ? obj.id.trim() : "";
2731
+ const token = typeof obj.token === "string" ? obj.token.trim() : "";
2732
+ const strfryConfig = typeof obj.strfryConfig === "string" ? obj.strfryConfig.trim() : "";
2733
+ const strfryDb = typeof obj.strfryDb === "string" ? obj.strfryDb.trim() : "";
2734
+ if (!id || !token || !strfryConfig || !strfryDb) continue;
2735
+ if (instances.some((i) => i.id === id)) continue;
2736
+ instances.push({
2737
+ id,
2738
+ token,
2739
+ strfryConfig,
2740
+ strfryDb,
2741
+ whitelistPath: typeof obj.whitelistPath === "string" ? obj.whitelistPath.trim() || void 0 : void 0
2742
+ });
2743
+ }
2744
+ return instances.length > 0 ? instances : null;
2745
+ } catch {
2746
+ return null;
2747
+ }
2748
+ }
2749
+ function getRelayInstances() {
2750
+ if (cachedInstances === null) {
2751
+ cachedInstances = parseRelayInstances();
2752
+ }
2753
+ return cachedInstances;
2754
+ }
2755
+ function getRelayInstance(relayId) {
2756
+ const instances = getRelayInstances();
2757
+ if (!instances) return null;
2758
+ return instances.find((i) => i.id === relayId) ?? null;
2759
+ }
2760
+ function isMultiRelayMode() {
2761
+ return getRelayInstances() !== null;
2762
+ }
2763
+
2717
2764
  // src/middleware/auth.ts
2718
2765
  var UNAUTHORIZED_JSON = { error: "unauthorized" };
2719
- async function authMiddleware(c, next) {
2766
+ function getBearerToken(c) {
2720
2767
  const authHeader = c.req.header("Authorization");
2721
- const expectedToken = process.env.RELAY_AGENT_TOKEN;
2722
- if (!expectedToken) {
2723
- return c.json(UNAUTHORIZED_JSON, 401);
2724
- }
2725
- if (!authHeader?.startsWith("Bearer ")) {
2726
- return c.json(UNAUTHORIZED_JSON, 401);
2727
- }
2728
- const token2 = authHeader.slice(7);
2729
- if (token2 !== expectedToken) {
2730
- return c.json(UNAUTHORIZED_JSON, 401);
2768
+ if (!authHeader?.startsWith("Bearer ")) return null;
2769
+ return authHeader.slice(7);
2770
+ }
2771
+ async function authMiddleware(c, next) {
2772
+ const path = c.req.path;
2773
+ if (path === "/health") return next();
2774
+ if (isMultiRelayMode()) {
2775
+ const segments = path.split("/").filter(Boolean);
2776
+ if (segments.length >= 2 && segments[1] === "health") return next();
2777
+ }
2778
+ const token = getBearerToken(c);
2779
+ if (!token) return c.json(UNAUTHORIZED_JSON, 401);
2780
+ if (isMultiRelayMode()) {
2781
+ const segments = path.split("/").filter(Boolean);
2782
+ const relayId = segments[0];
2783
+ if (!relayId) return c.json(UNAUTHORIZED_JSON, 401);
2784
+ const instance = getRelayInstance(relayId);
2785
+ if (!instance) return c.json(UNAUTHORIZED_JSON, 401);
2786
+ if (token !== instance.token) return c.json(UNAUTHORIZED_JSON, 401);
2787
+ c.set("relayInstance", instance);
2788
+ } else {
2789
+ const expectedToken = process.env.RELAY_AGENT_TOKEN;
2790
+ if (!expectedToken) return c.json(UNAUTHORIZED_JSON, 401);
2791
+ if (token !== expectedToken) return c.json(UNAUTHORIZED_JSON, 401);
2731
2792
  }
2732
2793
  await next();
2733
2794
  }
@@ -2735,8 +2796,25 @@ async function authMiddleware(c, next) {
2735
2796
  // src/routes/health.ts
2736
2797
  var healthRoutes = new Hono2();
2737
2798
  healthRoutes.get("/health", (c) => {
2799
+ if (isMultiRelayMode()) {
2800
+ const instances = getRelayInstances() ?? [];
2801
+ return c.json({
2802
+ status: "ok",
2803
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2804
+ relayIds: instances.map((i) => i.id)
2805
+ });
2806
+ }
2738
2807
  return c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
2739
2808
  });
2809
+ var healthMultiRoutes = new Hono2();
2810
+ healthMultiRoutes.get("/:relayId/health", (c) => {
2811
+ const relayId = c.req.param("relayId");
2812
+ const instances = getRelayInstances();
2813
+ if (!instances?.some((i) => i.id === relayId)) {
2814
+ return c.json({ error: "relay not found", relayId }, 404);
2815
+ }
2816
+ return c.json({ status: "ok", relayId, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
2817
+ });
2740
2818
 
2741
2819
  // src/adapters/strfry.ts
2742
2820
  import { spawn } from "child_process";
@@ -2822,8 +2900,22 @@ ${stderr}`), {
2822
2900
  });
2823
2901
  }
2824
2902
  var STRFRY_BIN = process.env.STRFRY_BIN ?? "strfry";
2825
- var STRFRY_CONFIG = process.env.STRFRY_CONFIG;
2826
- var WHITELIST_PATH = process.env.WHITELIST_PATH ?? "/etc/strfry/whitelist.txt";
2903
+ var DEFAULT_STRFRY_CONFIG = process.env.STRFRY_CONFIG;
2904
+ var DEFAULT_WHITELIST_PATH = process.env.WHITELIST_PATH ?? "/etc/strfry/whitelist.txt";
2905
+ function resolveConfig(cfg) {
2906
+ if (cfg) {
2907
+ return {
2908
+ strfryConfig: cfg.strfryConfig || void 0,
2909
+ strfryDb: cfg.strfryDb,
2910
+ whitelistPath: cfg.whitelistPath ?? DEFAULT_WHITELIST_PATH
2911
+ };
2912
+ }
2913
+ return {
2914
+ strfryConfig: DEFAULT_STRFRY_CONFIG,
2915
+ strfryDb: process.env.STRFRY_DB_PATH ?? "./strfry-db",
2916
+ whitelistPath: DEFAULT_WHITELIST_PATH
2917
+ };
2918
+ }
2827
2919
  function logStrfryError(operation, err) {
2828
2920
  const msg = err instanceof Error ? err.message : String(err);
2829
2921
  const extra = err && typeof err === "object" ? err : {};
@@ -2836,15 +2928,11 @@ function logStrfryError(operation, err) {
2836
2928
  if (stderr) parts.push(`stderr: ${stderr}`);
2837
2929
  console.error(parts.join("\n"));
2838
2930
  }
2839
- function getStrfryDbPath() {
2840
- return process.env.STRFRY_DB_PATH ?? "./strfry-db";
2841
- }
2842
- function strfryArgs(subcommand, ...args) {
2843
- const base = STRFRY_CONFIG ? ["--config", STRFRY_CONFIG, subcommand] : [subcommand];
2931
+ function strfryArgs(cfg, subcommand, ...args) {
2932
+ const base = cfg.strfryConfig ? ["--config", cfg.strfryConfig, subcommand] : [subcommand];
2844
2933
  return [...base, ...args];
2845
2934
  }
2846
- function getStrfryCwd() {
2847
- const dbPath = getStrfryDbPath();
2935
+ function getStrfryCwd(dbPath) {
2848
2936
  if (!dbPath) return void 0;
2849
2937
  const parent = dirname(dbPath);
2850
2938
  return parent !== "." ? parent : void 0;
@@ -2859,57 +2947,75 @@ function buildFilterJson(filter) {
2859
2947
  if (filter.limit != null) obj.limit = filter.limit;
2860
2948
  return JSON.stringify(obj);
2861
2949
  }
2862
- async function scanEvents(filter) {
2863
- try {
2864
- const filterJson = buildFilterJson(filter);
2865
- const cwd = getStrfryCwd();
2866
- const { stdout } = await spawnAsync(STRFRY_BIN, strfryArgs("scan", filterJson), {
2867
- maxBuffer: 50 * 1024 * 1024,
2868
- cwd: cwd || void 0
2869
- });
2870
- const events = [];
2871
- for (const line of stdout.trim().split("\n")) {
2872
- if (!line) continue;
2873
- try {
2874
- const event = JSON.parse(line);
2875
- events.push(event);
2876
- } catch {
2950
+ var strfryLocks = /* @__PURE__ */ new Map();
2951
+ async function withStrfryMutex(strfryDb, fn) {
2952
+ const prev = strfryLocks.get(strfryDb) ?? Promise.resolve();
2953
+ const work = prev.then(() => fn());
2954
+ strfryLocks.set(strfryDb, work.finally(() => {
2955
+ }));
2956
+ return work;
2957
+ }
2958
+ async function scanEvents(filter, cfg = null) {
2959
+ const resolved = resolveConfig(cfg);
2960
+ return withStrfryMutex(resolved.strfryDb, async () => {
2961
+ try {
2962
+ const filterJson = buildFilterJson(filter);
2963
+ const cwd = getStrfryCwd(resolved.strfryDb);
2964
+ const { stdout } = await spawnAsync(STRFRY_BIN, strfryArgs(resolved, "scan", filterJson), {
2965
+ maxBuffer: 50 * 1024 * 1024,
2966
+ cwd: cwd || void 0
2967
+ });
2968
+ const events = [];
2969
+ for (const line of stdout.trim().split("\n")) {
2970
+ if (!line) continue;
2971
+ try {
2972
+ const event = JSON.parse(line);
2973
+ events.push(event);
2974
+ } catch {
2975
+ }
2877
2976
  }
2977
+ return events;
2978
+ } catch (err) {
2979
+ logStrfryError("scanEvents", err);
2980
+ throw err;
2878
2981
  }
2879
- return events;
2880
- } catch (err) {
2881
- logStrfryError("scanEvents", err);
2882
- throw err;
2883
- }
2982
+ });
2884
2983
  }
2885
- async function deleteEvent(id) {
2886
- try {
2887
- const filterJson = JSON.stringify({ ids: [id] });
2888
- const cwd = getStrfryCwd();
2889
- await spawnAsync(STRFRY_BIN, strfryArgs("delete", "--filter", filterJson), {
2890
- cwd: cwd || void 0
2891
- });
2892
- } catch (err) {
2893
- logStrfryError("deleteEvent", err);
2894
- throw err;
2895
- }
2984
+ async function deleteEvent(id, cfg = null) {
2985
+ const resolved = resolveConfig(cfg);
2986
+ return withStrfryMutex(resolved.strfryDb, async () => {
2987
+ try {
2988
+ const filterJson = JSON.stringify({ ids: [id] });
2989
+ const cwd = getStrfryCwd(resolved.strfryDb);
2990
+ await spawnAsync(STRFRY_BIN, strfryArgs(resolved, "delete", "--filter", filterJson), {
2991
+ cwd: cwd || void 0
2992
+ });
2993
+ } catch (err) {
2994
+ logStrfryError("deleteEvent", err);
2995
+ throw err;
2996
+ }
2997
+ });
2896
2998
  }
2897
- async function deleteByPubkey(pubkey) {
2898
- try {
2899
- const filterJson = JSON.stringify({ authors: [pubkey] });
2900
- const cwd = getStrfryCwd();
2901
- await spawnAsync(STRFRY_BIN, strfryArgs("delete", "--filter", filterJson), {
2902
- cwd: cwd || void 0
2903
- });
2904
- } catch (err) {
2905
- logStrfryError("deleteByPubkey", err);
2906
- throw err;
2907
- }
2999
+ async function deleteByPubkey(pubkey, cfg = null) {
3000
+ const resolved = resolveConfig(cfg);
3001
+ return withStrfryMutex(resolved.strfryDb, async () => {
3002
+ try {
3003
+ const filterJson = JSON.stringify({ authors: [pubkey] });
3004
+ const cwd = getStrfryCwd(resolved.strfryDb);
3005
+ await spawnAsync(STRFRY_BIN, strfryArgs(resolved, "delete", "--filter", filterJson), {
3006
+ cwd: cwd || void 0
3007
+ });
3008
+ } catch (err) {
3009
+ logStrfryError("deleteByPubkey", err);
3010
+ throw err;
3011
+ }
3012
+ });
2908
3013
  }
2909
3014
  var SCAN_COUNT_TIMEOUT_MS = 6e4;
2910
- function spawnScanCount(cwd) {
3015
+ function spawnScanCount(resolved) {
3016
+ const cwd = getStrfryCwd(resolved.strfryDb);
2911
3017
  return new Promise((resolve) => {
2912
- const child = spawn(STRFRY_BIN, strfryArgs("scan", "{}"), {
3018
+ const child = spawn(STRFRY_BIN, strfryArgs(resolved, "scan", "{}"), {
2913
3019
  stdio: ["ignore", "pipe", "pipe"],
2914
3020
  cwd: cwd || void 0
2915
3021
  });
@@ -2933,68 +3039,92 @@ function spawnScanCount(cwd) {
2933
3039
  });
2934
3040
  });
2935
3041
  }
2936
- async function getStats() {
2937
- let total_events = 0;
2938
- let strfry_version = "unknown";
2939
- const cwd = getStrfryCwd();
2940
- total_events = await spawnScanCount(cwd);
2941
- try {
2942
- const { stdout } = await spawnAsync(STRFRY_BIN, ["--version"], {
2943
- cwd: cwd || void 0
2944
- });
2945
- const match2 = stdout.match(/strfry\s+([\d.]+)/i);
2946
- strfry_version = match2?.[1] ?? "unknown";
2947
- } catch {
2948
- }
2949
- let db_size = "0";
2950
- try {
2951
- const { stdout } = await spawnAsync("du", ["-sh", getStrfryDbPath()]);
2952
- db_size = stdout.trim().split(/\s+/)[0] ?? "0";
2953
- } catch {
2954
- db_size = "unknown";
2955
- }
2956
- let uptime_seconds = 0;
2957
- try {
2958
- const { stdout: pidOut } = await spawnAsync("pgrep", ["-x", "strfry"]);
2959
- const pid = pidOut.trim().split("\n")[0];
2960
- if (pid) {
2961
- const { stdout } = await spawnAsync("ps", ["-o", "etimes=", "-p", pid]);
2962
- uptime_seconds = parseInt(stdout.trim(), 10) || 0;
3042
+ async function getStats(cfg = null) {
3043
+ const resolved = resolveConfig(cfg);
3044
+ return withStrfryMutex(resolved.strfryDb, async () => {
3045
+ let total_events = 0;
3046
+ let strfry_version = "unknown";
3047
+ const cwd = getStrfryCwd(resolved.strfryDb);
3048
+ total_events = await spawnScanCount(resolved);
3049
+ try {
3050
+ const { stdout } = await spawnAsync(STRFRY_BIN, ["--version"], {
3051
+ cwd: cwd || void 0
3052
+ });
3053
+ const match2 = stdout.match(/strfry\s+([\d.]+)/i);
3054
+ strfry_version = match2?.[1] ?? "unknown";
3055
+ } catch {
2963
3056
  }
2964
- } catch {
2965
- }
2966
- return {
2967
- total_events,
2968
- db_size,
2969
- uptime_seconds,
2970
- strfry_version
2971
- };
3057
+ let db_size = "0";
3058
+ try {
3059
+ const { stdout } = await spawnAsync("du", ["-sh", resolved.strfryDb]);
3060
+ db_size = stdout.trim().split(/\s+/)[0] ?? "0";
3061
+ } catch {
3062
+ db_size = "unknown";
3063
+ }
3064
+ let uptime_seconds = 0;
3065
+ try {
3066
+ const { stdout: pidOut } = await spawnAsync("pgrep", ["-x", "strfry"]);
3067
+ const pid = pidOut.trim().split("\n")[0];
3068
+ if (pid) {
3069
+ const { stdout } = await spawnAsync("ps", ["-o", "etimes=", "-p", pid]);
3070
+ uptime_seconds = parseInt(stdout.trim(), 10) || 0;
3071
+ }
3072
+ } catch {
3073
+ }
3074
+ return {
3075
+ total_events,
3076
+ db_size,
3077
+ uptime_seconds,
3078
+ strfry_version
3079
+ };
3080
+ });
2972
3081
  }
2973
- async function listUsers(limit = 1e3) {
3082
+ async function listUsers(limit = 1e3, cfg = null) {
2974
3083
  const filter = { kinds: [0, 1, 3], limit };
2975
- const events = await scanEvents(filter);
3084
+ const events = await scanEvents(filter, cfg);
2976
3085
  const pubkeys = /* @__PURE__ */ new Set();
2977
3086
  for (const e of events) {
2978
3087
  pubkeys.add(e.pubkey);
2979
3088
  }
2980
3089
  return Array.from(pubkeys);
2981
3090
  }
2982
- async function readWhitelist() {
2983
- if (!existsSync(WHITELIST_PATH)) {
3091
+ async function readWhitelist(whitelistPath) {
3092
+ if (!existsSync(whitelistPath)) {
2984
3093
  return [];
2985
3094
  }
2986
- const content = await readFile(WHITELIST_PATH, "utf-8");
3095
+ const content = await readFile(whitelistPath, "utf-8");
2987
3096
  return content.split("\n").map((l) => l.trim()).filter(Boolean);
2988
3097
  }
2989
- async function writeWhitelist(lines) {
2990
- const dir = dirname(WHITELIST_PATH);
3098
+ var PUBKEY_HEX_REGEX = /^[0-9a-f]{64}$/;
3099
+ function isValidPubkey(s) {
3100
+ return PUBKEY_HEX_REGEX.test(s.toLowerCase());
3101
+ }
3102
+ async function getPolicyEntries(cfg = null) {
3103
+ const resolved = resolveConfig(cfg);
3104
+ const lines = await readWhitelist(resolved.whitelistPath);
3105
+ const entries = [];
3106
+ for (const line of lines) {
3107
+ if (line.startsWith("#") || !line) continue;
3108
+ if (line.startsWith("!")) {
3109
+ const pubkey2 = line.slice(1).toLowerCase();
3110
+ if (isValidPubkey(pubkey2)) entries.push({ pubkey: pubkey2, status: "blocked" });
3111
+ continue;
3112
+ }
3113
+ const pubkey = line.toLowerCase();
3114
+ if (isValidPubkey(pubkey)) entries.push({ pubkey, status: "allowed" });
3115
+ }
3116
+ return entries;
3117
+ }
3118
+ async function writeWhitelist(whitelistPath, lines) {
3119
+ const dir = dirname(whitelistPath);
2991
3120
  if (!existsSync(dir)) {
2992
3121
  await mkdir(dir, { recursive: true });
2993
3122
  }
2994
- await writeFile(WHITELIST_PATH, lines.join("\n") + "\n", "utf-8");
3123
+ await writeFile(whitelistPath, lines.join("\n") + "\n", "utf-8");
2995
3124
  }
2996
- async function blockPubkey(pubkey) {
2997
- const lines = await readWhitelist();
3125
+ async function blockPubkey(pubkey, cfg = null) {
3126
+ const resolved = resolveConfig(cfg);
3127
+ const lines = await readWhitelist(resolved.whitelistPath);
2998
3128
  const blockLine = `!${pubkey}`;
2999
3129
  const withoutPubkey = lines.filter(
3000
3130
  (l) => l !== pubkey && l !== blockLine
@@ -3002,80 +3132,143 @@ async function blockPubkey(pubkey) {
3002
3132
  if (!withoutPubkey.includes(blockLine)) {
3003
3133
  withoutPubkey.push(blockLine);
3004
3134
  }
3005
- await writeWhitelist(withoutPubkey);
3006
- await deleteByPubkey(pubkey);
3135
+ await writeWhitelist(resolved.whitelistPath, withoutPubkey);
3136
+ await deleteByPubkey(pubkey, cfg);
3007
3137
  }
3008
- async function allowPubkey(pubkey) {
3009
- const lines = await readWhitelist();
3138
+ async function allowPubkey(pubkey, cfg = null) {
3139
+ const resolved = resolveConfig(cfg);
3140
+ const lines = await readWhitelist(resolved.whitelistPath);
3010
3141
  const blockLine = `!${pubkey}`;
3011
3142
  const filtered = lines.filter((l) => l !== blockLine);
3012
3143
  if (!filtered.includes(pubkey)) {
3013
3144
  filtered.push(pubkey);
3014
3145
  }
3015
- await writeWhitelist(filtered);
3146
+ await writeWhitelist(resolved.whitelistPath, filtered);
3016
3147
  }
3017
3148
 
3149
+ // src/routes/stats.ts
3150
+ var statsLegacyRoutes = new Hono2();
3151
+ statsLegacyRoutes.get("/stats", async (c) => {
3152
+ try {
3153
+ const raw2 = await getStats(null);
3154
+ const stats = {
3155
+ total_events: raw2.total_events,
3156
+ db_size: raw2.db_size,
3157
+ uptime: raw2.uptime_seconds,
3158
+ version: raw2.strfry_version
3159
+ };
3160
+ return c.json(stats);
3161
+ } catch {
3162
+ return c.json({ error: "relay unavailable" }, 503);
3163
+ }
3164
+ });
3165
+ var statsMultiRoutes = new Hono2();
3166
+ statsMultiRoutes.get("/:relayId/stats", async (c) => {
3167
+ const relayId = c.req.param("relayId");
3168
+ const instance = getRelayInstance(relayId);
3169
+ if (!instance) return c.json({ error: "relay not found", relayId }, 404);
3170
+ try {
3171
+ const cfg = {
3172
+ strfryConfig: instance.strfryConfig,
3173
+ strfryDb: instance.strfryDb,
3174
+ whitelistPath: instance.whitelistPath
3175
+ };
3176
+ const raw2 = await getStats(cfg);
3177
+ const stats = {
3178
+ total_events: raw2.total_events,
3179
+ db_size: raw2.db_size,
3180
+ uptime: raw2.uptime_seconds,
3181
+ version: raw2.strfry_version
3182
+ };
3183
+ return c.json(stats);
3184
+ } catch {
3185
+ return c.json({ error: "relay unavailable" }, 503);
3186
+ }
3187
+ });
3188
+
3018
3189
  // src/routes/events.ts
3019
- var eventsRoutes = new Hono2();
3020
- eventsRoutes.get("/events", async (c) => {
3190
+ function parseFilter(c) {
3191
+ const kinds = c.req.query("kinds");
3192
+ const authors = c.req.query("authors");
3193
+ const since = c.req.query("since");
3194
+ const until = c.req.query("until");
3195
+ const limit = c.req.query("limit");
3196
+ const filter = {};
3197
+ if (kinds) {
3198
+ const parsed = kinds.split(",").map((k) => parseInt(k, 10));
3199
+ if (parsed.some((n) => Number.isNaN(n))) return null;
3200
+ filter.kinds = parsed;
3201
+ }
3202
+ if (authors) filter.authors = authors.split(",").map((a) => a.trim());
3203
+ if (since) {
3204
+ const n = parseInt(since, 10);
3205
+ if (Number.isNaN(n)) return null;
3206
+ filter.since = n;
3207
+ }
3208
+ if (until) {
3209
+ const n = parseInt(until, 10);
3210
+ if (Number.isNaN(n)) return null;
3211
+ filter.until = n;
3212
+ }
3213
+ if (limit) {
3214
+ const n = parseInt(limit, 10);
3215
+ if (Number.isNaN(n)) return null;
3216
+ filter.limit = n;
3217
+ }
3218
+ return filter;
3219
+ }
3220
+ var eventsLegacyRoutes = new Hono2();
3221
+ eventsLegacyRoutes.get("/events", async (c) => {
3222
+ const filter = parseFilter(c);
3223
+ if (!filter) return c.json({ error: "invalid query params" }, 400);
3021
3224
  try {
3022
- const kinds = c.req.query("kinds");
3023
- const authors = c.req.query("authors");
3024
- const since = c.req.query("since");
3025
- const until = c.req.query("until");
3026
- const limit = c.req.query("limit");
3027
- const filter = {};
3028
- if (kinds) {
3029
- const parsed = kinds.split(",").map((k) => parseInt(k, 10));
3030
- if (parsed.some((n) => Number.isNaN(n))) {
3031
- return c.json({ error: "invalid kinds" }, 400);
3032
- }
3033
- filter.kinds = parsed;
3034
- }
3035
- if (authors) filter.authors = authors.split(",").map((a) => a.trim());
3036
- if (since) {
3037
- const n = parseInt(since, 10);
3038
- if (Number.isNaN(n)) return c.json({ error: "invalid since" }, 400);
3039
- filter.since = n;
3040
- }
3041
- if (until) {
3042
- const n = parseInt(until, 10);
3043
- if (Number.isNaN(n)) return c.json({ error: "invalid until" }, 400);
3044
- filter.until = n;
3045
- }
3046
- if (limit) {
3047
- const n = parseInt(limit, 10);
3048
- if (Number.isNaN(n)) return c.json({ error: "invalid limit" }, 400);
3049
- filter.limit = n;
3050
- }
3051
- const events = await scanEvents(filter);
3225
+ const events = await scanEvents(filter, null);
3052
3226
  return c.json(events);
3053
3227
  } catch {
3054
3228
  return c.json({ error: "relay unavailable" }, 503);
3055
3229
  }
3056
3230
  });
3057
- eventsRoutes.delete("/events/:id", async (c) => {
3231
+ eventsLegacyRoutes.delete("/events/:id", async (c) => {
3058
3232
  try {
3059
3233
  const id = c.req.param("id");
3060
- await deleteEvent(id);
3234
+ await deleteEvent(id, null);
3061
3235
  return c.json({ deleted: id });
3062
3236
  } catch {
3063
3237
  return c.json({ error: "relay unavailable" }, 503);
3064
3238
  }
3065
3239
  });
3066
-
3067
- // src/routes/stats.ts
3068
- var statsRoutes = new Hono2();
3069
- statsRoutes.get("/stats", async (c) => {
3240
+ var eventsMultiRoutes = new Hono2();
3241
+ eventsMultiRoutes.get("/:relayId/events", async (c) => {
3242
+ const relayId = c.req.param("relayId");
3243
+ const instance = getRelayInstance(relayId);
3244
+ if (!instance) return c.json({ error: "relay not found", relayId }, 404);
3245
+ const filter = parseFilter(c);
3246
+ if (!filter) return c.json({ error: "invalid query params" }, 400);
3070
3247
  try {
3071
- const raw2 = await getStats();
3072
- const stats = {
3073
- total_events: raw2.total_events,
3074
- db_size: raw2.db_size,
3075
- uptime: raw2.uptime_seconds,
3076
- version: raw2.strfry_version
3248
+ const cfg = {
3249
+ strfryConfig: instance.strfryConfig,
3250
+ strfryDb: instance.strfryDb,
3251
+ whitelistPath: instance.whitelistPath
3077
3252
  };
3078
- return c.json(stats);
3253
+ const events = await scanEvents(filter, cfg);
3254
+ return c.json(events);
3255
+ } catch {
3256
+ return c.json({ error: "relay unavailable" }, 503);
3257
+ }
3258
+ });
3259
+ eventsMultiRoutes.delete("/:relayId/events/:id", async (c) => {
3260
+ const relayId = c.req.param("relayId");
3261
+ const id = c.req.param("id");
3262
+ const instance = getRelayInstance(relayId);
3263
+ if (!instance) return c.json({ error: "relay not found", relayId }, 404);
3264
+ try {
3265
+ const cfg = {
3266
+ strfryConfig: instance.strfryConfig,
3267
+ strfryDb: instance.strfryDb,
3268
+ whitelistPath: instance.whitelistPath
3269
+ };
3270
+ await deleteEvent(id, cfg);
3271
+ return c.json({ deleted: id });
3079
3272
  } catch {
3080
3273
  return c.json({ error: "relay unavailable" }, 503);
3081
3274
  }
@@ -3083,34 +3276,83 @@ statsRoutes.get("/stats", async (c) => {
3083
3276
 
3084
3277
  // src/routes/policy.ts
3085
3278
  var PUBKEY_REGEX = /^[0-9a-f]{64}$/;
3086
- var policyRoutes = new Hono2();
3087
- policyRoutes.post("/policy/block", async (c) => {
3279
+ function cfgFromInstance(instance) {
3280
+ return {
3281
+ strfryConfig: instance.strfryConfig,
3282
+ strfryDb: instance.strfryDb,
3283
+ whitelistPath: instance.whitelistPath
3284
+ };
3285
+ }
3286
+ var policyLegacyRoutes = new Hono2();
3287
+ policyLegacyRoutes.get("/policy", async (c) => {
3288
+ try {
3289
+ const entries = await getPolicyEntries(null);
3290
+ return c.json({ entries });
3291
+ } catch {
3292
+ return c.json({ error: "relay unavailable" }, 503);
3293
+ }
3294
+ });
3295
+ policyLegacyRoutes.post("/policy/block", async (c) => {
3088
3296
  try {
3089
3297
  const body = await c.req.json();
3090
3298
  const { pubkey } = body;
3091
- if (!pubkey || typeof pubkey !== "string") {
3092
- return c.json({ error: "pubkey is required" }, 400);
3093
- }
3094
- if (!PUBKEY_REGEX.test(pubkey.toLowerCase())) {
3095
- return c.json({ error: "invalid pubkey format" }, 400);
3096
- }
3097
- await blockPubkey(pubkey);
3299
+ if (!pubkey || typeof pubkey !== "string") return c.json({ error: "pubkey is required" }, 400);
3300
+ if (!PUBKEY_REGEX.test(pubkey.toLowerCase())) return c.json({ error: "invalid pubkey format" }, 400);
3301
+ await blockPubkey(pubkey, null);
3098
3302
  return c.json({ blocked: pubkey });
3099
3303
  } catch {
3100
3304
  return c.json({ error: "relay unavailable" }, 503);
3101
3305
  }
3102
3306
  });
3103
- policyRoutes.post("/policy/allow", async (c) => {
3307
+ policyLegacyRoutes.post("/policy/allow", async (c) => {
3104
3308
  try {
3105
3309
  const body = await c.req.json();
3106
3310
  const { pubkey } = body;
3107
- if (!pubkey || typeof pubkey !== "string") {
3108
- return c.json({ error: "pubkey is required" }, 400);
3109
- }
3110
- if (!PUBKEY_REGEX.test(pubkey.toLowerCase())) {
3111
- return c.json({ error: "invalid pubkey format" }, 400);
3112
- }
3113
- await allowPubkey(pubkey);
3311
+ if (!pubkey || typeof pubkey !== "string") return c.json({ error: "pubkey is required" }, 400);
3312
+ if (!PUBKEY_REGEX.test(pubkey.toLowerCase())) return c.json({ error: "invalid pubkey format" }, 400);
3313
+ await allowPubkey(pubkey, null);
3314
+ return c.json({ allowed: pubkey });
3315
+ } catch {
3316
+ return c.json({ error: "relay unavailable" }, 503);
3317
+ }
3318
+ });
3319
+ var policyMultiRoutes = new Hono2();
3320
+ policyMultiRoutes.get("/:relayId/policy", async (c) => {
3321
+ const relayId = c.req.param("relayId");
3322
+ const instance = getRelayInstance(relayId);
3323
+ if (!instance) return c.json({ error: "relay not found", relayId }, 404);
3324
+ try {
3325
+ const entries = await getPolicyEntries(cfgFromInstance(instance));
3326
+ return c.json({ entries });
3327
+ } catch {
3328
+ return c.json({ error: "relay unavailable" }, 503);
3329
+ }
3330
+ });
3331
+ policyMultiRoutes.post("/:relayId/policy/block", async (c) => {
3332
+ const relayId = c.req.param("relayId");
3333
+ const instance = getRelayInstance(relayId);
3334
+ if (!instance) return c.json({ error: "relay not found", relayId }, 404);
3335
+ try {
3336
+ const body = await c.req.json();
3337
+ const { pubkey } = body;
3338
+ if (!pubkey || typeof pubkey !== "string") return c.json({ error: "pubkey is required" }, 400);
3339
+ if (!PUBKEY_REGEX.test(pubkey.toLowerCase())) return c.json({ error: "invalid pubkey format" }, 400);
3340
+ await blockPubkey(pubkey, cfgFromInstance(instance));
3341
+ return c.json({ blocked: pubkey });
3342
+ } catch {
3343
+ return c.json({ error: "relay unavailable" }, 503);
3344
+ }
3345
+ });
3346
+ policyMultiRoutes.post("/:relayId/policy/allow", async (c) => {
3347
+ const relayId = c.req.param("relayId");
3348
+ const instance = getRelayInstance(relayId);
3349
+ if (!instance) return c.json({ error: "relay not found", relayId }, 404);
3350
+ try {
3351
+ const body = await c.req.json();
3352
+ const { pubkey } = body;
3353
+ if (!pubkey || typeof pubkey !== "string") return c.json({ error: "pubkey is required" }, 400);
3354
+ if (!PUBKEY_REGEX.test(pubkey.toLowerCase())) return c.json({ error: "invalid pubkey format" }, 400);
3355
+ await allowPubkey(pubkey, cfgFromInstance(instance));
3114
3356
  return c.json({ allowed: pubkey });
3115
3357
  } catch {
3116
3358
  return c.json({ error: "relay unavailable" }, 503);
@@ -3118,17 +3360,41 @@ policyRoutes.post("/policy/allow", async (c) => {
3118
3360
  });
3119
3361
 
3120
3362
  // src/routes/users.ts
3121
- var usersRoutes = new Hono2();
3122
- usersRoutes.get("/users", async (c) => {
3363
+ var usersLegacyRoutes = new Hono2();
3364
+ usersLegacyRoutes.get("/users", async (c) => {
3365
+ const limitParam = c.req.query("limit");
3366
+ let limit;
3367
+ if (limitParam) {
3368
+ const n = parseInt(limitParam, 10);
3369
+ if (Number.isNaN(n)) return c.json({ error: "invalid limit" }, 400);
3370
+ limit = n;
3371
+ }
3372
+ try {
3373
+ const pubkeys = await listUsers(limit ?? 1e3, null);
3374
+ return c.json({ users: pubkeys });
3375
+ } catch {
3376
+ return c.json({ error: "relay unavailable" }, 503);
3377
+ }
3378
+ });
3379
+ var usersMultiRoutes = new Hono2();
3380
+ usersMultiRoutes.get("/:relayId/users", async (c) => {
3381
+ const relayId = c.req.param("relayId");
3382
+ const instance = getRelayInstance(relayId);
3383
+ if (!instance) return c.json({ error: "relay not found", relayId }, 404);
3384
+ const limitParam = c.req.query("limit");
3385
+ let limit;
3386
+ if (limitParam) {
3387
+ const n = parseInt(limitParam, 10);
3388
+ if (Number.isNaN(n)) return c.json({ error: "invalid limit" }, 400);
3389
+ limit = n;
3390
+ }
3123
3391
  try {
3124
- const limitParam = c.req.query("limit");
3125
- let limit;
3126
- if (limitParam) {
3127
- const n = parseInt(limitParam, 10);
3128
- if (Number.isNaN(n)) return c.json({ error: "invalid limit" }, 400);
3129
- limit = n;
3130
- }
3131
- const pubkeys = await listUsers(limit);
3392
+ const cfg = {
3393
+ strfryConfig: instance.strfryConfig,
3394
+ strfryDb: instance.strfryDb,
3395
+ whitelistPath: instance.whitelistPath
3396
+ };
3397
+ const pubkeys = await listUsers(limit ?? 1e3, cfg);
3132
3398
  return c.json({ users: pubkeys });
3133
3399
  } catch {
3134
3400
  return c.json({ error: "relay unavailable" }, 503);
@@ -3137,22 +3403,32 @@ usersRoutes.get("/users", async (c) => {
3137
3403
 
3138
3404
  // src/index.ts
3139
3405
  var DEFAULT_ORIGINS = [
3140
- "https://admin.bitmacro.io",
3406
+ "https://relay-panel.bitmacro.io",
3141
3407
  "http://localhost:3000"
3142
3408
  ];
3143
3409
  var EXTRA_ORIGINS = (process.env.ALLOWED_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3144
3410
  var ALLOWED_ORIGINS = [...DEFAULT_ORIGINS, ...EXTRA_ORIGINS];
3145
3411
  var app = new Hono2();
3146
- app.use("*", cors({ origin: ALLOWED_ORIGINS }));
3147
3412
  app.use("*", async (c, next) => {
3148
- if (c.req.path === "/health") return next();
3149
- return authMiddleware(c, next);
3413
+ const start = Date.now();
3414
+ await next();
3415
+ console.log(`[relay-agent] ${c.req.method} ${c.req.path} ${c.res.status} ${Date.now() - start}ms`);
3150
3416
  });
3417
+ app.use("*", cors({ origin: ALLOWED_ORIGINS }));
3418
+ app.use("*", authMiddleware);
3151
3419
  app.route("/", healthRoutes);
3152
- app.route("/", eventsRoutes);
3153
- app.route("/", statsRoutes);
3154
- app.route("/", policyRoutes);
3155
- app.route("/", usersRoutes);
3420
+ if (isMultiRelayMode()) {
3421
+ app.route("/", healthMultiRoutes);
3422
+ app.route("/", statsMultiRoutes);
3423
+ app.route("/", eventsMultiRoutes);
3424
+ app.route("/", policyMultiRoutes);
3425
+ app.route("/", usersMultiRoutes);
3426
+ } else {
3427
+ app.route("/", statsLegacyRoutes);
3428
+ app.route("/", eventsLegacyRoutes);
3429
+ app.route("/", policyLegacyRoutes);
3430
+ app.route("/", usersLegacyRoutes);
3431
+ }
3156
3432
  function createServer(port2) {
3157
3433
  return serve({
3158
3434
  fetch: app.fetch,
@@ -3206,12 +3482,15 @@ if (values.help) {
3206
3482
  console.log(HELP);
3207
3483
  process.exit(0);
3208
3484
  }
3209
- var token = values.token ?? process.env.RELAY_AGENT_TOKEN ?? process.env.TOKEN;
3210
- if (!token) {
3211
- console.error("Error: --token is required (or set RELAY_AGENT_TOKEN env var)");
3212
- process.exit(1);
3485
+ var relayInstances = process.env.RELAY_INSTANCES;
3486
+ if (!relayInstances) {
3487
+ const token = values.token ?? process.env.RELAY_AGENT_TOKEN ?? process.env.TOKEN;
3488
+ if (!token) {
3489
+ console.error("Error: --token is required (or set RELAY_AGENT_TOKEN env var)");
3490
+ process.exit(1);
3491
+ }
3492
+ process.env.RELAY_AGENT_TOKEN = token;
3213
3493
  }
3214
- process.env.RELAY_AGENT_TOKEN = token;
3215
3494
  var port = parseInt(values.port ?? process.env.PORT ?? "7800", 10);
3216
3495
  createServer(port);
3217
3496
  console.log(`relay-agent listening on http://localhost:${port}`);