@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.
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,68 +3034,92 @@ 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
- async function writeWhitelist(lines) {
2985
- const dir = dirname(WHITELIST_PATH);
3093
+ var PUBKEY_HEX_REGEX = /^[0-9a-f]{64}$/;
3094
+ function isValidPubkey(s) {
3095
+ return PUBKEY_HEX_REGEX.test(s.toLowerCase());
3096
+ }
3097
+ async function getPolicyEntries(cfg = null) {
3098
+ const resolved = resolveConfig(cfg);
3099
+ const lines = await readWhitelist(resolved.whitelistPath);
3100
+ const entries = [];
3101
+ for (const line of lines) {
3102
+ if (line.startsWith("#") || !line) continue;
3103
+ if (line.startsWith("!")) {
3104
+ const pubkey2 = line.slice(1).toLowerCase();
3105
+ if (isValidPubkey(pubkey2)) entries.push({ pubkey: pubkey2, status: "blocked" });
3106
+ continue;
3107
+ }
3108
+ const pubkey = line.toLowerCase();
3109
+ if (isValidPubkey(pubkey)) entries.push({ pubkey, status: "allowed" });
3110
+ }
3111
+ return entries;
3112
+ }
3113
+ async function writeWhitelist(whitelistPath, lines) {
3114
+ const dir = dirname(whitelistPath);
2986
3115
  if (!existsSync(dir)) {
2987
3116
  await mkdir(dir, { recursive: true });
2988
3117
  }
2989
- await writeFile(WHITELIST_PATH, lines.join("\n") + "\n", "utf-8");
3118
+ await writeFile(whitelistPath, lines.join("\n") + "\n", "utf-8");
2990
3119
  }
2991
- async function blockPubkey(pubkey) {
2992
- const lines = await readWhitelist();
3120
+ async function blockPubkey(pubkey, cfg = null) {
3121
+ const resolved = resolveConfig(cfg);
3122
+ const lines = await readWhitelist(resolved.whitelistPath);
2993
3123
  const blockLine = `!${pubkey}`;
2994
3124
  const withoutPubkey = lines.filter(
2995
3125
  (l) => l !== pubkey && l !== blockLine
@@ -2997,80 +3127,143 @@ async function blockPubkey(pubkey) {
2997
3127
  if (!withoutPubkey.includes(blockLine)) {
2998
3128
  withoutPubkey.push(blockLine);
2999
3129
  }
3000
- await writeWhitelist(withoutPubkey);
3001
- await deleteByPubkey(pubkey);
3130
+ await writeWhitelist(resolved.whitelistPath, withoutPubkey);
3131
+ await deleteByPubkey(pubkey, cfg);
3002
3132
  }
3003
- async function allowPubkey(pubkey) {
3004
- const lines = await readWhitelist();
3133
+ async function allowPubkey(pubkey, cfg = null) {
3134
+ const resolved = resolveConfig(cfg);
3135
+ const lines = await readWhitelist(resolved.whitelistPath);
3005
3136
  const blockLine = `!${pubkey}`;
3006
3137
  const filtered = lines.filter((l) => l !== blockLine);
3007
3138
  if (!filtered.includes(pubkey)) {
3008
3139
  filtered.push(pubkey);
3009
3140
  }
3010
- await writeWhitelist(filtered);
3141
+ await writeWhitelist(resolved.whitelistPath, filtered);
3011
3142
  }
3012
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
+
3013
3184
  // src/routes/events.ts
3014
- var eventsRoutes = new Hono2();
3015
- 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);
3016
3219
  try {
3017
- const kinds = c.req.query("kinds");
3018
- const authors = c.req.query("authors");
3019
- const since = c.req.query("since");
3020
- const until = c.req.query("until");
3021
- const limit = c.req.query("limit");
3022
- const filter = {};
3023
- if (kinds) {
3024
- const parsed = kinds.split(",").map((k) => parseInt(k, 10));
3025
- if (parsed.some((n) => Number.isNaN(n))) {
3026
- return c.json({ error: "invalid kinds" }, 400);
3027
- }
3028
- filter.kinds = parsed;
3029
- }
3030
- if (authors) filter.authors = authors.split(",").map((a) => a.trim());
3031
- if (since) {
3032
- const n = parseInt(since, 10);
3033
- if (Number.isNaN(n)) return c.json({ error: "invalid since" }, 400);
3034
- filter.since = n;
3035
- }
3036
- if (until) {
3037
- const n = parseInt(until, 10);
3038
- if (Number.isNaN(n)) return c.json({ error: "invalid until" }, 400);
3039
- filter.until = n;
3040
- }
3041
- if (limit) {
3042
- const n = parseInt(limit, 10);
3043
- if (Number.isNaN(n)) return c.json({ error: "invalid limit" }, 400);
3044
- filter.limit = n;
3045
- }
3046
- const events = await scanEvents(filter);
3220
+ const events = await scanEvents(filter, null);
3047
3221
  return c.json(events);
3048
3222
  } catch {
3049
3223
  return c.json({ error: "relay unavailable" }, 503);
3050
3224
  }
3051
3225
  });
3052
- eventsRoutes.delete("/events/:id", async (c) => {
3226
+ eventsLegacyRoutes.delete("/events/:id", async (c) => {
3053
3227
  try {
3054
3228
  const id = c.req.param("id");
3055
- await deleteEvent(id);
3229
+ await deleteEvent(id, null);
3056
3230
  return c.json({ deleted: id });
3057
3231
  } catch {
3058
3232
  return c.json({ error: "relay unavailable" }, 503);
3059
3233
  }
3060
3234
  });
3061
-
3062
- // src/routes/stats.ts
3063
- var statsRoutes = new Hono2();
3064
- 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);
3065
3242
  try {
3066
- const raw2 = await getStats();
3067
- const stats = {
3068
- total_events: raw2.total_events,
3069
- db_size: raw2.db_size,
3070
- uptime: raw2.uptime_seconds,
3071
- version: raw2.strfry_version
3243
+ const cfg = {
3244
+ strfryConfig: instance.strfryConfig,
3245
+ strfryDb: instance.strfryDb,
3246
+ whitelistPath: instance.whitelistPath
3072
3247
  };
3073
- 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 });
3074
3267
  } catch {
3075
3268
  return c.json({ error: "relay unavailable" }, 503);
3076
3269
  }
@@ -3078,34 +3271,83 @@ statsRoutes.get("/stats", async (c) => {
3078
3271
 
3079
3272
  // src/routes/policy.ts
3080
3273
  var PUBKEY_REGEX = /^[0-9a-f]{64}$/;
3081
- var policyRoutes = new Hono2();
3082
- policyRoutes.post("/policy/block", 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) => {
3283
+ try {
3284
+ const entries = await getPolicyEntries(null);
3285
+ return c.json({ entries });
3286
+ } catch {
3287
+ return c.json({ error: "relay unavailable" }, 503);
3288
+ }
3289
+ });
3290
+ policyLegacyRoutes.post("/policy/block", async (c) => {
3083
3291
  try {
3084
3292
  const body = await c.req.json();
3085
3293
  const { pubkey } = body;
3086
- if (!pubkey || typeof pubkey !== "string") {
3087
- return c.json({ error: "pubkey is required" }, 400);
3088
- }
3089
- if (!PUBKEY_REGEX.test(pubkey.toLowerCase())) {
3090
- return c.json({ error: "invalid pubkey format" }, 400);
3091
- }
3092
- 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);
3093
3297
  return c.json({ blocked: pubkey });
3094
3298
  } catch {
3095
3299
  return c.json({ error: "relay unavailable" }, 503);
3096
3300
  }
3097
3301
  });
3098
- policyRoutes.post("/policy/allow", async (c) => {
3302
+ policyLegacyRoutes.post("/policy/allow", async (c) => {
3099
3303
  try {
3100
3304
  const body = await c.req.json();
3101
3305
  const { pubkey } = body;
3102
- if (!pubkey || typeof pubkey !== "string") {
3103
- return c.json({ error: "pubkey is required" }, 400);
3104
- }
3105
- if (!PUBKEY_REGEX.test(pubkey.toLowerCase())) {
3106
- return c.json({ error: "invalid pubkey format" }, 400);
3107
- }
3108
- 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));
3109
3351
  return c.json({ allowed: pubkey });
3110
3352
  } catch {
3111
3353
  return c.json({ error: "relay unavailable" }, 503);
@@ -3113,17 +3355,41 @@ policyRoutes.post("/policy/allow", async (c) => {
3113
3355
  });
3114
3356
 
3115
3357
  // src/routes/users.ts
3116
- var usersRoutes = new Hono2();
3117
- 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
+ }
3118
3367
  try {
3119
- const limitParam = c.req.query("limit");
3120
- let limit;
3121
- if (limitParam) {
3122
- const n = parseInt(limitParam, 10);
3123
- if (Number.isNaN(n)) return c.json({ error: "invalid limit" }, 400);
3124
- limit = n;
3125
- }
3126
- const pubkeys = await listUsers(limit);
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
+ }
3386
+ try {
3387
+ const cfg = {
3388
+ strfryConfig: instance.strfryConfig,
3389
+ strfryDb: instance.strfryDb,
3390
+ whitelistPath: instance.whitelistPath
3391
+ };
3392
+ const pubkeys = await listUsers(limit ?? 1e3, cfg);
3127
3393
  return c.json({ users: pubkeys });
3128
3394
  } catch {
3129
3395
  return c.json({ error: "relay unavailable" }, 503);
@@ -3132,22 +3398,32 @@ usersRoutes.get("/users", async (c) => {
3132
3398
 
3133
3399
  // src/index.ts
3134
3400
  var DEFAULT_ORIGINS = [
3135
- "https://admin.bitmacro.io",
3401
+ "https://relay-panel.bitmacro.io",
3136
3402
  "http://localhost:3000"
3137
3403
  ];
3138
3404
  var EXTRA_ORIGINS = (process.env.ALLOWED_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3139
3405
  var ALLOWED_ORIGINS = [...DEFAULT_ORIGINS, ...EXTRA_ORIGINS];
3140
3406
  var app = new Hono2();
3141
- app.use("*", cors({ origin: ALLOWED_ORIGINS }));
3142
3407
  app.use("*", async (c, next) => {
3143
- if (c.req.path === "/health") return next();
3144
- return authMiddleware(c, next);
3408
+ const start = Date.now();
3409
+ await next();
3410
+ console.log(`[relay-agent] ${c.req.method} ${c.req.path} ${c.res.status} ${Date.now() - start}ms`);
3145
3411
  });
3412
+ app.use("*", cors({ origin: ALLOWED_ORIGINS }));
3413
+ app.use("*", authMiddleware);
3146
3414
  app.route("/", healthRoutes);
3147
- app.route("/", eventsRoutes);
3148
- app.route("/", statsRoutes);
3149
- app.route("/", policyRoutes);
3150
- 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
+ }
3151
3427
  function createServer(port) {
3152
3428
  return serve({
3153
3429
  fetch: app.fetch,