@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/README.md +85 -56
- package/dist/bin/relay-agent.mjs +482 -203
- package/dist/bin/relay-agent.mjs.map +1 -1
- package/dist/index.mjs +470 -194
- 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,68 +3034,92 @@ 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
|
-
|
|
2985
|
-
|
|
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(
|
|
3118
|
+
await writeFile(whitelistPath, lines.join("\n") + "\n", "utf-8");
|
|
2990
3119
|
}
|
|
2991
|
-
async function blockPubkey(pubkey) {
|
|
2992
|
-
const
|
|
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
|
|
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
|
-
|
|
3015
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
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
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3082
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
}
|
|
3108
|
-
|
|
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
|
|
3117
|
-
|
|
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
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
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://
|
|
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
|
-
|
|
3144
|
-
|
|
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
|
-
|
|
3148
|
-
app.route("/",
|
|
3149
|
-
app.route("/",
|
|
3150
|
-
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
|
+
}
|
|
3151
3427
|
function createServer(port) {
|
|
3152
3428
|
return serve({
|
|
3153
3429
|
fetch: app.fetch,
|