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