mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Make @ditto/auth and @ditto/storages packages
This commit is contained in:
parent
5210275d23
commit
d1f8e3b92c
23 changed files with 158 additions and 16 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"workspace": [
|
"workspace": [
|
||||||
"./packages/api",
|
"./packages/api",
|
||||||
|
"./packages/auth",
|
||||||
"./packages/conf",
|
"./packages/conf",
|
||||||
"./packages/db",
|
"./packages/db",
|
||||||
"./packages/ditto",
|
"./packages/ditto",
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
"./packages/policies",
|
"./packages/policies",
|
||||||
"./packages/ratelimiter",
|
"./packages/ratelimiter",
|
||||||
"./packages/signers",
|
"./packages/signers",
|
||||||
|
"./packages/storages",
|
||||||
"./packages/translators",
|
"./packages/translators",
|
||||||
"./packages/uploaders",
|
"./packages/uploaders",
|
||||||
"./packages/utils"
|
"./packages/utils"
|
||||||
|
|
|
||||||
5
packages/api/DittoMiddleware.ts
Normal file
5
packages/api/DittoMiddleware.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import type { MiddlewareHandler } from '@hono/hono';
|
||||||
|
import type { DittoEnv } from './DittoEnv.ts';
|
||||||
|
|
||||||
|
// deno-lint-ignore ban-types
|
||||||
|
export type DittoMiddleware<T extends {}> = MiddlewareHandler<DittoEnv & { Variables: T }>;
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./mod.ts",
|
".": "./mod.ts",
|
||||||
"./middleware": "./middleware/mod.ts",
|
"./middleware": "./middleware/mod.ts",
|
||||||
|
"./routes": "./routes/mod.ts",
|
||||||
|
"./schema": "./schema.ts",
|
||||||
"./views": "./views/mod.ts"
|
"./views": "./views/mod.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
packages/api/middleware/mod.ts
Normal file
1
packages/api/middleware/mod.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { userMiddleware } from './userMiddleware.ts';
|
||||||
92
packages/api/middleware/userMiddleware.ts
Normal file
92
packages/api/middleware/userMiddleware.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { aesDecrypt, getTokenHash } from '@ditto/auth';
|
||||||
|
import { ConnectSigner, ReadOnlySigner } from '@ditto/signers';
|
||||||
|
import { HTTPException } from '@hono/hono/http-exception';
|
||||||
|
import { type NostrSigner, NSecSigner, type NStore } from '@nostrify/nostrify';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
import type { DittoMiddleware } from '../DittoMiddleware.ts';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
signer: NostrSigner;
|
||||||
|
store: NStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserMiddlewareOpts {
|
||||||
|
/** Returns a 401 response if no user can be determined. */
|
||||||
|
required?: boolean;
|
||||||
|
/** Whether the user must prove themselves with a NIP-98 auth challenge. */
|
||||||
|
privileged: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore The types are right.
|
||||||
|
export function userMiddleware(opts: { privileged: boolean; required: true }): DittoMiddleware<{ user: User }>;
|
||||||
|
export function userMiddleware(opts: { privileged: true; required?: boolean }): DittoMiddleware<{ user: User }>;
|
||||||
|
export function userMiddleware(opts: { privileged: false; required?: boolean }): DittoMiddleware<{ user?: User }>;
|
||||||
|
export function userMiddleware(opts: { privileged?: boolean; required?: boolean }): DittoMiddleware<{ user?: User }> {
|
||||||
|
/** We only accept "Bearer" type. */
|
||||||
|
const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`);
|
||||||
|
|
||||||
|
return async (c, next) => {
|
||||||
|
const { conf, db, store } = c.var;
|
||||||
|
|
||||||
|
const header = c.req.header('authorization');
|
||||||
|
const match = header?.match(BEARER_REGEX);
|
||||||
|
|
||||||
|
let signer: NostrSigner | undefined;
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [_, bech32] = match;
|
||||||
|
|
||||||
|
if (bech32.startsWith('token1')) {
|
||||||
|
try {
|
||||||
|
const tokenHash = await getTokenHash(bech32 as `token1${string}`);
|
||||||
|
|
||||||
|
const { pubkey: userPubkey, bunker_pubkey: bunkerPubkey, nip46_sk_enc, nip46_relays } = await db.kysely
|
||||||
|
.selectFrom('auth_tokens')
|
||||||
|
.select(['pubkey', 'bunker_pubkey', 'nip46_sk_enc', 'nip46_relays'])
|
||||||
|
.where('token_hash', '=', tokenHash)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
const nep46Seckey = await aesDecrypt(conf.seckey, nip46_sk_enc);
|
||||||
|
|
||||||
|
signer = new ConnectSigner({
|
||||||
|
bunkerPubkey,
|
||||||
|
userPubkey,
|
||||||
|
signer: new NSecSigner(nep46Seckey),
|
||||||
|
relays: nip46_relays,
|
||||||
|
relay: store,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new HTTPException(401);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const decoded = nip19.decode(bech32!);
|
||||||
|
|
||||||
|
switch (decoded.type) {
|
||||||
|
case 'npub':
|
||||||
|
signer = new ReadOnlySigner(decoded.data);
|
||||||
|
break;
|
||||||
|
case 'nprofile':
|
||||||
|
signer = new ReadOnlySigner(decoded.data.pubkey);
|
||||||
|
break;
|
||||||
|
case 'nsec':
|
||||||
|
signer = new NSecSigner(decoded.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
throw new HTTPException(401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signer) {
|
||||||
|
const user: User = { signer, store };
|
||||||
|
c.set('user', user);
|
||||||
|
} else {
|
||||||
|
throw new HTTPException(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
||||||
1
packages/api/routes/mod.ts
Normal file
1
packages/api/routes/mod.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { timelinesRoute } from './timelinesRoute.ts';
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { DittoRoute } from '@ditto/api';
|
import { DittoRoute } from '@ditto/api';
|
||||||
|
import { userMiddleware } from '@ditto/api/middleware';
|
||||||
|
import { booleanParamSchema, languageSchema } from '@ditto/api/schema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { NostrFilter } from '@nostrify/nostrify';
|
import type { NostrFilter } from '@nostrify/nostrify';
|
||||||
|
|
@ -11,10 +13,9 @@ const homeQuerySchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
route.get('/home', async (c) => {
|
route.get('/home', async (c) => {
|
||||||
c.req.valid('json');
|
|
||||||
const { user, pagination } = c.var;
|
const { user, pagination } = c.var;
|
||||||
|
|
||||||
const pubkey = await user!.signer.getPublicKey()!;
|
const pubkey = await user.signer.getPublicKey()!;
|
||||||
const result = homeQuerySchema.safeParse(c.req.query());
|
const result = homeQuerySchema.safeParse(c.req.query());
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|
@ -123,4 +124,4 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
||||||
return paginated(c, events, statuses);
|
return paginated(c, events, statuses);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { hashtagTimelineController, homeTimelineController, publicTimelineController, suggestedTimelineController };
|
export { route as timelinesRoute };
|
||||||
|
|
|
||||||
13
packages/api/schema.test.ts
Normal file
13
packages/api/schema.test.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { assertEquals, assertThrows } from '@std/assert';
|
||||||
|
|
||||||
|
import { booleanParamSchema } from './schema.ts';
|
||||||
|
|
||||||
|
Deno.test('booleanParamSchema', () => {
|
||||||
|
assertEquals(booleanParamSchema.parse('true'), true);
|
||||||
|
assertEquals(booleanParamSchema.parse('false'), false);
|
||||||
|
|
||||||
|
assertThrows(() => booleanParamSchema.parse('invalid'));
|
||||||
|
assertThrows(() => booleanParamSchema.parse('undefined'));
|
||||||
|
|
||||||
|
assertEquals(booleanParamSchema.optional().parse(undefined), undefined);
|
||||||
|
});
|
||||||
11
packages/api/schema.ts
Normal file
11
packages/api/schema.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import ISO6391 from 'iso-639-1';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/** https://github.com/colinhacks/zod/issues/1630#issuecomment-1365983831 */
|
||||||
|
export const booleanParamSchema = z.enum(['true', 'false']).transform((value) => value === 'true');
|
||||||
|
|
||||||
|
/** Value is a ISO-639-1 language code. */
|
||||||
|
export const languageSchema = z.string().refine(
|
||||||
|
(val) => ISO6391.validate(val),
|
||||||
|
{ message: 'Not a valid language in ISO-639-1 format' },
|
||||||
|
);
|
||||||
6
packages/auth/deno.json
Normal file
6
packages/auth/deno.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@ditto/auth",
|
||||||
|
"exports": {
|
||||||
|
".": "./mod.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/auth/mod.ts
Normal file
2
packages/auth/mod.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { aesDecrypt, aesEncrypt } from './aes.ts';
|
||||||
|
export { generateToken, getTokenHash } from './token.ts';
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { generateToken, getTokenHash } from './auth.ts';
|
import { generateToken, getTokenHash } from './token.ts';
|
||||||
|
|
||||||
Deno.bench('generateToken', async () => {
|
Deno.bench('generateToken', async () => {
|
||||||
await generateToken();
|
await generateToken();
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { assertEquals } from '@std/assert';
|
import { assertEquals } from '@std/assert';
|
||||||
import { decodeHex, encodeHex } from '@std/encoding/hex';
|
import { decodeHex, encodeHex } from '@std/encoding/hex';
|
||||||
|
|
||||||
import { generateToken, getTokenHash } from './auth.ts';
|
import { generateToken, getTokenHash } from './token.ts';
|
||||||
|
|
||||||
Deno.test('generateToken', async () => {
|
Deno.test('generateToken', async () => {
|
||||||
const sk = decodeHex('a0968751df8fd42f362213f08751911672f2a037113b392403bbb7dd31b71c95');
|
const sk = decodeHex('a0968751df8fd42f362213f08751911672f2a037113b392403bbb7dd31b71c95');
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import ISO6391, { LanguageCode } from 'iso-639-1';
|
|
||||||
import { NSchema as n } from '@nostrify/nostrify';
|
import { NSchema as n } from '@nostrify/nostrify';
|
||||||
|
import ISO6391, { LanguageCode } from 'iso-639-1';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
/** Validates individual items in an array, dropping any that aren't valid. */
|
/** Validates individual items in an array, dropping any that aren't valid. */
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { MockRelay } from '@nostrify/nostrify/test';
|
import { MockRelay } from '@nostrify/nostrify/test';
|
||||||
|
|
||||||
import { assertEquals } from '@std/assert';
|
import { assertEquals } from '@std/assert';
|
||||||
import { UserStore } from '@/storages/UserStore.ts';
|
|
||||||
|
import { UserStore } from './UserStore.ts';
|
||||||
|
|
||||||
import userBlack from '~/fixtures/events/kind-0-black.json' with { type: 'json' };
|
import userBlack from '~/fixtures/events/kind-0-black.json' with { type: 'json' };
|
||||||
import userMe from '~/fixtures/events/event-0-makes-repost-with-quote-repost.json' with { type: 'json' };
|
import userMe from '~/fixtures/events/event-0-makes-repost-with-quote-repost.json' with { type: 'json' };
|
||||||
|
|
@ -23,7 +23,7 @@ Deno.test('query events of users that are not muted', async () => {
|
||||||
await store.event(userMeCopy);
|
await store.event(userMeCopy);
|
||||||
await store.event(event1authorUserMeCopy);
|
await store.event(event1authorUserMeCopy);
|
||||||
|
|
||||||
assertEquals(await store.query([{ kinds: [1] }], { limit: 1 }), []);
|
assertEquals(await store.query([{ kinds: [1], limit: 1 }]), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('user never muted anyone', async () => {
|
Deno.test('user never muted anyone', async () => {
|
||||||
|
|
@ -37,5 +37,5 @@ Deno.test('user never muted anyone', async () => {
|
||||||
await store.event(userBlackCopy);
|
await store.event(userBlackCopy);
|
||||||
await store.event(userMeCopy);
|
await store.event(userMeCopy);
|
||||||
|
|
||||||
assertEquals(await store.query([{ kinds: [0], authors: [userMeCopy.pubkey] }], { limit: 1 }), [userMeCopy]);
|
assertEquals(await store.query([{ kinds: [0], authors: [userMeCopy.pubkey], limit: 1 }]), [userMeCopy]);
|
||||||
});
|
});
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
|
import { getTagSet } from '@ditto/utils/tags';
|
||||||
import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
|
||||||
import { getTagSet } from '../../utils/tags.ts';
|
|
||||||
|
|
||||||
export class UserStore implements NStore {
|
export class UserStore implements NStore {
|
||||||
private promise: Promise<DittoEvent[]> | undefined;
|
private promise: Promise<NostrEvent[]> | undefined;
|
||||||
|
|
||||||
constructor(private pubkey: string, private store: NStore) {}
|
constructor(private pubkey: string, private store: NStore) {}
|
||||||
|
|
||||||
|
|
@ -16,7 +14,7 @@ export class UserStore implements NStore {
|
||||||
* Query events that `pubkey` did not mute
|
* Query events that `pubkey` did not mute
|
||||||
* https://github.com/nostr-protocol/nips/blob/master/51.md#standard-lists
|
* https://github.com/nostr-protocol/nips/blob/master/51.md#standard-lists
|
||||||
*/
|
*/
|
||||||
async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise<DittoEvent[]> {
|
async query(filters: NostrFilter[], opts: { signal?: AbortSignal } = {}): Promise<NostrEvent[]> {
|
||||||
const events = await this.store.query(filters, opts);
|
const events = await this.store.query(filters, opts);
|
||||||
const pubkeys = await this.getMutedPubkeys();
|
const pubkeys = await this.getMutedPubkeys();
|
||||||
|
|
||||||
|
|
@ -25,7 +23,7 @@ export class UserStore implements NStore {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMuteList(): Promise<DittoEvent | undefined> {
|
private async getMuteList(): Promise<NostrEvent | undefined> {
|
||||||
if (!this.promise) {
|
if (!this.promise) {
|
||||||
this.promise = this.store.query([{ authors: [this.pubkey], kinds: [10000], limit: 1 }]);
|
this.promise = this.store.query([{ authors: [this.pubkey], kinds: [10000], limit: 1 }]);
|
||||||
}
|
}
|
||||||
6
packages/storages/deno.json
Normal file
6
packages/storages/deno.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@ditto/storages",
|
||||||
|
"exports": {
|
||||||
|
".": "./mod.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/storages/mod.ts
Normal file
1
packages/storages/mod.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { UserStore } from './UserStore.ts';
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@ditto/utils",
|
"name": "@ditto/utils",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
"./auth": "./auth.ts",
|
||||||
"./tags": "./tags.ts"
|
"./tags": "./tags.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue