mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Merge branch 'name-request-refactor' into 'main'
Name request refactor See merge request soapbox-pub/ditto!708
This commit is contained in:
commit
10ea6f7635
19 changed files with 477 additions and 189 deletions
|
|
@ -74,6 +74,7 @@
|
||||||
"@soapbox/logi": "jsr:@soapbox/logi@^0.3.0",
|
"@soapbox/logi": "jsr:@soapbox/logi@^0.3.0",
|
||||||
"@soapbox/safe-fetch": "jsr:@soapbox/safe-fetch@^2.0.0",
|
"@soapbox/safe-fetch": "jsr:@soapbox/safe-fetch@^2.0.0",
|
||||||
"@std/assert": "jsr:@std/assert@^0.225.1",
|
"@std/assert": "jsr:@std/assert@^0.225.1",
|
||||||
|
"@std/async": "jsr:@std/async@^1.0.10",
|
||||||
"@std/cli": "jsr:@std/cli@^0.223.0",
|
"@std/cli": "jsr:@std/cli@^0.223.0",
|
||||||
"@std/crypto": "jsr:@std/crypto@^0.224.0",
|
"@std/crypto": "jsr:@std/crypto@^0.224.0",
|
||||||
"@std/encoding": "jsr:@std/encoding@^0.224.0",
|
"@std/encoding": "jsr:@std/encoding@^0.224.0",
|
||||||
|
|
|
||||||
5
deno.lock
generated
5
deno.lock
generated
|
|
@ -58,6 +58,7 @@
|
||||||
"jsr:@std/assert@^1.0.10": "1.0.11",
|
"jsr:@std/assert@^1.0.10": "1.0.11",
|
||||||
"jsr:@std/assert@~0.213.1": "0.213.1",
|
"jsr:@std/assert@~0.213.1": "0.213.1",
|
||||||
"jsr:@std/assert@~0.225.1": "0.225.3",
|
"jsr:@std/assert@~0.225.1": "0.225.3",
|
||||||
|
"jsr:@std/async@^1.0.10": "1.0.10",
|
||||||
"jsr:@std/bytes@0.223": "0.223.0",
|
"jsr:@std/bytes@0.223": "0.223.0",
|
||||||
"jsr:@std/bytes@0.224": "0.224.0",
|
"jsr:@std/bytes@0.224": "0.224.0",
|
||||||
"jsr:@std/bytes@0.224.0": "0.224.0",
|
"jsr:@std/bytes@0.224.0": "0.224.0",
|
||||||
|
|
@ -604,6 +605,9 @@
|
||||||
"jsr:@std/internal@^1.0.5"
|
"jsr:@std/internal@^1.0.5"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@std/async@1.0.10": {
|
||||||
|
"integrity": "2ff1b1c7d33d1416159989b0f69e59ec7ee8cb58510df01e454def2108b3dbec"
|
||||||
|
},
|
||||||
"@std/bytes@0.223.0": {
|
"@std/bytes@0.223.0": {
|
||||||
"integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8"
|
"integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8"
|
||||||
},
|
},
|
||||||
|
|
@ -2489,6 +2493,7 @@
|
||||||
"jsr:@soapbox/logi@0.3",
|
"jsr:@soapbox/logi@0.3",
|
||||||
"jsr:@soapbox/safe-fetch@2",
|
"jsr:@soapbox/safe-fetch@2",
|
||||||
"jsr:@std/assert@~0.225.1",
|
"jsr:@std/assert@~0.225.1",
|
||||||
|
"jsr:@std/async@^1.0.10",
|
||||||
"jsr:@std/cli@0.223",
|
"jsr:@std/cli@0.223",
|
||||||
"jsr:@std/crypto@0.224",
|
"jsr:@std/crypto@0.224",
|
||||||
"jsr:@std/encoding@0.224",
|
"jsr:@std/encoding@0.224",
|
||||||
|
|
|
||||||
25
packages/db/adapters/TestDB.test.ts
Normal file
25
packages/db/adapters/TestDB.test.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { DittoConf } from '@ditto/conf';
|
||||||
|
import { NPostgres } from '@nostrify/db';
|
||||||
|
import { genEvent } from '@nostrify/nostrify/test';
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
|
import { DittoPolyPg } from './DittoPolyPg.ts';
|
||||||
|
import { TestDB } from './TestDB.ts';
|
||||||
|
|
||||||
|
Deno.test('TestDB', async () => {
|
||||||
|
const conf = new DittoConf(Deno.env);
|
||||||
|
const orig = new DittoPolyPg(conf.databaseUrl);
|
||||||
|
|
||||||
|
await using db = new TestDB(orig);
|
||||||
|
await db.migrate();
|
||||||
|
await db.clear();
|
||||||
|
|
||||||
|
const store = new NPostgres(orig.kysely);
|
||||||
|
await store.event(genEvent());
|
||||||
|
|
||||||
|
assertEquals((await store.count([{}])).count, 1);
|
||||||
|
|
||||||
|
await db.clear();
|
||||||
|
|
||||||
|
assertEquals((await store.count([{}])).count, 0);
|
||||||
|
});
|
||||||
49
packages/db/adapters/TestDB.ts
Normal file
49
packages/db/adapters/TestDB.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { type Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
import type { DittoDB } from '../DittoDB.ts';
|
||||||
|
import type { DittoTables } from '../DittoTables.ts';
|
||||||
|
|
||||||
|
/** Wraps another DittoDB implementation to clear all data when disposed. */
|
||||||
|
export class TestDB implements DittoDB {
|
||||||
|
constructor(private db: DittoDB) {}
|
||||||
|
|
||||||
|
get kysely(): Kysely<DittoTables> {
|
||||||
|
return this.db.kysely;
|
||||||
|
}
|
||||||
|
|
||||||
|
get poolSize(): number {
|
||||||
|
return this.db.poolSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
get availableConnections(): number {
|
||||||
|
return this.db.availableConnections;
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate(): Promise<void> {
|
||||||
|
return this.db.migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(channel: string, callback: (payload: string) => void): void {
|
||||||
|
return this.db.listen(channel, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Truncate all tables. */
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
const query = sql<{ tablename: string }>`select tablename from pg_tables where schemaname = current_schema()`;
|
||||||
|
|
||||||
|
const { rows } = await query.execute(this.db.kysely);
|
||||||
|
|
||||||
|
for (const { tablename } of rows) {
|
||||||
|
if (tablename.startsWith('kysely_')) {
|
||||||
|
continue; // Skip Kysely's internal tables
|
||||||
|
} else {
|
||||||
|
await sql`truncate table ${sql.ref(tablename)} cascade`.execute(this.db.kysely);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async [Symbol.asyncDispose](): Promise<void> {
|
||||||
|
await this.clear();
|
||||||
|
await this.db[Symbol.asyncDispose]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ export { DittoPglite } from './adapters/DittoPglite.ts';
|
||||||
export { DittoPolyPg } from './adapters/DittoPolyPg.ts';
|
export { DittoPolyPg } from './adapters/DittoPolyPg.ts';
|
||||||
export { DittoPostgres } from './adapters/DittoPostgres.ts';
|
export { DittoPostgres } from './adapters/DittoPostgres.ts';
|
||||||
export { DummyDB } from './adapters/DummyDB.ts';
|
export { DummyDB } from './adapters/DummyDB.ts';
|
||||||
|
export { TestDB } from './adapters/TestDB.ts';
|
||||||
|
|
||||||
export type { DittoDB } from './DittoDB.ts';
|
export type { DittoDB } from './DittoDB.ts';
|
||||||
export type { DittoTables } from './DittoTables.ts';
|
export type { DittoTables } from './DittoTables.ts';
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,6 @@ import {
|
||||||
adminSetRelaysController,
|
adminSetRelaysController,
|
||||||
deleteZapSplitsController,
|
deleteZapSplitsController,
|
||||||
getZapSplitsController,
|
getZapSplitsController,
|
||||||
nameRequestController,
|
|
||||||
nameRequestsController,
|
|
||||||
statusZapSplitsController,
|
statusZapSplitsController,
|
||||||
updateInstanceController,
|
updateInstanceController,
|
||||||
updateZapSplitsController,
|
updateZapSplitsController,
|
||||||
|
|
@ -149,6 +147,7 @@ import { rateLimitMiddleware } from '@/middleware/rateLimitMiddleware.ts';
|
||||||
import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts';
|
import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts';
|
||||||
import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts';
|
import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts';
|
||||||
import { logiMiddleware } from '@/middleware/logiMiddleware.ts';
|
import { logiMiddleware } from '@/middleware/logiMiddleware.ts';
|
||||||
|
import dittoNamesRoute from '@/routes/dittoNamesRoute.ts';
|
||||||
import { DittoRelayStore } from '@/storages/DittoRelayStore.ts';
|
import { DittoRelayStore } from '@/storages/DittoRelayStore.ts';
|
||||||
|
|
||||||
export interface AppEnv extends DittoEnv {
|
export interface AppEnv extends DittoEnv {
|
||||||
|
|
@ -446,8 +445,7 @@ app.put('/api/v1/admin/ditto/relays', userMiddleware({ role: 'admin' }), adminSe
|
||||||
|
|
||||||
app.put('/api/v1/admin/ditto/instance', userMiddleware({ role: 'admin' }), updateInstanceController);
|
app.put('/api/v1/admin/ditto/instance', userMiddleware({ role: 'admin' }), updateInstanceController);
|
||||||
|
|
||||||
app.post('/api/v1/ditto/names', userMiddleware(), nameRequestController);
|
app.route('/api/v1/ditto/names', dittoNamesRoute);
|
||||||
app.get('/api/v1/ditto/names', userMiddleware(), nameRequestsController);
|
|
||||||
|
|
||||||
app.get('/api/v1/ditto/captcha', rateLimitMiddleware(3, Time.minutes(1)), captchaController);
|
app.get('/api/v1/ditto/captcha', rateLimitMiddleware(3, Time.minutes(1)), captchaController);
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
import { paginated } from '@ditto/mastoapi/pagination';
|
import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
|
||||||
import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AppController } from '@/app.ts';
|
import { AppController } from '@/app.ts';
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { getAuthor } from '@/queries.ts';
|
import { getAuthor } from '@/queries.ts';
|
||||||
import { addTag } from '@/utils/tags.ts';
|
import { addTag } from '@/utils/tags.ts';
|
||||||
import { createEvent, parseBody, updateAdminEvent } from '@/utils/api.ts';
|
import { parseBody, updateAdminEvent } from '@/utils/api.ts';
|
||||||
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 { screenshotsSchema } from '@/schemas/nostr.ts';
|
import { screenshotsSchema } from '@/schemas/nostr.ts';
|
||||||
import { booleanParamSchema, percentageSchema } from '@/schema.ts';
|
import { percentageSchema } from '@/schema.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { renderNameRequest } from '@/views/ditto.ts';
|
|
||||||
import { accountFromPubkey } from '@/views/mastodon/accounts.ts';
|
import { accountFromPubkey } from '@/views/mastodon/accounts.ts';
|
||||||
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { updateListAdminEvent } from '@/utils/api.ts';
|
import { updateListAdminEvent } from '@/utils/api.ts';
|
||||||
|
|
@ -81,102 +79,6 @@ function renderRelays(event: NostrEvent): RelayEntity[] {
|
||||||
}, [] as RelayEntity[]);
|
}, [] as RelayEntity[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameRequestSchema = z.object({
|
|
||||||
name: z.string().email(),
|
|
||||||
reason: z.string().max(500).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nameRequestController: AppController = async (c) => {
|
|
||||||
const { conf, relay, user } = c.var;
|
|
||||||
|
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
|
||||||
const result = nameRequestSchema.safeParse(await c.req.json());
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return c.json({ error: 'Invalid username', schema: result.error }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, reason } = result.data;
|
|
||||||
|
|
||||||
const [existing] = await relay.query([{ kinds: [3036], authors: [pubkey], '#r': [name.toLowerCase()], limit: 1 }]);
|
|
||||||
if (existing) {
|
|
||||||
return c.json({ error: 'Name request already exists' }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const r: string[][] = [['r', name]];
|
|
||||||
|
|
||||||
if (name !== name.toLowerCase()) {
|
|
||||||
r.push(['r', name.toLowerCase()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = await createEvent({
|
|
||||||
kind: 3036,
|
|
||||||
content: reason,
|
|
||||||
tags: [
|
|
||||||
...r,
|
|
||||||
['L', 'nip05.domain'],
|
|
||||||
['l', name.split('@')[1], 'nip05.domain'],
|
|
||||||
['p', await conf.signer.getPublicKey()],
|
|
||||||
],
|
|
||||||
}, c);
|
|
||||||
|
|
||||||
await hydrateEvents({ ...c.var, events: [event] });
|
|
||||||
|
|
||||||
const nameRequest = await renderNameRequest(event);
|
|
||||||
return c.json(nameRequest);
|
|
||||||
};
|
|
||||||
|
|
||||||
const nameRequestsSchema = z.object({
|
|
||||||
approved: booleanParamSchema.optional(),
|
|
||||||
rejected: booleanParamSchema.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nameRequestsController: AppController = async (c) => {
|
|
||||||
const { conf, relay, user } = c.var;
|
|
||||||
const pubkey = await user!.signer.getPublicKey();
|
|
||||||
|
|
||||||
const params = c.get('pagination');
|
|
||||||
const { approved, rejected } = nameRequestsSchema.parse(c.req.query());
|
|
||||||
|
|
||||||
const filter: NostrFilter = {
|
|
||||||
kinds: [30383],
|
|
||||||
authors: [await conf.signer.getPublicKey()],
|
|
||||||
'#k': ['3036'],
|
|
||||||
'#p': [pubkey],
|
|
||||||
...params,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (approved) {
|
|
||||||
filter['#n'] = ['approved'];
|
|
||||||
}
|
|
||||||
if (rejected) {
|
|
||||||
filter['#n'] = ['rejected'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const orig = await relay.query([filter]);
|
|
||||||
const ids = new Set<string>();
|
|
||||||
|
|
||||||
for (const event of orig) {
|
|
||||||
const d = event.tags.find(([name]) => name === 'd')?.[1];
|
|
||||||
if (d) {
|
|
||||||
ids.add(d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ids.size) {
|
|
||||||
return c.json([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = await relay.query([{ kinds: [3036], ids: [...ids], authors: [pubkey] }])
|
|
||||||
.then((events) => hydrateEvents({ ...c.var, events }));
|
|
||||||
|
|
||||||
const nameRequests = await Promise.all(
|
|
||||||
events.map((event) => renderNameRequest(event)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return paginated(c, orig, nameRequests);
|
|
||||||
};
|
|
||||||
|
|
||||||
const zapSplitSchema = z.record(
|
const zapSplitSchema = z.record(
|
||||||
n.id(),
|
n.id(),
|
||||||
z.object({
|
z.object({
|
||||||
|
|
|
||||||
62
packages/ditto/routes/dittoNamesRoute.test.ts
Normal file
62
packages/ditto/routes/dittoNamesRoute.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { TestApp } from '@ditto/mastoapi/test';
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
|
import route from './dittoNamesRoute.ts';
|
||||||
|
|
||||||
|
Deno.test('POST / creates a name request event', async () => {
|
||||||
|
await using app = new TestApp();
|
||||||
|
const { conf, relay } = app.var;
|
||||||
|
|
||||||
|
const user = app.user();
|
||||||
|
app.route('/', route);
|
||||||
|
|
||||||
|
const response = await app.api.post('/', { name: 'Alex@Ditto.pub', reason: 'for testing' });
|
||||||
|
|
||||||
|
assertEquals(response.status, 200);
|
||||||
|
|
||||||
|
const [event] = await relay.query([{ kinds: [3036], authors: [await user.signer.getPublicKey()] }]);
|
||||||
|
|
||||||
|
assertEquals(event?.tags, [
|
||||||
|
['r', 'Alex@Ditto.pub'],
|
||||||
|
['r', 'alex@ditto.pub'],
|
||||||
|
['L', 'nip05.domain'],
|
||||||
|
['l', 'ditto.pub', 'nip05.domain'],
|
||||||
|
['p', await conf.signer.getPublicKey()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(event?.content, 'for testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('POST / can be called multiple times with the same name', async () => {
|
||||||
|
await using app = new TestApp();
|
||||||
|
|
||||||
|
app.user();
|
||||||
|
app.route('/', route);
|
||||||
|
|
||||||
|
const response1 = await app.api.post('/', { name: 'alex@ditto.pub' });
|
||||||
|
const response2 = await app.api.post('/', { name: 'alex@ditto.pub' });
|
||||||
|
|
||||||
|
assertEquals(response1.status, 200);
|
||||||
|
assertEquals(response2.status, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('POST / returns 400 if the name has already been granted', async () => {
|
||||||
|
await using app = new TestApp();
|
||||||
|
const { conf, relay } = app.var;
|
||||||
|
|
||||||
|
app.user();
|
||||||
|
app.route('/', route);
|
||||||
|
|
||||||
|
const grant = await conf.signer.signEvent({
|
||||||
|
kind: 30360,
|
||||||
|
tags: [['d', 'alex@ditto.pub']],
|
||||||
|
content: '',
|
||||||
|
created_at: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await relay.event(grant);
|
||||||
|
|
||||||
|
const response = await app.api.post('/', { name: 'alex@ditto.pub' });
|
||||||
|
|
||||||
|
assertEquals(response.status, 400);
|
||||||
|
});
|
||||||
130
packages/ditto/routes/dittoNamesRoute.ts
Normal file
130
packages/ditto/routes/dittoNamesRoute.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { paginationMiddleware, userMiddleware } from '@ditto/mastoapi/middleware';
|
||||||
|
import { DittoRoute } from '@ditto/mastoapi/router';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { createEvent } from '@/utils/api.ts';
|
||||||
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
|
import { renderNameRequest } from '@/views/ditto.ts';
|
||||||
|
import { booleanParamSchema } from '@/schema.ts';
|
||||||
|
import { NostrFilter } from '@nostrify/nostrify';
|
||||||
|
|
||||||
|
const nameRequestSchema = z.object({
|
||||||
|
name: z.string().email(),
|
||||||
|
reason: z.string().max(500).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = new DittoRoute();
|
||||||
|
|
||||||
|
route.post('/', userMiddleware(), async (c) => {
|
||||||
|
const { conf, relay, user } = c.var;
|
||||||
|
|
||||||
|
const result = nameRequestSchema.safeParse(await c.req.json());
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return c.json({ error: 'Invalid username', schema: result.error }, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pubkey = await user.signer.getPublicKey();
|
||||||
|
const adminPubkey = await conf.signer.getPublicKey();
|
||||||
|
|
||||||
|
const { name, reason } = result.data;
|
||||||
|
const [_localpart, domain] = name.split('@');
|
||||||
|
|
||||||
|
if (domain.toLowerCase() !== conf.url.host.toLowerCase()) {
|
||||||
|
return c.json({ error: 'Unsupported domain' }, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = name.toLowerCase();
|
||||||
|
|
||||||
|
const [grant] = await relay.query([{ kinds: [30360], authors: [adminPubkey], '#d': [d] }]);
|
||||||
|
if (grant) {
|
||||||
|
return c.json({ error: 'Name has already been granted' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [pending] = await relay.query([{
|
||||||
|
kinds: [30383],
|
||||||
|
authors: [adminPubkey],
|
||||||
|
'#p': [pubkey],
|
||||||
|
'#k': ['3036'],
|
||||||
|
'#r': [d],
|
||||||
|
'#n': ['pending'],
|
||||||
|
limit: 1,
|
||||||
|
}]);
|
||||||
|
if (pending) {
|
||||||
|
return c.json({ error: 'You have already requested that name, and it is pending approval by staff' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags: string[][] = [['r', name]];
|
||||||
|
|
||||||
|
if (name !== name.toLowerCase()) {
|
||||||
|
tags.push(['r', name.toLowerCase()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = await createEvent({
|
||||||
|
kind: 3036,
|
||||||
|
content: reason,
|
||||||
|
tags: [
|
||||||
|
...tags,
|
||||||
|
['L', 'nip05.domain'],
|
||||||
|
['l', domain.toLowerCase(), 'nip05.domain'],
|
||||||
|
['p', await conf.signer.getPublicKey()],
|
||||||
|
],
|
||||||
|
}, c);
|
||||||
|
|
||||||
|
await hydrateEvents({ ...c.var, events: [event] });
|
||||||
|
|
||||||
|
const nameRequest = await renderNameRequest(event);
|
||||||
|
return c.json(nameRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nameRequestsSchema = z.object({
|
||||||
|
approved: booleanParamSchema.optional(),
|
||||||
|
rejected: booleanParamSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
route.get('/', paginationMiddleware(), userMiddleware(), async (c) => {
|
||||||
|
const { conf, relay, user, pagination } = c.var;
|
||||||
|
const pubkey = await user!.signer.getPublicKey();
|
||||||
|
|
||||||
|
const { approved, rejected } = nameRequestsSchema.parse(c.req.query());
|
||||||
|
|
||||||
|
const filter: NostrFilter = {
|
||||||
|
kinds: [30383],
|
||||||
|
authors: [await conf.signer.getPublicKey()],
|
||||||
|
'#k': ['3036'],
|
||||||
|
'#p': [pubkey],
|
||||||
|
...pagination,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (approved) {
|
||||||
|
filter['#n'] = ['approved'];
|
||||||
|
}
|
||||||
|
if (rejected) {
|
||||||
|
filter['#n'] = ['rejected'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const orig = await relay.query([filter]);
|
||||||
|
const ids = new Set<string>();
|
||||||
|
|
||||||
|
for (const event of orig) {
|
||||||
|
const d = event.tags.find(([name]) => name === 'd')?.[1];
|
||||||
|
if (d) {
|
||||||
|
ids.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ids.size) {
|
||||||
|
return c.json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await relay.query([{ kinds: [3036], ids: [...ids], authors: [pubkey] }])
|
||||||
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
|
const nameRequests = await Promise.all(
|
||||||
|
events.map((event) => renderNameRequest(event)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.var.paginate(orig, nameRequests);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default route;
|
||||||
|
|
@ -2,12 +2,39 @@ import { DittoPolyPg } from '@ditto/db';
|
||||||
import { DittoConf } from '@ditto/conf';
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { genEvent, MockRelay } from '@nostrify/nostrify/test';
|
import { genEvent, MockRelay } from '@nostrify/nostrify/test';
|
||||||
import { assertEquals } from '@std/assert';
|
import { assertEquals } from '@std/assert';
|
||||||
|
import { waitFor } from '@std/async/unstable-wait-for';
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||||
|
|
||||||
import { DittoRelayStore } from './DittoRelayStore.ts';
|
import { DittoRelayStore } from './DittoRelayStore.ts';
|
||||||
|
|
||||||
import type { NostrMetadata } from '@nostrify/types';
|
import type { NostrMetadata } from '@nostrify/types';
|
||||||
|
|
||||||
|
Deno.test('generates set event for nip05 request', async () => {
|
||||||
|
await using test = setupTest();
|
||||||
|
|
||||||
|
const admin = await test.conf.signer.getPublicKey();
|
||||||
|
const event = genEvent({ kind: 3036, tags: [['r', 'alex@gleasonator.dev'], ['p', admin]] });
|
||||||
|
|
||||||
|
await test.store.event(event);
|
||||||
|
|
||||||
|
const filter = { kinds: [30383], authors: [admin], '#d': [event.id] };
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
const { count } = await test.store.count([filter]);
|
||||||
|
return count > 0;
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
const [result] = await test.store.query([filter]);
|
||||||
|
|
||||||
|
assertEquals(result?.tags, [
|
||||||
|
['d', event.id],
|
||||||
|
['p', event.pubkey],
|
||||||
|
['k', '3036'],
|
||||||
|
['r', 'alex@gleasonator.dev'],
|
||||||
|
['n', 'pending'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test('updateAuthorData sets nip05', async () => {
|
Deno.test('updateAuthorData sets nip05', async () => {
|
||||||
const alex = generateSecretKey();
|
const alex = generateSecretKey();
|
||||||
|
|
||||||
|
|
@ -38,20 +65,25 @@ Deno.test('updateAuthorData sets nip05', async () => {
|
||||||
assertEquals(row?.nip05_hostname, 'gleasonator.dev');
|
assertEquals(row?.nip05_hostname, 'gleasonator.dev');
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupTest(cb: (req: Request) => Response | Promise<Response>) {
|
function setupTest(cb?: (req: Request) => Response | Promise<Response>) {
|
||||||
const conf = new DittoConf(Deno.env);
|
const conf = new DittoConf(Deno.env);
|
||||||
const db = new DittoPolyPg(conf.databaseUrl);
|
const db = new DittoPolyPg(conf.databaseUrl);
|
||||||
const relay = new MockRelay();
|
const relay = new MockRelay();
|
||||||
|
|
||||||
const mockFetch: typeof fetch = async (input, init) => {
|
const mockFetch: typeof fetch = async (input, init) => {
|
||||||
const req = new Request(input, init);
|
const req = new Request(input, init);
|
||||||
return await cb(req);
|
if (cb) {
|
||||||
|
return await cb(req);
|
||||||
|
} else {
|
||||||
|
return new Response('Not mocked', { status: 404 });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = new DittoRelayStore({ conf, db, relay, fetch: mockFetch });
|
const store = new DittoRelayStore({ conf, db, relay, fetch: mockFetch });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
db,
|
db,
|
||||||
|
conf,
|
||||||
store,
|
store,
|
||||||
[Symbol.asyncDispose]: async () => {
|
[Symbol.asyncDispose]: async () => {
|
||||||
await store[Symbol.asyncDispose]();
|
await store[Symbol.asyncDispose]();
|
||||||
|
|
|
||||||
|
|
@ -358,19 +358,24 @@ export class DittoRelayStore implements NRelay {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === 3036 && tagsAdmin) {
|
if (event.kind === 3036 && tagsAdmin) {
|
||||||
const rel = await signer.signEvent({
|
const r = event.tags.find(([name]) => name === 'r')?.[1];
|
||||||
kind: 30383,
|
|
||||||
content: '',
|
|
||||||
tags: [
|
|
||||||
['d', event.id],
|
|
||||||
['p', event.pubkey],
|
|
||||||
['k', '3036'],
|
|
||||||
['n', 'pending'],
|
|
||||||
],
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.event(rel, { signal: AbortSignal.timeout(1000) });
|
if (r) {
|
||||||
|
const rel = await signer.signEvent({
|
||||||
|
kind: 30383,
|
||||||
|
content: '',
|
||||||
|
tags: [
|
||||||
|
['d', event.id],
|
||||||
|
['p', event.pubkey],
|
||||||
|
['k', '3036'],
|
||||||
|
['r', r.toLowerCase()],
|
||||||
|
['n', 'pending'],
|
||||||
|
],
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.event(rel, { signal: AbortSignal.timeout(1000) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,17 +58,19 @@ async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
return result;
|
return result;
|
||||||
}, new Set<string>());
|
}, new Set<string>());
|
||||||
|
|
||||||
const favicons = (
|
const favicons: Record<string, string> = domains.size
|
||||||
await db.kysely
|
? (
|
||||||
.selectFrom('domain_favicons')
|
await db.kysely
|
||||||
.select(['domain', 'favicon'])
|
.selectFrom('domain_favicons')
|
||||||
.where('domain', 'in', [...domains])
|
.select(['domain', 'favicon'])
|
||||||
.execute()
|
.where('domain', 'in', [...domains])
|
||||||
)
|
.execute()
|
||||||
.reduce((result, { domain, favicon }) => {
|
)
|
||||||
result[domain] = favicon;
|
.reduce((result, { domain, favicon }) => {
|
||||||
return result;
|
result[domain] = favicon;
|
||||||
}, {} as Record<string, string>);
|
return result;
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
: {};
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
authors: authorStats,
|
authors: authorStats,
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,10 @@ async function createEvent<E extends (DittoEnv & { Variables: { user?: User } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = await user.signer.signEvent({
|
const event = await user.signer.signEvent({
|
||||||
content: '',
|
|
||||||
created_at: nostrNow(),
|
|
||||||
tags: [],
|
|
||||||
...t,
|
...t,
|
||||||
|
content: t.content ?? '',
|
||||||
|
created_at: t.created_at ?? nostrNow(),
|
||||||
|
tags: t.tags ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
await relay.event(event, { signal, publish: true });
|
await relay.event(event, { signal, publish: true });
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,26 @@
|
||||||
import { setUser, testApp } from '@ditto/mastoapi/test';
|
import { TestApp } from '@ditto/mastoapi/test';
|
||||||
import { assertEquals } from '@std/assert';
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
import { userMiddleware } from './userMiddleware.ts';
|
import { userMiddleware } from './userMiddleware.ts';
|
||||||
import { ReadOnlySigner } from '../signers/ReadOnlySigner.ts';
|
import { ReadOnlySigner } from '../signers/ReadOnlySigner.ts';
|
||||||
|
|
||||||
Deno.test('no user 401', async () => {
|
Deno.test('no user 401', async () => {
|
||||||
const { app } = testApp();
|
await using app = new TestApp();
|
||||||
const response = await app.use(userMiddleware()).request('/');
|
const response = await app.use(userMiddleware()).request('/');
|
||||||
assertEquals(response.status, 401);
|
assertEquals(response.status, 401);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('unsupported signer 400', async () => {
|
Deno.test('unsupported signer 400', async () => {
|
||||||
const { app, relay } = testApp();
|
await using app = new TestApp();
|
||||||
const signer = new ReadOnlySigner('0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd');
|
|
||||||
|
const user = {
|
||||||
|
signer: new ReadOnlySigner('0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd'),
|
||||||
|
relay: app.var.relay,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.user(user);
|
||||||
|
|
||||||
const response = await app
|
const response = await app
|
||||||
.use(setUser({ signer, relay }))
|
|
||||||
.use(userMiddleware({ enc: 'nip44' }))
|
.use(userMiddleware({ enc: 'nip44' }))
|
||||||
.use((c, next) => {
|
.use((c, next) => {
|
||||||
c.var.user.signer.nip44.encrypt; // test that the type is set
|
c.var.user.signer.nip44.encrypt; // test that the type is set
|
||||||
|
|
@ -27,10 +32,11 @@ Deno.test('unsupported signer 400', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('with user 200', async () => {
|
Deno.test('with user 200', async () => {
|
||||||
const { app, user } = testApp();
|
await using app = new TestApp();
|
||||||
|
|
||||||
|
app.user();
|
||||||
|
|
||||||
const response = await app
|
const response = await app
|
||||||
.use(setUser(user))
|
|
||||||
.use(userMiddleware())
|
.use(userMiddleware())
|
||||||
.get('/', (c) => c.text('ok'))
|
.get('/', (c) => c.text('ok'))
|
||||||
.request('/');
|
.request('/');
|
||||||
|
|
@ -39,10 +45,11 @@ Deno.test('with user 200', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('user and role 403', async () => {
|
Deno.test('user and role 403', async () => {
|
||||||
const { app, user } = testApp();
|
await using app = new TestApp();
|
||||||
|
|
||||||
|
app.user();
|
||||||
|
|
||||||
const response = await app
|
const response = await app
|
||||||
.use(setUser(user))
|
|
||||||
.use(userMiddleware({ role: 'admin' }))
|
.use(userMiddleware({ role: 'admin' }))
|
||||||
.request('/');
|
.request('/');
|
||||||
|
|
||||||
|
|
@ -50,7 +57,10 @@ Deno.test('user and role 403', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('admin role 200', async () => {
|
Deno.test('admin role 200', async () => {
|
||||||
const { conf, app, user, relay } = testApp();
|
await using app = new TestApp();
|
||||||
|
const { conf, relay } = app.var;
|
||||||
|
|
||||||
|
const user = app.user();
|
||||||
|
|
||||||
const event = await conf.signer.signEvent({
|
const event = await conf.signer.signEvent({
|
||||||
kind: 30382,
|
kind: 30382,
|
||||||
|
|
@ -65,7 +75,6 @@ Deno.test('admin role 200', async () => {
|
||||||
await relay.event(event);
|
await relay.event(event);
|
||||||
|
|
||||||
const response = await app
|
const response = await app
|
||||||
.use(setUser(user))
|
|
||||||
.use(userMiddleware({ role: 'admin' }))
|
.use(userMiddleware({ role: 'admin' }))
|
||||||
.get('/', (c) => c.text('ok'))
|
.get('/', (c) => c.text('ok'))
|
||||||
.request('/');
|
.request('/');
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { DittoConf } from '@ditto/conf';
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { DittoPolyPg } from '@ditto/db';
|
import { DummyDB } from '@ditto/db';
|
||||||
import { Hono } from '@hono/hono';
|
import { Hono } from '@hono/hono';
|
||||||
import { MockRelay } from '@nostrify/nostrify/test';
|
import { MockRelay } from '@nostrify/nostrify/test';
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
import { DittoApp } from './DittoApp.ts';
|
import { DittoApp } from './DittoApp.ts';
|
||||||
import { DittoRoute } from './DittoRoute.ts';
|
import { DittoRoute } from './DittoRoute.ts';
|
||||||
|
|
||||||
Deno.test('DittoApp', async () => {
|
Deno.test('DittoApp', async () => {
|
||||||
await using db = new DittoPolyPg('memory://');
|
await using db = new DummyDB();
|
||||||
const conf = new DittoConf(new Map());
|
const conf = new DittoConf(new Map());
|
||||||
const relay = new MockRelay();
|
const relay = new MockRelay();
|
||||||
|
|
||||||
|
|
@ -20,4 +21,11 @@ Deno.test('DittoApp', async () => {
|
||||||
|
|
||||||
// @ts-expect-error Passing a non-DittoRoute to route.
|
// @ts-expect-error Passing a non-DittoRoute to route.
|
||||||
app.route('/', hono);
|
app.route('/', hono);
|
||||||
|
|
||||||
|
app.get('/error', () => {
|
||||||
|
throw new Error('test error');
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app.request('/error');
|
||||||
|
assertEquals(response.status, 500);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ import { Hono } from '@hono/hono';
|
||||||
import type { HonoOptions } from '@hono/hono/hono-base';
|
import type { HonoOptions } from '@hono/hono/hono-base';
|
||||||
import type { DittoEnv } from './DittoEnv.ts';
|
import type { DittoEnv } from './DittoEnv.ts';
|
||||||
|
|
||||||
|
export type DittoAppOpts = Omit<DittoEnv['Variables'], 'signal' | 'requestId'> & HonoOptions<DittoEnv>;
|
||||||
|
|
||||||
export class DittoApp extends Hono<DittoEnv> {
|
export class DittoApp extends Hono<DittoEnv> {
|
||||||
// @ts-ignore Require a DittoRoute for type safety.
|
// @ts-ignore Require a DittoRoute for type safety.
|
||||||
declare route: (path: string, app: Hono<DittoEnv>) => Hono<DittoEnv>;
|
declare route: (path: string, app: Hono<DittoEnv>) => Hono<DittoEnv>;
|
||||||
|
|
||||||
constructor(opts: Omit<DittoEnv['Variables'], 'signal' | 'requestId'> & HonoOptions<DittoEnv>) {
|
constructor(protected opts: DittoAppOpts) {
|
||||||
super(opts);
|
super(opts);
|
||||||
|
|
||||||
this.use((c, next) => {
|
this.use((c, next) => {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,6 @@ export class DittoRoute extends Hono<DittoEnv> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ error: 'Something went wrong' }, 500);
|
throw error;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1 @@
|
||||||
import { DittoConf } from '@ditto/conf';
|
export { TestApp } from './test/TestApp.ts';
|
||||||
import { type DittoDB, DummyDB } from '@ditto/db';
|
|
||||||
import { DittoApp, type DittoMiddleware } from '@ditto/mastoapi/router';
|
|
||||||
import { type NostrSigner, type NRelay, NSecSigner } from '@nostrify/nostrify';
|
|
||||||
import { MockRelay } from '@nostrify/nostrify/test';
|
|
||||||
import { generateSecretKey, nip19 } from 'nostr-tools';
|
|
||||||
|
|
||||||
import type { User } from '@ditto/mastoapi/middleware';
|
|
||||||
|
|
||||||
export function testApp(): {
|
|
||||||
app: DittoApp;
|
|
||||||
relay: NRelay;
|
|
||||||
conf: DittoConf;
|
|
||||||
db: DittoDB;
|
|
||||||
user: {
|
|
||||||
signer: NostrSigner;
|
|
||||||
relay: NRelay;
|
|
||||||
};
|
|
||||||
} {
|
|
||||||
const db = new DummyDB();
|
|
||||||
|
|
||||||
const nsec = nip19.nsecEncode(generateSecretKey());
|
|
||||||
const conf = new DittoConf(new Map([['DITTO_NSEC', nsec]]));
|
|
||||||
|
|
||||||
const relay = new MockRelay();
|
|
||||||
const app = new DittoApp({ conf, relay, db });
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
signer: new NSecSigner(generateSecretKey()),
|
|
||||||
relay,
|
|
||||||
};
|
|
||||||
|
|
||||||
return { app, relay, conf, db, user };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setUser<S extends NostrSigner>(user: User<S>): DittoMiddleware<{ user: User<S> }> {
|
|
||||||
return async (c, next) => {
|
|
||||||
c.set('user', user);
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
97
packages/mastoapi/test/TestApp.ts
Normal file
97
packages/mastoapi/test/TestApp.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { DittoConf } from '@ditto/conf';
|
||||||
|
import { type DittoDB, DummyDB } from '@ditto/db';
|
||||||
|
import { HTTPException } from '@hono/hono/http-exception';
|
||||||
|
import { type NRelay, NSecSigner } from '@nostrify/nostrify';
|
||||||
|
import { generateSecretKey, nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
import { DittoApp, type DittoAppOpts } from '../router/DittoApp.ts';
|
||||||
|
|
||||||
|
import type { Context } from '@hono/hono';
|
||||||
|
import type { User } from '../middleware/User.ts';
|
||||||
|
import { MockRelay } from '@nostrify/nostrify/test';
|
||||||
|
|
||||||
|
interface DittoVars {
|
||||||
|
db: DittoDB;
|
||||||
|
conf: DittoConf;
|
||||||
|
relay: NRelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestApp extends DittoApp implements AsyncDisposable {
|
||||||
|
private _user?: User;
|
||||||
|
|
||||||
|
constructor(opts?: Partial<DittoAppOpts>) {
|
||||||
|
const nsec = nip19.nsecEncode(generateSecretKey());
|
||||||
|
|
||||||
|
const conf = opts?.conf ?? new DittoConf(
|
||||||
|
new Map([
|
||||||
|
['DITTO_NSEC', nsec],
|
||||||
|
['LOCAL_DOMAIN', 'https://ditto.pub'],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const db = opts?.db ?? new DummyDB();
|
||||||
|
const relay = opts?.relay ?? new MockRelay();
|
||||||
|
|
||||||
|
super({
|
||||||
|
db,
|
||||||
|
conf,
|
||||||
|
relay,
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.use(async (c: Context<{ Variables: { user?: User } }>, next) => {
|
||||||
|
c.set('user', this._user);
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.onError((err, c) => {
|
||||||
|
if (err instanceof HTTPException) {
|
||||||
|
if (err.res) {
|
||||||
|
return err.res;
|
||||||
|
} else {
|
||||||
|
return c.json({ error: err.message }, err.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get var(): DittoVars {
|
||||||
|
return {
|
||||||
|
db: this.opts.db,
|
||||||
|
conf: this.opts.conf,
|
||||||
|
relay: this.opts.relay,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
user(user?: User): User {
|
||||||
|
user ??= this.createUser();
|
||||||
|
this._user = user;
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
createUser(sk?: Uint8Array): User {
|
||||||
|
return {
|
||||||
|
relay: this.opts.relay,
|
||||||
|
signer: new NSecSigner(sk ?? generateSecretKey()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
api = {
|
||||||
|
get: async (path: string): Promise<Response> => {
|
||||||
|
return await this.request(path);
|
||||||
|
},
|
||||||
|
post: async (path: string, body: unknown): Promise<Response> => {
|
||||||
|
return await this.request(path, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async [Symbol.asyncDispose](): Promise<void> {
|
||||||
|
await this.opts.db[Symbol.asyncDispose]();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue