basic nip86 implmentation
This commit is contained in:
parent
22a4fe069f
commit
df94aed0d0
6 changed files with 370 additions and 56 deletions
31
biome.json
Normal file
31
biome.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": []
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
}
|
||||
}
|
29
bun.lock
29
bun.lock
|
@ -3,6 +3,9 @@
|
|||
"workspaces": {
|
||||
"": {
|
||||
"name": "nip42-proxy",
|
||||
"dependencies": {
|
||||
"nostr-tools": "^2.16.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
|
@ -12,6 +15,18 @@
|
|||
},
|
||||
},
|
||||
"packages": {
|
||||
"@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="],
|
||||
|
||||
"@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
|
||||
|
||||
"@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
|
||||
|
||||
"@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="],
|
||||
|
||||
"@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
|
||||
|
@ -22,8 +37,22 @@
|
|||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"nostr-tools": ["nostr-tools@2.16.2", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-ZxH9EbSt5ypURZj2TGNJxZd0Omb5ag5KZSu8IyJMCdLyg2KKz+2GA0sP/cSawCQEkyviIN4eRT4G2gB/t9lMRw=="],
|
||||
|
||||
"nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
|
||||
"@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
|
||||
|
||||
"@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="],
|
||||
|
||||
"@scure/bip32/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
|
||||
|
||||
"@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
|
||||
|
||||
"@scure/bip32/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
|
||||
}
|
||||
}
|
||||
|
|
25
index.ts
25
index.ts
|
@ -1,8 +1,21 @@
|
|||
import { main } from "./src/main.ts";
|
||||
import { main, type RelayConfig } from "./src/main.ts";
|
||||
|
||||
let allowUnauthedPublish = Boolean(process.env.ALLOW_UNAUTHED_PUBLISH) || false;
|
||||
let relay = process.env.RELAY_URL ?? Bun.argv[Bun.argv.length - 1];
|
||||
if (!relay?.startsWith("wss://") && !relay?.startsWith("ws://"))
|
||||
relay = "wss://relay.arx-ccn.com";
|
||||
let config: RelayConfig = {
|
||||
allowUnauthedPublish: Boolean(process.env.ALLOW_UNAUTHED_PUBLISH) || false,
|
||||
relay: process.env.RELAY_URL!,
|
||||
name: process.env.RELAY_NAME,
|
||||
description: process.env.RELAY_DESCRIPTION,
|
||||
banner: process.env.RELAY_BANNER,
|
||||
icon: process.env.RELAY_ICON,
|
||||
contact: process.env.RELAY_CONTACT,
|
||||
policy: process.env.RELAY_POLICY,
|
||||
adminPubkey: process.env.ADMIN_PUBKEY,
|
||||
};
|
||||
|
||||
main(relay, allowUnauthedPublish)
|
||||
if (
|
||||
!config.relay ||
|
||||
(!config.relay?.startsWith("wss://") && !config.relay?.startsWith("ws://"))
|
||||
)
|
||||
config.relay = "wss://relay.arx-ccn.com";
|
||||
|
||||
main(config);
|
||||
|
|
|
@ -8,5 +8,8 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"nostr-tools": "^2.16.2"
|
||||
}
|
||||
}
|
||||
|
|
271
src/main.ts
271
src/main.ts
|
@ -1,99 +1,270 @@
|
|||
import type { ServerWebSocket } from "bun";
|
||||
|
||||
interface Event {
|
||||
kind: number;
|
||||
tags: string[][];
|
||||
content: string;
|
||||
created_at: number;
|
||||
pubkey: string;
|
||||
id: string;
|
||||
sig: string;
|
||||
}
|
||||
import {
|
||||
allowPubkey,
|
||||
banPubkey,
|
||||
getAllAllowedPubkeys,
|
||||
isPubkeyAllowed,
|
||||
validateAuthEvent,
|
||||
} from "./utils.ts";
|
||||
import { getGitCommitHash } from "./utils.ts" with { type: "macro" };
|
||||
import { nip98 } from "nostr-tools";
|
||||
|
||||
type Nip42ProxySocketData = {
|
||||
authenticated: boolean;
|
||||
authToken: string;
|
||||
remoteWs: WebSocket;
|
||||
remoteWs?: WebSocket;
|
||||
};
|
||||
|
||||
async function validateAuthEvent(event: Event, challenge: string): Promise<boolean> {
|
||||
if (event.kind !== 22242) return false;
|
||||
const last30Seconds = Math.floor(Date.now() / 1000) - 30;
|
||||
if (event.created_at < last30Seconds) return false;
|
||||
const challengeTag = event.tags.find(tag => tag[0] === 'challenge')?.[1];
|
||||
if (challengeTag !== challenge) return false;
|
||||
return await isPubkeyAllowed(event);
|
||||
const sendMessage = (
|
||||
ws: ServerWebSocket<Nip42ProxySocketData>,
|
||||
message: any[],
|
||||
) => ws.send(JSON.stringify(message), true);
|
||||
const sendAuth = (ws: ServerWebSocket<Nip42ProxySocketData>) =>
|
||||
sendMessage(ws, [
|
||||
"AUTH",
|
||||
ws.data.authToken,
|
||||
"This is an authenticated relay.",
|
||||
]);
|
||||
|
||||
export interface RelayConfig {
|
||||
adminPubkey?: string;
|
||||
relay: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
banner?: string;
|
||||
contact?: string;
|
||||
policy?: string;
|
||||
allowUnauthedPublish: boolean;
|
||||
}
|
||||
|
||||
async function isPubkeyAllowed(event: Event): Promise<boolean> {
|
||||
const file = Bun.file("./allowed-pubkeys.json");
|
||||
if (!await file.exists()) return true;
|
||||
const allowedPubkeys = JSON.parse(await file.text());
|
||||
return allowedPubkeys.includes(event.pubkey);
|
||||
}
|
||||
export function main({
|
||||
relay,
|
||||
adminPubkey,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
banner,
|
||||
contact,
|
||||
policy,
|
||||
allowUnauthedPublish,
|
||||
}: RelayConfig) {
|
||||
function relayInfo() {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
name,
|
||||
description,
|
||||
banner,
|
||||
pubkey: adminPubkey,
|
||||
contact,
|
||||
software: "https://git.arx-ccn.com/Arx/nip42-proxy.git",
|
||||
supported_nips: [1, 2, 4, 9, 11, 22, 28, 40, 42, 70, 77, 86],
|
||||
version: `nip42-proxy - ${getGitCommitHash()}`,
|
||||
posting_policy: policy,
|
||||
icon,
|
||||
limitation: {
|
||||
auth_required: true,
|
||||
restricted_writes: true,
|
||||
},
|
||||
}),
|
||||
{ headers: { "content-type": "application/json; charset=utf-8" } },
|
||||
);
|
||||
}
|
||||
|
||||
const sendMessage = (ws: ServerWebSocket<Nip42ProxySocketData>, message: any[]) => ws.send(JSON.stringify(message), true);
|
||||
const sendAuth = (ws: ServerWebSocket<Nip42ProxySocketData>) => sendMessage(ws, ["AUTH", ws.data.authToken, "This is an authenticated relay."]);
|
||||
async function relayRPC(
|
||||
config: RelayConfig,
|
||||
token: string,
|
||||
url: string,
|
||||
method: string,
|
||||
params: any[],
|
||||
) {
|
||||
if (method === "supportedmethods")
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
result: [
|
||||
"supportedmethods",
|
||||
"banpubkey",
|
||||
"allowpubkey",
|
||||
"listallowedpubkeys",
|
||||
],
|
||||
}),
|
||||
);
|
||||
const valid = await nip98.validateToken(token, url, "POST");
|
||||
if (!token || !valid)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "You are not authorized",
|
||||
}),
|
||||
);
|
||||
const event = await nip98.unpackEventFromToken(token);
|
||||
if (adminPubkey && event.pubkey !== adminPubkey)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "You are not authorized",
|
||||
}),
|
||||
);
|
||||
else if (!(await isPubkeyAllowed(event)))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "You are not authorized",
|
||||
}),
|
||||
);
|
||||
|
||||
if (method === "allowpubkey") {
|
||||
const [pubkey] = params;
|
||||
if (!pubkey)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: `Missing pubkey parameter`,
|
||||
}),
|
||||
);
|
||||
await allowPubkey(pubkey);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
result: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (method === "listallowedpubkeys") {
|
||||
const resp = await getAllAllowedPubkeys();
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
result: resp.map((k: string) => ({ pubkey: k })),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (method === "banpubkey") {
|
||||
const [pubkey] = params;
|
||||
if (!pubkey)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: `Missing pubkey parameter`,
|
||||
}),
|
||||
);
|
||||
await banPubkey(pubkey);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
result: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: `Unsupported method ${method}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function main(mainRelayUrl: string, allowUnauthedPublish: boolean) {
|
||||
const server = Bun.serve<Nip42ProxySocketData, {}>({
|
||||
fetch(req, server) {
|
||||
const upgrade = server.upgrade(req, {
|
||||
async fetch(req, server) {
|
||||
const upgraded = server.upgrade(req, {
|
||||
data: {
|
||||
authenticated: false,
|
||||
authToken: Bun.randomUUIDv7()
|
||||
}
|
||||
authToken: Bun.randomUUIDv7(),
|
||||
},
|
||||
});
|
||||
if (!upgrade) console.log(`http request`)
|
||||
return new Response("this is a proxy. Use http to access it");
|
||||
if (upgraded) return undefined as any;
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/") {
|
||||
switch (req.headers.get("content-type")) {
|
||||
case "application/nostr+json":
|
||||
return relayInfo();
|
||||
case "application/nostr+json+rpc":
|
||||
const token = req.headers.get("Authorization") || "";
|
||||
let data = await req.json();
|
||||
if (!data)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Please send a valid nostr rpc request",
|
||||
}),
|
||||
);
|
||||
let { method, params } = await req.json();
|
||||
params ||= [];
|
||||
return relayRPC(adminPubkey, token, req.url, method, params);
|
||||
break;
|
||||
default:
|
||||
return new Response("Nip42 Proxy");
|
||||
}
|
||||
}
|
||||
if (url.pathname === "/api/health")
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
activeRequests: server.pendingRequests,
|
||||
activeWebsockets: server.pendingWebSockets,
|
||||
}),
|
||||
);
|
||||
},
|
||||
websocket: {
|
||||
async message(ws, msg) {
|
||||
const [command, ...data] = JSON.parse(msg);
|
||||
const [command, ...data] = JSON.parse(msg as string);
|
||||
if (!ws.data.authenticated) {
|
||||
if (command === "REQ") {
|
||||
const [name, ...filters] = data;
|
||||
sendMessage(ws, ["CLOSED", name, 'auth-required: you must authenticate first']);
|
||||
sendMessage(ws, [
|
||||
"CLOSED",
|
||||
name,
|
||||
"auth-required: you must authenticate first",
|
||||
]);
|
||||
}
|
||||
if (command === "EVENT") {
|
||||
const [event] = data;
|
||||
if (allowUnauthedPublish && await isPubkeyAllowed(event)) {
|
||||
if (allowUnauthedPublish && (await isPubkeyAllowed(event))) {
|
||||
ws.data.remoteWs.send(msg);
|
||||
return;
|
||||
}
|
||||
sendMessage(ws, ["OK", event.id, false, 'auth-required: you must authenticate first']);
|
||||
sendMessage(ws, [
|
||||
"OK",
|
||||
event.id,
|
||||
false,
|
||||
"auth-required: you must authenticate first",
|
||||
]);
|
||||
}
|
||||
if (command === "AUTH") {
|
||||
const [event] = data;
|
||||
const valid = await validateAuthEvent(event, ws.data.authToken);
|
||||
if (!valid) return sendMessage(ws, ['NOTICE', "Invalid auth event"]);
|
||||
sendMessage(ws, ['OK', event.id, true, "successully authenticated. welcome"]);
|
||||
if (!valid)
|
||||
return sendMessage(ws, ["NOTICE", "Invalid auth event"]);
|
||||
sendMessage(ws, [
|
||||
"OK",
|
||||
event.id,
|
||||
true,
|
||||
"successully authenticated. welcome",
|
||||
]);
|
||||
ws.data.authenticated = true;
|
||||
return;
|
||||
}
|
||||
return sendAuth(ws);
|
||||
} else if (ws.data.authenticated && command === "AUTH") {
|
||||
const [event] = data;
|
||||
return sendMessage(ws, ['OK', event.id, true, 'you were already authenticated'])
|
||||
return sendMessage(ws, [
|
||||
"OK",
|
||||
event.id,
|
||||
true,
|
||||
"you were already authenticated",
|
||||
]);
|
||||
}
|
||||
ws.data.remoteWs.send(msg);
|
||||
},
|
||||
open(ws) {
|
||||
const remoteWs = new WebSocket(mainRelayUrl);
|
||||
remoteWs.addEventListener("message", data => ws.send(data.data, true));
|
||||
const remoteWs = new WebSocket(relay);
|
||||
remoteWs.addEventListener("message", (data) =>
|
||||
ws.send(
|
||||
(data as MessageEvent).data as string | ArrayBufferLike,
|
||||
true,
|
||||
),
|
||||
);
|
||||
remoteWs.addEventListener("open", () => {
|
||||
ws.data.remoteWs = remoteWs;
|
||||
sendAuth(ws);
|
||||
})
|
||||
remoteWs.addEventListener("error", console.error)
|
||||
});
|
||||
remoteWs.addEventListener("error", console.error);
|
||||
},
|
||||
perMessageDeflate: true,
|
||||
perMessageDeflateCompressionLevel: 9
|
||||
perMessageDeflateCompressionLevel: 9,
|
||||
close(ws) {
|
||||
ws.data.remoteWs?.close();
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log('Server listening @', server.url.host)
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log("Server listening @", server.url.host);
|
||||
}
|
||||
|
||||
|
|
67
src/utils.ts
Normal file
67
src/utils.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import type { Event } from "nostr-tools";
|
||||
|
||||
export function getGitCommitHash() {
|
||||
try {
|
||||
const { stdout } = Bun.spawnSync({
|
||||
cmd: ["git", "rev-parse", "HEAD"],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const out = stdout?.toString()?.trim();
|
||||
return out?.split("\n")[0] || "unknown";
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateAuthEvent(
|
||||
event: Event,
|
||||
challenge: string,
|
||||
): Promise<boolean> {
|
||||
if (event.kind !== 22242) return false;
|
||||
const last30Seconds = Math.floor(Date.now() / 1000) - 30;
|
||||
if (event.created_at < last30Seconds) return false;
|
||||
const challengeTag = event.tags.find((tag) => tag[0] === "challenge")?.[1];
|
||||
if (challengeTag !== challenge) return false;
|
||||
return await isPubkeyAllowed(event);
|
||||
}
|
||||
|
||||
export async function isPubkeyAllowed(event: Event): Promise<boolean> {
|
||||
const file = Bun.file("./allowed-pubkeys.json");
|
||||
if (!(await file.exists())) return true;
|
||||
try {
|
||||
const allowedPubkeys = JSON.parse(await file.text());
|
||||
return (
|
||||
Array.isArray(allowedPubkeys) && allowedPubkeys.includes(event.pubkey)
|
||||
);
|
||||
} catch {
|
||||
// If the file is malformed, default to deny
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllAllowedPubkeys() {
|
||||
const file = Bun.file("./allowed-pubkeys.json");
|
||||
try {
|
||||
if (await file.exists()) return JSON.parse(await file.text());
|
||||
} catch {}
|
||||
return [] as string[];
|
||||
}
|
||||
|
||||
export async function saveAllowedPubkeys(pubkeys: string[]): Promise<number> {
|
||||
const file = Bun.file("./allowed-pubkeys.json");
|
||||
const unique = Array.from(new Set(pubkeys));
|
||||
return await file.write(JSON.stringify(unique));
|
||||
}
|
||||
|
||||
export async function allowPubkey(pubkey: string): Promise<number> {
|
||||
const pubkeys = await getAllAllowedPubkeys();
|
||||
if (!pubkeys.includes(pubkey)) pubkeys.push(pubkey);
|
||||
return await saveAllowedPubkeys(pubkeys);
|
||||
}
|
||||
|
||||
export async function banPubkey(pubkey: string): Promise<number> {
|
||||
const pubkeys = await getAllAllowedPubkeys();
|
||||
const filtered = pubkeys.filter((p: string) => p !== pubkey);
|
||||
return await saveAllowedPubkeys(filtered);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue