@bitmacro/relay-agent 0.1.5 → 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,65 +3039,69 @@ 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
3098
  var PUBKEY_HEX_REGEX = /^[0-9a-f]{64}$/;
2990
3099
  function isValidPubkey(s) {
2991
3100
  return PUBKEY_HEX_REGEX.test(s.toLowerCase());
2992
3101
  }
2993
- async function getPolicyEntries() {
2994
- const lines = await readWhitelist();
3102
+ async function getPolicyEntries(cfg = null) {
3103
+ const resolved = resolveConfig(cfg);
3104
+ const lines = await readWhitelist(resolved.whitelistPath);
2995
3105
  const entries = [];
2996
3106
  for (const line of lines) {
2997
3107
  if (line.startsWith("#") || !line) continue;
@@ -3005,15 +3115,16 @@ async function getPolicyEntries() {
3005
3115
  }
3006
3116
  return entries;
3007
3117
  }
3008
- async function writeWhitelist(lines) {
3009
- const dir = dirname(WHITELIST_PATH);
3118
+ async function writeWhitelist(whitelistPath, lines) {
3119
+ const dir = dirname(whitelistPath);
3010
3120
  if (!existsSync(dir)) {
3011
3121
  await mkdir(dir, { recursive: true });
3012
3122
  }
3013
- await writeFile(WHITELIST_PATH, lines.join("\n") + "\n", "utf-8");
3123
+ await writeFile(whitelistPath, lines.join("\n") + "\n", "utf-8");
3014
3124
  }
3015
- async function blockPubkey(pubkey) {
3016
- const lines = await readWhitelist();
3125
+ async function blockPubkey(pubkey, cfg = null) {
3126
+ const resolved = resolveConfig(cfg);
3127
+ const lines = await readWhitelist(resolved.whitelistPath);
3017
3128
  const blockLine = `!${pubkey}`;
3018
3129
  const withoutPubkey = lines.filter(
3019
3130
  (l) => l !== pubkey && l !== blockLine
@@ -3021,80 +3132,143 @@ async function blockPubkey(pubkey) {
3021
3132
  if (!withoutPubkey.includes(blockLine)) {
3022
3133
  withoutPubkey.push(blockLine);
3023
3134
  }
3024
- await writeWhitelist(withoutPubkey);
3025
- await deleteByPubkey(pubkey);
3135
+ await writeWhitelist(resolved.whitelistPath, withoutPubkey);
3136
+ await deleteByPubkey(pubkey, cfg);
3026
3137
  }
3027
- async function allowPubkey(pubkey) {
3028
- const lines = await readWhitelist();
3138
+ async function allowPubkey(pubkey, cfg = null) {
3139
+ const resolved = resolveConfig(cfg);
3140
+ const lines = await readWhitelist(resolved.whitelistPath);
3029
3141
  const blockLine = `!${pubkey}`;
3030
3142
  const filtered = lines.filter((l) => l !== blockLine);
3031
3143
  if (!filtered.includes(pubkey)) {
3032
3144
  filtered.push(pubkey);
3033
3145
  }
3034
- await writeWhitelist(filtered);
3146
+ await writeWhitelist(resolved.whitelistPath, filtered);
3035
3147
  }
3036
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
+
3037
3189
  // src/routes/events.ts
3038
- var eventsRoutes = new Hono2();
3039
- 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);
3040
3224
  try {
3041
- const kinds = c.req.query("kinds");
3042
- const authors = c.req.query("authors");
3043
- const since = c.req.query("since");
3044
- const until = c.req.query("until");
3045
- const limit = c.req.query("limit");
3046
- const filter = {};
3047
- if (kinds) {
3048
- const parsed = kinds.split(",").map((k) => parseInt(k, 10));
3049
- if (parsed.some((n) => Number.isNaN(n))) {
3050
- return c.json({ error: "invalid kinds" }, 400);
3051
- }
3052
- filter.kinds = parsed;
3053
- }
3054
- if (authors) filter.authors = authors.split(",").map((a) => a.trim());
3055
- if (since) {
3056
- const n = parseInt(since, 10);
3057
- if (Number.isNaN(n)) return c.json({ error: "invalid since" }, 400);
3058
- filter.since = n;
3059
- }
3060
- if (until) {
3061
- const n = parseInt(until, 10);
3062
- if (Number.isNaN(n)) return c.json({ error: "invalid until" }, 400);
3063
- filter.until = n;
3064
- }
3065
- if (limit) {
3066
- const n = parseInt(limit, 10);
3067
- if (Number.isNaN(n)) return c.json({ error: "invalid limit" }, 400);
3068
- filter.limit = n;
3069
- }
3070
- const events = await scanEvents(filter);
3225
+ const events = await scanEvents(filter, null);
3071
3226
  return c.json(events);
3072
3227
  } catch {
3073
3228
  return c.json({ error: "relay unavailable" }, 503);
3074
3229
  }
3075
3230
  });
3076
- eventsRoutes.delete("/events/:id", async (c) => {
3231
+ eventsLegacyRoutes.delete("/events/:id", async (c) => {
3077
3232
  try {
3078
3233
  const id = c.req.param("id");
3079
- await deleteEvent(id);
3234
+ await deleteEvent(id, null);
3080
3235
  return c.json({ deleted: id });
3081
3236
  } catch {
3082
3237
  return c.json({ error: "relay unavailable" }, 503);
3083
3238
  }
3084
3239
  });
3085
-
3086
- // src/routes/stats.ts
3087
- var statsRoutes = new Hono2();
3088
- 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);
3089
3247
  try {
3090
- const raw2 = await getStats();
3091
- const stats = {
3092
- total_events: raw2.total_events,
3093
- db_size: raw2.db_size,
3094
- uptime: raw2.uptime_seconds,
3095
- version: raw2.strfry_version
3248
+ const cfg = {
3249
+ strfryConfig: instance.strfryConfig,
3250
+ strfryDb: instance.strfryDb,
3251
+ whitelistPath: instance.whitelistPath
3096
3252
  };
3097
- 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 });
3098
3272
  } catch {
3099
3273
  return c.json({ error: "relay unavailable" }, 503);
3100
3274
  }
@@ -3102,42 +3276,83 @@ statsRoutes.get("/stats", async (c) => {
3102
3276
 
3103
3277
  // src/routes/policy.ts
3104
3278
  var PUBKEY_REGEX = /^[0-9a-f]{64}$/;
3105
- var policyRoutes = new Hono2();
3106
- policyRoutes.get("/policy", 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) => {
3107
3288
  try {
3108
- const entries = await getPolicyEntries();
3289
+ const entries = await getPolicyEntries(null);
3109
3290
  return c.json({ entries });
3110
3291
  } catch {
3111
3292
  return c.json({ error: "relay unavailable" }, 503);
3112
3293
  }
3113
3294
  });
3114
- policyRoutes.post("/policy/block", async (c) => {
3295
+ policyLegacyRoutes.post("/policy/block", async (c) => {
3115
3296
  try {
3116
3297
  const body = await c.req.json();
3117
3298
  const { pubkey } = body;
3118
- if (!pubkey || typeof pubkey !== "string") {
3119
- return c.json({ error: "pubkey is required" }, 400);
3120
- }
3121
- if (!PUBKEY_REGEX.test(pubkey.toLowerCase())) {
3122
- return c.json({ error: "invalid pubkey format" }, 400);
3123
- }
3124
- 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);
3125
3302
  return c.json({ blocked: pubkey });
3126
3303
  } catch {
3127
3304
  return c.json({ error: "relay unavailable" }, 503);
3128
3305
  }
3129
3306
  });
3130
- policyRoutes.post("/policy/allow", async (c) => {
3307
+ policyLegacyRoutes.post("/policy/allow", async (c) => {
3131
3308
  try {
3132
3309
  const body = await c.req.json();
3133
3310
  const { pubkey } = body;
3134
- if (!pubkey || typeof pubkey !== "string") {
3135
- return c.json({ error: "pubkey is required" }, 400);
3136
- }
3137
- if (!PUBKEY_REGEX.test(pubkey.toLowerCase())) {
3138
- return c.json({ error: "invalid pubkey format" }, 400);
3139
- }
3140
- 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));
3141
3356
  return c.json({ allowed: pubkey });
3142
3357
  } catch {
3143
3358
  return c.json({ error: "relay unavailable" }, 503);
@@ -3145,17 +3360,41 @@ policyRoutes.post("/policy/allow", async (c) => {
3145
3360
  });
3146
3361
 
3147
3362
  // src/routes/users.ts
3148
- var usersRoutes = new Hono2();
3149
- 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
+ }
3150
3372
  try {
3151
- const limitParam = c.req.query("limit");
3152
- let limit;
3153
- if (limitParam) {
3154
- const n = parseInt(limitParam, 10);
3155
- if (Number.isNaN(n)) return c.json({ error: "invalid limit" }, 400);
3156
- limit = n;
3157
- }
3158
- const pubkeys = await listUsers(limit);
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
+ }
3391
+ try {
3392
+ const cfg = {
3393
+ strfryConfig: instance.strfryConfig,
3394
+ strfryDb: instance.strfryDb,
3395
+ whitelistPath: instance.whitelistPath
3396
+ };
3397
+ const pubkeys = await listUsers(limit ?? 1e3, cfg);
3159
3398
  return c.json({ users: pubkeys });
3160
3399
  } catch {
3161
3400
  return c.json({ error: "relay unavailable" }, 503);
@@ -3164,8 +3403,7 @@ usersRoutes.get("/users", async (c) => {
3164
3403
 
3165
3404
  // src/index.ts
3166
3405
  var DEFAULT_ORIGINS = [
3167
- "https://relay-panel.bitmacro.cloud",
3168
- "https://relay-panel.bitmacro.pro",
3406
+ "https://relay-panel.bitmacro.io",
3169
3407
  "http://localhost:3000"
3170
3408
  ];
3171
3409
  var EXTRA_ORIGINS = (process.env.ALLOWED_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -3177,15 +3415,20 @@ app.use("*", async (c, next) => {
3177
3415
  console.log(`[relay-agent] ${c.req.method} ${c.req.path} ${c.res.status} ${Date.now() - start}ms`);
3178
3416
  });
3179
3417
  app.use("*", cors({ origin: ALLOWED_ORIGINS }));
3180
- app.use("*", async (c, next) => {
3181
- if (c.req.path === "/health") return next();
3182
- return authMiddleware(c, next);
3183
- });
3418
+ app.use("*", authMiddleware);
3184
3419
  app.route("/", healthRoutes);
3185
- app.route("/", eventsRoutes);
3186
- app.route("/", statsRoutes);
3187
- app.route("/", policyRoutes);
3188
- 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
+ }
3189
3432
  function createServer(port2) {
3190
3433
  return serve({
3191
3434
  fetch: app.fetch,
@@ -3239,12 +3482,15 @@ if (values.help) {
3239
3482
  console.log(HELP);
3240
3483
  process.exit(0);
3241
3484
  }
3242
- var token = values.token ?? process.env.RELAY_AGENT_TOKEN ?? process.env.TOKEN;
3243
- if (!token) {
3244
- console.error("Error: --token is required (or set RELAY_AGENT_TOKEN env var)");
3245
- 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;
3246
3493
  }
3247
- process.env.RELAY_AGENT_TOKEN = token;
3248
3494
  var port = parseInt(values.port ?? process.env.PORT ?? "7800", 10);
3249
3495
  createServer(port);
3250
3496
  console.log(`relay-agent listening on http://localhost:${port}`);