mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
111 lines
3.1 KiB
TypeScript
111 lines
3.1 KiB
TypeScript
import { HTTPException } from '@hono/hono/http-exception';
|
|
import { type NostrSigner, type NRelay, NSecSigner } from '@nostrify/nostrify';
|
|
import { nip19 } from 'nostr-tools';
|
|
|
|
import { aesDecrypt } from '../auth/aes.ts';
|
|
import { getTokenHash } from '../auth/token.ts';
|
|
import { ConnectSigner } from '../signers/ConnectSigner.ts';
|
|
import { ReadOnlySigner } from '../signers/ReadOnlySigner.ts';
|
|
import { UserStore } from '../storages/UserStore.ts';
|
|
|
|
import type { DittoConf } from '@ditto/conf';
|
|
import type { DittoDB } from '@ditto/db';
|
|
import type { DittoMiddleware } from '@ditto/router';
|
|
import type { User } from './User.ts';
|
|
|
|
/** We only accept "Bearer" type. */
|
|
const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`);
|
|
|
|
export function tokenMiddleware(): DittoMiddleware<{ user?: User }> {
|
|
return async (c, next) => {
|
|
const header = c.req.header('authorization');
|
|
|
|
if (header) {
|
|
const { relay, conf } = c.var;
|
|
|
|
const signer = await getSigner(header, c.var);
|
|
const userPubkey = await signer.getPublicKey();
|
|
const adminPubkey = await conf.signer.getPublicKey();
|
|
|
|
const user: User = {
|
|
signer,
|
|
relay: new UserStore({ relay, userPubkey, adminPubkey }),
|
|
};
|
|
|
|
c.set('user', user);
|
|
}
|
|
|
|
await next();
|
|
};
|
|
}
|
|
|
|
interface GetSignerOpts {
|
|
db: DittoDB;
|
|
conf: DittoConf;
|
|
relay: NRelay;
|
|
}
|
|
|
|
function getSigner(header: string, opts: GetSignerOpts): NostrSigner | Promise<NostrSigner> {
|
|
const match = header.match(BEARER_REGEX);
|
|
|
|
if (!match) {
|
|
throw new HTTPException(400, { message: 'Invalid Authorization header.' });
|
|
}
|
|
|
|
const [_, bech32] = match;
|
|
|
|
if (isToken(bech32)) {
|
|
return getSignerFromToken(bech32, opts);
|
|
} else {
|
|
return getSignerFromNip19(bech32);
|
|
}
|
|
}
|
|
|
|
function isToken(value: string): value is `token1${string}` {
|
|
return value.startsWith('token1');
|
|
}
|
|
|
|
async function getSignerFromToken(token: `token1${string}`, opts: GetSignerOpts): Promise<NostrSigner> {
|
|
const { conf, db, relay } = opts;
|
|
|
|
try {
|
|
const tokenHash = await getTokenHash(token);
|
|
|
|
const row = 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, row.nip46_sk_enc);
|
|
|
|
return new ConnectSigner({
|
|
bunkerPubkey: row.bunker_pubkey,
|
|
userPubkey: row.pubkey,
|
|
signer: new NSecSigner(nep46Seckey),
|
|
relays: row.nip46_relays,
|
|
relay,
|
|
});
|
|
} catch {
|
|
throw new HTTPException(401, { message: 'Token is wrong or expired.' });
|
|
}
|
|
}
|
|
|
|
function getSignerFromNip19(bech32: string): NostrSigner {
|
|
try {
|
|
const decoded = nip19.decode(bech32);
|
|
|
|
switch (decoded.type) {
|
|
case 'npub':
|
|
return new ReadOnlySigner(decoded.data);
|
|
case 'nprofile':
|
|
return new ReadOnlySigner(decoded.data.pubkey);
|
|
case 'nsec':
|
|
return new NSecSigner(decoded.data);
|
|
}
|
|
} catch {
|
|
// fallthrough
|
|
}
|
|
|
|
throw new HTTPException(401, { message: 'Invalid NIP-19 identifier in Authorization header.' });
|
|
}
|