mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Merge branch 'signer-middleware' into 'main'
Add a signerMiddleware, c.get('pubkey') -> c.get('signer')
See merge request soapbox-pub/ditto!251
This commit is contained in:
commit
a3597edb90
22 changed files with 240 additions and 223 deletions
103
src/app.ts
103
src/app.ts
|
|
@ -1,9 +1,8 @@
|
||||||
import { NostrEvent, NStore } from '@nostrify/nostrify';
|
import { NostrEvent, NostrSigner, NStore } from '@nostrify/nostrify';
|
||||||
import Debug from '@soapbox/stickynotes/debug';
|
import Debug from '@soapbox/stickynotes/debug';
|
||||||
import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono';
|
import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono';
|
||||||
import { cors, logger, serveStatic } from 'hono/middleware';
|
import { cors, logger, serveStatic } from 'hono/middleware';
|
||||||
|
|
||||||
import { type User } from '@/db/users.ts';
|
|
||||||
import '@/firehose.ts';
|
import '@/firehose.ts';
|
||||||
import { Time } from '@/utils.ts';
|
import { Time } from '@/utils.ts';
|
||||||
|
|
||||||
|
|
@ -29,6 +28,7 @@ import { adminAccountAction, adminAccountsController } from '@/controllers/api/a
|
||||||
import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts';
|
import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts';
|
||||||
import { blocksController } from '@/controllers/api/blocks.ts';
|
import { blocksController } from '@/controllers/api/blocks.ts';
|
||||||
import { bookmarksController } from '@/controllers/api/bookmarks.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 { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts';
|
||||||
import { instanceController } from '@/controllers/api/instance.ts';
|
import { instanceController } from '@/controllers/api/instance.ts';
|
||||||
import { markersController, updateMarkersController } from '@/controllers/api/markers.ts';
|
import { markersController, updateMarkersController } from '@/controllers/api/markers.ts';
|
||||||
|
|
@ -80,25 +80,21 @@ import { hostMetaController } from '@/controllers/well-known/host-meta.ts';
|
||||||
import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts';
|
import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts';
|
||||||
import { nostrController } from '@/controllers/well-known/nostr.ts';
|
import { nostrController } from '@/controllers/well-known/nostr.ts';
|
||||||
import { webfingerController } from '@/controllers/well-known/webfinger.ts';
|
import { webfingerController } from '@/controllers/well-known/webfinger.ts';
|
||||||
import { auth19, requirePubkey } from '@/middleware/auth19.ts';
|
import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts';
|
||||||
import { auth98, requireProof, requireRole } from '@/middleware/auth98.ts';
|
import { cacheMiddleware } from '@/middleware/cacheMiddleware.ts';
|
||||||
import { cache } from '@/middleware/cache.ts';
|
import { cspMiddleware } from '@/middleware/cspMiddleware.ts';
|
||||||
import { csp } from '@/middleware/csp.ts';
|
import { requireSigner } from '@/middleware/requireSigner.ts';
|
||||||
import { adminRelaysController, adminSetRelaysController } from '@/controllers/api/ditto.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 { blockController } from '@/controllers/api/accounts.ts';
|
||||||
import { unblockController } from '@/controllers/api/accounts.ts';
|
import { unblockController } from '@/controllers/api/accounts.ts';
|
||||||
|
|
||||||
interface AppEnv extends HonoEnv {
|
interface AppEnv extends HonoEnv {
|
||||||
Variables: {
|
Variables: {
|
||||||
/** Hex pubkey for the current user. If provided, the user is considered "logged in." */
|
/** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */
|
||||||
pubkey?: string;
|
signer?: NostrSigner;
|
||||||
/** 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. */
|
/** NIP-98 signed event proving the pubkey is owned by the user. */
|
||||||
proof?: NostrEvent;
|
proof?: NostrEvent;
|
||||||
/** User associated with the pubkey, if any. */
|
|
||||||
user?: User;
|
|
||||||
/** Store */
|
/** Store */
|
||||||
store: NStore;
|
store: NStore;
|
||||||
};
|
};
|
||||||
|
|
@ -123,7 +119,14 @@ app.get('/api/v1/streaming', streamingController);
|
||||||
app.get('/api/v1/streaming/', streamingController);
|
app.get('/api/v1/streaming/', streamingController);
|
||||||
app.get('/relay', relayController);
|
app.get('/relay', relayController);
|
||||||
|
|
||||||
app.use('*', csp(), cors({ origin: '*', exposeHeaders: ['link'] }), auth19, auth98(), storeMiddleware);
|
app.use(
|
||||||
|
'*',
|
||||||
|
cspMiddleware(),
|
||||||
|
cors({ origin: '*', exposeHeaders: ['link'] }),
|
||||||
|
signerMiddleware,
|
||||||
|
auth98Middleware(),
|
||||||
|
storeMiddleware,
|
||||||
|
);
|
||||||
|
|
||||||
app.get('/.well-known/webfinger', webfingerController);
|
app.get('/.well-known/webfinger', webfingerController);
|
||||||
app.get('/.well-known/host-meta', hostMetaController);
|
app.get('/.well-known/host-meta', hostMetaController);
|
||||||
|
|
@ -134,7 +137,7 @@ app.get('/users/:username', actorController);
|
||||||
|
|
||||||
app.get('/nodeinfo/:version', nodeInfoSchemaController);
|
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.get('/api/v1/apps/verify_credentials', appCredentialsController);
|
||||||
app.post('/api/v1/apps', createAppController);
|
app.post('/api/v1/apps', createAppController);
|
||||||
|
|
@ -145,17 +148,17 @@ app.post('/oauth/authorize', oauthAuthorizeController);
|
||||||
app.get('/oauth/authorize', oauthController);
|
app.get('/oauth/authorize', oauthController);
|
||||||
|
|
||||||
app.post('/api/v1/accounts', requireProof({ pow: 20 }), createAccountController);
|
app.post('/api/v1/accounts', requireProof({ pow: 20 }), createAccountController);
|
||||||
app.get('/api/v1/accounts/verify_credentials', requirePubkey, verifyCredentialsController);
|
app.get('/api/v1/accounts/verify_credentials', requireSigner, verifyCredentialsController);
|
||||||
app.patch('/api/v1/accounts/update_credentials', requirePubkey, updateCredentialsController);
|
app.patch('/api/v1/accounts/update_credentials', requireSigner, updateCredentialsController);
|
||||||
app.get('/api/v1/accounts/search', accountSearchController);
|
app.get('/api/v1/accounts/search', accountSearchController);
|
||||||
app.get('/api/v1/accounts/lookup', accountLookupController);
|
app.get('/api/v1/accounts/lookup', accountLookupController);
|
||||||
app.get('/api/v1/accounts/relationships', requirePubkey, relationshipsController);
|
app.get('/api/v1/accounts/relationships', requireSigner, relationshipsController);
|
||||||
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', requirePubkey, blockController);
|
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/block', requireSigner, blockController);
|
||||||
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', requirePubkey, unblockController);
|
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unblock', requireSigner, unblockController);
|
||||||
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/mute', requirePubkey, muteController);
|
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/mute', requireSigner, muteController);
|
||||||
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unmute', requirePubkey, unmuteController);
|
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unmute', requireSigner, unmuteController);
|
||||||
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', requirePubkey, followController);
|
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/follow', requireSigner, followController);
|
||||||
app.post('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/unfollow', requirePubkey, unfollowController);
|
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}}/followers', followersController);
|
||||||
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/following', followingController);
|
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/following', followingController);
|
||||||
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/statuses', accountStatusesController);
|
app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}/statuses', accountStatusesController);
|
||||||
|
|
@ -165,21 +168,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}}/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}}/context', contextController);
|
||||||
app.get('/api/v1/statuses/:id{[0-9a-f]{64}}', statusController);
|
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}}/favourite', requireSigner, favouriteController);
|
||||||
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/bookmark', requirePubkey, bookmarkController);
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/bookmark', requireSigner, bookmarkController);
|
||||||
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', requirePubkey, unbookmarkController);
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', requireSigner, unbookmarkController);
|
||||||
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', requirePubkey, pinController);
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', requireSigner, pinController);
|
||||||
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requirePubkey, unpinController);
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requireSigner, unpinController);
|
||||||
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/zap', requirePubkey, zapController);
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/zap', requireSigner, zapController);
|
||||||
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/reblog', requirePubkey, reblogStatusController);
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/reblog', requireSigner, reblogStatusController);
|
||||||
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unreblog', requirePubkey, unreblogStatusController);
|
app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unreblog', requireSigner, unreblogStatusController);
|
||||||
app.post('/api/v1/statuses', requirePubkey, createStatusController);
|
app.post('/api/v1/statuses', requireSigner, createStatusController);
|
||||||
app.delete('/api/v1/statuses/:id{[0-9a-f]{64}}', requirePubkey, deleteStatusController);
|
app.delete('/api/v1/statuses/:id{[0-9a-f]{64}}', requireSigner, deleteStatusController);
|
||||||
|
|
||||||
app.post('/api/v1/media', mediaController);
|
app.post('/api/v1/media', mediaController);
|
||||||
app.post('/api/v2/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/public', publicTimelineController);
|
||||||
app.get('/api/v1/timelines/tag/:hashtag', hashtagTimelineController);
|
app.get('/api/v1/timelines/tag/:hashtag', hashtagTimelineController);
|
||||||
|
|
||||||
|
|
@ -189,17 +192,21 @@ app.get('/api/v2/search', searchController);
|
||||||
|
|
||||||
app.get('/api/pleroma/frontend_configurations', frontendConfigController);
|
app.get('/api/pleroma/frontend_configurations', frontendConfigController);
|
||||||
|
|
||||||
app.get('/api/v1/trends/tags', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController);
|
app.get(
|
||||||
app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController);
|
'/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/v1/suggestions', suggestionsV1Controller);
|
||||||
app.get('/api/v2/suggestions', suggestionsV2Controller);
|
app.get('/api/v2/suggestions', suggestionsV2Controller);
|
||||||
|
|
||||||
app.get('/api/v1/notifications', requirePubkey, notificationsController);
|
app.get('/api/v1/notifications', requireSigner, notificationsController);
|
||||||
app.get('/api/v1/favourites', requirePubkey, favouritesController);
|
app.get('/api/v1/favourites', requireSigner, favouritesController);
|
||||||
app.get('/api/v1/bookmarks', requirePubkey, bookmarksController);
|
app.get('/api/v1/bookmarks', requireSigner, bookmarksController);
|
||||||
app.get('/api/v1/blocks', requirePubkey, blocksController);
|
app.get('/api/v1/blocks', requireSigner, blocksController);
|
||||||
app.get('/api/v1/mutes', requirePubkey, mutesController);
|
app.get('/api/v1/mutes', requireSigner, mutesController);
|
||||||
|
|
||||||
app.get('/api/v1/markers', requireProof(), markersController);
|
app.get('/api/v1/markers', requireProof(), markersController);
|
||||||
app.post('/api/v1/markers', requireProof(), updateMarkersController);
|
app.post('/api/v1/markers', requireProof(), updateMarkersController);
|
||||||
|
|
@ -212,17 +219,17 @@ app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAd
|
||||||
app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController);
|
app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController);
|
||||||
app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController);
|
app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController);
|
||||||
|
|
||||||
app.post('/api/v1/reports', requirePubkey, reportController);
|
app.post('/api/v1/reports', requireSigner, reportController);
|
||||||
app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), adminReportsController);
|
app.get('/api/v1/admin/reports', requireSigner, requireRole('admin'), adminReportsController);
|
||||||
app.get('/api/v1/admin/reports/:id{[0-9a-f]{64}}', requirePubkey, requireRole('admin'), adminReportController);
|
app.get('/api/v1/admin/reports/:id{[0-9a-f]{64}}', requireSigner, requireRole('admin'), adminReportController);
|
||||||
app.post(
|
app.post(
|
||||||
'/api/v1/admin/reports/:id{[0-9a-f]{64}}/resolve',
|
'/api/v1/admin/reports/:id{[0-9a-f]{64}}/resolve',
|
||||||
requirePubkey,
|
requireSigner,
|
||||||
requireRole('admin'),
|
requireRole('admin'),
|
||||||
adminReportResolveController,
|
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.
|
// Not (yet) implemented.
|
||||||
app.get('/api/v1/custom_emojis', emptyArrayController);
|
app.get('/api/v1/custom_emojis', emptyArrayController);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const createAccountSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const createAccountController: AppController = async (c) => {
|
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());
|
const result = createAccountSchema.safeParse(await c.req.json());
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|
@ -45,7 +45,7 @@ const createAccountController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyCredentialsController: 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'] });
|
const event = await getAuthor(pubkey, { relations: ['author_stats'] });
|
||||||
if (event) {
|
if (event) {
|
||||||
|
|
@ -122,7 +122,7 @@ const accountSearchController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const relationshipsController: 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[]'));
|
const ids = z.array(z.string()).safeParse(c.req.queries('id[]'));
|
||||||
|
|
||||||
if (!ids.success) {
|
if (!ids.success) {
|
||||||
|
|
@ -178,7 +178,11 @@ const accountStatusesController: AppController = async (c) => {
|
||||||
return events;
|
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);
|
return paginated(c, events, statuses);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -194,7 +198,7 @@ const updateCredentialsSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateCredentialsController: AppController = async (c) => {
|
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 body = await parseBody(c.req.raw);
|
||||||
const result = updateCredentialsSchema.safeParse(body);
|
const result = updateCredentialsSchema.safeParse(body);
|
||||||
|
|
||||||
|
|
@ -236,7 +240,7 @@ const updateCredentialsController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/accounts/#follow */
|
/** https://docs.joinmastodon.org/methods/accounts/#follow */
|
||||||
const followController: AppController = async (c) => {
|
const followController: AppController = async (c) => {
|
||||||
const sourcePubkey = c.get('pubkey')!;
|
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -253,7 +257,7 @@ const followController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/accounts/#unfollow */
|
/** https://docs.joinmastodon.org/methods/accounts/#unfollow */
|
||||||
const unfollowController: AppController = async (c) => {
|
const unfollowController: AppController = async (c) => {
|
||||||
const sourcePubkey = c.get('pubkey')!;
|
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -290,7 +294,7 @@ const unblockController: AppController = (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/accounts/#mute */
|
/** https://docs.joinmastodon.org/methods/accounts/#mute */
|
||||||
const muteController: AppController = async (c) => {
|
const muteController: AppController = async (c) => {
|
||||||
const sourcePubkey = c.get('pubkey')!;
|
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -305,7 +309,7 @@ const muteController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/accounts/#unmute */
|
/** https://docs.joinmastodon.org/methods/accounts/#unmute */
|
||||||
const unmuteController: AppController = async (c) => {
|
const unmuteController: AppController = async (c) => {
|
||||||
const sourcePubkey = c.get('pubkey')!;
|
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const targetPubkey = c.req.param('pubkey');
|
const targetPubkey = c.req.param('pubkey');
|
||||||
|
|
||||||
await updateListEvent(
|
await updateListEvent(
|
||||||
|
|
@ -319,7 +323,7 @@ const unmuteController: AppController = async (c) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const favouritesController: 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 params = paginationSchema.parse(c.req.query());
|
||||||
const { signal } = c.req.raw;
|
const { signal } = c.req.raw;
|
||||||
|
|
||||||
|
|
@ -335,7 +339,11 @@ const favouritesController: AppController = async (c) => {
|
||||||
const events1 = await Storages.db.query([{ kinds: [1], ids }], { signal })
|
const events1 = await Storages.db.query([{ kinds: [1], ids }], { signal })
|
||||||
.then((events) => hydrateEvents({ events, storage: Storages.db, 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);
|
return paginated(c, events1, statuses);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { renderStatuses } from '@/views.ts';
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/bookmarks/#get */
|
/** https://docs.joinmastodon.org/methods/bookmarks/#get */
|
||||||
const bookmarksController: AppController = async (c) => {
|
const bookmarksController: AppController = async (c) => {
|
||||||
const pubkey = c.get('pubkey')!;
|
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const { signal } = c.req.raw;
|
const { signal } = c.req.raw;
|
||||||
|
|
||||||
const [event10003] = await Storages.db.query(
|
const [event10003] = await Storages.db.query(
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ interface Marker {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const markersController: AppController = async (c) => {
|
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 timelines = c.req.queries('timeline[]') ?? [];
|
||||||
|
|
||||||
const results = await kv.getMany<Marker[]>(
|
const results = await kv.getMany<Marker[]>(
|
||||||
|
|
@ -37,7 +37,7 @@ const markerDataSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateMarkersController: AppController = async (c) => {
|
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 record = z.record(z.enum(['home', 'notifications']), markerDataSchema).parse(await parseBody(c.req.raw));
|
||||||
const timelines = Object.keys(record) as Timeline[];
|
const timelines = Object.keys(record) as Timeline[];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const mediaBodySchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mediaController: AppController = async (c) => {
|
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 result = mediaBodySchema.safeParse(await parseBody(c.req.raw));
|
||||||
const { signal } = c.req.raw;
|
const { signal } = c.req.raw;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { renderAccounts } from '@/views.ts';
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/mutes/#get */
|
/** https://docs.joinmastodon.org/methods/mutes/#get */
|
||||||
const mutesController: AppController = async (c) => {
|
const mutesController: AppController = async (c) => {
|
||||||
const pubkey = c.get('pubkey')!;
|
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const { signal } = c.req.raw;
|
const { signal } = c.req.raw;
|
||||||
|
|
||||||
const [event10000] = await Storages.db.query(
|
const [event10000] = await Storages.db.query(
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { paginated, paginationSchema } from '@/utils/api.ts';
|
import { paginated, paginationSchema } from '@/utils/api.ts';
|
||||||
import { renderNotification } from '@/views/mastodon/notifications.ts';
|
import { renderNotification } from '@/views/mastodon/notifications.ts';
|
||||||
|
|
||||||
const notificationsController: AppController = (c) => {
|
const notificationsController: AppController = async (c) => {
|
||||||
const pubkey = c.get('pubkey')!;
|
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const { since, until } = paginationSchema.parse(c.req.query());
|
const { since, until } = paginationSchema.parse(c.req.query());
|
||||||
|
|
||||||
return renderNotifications(c, [{ kinds: [1, 6, 7], '#p': [pubkey], since, until }]);
|
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[]) {
|
async function renderNotifications(c: AppContext, filters: NostrFilter[]) {
|
||||||
const store = c.get('store');
|
const store = c.get('store');
|
||||||
const pubkey = c.get('pubkey')!;
|
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const { signal } = c.req.raw;
|
const { signal } = c.req.raw;
|
||||||
|
|
||||||
const events = await store
|
const events = await store
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,15 @@ const reportController: AppController = async (c) => {
|
||||||
/** https://docs.joinmastodon.org/methods/admin/reports/#get */
|
/** https://docs.joinmastodon.org/methods/admin/reports/#get */
|
||||||
const adminReportsController: AppController = async (c) => {
|
const adminReportsController: AppController = async (c) => {
|
||||||
const store = c.get('store');
|
const store = c.get('store');
|
||||||
|
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||||
|
|
||||||
const reports = await store.query([{ kinds: [1984], '#P': [Conf.pubkey] }])
|
const reports = await store.query([{ kinds: [1984], '#P': [Conf.pubkey] }])
|
||||||
.then((events) => hydrateEvents({ storage: store, events: events, signal: c.req.raw.signal }))
|
.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);
|
return c.json(reports);
|
||||||
};
|
};
|
||||||
|
|
@ -67,7 +73,7 @@ const adminReportController: AppController = async (c) => {
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
const { signal } = c.req.raw;
|
const { signal } = c.req.raw;
|
||||||
const store = c.get('store');
|
const store = c.get('store');
|
||||||
const pubkey = c.get('pubkey');
|
const pubkey = await c.get('signer')?.getPublicKey();
|
||||||
|
|
||||||
const [event] = await store.query([{
|
const [event] = await store.query([{
|
||||||
kinds: [1984],
|
kinds: [1984],
|
||||||
|
|
@ -89,7 +95,7 @@ const adminReportResolveController: AppController = async (c) => {
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
const { signal } = c.req.raw;
|
const { signal } = c.req.raw;
|
||||||
const store = c.get('store');
|
const store = c.get('store');
|
||||||
const pubkey = c.get('pubkey');
|
const pubkey = await c.get('signer')?.getPublicKey();
|
||||||
|
|
||||||
const [event] = await store.query([{
|
const [event] = await store.query([{
|
||||||
kinds: [1984],
|
kinds: [1984],
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ const searchController: AppController = async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = dedupeEvents(events);
|
const results = dedupeEvents(events);
|
||||||
|
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||||
|
|
||||||
const [accounts, statuses] = await Promise.all([
|
const [accounts, statuses] = await Promise.all([
|
||||||
Promise.all(
|
Promise.all(
|
||||||
|
|
@ -54,7 +55,7 @@ const searchController: AppController = async (c) => {
|
||||||
Promise.all(
|
Promise.all(
|
||||||
results
|
results
|
||||||
.filter((event) => event.kind === 1)
|
.filter((event) => event.kind === 1)
|
||||||
.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))
|
.map((event) => renderStatus(event, { viewerPubkey }))
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ const statusController: AppController = async (c) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (event) {
|
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);
|
return c.json({ error: 'Event not found.' }, 404);
|
||||||
|
|
@ -89,9 +89,11 @@ const createStatusController: AppController = async (c) => {
|
||||||
tags.push(['subject', data.spoiler_text]);
|
tags.push(['subject', data.spoiler_text]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||||
|
|
||||||
if (data.media_ids?.length) {
|
if (data.media_ids?.length) {
|
||||||
const media = await getUnattachedMediaByIds(data.media_ids)
|
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]));
|
.then((media) => media.map(({ url, data }) => ['media', url, data]));
|
||||||
|
|
||||||
tags.push(...media);
|
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 deleteStatusController: AppController = async (c) => {
|
||||||
const id = c.req.param('id');
|
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 });
|
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 contextController: AppController = async (c) => {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const event = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] });
|
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[]) {
|
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);
|
return statuses.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,7 +209,7 @@ const favouriteController: AppController = async (c) => {
|
||||||
],
|
],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') });
|
const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() });
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
status.favourited = true;
|
status.favourited = true;
|
||||||
|
|
@ -247,7 +252,7 @@ const reblogStatusController: AppController = async (c) => {
|
||||||
signal: signal,
|
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);
|
return c.json(status);
|
||||||
};
|
};
|
||||||
|
|
@ -255,7 +260,7 @@ const reblogStatusController: AppController = async (c) => {
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#unreblog */
|
/** https://docs.joinmastodon.org/methods/statuses/#unreblog */
|
||||||
const unreblogStatusController: AppController = async (c) => {
|
const unreblogStatusController: AppController = async (c) => {
|
||||||
const eventId = c.req.param('id');
|
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, {
|
const event = await getEvent(eventId, {
|
||||||
kind: 1,
|
kind: 1,
|
||||||
|
|
@ -282,7 +287,7 @@ const rebloggedByController: AppController = (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#bookmark */
|
/** https://docs.joinmastodon.org/methods/statuses/#bookmark */
|
||||||
const bookmarkController: AppController = async (c) => {
|
const bookmarkController: AppController = async (c) => {
|
||||||
const pubkey = c.get('pubkey')!;
|
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
|
|
||||||
const event = await getEvent(eventId, {
|
const event = await getEvent(eventId, {
|
||||||
|
|
@ -309,7 +314,7 @@ const bookmarkController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#unbookmark */
|
/** https://docs.joinmastodon.org/methods/statuses/#unbookmark */
|
||||||
const unbookmarkController: AppController = async (c) => {
|
const unbookmarkController: AppController = async (c) => {
|
||||||
const pubkey = c.get('pubkey')!;
|
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
|
|
||||||
const event = await getEvent(eventId, {
|
const event = await getEvent(eventId, {
|
||||||
|
|
@ -336,7 +341,7 @@ const unbookmarkController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#pin */
|
/** https://docs.joinmastodon.org/methods/statuses/#pin */
|
||||||
const pinController: AppController = async (c) => {
|
const pinController: AppController = async (c) => {
|
||||||
const pubkey = c.get('pubkey')!;
|
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
|
|
||||||
const event = await getEvent(eventId, {
|
const event = await getEvent(eventId, {
|
||||||
|
|
@ -363,7 +368,7 @@ const pinController: AppController = async (c) => {
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/statuses/#unpin */
|
/** https://docs.joinmastodon.org/methods/statuses/#unpin */
|
||||||
const unpinController: AppController = async (c) => {
|
const unpinController: AppController = async (c) => {
|
||||||
const pubkey = c.get('pubkey')!;
|
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const eventId = c.req.param('id');
|
const eventId = c.req.param('id');
|
||||||
const { signal } = c.req.raw;
|
const { signal } = c.req.raw;
|
||||||
|
|
||||||
|
|
@ -423,7 +428,7 @@ const zapController: AppController = async (c) => {
|
||||||
],
|
],
|
||||||
}, 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;
|
status.zapped = true;
|
||||||
|
|
||||||
return c.json(status);
|
return c.json(status);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
|
|
||||||
const homeTimelineController: AppController = async (c) => {
|
const homeTimelineController: AppController = async (c) => {
|
||||||
const params = paginationSchema.parse(c.req.query());
|
const params = paginationSchema.parse(c.req.query());
|
||||||
const pubkey = c.get('pubkey')!;
|
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||||
const authors = await getFeedPubkeys(pubkey);
|
const authors = await getFeedPubkeys(pubkey);
|
||||||
return renderStatuses(c, [{ authors, kinds: [1, 6], ...params }]);
|
return renderStatuses(c, [{ authors, kinds: [1, 6], ...params }]);
|
||||||
};
|
};
|
||||||
|
|
@ -61,11 +61,13 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||||
|
|
||||||
const statuses = (await Promise.all(events.map((event) => {
|
const statuses = (await Promise.all(events.map((event) => {
|
||||||
if (event.kind === 6) {
|
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);
|
}))).filter((boolean) => boolean);
|
||||||
|
|
||||||
if (!statuses.length) {
|
if (!statuses.length) {
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
|
||||||
|
|
@ -1,27 +1,28 @@
|
||||||
import { NostrEvent } from '@nostrify/nostrify';
|
import { NostrEvent } from '@nostrify/nostrify';
|
||||||
import { HTTPException } from 'hono';
|
import { HTTPException } from 'hono';
|
||||||
|
|
||||||
import { type AppContext, type AppMiddleware } from '@/app.ts';
|
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 {
|
import {
|
||||||
buildAuthEventTemplate,
|
buildAuthEventTemplate,
|
||||||
parseAuthRequest,
|
parseAuthRequest,
|
||||||
type ParseAuthRequestOpts,
|
type ParseAuthRequestOpts,
|
||||||
validateAuthEvent,
|
validateAuthEvent,
|
||||||
} from '@/utils/nip98.ts';
|
} from '@/utils/nip98.ts';
|
||||||
import { localRequest } from '@/utils/api.ts';
|
|
||||||
import { APISigner } from '@/signers/APISigner.ts';
|
|
||||||
import { findUser, User } from '@/db/users.ts';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-98 auth.
|
* NIP-98 auth.
|
||||||
* https://github.com/nostr-protocol/nips/blob/master/98.md
|
* https://github.com/nostr-protocol/nips/blob/master/98.md
|
||||||
*/
|
*/
|
||||||
function auth98(opts: ParseAuthRequestOpts = {}): AppMiddleware {
|
function auth98Middleware(opts: ParseAuthRequestOpts = {}): AppMiddleware {
|
||||||
return async (c, next) => {
|
return async (c, next) => {
|
||||||
const req = localRequest(c);
|
const req = localRequest(c);
|
||||||
const result = await parseAuthRequest(req, opts);
|
const result = await parseAuthRequest(req, opts);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
c.set('pubkey', result.data.pubkey);
|
c.set('signer', new ConnectSigner(result.data.pubkey));
|
||||||
c.set('proof', result.data);
|
c.set('proof', result.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,9 +34,8 @@ type UserRole = 'user' | 'admin';
|
||||||
|
|
||||||
/** Require the user to prove their role before invoking the controller. */
|
/** Require the user to prove their role before invoking the controller. */
|
||||||
function requireRole(role: UserRole, opts?: ParseAuthRequestOpts): AppMiddleware {
|
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 });
|
const user = await findUser({ pubkey: proof.pubkey });
|
||||||
c.set('user', user);
|
|
||||||
|
|
||||||
if (user && matchesRole(user, role)) {
|
if (user && matchesRole(user, role)) {
|
||||||
await next();
|
await next();
|
||||||
|
|
@ -70,7 +70,7 @@ function withProof(
|
||||||
opts?: ParseAuthRequestOpts,
|
opts?: ParseAuthRequestOpts,
|
||||||
): AppMiddleware {
|
): AppMiddleware {
|
||||||
return async (c, next) => {
|
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);
|
const proof = c.get('proof') || await obtainProof(c, opts);
|
||||||
|
|
||||||
// Prevent people from accidentally using the wrong account. This has no other security implications.
|
// Prevent people from accidentally using the wrong account. This has no other security implications.
|
||||||
|
|
@ -79,7 +79,7 @@ function withProof(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (proof) {
|
if (proof) {
|
||||||
c.set('pubkey', proof.pubkey);
|
c.set('signer', new ConnectSigner(proof.pubkey));
|
||||||
c.set('proof', proof);
|
c.set('proof', proof);
|
||||||
await handler(c, proof, next);
|
await handler(c, proof, next);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -90,9 +90,16 @@ function withProof(
|
||||||
|
|
||||||
/** Get the proof over Nostr Connect. */
|
/** Get the proof over Nostr Connect. */
|
||||||
async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) {
|
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 req = localRequest(c);
|
||||||
const reqEvent = await buildAuthEventTemplate(req, opts);
|
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);
|
const result = await validateAuthEvent(req, resEvent, opts);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|
@ -100,4 +107,4 @@ async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { auth98, requireProof, requireRole };
|
export { auth98Middleware, requireProof, requireRole };
|
||||||
|
|
@ -5,7 +5,7 @@ import ExpiringCache from '@/utils/expiring-cache.ts';
|
||||||
|
|
||||||
const debug = Debug('ditto:middleware:cache');
|
const debug = Debug('ditto:middleware:cache');
|
||||||
|
|
||||||
export const cache = (options: {
|
export const cacheMiddleware = (options: {
|
||||||
cacheName: string;
|
cacheName: string;
|
||||||
expires?: number;
|
expires?: number;
|
||||||
}): MiddlewareHandler => {
|
}): MiddlewareHandler => {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { AppMiddleware } from '@/app.ts';
|
import { AppMiddleware } from '@/app.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
|
|
||||||
const csp = (): AppMiddleware => {
|
export const cspMiddleware = (): AppMiddleware => {
|
||||||
return async (c, next) => {
|
return async (c, next) => {
|
||||||
const { host, protocol, origin } = Conf.url;
|
const { host, protocol, origin } = Conf.url;
|
||||||
const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:';
|
const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:';
|
||||||
|
|
@ -26,5 +26,3 @@ const csp = (): AppMiddleware => {
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export { csp };
|
|
||||||
12
src/middleware/requireSigner.ts
Normal file
12
src/middleware/requireSigner.ts
Normal file
|
|
@ -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();
|
||||||
|
};
|
||||||
41
src/middleware/signerMiddleware.ts
Normal file
41
src/middleware/signerMiddleware.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
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})$`);
|
||||||
|
|
||||||
|
/** Make a `signer` object available to all controllers, or unset if the user isn't logged in. */
|
||||||
|
export const signerMiddleware: AppMiddleware = async (c, next) => {
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.debug('The user is not logged in');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
@ -3,8 +3,8 @@ import { UserStore } from '@/storages/UserStore.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
|
|
||||||
/** Store middleware. */
|
/** Store middleware. */
|
||||||
const storeMiddleware: AppMiddleware = async (c, next) => {
|
export const storeMiddleware: AppMiddleware = async (c, next) => {
|
||||||
const pubkey = c.get('pubkey');
|
const pubkey = await c.get('signer')?.getPublicKey();
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
const store = new UserStore(pubkey, Storages.admin);
|
const store = new UserStore(pubkey, Storages.admin);
|
||||||
|
|
@ -14,5 +14,3 @@ const storeMiddleware: AppMiddleware = async (c, next) => {
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export { storeMiddleware };
|
|
||||||
|
|
@ -1,65 +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<string> {
|
|
||||||
return this.signer.getPublicKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
async signEvent(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> {
|
|
||||||
return this.signer.signEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly nip04 = {
|
|
||||||
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
|
||||||
return this.signer.nip04!.encrypt(pubkey, plaintext);
|
|
||||||
},
|
|
||||||
|
|
||||||
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
|
||||||
return this.signer.nip04!.decrypt(pubkey, ciphertext);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
readonly nip44 = {
|
|
||||||
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
|
||||||
return this.signer.nip44!.encrypt(pubkey, plaintext);
|
|
||||||
},
|
|
||||||
|
|
||||||
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
|
||||||
return this.signer.nip44!.decrypt(pubkey, ciphertext);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
39
src/signers/ConnectSigner.ts
Normal file
39
src/signers/ConnectSigner.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
// deno-lint-ignore-file require-await
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
relay: Storages.pubsub,
|
||||||
|
signer: new AdminSigner(),
|
||||||
|
timeout: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._pubkey = pubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent unnecessary NIP-46 round-trips.
|
||||||
|
async getPublicKey(): Promise<string> {
|
||||||
|
return this._pubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the user's relays if they passed in an `nprofile` auth token. */
|
||||||
|
async getRelays(): Promise<Record<string, { read: boolean; write: boolean }>> {
|
||||||
|
return this.relays?.reduce<Record<string, { read: boolean; write: boolean }>>((acc, relay) => {
|
||||||
|
acc[relay] = { read: true, write: true };
|
||||||
|
return acc;
|
||||||
|
}, {}) ?? {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,6 @@ import { Conf } from '@/config.ts';
|
||||||
import * as pipeline from '@/pipeline.ts';
|
import * as pipeline from '@/pipeline.ts';
|
||||||
import { RelayError } from '@/RelayError.ts';
|
import { RelayError } from '@/RelayError.ts';
|
||||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||||
import { APISigner } from '@/signers/APISigner.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
|
|
||||||
|
|
@ -22,7 +21,13 @@ type EventStub = TypeFest.SetOptional<EventTemplate, 'content' | 'created_at' |
|
||||||
|
|
||||||
/** Publish an event through the pipeline. */
|
/** Publish an event through the pipeline. */
|
||||||
async function createEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
async function createEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
||||||
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({
|
const event = await signer.signEvent({
|
||||||
content: '',
|
content: '',
|
||||||
|
|
|
||||||
|
|
@ -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 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(
|
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`.
|
// TODO: pagination with min_id and max_id based on the order of `ids`.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue