@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/README.md +72 -52
- package/dist/bin/relay-agent.mjs +455 -209
- package/dist/bin/relay-agent.mjs.map +1 -1
- package/dist/index.mjs +443 -200
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
2761
|
+
function getBearerToken(c) {
|
|
2715
2762
|
const authHeader = c.req.header("Authorization");
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
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
|
|
2821
|
-
var
|
|
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
|
|
2835
|
-
|
|
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
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
}
|
|
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
|
-
|
|
2875
|
-
} catch (err) {
|
|
2876
|
-
logStrfryError("scanEvents", err);
|
|
2877
|
-
throw err;
|
|
2878
|
-
}
|
|
2977
|
+
});
|
|
2879
2978
|
}
|
|
2880
|
-
async function deleteEvent(id) {
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
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
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
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(
|
|
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
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
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
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
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(
|
|
3086
|
+
async function readWhitelist(whitelistPath) {
|
|
3087
|
+
if (!existsSync(whitelistPath)) {
|
|
2979
3088
|
return [];
|
|
2980
3089
|
}
|
|
2981
|
-
const content = await readFile(
|
|
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
|
|
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(
|
|
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(
|
|
3118
|
+
await writeFile(whitelistPath, lines.join("\n") + "\n", "utf-8");
|
|
3009
3119
|
}
|
|
3010
|
-
async function blockPubkey(pubkey) {
|
|
3011
|
-
const
|
|
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
|
|
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
|
-
|
|
3034
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
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
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
}
|
|
3135
|
-
|
|
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
|
|
3144
|
-
|
|
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
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
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.
|
|
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("*",
|
|
3176
|
-
if (c.req.path === "/health") return next();
|
|
3177
|
-
return authMiddleware(c, next);
|
|
3178
|
-
});
|
|
3413
|
+
app.use("*", authMiddleware);
|
|
3179
3414
|
app.route("/", healthRoutes);
|
|
3180
|
-
|
|
3181
|
-
app.route("/",
|
|
3182
|
-
app.route("/",
|
|
3183
|
-
app.route("/",
|
|
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,
|