From cbe156ae2b7e755dbc75d162585f1e222cc499b1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 16:26:37 -0600 Subject: [PATCH 01/13] Create @ditto/config module --- deno.json | 1 + packages/config/DittoConfig.ts | 488 ++++++++++++++++++ .../{ditto/utils => config}/crypto.test.ts | 2 +- packages/{ditto/utils => config}/crypto.ts | 0 packages/config/deno.json | 7 + packages/config/mod.ts | 1 + packages/ditto/config.ts | 397 +------------- 7 files changed, 501 insertions(+), 395 deletions(-) create mode 100644 packages/config/DittoConfig.ts rename packages/{ditto/utils => config}/crypto.test.ts (92%) rename packages/{ditto/utils => config}/crypto.ts (100%) create mode 100644 packages/config/deno.json create mode 100644 packages/config/mod.ts diff --git a/deno.json b/deno.json index ee6868d9..1a12f40e 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,6 @@ { "workspace": [ + "./packages/config", "./packages/ditto" ], "tasks": { diff --git a/packages/config/DittoConfig.ts b/packages/config/DittoConfig.ts new file mode 100644 index 00000000..01ba4dce --- /dev/null +++ b/packages/config/DittoConfig.ts @@ -0,0 +1,488 @@ +import os from 'node:os'; +import ISO6391, { type LanguageCode } from 'iso-639-1'; +import { getPublicKey, nip19 } from 'nostr-tools'; +import { z } from 'zod'; +import { decodeBase64 } from '@std/encoding/base64'; +import { encodeBase64Url } from '@std/encoding/base64url'; + +import { getEcdsaPublicKey } from './crypto.ts'; + +/** Ditto application-wide configuration. */ +export class DittoConfig { + constructor(private env: { get(key: string): string | undefined }) {} + + /** Cached parsed admin pubkey value. */ + private _pubkey: string | undefined; + + /** Cached parsed VAPID public key value. */ + private _vapidPublicKey: Promise | undefined; + + /** Ditto admin secret key in nip19 format. This is the way it's configured by an admin. */ + get nsec(): `nsec1${string}` { + const value = this.env.get('DITTO_NSEC'); + if (!value) { + throw new Error('Missing DITTO_NSEC'); + } + if (!value.startsWith('nsec1')) { + throw new Error('Invalid DITTO_NSEC'); + } + return value as `nsec1${string}`; + } + + /** Ditto admin secret key in hex format. */ + get seckey(): Uint8Array { + return nip19.decode(this.nsec).data; + } + + /** Ditto admin public key in hex format. */ + get pubkey(): string { + if (!this._pubkey) { + this._pubkey = getPublicKey(this.seckey); + } + return this._pubkey; + } + + /** Port to use when serving the HTTP server. */ + get port(): number { + return parseInt(this.env.get('PORT') || '4036'); + } + + /** IP addresses not affected by rate limiting. */ + get ipWhitelist(): string[] { + return this.env.get('IP_WHITELIST')?.split(',') || []; + } + + /** Relay URL to the Ditto server's relay. */ + get relay(): `wss://${string}` | `ws://${string}` { + const { protocol, host } = this.url; + return `${protocol === 'https:' ? 'wss:' : 'ws:'}//${host}/relay`; + } + + /** Relay to use for NIP-50 `search` queries. */ + get searchRelay(): string | undefined { + return this.env.get('SEARCH_RELAY'); + } + + /** Origin of the Ditto server, including the protocol and port. */ + get localDomain(): string { + return this.env.get('LOCAL_DOMAIN') || `http://localhost:${this.port}`; + } + + /** Link to an external nostr viewer. */ + get externalDomain(): string { + return this.env.get('NOSTR_EXTERNAL') || 'https://njump.me'; + } + + /** Get a link to a nip19-encoded entity in the configured external viewer. */ + external(path: string) { + return new URL(path, this.externalDomain).toString(); + } + + /** + * Heroku-style database URL. This is used in production to connect to the + * database. + * + * Follows the format: + * + * ```txt + * protocol://username:password@host:port/database_name + * ``` + */ + get databaseUrl(): string { + return this.env.get('DATABASE_URL') ?? 'file://data/pgdata'; + } + + /** PGlite debug level. 0 disables logging. */ + get pgliteDebug(): 0 | 1 | 2 | 3 | 4 | 5 { + return Number(this.env.get('PGLITE_DEBUG') || 0) as 0 | 1 | 2 | 3 | 4 | 5; + } + + get vapidPublicKey(): Promise { + if (!this._vapidPublicKey) { + this._vapidPublicKey = (async () => { + const keys = await this.vapidKeys; + if (keys) { + const { publicKey } = keys; + const bytes = await crypto.subtle.exportKey('raw', publicKey); + return encodeBase64Url(bytes); + } + })(); + } + + return this._vapidPublicKey; + } + + get vapidKeys(): Promise { + return (async () => { + const encoded = this.env.get('VAPID_PRIVATE_KEY'); + + if (!encoded) { + return; + } + + const keyData = decodeBase64(encoded); + + const privateKey = await crypto.subtle.importKey( + 'pkcs8', + keyData, + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign'], + ); + const publicKey = await getEcdsaPublicKey(privateKey, true); + + return { privateKey, publicKey }; + })(); + } + + get db(): { timeouts: { default: number; relay: number; timelines: number } } { + const env = this.env; + return { + /** Database query timeout configurations. */ + timeouts: { + /** Default query timeout when another setting isn't more specific. */ + get default(): number { + return Number(env.get('DB_TIMEOUT_DEFAULT') || 5_000); + }, + /** Timeout used for queries made through the Nostr relay. */ + get relay(): number { + return Number(env.get('DB_TIMEOUT_RELAY') || 1_000); + }, + /** Timeout used for timelines such as home, notifications, hashtag, etc. */ + get timelines(): number { + return Number(env.get('DB_TIMEOUT_TIMELINES') || 15_000); + }, + }, + }; + } + + /** Time-to-live for captchas in milliseconds. */ + get captchaTTL(): number { + return Number(this.env.get('CAPTCHA_TTL') || 5 * 60 * 1000); + } + + /** Character limit to enforce for posts made through Mastodon API. */ + get postCharLimit(): number { + return Number(this.env.get('POST_CHAR_LIMIT') || 5000); + } + + /** S3 media storage configuration. */ + get s3(): { + endPoint?: string; + region?: string; + accessKey?: string; + secretKey?: string; + bucket?: string; + pathStyle?: boolean; + port?: number; + sessionToken?: string; + useSSL?: boolean; + } { + const env = this.env; + + return { + get endPoint(): string | undefined { + return env.get('S3_ENDPOINT'); + }, + get region(): string | undefined { + return env.get('S3_REGION'); + }, + get accessKey(): string | undefined { + return env.get('S3_ACCESS_KEY'); + }, + get secretKey(): string | undefined { + return env.get('S3_SECRET_KEY'); + }, + get bucket(): string | undefined { + return env.get('S3_BUCKET'); + }, + get pathStyle(): boolean | undefined { + return optionalBooleanSchema.parse(env.get('S3_PATH_STYLE')); + }, + get port(): number | undefined { + return optionalNumberSchema.parse(env.get('S3_PORT')); + }, + get sessionToken(): string | undefined { + return env.get('S3_SESSION_TOKEN'); + }, + get useSSL(): boolean | undefined { + return optionalBooleanSchema.parse(env.get('S3_USE_SSL')); + }, + }; + } + + /** IPFS uploader configuration. */ + get ipfs(): { apiUrl: string } { + const env = this.env; + + return { + /** Base URL for private IPFS API calls. */ + get apiUrl(): string { + return env.get('IPFS_API_URL') || 'http://localhost:5001'; + }, + }; + } + + /** nostr.build API endpoint when the `nostrbuild` uploader is used. */ + get nostrbuildEndpoint(): string { + return this.env.get('NOSTRBUILD_ENDPOINT') || 'https://nostr.build/api/v2/upload/files'; + } + + /** Default Blossom servers to use when the `blossom` uploader is set. */ + get blossomServers(): string[] { + return this.env.get('BLOSSOM_SERVERS')?.split(',') || ['https://blossom.primal.net/']; + } + + /** Module to upload files with. */ + get uploader(): string | undefined { + return this.env.get('DITTO_UPLOADER'); + } + + /** Location to use for local uploads. */ + get uploadsDir(): string { + return this.env.get('UPLOADS_DIR') || 'data/uploads'; + } + + /** Media base URL for uploads. */ + get mediaDomain(): string { + const value = this.env.get('MEDIA_DOMAIN'); + + if (!value) { + const url = this.url; + url.host = `media.${url.host}`; + return url.toString(); + } + + return value; + } + + /** + * Whether to analyze media metadata with [blurhash](https://www.npmjs.com/package/blurhash) and [sharp](https://www.npmjs.com/package/sharp). + * This is prone to security vulnerabilities, which is why it's not enabled by default. + */ + get mediaAnalyze(): boolean { + return optionalBooleanSchema.parse(this.env.get('MEDIA_ANALYZE')) ?? false; + } + + /** Max upload size for files in number of bytes. Default 100MiB. */ + get maxUploadSize(): number { + return Number(this.env.get('MAX_UPLOAD_SIZE') || 100 * 1024 * 1024); + } + + /** Usernames that regular users cannot sign up with. */ + get forbiddenUsernames(): string[] { + return this.env.get('FORBIDDEN_USERNAMES')?.split(',') || [ + '_', + 'admin', + 'administrator', + 'root', + 'sysadmin', + 'system', + ]; + } + + /** Domain of the Ditto server as a `URL` object, for easily grabbing the `hostname`, etc. */ + get url(): URL { + return new URL(this.localDomain); + } + + /** Merges the path with the localDomain. */ + local(path: string): string { + return mergePaths(this.localDomain, path); + } + + /** URL to send Sentry errors to. */ + get sentryDsn(): string | undefined { + return this.env.get('SENTRY_DSN'); + } + + /** Postgres settings. */ + get pg(): { poolSize: number } { + const env = this.env; + + return { + /** Number of connections to use in the pool. */ + get poolSize(): number { + return Number(env.get('PG_POOL_SIZE') ?? 20); + }, + }; + } + + /** Whether to enable requesting events from known relays. */ + get firehoseEnabled(): boolean { + return optionalBooleanSchema.parse(this.env.get('FIREHOSE_ENABLED')) ?? true; + } + + /** Number of events the firehose is allowed to process at one time before they have to wait in a queue. */ + get firehoseConcurrency(): number { + return Math.ceil(Number(this.env.get('FIREHOSE_CONCURRENCY') ?? 1)); + } + + /** Nostr event kinds of events to listen for on the firehose. */ + get firehoseKinds(): number[] { + return (this.env.get('FIREHOSE_KINDS') ?? '0, 1, 3, 5, 6, 7, 20, 9735, 10002') + .split(/[, ]+/g) + .map(Number); + } + + /** + * Whether Ditto should subscribe to Nostr events from the Postgres database itself. + * This would make Nostr events inserted directly into Postgres available to the streaming API and relay. + */ + get notifyEnabled(): boolean { + return optionalBooleanSchema.parse(this.env.get('NOTIFY_ENABLED')) ?? true; + } + + /** Whether to enable Ditto cron jobs. */ + get cronEnabled(): boolean { + return optionalBooleanSchema.parse(this.env.get('CRON_ENABLED')) ?? true; + } + + /** User-Agent to use when fetching link previews. Pretend to be Facebook by default. */ + get fetchUserAgent(): string { + return this.env.get('DITTO_FETCH_USER_AGENT') ?? 'facebookexternalhit'; + } + + /** Path to the custom policy module. Must be an absolute path, https:, npm:, or jsr: URI. */ + get policy(): string { + return this.env.get('DITTO_POLICY') || new URL('../data/policy.ts', import.meta.url).pathname; + } + + /** Absolute path to the data directory used by Ditto. */ + get dataDir(): string { + return this.env.get('DITTO_DATA_DIR') || new URL('../data', import.meta.url).pathname; + } + + /** Absolute path of the Deno directory. */ + get denoDir(): string { + return this.env.get('DENO_DIR') || `${os.userInfo().homedir}/.cache/deno`; + } + + /** Whether zap splits should be enabled. */ + get zapSplitsEnabled(): boolean { + return optionalBooleanSchema.parse(this.env.get('ZAP_SPLITS_ENABLED')) ?? false; + } + + /** Languages this server wishes to highlight. Used when querying trends.*/ + get preferredLanguages(): LanguageCode[] | undefined { + return this.env.get('DITTO_LANGUAGES')?.split(',')?.filter(ISO6391.validate); + } + + /** Mints to be displayed in the UI when the user decides to create a wallet.*/ + get cashuMints(): string[] { + return this.env.get('CASHU_MINTS')?.split(',') ?? []; + } + + /** Translation provider used to translate posts. */ + get translationProvider(): string | undefined { + return this.env.get('TRANSLATION_PROVIDER'); + } + + /** DeepL URL endpoint. */ + get deeplBaseUrl(): string | undefined { + return this.env.get('DEEPL_BASE_URL'); + } + + /** DeepL API KEY. */ + get deeplApiKey(): string | undefined { + return this.env.get('DEEPL_API_KEY'); + } + + /** LibreTranslate URL endpoint. */ + get libretranslateBaseUrl(): string | undefined { + return this.env.get('LIBRETRANSLATE_BASE_URL'); + } + + /** LibreTranslate API KEY. */ + get libretranslateApiKey(): string | undefined { + return this.env.get('LIBRETRANSLATE_API_KEY'); + } + + /** Cache settings. */ + get caches(): { + nip05: { max: number; ttl: number }; + favicon: { max: number; ttl: number }; + linkPreview: { max: number; ttl: number }; + translation: { max: number; ttl: number }; + } { + const env = this.env; + + return { + /** NIP-05 cache settings. */ + get nip05(): { max: number; ttl: number } { + return { + max: Number(env.get('DITTO_CACHE_NIP05_MAX') || 3000), + ttl: Number(env.get('DITTO_CACHE_NIP05_TTL') || 1 * 60 * 60 * 1000), + }; + }, + /** Favicon cache settings. */ + get favicon(): { max: number; ttl: number } { + return { + max: Number(env.get('DITTO_CACHE_FAVICON_MAX') || 500), + ttl: Number(env.get('DITTO_CACHE_FAVICON_TTL') || 1 * 60 * 60 * 1000), + }; + }, + /** Link preview cache settings. */ + get linkPreview(): { max: number; ttl: number } { + return { + max: Number(env.get('DITTO_CACHE_LINK_PREVIEW_MAX') || 3000), + ttl: Number(env.get('DITTO_CACHE_LINK_PREVIEW_TTL') || 12 * 60 * 60 * 1000), + }; + }, + /** Translation cache settings. */ + get translation(): { max: number; ttl: number } { + return { + max: Number(env.get('DITTO_CACHE_TRANSLATION_MAX') || 1000), + ttl: Number(env.get('DITTO_CACHE_TRANSLATION_TTL') || 6 * 60 * 60 * 1000), + }; + }, + }; + } + + /** Custom profile fields configuration. */ + get profileFields(): { maxFields: number; nameLength: number; valueLength: number } { + const env = this.env; + + return { + get maxFields(): number { + return Number(env.get('PROFILE_FIELDS_MAX_FIELDS') || 10); + }, + get nameLength(): number { + return Number(env.get('PROFILE_FIELDS_NAME_LENGTH') || 255); + }, + get valueLength(): number { + return Number(env.get('PROFILE_FIELDS_VALUE_LENGTH') || 2047); + }, + }; + } + + /** Maximum time between events before a streak is broken, *in seconds*. */ + get streakWindow(): number { + return Number(this.env.get('STREAK_WINDOW') || 129600); + } +} + +const optionalBooleanSchema = z + .enum(['true', 'false']) + .optional() + .transform((value) => value !== undefined ? value === 'true' : undefined); + +const optionalNumberSchema = z + .string() + .optional() + .transform((value) => value !== undefined ? Number(value) : undefined); + +function mergePaths(base: string, path: string) { + const url = new URL( + path.startsWith('/') ? path : new URL(path).pathname, + base, + ); + + if (!path.startsWith('/')) { + // Copy query parameters from the original URL to the new URL + const originalUrl = new URL(path); + url.search = originalUrl.search; + } + + return url.toString(); +} diff --git a/packages/ditto/utils/crypto.test.ts b/packages/config/crypto.test.ts similarity index 92% rename from packages/ditto/utils/crypto.test.ts rename to packages/config/crypto.test.ts index d2b444a1..b3f758eb 100644 --- a/packages/ditto/utils/crypto.test.ts +++ b/packages/config/crypto.test.ts @@ -1,6 +1,6 @@ import { assertEquals } from '@std/assert'; -import { getEcdsaPublicKey } from '@/utils/crypto.ts'; +import { getEcdsaPublicKey } from './crypto.ts'; Deno.test('getEcdsaPublicKey', async () => { const { publicKey, privateKey } = await crypto.subtle.generateKey( diff --git a/packages/ditto/utils/crypto.ts b/packages/config/crypto.ts similarity index 100% rename from packages/ditto/utils/crypto.ts rename to packages/config/crypto.ts diff --git a/packages/config/deno.json b/packages/config/deno.json new file mode 100644 index 00000000..a726b21d --- /dev/null +++ b/packages/config/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@ditto/config", + "version": "1.1.0", + "exports": { + ".": "./mod.ts" + } +} diff --git a/packages/config/mod.ts b/packages/config/mod.ts new file mode 100644 index 00000000..76e94a0d --- /dev/null +++ b/packages/config/mod.ts @@ -0,0 +1 @@ +export { DittoConfig } from './DittoConfig.ts'; diff --git a/packages/ditto/config.ts b/packages/ditto/config.ts index be333334..56e67f49 100644 --- a/packages/ditto/config.ts +++ b/packages/ditto/config.ts @@ -1,395 +1,4 @@ -import os from 'node:os'; -import ISO6391, { LanguageCode } from 'iso-639-1'; -import { getPublicKey, nip19 } from 'nostr-tools'; -import { z } from 'zod'; -import { decodeBase64 } from '@std/encoding/base64'; -import { encodeBase64Url } from '@std/encoding/base64url'; +import { DittoConfig } from '@ditto/config'; -import { getEcdsaPublicKey } from '@/utils/crypto.ts'; - -/** Application-wide configuration. */ -class Conf { - private static _pubkey: string | undefined; - /** Ditto admin secret key in nip19 format. This is the way it's configured by an admin. */ - static get nsec(): `nsec1${string}` { - const value = Deno.env.get('DITTO_NSEC'); - if (!value) { - throw new Error('Missing DITTO_NSEC'); - } - if (!value.startsWith('nsec1')) { - throw new Error('Invalid DITTO_NSEC'); - } - return value as `nsec1${string}`; - } - /** Ditto admin secret key in hex format. */ - static get seckey(): Uint8Array { - return nip19.decode(Conf.nsec).data; - } - /** Ditto admin public key in hex format. */ - static get pubkey(): string { - if (!this._pubkey) { - this._pubkey = getPublicKey(Conf.seckey); - } - return this._pubkey; - } - /** Port to use when serving the HTTP server. */ - static get port(): number { - return parseInt(Deno.env.get('PORT') || '4036'); - } - /** IP addresses not affected by rate limiting. */ - static get ipWhitelist(): string[] { - return Deno.env.get('IP_WHITELIST')?.split(',') || []; - } - /** Relay URL to the Ditto server's relay. */ - static get relay(): `wss://${string}` | `ws://${string}` { - const { protocol, host } = Conf.url; - return `${protocol === 'https:' ? 'wss:' : 'ws:'}//${host}/relay`; - } - /** Relay to use for NIP-50 `search` queries. */ - static get searchRelay(): string | undefined { - return Deno.env.get('SEARCH_RELAY'); - } - /** Origin of the Ditto server, including the protocol and port. */ - static get localDomain(): string { - return Deno.env.get('LOCAL_DOMAIN') || `http://localhost:${Conf.port}`; - } - /** Link to an external nostr viewer. */ - static get externalDomain(): string { - return Deno.env.get('NOSTR_EXTERNAL') || 'https://njump.me'; - } - /** Get a link to a nip19-encoded entity in the configured external viewer. */ - static external(path: string) { - return new URL(path, Conf.externalDomain).toString(); - } - /** - * Heroku-style database URL. This is used in production to connect to the - * database. - * - * Follows the format: - * - * ```txt - * protocol://username:password@host:port/database_name - * ``` - */ - static get databaseUrl(): string { - return Deno.env.get('DATABASE_URL') ?? 'file://data/pgdata'; - } - /** PGlite debug level. 0 disables logging. */ - static get pgliteDebug(): 0 | 1 | 2 | 3 | 4 | 5 { - return Number(Deno.env.get('PGLITE_DEBUG') || 0) as 0 | 1 | 2 | 3 | 4 | 5; - } - private static _vapidPublicKey: Promise | undefined; - static get vapidPublicKey(): Promise { - if (!this._vapidPublicKey) { - this._vapidPublicKey = (async () => { - const keys = await Conf.vapidKeys; - if (keys) { - const { publicKey } = keys; - const bytes = await crypto.subtle.exportKey('raw', publicKey); - return encodeBase64Url(bytes); - } - })(); - } - - return this._vapidPublicKey; - } - static get vapidKeys(): Promise { - return (async () => { - const encoded = Deno.env.get('VAPID_PRIVATE_KEY'); - - if (!encoded) { - return; - } - - const keyData = decodeBase64(encoded); - - const privateKey = await crypto.subtle.importKey( - 'pkcs8', - keyData, - { name: 'ECDSA', namedCurve: 'P-256' }, - true, - ['sign'], - ); - const publicKey = await getEcdsaPublicKey(privateKey, true); - - return { privateKey, publicKey }; - })(); - } - static db = { - /** Database query timeout configurations. */ - timeouts: { - /** Default query timeout when another setting isn't more specific. */ - get default(): number { - return Number(Deno.env.get('DB_TIMEOUT_DEFAULT') || 5_000); - }, - /** Timeout used for queries made through the Nostr relay. */ - get relay(): number { - return Number(Deno.env.get('DB_TIMEOUT_RELAY') || 1_000); - }, - /** Timeout used for timelines such as home, notifications, hashtag, etc. */ - get timelines(): number { - return Number(Deno.env.get('DB_TIMEOUT_TIMELINES') || 15_000); - }, - }, - }; - /** Time-to-live for captchas in milliseconds. */ - static get captchaTTL(): number { - return Number(Deno.env.get('CAPTCHA_TTL') || 5 * 60 * 1000); - } - /** Character limit to enforce for posts made through Mastodon API. */ - static get postCharLimit(): number { - return Number(Deno.env.get('POST_CHAR_LIMIT') || 5000); - } - /** S3 media storage configuration. */ - static s3 = { - get endPoint(): string | undefined { - return Deno.env.get('S3_ENDPOINT'); - }, - get region(): string | undefined { - return Deno.env.get('S3_REGION'); - }, - get accessKey(): string | undefined { - return Deno.env.get('S3_ACCESS_KEY'); - }, - get secretKey(): string | undefined { - return Deno.env.get('S3_SECRET_KEY'); - }, - get bucket(): string | undefined { - return Deno.env.get('S3_BUCKET'); - }, - get pathStyle(): boolean | undefined { - return optionalBooleanSchema.parse(Deno.env.get('S3_PATH_STYLE')); - }, - get port(): number | undefined { - return optionalNumberSchema.parse(Deno.env.get('S3_PORT')); - }, - get sessionToken(): string | undefined { - return Deno.env.get('S3_SESSION_TOKEN'); - }, - get useSSL(): boolean | undefined { - return optionalBooleanSchema.parse(Deno.env.get('S3_USE_SSL')); - }, - }; - /** IPFS uploader configuration. */ - static ipfs = { - /** Base URL for private IPFS API calls. */ - get apiUrl(): string { - return Deno.env.get('IPFS_API_URL') || 'http://localhost:5001'; - }, - }; - /** nostr.build API endpoint when the `nostrbuild` uploader is used. */ - static get nostrbuildEndpoint(): string { - return Deno.env.get('NOSTRBUILD_ENDPOINT') || 'https://nostr.build/api/v2/upload/files'; - } - /** Default Blossom servers to use when the `blossom` uploader is set. */ - static get blossomServers(): string[] { - return Deno.env.get('BLOSSOM_SERVERS')?.split(',') || ['https://blossom.primal.net/']; - } - /** Module to upload files with. */ - static get uploader(): string | undefined { - return Deno.env.get('DITTO_UPLOADER'); - } - /** Location to use for local uploads. */ - static get uploadsDir(): string { - return Deno.env.get('UPLOADS_DIR') || 'data/uploads'; - } - /** Media base URL for uploads. */ - static get mediaDomain(): string { - const value = Deno.env.get('MEDIA_DOMAIN'); - - if (!value) { - const url = Conf.url; - url.host = `media.${url.host}`; - return url.toString(); - } - - return value; - } - /** - * Whether to analyze media metadata with [blurhash](https://www.npmjs.com/package/blurhash) and [sharp](https://www.npmjs.com/package/sharp). - * This is prone to security vulnerabilities, which is why it's not enabled by default. - */ - static get mediaAnalyze(): boolean { - return optionalBooleanSchema.parse(Deno.env.get('MEDIA_ANALYZE')) ?? false; - } - /** Max upload size for files in number of bytes. Default 100MiB. */ - static get maxUploadSize(): number { - return Number(Deno.env.get('MAX_UPLOAD_SIZE') || 100 * 1024 * 1024); - } - /** Usernames that regular users cannot sign up with. */ - static get forbiddenUsernames(): string[] { - return Deno.env.get('FORBIDDEN_USERNAMES')?.split(',') || [ - '_', - 'admin', - 'administrator', - 'root', - 'sysadmin', - 'system', - ]; - } - /** Domain of the Ditto server as a `URL` object, for easily grabbing the `hostname`, etc. */ - static get url(): URL { - return new URL(Conf.localDomain); - } - /** Merges the path with the localDomain. */ - static local(path: string): string { - return mergePaths(Conf.localDomain, path); - } - /** URL to send Sentry errors to. */ - static get sentryDsn(): string | undefined { - return Deno.env.get('SENTRY_DSN'); - } - /** Postgres settings. */ - static pg = { - /** Number of connections to use in the pool. */ - get poolSize(): number { - return Number(Deno.env.get('PG_POOL_SIZE') ?? 20); - }, - }; - /** Whether to enable requesting events from known relays. */ - static get firehoseEnabled(): boolean { - return optionalBooleanSchema.parse(Deno.env.get('FIREHOSE_ENABLED')) ?? true; - } - /** Number of events the firehose is allowed to process at one time before they have to wait in a queue. */ - static get firehoseConcurrency(): number { - return Math.ceil(Number(Deno.env.get('FIREHOSE_CONCURRENCY') ?? 1)); - } - /** Nostr event kinds of events to listen for on the firehose. */ - static get firehoseKinds(): number[] { - return (Deno.env.get('FIREHOSE_KINDS') ?? '0, 1, 3, 5, 6, 7, 20, 9735, 10002') - .split(/[, ]+/g) - .map(Number); - } - /** - * Whether Ditto should subscribe to Nostr events from the Postgres database itself. - * This would make Nostr events inserted directly into Postgres available to the streaming API and relay. - */ - static get notifyEnabled(): boolean { - return optionalBooleanSchema.parse(Deno.env.get('NOTIFY_ENABLED')) ?? true; - } - /** Whether to enable Ditto cron jobs. */ - static get cronEnabled(): boolean { - return optionalBooleanSchema.parse(Deno.env.get('CRON_ENABLED')) ?? true; - } - /** User-Agent to use when fetching link previews. Pretend to be Facebook by default. */ - static get fetchUserAgent(): string { - return Deno.env.get('DITTO_FETCH_USER_AGENT') ?? 'facebookexternalhit'; - } - /** Path to the custom policy module. Must be an absolute path, https:, npm:, or jsr: URI. */ - static get policy(): string { - return Deno.env.get('DITTO_POLICY') || new URL('../data/policy.ts', import.meta.url).pathname; - } - /** Absolute path to the data directory used by Ditto. */ - static get dataDir(): string { - return Deno.env.get('DITTO_DATA_DIR') || new URL('../data', import.meta.url).pathname; - } - /** Absolute path of the Deno directory. */ - static get denoDir(): string { - return Deno.env.get('DENO_DIR') || `${os.userInfo().homedir}/.cache/deno`; - } - /** Whether zap splits should be enabled. */ - static get zapSplitsEnabled(): boolean { - return optionalBooleanSchema.parse(Deno.env.get('ZAP_SPLITS_ENABLED')) ?? false; - } - /** Languages this server wishes to highlight. Used when querying trends.*/ - static get preferredLanguages(): LanguageCode[] | undefined { - return Deno.env.get('DITTO_LANGUAGES')?.split(',')?.filter(ISO6391.validate); - } - /** Mints to be displayed in the UI when the user decides to create a wallet.*/ - static get cashuMints(): string[] { - return Deno.env.get('CASHU_MINTS')?.split(',') ?? []; - } - /** Translation provider used to translate posts. */ - static get translationProvider(): string | undefined { - return Deno.env.get('TRANSLATION_PROVIDER'); - } - /** DeepL URL endpoint. */ - static get deeplBaseUrl(): string | undefined { - return Deno.env.get('DEEPL_BASE_URL'); - } - /** DeepL API KEY. */ - static get deeplApiKey(): string | undefined { - return Deno.env.get('DEEPL_API_KEY'); - } - /** LibreTranslate URL endpoint. */ - static get libretranslateBaseUrl(): string | undefined { - return Deno.env.get('LIBRETRANSLATE_BASE_URL'); - } - /** LibreTranslate API KEY. */ - static get libretranslateApiKey(): string | undefined { - return Deno.env.get('LIBRETRANSLATE_API_KEY'); - } - /** Cache settings. */ - static caches = { - /** NIP-05 cache settings. */ - get nip05(): { max: number; ttl: number } { - return { - max: Number(Deno.env.get('DITTO_CACHE_NIP05_MAX') || 3000), - ttl: Number(Deno.env.get('DITTO_CACHE_NIP05_TTL') || 1 * 60 * 60 * 1000), - }; - }, - /** Favicon cache settings. */ - get favicon(): { max: number; ttl: number } { - return { - max: Number(Deno.env.get('DITTO_CACHE_FAVICON_MAX') || 500), - ttl: Number(Deno.env.get('DITTO_CACHE_FAVICON_TTL') || 1 * 60 * 60 * 1000), - }; - }, - /** Link preview cache settings. */ - get linkPreview(): { max: number; ttl: number } { - return { - max: Number(Deno.env.get('DITTO_CACHE_LINK_PREVIEW_MAX') || 3000), - ttl: Number(Deno.env.get('DITTO_CACHE_LINK_PREVIEW_TTL') || 12 * 60 * 60 * 1000), - }; - }, - /** Translation cache settings. */ - get translation(): { max: number; ttl: number } { - return { - max: Number(Deno.env.get('DITTO_CACHE_TRANSLATION_MAX') || 1000), - ttl: Number(Deno.env.get('DITTO_CACHE_TRANSLATION_TTL') || 6 * 60 * 60 * 1000), - }; - }, - }; - static profileFields = { - get maxFields(): number { - return Number(Deno.env.get('PROFILE_FIELDS_MAX_FIELDS') || 10); - }, - get nameLength(): number { - return Number(Deno.env.get('PROFILE_FIELDS_NAME_LENGTH') || 255); - }, - get valueLength(): number { - return Number(Deno.env.get('PROFILE_FIELDS_VALUE_LENGTH') || 2047); - }, - }; - /** Maximum time between events before a streak is broken, *in seconds*. */ - static get streakWindow(): number { - return Number(Deno.env.get('STREAK_WINDOW') || 129600); - } -} - -const optionalBooleanSchema = z - .enum(['true', 'false']) - .optional() - .transform((value) => value !== undefined ? value === 'true' : undefined); - -const optionalNumberSchema = z - .string() - .optional() - .transform((value) => value !== undefined ? Number(value) : undefined); - -function mergePaths(base: string, path: string) { - const url = new URL( - path.startsWith('/') ? path : new URL(path).pathname, - base, - ); - - if (!path.startsWith('/')) { - // Copy query parameters from the original URL to the new URL - const originalUrl = new URL(path); - url.search = originalUrl.search; - } - - return url.toString(); -} - -export { Conf }; +/** @deprecated Use middleware to set/get the config instead. */ +export const Conf = new DittoConfig(Deno.env); From 1636601bfe32220b245ec0b78d693df157d6c743 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 16:32:10 -0600 Subject: [PATCH 02/13] config: crypto.ts -> utils/crypto.ts --- packages/config/DittoConfig.ts | 2 +- packages/config/{ => utils}/crypto.test.ts | 0 packages/config/{ => utils}/crypto.ts | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/config/{ => utils}/crypto.test.ts (100%) rename packages/config/{ => utils}/crypto.ts (100%) diff --git a/packages/config/DittoConfig.ts b/packages/config/DittoConfig.ts index 01ba4dce..2aee123f 100644 --- a/packages/config/DittoConfig.ts +++ b/packages/config/DittoConfig.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; import { decodeBase64 } from '@std/encoding/base64'; import { encodeBase64Url } from '@std/encoding/base64url'; -import { getEcdsaPublicKey } from './crypto.ts'; +import { getEcdsaPublicKey } from './utils/crypto.ts'; /** Ditto application-wide configuration. */ export class DittoConfig { diff --git a/packages/config/crypto.test.ts b/packages/config/utils/crypto.test.ts similarity index 100% rename from packages/config/crypto.test.ts rename to packages/config/utils/crypto.test.ts diff --git a/packages/config/crypto.ts b/packages/config/utils/crypto.ts similarity index 100% rename from packages/config/crypto.ts rename to packages/config/utils/crypto.ts From 5f6cdaf7d5b55839953684169657047b37ad678d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 16:37:33 -0600 Subject: [PATCH 03/13] config: refactor schemas into a separate file --- packages/config/DittoConfig.ts | 12 +----------- packages/config/utils/schema.test.ts | 17 +++++++++++++++++ packages/config/utils/schema.ts | 11 +++++++++++ 3 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 packages/config/utils/schema.test.ts create mode 100644 packages/config/utils/schema.ts diff --git a/packages/config/DittoConfig.ts b/packages/config/DittoConfig.ts index 2aee123f..b11ca681 100644 --- a/packages/config/DittoConfig.ts +++ b/packages/config/DittoConfig.ts @@ -1,11 +1,11 @@ import os from 'node:os'; import ISO6391, { type LanguageCode } from 'iso-639-1'; import { getPublicKey, nip19 } from 'nostr-tools'; -import { z } from 'zod'; import { decodeBase64 } from '@std/encoding/base64'; import { encodeBase64Url } from '@std/encoding/base64url'; import { getEcdsaPublicKey } from './utils/crypto.ts'; +import { optionalBooleanSchema, optionalNumberSchema } from './utils/schema.ts'; /** Ditto application-wide configuration. */ export class DittoConfig { @@ -462,16 +462,6 @@ export class DittoConfig { } } -const optionalBooleanSchema = z - .enum(['true', 'false']) - .optional() - .transform((value) => value !== undefined ? value === 'true' : undefined); - -const optionalNumberSchema = z - .string() - .optional() - .transform((value) => value !== undefined ? Number(value) : undefined); - function mergePaths(base: string, path: string) { const url = new URL( path.startsWith('/') ? path : new URL(path).pathname, diff --git a/packages/config/utils/schema.test.ts b/packages/config/utils/schema.test.ts new file mode 100644 index 00000000..9a52efe0 --- /dev/null +++ b/packages/config/utils/schema.test.ts @@ -0,0 +1,17 @@ +import { assertEquals, assertThrows } from '@std/assert'; + +import { optionalBooleanSchema, optionalNumberSchema } from './schema.ts'; + +Deno.test('optionalBooleanSchema', () => { + assertEquals(optionalBooleanSchema.parse('true'), true); + assertEquals(optionalBooleanSchema.parse('false'), false); + assertEquals(optionalBooleanSchema.parse(undefined), undefined); + + assertThrows(() => optionalBooleanSchema.parse('invalid')); +}); + +Deno.test('optionalNumberSchema', () => { + assertEquals(optionalNumberSchema.parse('123'), 123); + assertEquals(optionalNumberSchema.parse('invalid'), NaN); // maybe this should throw? + assertEquals(optionalNumberSchema.parse(undefined), undefined); +}); diff --git a/packages/config/utils/schema.ts b/packages/config/utils/schema.ts new file mode 100644 index 00000000..dcd1f85e --- /dev/null +++ b/packages/config/utils/schema.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const optionalBooleanSchema = z + .enum(['true', 'false']) + .optional() + .transform((value) => value !== undefined ? value === 'true' : undefined); + +export const optionalNumberSchema = z + .string() + .optional() + .transform((value) => value !== undefined ? Number(value) : undefined); From 13db5498a5d3e8ac98bf9dee482fe95e06be1c79 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 16:51:21 -0600 Subject: [PATCH 04/13] config: break mergeURLPath into a separate module --- packages/config/DittoConfig.ts | 18 ++---------------- packages/config/utils/url.test.ts | 9 +++++++++ packages/config/utils/url.ts | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 packages/config/utils/url.test.ts create mode 100644 packages/config/utils/url.ts diff --git a/packages/config/DittoConfig.ts b/packages/config/DittoConfig.ts index b11ca681..c8f63a60 100644 --- a/packages/config/DittoConfig.ts +++ b/packages/config/DittoConfig.ts @@ -6,6 +6,7 @@ import { encodeBase64Url } from '@std/encoding/base64url'; import { getEcdsaPublicKey } from './utils/crypto.ts'; import { optionalBooleanSchema, optionalNumberSchema } from './utils/schema.ts'; +import { mergeURLPath } from './utils/url.ts'; /** Ditto application-wide configuration. */ export class DittoConfig { @@ -288,7 +289,7 @@ export class DittoConfig { /** Merges the path with the localDomain. */ local(path: string): string { - return mergePaths(this.localDomain, path); + return mergeURLPath(this.localDomain, path); } /** URL to send Sentry errors to. */ @@ -461,18 +462,3 @@ export class DittoConfig { return Number(this.env.get('STREAK_WINDOW') || 129600); } } - -function mergePaths(base: string, path: string) { - const url = new URL( - path.startsWith('/') ? path : new URL(path).pathname, - base, - ); - - if (!path.startsWith('/')) { - // Copy query parameters from the original URL to the new URL - const originalUrl = new URL(path); - url.search = originalUrl.search; - } - - return url.toString(); -} diff --git a/packages/config/utils/url.test.ts b/packages/config/utils/url.test.ts new file mode 100644 index 00000000..1da9773c --- /dev/null +++ b/packages/config/utils/url.test.ts @@ -0,0 +1,9 @@ +import { assertEquals } from '@std/assert'; + +import { mergeURLPath } from './url.ts'; + +Deno.test('mergeURLPath', () => { + assertEquals(mergeURLPath('https://mario.com', '/path'), 'https://mario.com/path'); + assertEquals(mergeURLPath('https://mario.com', 'https://luigi.com/path'), 'https://mario.com/path'); + assertEquals(mergeURLPath('https://mario.com', 'https://luigi.com/path?q=1'), 'https://mario.com/path?q=1'); +}); diff --git a/packages/config/utils/url.ts b/packages/config/utils/url.ts new file mode 100644 index 00000000..f7287bab --- /dev/null +++ b/packages/config/utils/url.ts @@ -0,0 +1,23 @@ +/** + * Produce a URL whose origin is guaranteed to be the same as the base URL. + * The path is either an absolute path (starting with `/`), or a full URL. In either case, only its path is used. + */ +export function mergeURLPath( + /** Base URL. Result is guaranteed to use this URL's origin. */ + base: string, + /** Either an absolute path (starting with `/`), or a full URL. If a full URL, its path */ + path: string, +): string { + const url = new URL( + path.startsWith('/') ? path : new URL(path).pathname, + base, + ); + + if (!path.startsWith('/')) { + // Copy query parameters from the original URL to the new URL + const originalUrl = new URL(path); + url.search = originalUrl.search; + } + + return url.toString(); +} From 1e5278dc8cc0d4b6d2bc7815d7c125097b75eccc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 16:55:25 -0600 Subject: [PATCH 05/13] Add basic DittoConfig tests --- packages/config/DittoConfig.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/config/DittoConfig.test.ts diff --git a/packages/config/DittoConfig.test.ts b/packages/config/DittoConfig.test.ts new file mode 100644 index 00000000..fc2e472c --- /dev/null +++ b/packages/config/DittoConfig.test.ts @@ -0,0 +1,19 @@ +import { assertEquals } from '@std/assert'; + +import { DittoConfig } from './DittoConfig.ts'; + +Deno.test('DittoConfig', async (t) => { + const env = new Map([ + ['DITTO_NSEC', 'nsec19shyxpuzd0cq2p5078fwnws7tyykypud6z205fzhlmlrs2vpz6hs83zwkw'], + ]); + + const config = new DittoConfig(env); + + await t.step('nsec', () => { + assertEquals(config.nsec, 'nsec19shyxpuzd0cq2p5078fwnws7tyykypud6z205fzhlmlrs2vpz6hs83zwkw'); + }); + + await t.step('pubkey', () => { + assertEquals(config.pubkey, '1ba0c5ed1bbbf3b7eb0d7843ba16836a0201ea68a76bafcba507358c45911ff6'); + }); +}); From a2f273287d5f07c86cb9ef95d0efb8871155acf6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 16:59:12 -0600 Subject: [PATCH 06/13] config: test defaults --- packages/config/DittoConfig.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/config/DittoConfig.test.ts b/packages/config/DittoConfig.test.ts index fc2e472c..a61a0c77 100644 --- a/packages/config/DittoConfig.test.ts +++ b/packages/config/DittoConfig.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from '@std/assert'; +import { assertEquals, assertThrows } from '@std/assert'; import { DittoConfig } from './DittoConfig.ts'; @@ -17,3 +17,16 @@ Deno.test('DittoConfig', async (t) => { assertEquals(config.pubkey, '1ba0c5ed1bbbf3b7eb0d7843ba16836a0201ea68a76bafcba507358c45911ff6'); }); }); + +Deno.test('DittoConfig defaults', async (t) => { + const env = new Map(); + const config = new DittoConfig(env); + + await t.step('nsec throws', () => { + assertThrows(() => config.nsec); + }); + + await t.step('port', () => { + assertEquals(config.port, 4036); + }); +}); From 9bfc7e6fe3224fe4b6022aea13c880032373a9e1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 17:02:53 -0600 Subject: [PATCH 07/13] DittoConfig: fix missing return type of `.external()` --- packages/config/DittoConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/config/DittoConfig.ts b/packages/config/DittoConfig.ts index c8f63a60..5090fb7b 100644 --- a/packages/config/DittoConfig.ts +++ b/packages/config/DittoConfig.ts @@ -75,7 +75,7 @@ export class DittoConfig { } /** Get a link to a nip19-encoded entity in the configured external viewer. */ - external(path: string) { + external(path: string): string { return new URL(path, this.externalDomain).toString(); } From 665be0c1b2e7d0364f78f6b0d7b87ef31c413a91 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 17:46:40 -0600 Subject: [PATCH 08/13] Add @ditto/api package with conf middleware --- deno.json | 1 + packages/api/deno.json | 7 ++++++ packages/api/middleware/confMw.test.ts | 19 ++++++++++++++++ packages/api/middleware/confMw.ts | 15 +++++++++++++ .../api/middleware/confRequiredMw.test.ts | 22 +++++++++++++++++++ packages/api/middleware/confRequiredMw.ts | 15 +++++++++++++ packages/api/middleware/mod.ts | 2 ++ 7 files changed, 81 insertions(+) create mode 100644 packages/api/deno.json create mode 100644 packages/api/middleware/confMw.test.ts create mode 100644 packages/api/middleware/confMw.ts create mode 100644 packages/api/middleware/confRequiredMw.test.ts create mode 100644 packages/api/middleware/confRequiredMw.ts create mode 100644 packages/api/middleware/mod.ts diff --git a/deno.json b/deno.json index 1a12f40e..2c7392ce 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,6 @@ { "workspace": [ + "./packages/api", "./packages/config", "./packages/ditto" ], diff --git a/packages/api/deno.json b/packages/api/deno.json new file mode 100644 index 00000000..a8bbb3f5 --- /dev/null +++ b/packages/api/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@ditto/api", + "version": "1.1.0", + "exports": { + "./middleware": "./middleware/mod.ts" + } +} diff --git a/packages/api/middleware/confMw.test.ts b/packages/api/middleware/confMw.test.ts new file mode 100644 index 00000000..5eac707c --- /dev/null +++ b/packages/api/middleware/confMw.test.ts @@ -0,0 +1,19 @@ +import { Hono } from '@hono/hono'; +import { assertEquals } from '@std/assert'; + +import { confMw } from './confMw.ts'; + +Deno.test('confMw', async () => { + const env = new Map([ + ['DITTO_NSEC', 'nsec19shyxpuzd0cq2p5078fwnws7tyykypud6z205fzhlmlrs2vpz6hs83zwkw'], + ]); + + const app = new Hono(); + + app.get('/', confMw(env), (c) => c.text(c.var.conf.pubkey)); + + const response = await app.request('/'); + const body = await response.text(); + + assertEquals(body, '1ba0c5ed1bbbf3b7eb0d7843ba16836a0201ea68a76bafcba507358c45911ff6'); +}); diff --git a/packages/api/middleware/confMw.ts b/packages/api/middleware/confMw.ts new file mode 100644 index 00000000..ae53ab72 --- /dev/null +++ b/packages/api/middleware/confMw.ts @@ -0,0 +1,15 @@ +import { DittoConfig } from '@ditto/config'; + +import type { MiddlewareHandler } from '@hono/hono'; + +/** Set Ditto config. */ +export function confMw( + env: { get(key: string): string | undefined }, +): MiddlewareHandler<{ Variables: { conf: DittoConfig } }> { + const conf = new DittoConfig(env); + + return async (c, next) => { + c.set('conf', conf); + await next(); + }; +} diff --git a/packages/api/middleware/confRequiredMw.test.ts b/packages/api/middleware/confRequiredMw.test.ts new file mode 100644 index 00000000..9dfcc096 --- /dev/null +++ b/packages/api/middleware/confRequiredMw.test.ts @@ -0,0 +1,22 @@ +import { Hono } from '@hono/hono'; +import { assertEquals } from '@std/assert'; + +import { confMw } from './confMw.ts'; +import { confRequiredMw } from './confRequiredMw.ts'; + +Deno.test('confRequiredMw', async (t) => { + const app = new Hono(); + + app.get('/without', confRequiredMw, (c) => c.text('ok')); + app.get('/with', confMw(new Map()), confRequiredMw, (c) => c.text('ok')); + + await t.step('without conf returns 500', async () => { + const response = await app.request('/without'); + assertEquals(response.status, 500); + }); + + await t.step('with conf returns 200', async () => { + const response = await app.request('/with'); + assertEquals(response.status, 200); + }); +}); diff --git a/packages/api/middleware/confRequiredMw.ts b/packages/api/middleware/confRequiredMw.ts new file mode 100644 index 00000000..129734b4 --- /dev/null +++ b/packages/api/middleware/confRequiredMw.ts @@ -0,0 +1,15 @@ +import { HTTPException } from '@hono/hono/http-exception'; + +import type { DittoConfig } from '@ditto/config'; +import type { MiddlewareHandler } from '@hono/hono'; + +/** Throws an error if conf isn't set. */ +export const confRequiredMw: MiddlewareHandler<{ Variables: { conf: DittoConfig } }> = async (c, next) => { + const { conf } = c.var; + + if (!conf) { + throw new HTTPException(500, { message: 'Ditto config not set in request.' }); + } + + await next(); +}; diff --git a/packages/api/middleware/mod.ts b/packages/api/middleware/mod.ts new file mode 100644 index 00000000..54a1b35c --- /dev/null +++ b/packages/api/middleware/mod.ts @@ -0,0 +1,2 @@ +export { confMw } from './confMw.ts'; +export { confRequiredMw } from './confRequiredMw.ts'; From 02a7305ee99bdfae97358c5909aaaaf19536df6e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 17:58:24 -0600 Subject: [PATCH 09/13] @ditto/config -> @ditto/conf, DittoConfig -> DittoConf --- deno.json | 2 +- packages/api/middleware/confMw.ts | 6 +++--- packages/api/middleware/confRequiredMw.ts | 4 ++-- .../{config/DittoConfig.test.ts => conf/DittoConf.test.ts} | 6 +++--- packages/{config/DittoConfig.ts => conf/DittoConf.ts} | 2 +- packages/{config => conf}/deno.json | 2 +- packages/conf/mod.ts | 1 + packages/{config => conf}/utils/crypto.test.ts | 0 packages/{config => conf}/utils/crypto.ts | 0 packages/{config => conf}/utils/schema.test.ts | 0 packages/{config => conf}/utils/schema.ts | 0 packages/{config => conf}/utils/url.test.ts | 0 packages/{config => conf}/utils/url.ts | 0 packages/config/mod.ts | 1 - packages/ditto/config.ts | 4 ++-- 15 files changed, 14 insertions(+), 14 deletions(-) rename packages/{config/DittoConfig.test.ts => conf/DittoConf.test.ts} (85%) rename packages/{config/DittoConfig.ts => conf/DittoConf.ts} (99%) rename packages/{config => conf}/deno.json (70%) create mode 100644 packages/conf/mod.ts rename packages/{config => conf}/utils/crypto.test.ts (100%) rename packages/{config => conf}/utils/crypto.ts (100%) rename packages/{config => conf}/utils/schema.test.ts (100%) rename packages/{config => conf}/utils/schema.ts (100%) rename packages/{config => conf}/utils/url.test.ts (100%) rename packages/{config => conf}/utils/url.ts (100%) delete mode 100644 packages/config/mod.ts diff --git a/deno.json b/deno.json index 2c7392ce..f7296fa0 100644 --- a/deno.json +++ b/deno.json @@ -1,7 +1,7 @@ { "workspace": [ "./packages/api", - "./packages/config", + "./packages/conf", "./packages/ditto" ], "tasks": { diff --git a/packages/api/middleware/confMw.ts b/packages/api/middleware/confMw.ts index ae53ab72..ebfdfe4b 100644 --- a/packages/api/middleware/confMw.ts +++ b/packages/api/middleware/confMw.ts @@ -1,12 +1,12 @@ -import { DittoConfig } from '@ditto/config'; +import { DittoConf } from '@ditto/conf'; import type { MiddlewareHandler } from '@hono/hono'; /** Set Ditto config. */ export function confMw( env: { get(key: string): string | undefined }, -): MiddlewareHandler<{ Variables: { conf: DittoConfig } }> { - const conf = new DittoConfig(env); +): MiddlewareHandler<{ Variables: { conf: DittoConf } }> { + const conf = new DittoConf(env); return async (c, next) => { c.set('conf', conf); diff --git a/packages/api/middleware/confRequiredMw.ts b/packages/api/middleware/confRequiredMw.ts index 129734b4..dc4d661d 100644 --- a/packages/api/middleware/confRequiredMw.ts +++ b/packages/api/middleware/confRequiredMw.ts @@ -1,10 +1,10 @@ import { HTTPException } from '@hono/hono/http-exception'; -import type { DittoConfig } from '@ditto/config'; +import type { DittoConf } from '@ditto/conf'; import type { MiddlewareHandler } from '@hono/hono'; /** Throws an error if conf isn't set. */ -export const confRequiredMw: MiddlewareHandler<{ Variables: { conf: DittoConfig } }> = async (c, next) => { +export const confRequiredMw: MiddlewareHandler<{ Variables: { conf: DittoConf } }> = async (c, next) => { const { conf } = c.var; if (!conf) { diff --git a/packages/config/DittoConfig.test.ts b/packages/conf/DittoConf.test.ts similarity index 85% rename from packages/config/DittoConfig.test.ts rename to packages/conf/DittoConf.test.ts index a61a0c77..c2e87c46 100644 --- a/packages/config/DittoConfig.test.ts +++ b/packages/conf/DittoConf.test.ts @@ -1,13 +1,13 @@ import { assertEquals, assertThrows } from '@std/assert'; -import { DittoConfig } from './DittoConfig.ts'; +import { DittoConf } from './DittoConf.ts'; Deno.test('DittoConfig', async (t) => { const env = new Map([ ['DITTO_NSEC', 'nsec19shyxpuzd0cq2p5078fwnws7tyykypud6z205fzhlmlrs2vpz6hs83zwkw'], ]); - const config = new DittoConfig(env); + const config = new DittoConf(env); await t.step('nsec', () => { assertEquals(config.nsec, 'nsec19shyxpuzd0cq2p5078fwnws7tyykypud6z205fzhlmlrs2vpz6hs83zwkw'); @@ -20,7 +20,7 @@ Deno.test('DittoConfig', async (t) => { Deno.test('DittoConfig defaults', async (t) => { const env = new Map(); - const config = new DittoConfig(env); + const config = new DittoConf(env); await t.step('nsec throws', () => { assertThrows(() => config.nsec); diff --git a/packages/config/DittoConfig.ts b/packages/conf/DittoConf.ts similarity index 99% rename from packages/config/DittoConfig.ts rename to packages/conf/DittoConf.ts index 5090fb7b..b0f1256f 100644 --- a/packages/config/DittoConfig.ts +++ b/packages/conf/DittoConf.ts @@ -9,7 +9,7 @@ import { optionalBooleanSchema, optionalNumberSchema } from './utils/schema.ts'; import { mergeURLPath } from './utils/url.ts'; /** Ditto application-wide configuration. */ -export class DittoConfig { +export class DittoConf { constructor(private env: { get(key: string): string | undefined }) {} /** Cached parsed admin pubkey value. */ diff --git a/packages/config/deno.json b/packages/conf/deno.json similarity index 70% rename from packages/config/deno.json rename to packages/conf/deno.json index a726b21d..7ba0a49a 100644 --- a/packages/config/deno.json +++ b/packages/conf/deno.json @@ -1,5 +1,5 @@ { - "name": "@ditto/config", + "name": "@ditto/conf", "version": "1.1.0", "exports": { ".": "./mod.ts" diff --git a/packages/conf/mod.ts b/packages/conf/mod.ts new file mode 100644 index 00000000..4d7ef2b7 --- /dev/null +++ b/packages/conf/mod.ts @@ -0,0 +1 @@ +export { DittoConf } from './DittoConf.ts'; diff --git a/packages/config/utils/crypto.test.ts b/packages/conf/utils/crypto.test.ts similarity index 100% rename from packages/config/utils/crypto.test.ts rename to packages/conf/utils/crypto.test.ts diff --git a/packages/config/utils/crypto.ts b/packages/conf/utils/crypto.ts similarity index 100% rename from packages/config/utils/crypto.ts rename to packages/conf/utils/crypto.ts diff --git a/packages/config/utils/schema.test.ts b/packages/conf/utils/schema.test.ts similarity index 100% rename from packages/config/utils/schema.test.ts rename to packages/conf/utils/schema.test.ts diff --git a/packages/config/utils/schema.ts b/packages/conf/utils/schema.ts similarity index 100% rename from packages/config/utils/schema.ts rename to packages/conf/utils/schema.ts diff --git a/packages/config/utils/url.test.ts b/packages/conf/utils/url.test.ts similarity index 100% rename from packages/config/utils/url.test.ts rename to packages/conf/utils/url.test.ts diff --git a/packages/config/utils/url.ts b/packages/conf/utils/url.ts similarity index 100% rename from packages/config/utils/url.ts rename to packages/conf/utils/url.ts diff --git a/packages/config/mod.ts b/packages/config/mod.ts deleted file mode 100644 index 76e94a0d..00000000 --- a/packages/config/mod.ts +++ /dev/null @@ -1 +0,0 @@ -export { DittoConfig } from './DittoConfig.ts'; diff --git a/packages/ditto/config.ts b/packages/ditto/config.ts index 56e67f49..59554920 100644 --- a/packages/ditto/config.ts +++ b/packages/ditto/config.ts @@ -1,4 +1,4 @@ -import { DittoConfig } from '@ditto/config'; +import { DittoConf } from '@ditto/conf'; /** @deprecated Use middleware to set/get the config instead. */ -export const Conf = new DittoConfig(Deno.env); +export const Conf = new DittoConf(Deno.env); From 478c77bb62114f2a04ff7913949df87f9d9d1684 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 18:34:43 -0600 Subject: [PATCH 10/13] Eliminate Conf from most controllers --- packages/ditto/app.ts | 6 +- packages/ditto/controllers/api/accounts.ts | 19 +++--- packages/ditto/controllers/api/admin.ts | 14 ++-- packages/ditto/controllers/api/captcha.ts | 5 +- packages/ditto/controllers/api/cashu.ts | 18 ++--- packages/ditto/controllers/api/ditto.ts | 26 +++++--- packages/ditto/controllers/api/instance.ts | 33 +++++----- .../ditto/controllers/api/notifications.ts | 7 +- packages/ditto/controllers/api/oauth.ts | 14 ++-- packages/ditto/controllers/api/pleroma.ts | 10 +-- packages/ditto/controllers/api/push.ts | 7 +- packages/ditto/controllers/api/reports.ts | 7 +- packages/ditto/controllers/api/statuses.ts | 65 +++++++++++-------- packages/ditto/controllers/api/streaming.ts | 7 +- packages/ditto/controllers/api/suggestions.ts | 21 +++--- packages/ditto/controllers/api/timelines.ts | 10 +-- packages/ditto/controllers/api/trends.ts | 28 ++++---- .../ditto/controllers/nostr/relay-info.ts | 4 +- packages/ditto/controllers/nostr/relay.ts | 13 ++-- .../ditto/controllers/well-known/nodeinfo.ts | 8 +-- 20 files changed, 179 insertions(+), 143 deletions(-) diff --git a/packages/ditto/app.ts b/packages/ditto/app.ts index 0c677517..3f5abee4 100644 --- a/packages/ditto/app.ts +++ b/packages/ditto/app.ts @@ -1,3 +1,5 @@ +import { confMw } from '@ditto/api/middleware'; +import { type DittoConf } from '@ditto/conf'; import { type Context, Env as HonoEnv, Handler, Hono, Input as HonoInput, MiddlewareHandler } from '@hono/hono'; import { every } from '@hono/hono/combine'; import { cors } from '@hono/hono/cors'; @@ -149,6 +151,7 @@ import { logiMiddleware } from '@/middleware/logiMiddleware.ts'; export interface AppEnv extends HonoEnv { Variables: { + conf: DittoConf; /** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */ signer?: NostrSigner; /** Uploader for the user to upload files. */ @@ -180,7 +183,7 @@ const publicFiles = serveStatic({ root: './public/' }); /** Static files provided by the Ditto repo, checked into git. */ const staticFiles = serveStatic({ root: new URL('./static/', import.meta.url).pathname }); -app.use('*', cacheControlMiddleware({ noStore: true })); +app.use(confMw(Deno.env), cacheControlMiddleware({ noStore: true })); const ratelimit = every( rateLimitMiddleware(30, Time.seconds(5), false), @@ -196,7 +199,6 @@ app.get('/api/v1/streaming', metricsMiddleware, ratelimit, streamingController); app.get('/relay', metricsMiddleware, ratelimit, relayController); app.use( - '*', cspMiddleware(), cors({ origin: '*', exposeHeaders: ['link'] }), signerMiddleware, diff --git a/packages/ditto/controllers/api/accounts.ts b/packages/ditto/controllers/api/accounts.ts index 7b1b4216..252ddad6 100644 --- a/packages/ditto/controllers/api/accounts.ts +++ b/packages/ditto/controllers/api/accounts.ts @@ -3,7 +3,6 @@ import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; @@ -22,13 +21,8 @@ import { addTag, deleteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; import { getPubkeysBySearch } from '@/utils/search.ts'; import { MastodonAccount } from '@/entities/MastodonAccount.ts'; -const usernameSchema = z - .string().min(1).max(30) - .regex(/^[a-z0-9_]+$/i) - .refine((username) => !Conf.forbiddenUsernames.includes(username), 'Username is reserved.'); - const createAccountSchema = z.object({ - username: usernameSchema, + username: z.string().min(1).max(30).regex(/^[a-z0-9_]+$/i), }); const createAccountController: AppController = async (c) => { @@ -39,6 +33,10 @@ const createAccountController: AppController = async (c) => { return c.json({ error: 'Bad request', schema: result.error }, 400); } + if (c.var.conf.forbiddenUsernames.includes(result.data.username)) { + return c.json({ error: 'Username is reserved.' }, 422); + } + return c.json({ access_token: nip19.npubEncode(pubkey), token_type: 'Bearer', @@ -204,7 +202,8 @@ const accountStatusesQuerySchema = z.object({ const accountStatusesController: AppController = async (c) => { const pubkey = c.req.param('pubkey'); - const { since, until } = c.get('pagination'); + const { conf } = c.var; + const { since, until } = c.var.pagination; const { pinned, limit, exclude_replies, tagged, only_media } = accountStatusesQuerySchema.parse(c.req.query()); const { signal } = c.req.raw; @@ -212,7 +211,7 @@ const accountStatusesController: AppController = async (c) => { const [[author], [user]] = await Promise.all([ store.query([{ kinds: [0], authors: [pubkey], limit: 1 }], { signal }), - store.query([{ kinds: [30382], authors: [Conf.pubkey], '#d': [pubkey], limit: 1 }], { signal }), + store.query([{ kinds: [30382], authors: [conf.pubkey], '#d': [pubkey], limit: 1 }], { signal }), ]); if (author) { @@ -261,7 +260,7 @@ const accountStatusesController: AppController = async (c) => { filter.search = search.join(' '); } - const opts = { signal, limit, timeout: Conf.db.timeouts.timelines }; + const opts = { signal, limit, timeout: conf.db.timeouts.timelines }; const events = await store.query([filter], opts) .then((events) => hydrateEvents({ events, store, signal })) diff --git a/packages/ditto/controllers/api/admin.ts b/packages/ditto/controllers/api/admin.ts index 146c2869..1e3b4615 100644 --- a/packages/ditto/controllers/api/admin.ts +++ b/packages/ditto/controllers/api/admin.ts @@ -3,7 +3,6 @@ import { logi } from '@soapbox/logi'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { booleanParamSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; @@ -30,6 +29,7 @@ const adminAccountQuerySchema = z.object({ }); const adminAccountsController: AppController = async (c) => { + const { conf } = c.var; const store = await Storages.db(); const params = c.get('pagination'); const { signal } = c.req.raw; @@ -49,7 +49,7 @@ const adminAccountsController: AppController = async (c) => { } const orig = await store.query( - [{ kinds: [30383], authors: [Conf.pubkey], '#k': ['3036'], '#n': ['pending'], ...params }], + [{ kinds: [30383], authors: [conf.pubkey], '#k': ['3036'], '#n': ['pending'], ...params }], { signal }, ); @@ -86,7 +86,7 @@ const adminAccountsController: AppController = async (c) => { n.push('moderator'); } - const events = await store.query([{ kinds: [30382], authors: [Conf.pubkey], '#n': n, ...params }], { signal }); + const events = await store.query([{ kinds: [30382], authors: [conf.pubkey], '#n': n, ...params }], { signal }); const pubkeys = new Set( events @@ -110,7 +110,7 @@ const adminAccountsController: AppController = async (c) => { const filter: NostrFilter = { kinds: [0], ...params }; if (local) { - filter.search = `domain:${Conf.url.host}`; + filter.search = `domain:${conf.url.host}`; } const events = await store.query([filter], { signal }) @@ -125,6 +125,7 @@ const adminAccountActionSchema = z.object({ }); const adminActionController: AppController = async (c) => { + const { conf } = c.var; const body = await parseBody(c.req.raw); const store = await Storages.db(); const result = adminAccountActionSchema.safeParse(body); @@ -156,7 +157,7 @@ const adminActionController: AppController = async (c) => { } if (data.type === 'revoke_name') { n.revoke_name = true; - store.remove([{ kinds: [30360], authors: [Conf.pubkey], '#p': [authorId] }]).catch((e: unknown) => { + store.remove([{ kinds: [30360], authors: [conf.pubkey], '#p': [authorId] }]).catch((e: unknown) => { logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) }); }); } @@ -167,6 +168,7 @@ const adminActionController: AppController = async (c) => { }; const adminApproveController: AppController = async (c) => { + const { conf } = c.var; const eventId = c.req.param('id'); const store = await Storages.db(); @@ -183,7 +185,7 @@ const adminApproveController: AppController = async (c) => { return c.json({ error: 'Invalid NIP-05' }, 400); } - const [existing] = await store.query([{ kinds: [30360], authors: [Conf.pubkey], '#d': [r], limit: 1 }]); + const [existing] = await store.query([{ kinds: [30360], authors: [conf.pubkey], '#d': [r], limit: 1 }]); if (existing) { return c.json({ error: 'NIP-05 already granted to another user' }, 400); } diff --git a/packages/ditto/controllers/api/captcha.ts b/packages/ditto/controllers/api/captcha.ts index 1bb92118..6bbcc49f 100644 --- a/packages/ditto/controllers/api/captcha.ts +++ b/packages/ditto/controllers/api/captcha.ts @@ -3,7 +3,6 @@ import TTLCache from '@isaacs/ttlcache'; import { z } from 'zod'; import { AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { updateUser } from '@/utils/api.ts'; interface Point { @@ -24,6 +23,8 @@ const PUZZLE_SIZE = { w: 65, h: 65 }; /** Puzzle captcha controller. */ export const captchaController: AppController = async (c) => { + const { conf } = c.var; + const { bg, puzzle, solution } = generateCaptcha( await imagesAsync, BG_SIZE, @@ -32,7 +33,7 @@ export const captchaController: AppController = async (c) => { const id = crypto.randomUUID(); const now = new Date(); - const ttl = Conf.captchaTTL; + const ttl = conf.captchaTTL; captchas.set(id, solution, { ttl }); diff --git a/packages/ditto/controllers/api/cashu.ts b/packages/ditto/controllers/api/cashu.ts index 19a29658..dd753884 100644 --- a/packages/ditto/controllers/api/cashu.ts +++ b/packages/ditto/controllers/api/cashu.ts @@ -1,10 +1,10 @@ import { Proof } from '@cashu/cashu-ts'; +import { confRequiredMw } from '@ditto/api/middleware'; import { Hono } from '@hono/hono'; import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { bytesToString, stringToBytes } from '@scure/base'; import { z } from 'zod'; -import { Conf } from '@/config.ts'; import { createEvent, parseBody } from '@/utils/api.ts'; import { requireNip44Signer } from '@/middleware/requireSigner.ts'; import { requireStore } from '@/middleware/storeMiddleware.ts'; @@ -16,7 +16,7 @@ import { errorJson } from '@/utils/log.ts'; type Wallet = z.infer; -const app = new Hono().use('*', requireStore); +const app = new Hono().use('*', confRequiredMw, requireStore); // app.delete('/wallet') -> 204 @@ -45,7 +45,7 @@ const createCashuWalletAndNutzapInfoSchema = z.object({ * https://github.com/nostr-protocol/nips/blob/master/61.md#nutzap-informational-event */ app.put('/wallet', requireNip44Signer, async (c) => { - const signer = c.var.signer; + const { conf, signer } = c.var; const store = c.get('store'); const pubkey = await signer.getPublicKey(); const body = await parseBody(c.req.raw); @@ -88,7 +88,7 @@ app.put('/wallet', requireNip44Signer, async (c) => { kind: 10019, tags: [ ...mints.map((mint) => ['mint', mint, 'sat']), - ['relay', Conf.relay], // TODO: add more relays once things get more stable + ['relay', conf.relay], // TODO: add more relays once things get more stable ['pubkey', p2pk], ], }, c); @@ -97,7 +97,7 @@ app.put('/wallet', requireNip44Signer, async (c) => { const walletEntity: Wallet = { pubkey_p2pk: p2pk, mints, - relays: [Conf.relay], + relays: [conf.relay], balance: 0, // Newly created wallet, balance is zero. }; @@ -106,7 +106,7 @@ app.put('/wallet', requireNip44Signer, async (c) => { /** Gets a wallet, if it exists. */ app.get('/wallet', requireNip44Signer, swapNutzapsMiddleware, async (c) => { - const signer = c.get('signer'); + const { conf, signer } = c.var; const store = c.get('store'); const pubkey = await signer.getPublicKey(); const { signal } = c.req.raw; @@ -151,7 +151,7 @@ app.get('/wallet', requireNip44Signer, swapNutzapsMiddleware, async (c) => { const walletEntity: Wallet = { pubkey_p2pk: p2pk, mints, - relays: [Conf.relay], + relays: [conf.relay], balance, }; @@ -160,8 +160,10 @@ app.get('/wallet', requireNip44Signer, swapNutzapsMiddleware, async (c) => { /** Get mints set by the CASHU_MINTS environment variable. */ app.get('/mints', (c) => { + const { conf } = c.var; + // TODO: Return full Mint information: https://github.com/cashubtc/nuts/blob/main/06.md - const mints = Conf.cashuMints; + const mints = conf.cashuMints; return c.json({ mints }, 200); }); diff --git a/packages/ditto/controllers/api/ditto.ts b/packages/ditto/controllers/api/ditto.ts index 5022c141..9465517c 100644 --- a/packages/ditto/controllers/api/ditto.ts +++ b/packages/ditto/controllers/api/ditto.ts @@ -2,7 +2,6 @@ import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { z } from 'zod'; import { AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getAuthor } from '@/queries.ts'; import { addTag } from '@/utils/tags.ts'; @@ -30,10 +29,11 @@ const relaySchema = z.object({ type RelayEntity = z.infer; export const adminRelaysController: AppController = async (c) => { + const { conf } = c.var; const store = await Storages.db(); const [event] = await store.query([ - { kinds: [10002], authors: [Conf.pubkey], limit: 1 }, + { kinds: [10002], authors: [conf.pubkey], limit: 1 }, ]); if (!event) { @@ -82,6 +82,7 @@ export const nameRequestController: AppController = async (c) => { const store = await Storages.db(); const signer = c.get('signer')!; const pubkey = await signer.getPublicKey(); + const { conf } = c.var; const { name, reason } = nameRequestSchema.parse(await c.req.json()); @@ -97,7 +98,7 @@ export const nameRequestController: AppController = async (c) => { ['r', name], ['L', 'nip05.domain'], ['l', name.split('@')[1], 'nip05.domain'], - ['p', Conf.pubkey], + ['p', conf.pubkey], ], }, c); @@ -113,6 +114,7 @@ const nameRequestsSchema = z.object({ }); export const nameRequestsController: AppController = async (c) => { + const { conf } = c.var; const store = await Storages.db(); const signer = c.get('signer')!; const pubkey = await signer.getPublicKey(); @@ -122,7 +124,7 @@ export const nameRequestsController: AppController = async (c) => { const filter: NostrFilter = { kinds: [30383], - authors: [Conf.pubkey], + authors: [conf.pubkey], '#k': ['3036'], '#p': [pubkey], ...params, @@ -168,6 +170,7 @@ const zapSplitSchema = z.record( ); export const updateZapSplitsController: AppController = async (c) => { + const { conf } = c.var; const body = await parseBody(c.req.raw); const result = zapSplitSchema.safeParse(body); const store = c.get('store'); @@ -176,7 +179,7 @@ export const updateZapSplitsController: AppController = async (c) => { return c.json({ error: result.error }, 400); } - const dittoZapSplit = await getZapSplits(store, Conf.pubkey); + const dittoZapSplit = await getZapSplits(store, conf.pubkey); if (!dittoZapSplit) { return c.json({ error: 'Zap split not activated, restart the server.' }, 404); } @@ -189,7 +192,7 @@ export const updateZapSplitsController: AppController = async (c) => { } await updateListAdminEvent( - { kinds: [30078], authors: [Conf.pubkey], '#d': ['pub.ditto.zapSplits'], limit: 1 }, + { kinds: [30078], authors: [conf.pubkey], '#d': ['pub.ditto.zapSplits'], limit: 1 }, (tags) => pubkeys.reduce((accumulator, pubkey) => { return addTag(accumulator, ['p', pubkey, data[pubkey].weight.toString(), data[pubkey].message]); @@ -203,6 +206,7 @@ export const updateZapSplitsController: AppController = async (c) => { const deleteZapSplitSchema = z.array(n.id()).min(1); export const deleteZapSplitsController: AppController = async (c) => { + const { conf } = c.var; const body = await parseBody(c.req.raw); const result = deleteZapSplitSchema.safeParse(body); const store = c.get('store'); @@ -211,7 +215,7 @@ export const deleteZapSplitsController: AppController = async (c) => { return c.json({ error: result.error }, 400); } - const dittoZapSplit = await getZapSplits(store, Conf.pubkey); + const dittoZapSplit = await getZapSplits(store, conf.pubkey); if (!dittoZapSplit) { return c.json({ error: 'Zap split not activated, restart the server.' }, 404); } @@ -219,7 +223,7 @@ export const deleteZapSplitsController: AppController = async (c) => { const { data } = result; await updateListAdminEvent( - { kinds: [30078], authors: [Conf.pubkey], '#d': ['pub.ditto.zapSplits'], limit: 1 }, + { kinds: [30078], authors: [conf.pubkey], '#d': ['pub.ditto.zapSplits'], limit: 1 }, (tags) => data.reduce((accumulator, currentValue) => { return deleteTag(accumulator, ['p', currentValue]); @@ -231,9 +235,10 @@ export const deleteZapSplitsController: AppController = async (c) => { }; export const getZapSplitsController: AppController = async (c) => { + const { conf } = c.var; const store = c.get('store'); - const dittoZapSplit: DittoZapSplits | undefined = await getZapSplits(store, Conf.pubkey) ?? {}; + const dittoZapSplit: DittoZapSplits | undefined = await getZapSplits(store, conf.pubkey) ?? {}; if (!dittoZapSplit) { return c.json({ error: 'Zap split not activated, restart the server.' }, 404); } @@ -303,9 +308,10 @@ const updateInstanceSchema = z.object({ }); export const updateInstanceController: AppController = async (c) => { + const { conf } = c.var; const body = await parseBody(c.req.raw); const result = updateInstanceSchema.safeParse(body); - const pubkey = Conf.pubkey; + const pubkey = conf.pubkey; if (!result.success) { return c.json(result.error, 422); diff --git a/packages/ditto/controllers/api/instance.ts b/packages/ditto/controllers/api/instance.ts index 986537bb..d17a91c1 100644 --- a/packages/ditto/controllers/api/instance.ts +++ b/packages/ditto/controllers/api/instance.ts @@ -1,7 +1,6 @@ import denoJson from 'deno.json' with { type: 'json' }; import { AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { Storages } from '@/storages.ts'; import { getInstanceMetadata } from '@/utils/instance.ts'; @@ -17,7 +16,8 @@ const features = [ ]; const instanceV1Controller: AppController = async (c) => { - const { host, protocol } = Conf.url; + const { conf } = c.var; + const { host, protocol } = conf.url; const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal); /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ @@ -29,7 +29,7 @@ const instanceV1Controller: AppController = async (c) => { description: meta.about, short_description: meta.tagline, registrations: true, - max_toot_chars: Conf.postCharLimit, + max_toot_chars: conf.postCharLimit, configuration: { media_attachments: { image_size_limit: 100000000, @@ -42,7 +42,7 @@ const instanceV1Controller: AppController = async (c) => { min_expiration: 0, }, statuses: { - max_characters: Conf.postCharLimit, + max_characters: conf.postCharLimit, max_media_attachments: 20, }, }, @@ -50,9 +50,9 @@ const instanceV1Controller: AppController = async (c) => { metadata: { features, fields_limits: { - max_fields: Conf.profileFields.maxFields, - name_length: Conf.profileFields.nameLength, - value_length: Conf.profileFields.valueLength, + max_fields: conf.profileFields.maxFields, + name_length: conf.profileFields.nameLength, + value_length: conf.profileFields.valueLength, }, }, }, @@ -68,7 +68,7 @@ const instanceV1Controller: AppController = async (c) => { version, email: meta.email, nostr: { - pubkey: Conf.pubkey, + pubkey: conf.pubkey, relay: `${wsProtocol}//${host}/relay`, }, rules: [], @@ -76,7 +76,8 @@ const instanceV1Controller: AppController = async (c) => { }; const instanceV2Controller: AppController = async (c) => { - const { host, protocol } = Conf.url; + const { conf } = c.var; + const { host, protocol } = conf.url; const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal); /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ @@ -111,14 +112,14 @@ const instanceV2Controller: AppController = async (c) => { streaming: `${wsProtocol}//${host}`, }, vapid: { - public_key: await Conf.vapidPublicKey, + public_key: await conf.vapidPublicKey, }, accounts: { max_featured_tags: 10, max_pinned_statuses: 5, }, statuses: { - max_characters: Conf.postCharLimit, + max_characters: conf.postCharLimit, max_media_attachments: 20, characters_reserved_per_url: 23, }, @@ -136,20 +137,20 @@ const instanceV2Controller: AppController = async (c) => { max_expiration: 2629746, }, translation: { - enabled: Boolean(Conf.translationProvider), + enabled: Boolean(conf.translationProvider), }, }, nostr: { - pubkey: Conf.pubkey, + pubkey: conf.pubkey, relay: `${wsProtocol}//${host}/relay`, }, pleroma: { metadata: { features, fields_limits: { - max_fields: Conf.profileFields.maxFields, - name_length: Conf.profileFields.nameLength, - value_length: Conf.profileFields.valueLength, + max_fields: conf.profileFields.maxFields, + name_length: conf.profileFields.nameLength, + value_length: conf.profileFields.valueLength, }, }, }, diff --git a/packages/ditto/controllers/api/notifications.ts b/packages/ditto/controllers/api/notifications.ts index 1c251563..fd8b5720 100644 --- a/packages/ditto/controllers/api/notifications.ts +++ b/packages/ditto/controllers/api/notifications.ts @@ -2,7 +2,6 @@ import { NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { z } from 'zod'; import { AppContext, AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { DittoPagination } from '@/interfaces/DittoPagination.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { paginated } from '@/utils/api.ts'; @@ -31,6 +30,7 @@ const notificationsSchema = z.object({ }); const notificationsController: AppController = async (c) => { + const { conf } = c.var; const pubkey = await c.get('signer')?.getPublicKey()!; const params = c.get('pagination'); @@ -68,7 +68,7 @@ const notificationsController: AppController = async (c) => { } if (types.has('ditto:name_grant') && !account_id) { - filters.push({ kinds: [30360], authors: [Conf.pubkey], '#p': [pubkey], ...params }); + filters.push({ kinds: [30360], authors: [conf.pubkey], '#p': [pubkey], ...params }); } return renderNotifications(filters, types, params, c); @@ -105,10 +105,11 @@ async function renderNotifications( params: DittoPagination, c: AppContext, ) { + const { conf } = c.var; const store = c.get('store'); const pubkey = await c.get('signer')?.getPublicKey()!; const { signal } = c.req.raw; - const opts = { signal, limit: params.limit, timeout: Conf.db.timeouts.timelines }; + const opts = { signal, limit: params.limit, timeout: conf.db.timeouts.timelines }; const events = await store .query(filters, opts) diff --git a/packages/ditto/controllers/api/oauth.ts b/packages/ditto/controllers/api/oauth.ts index 2804df60..7ac2c2b2 100644 --- a/packages/ditto/controllers/api/oauth.ts +++ b/packages/ditto/controllers/api/oauth.ts @@ -4,7 +4,6 @@ import { generateSecretKey } from 'nostr-tools'; import { z } from 'zod'; import { AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { Storages } from '@/storages.ts'; import { nostrNow } from '@/utils.ts'; import { parseBody } from '@/utils/api.ts'; @@ -40,6 +39,7 @@ const createTokenSchema = z.discriminatedUnion('grant_type', [ ]); const createTokenController: AppController = async (c) => { + const { conf } = c.var; const body = await parseBody(c.req.raw); const result = createTokenSchema.safeParse(body); @@ -50,7 +50,7 @@ const createTokenController: AppController = async (c) => { switch (result.data.grant_type) { case 'nostr_bunker': return c.json({ - access_token: await getToken(result.data), + access_token: await getToken(result.data, conf.seckey), token_type: 'Bearer', scope: 'read write follow push', created_at: nostrNow(), @@ -112,6 +112,7 @@ const revokeTokenController: AppController = async (c) => { async function getToken( { pubkey: bunkerPubkey, secret, relays = [] }: { pubkey: string; secret?: string; relays?: string[] }, + dittoSeckey: Uint8Array, ): Promise<`token1${string}`> { const kysely = await Storages.kysely(); const { token, hash } = await generateToken(); @@ -133,7 +134,7 @@ async function getToken( token_hash: hash, pubkey: userPubkey, bunker_pubkey: bunkerPubkey, - nip46_sk_enc: await aesEncrypt(Conf.seckey, nip46Seckey), + nip46_sk_enc: await aesEncrypt(dittoSeckey, nip46Seckey), nip46_relays: relays, created_at: new Date(), }).execute(); @@ -143,6 +144,7 @@ async function getToken( /** Display the OAuth form. */ const oauthController: AppController = (c) => { + const { conf } = c.var; const encodedUri = c.req.query('redirect_uri'); if (!encodedUri) { return c.text('Missing `redirect_uri` query param.', 422); @@ -192,7 +194,7 @@ const oauthController: AppController = (c) => { -

Sign in with a Nostr bunker app. Please configure the app to use this relay: ${Conf.relay}

+

Sign in with a Nostr bunker app. Please configure the app to use this relay: ${conf.relay}

`); @@ -220,6 +222,8 @@ const oauthAuthorizeSchema = z.object({ /** Controller the OAuth form is POSTed to. */ const oauthAuthorizeController: AppController = async (c) => { + const { conf } = c.var; + /** FormData results in JSON. */ const result = oauthAuthorizeSchema.safeParse(await parseBody(c.req.raw)); @@ -236,7 +240,7 @@ const oauthAuthorizeController: AppController = async (c) => { pubkey: bunker.hostname, secret: bunker.searchParams.get('secret') || undefined, relays: bunker.searchParams.getAll('relay'), - }); + }, conf.seckey); if (redirectUri === 'urn:ietf:wg:oauth:2.0:oob') { return c.text(token); diff --git a/packages/ditto/controllers/api/pleroma.ts b/packages/ditto/controllers/api/pleroma.ts index d9289df1..976c2c0a 100644 --- a/packages/ditto/controllers/api/pleroma.ts +++ b/packages/ditto/controllers/api/pleroma.ts @@ -1,7 +1,6 @@ import { z } from 'zod'; import { type AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; @@ -34,7 +33,8 @@ const configController: AppController = async (c) => { /** Pleroma admin config controller. */ const updateConfigController: AppController = async (c) => { - const { pubkey } = Conf; + const { conf } = c.var; + const { pubkey } = conf; const store = await Storages.db(); const configs = await getPleromaConfigs(store, c.req.raw.signal); @@ -69,6 +69,7 @@ const pleromaAdminTagSchema = z.object({ }); const pleromaAdminTagController: AppController = async (c) => { + const { conf } = c.var; const params = pleromaAdminTagSchema.parse(await c.req.json()); for (const nickname of params.nicknames) { @@ -76,7 +77,7 @@ const pleromaAdminTagController: AppController = async (c) => { if (!pubkey) continue; await updateAdminEvent( - { kinds: [30382], authors: [Conf.pubkey], '#d': [pubkey], limit: 1 }, + { kinds: [30382], authors: [conf.pubkey], '#d': [pubkey], limit: 1 }, (prev) => { const tags = prev?.tags ?? [['d', pubkey]]; @@ -101,6 +102,7 @@ const pleromaAdminTagController: AppController = async (c) => { }; const pleromaAdminUntagController: AppController = async (c) => { + const { conf } = c.var; const params = pleromaAdminTagSchema.parse(await c.req.json()); for (const nickname of params.nicknames) { @@ -108,7 +110,7 @@ const pleromaAdminUntagController: AppController = async (c) => { if (!pubkey) continue; await updateAdminEvent( - { kinds: [30382], authors: [Conf.pubkey], '#d': [pubkey], limit: 1 }, + { kinds: [30382], authors: [conf.pubkey], '#d': [pubkey], limit: 1 }, (prev) => ({ kind: 30382, content: prev?.content ?? '', diff --git a/packages/ditto/controllers/api/push.ts b/packages/ditto/controllers/api/push.ts index 0fa7c107..79063622 100644 --- a/packages/ditto/controllers/api/push.ts +++ b/packages/ditto/controllers/api/push.ts @@ -3,7 +3,6 @@ import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { Storages } from '@/storages.ts'; import { parseBody } from '@/utils/api.ts'; import { getTokenHash } from '@/utils/auth.ts'; @@ -43,7 +42,8 @@ const pushSubscribeSchema = z.object({ }); export const pushSubscribeController: AppController = async (c) => { - const vapidPublicKey = await Conf.vapidPublicKey; + const { conf } = c.var; + const vapidPublicKey = await conf.vapidPublicKey; if (!vapidPublicKey) { return c.json({ error: 'The administrator of this server has not enabled Web Push notifications.' }, 404); @@ -97,7 +97,8 @@ export const pushSubscribeController: AppController = async (c) => { }; export const getSubscriptionController: AppController = async (c) => { - const vapidPublicKey = await Conf.vapidPublicKey; + const { conf } = c.var; + const vapidPublicKey = await conf.vapidPublicKey; if (!vapidPublicKey) { return c.json({ error: 'The administrator of this server has not enabled Web Push notifications.' }, 404); diff --git a/packages/ditto/controllers/api/reports.ts b/packages/ditto/controllers/api/reports.ts index 97d08751..b25e7233 100644 --- a/packages/ditto/controllers/api/reports.ts +++ b/packages/ditto/controllers/api/reports.ts @@ -2,7 +2,6 @@ import { NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { createEvent, paginated, parseBody, updateEventInfo } from '@/utils/api.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { renderAdminReport } from '@/views/mastodon/reports.ts'; @@ -19,6 +18,7 @@ const reportSchema = z.object({ /** https://docs.joinmastodon.org/methods/reports/#post */ const reportController: AppController = async (c) => { + const { conf } = c.var; const store = c.get('store'); const body = await parseBody(c.req.raw); const result = reportSchema.safeParse(body); @@ -36,7 +36,7 @@ const reportController: AppController = async (c) => { const tags = [ ['p', account_id, category], - ['P', Conf.pubkey], + ['P', conf.pubkey], ]; for (const status of status_ids) { @@ -61,6 +61,7 @@ const adminReportsSchema = z.object({ /** https://docs.joinmastodon.org/methods/admin/reports/#get */ const adminReportsController: AppController = async (c) => { + const { conf } = c.var; const store = c.get('store'); const viewerPubkey = await c.get('signer')?.getPublicKey(); @@ -69,7 +70,7 @@ const adminReportsController: AppController = async (c) => { const filter: NostrFilter = { kinds: [30383], - authors: [Conf.pubkey], + authors: [conf.pubkey], '#k': ['1984'], ...params, }; diff --git a/packages/ditto/controllers/api/statuses.ts b/packages/ditto/controllers/api/statuses.ts index 5573521b..7c2276c7 100644 --- a/packages/ditto/controllers/api/statuses.ts +++ b/packages/ditto/controllers/api/statuses.ts @@ -6,7 +6,6 @@ import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { DittoUpload, dittoUploads } from '@/DittoUploads.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts'; @@ -66,6 +65,7 @@ const statusController: AppController = async (c) => { }; const createStatusController: AppController = async (c) => { + const { conf } = c.var; const body = await parseBody(c.req.raw); const result = createStatusSchema.safeParse(body); const store = c.get('store'); @@ -97,12 +97,12 @@ const createStatusController: AppController = async (c) => { const root = rootId === ancestor.id ? ancestor : await store.query([{ ids: [rootId] }]).then(([event]) => event); if (root) { - tags.push(['e', root.id, Conf.relay, 'root', root.pubkey]); + tags.push(['e', root.id, conf.relay, 'root', root.pubkey]); } else { - tags.push(['e', rootId, Conf.relay, 'root']); + tags.push(['e', rootId, conf.relay, 'root']); } - tags.push(['e', ancestor.id, Conf.relay, 'reply', ancestor.pubkey]); + tags.push(['e', ancestor.id, conf.relay, 'reply', ancestor.pubkey]); } let quoted: DittoEvent | undefined; @@ -114,7 +114,7 @@ const createStatusController: AppController = async (c) => { return c.json({ error: 'Quoted post not found.' }, 404); } - tags.push(['q', quoted.id, Conf.relay, quoted.pubkey]); + tags.push(['q', quoted.id, conf.relay, quoted.pubkey]); } if (data.sensitive && data.spoiler_text) { @@ -162,7 +162,7 @@ const createStatusController: AppController = async (c) => { } try { - return `nostr:${nip19.nprofileEncode({ pubkey, relays: [Conf.relay] })}`; + return `nostr:${nip19.nprofileEncode({ pubkey, relays: [conf.relay] })}`; } catch { return match; } @@ -178,7 +178,7 @@ const createStatusController: AppController = async (c) => { } for (const pubkey of pubkeys) { - tags.push(['p', pubkey, Conf.relay]); + tags.push(['p', pubkey, conf.relay]); } for (const link of linkify.find(data.status ?? '')) { @@ -193,10 +193,10 @@ const createStatusController: AppController = async (c) => { const pubkey = await c.get('signer')?.getPublicKey()!; const author = pubkey ? await getAuthor(pubkey) : undefined; - if (Conf.zapSplitsEnabled) { + if (conf.zapSplitsEnabled) { const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content); const lnurl = getLnurl(meta); - const dittoZapSplit = await getZapSplits(store, Conf.pubkey); + const dittoZapSplit = await getZapSplits(store, conf.pubkey); if (lnurl && dittoZapSplit) { const totalSplit = Object.values(dittoZapSplit).reduce((total, { weight }) => total + weight, 0); for (const zapPubkey in dittoZapSplit) { @@ -204,7 +204,7 @@ const createStatusController: AppController = async (c) => { tags.push([ 'zap', zapPubkey, - Conf.relay, + conf.relay, (Math.max(0, 100 - totalSplit) + dittoZapSplit[zapPubkey].weight).toString(), ]); continue; @@ -212,13 +212,13 @@ const createStatusController: AppController = async (c) => { tags.push([ 'zap', zapPubkey, - Conf.relay, + conf.relay, dittoZapSplit[zapPubkey].weight.toString(), dittoZapSplit[zapPubkey].message, ]); } if (totalSplit && !dittoZapSplit[pubkey]) { - tags.push(['zap', pubkey, Conf.relay, Math.max(0, 100 - totalSplit).toString()]); + tags.push(['zap', pubkey, conf.relay, Math.max(0, 100 - totalSplit).toString()]); } } } @@ -235,7 +235,7 @@ const createStatusController: AppController = async (c) => { id: quoted.id, kind: quoted.kind, author: quoted.pubkey, - relays: [Conf.relay], + relays: [conf.relay], }); content += `nostr:${nevent}`; } @@ -265,6 +265,7 @@ const createStatusController: AppController = async (c) => { }; const deleteStatusController: AppController = async (c) => { + const { conf } = c.var; const id = c.req.param('id'); const pubkey = await c.get('signer')?.getPublicKey(); @@ -274,7 +275,7 @@ const deleteStatusController: AppController = async (c) => { if (event.pubkey === pubkey) { await createEvent({ kind: 5, - tags: [['e', id, Conf.relay, '', pubkey]], + tags: [['e', id, conf.relay, '', pubkey]], }, c); const author = await getAuthor(event.pubkey); @@ -324,6 +325,7 @@ const contextController: AppController = async (c) => { }; const favouriteController: AppController = async (c) => { + const { conf } = c.var; const id = c.req.param('id'); const store = await Storages.db(); const [target] = await store.query([{ ids: [id], kinds: [1, 20] }]); @@ -333,8 +335,8 @@ const favouriteController: AppController = async (c) => { kind: 7, content: '+', tags: [ - ['e', target.id, Conf.relay, '', target.pubkey], - ['p', target.pubkey, Conf.relay], + ['e', target.id, conf.relay, '', target.pubkey], + ['p', target.pubkey, conf.relay], ], }, c); @@ -364,6 +366,7 @@ const favouritedByController: AppController = (c) => { /** https://docs.joinmastodon.org/methods/statuses/#boost */ const reblogStatusController: AppController = async (c) => { + const { conf } = c.var; const eventId = c.req.param('id'); const { signal } = c.req.raw; @@ -378,8 +381,8 @@ const reblogStatusController: AppController = async (c) => { const reblogEvent = await createEvent({ kind: 6, tags: [ - ['e', event.id, Conf.relay, '', event.pubkey], - ['p', event.pubkey, Conf.relay], + ['e', event.id, conf.relay, '', event.pubkey], + ['p', event.pubkey, conf.relay], ], }, c); @@ -396,6 +399,7 @@ const reblogStatusController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#unreblog */ const unreblogStatusController: AppController = async (c) => { + const { conf } = c.var; const eventId = c.req.param('id'); const pubkey = await c.get('signer')?.getPublicKey()!; const store = await Storages.db(); @@ -415,7 +419,7 @@ const unreblogStatusController: AppController = async (c) => { await createEvent({ kind: 5, - tags: [['e', repostEvent.id, Conf.relay, '', repostEvent.pubkey]], + tags: [['e', repostEvent.id, conf.relay, '', repostEvent.pubkey]], }, c); return c.json(await renderStatus(event, { viewerPubkey: pubkey })); @@ -456,6 +460,7 @@ const quotesController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#bookmark */ const bookmarkController: AppController = async (c) => { + const { conf } = c.var; const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); @@ -467,7 +472,7 @@ const bookmarkController: AppController = async (c) => { if (event) { await updateListEvent( { kinds: [10003], authors: [pubkey], limit: 1 }, - (tags) => addTag(tags, ['e', event.id, Conf.relay, '', event.pubkey]), + (tags) => addTag(tags, ['e', event.id, conf.relay, '', event.pubkey]), c, ); @@ -483,6 +488,7 @@ const bookmarkController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#unbookmark */ const unbookmarkController: AppController = async (c) => { + const { conf } = c.var; const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); @@ -494,7 +500,7 @@ const unbookmarkController: AppController = async (c) => { if (event) { await updateListEvent( { kinds: [10003], authors: [pubkey], limit: 1 }, - (tags) => deleteTag(tags, ['e', event.id, Conf.relay, '', event.pubkey]), + (tags) => deleteTag(tags, ['e', event.id, conf.relay, '', event.pubkey]), c, ); @@ -510,6 +516,7 @@ const unbookmarkController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#pin */ const pinController: AppController = async (c) => { + const { conf } = c.var; const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); @@ -521,7 +528,7 @@ const pinController: AppController = async (c) => { if (event) { await updateListEvent( { kinds: [10001], authors: [pubkey], limit: 1 }, - (tags) => addTag(tags, ['e', event.id, Conf.relay, '', event.pubkey]), + (tags) => addTag(tags, ['e', event.id, conf.relay, '', event.pubkey]), c, ); @@ -537,6 +544,7 @@ const pinController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#unpin */ const unpinController: AppController = async (c) => { + const { conf } = c.var; const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); const { signal } = c.req.raw; @@ -550,7 +558,7 @@ const unpinController: AppController = async (c) => { if (event) { await updateListEvent( { kinds: [10001], authors: [pubkey], limit: 1 }, - (tags) => deleteTag(tags, ['e', event.id, Conf.relay, '', event.pubkey]), + (tags) => deleteTag(tags, ['e', event.id, conf.relay, '', event.pubkey]), c, ); @@ -572,6 +580,7 @@ const zapSchema = z.object({ }); const zapController: AppController = async (c) => { + const { conf } = c.var; const body = await parseBody(c.req.raw); const result = zapSchema.safeParse(body); const { signal } = c.req.raw; @@ -594,10 +603,10 @@ const zapController: AppController = async (c) => { lnurl = getLnurl(meta); if (target && lnurl) { tags.push( - ['e', target.id, Conf.relay], - ['p', target.pubkey, Conf.relay], + ['e', target.id, conf.relay], + ['p', target.pubkey, conf.relay], ['amount', amount.toString()], - ['relays', Conf.relay], + ['relays', conf.relay], ['lnurl', lnurl], ); } @@ -607,9 +616,9 @@ const zapController: AppController = async (c) => { lnurl = getLnurl(meta); if (target && lnurl) { tags.push( - ['p', target.pubkey, Conf.relay], + ['p', target.pubkey, conf.relay], ['amount', amount.toString()], - ['relays', Conf.relay], + ['relays', conf.relay], ['lnurl', lnurl], ); } diff --git a/packages/ditto/controllers/api/streaming.ts b/packages/ditto/controllers/api/streaming.ts index 405de96c..43bf92be 100644 --- a/packages/ditto/controllers/api/streaming.ts +++ b/packages/ditto/controllers/api/streaming.ts @@ -4,7 +4,6 @@ import { logi } from '@soapbox/logi'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { streamingClientMessagesCounter, streamingConnectionsGauge, @@ -69,6 +68,7 @@ const limiter = new TTLCache(); const connections = new Set(); const streamingController: AppController = async (c) => { + const { conf } = c.var; const upgrade = c.req.header('upgrade'); const token = c.req.header('sec-websocket-protocol'); const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream')); @@ -137,7 +137,7 @@ const streamingController: AppController = async (c) => { streamingConnectionsGauge.set(connections.size); if (!stream) return; - const topicFilter = await topicToFilter(stream, c.req.query(), pubkey); + const topicFilter = await topicToFilter(stream, c.req.query(), pubkey, conf.url.host); if (topicFilter) { sub([topicFilter], async (event) => { @@ -208,9 +208,8 @@ async function topicToFilter( topic: Stream, query: Record, pubkey: string | undefined, + host: string, ): Promise { - const { host } = Conf.url; - switch (topic) { case 'public': return { kinds: [1, 6, 20] }; diff --git a/packages/ditto/controllers/api/suggestions.ts b/packages/ditto/controllers/api/suggestions.ts index 0c887b12..0a85b95b 100644 --- a/packages/ditto/controllers/api/suggestions.ts +++ b/packages/ditto/controllers/api/suggestions.ts @@ -2,7 +2,6 @@ import { NostrFilter } from '@nostrify/nostrify'; import { matchFilter } from 'nostr-tools'; import { AppContext, AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { paginated, paginatedList } from '@/utils/api.ts'; import { getTagSet } from '@/utils/tags.ts'; @@ -24,6 +23,7 @@ export const suggestionsV2Controller: AppController = async (c) => { }; async function renderV2Suggestions(c: AppContext, params: { offset: number; limit: number }, signal?: AbortSignal) { + const { conf } = c.var; const { offset, limit } = params; const store = c.get('store'); @@ -31,8 +31,8 @@ async function renderV2Suggestions(c: AppContext, params: { offset: number; limi const pubkey = await signer?.getPublicKey(); const filters: NostrFilter[] = [ - { kinds: [30382], authors: [Conf.pubkey], '#n': ['suggested'], limit }, - { kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [Conf.pubkey], limit: 1 }, + { kinds: [30382], authors: [conf.pubkey], '#n': ['suggested'], limit }, + { kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [conf.pubkey], limit: 1 }, ]; if (pubkey) { @@ -43,11 +43,11 @@ async function renderV2Suggestions(c: AppContext, params: { offset: number; limi const events = await store.query(filters, { signal }); const [userEvents, followsEvent, mutesEvent, trendingEvent] = [ - events.filter((event) => matchFilter({ kinds: [30382], authors: [Conf.pubkey], '#n': ['suggested'] }, event)), + events.filter((event) => matchFilter({ kinds: [30382], authors: [conf.pubkey], '#n': ['suggested'] }, event)), pubkey ? events.find((event) => matchFilter({ kinds: [3], authors: [pubkey] }, event)) : undefined, pubkey ? events.find((event) => matchFilter({ kinds: [10000], authors: [pubkey] }, event)) : undefined, events.find((event) => - matchFilter({ kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [Conf.pubkey], limit: 1 }, event) + matchFilter({ kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [conf.pubkey], limit: 1 }, event) ), ]; @@ -89,12 +89,13 @@ async function renderV2Suggestions(c: AppContext, params: { offset: number; limi } export const localSuggestionsController: AppController = async (c) => { + const { conf } = c.var; const signal = c.req.raw.signal; const params = c.get('pagination'); const store = c.get('store'); const grants = await store.query( - [{ kinds: [30360], authors: [Conf.pubkey], ...params }], + [{ kinds: [30360], authors: [conf.pubkey], ...params }], { signal }, ); @@ -108,20 +109,20 @@ export const localSuggestionsController: AppController = async (c) => { } const profiles = await store.query( - [{ kinds: [0], authors: [...pubkeys], search: `domain:${Conf.url.host}`, ...params }], + [{ kinds: [0], authors: [...pubkeys], search: `domain:${conf.url.host}`, ...params }], { signal }, ) .then((events) => hydrateEvents({ store, events, signal })); - const suggestions = (await Promise.all([...pubkeys].map(async (pubkey) => { + const suggestions = [...pubkeys].map((pubkey) => { const profile = profiles.find((event) => event.pubkey === pubkey); if (!profile) return; return { source: 'global', - account: await renderAccount(profile), + account: renderAccount(profile), }; - }))).filter(Boolean); + }).filter(Boolean); return paginated(c, grants, suggestions); }; diff --git a/packages/ditto/controllers/api/timelines.ts b/packages/ditto/controllers/api/timelines.ts index f6bb8d37..e8b8987a 100644 --- a/packages/ditto/controllers/api/timelines.ts +++ b/packages/ditto/controllers/api/timelines.ts @@ -2,7 +2,6 @@ import { NostrFilter } from '@nostrify/nostrify'; import { z } from 'zod'; import { type AppContext, type AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { getFeedPubkeys } from '@/queries.ts'; import { booleanParamSchema, languageSchema } from '@/schema.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; @@ -53,6 +52,7 @@ const publicQuerySchema = z.object({ }); const publicTimelineController: AppController = (c) => { + const { conf } = c.var; const params = c.get('pagination'); const result = publicQuerySchema.safeParse(c.req.query()); @@ -67,7 +67,7 @@ const publicTimelineController: AppController = (c) => { const search: `${string}:${string}`[] = []; if (local) { - search.push(`domain:${Conf.url.host}`); + search.push(`domain:${conf.url.host}`); } else if (instance) { search.push(`domain:${instance}`); } @@ -90,11 +90,12 @@ const hashtagTimelineController: AppController = (c) => { }; const suggestedTimelineController: AppController = async (c) => { + const { conf } = c.var; const store = c.get('store'); const params = c.get('pagination'); const [follows] = await store.query( - [{ kinds: [3], authors: [Conf.pubkey], limit: 1 }], + [{ kinds: [3], authors: [conf.pubkey], limit: 1 }], ); const authors = [...getTagSet(follows?.tags ?? [], 'p')]; @@ -104,9 +105,10 @@ const suggestedTimelineController: AppController = async (c) => { /** Render statuses for timelines. */ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { + const { conf } = c.var; const { signal } = c.req.raw; const store = c.get('store'); - const opts = { signal, timeout: Conf.db.timeouts.timelines }; + const opts = { signal, timeout: conf.db.timeouts.timelines }; const events = await store .query(filters, opts) diff --git a/packages/ditto/controllers/api/trends.ts b/packages/ditto/controllers/api/trends.ts index c2577e13..88ea335e 100644 --- a/packages/ditto/controllers/api/trends.ts +++ b/packages/ditto/controllers/api/trends.ts @@ -1,3 +1,4 @@ +import { type DittoConf } from '@ditto/conf'; import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify'; import { logi } from '@soapbox/logi'; import { z } from 'zod'; @@ -13,7 +14,7 @@ import { paginated } from '@/utils/api.ts'; import { errorJson } from '@/utils/log.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; -let trendingHashtagsCache = getTrendingHashtags().catch((e: unknown) => { +let trendingHashtagsCache = getTrendingHashtags(Conf).catch((e: unknown) => { logi({ level: 'error', ns: 'ditto.trends.api', @@ -26,7 +27,7 @@ let trendingHashtagsCache = getTrendingHashtags().catch((e: unknown) => { Deno.cron('update trending hashtags cache', '35 * * * *', async () => { try { - const trends = await getTrendingHashtags(); + const trends = await getTrendingHashtags(Conf); trendingHashtagsCache = Promise.resolve(trends); } catch (e) { logi({ @@ -50,9 +51,9 @@ const trendingTagsController: AppController = async (c) => { return c.json(trends.slice(offset, offset + limit)); }; -async function getTrendingHashtags() { +async function getTrendingHashtags(conf: DittoConf) { const store = await Storages.db(); - const trends = await getTrendingTags(store, 't'); + const trends = await getTrendingTags(store, 't', conf.pubkey); return trends.map((trend) => { const hashtag = trend.value; @@ -65,13 +66,13 @@ async function getTrendingHashtags() { return { name: hashtag, - url: Conf.local(`/tags/${hashtag}`), + url: conf.local(`/tags/${hashtag}`), history, }; }); } -let trendingLinksCache = getTrendingLinks().catch((e: unknown) => { +let trendingLinksCache = getTrendingLinks(Conf).catch((e: unknown) => { logi({ level: 'error', ns: 'ditto.trends.api', @@ -84,7 +85,7 @@ let trendingLinksCache = getTrendingLinks().catch((e: unknown) => { Deno.cron('update trending links cache', '50 * * * *', async () => { try { - const trends = await getTrendingLinks(); + const trends = await getTrendingLinks(Conf); trendingLinksCache = Promise.resolve(trends); } catch (e) { logi({ @@ -103,9 +104,9 @@ const trendingLinksController: AppController = async (c) => { return c.json(trends.slice(offset, offset + limit)); }; -async function getTrendingLinks() { +async function getTrendingLinks(conf: DittoConf) { const store = await Storages.db(); - const trends = await getTrendingTags(store, 'r'); + const trends = await getTrendingTags(store, 'r', conf.pubkey); return Promise.all(trends.map(async (trend) => { const link = trend.value; @@ -139,6 +140,7 @@ async function getTrendingLinks() { } const trendingStatusesController: AppController = async (c) => { + const { conf } = c.var; const store = await Storages.db(); const { limit, offset, until } = paginationSchema.parse(c.req.query()); @@ -146,7 +148,7 @@ const trendingStatusesController: AppController = async (c) => { kinds: [1985], '#L': ['pub.ditto.trends'], '#l': ['#e'], - authors: [Conf.pubkey], + authors: [conf.pubkey], until, limit: 1, }]); @@ -185,12 +187,12 @@ interface TrendingTag { }[]; } -export async function getTrendingTags(store: NStore, tagName: string): Promise { +export async function getTrendingTags(store: NStore, tagName: string, pubkey: string): Promise { const [label] = await store.query([{ kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#${tagName}`], - authors: [Conf.pubkey], + authors: [pubkey], limit: 1, }]); @@ -213,7 +215,7 @@ export async function getTrendingTags(store: NStore, tagName: string): Promise { + const { conf } = c.var; const store = await Storages.db(); const meta = await getInstanceMetadata(store, c.req.raw.signal); @@ -14,7 +14,7 @@ const relayInfoController: AppController = async (c) => { return c.json({ name: meta.name, description: meta.about, - pubkey: Conf.pubkey, + pubkey: conf.pubkey, contact: meta.email, supported_nips: [1, 5, 9, 11, 16, 45, 50, 46, 98], software: 'Ditto', diff --git a/packages/ditto/controllers/nostr/relay.ts b/packages/ditto/controllers/nostr/relay.ts index e3e0b430..1b66415d 100644 --- a/packages/ditto/controllers/nostr/relay.ts +++ b/packages/ditto/controllers/nostr/relay.ts @@ -1,3 +1,4 @@ +import { type DittoConf } from '@ditto/conf'; import { logi } from '@soapbox/logi'; import { JsonValue } from '@std/json'; import { @@ -12,7 +13,6 @@ import { } from '@nostrify/nostrify'; import { AppController } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { relayInfoController } from '@/controllers/nostr/relay-info.ts'; import { relayConnectionsGauge, relayEventsCounter, relayMessagesCounter } from '@/metrics.ts'; import * as pipeline from '@/pipeline.ts'; @@ -47,7 +47,7 @@ const limiters = { const connections = new Set(); /** Set up the Websocket connection. */ -function connectStream(socket: WebSocket, ip: string | undefined) { +function connectStream(socket: WebSocket, ip: string | undefined, conf: DittoConf) { const controllers = new Map(); socket.onopen = () => { @@ -126,7 +126,7 @@ function connectStream(socket: WebSocket, ip: string | undefined) { const pubsub = await Storages.pubsub(); try { - for (const event of await store.query(filters, { limit: FILTER_LIMIT, timeout: Conf.db.timeouts.relay })) { + for (const event of await store.query(filters, { limit: FILTER_LIMIT, timeout: conf.db.timeouts.relay })) { send(['EVENT', subId, purifyEvent(event)]); } } catch (e) { @@ -188,7 +188,7 @@ function connectStream(socket: WebSocket, ip: string | undefined) { async function handleCount([_, subId, ...filters]: NostrClientCOUNT): Promise { if (rateLimited(limiters.req)) return; const store = await Storages.db(); - const { count } = await store.count(filters, { timeout: Conf.db.timeouts.relay }); + const { count } = await store.count(filters, { timeout: conf.db.timeouts.relay }); send(['COUNT', subId, { count, approximate: false }]); } @@ -201,6 +201,7 @@ function connectStream(socket: WebSocket, ip: string | undefined) { } const relayController: AppController = (c, next) => { + const { conf } = c.var; const upgrade = c.req.header('upgrade'); // NIP-11: https://github.com/nostr-protocol/nips/blob/master/11.md @@ -214,7 +215,7 @@ const relayController: AppController = (c, next) => { let ip = c.req.header('x-real-ip'); - if (ip && Conf.ipWhitelist.includes(ip)) { + if (ip && conf.ipWhitelist.includes(ip)) { ip = undefined; } @@ -229,7 +230,7 @@ const relayController: AppController = (c, next) => { } const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { idleTimeout: 30 }); - connectStream(socket, ip); + connectStream(socket, ip, conf); return response; }; diff --git a/packages/ditto/controllers/well-known/nodeinfo.ts b/packages/ditto/controllers/well-known/nodeinfo.ts index 4f03f425..bd446ce9 100644 --- a/packages/ditto/controllers/well-known/nodeinfo.ts +++ b/packages/ditto/controllers/well-known/nodeinfo.ts @@ -1,17 +1,17 @@ -import { Conf } from '@/config.ts'; - import type { AppController } from '@/app.ts'; const nodeInfoController: AppController = (c) => { + const { conf } = c.var; + return c.json({ links: [ { rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', - href: Conf.local('/nodeinfo/2.0'), + href: conf.local('/nodeinfo/2.0'), }, { rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', - href: Conf.local('/nodeinfo/2.1'), + href: conf.local('/nodeinfo/2.1'), }, ], }); From 8d2c83bb09e8c9b36749e209e65eb1f0d11c030d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 18:38:20 -0600 Subject: [PATCH 11/13] Remove Conf from S3Uploader, uploaderMiddleware --- .../ditto/middleware/uploaderMiddleware.ts | 32 +++++++++---------- packages/ditto/uploaders/S3Uploader.ts | 9 +++--- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/ditto/middleware/uploaderMiddleware.ts b/packages/ditto/middleware/uploaderMiddleware.ts index 6866b883..056106c1 100644 --- a/packages/ditto/middleware/uploaderMiddleware.ts +++ b/packages/ditto/middleware/uploaderMiddleware.ts @@ -2,44 +2,44 @@ import { BlossomUploader, NostrBuildUploader } from '@nostrify/nostrify/uploader import { safeFetch } from '@soapbox/safe-fetch'; import { AppMiddleware } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { DenoUploader } from '@/uploaders/DenoUploader.ts'; import { IPFSUploader } from '@/uploaders/IPFSUploader.ts'; import { S3Uploader } from '@/uploaders/S3Uploader.ts'; /** Set an uploader for the user. */ export const uploaderMiddleware: AppMiddleware = async (c, next) => { - const signer = c.get('signer'); + const { signer, conf } = c.var; - switch (Conf.uploader) { + switch (conf.uploader) { case 's3': c.set( 'uploader', new S3Uploader({ - accessKey: Conf.s3.accessKey, - bucket: Conf.s3.bucket, - endPoint: Conf.s3.endPoint!, - pathStyle: Conf.s3.pathStyle, - port: Conf.s3.port, - region: Conf.s3.region!, - secretKey: Conf.s3.secretKey, - sessionToken: Conf.s3.sessionToken, - useSSL: Conf.s3.useSSL, + accessKey: conf.s3.accessKey, + bucket: conf.s3.bucket, + endPoint: conf.s3.endPoint!, + pathStyle: conf.s3.pathStyle, + port: conf.s3.port, + region: conf.s3.region!, + secretKey: conf.s3.secretKey, + sessionToken: conf.s3.sessionToken, + useSSL: conf.s3.useSSL, + baseUrl: conf.mediaDomain, }), ); break; case 'ipfs': - c.set('uploader', new IPFSUploader({ baseUrl: Conf.mediaDomain, apiUrl: Conf.ipfs.apiUrl, fetch: safeFetch })); + c.set('uploader', new IPFSUploader({ baseUrl: conf.mediaDomain, apiUrl: conf.ipfs.apiUrl, fetch: safeFetch })); break; case 'local': - c.set('uploader', new DenoUploader({ baseUrl: Conf.mediaDomain, dir: Conf.uploadsDir })); + c.set('uploader', new DenoUploader({ baseUrl: conf.mediaDomain, dir: conf.uploadsDir })); break; case 'nostrbuild': - c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, signer, fetch: safeFetch })); + c.set('uploader', new NostrBuildUploader({ endpoint: conf.nostrbuildEndpoint, signer, fetch: safeFetch })); break; case 'blossom': if (signer) { - c.set('uploader', new BlossomUploader({ servers: Conf.blossomServers, signer, fetch: safeFetch })); + c.set('uploader', new BlossomUploader({ servers: conf.blossomServers, signer, fetch: safeFetch })); } break; } diff --git a/packages/ditto/uploaders/S3Uploader.ts b/packages/ditto/uploaders/S3Uploader.ts index b74796ab..c0d776f8 100644 --- a/packages/ditto/uploaders/S3Uploader.ts +++ b/packages/ditto/uploaders/S3Uploader.ts @@ -6,8 +6,6 @@ import { crypto } from '@std/crypto'; import { encodeHex } from '@std/encoding/hex'; import { extensionsByType } from '@std/media-types'; -import { Conf } from '@/config.ts'; - export interface S3UploaderOpts { endPoint: string; region: string; @@ -18,13 +16,14 @@ export interface S3UploaderOpts { port?: number; sessionToken?: string; useSSL?: boolean; + baseUrl?: string; } /** S3-compatible uploader for AWS, Wasabi, DigitalOcean Spaces, and more. */ export class S3Uploader implements NUploader { private client: S3Client; - constructor(opts: S3UploaderOpts) { + constructor(private opts: S3UploaderOpts) { this.client = new S3Client(opts); } @@ -40,10 +39,10 @@ export class S3Uploader implements NUploader { }, }); - const { pathStyle, bucket } = Conf.s3; + const { pathStyle, bucket, baseUrl } = this.opts; const path = (pathStyle && bucket) ? join(bucket, filename) : filename; - const url = new URL(path, Conf.mediaDomain).toString(); + const url = new URL(path, baseUrl).toString(); return [ ['url', url], From d0d37f5948c52c034e2c4bee510dec1d66555545 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 18:43:59 -0600 Subject: [PATCH 12/13] Remove Conf from middleware --- packages/ditto/middleware/auth98Middleware.ts | 6 +++--- packages/ditto/middleware/cspMiddleware.ts | 4 ++-- packages/ditto/middleware/rateLimitMiddleware.ts | 8 ++++---- packages/ditto/middleware/signerMiddleware.ts | 10 +++++++--- packages/ditto/middleware/swapNutzapsMiddleware.ts | 9 +++++---- packages/ditto/middleware/translatorMiddleware.ts | 9 +++++---- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/ditto/middleware/auth98Middleware.ts b/packages/ditto/middleware/auth98Middleware.ts index 85557151..889e5ea9 100644 --- a/packages/ditto/middleware/auth98Middleware.ts +++ b/packages/ditto/middleware/auth98Middleware.ts @@ -11,7 +11,6 @@ import { type ParseAuthRequestOpts, validateAuthEvent, } from '@/utils/nip98.ts'; -import { Conf } from '@/config.ts'; /** * NIP-98 auth. @@ -35,12 +34,13 @@ type UserRole = 'user' | 'admin'; /** Require the user to prove their role before invoking the controller. */ function requireRole(role: UserRole, opts?: ParseAuthRequestOpts): AppMiddleware { - return withProof(async (_c, proof, next) => { + return withProof(async (c, proof, next) => { + const { conf } = c.var; const store = await Storages.db(); const [user] = await store.query([{ kinds: [30382], - authors: [Conf.pubkey], + authors: [conf.pubkey], '#d': [proof.pubkey], limit: 1, }]); diff --git a/packages/ditto/middleware/cspMiddleware.ts b/packages/ditto/middleware/cspMiddleware.ts index 70c9316d..e16829cc 100644 --- a/packages/ditto/middleware/cspMiddleware.ts +++ b/packages/ditto/middleware/cspMiddleware.ts @@ -1,5 +1,4 @@ import { AppMiddleware } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { PleromaConfigDB } from '@/utils/PleromaConfigDB.ts'; import { Storages } from '@/storages.ts'; import { getPleromaConfigs } from '@/utils/pleroma.ts'; @@ -8,13 +7,14 @@ let configDBCache: Promise | undefined; export const cspMiddleware = (): AppMiddleware => { return async (c, next) => { + const { conf } = c.var; const store = await Storages.db(); if (!configDBCache) { configDBCache = getPleromaConfigs(store); } - const { host, protocol, origin } = Conf.url; + const { host, protocol, origin } = conf.url; const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; const configDB = await configDBCache; const sentryDsn = configDB.getIn(':pleroma', ':frontend_configurations', ':soapbox_fe', 'sentryDsn'); diff --git a/packages/ditto/middleware/rateLimitMiddleware.ts b/packages/ditto/middleware/rateLimitMiddleware.ts index 4d243d2c..651598b4 100644 --- a/packages/ditto/middleware/rateLimitMiddleware.ts +++ b/packages/ditto/middleware/rateLimitMiddleware.ts @@ -1,14 +1,13 @@ +import { type DittoConf } from '@ditto/conf'; import { MiddlewareHandler } from '@hono/hono'; import { rateLimiter } from 'hono-rate-limiter'; -import { Conf } from '@/config.ts'; - /** * Rate limit middleware for Hono, based on [`hono-rate-limiter`](https://github.com/rhinobase/hono-rate-limiter). */ export function rateLimitMiddleware(limit: number, windowMs: number, includeHeaders?: boolean): MiddlewareHandler { // @ts-ignore Mismatched hono versions. - return rateLimiter({ + return rateLimiter<{ Variables: { conf: DittoConf } }>({ limit, windowMs, standardHeaders: includeHeaders, @@ -17,8 +16,9 @@ export function rateLimitMiddleware(limit: number, windowMs: number, includeHead return c.text('Too many requests, please try again later.', 429); }, skip: (c) => { + const { conf } = c.var; const ip = c.req.header('x-real-ip'); - return !ip || Conf.ipWhitelist.includes(ip); + return !ip || conf.ipWhitelist.includes(ip); }, keyGenerator: (c) => c.req.header('x-real-ip')!, }); diff --git a/packages/ditto/middleware/signerMiddleware.ts b/packages/ditto/middleware/signerMiddleware.ts index aa7b537f..deea86b3 100644 --- a/packages/ditto/middleware/signerMiddleware.ts +++ b/packages/ditto/middleware/signerMiddleware.ts @@ -1,9 +1,9 @@ +import { type DittoConf } from '@ditto/conf'; import { MiddlewareHandler } from '@hono/hono'; import { HTTPException } from '@hono/hono/http-exception'; import { NostrSigner, NSecSigner } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; -import { Conf } from '@/config.ts'; import { ConnectSigner } from '@/signers/ConnectSigner.ts'; import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts'; import { Storages } from '@/storages.ts'; @@ -14,7 +14,11 @@ import { getTokenHash } from '@/utils/auth.ts'; const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); /** Make a `signer` object available to all controllers, or unset if the user isn't logged in. */ -export const signerMiddleware: MiddlewareHandler<{ Variables: { signer: NostrSigner } }> = async (c, next) => { +export const signerMiddleware: MiddlewareHandler<{ Variables: { signer: NostrSigner; conf: DittoConf } }> = async ( + c, + next, +) => { + const { conf } = c.var; const header = c.req.header('authorization'); const match = header?.match(BEARER_REGEX); @@ -32,7 +36,7 @@ export const signerMiddleware: MiddlewareHandler<{ Variables: { signer: NostrSig .where('token_hash', '=', tokenHash) .executeTakeFirstOrThrow(); - const nep46Seckey = await aesDecrypt(Conf.seckey, nip46_sk_enc); + const nep46Seckey = await aesDecrypt(conf.seckey, nip46_sk_enc); c.set( 'signer', diff --git a/packages/ditto/middleware/swapNutzapsMiddleware.ts b/packages/ditto/middleware/swapNutzapsMiddleware.ts index b24dee80..aa68c1c1 100644 --- a/packages/ditto/middleware/swapNutzapsMiddleware.ts +++ b/packages/ditto/middleware/swapNutzapsMiddleware.ts @@ -1,4 +1,5 @@ import { CashuMint, CashuWallet, getEncodedToken, type Proof } from '@cashu/cashu-ts'; +import { type DittoConf } from '@ditto/conf'; import { MiddlewareHandler } from '@hono/hono'; import { HTTPException } from '@hono/hono/http-exception'; import { getPublicKey } from 'nostr-tools'; @@ -9,7 +10,6 @@ import { logi } from '@soapbox/logi'; import { isNostrId } from '@/utils.ts'; import { errorJson } from '@/utils/log.ts'; -import { Conf } from '@/config.ts'; import { createEvent } from '@/utils/api.ts'; import { z } from 'zod'; @@ -18,8 +18,9 @@ import { z } from 'zod'; * Errors are only thrown if 'signer' and 'store' middlewares are not set. */ export const swapNutzapsMiddleware: MiddlewareHandler< - { Variables: { signer: SetRequired; store: NStore } } + { Variables: { signer: SetRequired; store: NStore; conf: DittoConf } } > = async (c, next) => { + const { conf } = c.var; const signer = c.get('signer'); const store = c.get('store'); @@ -133,7 +134,7 @@ export const swapNutzapsMiddleware: MiddlewareHandler< [ 'e', // nutzap event that has been redeemed event.id, - Conf.relay, + conf.relay, 'redeemed', ], ['p', event.pubkey], // pubkey of the author of the 9321 event (nutzap sender) @@ -173,7 +174,7 @@ export const swapNutzapsMiddleware: MiddlewareHandler< JSON.stringify([ ['direction', 'in'], ['amount', amount], - ['e', unspentProofs.id, Conf.relay, 'created'], + ['e', unspentProofs.id, conf.relay, 'created'], ]), ), tags: mintsToProofs[mint].redeemed, diff --git a/packages/ditto/middleware/translatorMiddleware.ts b/packages/ditto/middleware/translatorMiddleware.ts index ef123dab..eb97ae44 100644 --- a/packages/ditto/middleware/translatorMiddleware.ts +++ b/packages/ditto/middleware/translatorMiddleware.ts @@ -1,15 +1,16 @@ import { safeFetch } from '@soapbox/safe-fetch'; import { AppMiddleware } from '@/app.ts'; -import { Conf } from '@/config.ts'; import { DeepLTranslator } from '@/translators/DeepLTranslator.ts'; import { LibreTranslateTranslator } from '@/translators/LibreTranslateTranslator.ts'; /** Set the translator used for translating posts. */ export const translatorMiddleware: AppMiddleware = async (c, next) => { - switch (Conf.translationProvider) { + const { conf } = c.var; + + switch (conf.translationProvider) { case 'deepl': { - const { deeplApiKey: apiKey, deeplBaseUrl: baseUrl } = Conf; + const { deeplApiKey: apiKey, deeplBaseUrl: baseUrl } = conf; if (apiKey) { c.set('translator', new DeepLTranslator({ baseUrl, apiKey, fetch: safeFetch })); } @@ -17,7 +18,7 @@ export const translatorMiddleware: AppMiddleware = async (c, next) => { } case 'libretranslate': { - const { libretranslateApiKey: apiKey, libretranslateBaseUrl: baseUrl } = Conf; + const { libretranslateApiKey: apiKey, libretranslateBaseUrl: baseUrl } = conf; if (apiKey) { c.set('translator', new LibreTranslateTranslator({ baseUrl, apiKey, fetch: safeFetch })); } From 3073777d9b506c5225a2a9c46264a5cc4749d2b1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Feb 2025 18:51:15 -0600 Subject: [PATCH 13/13] Fix cashu tests --- packages/ditto/controllers/api/cashu.test.ts | 26 ++++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/ditto/controllers/api/cashu.test.ts b/packages/ditto/controllers/api/cashu.test.ts index 5aaa772c..773e9800 100644 --- a/packages/ditto/controllers/api/cashu.test.ts +++ b/packages/ditto/controllers/api/cashu.test.ts @@ -1,3 +1,4 @@ +import { confMw } from '@ditto/api/middleware'; import { Env as HonoEnv, Hono } from '@hono/hono'; import { NostrSigner, NSecSigner, NStore } from '@nostrify/nostrify'; import { generateSecretKey, getPublicKey } from 'nostr-tools'; @@ -40,7 +41,10 @@ Deno.test('PUT /wallet must be successful', { c.set('store', store); await next(); }, - ).route('/', cashuApp); + ); + + app.use(confMw(new Map())); + app.route('/', cashuApp); const response = await app.request('/wallet', { method: 'PUT', @@ -116,7 +120,10 @@ Deno.test('PUT /wallet must NOT be successful: wrong request body/schema', async c.set('store', store); await next(); }, - ).route('/', cashuApp); + ); + + app.use(confMw(new Map())); + app.route('/', cashuApp); const response = await app.request('/wallet', { method: 'PUT', @@ -149,7 +156,10 @@ Deno.test('PUT /wallet must NOT be successful: wallet already exists', async () c.set('store', store); await next(); }, - ).route('/', cashuApp); + ); + + app.use(confMw(new Map())); + app.route('/', cashuApp); await db.store.event(genEvent({ kind: 17375 }, sk)); @@ -187,7 +197,10 @@ Deno.test('GET /wallet must be successful', async () => { c.set('store', store); await next(); }, - ).route('/', cashuApp); + ); + + app.use(confMw(new Map())); + app.route('/', cashuApp); // Wallet await db.store.event(genEvent({ @@ -290,7 +303,10 @@ Deno.test('GET /mints must be successful', async () => { c.set('store', store); await next(); }, - ).route('/', cashuApp); + ); + + app.use(confMw(new Map())); + app.route('/', cashuApp); const response = await app.request('/mints', { method: 'GET',