diff --git a/.changeset/many-lemons-exercise.md b/.changeset/many-lemons-exercise.md new file mode 100644 index 0000000..291dd6a --- /dev/null +++ b/.changeset/many-lemons-exercise.md @@ -0,0 +1,5 @@ +--- +"nsite-ts": minor +--- + +Bundle nginx in docker image diff --git a/.changeset/metal-wasps-cry.md b/.changeset/metal-wasps-cry.md new file mode 100644 index 0000000..58d4b0a --- /dev/null +++ b/.changeset/metal-wasps-cry.md @@ -0,0 +1,5 @@ +--- +"nsite-ts": minor +--- + +Add NGINX_CACHE_DIR for invalidating nginx cache diff --git a/.changeset/popular-plants-beam.md b/.changeset/popular-plants-beam.md new file mode 100644 index 0000000..4baf79d --- /dev/null +++ b/.changeset/popular-plants-beam.md @@ -0,0 +1,5 @@ +--- +"nsite-ts": minor +--- + +Add SUBSCRIPTION_RELAYS for listening for new events diff --git a/.env.example b/.env.example index 4e33d8b..5de0c88 100644 --- a/.env.example +++ b/.env.example @@ -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' diff --git a/Dockerfile b/Dockerfile index 5e41345..1b30cdc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/docker-compose.yml b/docker-compose.yml index 3035b7c..219a8db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..dcb5821 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +chown -R nginx:nginx /var/cache/nginx + +exec "$@" diff --git a/nginx/default.conf b/nginx/default.conf index e38b330..5248e86 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -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; - # } } diff --git a/nginx/nginx.conf b/nginx/nginx.conf index e3236e3..5a19bdf 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -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; diff --git a/package.json b/package.json index 2803b96..adf632f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb1127b..948cc13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/blossom.ts b/src/blossom.ts index 1d4b645..ae727b4 100644 --- a/src/blossom.ts +++ b/src/blossom.ts @@ -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; } diff --git a/src/cache.ts b/src/cache.ts index b8c6b13..8f009b1 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -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, diff --git a/src/env.ts b/src/env.ts index 57cdc4f..9f6e3ed 100644 --- a/src/env.ts +++ b/src/env.ts @@ -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 }; diff --git a/src/events.ts b/src/events.ts index 417b103..cf7ba74 100644 --- a/src/events.ts +++ b/src/events.ts @@ -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) diff --git a/src/index.ts b/src/index.ts index 8381fa1..00a7862 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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(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(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(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); } diff --git a/src/ndk.ts b/src/ndk.ts deleted file mode 100644 index 4a00495..0000000 --- a/src/ndk.ts +++ /dev/null @@ -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; diff --git a/src/nginx.ts b/src/nginx.ts index d3dc05f..7184fc7 100644 --- a/src/nginx.ts +++ b/src/nginx.ts @@ -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((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[] = []; + 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 + } } diff --git a/src/nostr.ts b/src/nostr.ts new file mode 100644 index 0000000..fa05ac9 --- /dev/null +++ b/src/nostr.ts @@ -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(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; diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 0000000..e720627 --- /dev/null +++ b/supervisord.conf @@ -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