bump dependencies
This commit is contained in:
hzrd149 2025-03-17 17:35:11 +00:00
parent b2b8e0108e
commit d87497e6c0
8 changed files with 796 additions and 691 deletions

View File

@ -14,7 +14,7 @@ BLOSSOM_SERVERS=https://nostr.download,https://cdn.satellite.earth
# The max file size to serve # The max file size to serve
MAX_FILE_SIZE='2 MB' MAX_FILE_SIZE='2 MB'
# The cache folder for nginx # The cache folder for nginx (used for cache invalidation)
NGINX_CACHE_DIR='/var/nginx/cache' NGINX_CACHE_DIR='/var/nginx/cache'
# A nprofile pointer for an nsite to use as the default homepage # A nprofile pointer for an nsite to use as the default homepage

View File

@ -19,32 +19,32 @@
"public" "public"
], ],
"dependencies": { "dependencies": {
"@keyv/redis": "^3.0.1", "@keyv/redis": "^4.3.2",
"@keyv/sqlite": "^4.0.1", "@keyv/sqlite": "^4.0.1",
"@koa/cors": "^5.0.0", "@koa/cors": "^5.0.0",
"blossom-client-sdk": "^2.1.1", "blossom-client-sdk": "^3.0.1",
"debug": "^4.4.0", "debug": "^4.4.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"follow-redirects": "^1.15.9", "follow-redirects": "^1.15.9",
"keyv": "^5.2.3", "keyv": "^5.3.2",
"koa": "^2.15.3", "koa": "^2.16.0",
"koa-morgan": "^1.0.1", "koa-morgan": "^1.0.1",
"koa-send": "^5.0.1", "koa-send": "^5.0.1",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"mime": "^4.0.6", "mime": "^4.0.6",
"nostr-tools": "^2.10.4", "nostr-tools": "^2.11.0",
"nsite-cli": "^0.1.14", "nsite-cli": "^0.1.16",
"pac-proxy-agent": "^7.1.0", "pac-proxy-agent": "^7.2.0",
"proxy-agent": "^6.5.0", "proxy-agent": "^6.5.0",
"puppeteer": "^23.11.1", "puppeteer": "^23.11.1",
"websocket-polyfill": "1.0.0", "websocket-polyfill": "1.0.0",
"ws": "^8.18.0", "ws": "^8.18.1",
"xbytes": "^1.9.1" "xbytes": "^1.9.1"
}, },
"devDependencies": { "devDependencies": {
"@changesets/cli": "^2.27.11", "@changesets/cli": "^2.28.1",
"@swc-node/register": "^1.10.9", "@swc-node/register": "^1.10.10",
"@swc/core": "^1.10.9", "@swc/core": "^1.11.10",
"@types/better-sqlite3": "^7.6.12", "@types/better-sqlite3": "^7.6.12",
"@types/debug": "^4.1.12", "@types/debug": "^4.1.12",
"@types/follow-redirects": "^1.14.4", "@types/follow-redirects": "^1.14.4",
@ -54,12 +54,12 @@
"@types/koa-static": "^4.0.4", "@types/koa-static": "^4.0.4",
"@types/koa__cors": "^5.0.0", "@types/koa__cors": "^5.0.0",
"@types/koa__router": "^12.0.4", "@types/koa__router": "^12.0.4",
"@types/node": "^20.17.14", "@types/node": "^20.17.24",
"@types/proxy-from-env": "^1.0.4", "@types/proxy-from-env": "^1.0.4",
"@types/ws": "^8.5.13", "@types/ws": "^8.18.0",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"prettier": "^3.4.2", "prettier": "^3.5.3",
"typescript": "^5.7.3" "typescript": "^5.8.2"
}, },
"resolutions": { "resolutions": {
"websocket-polyfill": "1.0.0" "websocket-polyfill": "1.0.0"

1184
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,13 @@
import { IncomingMessage } from "node:http"; import { IncomingMessage } from "node:http";
import { getServersFromServerListEvent, USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk";
import { BLOSSOM_SERVERS, MAX_FILE_SIZE } from "./env.js"; import { BLOSSOM_SERVERS, MAX_FILE_SIZE } from "./env.js";
import { makeRequestWithAbort } from "./helpers/http.js"; import { makeRequestWithAbort } from "./helpers/http.js";
import pool from "./nostr.js";
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;
}
/** /**
* Downloads a file from multiple servers * Downloads a file from multiple servers
* @todo download the file to /tmp and verify it * @todo download the file to /tmp and verify it
*/ */
export function downloadFile(sha256: string, servers = BLOSSOM_SERVERS): Promise<IncomingMessage> { export function downloadBlob(sha256: string, servers = BLOSSOM_SERVERS): Promise<IncomingMessage> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const controllers = new Map<string, AbortController>(); const controllers = new Map<string, AbortController>();

View File

@ -1,4 +1,4 @@
import { extname, isAbsolute, join } from "path"; import { extname, join } from "path";
import { NSITE_KIND } from "./const.js"; import { NSITE_KIND } from "./const.js";
import { requestEvents } from "./nostr.js"; import { requestEvents } from "./nostr.js";
@ -11,7 +11,7 @@ export function getSearchPaths(path: string) {
return paths.filter((p) => !!p); return paths.filter((p) => !!p);
} }
export function parseNsiteEvent(event: { pubkey: string; tags: string[][] }) { export function parseNsiteEvent(event: { pubkey: string; tags: string[][]; created_at: number }) {
const path = event.tags.find((t) => t[0] === "d" && t[1])?.[1]; const path = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
const sha256 = event.tags.find((t) => t[0] === "x" && t[1])?.[1]; const sha256 = event.tags.find((t) => t[0] === "x" && t[1])?.[1];
@ -20,6 +20,7 @@ export function parseNsiteEvent(event: { pubkey: string; tags: string[][] }) {
pubkey: event.pubkey, pubkey: event.pubkey,
path: join("/", path), path: join("/", path),
sha256, sha256,
created_at: event.created_at,
}; };
} }

View File

@ -14,13 +14,12 @@ import { spawn } from "node:child_process";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { resolveNpubFromHostname } from "./helpers/dns.js"; import { resolveNpubFromHostname } from "./helpers/dns.js";
import { getNsiteBlobs, parseNsiteEvent } from "./events.js"; import { getNsiteBlobs } from "./events.js";
import { downloadFile, getUserBlossomServers } from "./blossom.js"; import { downloadBlob } from "./blossom.js";
import { import {
BLOSSOM_SERVERS, BLOSSOM_SERVERS,
ENABLE_SCREENSHOTS, ENABLE_SCREENSHOTS,
HOST, HOST,
NGINX_CACHE_DIR,
NSITE_HOMEPAGE, NSITE_HOMEPAGE,
NSITE_HOMEPAGE_DIR, NSITE_HOMEPAGE_DIR,
NSITE_HOST, NSITE_HOST,
@ -29,9 +28,9 @@ import {
SUBSCRIPTION_RELAYS, SUBSCRIPTION_RELAYS,
} from "./env.js"; } from "./env.js";
import { userDomains, userRelays, userServers } from "./cache.js"; import { userDomains, userRelays, userServers } from "./cache.js";
import { invalidatePubkeyPath } from "./nginx.js"; import pool, { getUserBlossomServers, getUserOutboxes } from "./nostr.js";
import pool, { getUserOutboxes, subscribeForEvents } from "./nostr.js";
import logger from "./logger.js"; import logger from "./logger.js";
import { watchInvalidation } from "./invalidation.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -79,7 +78,8 @@ app.use(async (ctx, next) => {
} }
} }
if (pubkey) { if (!pubkey) return await next();
const npub = npubEncode(pubkey); const npub = npubEncode(pubkey);
const log = logger.extend(npub); const log = logger.extend(npub);
ctx.state.pubkey = pubkey; ctx.state.pubkey = pubkey;
@ -101,6 +101,7 @@ app.use(async (ctx, next) => {
} }
} }
// always check subscription relays
relays.push(...SUBSCRIPTION_RELAYS); relays.push(...SUBSCRIPTION_RELAYS);
if (relays.length === 0) throw new Error("No nostr relays"); if (relays.length === 0) throw new Error("No nostr relays");
@ -142,16 +143,15 @@ app.use(async (ctx, next) => {
servers.push(...BLOSSOM_SERVERS); servers.push(...BLOSSOM_SERVERS);
for (const blob of blobs) { for (const blob of blobs) {
const res = await downloadFile(blob.sha256, servers); const res = await downloadBlob(blob.sha256, servers);
if (!res) continue;
if (res) {
const type = mime.getType(blob.path); const type = mime.getType(blob.path);
if (type) ctx.set("content-type", type); if (type) ctx.set("content-type", type);
else if (res.headers["content-type"]) ctx.set("content-type", res.headers["content-type"]); else if (res.headers["content-type"]) ctx.set("content-type", res.headers["content-type"]);
// pass headers along // pass headers along
if (res.headers["content-length"]) ctx.set("content-length", res.headers["content-length"]); if (res.headers["content-length"]) ctx.set("content-length", res.headers["content-length"]);
if (res.headers["last-modified"]) ctx.set("last-modified", res.headers["last-modified"]);
// set Onion-Location header // set Onion-Location header
if (ONION_HOST) { if (ONION_HOST) {
@ -160,15 +160,18 @@ app.use(async (ctx, next) => {
ctx.set("Onion-Location", url.toString().replace(/\/$/, "")); ctx.set("Onion-Location", url.toString().replace(/\/$/, ""));
} }
// add cache headers
ctx.set("ETag", res.headers["etag"] || `"${blob.sha256}"`);
ctx.set("Cache-Control", "public, max-age=3600");
ctx.set("Last-Modified", res.headers["last-modified"] || new Date(blob.created_at * 1000).toUTCString());
ctx.status = 200; ctx.status = 200;
ctx.body = res; ctx.body = res;
return; return;
} }
}
ctx.status = 500; ctx.status = 500;
ctx.body = "Failed to find blob"; ctx.body = "Failed to find blob";
} else await next();
}); });
if (ONION_HOST) { if (ONION_HOST) {
@ -247,35 +250,13 @@ try {
app.use(serve(www, serveOptions)); app.use(serve(www, serveOptions));
} }
// start the server
app.listen({ host: NSITE_HOST, port: NSITE_PORT }, () => { app.listen({ host: NSITE_HOST, port: NSITE_PORT }, () => {
logger("Started on port", HOST); logger("Started on port", HOST);
}); });
// invalidate nginx cache and screenshots on new events // watch for invalidations
if (SUBSCRIPTION_RELAYS.length > 0) { watchInvalidation();
logger(`Listening for new nsite events on: ${SUBSCRIPTION_RELAYS.join(", ")}`);
subscribeForEvents(SUBSCRIPTION_RELAYS, async (event) => {
try {
const nsite = parseNsiteEvent(event);
if (nsite) {
const log = logger.extend(nip19.npubEncode(nsite.pubkey));
if (NGINX_CACHE_DIR) {
log(`Invalidating ${nsite.path}`);
await invalidatePubkeyPath(nsite.pubkey, nsite.path);
}
// invalidate screenshot for nsite
if (ENABLE_SCREENSHOTS && (nsite.path === "/" || nsite.path === "/index.html")) {
const { removeScreenshot } = await import("./screenshots.js");
await removeScreenshot(nsite.pubkey);
}
}
} catch (error) {
console.log(`Failed to invalidate ${event.id}`);
}
});
}
process.on("unhandledRejection", (reason, promise) => { process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason); console.error("Unhandled Rejection at:", promise, "reason:", reason);

38
src/invalidation.ts Normal file
View File

@ -0,0 +1,38 @@
import { nip19 } from "nostr-tools";
import { ENABLE_SCREENSHOTS, NGINX_CACHE_DIR, SUBSCRIPTION_RELAYS } from "./env.js";
import { parseNsiteEvent } from "./events.js";
import pool from "./nostr.js";
import { invalidatePubkeyPath } from "./nginx.js";
import { NSITE_KIND } from "./const.js";
import logger from "./logger.js";
export function watchInvalidation() {
// invalidate nginx cache and screenshots on new events
if (SUBSCRIPTION_RELAYS.length > 0) {
logger(`Listening for new nsite events on: ${SUBSCRIPTION_RELAYS.join(", ")}`);
pool.subscribeMany(SUBSCRIPTION_RELAYS, [{ kinds: [NSITE_KIND], since: Math.round(Date.now() / 1000) - 60 * 60 }], {
onevent: async (event) => {
try {
const nsite = parseNsiteEvent(event);
if (nsite) {
const log = logger.extend(nip19.npubEncode(nsite.pubkey));
if (NGINX_CACHE_DIR) {
log(`Invalidating ${nsite.path}`);
await invalidatePubkeyPath(nsite.pubkey, nsite.path);
}
// invalidate screenshot for nsite
if (ENABLE_SCREENSHOTS && (nsite.path === "/" || nsite.path === "/index.html")) {
const { removeScreenshot } = await import("./screenshots.js");
await removeScreenshot(nsite.pubkey);
}
}
} catch (error) {
console.log(`Failed to invalidate ${event.id}`);
}
},
});
}
}

View File

@ -1,6 +1,7 @@
import { Filter, NostrEvent, SimplePool } from "nostr-tools"; import { Filter, NostrEvent, SimplePool } from "nostr-tools";
import { getServersFromServerListEvent, USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk";
import { LOOKUP_RELAYS } from "./env.js"; import { LOOKUP_RELAYS } from "./env.js";
import { NSITE_KIND } from "./const.js";
const pool = new SimplePool(); const pool = new SimplePool();
@ -11,10 +12,10 @@ export async function getUserOutboxes(pubkey: string) {
return mailboxes.tags.filter((t) => t[0] === "r" && (t[2] === undefined || t[2] === "write")).map((t) => t[1]); 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) { export async function getUserBlossomServers(pubkey: string, relays: string[]) {
return pool.subscribeMany(relays, [{ kinds: [NSITE_KIND], since: Math.round(Date.now() / 1000) - 60 * 60 }], { const blossomServersEvent = await pool.get(relays, { kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] });
onevent,
}); return blossomServersEvent ? getServersFromServerListEvent(blossomServersEvent).map((u) => u.toString()) : undefined;
} }
export function requestEvents(relays: string[], filter: Filter) { export function requestEvents(relays: string[], filter: Filter) {