From ee7864da8c191a626e5b579aa6bf2ef86d9576cc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 11:30:45 -0500 Subject: [PATCH 01/11] Add a signerMiddleware --- src/app.ts | 17 ++++++++++++++--- src/middleware/signerMiddleware.ts | 13 +++++++++++++ src/signers/APISigner.ts | 1 + 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 src/middleware/signerMiddleware.ts diff --git a/src/app.ts b/src/app.ts index 05a1c81..4f93608 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NStore } from '@nostrify/nostrify'; +import { NostrEvent, NostrSigner, NStore } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; @@ -29,6 +29,7 @@ import { adminAccountAction, adminAccountsController } from '@/controllers/api/a import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts'; import { blocksController } from '@/controllers/api/blocks.ts'; import { bookmarksController } from '@/controllers/api/bookmarks.ts'; +import { adminRelaysController, adminSetRelaysController } from '@/controllers/api/ditto.ts'; import { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts'; import { instanceController } from '@/controllers/api/instance.ts'; import { markersController, updateMarkersController } from '@/controllers/api/markers.ts'; @@ -84,13 +85,15 @@ import { auth19, requirePubkey } from '@/middleware/auth19.ts'; import { auth98, requireProof, requireRole } from '@/middleware/auth98.ts'; import { cache } from '@/middleware/cache.ts'; import { csp } from '@/middleware/csp.ts'; -import { adminRelaysController, adminSetRelaysController } from '@/controllers/api/ditto.ts'; +import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; import { storeMiddleware } from '@/middleware/store.ts'; import { blockController } from '@/controllers/api/accounts.ts'; import { unblockController } from '@/controllers/api/accounts.ts'; interface AppEnv extends HonoEnv { Variables: { + /** 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; /** Hex pubkey for the current user. If provided, the user is considered "logged in." */ pubkey?: string; /** Hex secret key for the current user. Optional, but easiest way to use legacy Mastodon apps. */ @@ -123,7 +126,15 @@ app.get('/api/v1/streaming', streamingController); app.get('/api/v1/streaming/', streamingController); app.get('/relay', relayController); -app.use('*', csp(), cors({ origin: '*', exposeHeaders: ['link'] }), auth19, auth98(), storeMiddleware); +app.use( + '*', + csp(), + cors({ origin: '*', exposeHeaders: ['link'] }), + auth19, + auth98(), + storeMiddleware, + signerMiddleware, +); app.get('/.well-known/webfinger', webfingerController); app.get('/.well-known/host-meta', hostMetaController); diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts new file mode 100644 index 0000000..8e7eb7a --- /dev/null +++ b/src/middleware/signerMiddleware.ts @@ -0,0 +1,13 @@ +import { AppMiddleware } from '@/app.ts'; +import { APISigner } from '@/signers/APISigner.ts'; + +/** Make a `signer` object available to all controllers, or unset if the user isn't logged in. */ +export const signerMiddleware: AppMiddleware = async (c, next) => { + try { + c.set('signer', new APISigner(c)); + } catch { + // do nothing + } + + await next(); +}; diff --git a/src/signers/APISigner.ts b/src/signers/APISigner.ts index e9914b1..0a9317b 100644 --- a/src/signers/APISigner.ts +++ b/src/signers/APISigner.ts @@ -2,6 +2,7 @@ import { NConnectSigner, NostrEvent, NostrSigner, NSecSigner } from '@nostrify/nostrify'; import { HTTPException } from 'hono'; + import { type AppContext } from '@/app.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; From 5a2b1b7de7aec546abed2c1f8fd02e4dabcbe4df Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 11:40:20 -0500 Subject: [PATCH 02/11] Destroy everything --- src/app.ts | 7 +--- src/middleware/auth19.ts | 49 ---------------------- src/middleware/signerMiddleware.ts | 41 ++++++++++++++++--- src/signers/APISigner.ts | 66 ------------------------------ 4 files changed, 37 insertions(+), 126 deletions(-) delete mode 100644 src/middleware/auth19.ts delete mode 100644 src/signers/APISigner.ts diff --git a/src/app.ts b/src/app.ts index 4f93608..057bded 100644 --- a/src/app.ts +++ b/src/app.ts @@ -81,7 +81,7 @@ import { hostMetaController } from '@/controllers/well-known/host-meta.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; import { webfingerController } from '@/controllers/well-known/webfinger.ts'; -import { auth19, requirePubkey } from '@/middleware/auth19.ts'; +import { requirePubkey } from '@/middleware/auth19.ts'; import { auth98, requireProof, requireRole } from '@/middleware/auth98.ts'; import { cache } from '@/middleware/cache.ts'; import { csp } from '@/middleware/csp.ts'; @@ -94,10 +94,6 @@ interface AppEnv extends HonoEnv { Variables: { /** 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; - /** Hex pubkey for the current user. If provided, the user is considered "logged in." */ - pubkey?: string; - /** Hex secret key for the current user. Optional, but easiest way to use legacy Mastodon apps. */ - seckey?: Uint8Array; /** NIP-98 signed event proving the pubkey is owned by the user. */ proof?: NostrEvent; /** User associated with the pubkey, if any. */ @@ -130,7 +126,6 @@ app.use( '*', csp(), cors({ origin: '*', exposeHeaders: ['link'] }), - auth19, auth98(), storeMiddleware, signerMiddleware, diff --git a/src/middleware/auth19.ts b/src/middleware/auth19.ts deleted file mode 100644 index 90fc444..0000000 --- a/src/middleware/auth19.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { HTTPException } from 'hono'; -import { getPublicKey, nip19 } from 'nostr-tools'; - -import { type AppMiddleware } from '@/app.ts'; - -/** We only accept "Bearer" type. */ -const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); - -/** NIP-19 auth middleware. */ -const auth19: AppMiddleware = async (c, next) => { - const authHeader = c.req.header('authorization'); - const match = authHeader?.match(BEARER_REGEX); - - if (match) { - const [_, bech32] = match; - - try { - const decoded = nip19.decode(bech32!); - - switch (decoded.type) { - case 'npub': - c.set('pubkey', decoded.data); - break; - case 'nprofile': - c.set('pubkey', decoded.data.pubkey); - break; - case 'nsec': - c.set('pubkey', getPublicKey(decoded.data)); - c.set('seckey', decoded.data); - break; - } - } catch (_e) { - // - } - } - - await next(); -}; - -/** Throw a 401 if the pubkey isn't set. */ -const requirePubkey: AppMiddleware = async (c, next) => { - if (!c.get('pubkey')) { - throw new HTTPException(401, { message: 'No pubkey provided' }); - } - - await next(); -}; - -export { auth19, requirePubkey }; diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index 8e7eb7a..d056391 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -1,12 +1,43 @@ +import { NConnectSigner, NSecSigner } from '@nostrify/nostrify'; +import { nip19 } from 'nostr-tools'; + import { AppMiddleware } from '@/app.ts'; -import { APISigner } from '@/signers/APISigner.ts'; +import { AdminSigner } from '@/signers/AdminSigner.ts'; +import { Storages } from '@/storages.ts'; + +/** We only accept "Bearer" type. */ +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: AppMiddleware = async (c, next) => { - try { - c.set('signer', new APISigner(c)); - } catch { - // do nothing + const header = c.req.header('authorization'); + const match = header?.match(BEARER_REGEX); + + if (match) { + const [_, bech32] = match; + + try { + const decoded = nip19.decode(bech32!); + + switch (decoded.type) { + case 'npub': + c.set( + 'signer', + new NConnectSigner({ + pubkey: decoded.data, + relay: Storages.pubsub, + signer: new AdminSigner(), + timeout: 60000, + }), + ); + break; + case 'nsec': + c.set('signer', new NSecSigner(decoded.data)); + break; + } + } catch { + // the user is not logged in + } } await next(); diff --git a/src/signers/APISigner.ts b/src/signers/APISigner.ts deleted file mode 100644 index 0a9317b..0000000 --- a/src/signers/APISigner.ts +++ /dev/null @@ -1,66 +0,0 @@ -// deno-lint-ignore-file require-await - -import { NConnectSigner, NostrEvent, NostrSigner, NSecSigner } from '@nostrify/nostrify'; -import { HTTPException } from 'hono'; - -import { type AppContext } from '@/app.ts'; -import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { Storages } from '@/storages.ts'; - -/** - * Sign Nostr event using the app context. - * - * - If a secret key is provided, it will be used to sign the event. - * - Otherwise, it will use NIP-46 to sign the event. - */ -export class APISigner implements NostrSigner { - private signer: NostrSigner; - - constructor(c: AppContext) { - const seckey = c.get('seckey'); - const pubkey = c.get('pubkey'); - - if (!pubkey) { - throw new HTTPException(401, { message: 'Missing pubkey' }); - } - - if (seckey) { - this.signer = new NSecSigner(seckey); - } else { - this.signer = new NConnectSigner({ - pubkey, - relay: Storages.pubsub, - signer: new AdminSigner(), - timeout: 60000, - }); - } - } - - async getPublicKey(): Promise { - return this.signer.getPublicKey(); - } - - async signEvent(event: Omit): Promise { - return this.signer.signEvent(event); - } - - readonly nip04 = { - encrypt: async (pubkey: string, plaintext: string): Promise => { - return this.signer.nip04!.encrypt(pubkey, plaintext); - }, - - decrypt: async (pubkey: string, ciphertext: string): Promise => { - return this.signer.nip04!.decrypt(pubkey, ciphertext); - }, - }; - - readonly nip44 = { - encrypt: async (pubkey: string, plaintext: string): Promise => { - return this.signer.nip44!.encrypt(pubkey, plaintext); - }, - - decrypt: async (pubkey: string, ciphertext: string): Promise => { - return this.signer.nip44!.decrypt(pubkey, ciphertext); - }, - }; -} From c5fbe69b80c2de965614e404742da8b96a1e6836 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 11:42:53 -0500 Subject: [PATCH 03/11] requirePubkey -> requireSigner --- src/app.ts | 62 ++++++++++++++++----------------- src/middleware/requireSigner.ts | 12 +++++++ 2 files changed, 43 insertions(+), 31 deletions(-) create mode 100644 src/middleware/requireSigner.ts diff --git a/src/app.ts b/src/app.ts index 057bded..5ef5056 100644 --- a/src/app.ts +++ b/src/app.ts @@ -81,10 +81,10 @@ import { hostMetaController } from '@/controllers/well-known/host-meta.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; import { webfingerController } from '@/controllers/well-known/webfinger.ts'; -import { requirePubkey } from '@/middleware/auth19.ts'; import { auth98, requireProof, requireRole } from '@/middleware/auth98.ts'; import { cache } from '@/middleware/cache.ts'; import { csp } from '@/middleware/csp.ts'; +import { requireSigner } from '@/middleware/requireSigner.ts'; import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; import { storeMiddleware } from '@/middleware/store.ts'; import { blockController } from '@/controllers/api/accounts.ts'; @@ -151,17 +151,17 @@ app.post('/oauth/authorize', oauthAuthorizeController); app.get('/oauth/authorize', oauthController); app.post('/api/v1/accounts', requireProof({ pow: 20 }), createAccountController); -app.get('/api/v1/accounts/verify_credentials', requirePubkey, verifyCredentialsController); -app.patch('/api/v1/accounts/update_credentials', requirePubkey, updateCredentialsController); +app.get('/api/v1/accounts/verify_credentials', requireSigner, verifyCredentialsController); +app.patch('/api/v1/accounts/update_credentials', requireSigner, updateCredentialsController); app.get('/api/v1/accounts/search', accountSearchController); app.get('/api/v1/accounts/lookup', accountLookupController); -app.get('/api/v1/accounts/relationships', requirePubkey, relationshipsController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', requirePubkey, blockController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', requirePubkey, unblockController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/mute', requirePubkey, muteController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unmute', requirePubkey, unmuteController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', requirePubkey, followController); -app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unfollow', requirePubkey, unfollowController); +app.get('/api/v1/accounts/relationships', requireSigner, relationshipsController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', requireSigner, blockController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', requireSigner, unblockController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/mute', requireSigner, muteController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unmute', requireSigner, unmuteController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', requireSigner, followController); +app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unfollow', requireSigner, unfollowController); app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/followers', followersController); app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/following', followingController); app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/statuses', accountStatusesController); @@ -171,21 +171,21 @@ app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/favourited_by', favouritedByControll app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/reblogged_by', rebloggedByController); app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/context', contextController); app.get('/api/v1/statuses/:id{[0-9a-f]{64}}', statusController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/favourite', requirePubkey, favouriteController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/bookmark', requirePubkey, bookmarkController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', requirePubkey, unbookmarkController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', requirePubkey, pinController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requirePubkey, unpinController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/zap', requirePubkey, zapController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/reblog', requirePubkey, reblogStatusController); -app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unreblog', requirePubkey, unreblogStatusController); -app.post('/api/v1/statuses', requirePubkey, createStatusController); -app.delete('/api/v1/statuses/:id{[0-9a-f]{64}}', requirePubkey, deleteStatusController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/favourite', requireSigner, favouriteController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/bookmark', requireSigner, bookmarkController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', requireSigner, unbookmarkController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', requireSigner, pinController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requireSigner, unpinController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/zap', requireSigner, zapController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/reblog', requireSigner, reblogStatusController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unreblog', requireSigner, unreblogStatusController); +app.post('/api/v1/statuses', requireSigner, createStatusController); +app.delete('/api/v1/statuses/:id{[0-9a-f]{64}}', requireSigner, deleteStatusController); app.post('/api/v1/media', mediaController); app.post('/api/v2/media', mediaController); -app.get('/api/v1/timelines/home', requirePubkey, homeTimelineController); +app.get('/api/v1/timelines/home', requireSigner, homeTimelineController); app.get('/api/v1/timelines/public', publicTimelineController); app.get('/api/v1/timelines/tag/:hashtag', hashtagTimelineController); @@ -201,11 +201,11 @@ app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }) app.get('/api/v1/suggestions', suggestionsV1Controller); app.get('/api/v2/suggestions', suggestionsV2Controller); -app.get('/api/v1/notifications', requirePubkey, notificationsController); -app.get('/api/v1/favourites', requirePubkey, favouritesController); -app.get('/api/v1/bookmarks', requirePubkey, bookmarksController); -app.get('/api/v1/blocks', requirePubkey, blocksController); -app.get('/api/v1/mutes', requirePubkey, mutesController); +app.get('/api/v1/notifications', requireSigner, notificationsController); +app.get('/api/v1/favourites', requireSigner, favouritesController); +app.get('/api/v1/bookmarks', requireSigner, bookmarksController); +app.get('/api/v1/blocks', requireSigner, blocksController); +app.get('/api/v1/mutes', requireSigner, mutesController); app.get('/api/v1/markers', requireProof(), markersController); app.post('/api/v1/markers', requireProof(), updateMarkersController); @@ -218,17 +218,17 @@ app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAd app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); -app.post('/api/v1/reports', requirePubkey, reportController); -app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), adminReportsController); -app.get('/api/v1/admin/reports/:id{[0-9a-f]{64}}', requirePubkey, requireRole('admin'), adminReportController); +app.post('/api/v1/reports', requireSigner, reportController); +app.get('/api/v1/admin/reports', requireSigner, requireRole('admin'), adminReportsController); +app.get('/api/v1/admin/reports/:id{[0-9a-f]{64}}', requireSigner, requireRole('admin'), adminReportController); app.post( '/api/v1/admin/reports/:id{[0-9a-f]{64}}/resolve', - requirePubkey, + requireSigner, requireRole('admin'), adminReportResolveController, ); -app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requirePubkey, requireRole('admin'), adminAccountAction); +app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requireSigner, requireRole('admin'), adminAccountAction); // Not (yet) implemented. app.get('/api/v1/custom_emojis', emptyArrayController); diff --git a/src/middleware/requireSigner.ts b/src/middleware/requireSigner.ts new file mode 100644 index 0000000..6e337c2 --- /dev/null +++ b/src/middleware/requireSigner.ts @@ -0,0 +1,12 @@ +import { HTTPException } from 'hono'; + +import { AppMiddleware } from '@/app.ts'; + +/** Throw a 401 if a signer isn't set. */ +export const requireSigner: AppMiddleware = async (c, next) => { + if (!c.get('signer')) { + throw new HTTPException(401, { message: 'No pubkey provided' }); + } + + await next(); +}; From c715827c81a9e85d8b68de655b865b9665267f0e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 11:57:03 -0500 Subject: [PATCH 04/11] c.get('pubkey') -> await c.get('signer')?.getPublicKey() --- src/controllers/api/accounts.ts | 30 +++++++++++++++++---------- src/controllers/api/bookmarks.ts | 2 +- src/controllers/api/markers.ts | 4 ++-- src/controllers/api/media.ts | 2 +- src/controllers/api/mutes.ts | 2 +- src/controllers/api/notifications.ts | 6 +++--- src/controllers/api/reports.ts | 12 ++++++++--- src/controllers/api/search.ts | 3 ++- src/controllers/api/statuses.ts | 31 ++++++++++++++++------------ src/controllers/api/timelines.ts | 8 ++++--- src/middleware/auth98.ts | 12 ++++++++--- src/middleware/store.ts | 2 +- src/utils/api.ts | 9 ++++++-- src/views.ts | 4 +++- 14 files changed, 81 insertions(+), 46 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 68edfbc..88d19b1 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -29,7 +29,7 @@ const createAccountSchema = z.object({ }); const createAccountController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const result = createAccountSchema.safeParse(await c.req.json()); if (!result.success) { @@ -45,7 +45,7 @@ const createAccountController: AppController = async (c) => { }; const verifyCredentialsController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const event = await getAuthor(pubkey, { relations: ['author_stats'] }); if (event) { @@ -122,7 +122,7 @@ const accountSearchController: AppController = async (c) => { }; const relationshipsController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const ids = z.array(z.string()).safeParse(c.req.queries('id[]')); if (!ids.success) { @@ -178,7 +178,11 @@ const accountStatusesController: AppController = async (c) => { return events; }); - const statuses = await Promise.all(events.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); + const viewerPubkey = await c.get('signer')?.getPublicKey(); + + const statuses = await Promise.all( + events.map((event) => renderStatus(event, { viewerPubkey })), + ); return paginated(c, events, statuses); }; @@ -194,7 +198,7 @@ const updateCredentialsSchema = z.object({ }); const updateCredentialsController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const body = await parseBody(c.req.raw); const result = updateCredentialsSchema.safeParse(body); @@ -236,7 +240,7 @@ const updateCredentialsController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/accounts/#follow */ const followController: AppController = async (c) => { - const sourcePubkey = c.get('pubkey')!; + const sourcePubkey = await c.get('signer')?.getPublicKey()!; const targetPubkey = c.req.param('pubkey'); await updateListEvent( @@ -253,7 +257,7 @@ const followController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/accounts/#unfollow */ const unfollowController: AppController = async (c) => { - const sourcePubkey = c.get('pubkey')!; + const sourcePubkey = await c.get('signer')?.getPublicKey()!; const targetPubkey = c.req.param('pubkey'); await updateListEvent( @@ -290,7 +294,7 @@ const unblockController: AppController = (c) => { /** https://docs.joinmastodon.org/methods/accounts/#mute */ const muteController: AppController = async (c) => { - const sourcePubkey = c.get('pubkey')!; + const sourcePubkey = await c.get('signer')?.getPublicKey()!; const targetPubkey = c.req.param('pubkey'); await updateListEvent( @@ -305,7 +309,7 @@ const muteController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/accounts/#unmute */ const unmuteController: AppController = async (c) => { - const sourcePubkey = c.get('pubkey')!; + const sourcePubkey = await c.get('signer')?.getPublicKey()!; const targetPubkey = c.req.param('pubkey'); await updateListEvent( @@ -319,7 +323,7 @@ const unmuteController: AppController = async (c) => { }; const favouritesController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const params = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; @@ -335,7 +339,11 @@ const favouritesController: AppController = async (c) => { const events1 = await Storages.db.query([{ kinds: [1], ids }], { signal }) .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); - const statuses = await Promise.all(events1.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); + const viewerPubkey = await c.get('signer')?.getPublicKey(); + + const statuses = await Promise.all( + events1.map((event) => renderStatus(event, { viewerPubkey })), + ); return paginated(c, events1, statuses); }; diff --git a/src/controllers/api/bookmarks.ts b/src/controllers/api/bookmarks.ts index 8d44f95..1616fa2 100644 --- a/src/controllers/api/bookmarks.ts +++ b/src/controllers/api/bookmarks.ts @@ -5,7 +5,7 @@ import { renderStatuses } from '@/views.ts'; /** https://docs.joinmastodon.org/methods/bookmarks/#get */ const bookmarksController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const { signal } = c.req.raw; const [event10003] = await Storages.db.query( diff --git a/src/controllers/api/markers.ts b/src/controllers/api/markers.ts index ce1c4ec..005ebbe 100644 --- a/src/controllers/api/markers.ts +++ b/src/controllers/api/markers.ts @@ -14,7 +14,7 @@ interface Marker { } export const markersController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const timelines = c.req.queries('timeline[]') ?? []; const results = await kv.getMany( @@ -37,7 +37,7 @@ const markerDataSchema = z.object({ }); export const updateMarkersController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const record = z.record(z.enum(['home', 'notifications']), markerDataSchema).parse(await parseBody(c.req.raw)); const timelines = Object.keys(record) as Timeline[]; diff --git a/src/controllers/api/media.ts b/src/controllers/api/media.ts index dd36a53..33b7981 100644 --- a/src/controllers/api/media.ts +++ b/src/controllers/api/media.ts @@ -14,7 +14,7 @@ const mediaBodySchema = z.object({ }); const mediaController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const result = mediaBodySchema.safeParse(await parseBody(c.req.raw)); const { signal } = c.req.raw; diff --git a/src/controllers/api/mutes.ts b/src/controllers/api/mutes.ts index 77b60e3..fe048e9 100644 --- a/src/controllers/api/mutes.ts +++ b/src/controllers/api/mutes.ts @@ -5,7 +5,7 @@ import { renderAccounts } from '@/views.ts'; /** https://docs.joinmastodon.org/methods/mutes/#get */ const mutesController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const { signal } = c.req.raw; const [event10000] = await Storages.db.query( diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index b2fa15e..857f2a3 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -5,8 +5,8 @@ import { hydrateEvents } from '@/storages/hydrate.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts'; -const notificationsController: AppController = (c) => { - const pubkey = c.get('pubkey')!; +const notificationsController: AppController = async (c) => { + const pubkey = await c.get('signer')?.getPublicKey()!; const { since, until } = paginationSchema.parse(c.req.query()); return renderNotifications(c, [{ kinds: [1, 6, 7], '#p': [pubkey], since, until }]); @@ -14,7 +14,7 @@ const notificationsController: AppController = (c) => { async function renderNotifications(c: AppContext, filters: NostrFilter[]) { const store = c.get('store'); - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const { signal } = c.req.raw; const events = await store diff --git a/src/controllers/api/reports.ts b/src/controllers/api/reports.ts index eb85bd2..55fb601 100644 --- a/src/controllers/api/reports.ts +++ b/src/controllers/api/reports.ts @@ -55,9 +55,15 @@ const reportController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/admin/reports/#get */ const adminReportsController: AppController = async (c) => { const store = c.get('store'); + const viewerPubkey = await c.get('signer')?.getPublicKey(); + const reports = await store.query([{ kinds: [1984], '#P': [Conf.pubkey] }]) .then((events) => hydrateEvents({ storage: store, events: events, signal: c.req.raw.signal })) - .then((events) => Promise.all(events.map((event) => renderAdminReport(event, { viewerPubkey: c.get('pubkey') })))); + .then((events) => + Promise.all( + events.map((event) => renderAdminReport(event, { viewerPubkey })), + ) + ); return c.json(reports); }; @@ -67,7 +73,7 @@ const adminReportController: AppController = async (c) => { const eventId = c.req.param('id'); const { signal } = c.req.raw; const store = c.get('store'); - const pubkey = c.get('pubkey'); + const pubkey = await c.get('signer')?.getPublicKey(); const [event] = await store.query([{ kinds: [1984], @@ -89,7 +95,7 @@ const adminReportResolveController: AppController = async (c) => { const eventId = c.req.param('id'); const { signal } = c.req.raw; const store = c.get('store'); - const pubkey = c.get('pubkey'); + const pubkey = await c.get('signer')?.getPublicKey(); const [event] = await store.query([{ kinds: [1984], diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index e674d0e..fe08ace 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -43,6 +43,7 @@ const searchController: AppController = async (c) => { } const results = dedupeEvents(events); + const viewerPubkey = await c.get('signer')?.getPublicKey(); const [accounts, statuses] = await Promise.all([ Promise.all( @@ -54,7 +55,7 @@ const searchController: AppController = async (c) => { Promise.all( results .filter((event) => event.kind === 1) - .map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') })) + .map((event) => renderStatus(event, { viewerPubkey })) .filter(Boolean), ), ]); diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 3420383..1138c0a 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -47,7 +47,7 @@ const statusController: AppController = async (c) => { }); if (event) { - return c.json(await renderStatus(event, { viewerPubkey: c.get('pubkey') })); + return c.json(await renderStatus(event, { viewerPubkey: await c.get('signer')?.getPublicKey() })); } return c.json({ error: 'Event not found.' }, 404); @@ -89,9 +89,11 @@ const createStatusController: AppController = async (c) => { tags.push(['subject', data.spoiler_text]); } + const viewerPubkey = await c.get('signer')?.getPublicKey(); + if (data.media_ids?.length) { const media = await getUnattachedMediaByIds(data.media_ids) - .then((media) => media.filter(({ pubkey }) => pubkey === c.get('pubkey'))) + .then((media) => media.filter(({ pubkey }) => pubkey === viewerPubkey)) .then((media) => media.map(({ url, data }) => ['media', url, data])); tags.push(...media); @@ -143,12 +145,12 @@ const createStatusController: AppController = async (c) => { }); } - return c.json(await renderStatus({ ...event, author }, { viewerPubkey: c.get('pubkey') })); + return c.json(await renderStatus({ ...event, author }, { viewerPubkey: await c.get('signer')?.getPublicKey() })); }; const deleteStatusController: AppController = async (c) => { const id = c.req.param('id'); - const pubkey = c.get('pubkey'); + const pubkey = await c.get('signer')?.getPublicKey(); const event = await getEvent(id, { signal: c.req.raw.signal }); @@ -172,9 +174,12 @@ const deleteStatusController: AppController = async (c) => { const contextController: AppController = async (c) => { const id = c.req.param('id'); const event = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] }); + const viewerPubkey = await c.get('signer')?.getPublicKey(); async function renderStatuses(events: NostrEvent[]) { - const statuses = await Promise.all(events.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); + const statuses = await Promise.all( + events.map((event) => renderStatus(event, { viewerPubkey })), + ); return statuses.filter(Boolean); } @@ -204,7 +209,7 @@ const favouriteController: AppController = async (c) => { ], }, c); - const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') }); + const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() }); if (status) { status.favourited = true; @@ -247,7 +252,7 @@ const reblogStatusController: AppController = async (c) => { signal: signal, }); - const status = await renderReblog(reblogEvent, { viewerPubkey: c.get('pubkey') }); + const status = await renderReblog(reblogEvent, { viewerPubkey: await c.get('signer')?.getPublicKey() }); return c.json(status); }; @@ -255,7 +260,7 @@ const reblogStatusController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#unreblog */ const unreblogStatusController: AppController = async (c) => { const eventId = c.req.param('id'); - const pubkey = c.get('pubkey') as string; + const pubkey = await c.get('signer')?.getPublicKey() as string; const event = await getEvent(eventId, { kind: 1, @@ -282,7 +287,7 @@ const rebloggedByController: AppController = (c) => { /** https://docs.joinmastodon.org/methods/statuses/#bookmark */ const bookmarkController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); const event = await getEvent(eventId, { @@ -309,7 +314,7 @@ const bookmarkController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#unbookmark */ const unbookmarkController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); const event = await getEvent(eventId, { @@ -336,7 +341,7 @@ const unbookmarkController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#pin */ const pinController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); const event = await getEvent(eventId, { @@ -363,7 +368,7 @@ const pinController: AppController = async (c) => { /** https://docs.joinmastodon.org/methods/statuses/#unpin */ const unpinController: AppController = async (c) => { - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const eventId = c.req.param('id'); const { signal } = c.req.raw; @@ -423,7 +428,7 @@ const zapController: AppController = async (c) => { ], }, c); - const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') }); + const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() }); status.zapped = true; return c.json(status); diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 27459a8..0880d84 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -11,7 +11,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; const homeTimelineController: AppController = async (c) => { const params = paginationSchema.parse(c.req.query()); - const pubkey = c.get('pubkey')!; + const pubkey = await c.get('signer')?.getPublicKey()!; const authors = await getFeedPubkeys(pubkey); return renderStatuses(c, [{ authors, kinds: [1, 6], ...params }]); }; @@ -61,11 +61,13 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { return c.json([]); } + const viewerPubkey = await c.get('signer')?.getPublicKey(); + const statuses = (await Promise.all(events.map((event) => { if (event.kind === 6) { - return renderReblog(event, { viewerPubkey: c.get('pubkey') }); + return renderReblog(event, { viewerPubkey }); } - return renderStatus(event, { viewerPubkey: c.get('pubkey') }); + return renderStatus(event, { viewerPubkey }); }))).filter((boolean) => boolean); if (!statuses.length) { diff --git a/src/middleware/auth98.ts b/src/middleware/auth98.ts index db025ae..d761b95 100644 --- a/src/middleware/auth98.ts +++ b/src/middleware/auth98.ts @@ -8,7 +8,6 @@ import { validateAuthEvent, } from '@/utils/nip98.ts'; import { localRequest } from '@/utils/api.ts'; -import { APISigner } from '@/signers/APISigner.ts'; import { findUser, User } from '@/db/users.ts'; /** @@ -70,7 +69,7 @@ function withProof( opts?: ParseAuthRequestOpts, ): AppMiddleware { return async (c, next) => { - const pubkey = c.get('pubkey'); + const pubkey = await c.get('signer')?.getPublicKey(); const proof = c.get('proof') || await obtainProof(c, opts); // Prevent people from accidentally using the wrong account. This has no other security implications. @@ -90,9 +89,16 @@ function withProof( /** Get the proof over Nostr Connect. */ async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) { + const signer = c.get('signer'); + if (!signer) { + throw new HTTPException(401, { + res: c.json({ error: 'No way to sign Nostr event' }, 401), + }); + } + const req = localRequest(c); const reqEvent = await buildAuthEventTemplate(req, opts); - const resEvent = await new APISigner(c).signEvent(reqEvent); + const resEvent = await signer.signEvent(reqEvent); const result = await validateAuthEvent(req, resEvent, opts); if (result.success) { diff --git a/src/middleware/store.ts b/src/middleware/store.ts index 60055b3..40b7c59 100644 --- a/src/middleware/store.ts +++ b/src/middleware/store.ts @@ -4,7 +4,7 @@ import { Storages } from '@/storages.ts'; /** Store middleware. */ const storeMiddleware: AppMiddleware = async (c, next) => { - const pubkey = c.get('pubkey'); + const pubkey = await c.get('signer')?.getPublicKey(); if (pubkey) { const store = new UserStore(pubkey, Storages.admin); diff --git a/src/utils/api.ts b/src/utils/api.ts index cba7c66..3dfdfa3 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -10,7 +10,6 @@ import { type AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { APISigner } from '@/signers/APISigner.ts'; import { Storages } from '@/storages.ts'; import { nostrNow } from '@/utils.ts'; @@ -21,7 +20,13 @@ type EventStub = TypeFest.SetOptional { - const signer = new APISigner(c); + const signer = c.get('signer'); + + if (!signer) { + throw new HTTPException(401, { + res: c.json({ error: 'No way to sign Nostr event' }, 401), + }); + } const event = await signer.signEvent({ content: '', diff --git a/src/views.ts b/src/views.ts index 9c31dfc..451ce14 100644 --- a/src/views.ts +++ b/src/views.ts @@ -59,8 +59,10 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal const sortedEvents = [...events].sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); + const viewerPubkey = await c.get('signer')?.getPublicKey(); + const statuses = await Promise.all( - sortedEvents.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') })), + sortedEvents.map((event) => renderStatus(event, { viewerPubkey })), ); // TODO: pagination with min_id and max_id based on the order of `ids`. From 1accae2222a5a7c7571e4e72c8d0fce93f576b97 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 12:04:31 -0500 Subject: [PATCH 05/11] Add a ConnectSigner to wrap our default opts to NConnectSigner, add c.set('signer') calls to nip98 middleware --- src/app.ts | 2 +- src/middleware/auth98.ts | 10 ++++++---- src/middleware/signerMiddleware.ts | 15 +++------------ src/signers/ConnectSigner.ts | 20 ++++++++++++++++++++ 4 files changed, 30 insertions(+), 17 deletions(-) create mode 100644 src/signers/ConnectSigner.ts diff --git a/src/app.ts b/src/app.ts index 5ef5056..62da64c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -126,9 +126,9 @@ app.use( '*', csp(), cors({ origin: '*', exposeHeaders: ['link'] }), + signerMiddleware, auth98(), storeMiddleware, - signerMiddleware, ); app.get('/.well-known/webfinger', webfingerController); diff --git a/src/middleware/auth98.ts b/src/middleware/auth98.ts index d761b95..fbadb2c 100644 --- a/src/middleware/auth98.ts +++ b/src/middleware/auth98.ts @@ -1,14 +1,16 @@ import { NostrEvent } from '@nostrify/nostrify'; import { HTTPException } from 'hono'; + import { type AppContext, type AppMiddleware } from '@/app.ts'; +import { findUser, User } from '@/db/users.ts'; +import { ConnectSigner } from '@/signers/ConnectSigner.ts'; +import { localRequest } from '@/utils/api.ts'; import { buildAuthEventTemplate, parseAuthRequest, type ParseAuthRequestOpts, validateAuthEvent, } from '@/utils/nip98.ts'; -import { localRequest } from '@/utils/api.ts'; -import { findUser, User } from '@/db/users.ts'; /** * NIP-98 auth. @@ -20,7 +22,7 @@ function auth98(opts: ParseAuthRequestOpts = {}): AppMiddleware { const result = await parseAuthRequest(req, opts); if (result.success) { - c.set('pubkey', result.data.pubkey); + c.set('signer', new ConnectSigner(result.data.pubkey)); c.set('proof', result.data); } @@ -78,7 +80,7 @@ function withProof( } if (proof) { - c.set('pubkey', proof.pubkey); + c.set('signer', new ConnectSigner(proof.pubkey)); c.set('proof', proof); await handler(c, proof, next); } else { diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index d056391..85a2ff1 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -1,9 +1,8 @@ -import { NConnectSigner, NSecSigner } from '@nostrify/nostrify'; +import { NSecSigner } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; import { AppMiddleware } from '@/app.ts'; -import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { Storages } from '@/storages.ts'; +import { ConnectSigner } from '@/signers/ConnectSigner.ts'; /** We only accept "Bearer" type. */ const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); @@ -21,15 +20,7 @@ export const signerMiddleware: AppMiddleware = async (c, next) => { switch (decoded.type) { case 'npub': - c.set( - 'signer', - new NConnectSigner({ - pubkey: decoded.data, - relay: Storages.pubsub, - signer: new AdminSigner(), - timeout: 60000, - }), - ); + c.set('signer', new ConnectSigner(decoded.data)); break; case 'nsec': c.set('signer', new NSecSigner(decoded.data)); diff --git a/src/signers/ConnectSigner.ts b/src/signers/ConnectSigner.ts new file mode 100644 index 0000000..b98a59f --- /dev/null +++ b/src/signers/ConnectSigner.ts @@ -0,0 +1,20 @@ +import { NConnectSigner } from '@nostrify/nostrify'; + +import { AdminSigner } from '@/signers/AdminSigner.ts'; +import { Storages } from '@/storages.ts'; + +/** + * NIP-46 signer. + * + * Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY. + */ +export class ConnectSigner extends NConnectSigner { + constructor(pubkey: string) { + super({ + pubkey, + relay: Storages.pubsub, + signer: new AdminSigner(), + timeout: 60000, + }); + } +} From 084143c5c8d9fd8a61c90cc3319005a6d8a3841c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 12:07:54 -0500 Subject: [PATCH 06/11] Rename all middleware to thingMiddleware --- src/app.ts | 22 +++++++++++-------- .../{auth98.ts => auth98Middleware.ts} | 4 ++-- .../{cache.ts => cacheMiddleware.ts} | 2 +- src/middleware/{csp.ts => cspMiddleware.ts} | 4 +--- .../{store.ts => storeMiddleware.ts} | 4 +--- 5 files changed, 18 insertions(+), 18 deletions(-) rename src/middleware/{auth98.ts => auth98Middleware.ts} (95%) rename src/middleware/{cache.ts => cacheMiddleware.ts} (95%) rename src/middleware/{csp.ts => cspMiddleware.ts} (93%) rename src/middleware/{store.ts => storeMiddleware.ts} (81%) diff --git a/src/app.ts b/src/app.ts index 62da64c..8c8cbc0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -81,12 +81,12 @@ import { hostMetaController } from '@/controllers/well-known/host-meta.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; import { webfingerController } from '@/controllers/well-known/webfinger.ts'; -import { auth98, requireProof, requireRole } from '@/middleware/auth98.ts'; -import { cache } from '@/middleware/cache.ts'; -import { csp } from '@/middleware/csp.ts'; +import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; +import { cacheMiddleware } from '@/middleware/cacheMiddleware.ts'; +import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; import { requireSigner } from '@/middleware/requireSigner.ts'; import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; -import { storeMiddleware } from '@/middleware/store.ts'; +import { storeMiddleware } from '@/middleware/storeMiddleware.ts'; import { blockController } from '@/controllers/api/accounts.ts'; import { unblockController } from '@/controllers/api/accounts.ts'; @@ -124,10 +124,10 @@ app.get('/relay', relayController); app.use( '*', - csp(), + cspMiddleware(), cors({ origin: '*', exposeHeaders: ['link'] }), signerMiddleware, - auth98(), + auth98Middleware(), storeMiddleware, ); @@ -140,7 +140,7 @@ app.get('/users/:username', actorController); app.get('/nodeinfo/:version', nodeInfoSchemaController); -app.get('/api/v1/instance', cache({ cacheName: 'web', expires: Time.minutes(5) }), instanceController); +app.get('/api/v1/instance', cacheMiddleware({ cacheName: 'web', expires: Time.minutes(5) }), instanceController); app.get('/api/v1/apps/verify_credentials', appCredentialsController); app.post('/api/v1/apps', createAppController); @@ -195,8 +195,12 @@ app.get('/api/v2/search', searchController); app.get('/api/pleroma/frontend_configurations', frontendConfigController); -app.get('/api/v1/trends/tags', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController); -app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController); +app.get( + '/api/v1/trends/tags', + cacheMiddleware({ cacheName: 'web', expires: Time.minutes(15) }), + trendingTagsController, +); +app.get('/api/v1/trends', cacheMiddleware({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController); app.get('/api/v1/suggestions', suggestionsV1Controller); app.get('/api/v2/suggestions', suggestionsV2Controller); diff --git a/src/middleware/auth98.ts b/src/middleware/auth98Middleware.ts similarity index 95% rename from src/middleware/auth98.ts rename to src/middleware/auth98Middleware.ts index fbadb2c..7cd7059 100644 --- a/src/middleware/auth98.ts +++ b/src/middleware/auth98Middleware.ts @@ -16,7 +16,7 @@ import { * NIP-98 auth. * https://github.com/nostr-protocol/nips/blob/master/98.md */ -function auth98(opts: ParseAuthRequestOpts = {}): AppMiddleware { +function auth98Middleware(opts: ParseAuthRequestOpts = {}): AppMiddleware { return async (c, next) => { const req = localRequest(c); const result = await parseAuthRequest(req, opts); @@ -108,4 +108,4 @@ async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) { } } -export { auth98, requireProof, requireRole }; +export { auth98Middleware, requireProof, requireRole }; diff --git a/src/middleware/cache.ts b/src/middleware/cacheMiddleware.ts similarity index 95% rename from src/middleware/cache.ts rename to src/middleware/cacheMiddleware.ts index 181623f..baa4976 100644 --- a/src/middleware/cache.ts +++ b/src/middleware/cacheMiddleware.ts @@ -5,7 +5,7 @@ import ExpiringCache from '@/utils/expiring-cache.ts'; const debug = Debug('ditto:middleware:cache'); -export const cache = (options: { +export const cacheMiddleware = (options: { cacheName: string; expires?: number; }): MiddlewareHandler => { diff --git a/src/middleware/csp.ts b/src/middleware/cspMiddleware.ts similarity index 93% rename from src/middleware/csp.ts rename to src/middleware/cspMiddleware.ts index fdce5c7..00c4ecc 100644 --- a/src/middleware/csp.ts +++ b/src/middleware/cspMiddleware.ts @@ -1,7 +1,7 @@ import { AppMiddleware } from '@/app.ts'; import { Conf } from '@/config.ts'; -const csp = (): AppMiddleware => { +export const cspMiddleware = (): AppMiddleware => { return async (c, next) => { const { host, protocol, origin } = Conf.url; const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; @@ -26,5 +26,3 @@ const csp = (): AppMiddleware => { await next(); }; }; - -export { csp }; diff --git a/src/middleware/store.ts b/src/middleware/storeMiddleware.ts similarity index 81% rename from src/middleware/store.ts rename to src/middleware/storeMiddleware.ts index 40b7c59..efb65ed 100644 --- a/src/middleware/store.ts +++ b/src/middleware/storeMiddleware.ts @@ -3,7 +3,7 @@ import { UserStore } from '@/storages/UserStore.ts'; import { Storages } from '@/storages.ts'; /** Store middleware. */ -const storeMiddleware: AppMiddleware = async (c, next) => { +export const storeMiddleware: AppMiddleware = async (c, next) => { const pubkey = await c.get('signer')?.getPublicKey(); if (pubkey) { @@ -14,5 +14,3 @@ const storeMiddleware: AppMiddleware = async (c, next) => { } await next(); }; - -export { storeMiddleware }; From 03182f8a5a260447173c0e40d9f6c388002ffabd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 12:14:27 -0500 Subject: [PATCH 07/11] ConnectSigner: implement getRelays, support nprofile auth again --- src/middleware/signerMiddleware.ts | 3 +++ src/signers/ConnectSigner.ts | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index 85a2ff1..8779937 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -22,6 +22,9 @@ export const signerMiddleware: AppMiddleware = async (c, next) => { case 'npub': c.set('signer', new ConnectSigner(decoded.data)); break; + case 'nprofile': + c.set('signer', new ConnectSigner(decoded.data.pubkey, decoded.data.relays)); + break; case 'nsec': c.set('signer', new NSecSigner(decoded.data)); break; diff --git a/src/signers/ConnectSigner.ts b/src/signers/ConnectSigner.ts index b98a59f..e7d61a8 100644 --- a/src/signers/ConnectSigner.ts +++ b/src/signers/ConnectSigner.ts @@ -9,12 +9,22 @@ import { Storages } from '@/storages.ts'; * Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY. */ export class ConnectSigner extends NConnectSigner { - constructor(pubkey: string) { + constructor(pubkey: string, private relays?: string[]) { super({ pubkey, + // TODO: use a remote relay for `nprofile` signing, if present and Conf.relay isn't already in the list relay: Storages.pubsub, signer: new AdminSigner(), timeout: 60000, }); } + + /** Get the user's relays if they passed in an `nprofile` auth token. */ + // deno-lint-ignore require-await + async getRelays(): Promise> { + return this.relays?.reduce>((acc, relay) => { + acc[relay] = { read: true, write: true }; + return acc; + }, {}) ?? {}; + } } From cd2a35d951e6db472e9fd36b496de8748e340376 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 12:20:36 -0500 Subject: [PATCH 08/11] ConnectSigner: make getPublicKey used the stored value instead of actually hitting the relay --- src/signers/ConnectSigner.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/signers/ConnectSigner.ts b/src/signers/ConnectSigner.ts index e7d61a8..4dda0b1 100644 --- a/src/signers/ConnectSigner.ts +++ b/src/signers/ConnectSigner.ts @@ -1,3 +1,4 @@ +// deno-lint-ignore-file require-await import { NConnectSigner } from '@nostrify/nostrify'; import { AdminSigner } from '@/signers/AdminSigner.ts'; @@ -9,18 +10,26 @@ import { Storages } from '@/storages.ts'; * Simple extension of nostrify's `NConnectSigner`, with our options to keep it DRY. */ export class ConnectSigner extends NConnectSigner { + private _pubkey: string; + constructor(pubkey: string, private relays?: string[]) { super({ pubkey, - // TODO: use a remote relay for `nprofile` signing, if present and Conf.relay isn't already in the list + // TODO: use a remote relay for `nprofile` signing (if present and `Conf.relay` isn't already in the list) relay: Storages.pubsub, signer: new AdminSigner(), timeout: 60000, }); + + this._pubkey = pubkey; + } + + // Prevent unnecessary NIP-46 round-trips. + async getPublicKey(): Promise { + return this._pubkey; } /** Get the user's relays if they passed in an `nprofile` auth token. */ - // deno-lint-ignore require-await async getRelays(): Promise> { return this.relays?.reduce>((acc, relay) => { acc[relay] = { read: true, write: true }; From a061c248bd5d288a27305c3a502add4296d6d999 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 14:18:44 -0500 Subject: [PATCH 09/11] signerMiddleware: add debug log --- src/middleware/signerMiddleware.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index 8779937..1d35708 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -1,9 +1,12 @@ import { NSecSigner } from '@nostrify/nostrify'; +import { Stickynotes } from '@soapbox/stickynotes'; import { nip19 } from 'nostr-tools'; import { AppMiddleware } from '@/app.ts'; import { ConnectSigner } from '@/signers/ConnectSigner.ts'; +const console = new Stickynotes('ditto:signerMiddleware'); + /** We only accept "Bearer" type. */ const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`); @@ -30,7 +33,7 @@ export const signerMiddleware: AppMiddleware = async (c, next) => { break; } } catch { - // the user is not logged in + console.debug('The user is not logged in'); } } From 45b766af4d768853c6236eeeadfeb7aee16f47a7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 14:24:48 -0500 Subject: [PATCH 10/11] Remove 'user' from AppContext --- src/app.ts | 2 -- src/middleware/auth98Middleware.ts | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index 8c8cbc0..447499f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -96,8 +96,6 @@ interface AppEnv extends HonoEnv { signer?: NostrSigner; /** NIP-98 signed event proving the pubkey is owned by the user. */ proof?: NostrEvent; - /** User associated with the pubkey, if any. */ - user?: User; /** Store */ store: NStore; }; diff --git a/src/middleware/auth98Middleware.ts b/src/middleware/auth98Middleware.ts index 7cd7059..abecea7 100644 --- a/src/middleware/auth98Middleware.ts +++ b/src/middleware/auth98Middleware.ts @@ -34,9 +34,8 @@ 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 user = await findUser({ pubkey: proof.pubkey }); - c.set('user', user); if (user && matchesRole(user, role)) { await next(); From e53ea222742ec0d7074988caa5b73884d140d469 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 14 May 2024 14:48:37 -0500 Subject: [PATCH 11/11] Remove unused import --- src/app.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 447499f..84ad780 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,7 +3,6 @@ import Debug from '@soapbox/stickynotes/debug'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; -import { type User } from '@/db/users.ts'; import '@/firehose.ts'; import { Time } from '@/utils.ts';