add nginx cache invalidation

bundle nginx in docker image
switch from ndk to nostr-tools
This commit is contained in:
hzrd149 2024-09-26 12:48:13 -05:00
parent 88a9229633
commit b7b43cff10
20 changed files with 238 additions and 218 deletions

View File

@ -0,0 +1,5 @@
---
"nsite-ts": minor
---
Bundle nginx in docker image

View File

@ -0,0 +1,5 @@
---
"nsite-ts": minor
---
Add NGINX_CACHE_DIR for invalidating nginx cache

View File

@ -0,0 +1,5 @@
---
"nsite-ts": minor
---
Add SUBSCRIPTION_RELAYS for listening for new events

View File

@ -2,8 +2,11 @@
# can be in-memory, redis:// or sqlite://
CACHE_PATH="in-memory"
# A list of nostr relays to search
NOSTR_RELAYS=wss://nos.lol,wss://relay.damus.io
# A list of relays to find users relay lists (10002) and blossom servers (10063)
LOOKUP_RELAYS=wss://user.kindpag.es,wss://purplepag.es
# A list of nostr relays to listen to for new nsite events
SUBSCRIPTION_RELAYS=wss://nos.lol,wss://relay.damus.io
# A list of fallback blossom servers
BLOSSOM_SERVERS=https://cdn.satellite.earth
@ -12,4 +15,4 @@ BLOSSOM_SERVERS=https://cdn.satellite.earth
MAX_FILE_SIZE='2 MB'
# the hostname or ip of the upstream nginx proxy cache
NGINX_HOST='nginx'
NGINX_CACHE_DIR='/var/nginx/cache'

View File

@ -5,6 +5,9 @@ ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN apk update && apk add --no-cache nginx supervisor
COPY supervisord.conf /etc/supervisord.conf
WORKDIR /app
COPY package.json .
COPY pnpm-lock.yaml .
@ -19,12 +22,29 @@ COPY src ./src
RUN pnpm build
FROM base AS main
# Setup user
RUN addgroup -S nsite && adduser -S nsite -G nsite
RUN chown -R nsite:nsite /app
# Setup nginx
COPY nginx/nginx.conf /etc/nginx/nginx.conf
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
# setup nsite
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build ./app/build ./build
COPY ./public ./public
EXPOSE 80
ENV PORT="80"
VOLUME [ "/var/cache/nginx" ]
ENTRYPOINT [ "node", "." ]
EXPOSE 80 3000
ENV NSITE_PORT="3000"
ENV NGINX_CACHE_DIR="/var/cache/nginx"
COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

View File

@ -1,22 +1,17 @@
version: "3.7"
volumes:
cache: {}
services:
nginx:
image: nginx:alpine
ports:
- 8080:80
volumes:
- cache:/var/cache/nginx
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
nsite:
build: .
image: ghcr.io/hzrd149/nsite-ts:master
environment:
LOOKUP_RELAYS: wss://user.kindpag.es,wss://purplepag.es
SUBSCRIPTION_RELAYS: wss://nostrue.com/,wss://nos.lol/,wss://relay.damus.io/,wss://purplerelay.com/
volumes:
- type: tmpfs
target: /var/cache/nginx
tmpfs:
size: 100M
ports:
- 3000:80
- 8080:80
- 3000:3000

5
docker-entrypoint.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
chown -R nginx:nginx /var/cache/nginx
exec "$@"

View File

