Merge branch 'rm-admin-signer' into 'main'

Remove AdminSigner, Conf.pubkey, Conf.nsec, add Conf.signer

See merge request soapbox-pub/ditto!682
This commit is contained in:
Alex Gleason 2025-02-20 18:07:01 +00:00
commit 64e71b0ba8
36 changed files with 135 additions and 125 deletions

View file

@ -10,7 +10,7 @@ Deno.test('confMw', async () => {
const app = new Hono(); const app = new Hono();
app.get('/', confMw(env), (c) => c.text(c.var.conf.pubkey)); app.get('/', confMw(env), async (c) => c.text(await c.var.conf.signer.getPublicKey()));
const response = await app.request('/'); const response = await app.request('/');
const body = await response.text(); const body = await response.text();

View file

@ -9,12 +9,11 @@ Deno.test('DittoConfig', async (t) => {
const config = new DittoConf(env); const config = new DittoConf(env);
await t.step('nsec', () => { await t.step('signer', async () => {
assertEquals(config.nsec, 'nsec19shyxpuzd0cq2p5078fwnws7tyykypud6z205fzhlmlrs2vpz6hs83zwkw'); assertEquals(
}); await config.signer.getPublicKey(),
'1ba0c5ed1bbbf3b7eb0d7843ba16836a0201ea68a76bafcba507358c45911ff6',
await t.step('pubkey', () => { );
assertEquals(config.pubkey, '1ba0c5ed1bbbf3b7eb0d7843ba16836a0201ea68a76bafcba507358c45911ff6');
}); });
}); });
@ -22,8 +21,8 @@ Deno.test('DittoConfig defaults', async (t) => {
const env = new Map<string, string>(); const env = new Map<string, string>();
const config = new DittoConf(env); const config = new DittoConf(env);
await t.step('nsec throws', () => { await t.step('signer throws', () => {
assertThrows(() => config.nsec); assertThrows(() => config.signer);
}); });
await t.step('port', () => { await t.step('port', () => {

View file

@ -1,10 +1,11 @@
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import ISO6391, { type LanguageCode } from 'iso-639-1'; import { NSecSigner } from '@nostrify/nostrify';
import { getPublicKey, nip19 } from 'nostr-tools';
import { decodeBase64 } from '@std/encoding/base64'; import { decodeBase64 } from '@std/encoding/base64';
import { encodeBase64Url } from '@std/encoding/base64url'; import { encodeBase64Url } from '@std/encoding/base64url';
import ISO6391, { type LanguageCode } from 'iso-639-1';
import { nip19 } from 'nostr-tools';
import { getEcdsaPublicKey } from './utils/crypto.ts'; import { getEcdsaPublicKey } from './utils/crypto.ts';
import { optionalBooleanSchema, optionalNumberSchema } from './utils/schema.ts'; import { optionalBooleanSchema, optionalNumberSchema } from './utils/schema.ts';
@ -14,35 +15,36 @@ import { mergeURLPath } from './utils/url.ts';
export class DittoConf { export class DittoConf {
constructor(private env: { get(key: string): string | undefined }) {} constructor(private env: { get(key: string): string | undefined }) {}
/** Cached parsed admin pubkey value. */ /** Cached parsed admin signer. */
private _pubkey: string | undefined; private _signer: NSecSigner | undefined;
/** Cached parsed VAPID public key value. */ /** Cached parsed VAPID public key value. */
private _vapidPublicKey: Promise<string | undefined> | undefined; private _vapidPublicKey: Promise<string | undefined> | undefined;
/** Ditto admin secret key in nip19 format. This is the way it's configured by an admin. */ /**
get nsec(): `nsec1${string}` { * Ditto admin secret key in hex format.
const value = this.env.get('DITTO_NSEC'); * @deprecated Use `signer` instead. TODO: handle auth tokens.
if (!value) { */
get seckey(): Uint8Array {
const nsec = this.env.get('DITTO_NSEC');
if (!nsec) {
throw new Error('Missing DITTO_NSEC'); throw new Error('Missing DITTO_NSEC');
} }
if (!value.startsWith('nsec1')) {
if (!nsec.startsWith('nsec1')) {
throw new Error('Invalid DITTO_NSEC'); throw new Error('Invalid DITTO_NSEC');
} }
return value as `nsec1${string}`;
return nip19.decode(nsec as `nsec1${string}`).data;
} }
/** Ditto admin secret key in hex format. */ /** Ditto admin signer. */
get seckey(): Uint8Array { get signer(): NSecSigner {
return nip19.decode(this.nsec).data; if (!this._signer) {
this._signer = new NSecSigner(this.seckey);
} }
return this._signer;
/** Ditto admin public key in hex format. */
get pubkey(): string {
if (!this._pubkey) {
this._pubkey = getPublicKey(this.seckey);
}
return this._pubkey;
} }
/** Port to use when serving the HTTP server. */ /** Port to use when serving the HTTP server. */

View file

@ -211,7 +211,9 @@ const accountStatusesController: AppController = async (c) => {
const [[author], [user]] = await Promise.all([ const [[author], [user]] = await Promise.all([
store.query([{ kinds: [0], authors: [pubkey], limit: 1 }], { signal }), store.query([{ kinds: [0], authors: [pubkey], limit: 1 }], { signal }),
store.query([{ kinds: [30382], authors: [conf.pubkey], '#d': [pubkey], limit: 1 }], { signal }), store.query([{ kinds: [30382], authors: [await conf.signer.getPublicKey()], '#d': [pubkey], limit: 1 }], {
signal,
}),
]); ]);
if (author) { if (author) {

View file

@ -43,13 +43,15 @@ const adminAccountsController: AppController = async (c) => {
staff, staff,
} = adminAccountQuerySchema.parse(c.req.query()); } = adminAccountQuerySchema.parse(c.req.query());
const adminPubkey = await conf.signer.getPublicKey();
if (pending) { if (pending) {
if (disabled || silenced || suspended || sensitized) { if (disabled || silenced || suspended || sensitized) {
return c.json([]); return c.json([]);
} }
const orig = await store.query( const orig = await store.query(
[{ kinds: [30383], authors: [conf.pubkey], '#k': ['3036'], '#n': ['pending'], ...params }], [{ kinds: [30383], authors: [adminPubkey], '#k': ['3036'], '#n': ['pending'], ...params }],
{ signal }, { signal },
); );
@ -86,7 +88,10 @@ const adminAccountsController: AppController = async (c) => {
n.push('moderator'); n.push('moderator');
} }
const events = await store.query([{ kinds: [30382], authors: [conf.pubkey], '#n': n, ...params }], { signal }); const events = await store.query(
[{ kinds: [30382], authors: [adminPubkey], '#n': n, ...params }],
{ signal },
);
const pubkeys = new Set<string>( const pubkeys = new Set<string>(
events events
@ -157,9 +162,11 @@ const adminActionController: AppController = async (c) => {
} }
if (data.type === 'revoke_name') { if (data.type === 'revoke_name') {
n.revoke_name = true; n.revoke_name = true;
store.remove([{ kinds: [30360], authors: [conf.pubkey], '#p': [authorId] }]).catch((e: unknown) => { store.remove([{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#p': [authorId] }]).catch(
(e: unknown) => {
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) });
}); },
);
} }
await updateUser(authorId, n, c); await updateUser(authorId, n, c);
@ -185,7 +192,10 @@ const adminApproveController: AppController = async (c) => {
return c.json({ error: 'Invalid NIP-05' }, 400); return c.json({ error: 'Invalid NIP-05' }, 400);
} }
const [existing] = await store.query([{ kinds: [30360], authors: [conf.pubkey], '#d': [r], limit: 1 }]); const [existing] = await store.query([
{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#d': [r], limit: 1 },
]);
if (existing) { if (existing) {
return c.json({ error: 'NIP-05 already granted to another user' }, 400); return c.json({ error: 'NIP-05 already granted to another user' }, 400);
} }

View file

@ -9,7 +9,6 @@ import { createEvent, paginated, parseBody, updateAdminEvent } from '@/utils/api
import { getInstanceMetadata } from '@/utils/instance.ts'; import { getInstanceMetadata } from '@/utils/instance.ts';
import { deleteTag } from '@/utils/tags.ts'; import { deleteTag } from '@/utils/tags.ts';
import { DittoZapSplits, getZapSplits } from '@/utils/zap-split.ts'; import { DittoZapSplits, getZapSplits } from '@/utils/zap-split.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts';
import { screenshotsSchema } from '@/schemas/nostr.ts'; import { screenshotsSchema } from '@/schemas/nostr.ts';
import { booleanParamSchema, percentageSchema, wsUrlSchema } from '@/schema.ts'; import { booleanParamSchema, percentageSchema, wsUrlSchema } from '@/schema.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
@ -33,7 +32,7 @@ export const adminRelaysController: AppController = async (c) => {
const store = await Storages.db(); const store = await Storages.db();
const [event] = await store.query([ const [event] = await store.query([
{ kinds: [10002], authors: [conf.pubkey], limit: 1 }, { kinds: [10002], authors: [await conf.signer.getPublicKey()], limit: 1 },
]); ]);
if (!event) { if (!event) {
@ -44,10 +43,11 @@ export const adminRelaysController: AppController = async (c) => {
}; };
export const adminSetRelaysController: AppController = async (c) => { export const adminSetRelaysController: AppController = async (c) => {
const { conf } = c.var;
const store = await Storages.db(); const store = await Storages.db();
const relays = relaySchema.array().parse(await c.req.json()); const relays = relaySchema.array().parse(await c.req.json());
const event = await new AdminSigner().signEvent({ const event = await conf.signer.signEvent({
kind: 10002, kind: 10002,
tags: relays.map(({ url, marker }) => marker ? ['r', url, marker] : ['r', url]), tags: relays.map(({ url, marker }) => marker ? ['r', url, marker] : ['r', url]),
content: '', content: '',
@ -98,7 +98,7 @@ export const nameRequestController: AppController = async (c) => {
['r', name], ['r', name],
['L', 'nip05.domain'], ['L', 'nip05.domain'],
['l', name.split('@')[1], 'nip05.domain'], ['l', name.split('@')[1], 'nip05.domain'],
['p', conf.pubkey], ['p', await conf.signer.getPublicKey()],
], ],
}, c); }, c);
@ -124,7 +124,7 @@ export const nameRequestsController: AppController = async (c) => {
const filter: NostrFilter = { const filter: NostrFilter = {
kinds: [30383], kinds: [30383],
authors: [conf.pubkey], authors: [await conf.signer.getPublicKey()],
'#k': ['3036'], '#k': ['3036'],
'#p': [pubkey], '#p': [pubkey],
...params, ...params,
@ -179,7 +179,9 @@ export const updateZapSplitsController: AppController = async (c) => {
return c.json({ error: result.error }, 400); return c.json({ error: result.error }, 400);
} }
const dittoZapSplit = await getZapSplits(store, conf.pubkey); const adminPubkey = await conf.signer.getPublicKey();
const dittoZapSplit = await getZapSplits(store, adminPubkey);
if (!dittoZapSplit) { if (!dittoZapSplit) {
return c.json({ error: 'Zap split not activated, restart the server.' }, 404); return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
} }
@ -192,7 +194,7 @@ export const updateZapSplitsController: AppController = async (c) => {
} }
await updateListAdminEvent( await updateListAdminEvent(
{ kinds: [30078], authors: [conf.pubkey], '#d': ['pub.ditto.zapSplits'], limit: 1 }, { kinds: [30078], authors: [adminPubkey], '#d': ['pub.ditto.zapSplits'], limit: 1 },
(tags) => (tags) =>
pubkeys.reduce((accumulator, pubkey) => { pubkeys.reduce((accumulator, pubkey) => {
return addTag(accumulator, ['p', pubkey, data[pubkey].weight.toString(), data[pubkey].message]); return addTag(accumulator, ['p', pubkey, data[pubkey].weight.toString(), data[pubkey].message]);
@ -215,7 +217,9 @@ export const deleteZapSplitsController: AppController = async (c) => {
return c.json({ error: result.error }, 400); return c.json({ error: result.error }, 400);
} }
const dittoZapSplit = await getZapSplits(store, conf.pubkey); const adminPubkey = await conf.signer.getPublicKey();
const dittoZapSplit = await getZapSplits(store, adminPubkey);
if (!dittoZapSplit) { if (!dittoZapSplit) {
return c.json({ error: 'Zap split not activated, restart the server.' }, 404); return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
} }
@ -223,7 +227,7 @@ export const deleteZapSplitsController: AppController = async (c) => {
const { data } = result; const { data } = result;
await updateListAdminEvent( await updateListAdminEvent(
{ kinds: [30078], authors: [conf.pubkey], '#d': ['pub.ditto.zapSplits'], limit: 1 }, { kinds: [30078], authors: [adminPubkey], '#d': ['pub.ditto.zapSplits'], limit: 1 },
(tags) => (tags) =>
data.reduce((accumulator, currentValue) => { data.reduce((accumulator, currentValue) => {
return deleteTag(accumulator, ['p', currentValue]); return deleteTag(accumulator, ['p', currentValue]);
@ -238,7 +242,7 @@ export const getZapSplitsController: AppController = async (c) => {
const { conf } = c.var; const { conf } = c.var;
const store = c.get('store'); const store = c.get('store');
const dittoZapSplit: DittoZapSplits | undefined = await getZapSplits(store, conf.pubkey) ?? {}; const dittoZapSplit: DittoZapSplits | undefined = await getZapSplits(store, await conf.signer.getPublicKey()) ?? {};
if (!dittoZapSplit) { if (!dittoZapSplit) {
return c.json({ error: 'Zap split not activated, restart the server.' }, 404); return c.json({ error: 'Zap split not activated, restart the server.' }, 404);
} }
@ -311,7 +315,7 @@ export const updateInstanceController: AppController = async (c) => {
const { conf } = c.var; const { conf } = c.var;
const body = await parseBody(c.req.raw); const body = await parseBody(c.req.raw);
const result = updateInstanceSchema.safeParse(body); const result = updateInstanceSchema.safeParse(body);
const pubkey = conf.pubkey; const pubkey = await conf.signer.getPublicKey();
if (!result.success) { if (!result.success) {
return c.json(result.error, 422); return c.json(result.error, 422);

View file

@ -68,7 +68,7 @@ const instanceV1Controller: AppController = async (c) => {
version, version,
email: meta.email, email: meta.email,
nostr: { nostr: {
pubkey: conf.pubkey, pubkey: await conf.signer.getPublicKey(),
relay: `${wsProtocol}//${host}/relay`, relay: `${wsProtocol}//${host}/relay`,
}, },
rules: [], rules: [],
@ -141,7 +141,7 @@ const instanceV2Controller: AppController = async (c) => {
}, },
}, },
nostr: { nostr: {
pubkey: conf.pubkey, pubkey: await conf.signer.getPublicKey(),
relay: `${wsProtocol}//${host}/relay`, relay: `${wsProtocol}//${host}/relay`,
}, },
pleroma: { pleroma: {

View file

@ -68,7 +68,7 @@ const notificationsController: AppController = async (c) => {
} }
if (types.has('ditto:name_grant') && !account_id) { if (types.has('ditto:name_grant') && !account_id) {
filters.push({ kinds: [30360], authors: [conf.pubkey], '#p': [pubkey], ...params }); filters.push({ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#p': [pubkey], ...params });
} }
return renderNotifications(filters, types, params, c); return renderNotifications(filters, types, params, c);

View file

@ -2,7 +2,6 @@ import { z } from 'zod';
import { type AppController } from '@/app.ts'; import { type AppController } from '@/app.ts';
import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts'; import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { createAdminEvent, updateAdminEvent, updateUser } from '@/utils/api.ts'; import { createAdminEvent, updateAdminEvent, updateUser } from '@/utils/api.ts';
import { lookupPubkey } from '@/utils/lookup.ts'; import { lookupPubkey } from '@/utils/lookup.ts';
@ -34,7 +33,6 @@ const configController: AppController = async (c) => {
/** Pleroma admin config controller. */ /** Pleroma admin config controller. */
const updateConfigController: AppController = async (c) => { const updateConfigController: AppController = async (c) => {
const { conf } = c.var; const { conf } = c.var;
const { pubkey } = conf;
const store = await Storages.db(); const store = await Storages.db();
const configs = await getPleromaConfigs(store, c.req.raw.signal); const configs = await getPleromaConfigs(store, c.req.raw.signal);
@ -44,7 +42,7 @@ const updateConfigController: AppController = async (c) => {
await createAdminEvent({ await createAdminEvent({
kind: 30078, kind: 30078,
content: await new AdminSigner().nip44.encrypt(pubkey, JSON.stringify(configs)), content: await conf.signer.nip44.encrypt(await conf.signer.getPublicKey(), JSON.stringify(configs)),
tags: [ tags: [
['d', 'pub.ditto.pleroma.config'], ['d', 'pub.ditto.pleroma.config'],
['encrypted', 'nip44'], ['encrypted', 'nip44'],
@ -77,7 +75,7 @@ const pleromaAdminTagController: AppController = async (c) => {
if (!pubkey) continue; if (!pubkey) continue;
await updateAdminEvent( await updateAdminEvent(
{ kinds: [30382], authors: [conf.pubkey], '#d': [pubkey], limit: 1 }, { kinds: [30382], authors: [await conf.signer.getPublicKey()], '#d': [pubkey], limit: 1 },
(prev) => { (prev) => {
const tags = prev?.tags ?? [['d', pubkey]]; const tags = prev?.tags ?? [['d', pubkey]];
@ -110,7 +108,7 @@ const pleromaAdminUntagController: AppController = async (c) => {
if (!pubkey) continue; if (!pubkey) continue;
await updateAdminEvent( await updateAdminEvent(
{ kinds: [30382], authors: [conf.pubkey], '#d': [pubkey], limit: 1 }, { kinds: [30382], authors: [await conf.signer.getPublicKey()], '#d': [pubkey], limit: 1 },
(prev) => ({ (prev) => ({
kind: 30382, kind: 30382,
content: prev?.content ?? '', content: prev?.content ?? '',

View file

@ -36,7 +36,7 @@ const reportController: AppController = async (c) => {
const tags = [ const tags = [
['p', account_id, category], ['p', account_id, category],
['P', conf.pubkey], ['P', await conf.signer.getPublicKey()],
]; ];
for (const status of status_ids) { for (const status of status_ids) {
@ -70,7 +70,7 @@ const adminReportsController: AppController = async (c) => {
const filter: NostrFilter = { const filter: NostrFilter = {
kinds: [30383], kinds: [30383],
authors: [conf.pubkey], authors: [await conf.signer.getPublicKey()],
'#k': ['1984'], '#k': ['1984'],
...params, ...params,
}; };

View file

@ -196,7 +196,7 @@ const createStatusController: AppController = async (c) => {
if (conf.zapSplitsEnabled) { if (conf.zapSplitsEnabled) {
const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content); const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content);
const lnurl = getLnurl(meta); const lnurl = getLnurl(meta);
const dittoZapSplit = await getZapSplits(store, conf.pubkey); const dittoZapSplit = await getZapSplits(store, await conf.signer.getPublicKey());
if (lnurl && dittoZapSplit) { if (lnurl && dittoZapSplit) {
const totalSplit = Object.values(dittoZapSplit).reduce((total, { weight }) => total + weight, 0); const totalSplit = Object.values(dittoZapSplit).reduce((total, { weight }) => total + weight, 0);
for (const zapPubkey in dittoZapSplit) { for (const zapPubkey in dittoZapSplit) {

View file

@ -31,8 +31,8 @@ async function renderV2Suggestions(c: AppContext, params: { offset: number; limi
const pubkey = await signer?.getPublicKey(); const pubkey = await signer?.getPublicKey();
const filters: NostrFilter[] = [ const filters: NostrFilter[] = [
{ kinds: [30382], authors: [conf.pubkey], '#n': ['suggested'], limit }, { kinds: [30382], authors: [await conf.signer.getPublicKey()], '#n': ['suggested'], limit },
{ kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [conf.pubkey], limit: 1 }, { kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [await conf.signer.getPublicKey()], limit: 1 },
]; ];
if (pubkey) { if (pubkey) {
@ -41,13 +41,20 @@ async function renderV2Suggestions(c: AppContext, params: { offset: number; limi
} }
const events = await store.query(filters, { signal }); const events = await store.query(filters, { signal });
const adminPubkey = await conf.signer.getPublicKey();
const [userEvents, followsEvent, mutesEvent, trendingEvent] = [ const [userEvents, followsEvent, mutesEvent, trendingEvent] = [
events.filter((event) => matchFilter({ kinds: [30382], authors: [conf.pubkey], '#n': ['suggested'] }, event)), events.filter((event) => matchFilter({ kinds: [30382], authors: [adminPubkey], '#n': ['suggested'] }, event)),
pubkey ? events.find((event) => matchFilter({ kinds: [3], authors: [pubkey] }, event)) : undefined, pubkey ? events.find((event) => matchFilter({ kinds: [3], authors: [pubkey] }, event)) : undefined,
pubkey ? events.find((event) => matchFilter({ kinds: [10000], authors: [pubkey] }, event)) : undefined, pubkey ? events.find((event) => matchFilter({ kinds: [10000], authors: [pubkey] }, event)) : undefined,
events.find((event) => events.find((event) =>
matchFilter({ kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [conf.pubkey], limit: 1 }, event) matchFilter({
kinds: [1985],
'#L': ['pub.ditto.trends'],
'#l': [`#p`],
authors: [adminPubkey],
limit: 1,
}, event)
), ),
]; ];
@ -95,7 +102,7 @@ export const localSuggestionsController: AppController = async (c) => {
const store = c.get('store'); const store = c.get('store');
const grants = await store.query( const grants = await store.query(
[{ kinds: [30360], authors: [conf.pubkey], ...params }], [{ kinds: [30360], authors: [await conf.signer.getPublicKey()], ...params }],
{ signal }, { signal },
); );

View file

@ -95,7 +95,7 @@ const suggestedTimelineController: AppController = async (c) => {
const params = c.get('pagination'); const params = c.get('pagination');
const [follows] = await store.query( const [follows] = await store.query(
[{ kinds: [3], authors: [conf.pubkey], limit: 1 }], [{ kinds: [3], authors: [await conf.signer.getPublicKey()], limit: 1 }],
); );
const authors = [...getTagSet(follows?.tags ?? [], 'p')]; const authors = [...getTagSet(follows?.tags ?? [], 'p')];

View file

@ -53,7 +53,7 @@ const trendingTagsController: AppController = async (c) => {
async function getTrendingHashtags(conf: DittoConf) { async function getTrendingHashtags(conf: DittoConf) {
const store = await Storages.db(); const store = await Storages.db();
const trends = await getTrendingTags(store, 't', conf.pubkey); const trends = await getTrendingTags(store, 't', await conf.signer.getPublicKey());
return trends.map((trend) => { return trends.map((trend) => {
const hashtag = trend.value; const hashtag = trend.value;
@ -106,7 +106,7 @@ const trendingLinksController: AppController = async (c) => {
async function getTrendingLinks(conf: DittoConf) { async function getTrendingLinks(conf: DittoConf) {
const store = await Storages.db(); const store = await Storages.db();
const trends = await getTrendingTags(store, 'r', conf.pubkey); const trends = await getTrendingTags(store, 'r', await conf.signer.getPublicKey());
return Promise.all(trends.map(async (trend) => { return Promise.all(trends.map(async (trend) => {
const link = trend.value; const link = trend.value;
@ -148,7 +148,7 @@ const trendingStatusesController: AppController = async (c) => {
kinds: [1985], kinds: [1985],
'#L': ['pub.ditto.trends'], '#L': ['pub.ditto.trends'],
'#l': ['#e'], '#l': ['#e'],
authors: [conf.pubkey], authors: [await conf.signer.getPublicKey()],
until, until,
limit: 1, limit: 1,
}]); }]);

View file

@ -14,7 +14,7 @@ const relayInfoController: AppController = async (c) => {
return c.json({ return c.json({
name: meta.name, name: meta.name,
description: meta.about, description: meta.about,
pubkey: conf.pubkey, pubkey: await conf.signer.getPublicKey(),
contact: meta.email, contact: meta.email,
supported_nips: [1, 5, 9, 11, 16, 45, 50, 46, 98], supported_nips: [1, 5, 9, 11, 16, 45, 50, 46, 98],
software: 'Ditto', software: 'Ditto',

View file

@ -40,7 +40,7 @@ function requireRole(role: UserRole, opts?: ParseAuthRequestOpts): AppMiddleware
const [user] = await store.query([{ const [user] = await store.query([{
kinds: [30382], kinds: [30382],
authors: [conf.pubkey], authors: [await conf.signer.getPublicKey()],
'#d': [proof.pubkey], '#d': [proof.pubkey],
limit: 1, limit: 1,
}]); }]);

View file

@ -11,7 +11,6 @@ import { Conf } from '@/config.ts';
import { DittoPush } from '@/DittoPush.ts'; import { DittoPush } from '@/DittoPush.ts';
import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts';
import { RelayError } from '@/RelayError.ts'; import { RelayError } from '@/RelayError.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { eventAge, Time } from '@/utils.ts'; import { eventAge, Time } from '@/utils.ts';
@ -83,7 +82,7 @@ async function handleEvent(event: DittoEvent, opts: PipelineOpts): Promise<void>
} }
// Ensure the event doesn't violate the policy. // Ensure the event doesn't violate the policy.
if (event.pubkey !== Conf.pubkey) { if (event.pubkey !== await Conf.signer.getPublicKey()) {
await policyFilter(event, opts.signal); await policyFilter(event, opts.signal);
} }
@ -297,11 +296,12 @@ async function webPush(event: NostrEvent): Promise<void> {
} }
async function generateSetEvents(event: NostrEvent): Promise<void> { async function generateSetEvents(event: NostrEvent): Promise<void> {
const tagsAdmin = event.tags.some(([name, value]) => ['p', 'P'].includes(name) && value === Conf.pubkey); const signer = Conf.signer;
const pubkey = await signer.getPublicKey();
const tagsAdmin = event.tags.some(([name, value]) => ['p', 'P'].includes(name) && value === pubkey);
if (event.kind === 1984 && tagsAdmin) { if (event.kind === 1984 && tagsAdmin) {
const signer = new AdminSigner();
const rel = await signer.signEvent({ const rel = await signer.signEvent({
kind: 30383, kind: 30383,
content: '', content: '',
@ -310,8 +310,8 @@ async function generateSetEvents(event: NostrEvent): Promise<void> {
['p', event.pubkey], ['p', event.pubkey],
['k', '1984'], ['k', '1984'],
['n', 'open'], ['n', 'open'],
...[...getTagSet(event.tags, 'p')].map((pubkey) => ['P', pubkey]), ...[...getTagSet(event.tags, 'p')].map((value) => ['P', value]),
...[...getTagSet(event.tags, 'e')].map((pubkey) => ['e', pubkey]), ...[...getTagSet(event.tags, 'e')].map((value) => ['e', value]),
], ],
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
}); });
@ -320,8 +320,6 @@ async function generateSetEvents(event: NostrEvent): Promise<void> {
} }
if (event.kind === 3036 && tagsAdmin) { if (event.kind === 3036 && tagsAdmin) {
const signer = new AdminSigner();
const rel = await signer.signEvent({ const rel = await signer.signEvent({
kind: 30383, kind: 30383,
content: '', content: '',

View file

@ -1,9 +0,0 @@
import { NSecSigner } from '@nostrify/nostrify';
import { Conf } from '@/config.ts';
/** Sign events as the Ditto server. */
export class AdminSigner extends NSecSigner {
constructor() {
super(Conf.seckey);
}
}

View file

@ -42,7 +42,7 @@ export class Storages {
const db = await this.database(); const db = await this.database();
const store = new DittoPgStore({ const store = new DittoPgStore({
db, db,
pubkey: Conf.pubkey, pubkey: await Conf.signer.getPublicKey(),
timeout: Conf.db.timeouts.default, timeout: Conf.db.timeouts.default,
notify: Conf.notifyEnabled, notify: Conf.notifyEnabled,
}); });
@ -68,7 +68,7 @@ export class Storages {
const db = await this.db(); const db = await this.db();
const [relayList] = await db.query([ const [relayList] = await db.query([
{ kinds: [10002], authors: [Conf.pubkey], limit: 1 }, { kinds: [10002], authors: [await Conf.signer.getPublicKey()], limit: 1 },
]); ]);
const tags = relayList?.tags ?? []; const tags = relayList?.tags ?? [];

View file

@ -18,15 +18,17 @@ export class AdminStore implements NStore {
const users = await this.store.query([{ const users = await this.store.query([{
kinds: [30382], kinds: [30382],
authors: [Conf.pubkey], authors: [await Conf.signer.getPublicKey()],
'#d': [...pubkeys], '#d': [...pubkeys],
limit: pubkeys.size, limit: pubkeys.size,
}]); }]);
const adminPubkey = await Conf.signer.getPublicKey();
return events.filter((event) => { return events.filter((event) => {
const user = users.find( const user = users.find(
({ kind, pubkey, tags }) => ({ kind, pubkey, tags }) =>
kind === 30382 && pubkey === Conf.pubkey && tags.find(([name]) => name === 'd')?.[1] === event.pubkey, kind === 30382 && pubkey === adminPubkey && tags.find(([name]) => name === 'd')?.[1] === event.pubkey,
); );
const n = getTagSet(user?.tags ?? [], 'n'); const n = getTagSet(user?.tags ?? [], 'n');

View file

@ -10,5 +10,5 @@ const testStats = JSON.parse(await Deno.readTextFile('fixtures/stats.json'));
const events = testEvents.slice(0, 20); const events = testEvents.slice(0, 20);
Deno.bench('assembleEvents with home feed', () => { Deno.bench('assembleEvents with home feed', () => {
assembleEvents(events, testEvents, testStats); assembleEvents('', events, testEvents, testStats);
}); });

View file

@ -79,15 +79,18 @@ async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
// Dedupe events. // Dedupe events.
const results = [...new Map(cache.map((event) => [event.id, event])).values()]; const results = [...new Map(cache.map((event) => [event.id, event])).values()];
const admin = await Conf.signer.getPublicKey();
// First connect all the events to each-other, then connect the connected events to the original list. // First connect all the events to each-other, then connect the connected events to the original list.
assembleEvents(results, results, stats); assembleEvents(admin, results, results, stats);
assembleEvents(events, results, stats); assembleEvents(admin, events, results, stats);
return events; return events;
} }
/** Connect the events in list `b` to the DittoEvent fields in list `a`. */ /** Connect the events in list `b` to the DittoEvent fields in list `a`. */
export function assembleEvents( export function assembleEvents(
admin: string,
a: DittoEvent[], a: DittoEvent[],
b: DittoEvent[], b: DittoEvent[],
stats: { stats: {
@ -96,8 +99,6 @@ export function assembleEvents(
favicons: Record<string, string>; favicons: Record<string, string>;
}, },
): DittoEvent[] { ): DittoEvent[] {
const admin = Conf.pubkey;
const authorStats = stats.authors.reduce((result, { pubkey, ...stat }) => { const authorStats = stats.authors.reduce((result, { pubkey, ...stat }) => {
result[pubkey] = { result[pubkey] = {
...stat, ...stat,
@ -316,7 +317,7 @@ async function gatherProfiles({ events, store, signal }: HydrateOpts): Promise<D
} }
/** Collect users from the events. */ /** Collect users from the events. */
function gatherUsers({ events, store, signal }: HydrateOpts): Promise<DittoEvent[]> { async function gatherUsers({ events, store, signal }: HydrateOpts): Promise<DittoEvent[]> {
const pubkeys = new Set(events.map((event) => event.pubkey)); const pubkeys = new Set(events.map((event) => event.pubkey));
if (!pubkeys.size) { if (!pubkeys.size) {
@ -324,13 +325,13 @@ function gatherUsers({ events, store, signal }: HydrateOpts): Promise<DittoEvent
} }
return store.query( return store.query(
[{ kinds: [30382], authors: [Conf.pubkey], '#d': [...pubkeys], limit: pubkeys.size }], [{ kinds: [30382], authors: [await Conf.signer.getPublicKey()], '#d': [...pubkeys], limit: pubkeys.size }],
{ signal }, { signal },
); );
} }
/** Collect info events from the events. */ /** Collect info events from the events. */
function gatherInfo({ events, store, signal }: HydrateOpts): Promise<DittoEvent[]> { async function gatherInfo({ events, store, signal }: HydrateOpts): Promise<DittoEvent[]> {
const ids = new Set<string>(); const ids = new Set<string>();
for (const event of events) { for (const event of events) {
@ -344,7 +345,7 @@ function gatherInfo({ events, store, signal }: HydrateOpts): Promise<DittoEvent[
} }
return store.query( return store.query(
[{ kinds: [30383], authors: [Conf.pubkey], '#d': [...ids], limit: ids.size }], [{ kinds: [30383], authors: [await Conf.signer.getPublicKey()], '#d': [...ids], limit: ids.size }],
{ signal }, { signal },
); );
} }

View file

@ -20,7 +20,7 @@ export async function createTestDB(opts?: { pure?: boolean }) {
const store = new DittoPgStore({ const store = new DittoPgStore({
db, db,
timeout: Conf.db.timeouts.default, timeout: Conf.db.timeouts.default,
pubkey: Conf.pubkey, pubkey: await Conf.signer.getPublicKey(),
pure: opts?.pure ?? false, pure: opts?.pure ?? false,
notify: true, notify: true,
}); });

View file

@ -5,7 +5,6 @@ import { Kysely, sql } from 'kysely';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { handleEvent } from '@/pipeline.ts'; import { handleEvent } from '@/pipeline.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { errorJson } from '@/utils/log.ts'; import { errorJson } from '@/utils/log.ts';
import { Time } from '@/utils/time.ts'; import { Time } from '@/utils/time.ts';
@ -100,7 +99,7 @@ export async function updateTrendingTags(
return; return;
} }
const signer = new AdminSigner(); const signer = Conf.signer;
const label = await signer.signEvent({ const label = await signer.signEvent({
kind: 1985, kind: 1985,

View file

@ -9,7 +9,6 @@ import { type AppContext } from '@/app.ts';
import { Conf } from '@/config.ts'; 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 { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { nostrNow } from '@/utils.ts'; import { nostrNow } from '@/utils.ts';
import { parseFormData } from '@/utils/formdata.ts'; import { parseFormData } from '@/utils/formdata.ts';
@ -81,7 +80,7 @@ function updateListEvent(
/** Publish an admin event through the pipeline. */ /** Publish an admin event through the pipeline. */
async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent> { async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
const signer = new AdminSigner(); const signer = Conf.signer;
const event = await signer.signEvent({ const event = await signer.signEvent({
content: '', content: '',
@ -126,7 +125,7 @@ function updateEventInfo(id: string, n: Record<string, boolean>, c: AppContext):
} }
async function updateNames(k: number, d: string, n: Record<string, boolean>, c: AppContext): Promise<NostrEvent> { async function updateNames(k: number, d: string, n: Record<string, boolean>, c: AppContext): Promise<NostrEvent> {
const signer = new AdminSigner(); const signer = Conf.signer;
const admin = await signer.getPublicKey(); const admin = await signer.getPublicKey();
return updateAdminEvent( return updateAdminEvent(

View file

@ -20,7 +20,7 @@ export async function getClientConnectUri(signal?: AbortSignal): Promise<string>
url: Conf.localDomain, url: Conf.localDomain,
}; };
uri.host = Conf.pubkey; uri.host = await Conf.signer.getPublicKey();
uri.searchParams.set('relay', Conf.relay); uri.searchParams.set('relay', Conf.relay);
uri.searchParams.set('metadata', JSON.stringify(metadata)); uri.searchParams.set('metadata', JSON.stringify(metadata));

View file

@ -18,7 +18,7 @@ export interface InstanceMetadata extends NostrMetadata {
/** Get and parse instance metadata from the kind 0 of the admin user. */ /** Get and parse instance metadata from the kind 0 of the admin user. */
export async function getInstanceMetadata(store: NStore, signal?: AbortSignal): Promise<InstanceMetadata> { export async function getInstanceMetadata(store: NStore, signal?: AbortSignal): Promise<InstanceMetadata> {
const [event] = await store.query( const [event] = await store.query(
[{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], [{ kinds: [0], authors: [await Conf.signer.getPublicKey()], limit: 1 }],
{ signal }, { signal },
); );

View file

@ -57,7 +57,7 @@ export async function localNip05Lookup(store: NStore, localpart: string): Promis
const [grant] = await store.query([{ const [grant] = await store.query([{
kinds: [30360], kinds: [30360],
'#d': [`${localpart}@${Conf.url.host}`], '#d': [`${localpart}@${Conf.url.host}`],
authors: [Conf.pubkey], authors: [await Conf.signer.getPublicKey()],
limit: 1, limit: 1,
}]); }]);

View file

@ -6,7 +6,7 @@ export async function getRelays(store: NStore, pubkey: string): Promise<Set<stri
const relays = new Set<`wss://${string}`>(); const relays = new Set<`wss://${string}`>();
const events = await store.query([ const events = await store.query([
{ kinds: [10002], authors: [pubkey, Conf.pubkey], limit: 2 }, { kinds: [10002], authors: [pubkey, await Conf.signer.getPublicKey()], limit: 2 },
]); ]);
for (const event of events) { for (const event of events) {

View file

@ -2,11 +2,11 @@ import { NSchema as n, NStore } from '@nostrify/nostrify';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { configSchema } from '@/schemas/pleroma-api.ts'; import { configSchema } from '@/schemas/pleroma-api.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts';
import { PleromaConfigDB } from '@/utils/PleromaConfigDB.ts'; import { PleromaConfigDB } from '@/utils/PleromaConfigDB.ts';
export async function getPleromaConfigs(store: NStore, signal?: AbortSignal): Promise<PleromaConfigDB> { export async function getPleromaConfigs(store: NStore, signal?: AbortSignal): Promise<PleromaConfigDB> {
const { pubkey } = Conf; const signer = Conf.signer;
const pubkey = await signer.getPublicKey();
const [event] = await store.query([{ const [event] = await store.query([{
kinds: [30078], kinds: [30078],
@ -20,7 +20,7 @@ export async function getPleromaConfigs(store: NStore, signal?: AbortSignal): Pr
} }
try { try {
const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content); const decrypted = await signer.nip44.decrypt(pubkey, event.content);
const configs = n.json().pipe(configSchema.array()).catch([]).parse(decrypted); const configs = n.json().pipe(configSchema.array()).catch([]).parse(decrypted);
return new PleromaConfigDB(configs); return new PleromaConfigDB(configs);
} catch (_e) { } catch (_e) {

View file

@ -1,4 +1,3 @@
import { AdminSigner } from '@/signers/AdminSigner.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { NSchema as n, NStore } from '@nostrify/nostrify'; import { NSchema as n, NStore } from '@nostrify/nostrify';
import { nostrNow } from '@/utils.ts'; import { nostrNow } from '@/utils.ts';
@ -38,13 +37,13 @@ export async function getZapSplits(store: NStore, pubkey: string): Promise<Ditto
} }
export async function seedZapSplits(store: NStore) { export async function seedZapSplits(store: NStore) {
const zapSplit: DittoZapSplits | undefined = await getZapSplits(store, Conf.pubkey); const zapSplit: DittoZapSplits | undefined = await getZapSplits(store, await Conf.signer.getPublicKey());
if (!zapSplit) { if (!zapSplit) {
const dittoPubkey = '781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5'; const dittoPubkey = '781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5';
const dittoMsg = 'Official Ditto Account'; const dittoMsg = 'Official Ditto Account';
const signer = new AdminSigner(); const signer = Conf.signer;
const event = await signer.signEvent({ const event = await signer.signEvent({
content: '', content: '',
created_at: nostrNow(), created_at: nostrNow(),

View file

@ -10,7 +10,7 @@ interface RenderNotificationOpts {
viewerPubkey: string; viewerPubkey: string;
} }
function renderNotification(event: DittoEvent, opts: RenderNotificationOpts) { async function renderNotification(event: DittoEvent, opts: RenderNotificationOpts) {
const mentioned = !!event.tags.find(([name, value]) => name === 'p' && value === opts.viewerPubkey); const mentioned = !!event.tags.find(([name, value]) => name === 'p' && value === opts.viewerPubkey);
if (event.kind === 1 && mentioned) { if (event.kind === 1 && mentioned) {
@ -29,7 +29,7 @@ function renderNotification(event: DittoEvent, opts: RenderNotificationOpts) {
return renderReaction(event, opts); return renderReaction(event, opts);
} }
if (event.kind === 30360 && event.pubkey === Conf.pubkey) { if (event.kind === 30360 && event.pubkey === await Conf.signer.getPublicKey()) {
return renderNameGrant(event); return renderNameGrant(event);
} }

View file

@ -48,7 +48,7 @@ class PolicyWorker implements NPolicy {
await this.worker.init({ await this.worker.init({
path: Conf.policy, path: Conf.policy,
databaseUrl: Conf.databaseUrl, databaseUrl: Conf.databaseUrl,
pubkey: Conf.pubkey, pubkey: await Conf.signer.getPublicKey(),
}); });
logi({ logi({

View file

@ -1,12 +1,12 @@
import { JsonParseStream } from '@std/json/json-parse-stream'; import { JsonParseStream } from '@std/json/json-parse-stream';
import { TextLineStream } from '@std/streams/text-line-stream'; import { TextLineStream } from '@std/streams/text-line-stream';
import { AdminSigner } from '../packages/ditto/signers/AdminSigner.ts'; import { Conf } from '../packages/ditto/config.ts';
import { Storages } from '../packages/ditto/storages.ts'; import { Storages } from '../packages/ditto/storages.ts';
import { type EventStub } from '../packages/ditto/utils/api.ts'; import { type EventStub } from '../packages/ditto/utils/api.ts';
import { nostrNow } from '../packages/ditto/utils.ts'; import { nostrNow } from '../packages/ditto/utils.ts';
const signer = new AdminSigner(); const signer = Conf.signer;
const store = await Storages.db(); const store = await Storages.db();
const readable = Deno.stdin.readable const readable = Deno.stdin.readable

View file

@ -1,7 +1,7 @@
import { NSchema } from '@nostrify/nostrify'; import { NSchema } from '@nostrify/nostrify';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { AdminSigner } from '../packages/ditto/signers/AdminSigner.ts'; import { Conf } from '../packages/ditto/config.ts';
import { Storages } from '../packages/ditto/storages.ts'; import { Storages } from '../packages/ditto/storages.ts';
import { nostrNow } from '../packages/ditto/utils.ts'; import { nostrNow } from '../packages/ditto/utils.ts';
@ -20,7 +20,7 @@ if (!['admin', 'user'].includes(role)) {
Deno.exit(1); Deno.exit(1);
} }
const signer = new AdminSigner(); const signer = Conf.signer;
const admin = await signer.getPublicKey(); const admin = await signer.getPublicKey();
const [existing] = await store.query([{ const [existing] = await store.query([{

View file

@ -1,7 +1,6 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { NostrEvent } from 'nostr-tools'; import { NostrEvent } from 'nostr-tools';
import { AdminSigner } from '../packages/ditto/signers/AdminSigner.ts';
import { nostrNow } from '../packages/ditto/utils.ts'; import { nostrNow } from '../packages/ditto/utils.ts';
import { Conf } from '../packages/ditto/config.ts'; import { Conf } from '../packages/ditto/config.ts';
import { Storages } from '../packages/ditto/storages.ts'; import { Storages } from '../packages/ditto/storages.ts';
@ -36,7 +35,7 @@ if (import.meta.main) {
content.picture = image; content.picture = image;
content.website = Conf.localDomain; content.website = Conf.localDomain;
const signer = new AdminSigner(); const signer = Conf.signer;
const bare: Omit<NostrEvent, 'id' | 'sig' | 'pubkey'> = { const bare: Omit<NostrEvent, 'id' | 'sig' | 'pubkey'> = {
created_at: nostrNow(), created_at: nostrNow(),
kind: 0, kind: 0,