@ -1,7 +1,7 @@
server {
listen 80;
listen [::]:80;
server_name nsite-proxy;
server_name nsite;
location / {
proxy_cache request_cache;
@ -17,13 +17,6 @@ server {
add_header Cache-Control "public, no-transform";
proxy_set_header Host $host;
proxy_pass http://nsite;
proxy_pass http://127.0.0.1:3000;
}
# Manual cache invalidation ( cant use proxy_cache_purge )
# location ~ /purge(/.*) {
# allow 127.0.0.1;
# deny all;
# proxy_cache_purge request_cache $scheme$proxy_host$1;
# }
}

View File

@ -1,7 +1,7 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
error_log /dev/stderr notice;
pid /var/run/nginx.pid;
events {
@ -19,7 +19,7 @@ http {
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
access_log /dev/stdout main;
sendfile on;
#tcp_nopush on;

View File

@ -21,7 +21,6 @@
"@keyv/redis": "^3.0.1",
"@keyv/sqlite": "^4.0.1",
"@koa/cors": "^5.0.0",
"@nostr-dev-kit/ndk": "^2.10.0",
"blossom-client-sdk": "^1.1.1",
"dotenv": "^16.4.5",
"follow-redirects": "^1.15.6",

113
pnpm-lock.yaml generated
View File

@ -20,9 +20,6 @@ importers:
'@koa/cors':
specifier: ^5.0.0
version: 5.0.0
'@nostr-dev-kit/ndk':
specifier: ^2.10.0
version: 2.10.0(typescript@5.6.2)
blossom-client-sdk:
specifier: ^1.1.1
version: 1.1.1
@ -215,10 +212,6 @@ packages:
'@noble/curves@1.2.0':
resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==}
'@noble/curves@1.6.0':
resolution: {integrity: sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==}
engines: {node: ^14.21.3 || >=16}
'@noble/hashes@1.3.1':
resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==}
engines: {node: '>= 16'}
@ -231,9 +224,6 @@ packages:
resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==}
engines: {node: ^14.21.3 || >=16}
'@noble/secp256k1@2.1.0':
resolution: {integrity: sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -246,10 +236,6 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@nostr-dev-kit/ndk@2.10.0':
resolution: {integrity: sha512-TqCAAo6ylORraAXrzRkCGFN2xTMiFbdER8Y8CtUT0HwOpFG/Wn+PBNeDeDmqkl/6LaPdeyXmVwCWj2KcUjIwYA==}
engines: {node: '>=16'}
'@npmcli/fs@1.1.1':
resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==}
@ -316,9 +302,6 @@ packages:
'@scure/base@1.1.1':
resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==}
'@scure/base@1.1.9':
resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==}
'@scure/bip32@1.3.1':
resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==}
@ -666,10 +649,6 @@ packages:
cross-spawn@5.1.0:
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@ -794,10 +773,6 @@ packages:
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
@ -818,10 +793,6 @@ packages:
debug:
optional: true
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@ -1046,9 +1017,6 @@ packages:
resolution: {integrity: sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==}
engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4}
light-bolt11-decoder@3.1.1:
resolution: {integrity: sha512-sLg/KCwYkgsHWkefWd6KqpCHrLFWWaXTOX3cf6yD2hAzL0SLpX+lFcaFK2spkjbgzG6hhijKfORDc9WoUHwX0A==}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@ -1176,14 +1144,6 @@ packages:
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-gyp@8.4.1:
resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==}
engines: {node: '>= 10.12.0'}
@ -1539,9 +1499,6 @@ packages:
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
hasBin: true
tseep@1.2.2:
resolution: {integrity: sha512-GgPFuNx+08UaYBYmJQmuI86ykYa2PUUtfXAYb4MLRHGunSCp8k9N+dbsR4PK1yk4/zV9q4e4PrNg8ymXqGYaYA==}
tslib@2.7.0:
resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
@ -1559,9 +1516,6 @@ packages:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
typescript-lru-cache@2.0.0:
resolution: {integrity: sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==}
typescript@5.6.2:
resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==}
engines: {node: '>=14.17'}
@ -1583,10 +1537,6 @@ packages:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
utf8-buffer@1.0.0:
resolution: {integrity: sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg==}
engines: {node: '>=8'}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -1594,10 +1544,6 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
websocket-polyfill@1.0.0:
resolution: {integrity: sha512-QwfEy8jcOOCVO9su9UP+msEmhZa4a9WSJfePIdCT8GxwVl2Z9toM7nCqFfDDxA/sRmxgf1KNiwL6PXvjJ9qRxw==}
@ -1867,18 +1813,12 @@ snapshots:
dependencies:
'@noble/hashes': 1.3.2
'@noble/curves@1.6.0':
dependencies:
'@noble/hashes': 1.5.0
'@noble/hashes@1.3.1': {}
'@noble/hashes@1.3.2': {}
'@noble/hashes@1.5.0': {}
'@noble/secp256k1@2.1.0': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -1891,26 +1831,6 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@nostr-dev-kit/ndk@2.10.0(typescript@5.6.2)':
dependencies:
'@noble/curves': 1.6.0
'@noble/hashes': 1.5.0
'@noble/secp256k1': 2.1.0
'@scure/base': 1.1.9
debug: 4.3.7(supports-color@5.5.0)
light-bolt11-decoder: 3.1.1
node-fetch: 3.3.2
nostr-tools: 2.7.2(typescript@5.6.2)
tseep: 1.2.2
typescript-lru-cache: 2.0.0
utf8-buffer: 1.0.0
websocket-polyfill: 1.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- typescript
- utf-8-validate
'@npmcli/fs@1.1.1':
dependencies:
'@gar/promisify': 1.1.3
@ -1960,8 +1880,6 @@ snapshots:
'@scure/base@1.1.1': {}
'@scure/base@1.1.9': {}
'@scure/bip32@1.3.1':
dependencies:
'@noble/curves': 1.1.0
@ -2360,8 +2278,6 @@ snapshots:
shebang-command: 1.2.0
which: 1.3.1
data-uri-to-buffer@4.0.1: {}
debug@2.6.9:
dependencies:
ms: 2.0.0
@ -2457,11 +2373,6 @@ snapshots:
dependencies:
reusify: 1.0.4
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
file-uri-to-path@1.0.0: {}
fill-range@7.1.1:
@ -2475,10 +2386,6 @@ snapshots:
follow-redirects@1.15.9: {}
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
fresh@0.5.2: {}
fs-constants@1.0.0: {}
@ -2766,10 +2673,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
light-bolt11-decoder@3.1.1:
dependencies:
'@scure/base': 1.1.1
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@ -2908,14 +2811,6 @@ snapshots:
node-addon-api@7.1.1: {}
node-domexception@1.0.0: {}
node-fetch@3.3.2:
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-gyp@8.4.1:
dependencies:
env-paths: 2.2.1
@ -3295,8 +3190,6 @@ snapshots:
touch@3.1.1: {}
tseep@1.2.2: {}
tslib@2.7.0: {}
tsscmp@1.0.6: {}
@ -3312,8 +3205,6 @@ snapshots:
media-typer: 0.3.0
mime-types: 2.1.35
typescript-lru-cache@2.0.0: {}
typescript@5.6.2: {}
undefsafe@2.0.5: {}
@ -3332,14 +3223,10 @@ snapshots:
universalify@0.1.2: {}
utf8-buffer@1.0.0: {}
util-deprecate@1.0.2: {}
vary@1.1.2: {}
web-streams-polyfill@3.3.3: {}
websocket-polyfill@1.0.0:
dependencies:
import2: 1.0.3

View File

@ -1,16 +1,11 @@
import { getServersFromServerListEvent, USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk";
import { NDKRelaySet } from "@nostr-dev-kit/ndk";
import ndk from "./ndk.js";
import { BLOSSOM_SERVERS, MAX_FILE_SIZE } from "./env.js";
import { makeRequestWithAbort } from "./helpers/http.js";
import pool from "./nostr.js";
export async function getUserBlossomServers(pubkey: string, relays?: string[]) {
const blossomServersEvent = await ndk.fetchEvent(
[{ kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] }],
{},
relays ? NDKRelaySet.fromRelayUrls(relays, ndk, true) : undefined,
);
export async function getUserBlossomServers(pubkey: string, relays: string[]) {
const blossomServersEvent = await pool.get(relays, { kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] });
return blossomServersEvent ? getServersFromServerListEvent(blossomServersEvent).map((u) => u.toString()) : undefined;
}

View File

@ -26,6 +26,14 @@ store?.on("error", (err) => {
const opts = store ? { store } : {};
/** domain -> pubkey */
export const userDomains = new Keyv({
...opts,
namespace: "domains",
// cache domains for an hour
ttl: 60 * 60 * 1000,
});
/** pubkey -> blossom servers */
export const userServers = new Keyv({
...opts,

View File

@ -1,13 +1,16 @@
import "dotenv/config";
import xbytes from "xbytes";
const LOOKUP_RELAYS = process.env.LOOKUP_RELAYS?.split(",") ?? ["wss://user.kindpag.es/", "wss://purplepag.es/"];
const NOSTR_RELAYS = process.env.NOSTR_RELAYS?.split(",") ?? [];
const BLOSSOM_SERVERS = process.env.BLOSSOM_SERVERS?.split(",") ?? [];
const LOOKUP_RELAYS = process.env.LOOKUP_RELAYS?.split(",").map((u) => u.trim()) ?? [
"wss://user.kindpag.es/",
"wss://purplepag.es/",
];
const SUBSCRIPTION_RELAYS = process.env.SUBSCRIPTION_RELAYS?.split(",").map((u) => u.trim()) ?? [];
const BLOSSOM_SERVERS = process.env.BLOSSOM_SERVERS?.split(",").map((u) => u.trim()) ?? [];
const MAX_FILE_SIZE = process.env.MAX_FILE_SIZE ? xbytes.parseSize(process.env.MAX_FILE_SIZE) : Infinity;
const NGINX_HOST = process.env.NGINX_HOST;
const NGINX_CACHE_DIR = process.env.NGINX_CACHE_DIR;
const CACHE_PATH = process.env.CACHE_PATH;
export { NOSTR_RELAYS, LOOKUP_RELAYS, BLOSSOM_SERVERS, MAX_FILE_SIZE, NGINX_HOST, CACHE_PATH };
export { SUBSCRIPTION_RELAYS, LOOKUP_RELAYS, BLOSSOM_SERVERS, MAX_FILE_SIZE, NGINX_CACHE_DIR, CACHE_PATH };

View File

@ -1,7 +1,6 @@
import { extname, isAbsolute, join } from "path";
import { NSITE_KIND } from "./const.js";
import ndk from "./ndk.js";
import { NDKRelaySet } from "@nostr-dev-kit/ndk";
import { requestEvents } from "./nostr.js";
export function getSearchPaths(path: string) {
const paths = [path];
@ -9,11 +8,6 @@ export function getSearchPaths(path: string) {
// if the path does not have an extension, also look for index.html
if (extname(path) === "") paths.push(join(path, "index.html"));
// also look for relative paths
for (const p of Array.from(paths)) {
if (isAbsolute(p)) paths.push(p.replace(/^\//, ""));
}
return paths.filter((p) => !!p);
}
@ -29,13 +23,10 @@ export function parseNsiteEvent(event: { pubkey: string; tags: string[][] }) {
};
}
export async function getNsiteBlobs(pubkey: string, path: string, relays?: string[]) {
const paths = getSearchPaths(path);
const events = await ndk.fetchEvents(
{ kinds: [NSITE_KIND], "#d": paths, authors: [pubkey] },
{},
relays && NDKRelaySet.fromRelayUrls(relays, ndk, true),
);
export async function getNsiteBlobs(pubkey: string, path: string, relays: string[]) {
// NOTE: hack, remove "/" paths since it breaks some relays
const paths = getSearchPaths(path).filter((p) => p !== "/");
const events = await requestEvents(relays, { kinds: [NSITE_KIND], "#d": paths, authors: [pubkey] });
return Array.from(events)
.map(parseNsiteEvent)

View File

@ -10,11 +10,13 @@ import mime from "mime";
import morgan from "koa-morgan";
import { resolveNpubFromHostname } from "./helpers/dns.js";
import { getNsiteBlobs } from "./events.js";
import { getNsiteBlobs, parseNsiteEvent } from "./events.js";
import { downloadFile, getUserBlossomServers } from "./blossom.js";
import { BLOSSOM_SERVERS } from "./env.js";
import { userRelays, userServers } from "./cache.js";
import { getUserOutboxes } from "./ndk.js";
import { BLOSSOM_SERVERS, NGINX_CACHE_DIR, SUBSCRIPTION_RELAYS } from "./env.js";
import { userDomains, userRelays, userServers } from "./cache.js";
import { NSITE_KIND } from "./const.js";
import { invalidatePubkeyPath } from "./nginx.js";
import pool, { getUserOutboxes, subscribeForEvents } from "./nostr.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -45,12 +47,29 @@ app.use(async (ctx, next) => {
}
});
// map pubkeys to folders in sites dir
// handle nsite requests
app.use(async (ctx, next) => {
const pubkey = (ctx.state.pubkey = await resolveNpubFromHostname(ctx.hostname));
let pubkey = await userDomains.get<string | undefined>(ctx.hostname);
// resolve pubkey if not in cache
if (!pubkey) {
console.log(`${ctx.hostname}: Resolving`);
pubkey = await resolveNpubFromHostname(ctx.hostname);
if (pubkey) {
await userDomains.set(ctx.hostname, pubkey);
console.log(`${ctx.hostname}: Found ${pubkey}`);
} else {
await userDomains.set(ctx.hostname, "");
}
}
if (pubkey) {
ctx.state.pubkey = pubkey;
let relays = await userRelays.get<string[] | undefined>(pubkey);
// fetch relays if not in cache
if (!relays) {
console.log(`${pubkey}: Fetching relays`);
@ -69,12 +88,15 @@ app.use(async (ctx, next) => {
const blobs = await getNsiteBlobs(pubkey, ctx.path, relays);
if (blobs.length === 0) {
console.log(`${pubkey}: Found 0 events`);
ctx.status = 404;
ctx.body = "Not Found";
return;
}
let servers = await userServers.get<string[] | undefined>(pubkey);
// fetch blossom servers if not in cache
if (!servers) {
console.log(`${pubkey}: Searching for blossom servers`);
servers = await getUserBlossomServers(pubkey, relays);
@ -88,6 +110,8 @@ app.use(async (ctx, next) => {
console.log(`${pubkey}: Failed to find servers`);
}
}
// always fetch from additional servers
servers.push(...BLOSSOM_SERVERS);
for (const blob of blobs) {
@ -121,12 +145,40 @@ try {
app.use(serve(www));
}
app.listen(process.env.PORT || 3000, () => {
console.log("Started on port", process.env.PORT || 3000);
app.listen(
{
port: process.env.NSITE_PORT || 3000,
host: process.env.NSITE_HOST || "0.0.0.0",
},
() => {
console.log("Started on port", process.env.PORT || 3000);
},
);
// invalidate nginx cache on new events
if (NGINX_CACHE_DIR && SUBSCRIPTION_RELAYS.length > 0) {
console.log(`Listening for new nsite events`);
subscribeForEvents(SUBSCRIPTION_RELAYS, async (event) => {
try {
const nsite = parseNsiteEvent(event);
if (nsite) {
console.log(`${nsite.pubkey}: Invalidating ${nsite.path}`);
await invalidatePubkeyPath(nsite.pubkey, nsite.path);
}
} catch (error) {
console.log(`Failed to invalidate ${event.id}`);
}
});
}
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
});
async function shutdown() {
console.log("Shutting down...");
pool.destroy();
process.exit(0);
}

View File

@ -1,17 +0,0 @@
import NDK from "@nostr-dev-kit/ndk";
import { LOOKUP_RELAYS, NOSTR_RELAYS } from "./env.js";
const ndk = new NDK({
explicitRelayUrls: [...LOOKUP_RELAYS, ...NOSTR_RELAYS],
});
ndk.connect();
export async function getUserOutboxes(pubkey: string) {
const mailboxes = await ndk.fetchEvent({ kinds: [10002], authors: [pubkey] });
if (!mailboxes) return;
return mailboxes.tags.filter((t) => t[0] === "r" && (t[2] === undefined || t[2] === "write")).map((t) => t[1]);
}
export default ndk;

View File

@ -1,26 +1,37 @@
import http from "node:http";
import { NGINX_HOST } from "./env.js";
import pfs from "node:fs/promises";
import crypto from "node:crypto";
import { join } from "node:path";
export function invalidateCache(host: string, path: string) {
if (!NGINX_HOST) return Promise.resolve(false);
import { NGINX_CACHE_DIR } from "./env.js";
import { userDomains } from "./cache.js";
return new Promise<boolean>((resolve, reject) => {
const req = http.request(
{
hostname: NGINX_HOST,
method: "GET",
port: 80,
path,
headers: {
Host: host,
},
},
(res) => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve(true);
else reject(new Error("Failed to invalidate"));
},
);
export async function invalidatePubkeyPath(pubkey: string, path: string) {
const iterator = userDomains.iterator?.(undefined);
if (!iterator) return;
req.end();
});
const promises: Promise<boolean | undefined>[] = [];
for await (const [domain, key] of iterator) {
if (key === pubkey) {
promises.push(invalidateNginxCache(domain, path));
}
}
await Promise.allSettled(promises);
}
export async function invalidateNginxCache(host: string, path: string) {
if (!NGINX_CACHE_DIR) return Promise.resolve(false);
try {
const key = `${host}${path}`;
const md5 = crypto.createHash("md5").update(key).digest("hex");
// NOTE: hard coded to cache levels 1:2
const cachePath = join(NGINX_CACHE_DIR, md5.slice(-1), md5.slice(-3, -1), md5);
await pfs.rm(cachePath);
console.log(`Invalidated ${key} (${md5})`);
} catch (error) {
// ignore errors
}
}

38
src/nostr.ts Normal file
View File

@ -0,0 +1,38 @@
import { Filter, NostrEvent, SimplePool } from "nostr-tools";
import { LOOKUP_RELAYS } from "./env.js";
import { NSITE_KIND } from "./const.js";
const pool = new SimplePool();
export async function getUserOutboxes(pubkey: string) {
const mailboxes = await pool.get(LOOKUP_RELAYS, { kinds: [10002], authors: [pubkey] });
if (!mailboxes) return;
return mailboxes.tags.filter((t) => t[0] === "r" && (t[2] === undefined || t[2] === "write")).map((t) => t[1]);
}
export function subscribeForEvents(relays: string[], onevent: (event: NostrEvent) => any) {
return pool.subscribeMany(relays, [{ kinds: [NSITE_KIND], since: Math.round(Date.now() / 1000) - 60 * 60 }], {
onevent,
});
}
export function requestEvents(relays: string[], filter: Filter) {
return new Promise<NostrEvent[]>(async (res, rej) => {
const events: NostrEvent[] = [];
await Promise.allSettled(relays.map((url) => pool.ensureRelay(url).catch((e) => {})));
const sub = pool.subscribeMany(relays, [filter], {
onevent: (e) => events.push(e),
oneose: () => sub.close(),
onclose: (reasons) => {
const errs = reasons.filter((r) => r !== "closed by caller");
if (errs.length > 0 && events.length === 0) rej(new Error(errs.join(", ")));
else res(events);
},
});
});
}
export default pool;

22
supervisord.conf Normal file
View File

@ -0,0 +1,22 @@
[supervisord]
nodaemon=true
user=root
[program:nginx]
command=nginx -g "daemon off;"
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nsite]
command=node /app
autostart=true
autorestart=true
user=root
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0