@nostrify/policies 0.36.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/daemon/7bb8240f68f7ad88-turbo.log.2025-07-29 +0 -0
- package/.turbo/turbo-build.log +5 -0
- package/AntiDuplicationPolicy.test.ts +50 -0
- package/AntiDuplicationPolicy.ts +78 -0
- package/AnyPolicy.test.ts +57 -0
- package/AnyPolicy.ts +21 -0
- package/AuthorPolicy.test.ts +23 -0
- package/AuthorPolicy.ts +23 -0
- package/CHANGELOG.md +66 -0
- package/DomainPolicy.test.ts +246 -0
- package/DomainPolicy.ts +96 -0
- package/FiltersPolicy.test.ts +14 -0
- package/FiltersPolicy.ts +27 -0
- package/HashtagPolicy.test.ts +34 -0
- package/HashtagPolicy.ts +25 -0
- package/HellthreadPolicy.test.ts +23 -0
- package/HellthreadPolicy.ts +27 -0
- package/InvertPolicy.test.ts +19 -0
- package/InvertPolicy.ts +17 -0
- package/KeywordPolicy.test.ts +28 -0
- package/KeywordPolicy.ts +25 -0
- package/LICENSE +21 -0
- package/NoOpPolicy.test.ts +18 -0
- package/NoOpPolicy.ts +9 -0
- package/OpenAIPolicy.test.ts +42 -0
- package/OpenAIPolicy.ts +113 -0
- package/PipePolicy.test.ts +58 -0
- package/PipePolicy.ts +36 -0
- package/PowPolicy.test.ts +27 -0
- package/PowPolicy.ts +41 -0
- package/PubkeyBanPolicy.test.ts +17 -0
- package/PubkeyBanPolicy.ts +24 -0
- package/README.md +34 -0
- package/ReadOnlyPolicy.test.ts +19 -0
- package/ReadOnlyPolicy.ts +9 -0
- package/RegexPolicy.test.ts +17 -0
- package/RegexPolicy.ts +22 -0
- package/ReplyBotPolicy.test.ts +50 -0
- package/ReplyBotPolicy.ts +59 -0
- package/SizePolicy.test.ts +21 -0
- package/SizePolicy.ts +35 -0
- package/WhitelistPolicy.test.ts +17 -0
- package/WhitelistPolicy.ts +32 -0
- package/WoTPolicy.test.ts +35 -0
- package/WoTPolicy.ts +61 -0
- package/dist/AntiDuplicationPolicy.d.ts +43 -0
- package/dist/AntiDuplicationPolicy.d.ts.map +1 -0
- package/dist/AntiDuplicationPolicy.js +63 -0
- package/dist/AntiDuplicationPolicy.js.map +1 -0
- package/dist/AnyPolicy.d.ts +8 -0
- package/dist/AnyPolicy.d.ts.map +1 -0
- package/dist/AnyPolicy.js +23 -0
- package/dist/AnyPolicy.js.map +1 -0
- package/dist/AuthorPolicy.d.ts +9 -0
- package/dist/AuthorPolicy.d.ts.map +1 -0
- package/dist/AuthorPolicy.js +27 -0
- package/dist/AuthorPolicy.js.map +1 -0
- package/dist/DomainPolicy.d.ts +21 -0
- package/dist/DomainPolicy.d.ts.map +1 -0
- package/dist/DomainPolicy.js +68 -0
- package/dist/DomainPolicy.js.map +1 -0
- package/dist/FiltersPolicy.d.ts +18 -0
- package/dist/FiltersPolicy.d.ts.map +1 -0
- package/dist/FiltersPolicy.js +30 -0
- package/dist/FiltersPolicy.js.map +1 -0
- package/dist/HashtagPolicy.d.ts +16 -0
- package/dist/HashtagPolicy.d.ts.map +1 -0
- package/dist/HashtagPolicy.js +29 -0
- package/dist/HashtagPolicy.js.map +1 -0
- package/dist/HellthreadPolicy.d.ts +14 -0
- package/dist/HellthreadPolicy.d.ts.map +1 -0
- package/dist/HellthreadPolicy.js +23 -0
- package/dist/HellthreadPolicy.js.map +1 -0
- package/dist/InvertPolicy.d.ts +9 -0
- package/dist/InvertPolicy.d.ts.map +1 -0
- package/dist/InvertPolicy.js +24 -0
- package/dist/InvertPolicy.js.map +1 -0
- package/dist/KeywordPolicy.d.ts +16 -0
- package/dist/KeywordPolicy.d.ts.map +1 -0
- package/dist/KeywordPolicy.js +29 -0
- package/dist/KeywordPolicy.js.map +1 -0
- package/dist/NoOpPolicy.d.ts +6 -0
- package/dist/NoOpPolicy.d.ts.map +1 -0
- package/dist/NoOpPolicy.js +12 -0
- package/dist/NoOpPolicy.js.map +1 -0
- package/dist/OpenAIPolicy.d.ts +80 -0
- package/dist/OpenAIPolicy.d.ts.map +1 -0
- package/dist/OpenAIPolicy.js +62 -0
- package/dist/OpenAIPolicy.js.map +1 -0
- package/dist/PipePolicy.d.ts +26 -0
- package/dist/PipePolicy.d.ts.map +1 -0
- package/dist/PipePolicy.js +39 -0
- package/dist/PipePolicy.js.map +1 -0
- package/dist/PowPolicy.d.ts +21 -0
- package/dist/PowPolicy.d.ts.map +1 -0
- package/dist/PowPolicy.js +36 -0
- package/dist/PowPolicy.js.map +1 -0
- package/dist/PubkeyBanPolicy.d.ts +15 -0
- package/dist/PubkeyBanPolicy.d.ts.map +1 -0
- package/dist/PubkeyBanPolicy.js +28 -0
- package/dist/PubkeyBanPolicy.js.map +1 -0
- package/dist/ReadOnlyPolicy.d.ts +6 -0
- package/dist/ReadOnlyPolicy.d.ts.map +1 -0
- package/dist/ReadOnlyPolicy.js +12 -0
- package/dist/ReadOnlyPolicy.js.map +1 -0
- package/dist/RegexPolicy.d.ts +15 -0
- package/dist/RegexPolicy.d.ts.map +1 -0
- package/dist/RegexPolicy.js +26 -0
- package/dist/RegexPolicy.js.map +1 -0
- package/dist/ReplyBotPolicy.d.ts +25 -0
- package/dist/ReplyBotPolicy.d.ts.map +1 -0
- package/dist/ReplyBotPolicy.js +45 -0
- package/dist/ReplyBotPolicy.js.map +1 -0
- package/dist/SizePolicy.d.ts +23 -0
- package/dist/SizePolicy.d.ts.map +1 -0
- package/dist/SizePolicy.js +31 -0
- package/dist/SizePolicy.js.map +1 -0
- package/dist/WhitelistPolicy.d.ts +16 -0
- package/dist/WhitelistPolicy.d.ts.map +1 -0
- package/dist/WhitelistPolicy.js +35 -0
- package/dist/WhitelistPolicy.js.map +1 -0
- package/dist/WoTPolicy.d.ts +26 -0
- package/dist/WoTPolicy.d.ts.map +1 -0
- package/dist/WoTPolicy.js +42 -0
- package/dist/WoTPolicy.js.map +1 -0
- package/dist/mod.d.ts +21 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +44 -0
- package/dist/mod.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/mod.ts +20 -0
- package/package.json +20 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Nostrify Policies
|
|
2
|
+
|
|
3
|
+
Policies allow you to prevent spam on your relay (or in your client).
|
|
4
|
+
|
|
5
|
+
Policies inspect one event at a time, which they either accept or reject. It's up to the application to decide how to handle the result.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
AntiDuplicationPolicy,
|
|
10
|
+
FiltersPolicy,
|
|
11
|
+
HellthreadPolicy,
|
|
12
|
+
KeywordPolicy,
|
|
13
|
+
PipePolicy,
|
|
14
|
+
PowPolicy,
|
|
15
|
+
RegexPolicy,
|
|
16
|
+
} from '@nostrify/policies';
|
|
17
|
+
|
|
18
|
+
const policy = new PipePolicy([
|
|
19
|
+
new FiltersPolicy([{ kinds: [0, 1, 3, 5, 7, 1984, 9734, 9735, 10002] }]),
|
|
20
|
+
new KeywordPolicy(['https://t.me/']),
|
|
21
|
+
new RegexPolicy(/(🟠|🔥|😳)ChtaGPT/i),
|
|
22
|
+
new PubkeyBanPolicy(['e810fafa1e89cdf80cced8e013938e87e21b699b24c8570537be92aec4b12c18']),
|
|
23
|
+
new HellthreadPolicy({ limit: 100 }),
|
|
24
|
+
new AntiDuplicationPolicy({ kv: await Deno.openKv(), expireIn: 60000, minLength: 50 }),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const [_, eventId, ok, reason] = await policy.call(event);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Full documentation: https://nostrify.dev/policy
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { ReadOnlyPolicy } from './ReadOnlyPolicy.ts';
|
|
5
|
+
|
|
6
|
+
Deno.test('ReadOnlyPolicy', async () => {
|
|
7
|
+
const policy = new ReadOnlyPolicy();
|
|
8
|
+
|
|
9
|
+
const event = finalizeEvent(
|
|
10
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
11
|
+
generateSecretKey(),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const [_, eventId, ok, reason] = await policy.call(event);
|
|
15
|
+
|
|
16
|
+
assertEquals(eventId, event.id);
|
|
17
|
+
assertEquals(reason, 'blocked: the relay is read-only');
|
|
18
|
+
assertEquals(ok, false);
|
|
19
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/** This policy rejects all messages. */
|
|
4
|
+
export class ReadOnlyPolicy implements NPolicy {
|
|
5
|
+
// deno-lint-ignore require-await
|
|
6
|
+
async call(event: NostrEvent): Promise<NostrRelayOK> {
|
|
7
|
+
return ['OK', event.id, false, 'blocked: the relay is read-only'];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { RegexPolicy } from './RegexPolicy.ts';
|
|
5
|
+
|
|
6
|
+
Deno.test('RegexPolicy', async () => {
|
|
7
|
+
const event = finalizeEvent(
|
|
8
|
+
{ kind: 1, content: '🔥🔥🔥 https://t.me/spam 我想死', tags: [], created_at: 0 },
|
|
9
|
+
generateSecretKey(),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
assertEquals((await new RegexPolicy(/https:\/\/t\.me\/\w+/i).call(event))[2], false);
|
|
13
|
+
assertEquals((await new RegexPolicy(/🔥{1,3}/).call(event))[2], false);
|
|
14
|
+
assertEquals((await new RegexPolicy(/🔥{4}/).call(event))[2], true);
|
|
15
|
+
assertEquals((await new RegexPolicy(/🔥$/).call(event))[2], true);
|
|
16
|
+
assertEquals((await new RegexPolicy(/^🔥/).call(event))[2], false);
|
|
17
|
+
});
|
package/RegexPolicy.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reject events whose content matches the regex.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // Ban events matching a regex.
|
|
8
|
+
* new RegexPolicy(/(🟠|🔥|😳)ChtaGPT/i);
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export class RegexPolicy implements NPolicy {
|
|
12
|
+
constructor(private regex: RegExp) {}
|
|
13
|
+
|
|
14
|
+
// deno-lint-ignore require-await
|
|
15
|
+
async call({ id, content }: NostrEvent): Promise<NostrRelayOK> {
|
|
16
|
+
if (this.regex.test(content)) {
|
|
17
|
+
return ['OK', id, false, 'blocked: text matches a banned expression'];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return ['OK', id, true, ''];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { genEvent, MockRelay } from '@nostrify/nostrify/test';
|
|
3
|
+
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
|
4
|
+
|
|
5
|
+
import { ReplyBotPolicy } from './ReplyBotPolicy.ts';
|
|
6
|
+
|
|
7
|
+
Deno.test('ReplyBotPolicy blocks replies within the same second', async () => {
|
|
8
|
+
const store = new MockRelay();
|
|
9
|
+
const policy = new ReplyBotPolicy({ store });
|
|
10
|
+
|
|
11
|
+
const event = genEvent({ kind: 1, created_at: 0 });
|
|
12
|
+
const reply = genEvent({ kind: 1, created_at: 1, tags: [['e', event.id]] });
|
|
13
|
+
|
|
14
|
+
await store.event(event);
|
|
15
|
+
|
|
16
|
+
const [, , ok] = await policy.call(reply);
|
|
17
|
+
|
|
18
|
+
assertEquals(ok, false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
Deno.test('ReplyBotPolicy allows replies after 1 second', async () => {
|
|
22
|
+
const store = new MockRelay();
|
|
23
|
+
const policy = new ReplyBotPolicy({ store });
|
|
24
|
+
|
|
25
|
+
const event = genEvent({ kind: 1, created_at: 0 });
|
|
26
|
+
const reply = genEvent({ kind: 1, created_at: 2, tags: [['e', event.id]] });
|
|
27
|
+
|
|
28
|
+
await store.event(event);
|
|
29
|
+
|
|
30
|
+
const [, , ok] = await policy.call(reply);
|
|
31
|
+
|
|
32
|
+
assertEquals(ok, true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
Deno.test('ReplyBotPolicy allows replies within the same second from users who are tagged', async () => {
|
|
36
|
+
const store = new MockRelay();
|
|
37
|
+
const policy = new ReplyBotPolicy({ store });
|
|
38
|
+
|
|
39
|
+
const sk = generateSecretKey();
|
|
40
|
+
const pubkey = getPublicKey(sk);
|
|
41
|
+
|
|
42
|
+
const event = genEvent({ kind: 1, created_at: 0, tags: [['p', pubkey]] });
|
|
43
|
+
const reply = genEvent({ kind: 1, created_at: 1, tags: [['e', event.id]] }, sk);
|
|
44
|
+
|
|
45
|
+
await store.event(event);
|
|
46
|
+
|
|
47
|
+
const [, , ok] = await policy.call(reply);
|
|
48
|
+
|
|
49
|
+
assertEquals(ok, true);
|
|
50
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayOK, NPolicy, NStore } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/** Options for the ReplyBotPolicy. */
|
|
4
|
+
export interface ReplyBotPolicyOpts {
|
|
5
|
+
/** The store to use for fetching events. */
|
|
6
|
+
store: NStore;
|
|
7
|
+
/** The minimum time in seconds between two posts. */
|
|
8
|
+
threshold?: number;
|
|
9
|
+
/** The kinds of events to apply the policy to. */
|
|
10
|
+
kinds?: number[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Block events that reply too quickly to another event. */
|
|
14
|
+
export class ReplyBotPolicy implements NPolicy {
|
|
15
|
+
constructor(private opts: ReplyBotPolicyOpts) {}
|
|
16
|
+
|
|
17
|
+
async call(event: NostrEvent, signal?: AbortSignal): Promise<NostrRelayOK> {
|
|
18
|
+
const { store, threshold = 1, kinds = [1] } = this.opts;
|
|
19
|
+
|
|
20
|
+
if (kinds.includes(event.kind)) {
|
|
21
|
+
const [, replyToId] = ReplyBotPolicy.findReplyTag(event.tags) ?? [];
|
|
22
|
+
|
|
23
|
+
if (replyToId) {
|
|
24
|
+
const [prevEvent] = await store.query([{ ids: [replyToId] }], { signal });
|
|
25
|
+
|
|
26
|
+
if (prevEvent) {
|
|
27
|
+
const diff = event.created_at - prevEvent.created_at;
|
|
28
|
+
const pTag = prevEvent.tags.find(([name, value]) => name === 'p' && value === event.pubkey);
|
|
29
|
+
|
|
30
|
+
if (diff <= threshold && !pTag) {
|
|
31
|
+
return ['OK', event.id, false, 'rate-limited: replied too quickly'];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return ['OK', event.id, true, ''];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Tag is a NIP-10 root tag. */
|
|
41
|
+
private static isRootTag(tag: string[]): tag is ['e', string, string, 'root', ...string[]] {
|
|
42
|
+
return tag[0] === 'e' && tag[3] === 'root';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Tag is a NIP-10 reply tag. */
|
|
46
|
+
private static isReplyTag(tag: string[]): tag is ['e', string, string, 'reply', ...string[]] {
|
|
47
|
+
return tag[0] === 'e' && tag[3] === 'reply';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Tag is an "e" tag without a NIP-10 marker. */
|
|
51
|
+
private static isLegacyReplyTag(tag: string[]): tag is ['e', string, string] {
|
|
52
|
+
return tag[0] === 'e' && !tag[3];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Get the "e" tag for the event being replied to, first according to the NIPs then falling back to the legacy way. */
|
|
56
|
+
private static findReplyTag(tags: string[][]): ['e', ...string[]] | undefined {
|
|
57
|
+
return tags.find(this.isReplyTag) || tags.find(this.isRootTag) || tags.findLast(this.isLegacyReplyTag);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { SizePolicy } from './SizePolicy.ts';
|
|
5
|
+
|
|
6
|
+
Deno.test('SizePolicy', async () => {
|
|
7
|
+
const policy = new SizePolicy();
|
|
8
|
+
|
|
9
|
+
const event = finalizeEvent(
|
|
10
|
+
{ kind: 1, content: 'yolo'.repeat(100), tags: [], created_at: 0 },
|
|
11
|
+
generateSecretKey(),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const bigEvent = finalizeEvent(
|
|
15
|
+
{ kind: 1, content: 'yolo'.repeat(2500), tags: [], created_at: 0 },
|
|
16
|
+
generateSecretKey(),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
assertEquals((await policy.call(event))[2], true);
|
|
20
|
+
assertEquals((await policy.call(bigEvent))[2], false);
|
|
21
|
+
});
|
package/SizePolicy.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/** Policy options for `SizePolicy`. */
|
|
4
|
+
interface SizePolicyOpts {
|
|
5
|
+
/** Maximum size of the message content in bytes. Default: 8192 (8KB) */
|
|
6
|
+
maxBytes?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reject events larger than a specified size in bytes.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // Reject events larger than the default size (8KB) .
|
|
14
|
+
* new SizePolicy();
|
|
15
|
+
* // Reject events larger than a custom size (15KB).
|
|
16
|
+
* new SizePolicy({ maxBytes: 15 * 1024 });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class SizePolicy implements NPolicy {
|
|
20
|
+
constructor(private opts: SizePolicyOpts = {}) {}
|
|
21
|
+
|
|
22
|
+
// deno-lint-ignore require-await
|
|
23
|
+
async call(event: NostrEvent): Promise<NostrRelayOK> {
|
|
24
|
+
const { maxBytes = 8 * 1024 } = this.opts;
|
|
25
|
+
|
|
26
|
+
const json = JSON.stringify(event);
|
|
27
|
+
const size = new TextEncoder().encode(json).length;
|
|
28
|
+
|
|
29
|
+
if (size > maxBytes) {
|
|
30
|
+
return ['OK', event.id, false, `blocked: event is too large`];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return ['OK', event.id, true, ''];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { WhitelistPolicy } from './WhitelistPolicy.ts';
|
|
5
|
+
|
|
6
|
+
Deno.test('WhitelistPolicy', async () => {
|
|
7
|
+
const [event1, event2, event3] = new Array(3).fill(0).map(() => {
|
|
8
|
+
return finalizeEvent(
|
|
9
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
10
|
+
generateSecretKey(),
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
assertEquals((await new WhitelistPolicy([]).call(event1))[2], false);
|
|
15
|
+
assertEquals((await new WhitelistPolicy([event2.pubkey, event1.pubkey]).call(event3))[2], false);
|
|
16
|
+
assertEquals((await new WhitelistPolicy([event2.pubkey, event1.pubkey]).call(event2))[2], true);
|
|
17
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayInfo, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Allows only the listed pubkeys to post. All other events are rejected.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // Only the given pubkey may post.
|
|
8
|
+
* new WhitelistPolicy(['e810fafa1e89cdf80cced8e013938e87e21b699b24c8570537be92aec4b12c18']);
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export class WhitelistPolicy implements NPolicy {
|
|
12
|
+
constructor(private pubkeys: Iterable<string>) {}
|
|
13
|
+
|
|
14
|
+
// deno-lint-ignore require-await
|
|
15
|
+
async call({ id, pubkey }: NostrEvent): Promise<NostrRelayOK> {
|
|
16
|
+
for (const p of this.pubkeys) {
|
|
17
|
+
if (p === pubkey) {
|
|
18
|
+
return ['OK', id, true, ''];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return ['OK', id, false, 'blocked: only certain pubkeys are allowed to post'];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get info(): NostrRelayInfo {
|
|
26
|
+
return {
|
|
27
|
+
limitation: {
|
|
28
|
+
restricted_writes: true,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ErrorRelay, genEvent, MockRelay } from '@nostrify/nostrify/test';
|
|
2
|
+
import { assert, assertFalse } from '@std/assert';
|
|
3
|
+
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
|
4
|
+
|
|
5
|
+
import { WoTPolicy } from './WoTPolicy.ts';
|
|
6
|
+
|
|
7
|
+
function keygen(): { pubkey: string; seckey: Uint8Array } {
|
|
8
|
+
const seckey = generateSecretKey();
|
|
9
|
+
const pubkey = getPublicKey(seckey);
|
|
10
|
+
return { pubkey, seckey };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Deno.test('WoTPolicy', async () => {
|
|
14
|
+
const store = new MockRelay();
|
|
15
|
+
const [alex, patrick, fiatjaf, replyguy] = [keygen(), keygen(), keygen(), keygen()];
|
|
16
|
+
|
|
17
|
+
await store.event(genEvent({ kind: 3, tags: [['p', patrick.pubkey]] }, alex.seckey));
|
|
18
|
+
await store.event(genEvent({ kind: 3, tags: [['p', fiatjaf.pubkey]] }, patrick.seckey));
|
|
19
|
+
await store.event(genEvent({ kind: 3, tags: [['p', patrick.pubkey]] }, fiatjaf.seckey));
|
|
20
|
+
|
|
21
|
+
const policy = new WoTPolicy({ store, pubkeys: [alex.pubkey], depth: 2 });
|
|
22
|
+
|
|
23
|
+
assert((await policy.call(genEvent({}, alex.seckey)))[2]);
|
|
24
|
+
assert((await policy.call(genEvent({}, patrick.seckey)))[2]);
|
|
25
|
+
assert((await policy.call(genEvent({}, fiatjaf.seckey)))[2]);
|
|
26
|
+
assertFalse((await policy.call(genEvent({}, replyguy.seckey)))[2]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
Deno.test('WoTPolicy constructor with error store', () => {
|
|
30
|
+
const store = new ErrorRelay();
|
|
31
|
+
const alex = keygen();
|
|
32
|
+
|
|
33
|
+
// Ensure this doesn't result in an unhandled promise rejection.
|
|
34
|
+
new WoTPolicy({ store, pubkeys: [alex.pubkey], depth: 1 });
|
|
35
|
+
});
|
package/WoTPolicy.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayOK, NPolicy, NStore } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/** Options for the `WoTPolicy`. */
|
|
4
|
+
interface WoTPolicyOpts {
|
|
5
|
+
/** Store to get kind 3 follow lists from. */
|
|
6
|
+
store: NStore;
|
|
7
|
+
/** Initial set of trusted pubkeys to query follow lists for. */
|
|
8
|
+
pubkeys: Iterable<string>;
|
|
9
|
+
/**
|
|
10
|
+
* How many levels of follow lists to query.
|
|
11
|
+
* `0` will just whitelist the given `pubkeys` without checking their follow lists.
|
|
12
|
+
* `1` will query their follows,
|
|
13
|
+
* `2` will query their follows follows, etc.
|
|
14
|
+
*/
|
|
15
|
+
depth: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Whitelist pubkeys the given user follows, people those users follow, etc. up to `depth`. */
|
|
19
|
+
export class WoTPolicy implements NPolicy {
|
|
20
|
+
private pubkeys: Promise<Set<string>> | undefined;
|
|
21
|
+
|
|
22
|
+
constructor(private opts: WoTPolicyOpts) {}
|
|
23
|
+
|
|
24
|
+
async call(event: NostrEvent): Promise<NostrRelayOK> {
|
|
25
|
+
this.pubkeys ??= this.getPubkeys();
|
|
26
|
+
const pubkeys = await this.pubkeys;
|
|
27
|
+
|
|
28
|
+
if (pubkeys.has(event.pubkey)) {
|
|
29
|
+
return ['OK', event.id, true, ''];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return ['OK', event.id, false, 'blocked: only certain pubkeys are allowed to post'];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Retrieve the complete set of pubkeys to whitelist. */
|
|
36
|
+
private async getPubkeys(): Promise<Set<string>> {
|
|
37
|
+
const { store, depth } = this.opts;
|
|
38
|
+
|
|
39
|
+
const pubkeys = new Set<string>([...this.opts.pubkeys]);
|
|
40
|
+
const authors = new Set(pubkeys);
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < depth; i++) {
|
|
43
|
+
const events = await store.query([{ kinds: [3], authors: [...authors] }]);
|
|
44
|
+
|
|
45
|
+
authors.clear();
|
|
46
|
+
|
|
47
|
+
for (const event of events) {
|
|
48
|
+
for (const [name, value] of event.tags) {
|
|
49
|
+
if (name === 'p') {
|
|
50
|
+
if (!pubkeys.has(value)) { // Avoid infinite loops.
|
|
51
|
+
authors.add(value);
|
|
52
|
+
}
|
|
53
|
+
pubkeys.add(value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return pubkeys;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
import { Kv } from '@deno/kv';
|
|
3
|
+
/** Policy options for `AntiDuplicationPolicy`. */
|
|
4
|
+
interface AntiDuplicationPolicyOpts {
|
|
5
|
+
/** Deno.Kv implementation to use. */
|
|
6
|
+
kv: Pick<Kv, 'get' | 'set'>;
|
|
7
|
+
/** Time in ms until a message with this content may be posted again. Default: `60000` (1 minute). */
|
|
8
|
+
expireIn?: number;
|
|
9
|
+
/** Note text under this limit will be skipped by the policy. Default: `50`. */
|
|
10
|
+
minLength?: number;
|
|
11
|
+
/** Normalize the event's content before a hash is taken, to prevent the attacker from making small changes. Should return the normalized content. */
|
|
12
|
+
deobfuscate?(event: NostrEvent): string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Prevent messages with the exact same content from being submitted repeatedly.
|
|
16
|
+
*
|
|
17
|
+
* It stores a hashcode for each content in a Deno.Kv database and rate-limits them. Only messages that meet the minimum length criteria are selected.
|
|
18
|
+
* Each time a matching message is submitted, the timer will reset, so spammers sending the same message will only ever get the first one through.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* // Open a Deno.KV instance.
|
|
22
|
+
* const kv = await Deno.openKv();
|
|
23
|
+
*
|
|
24
|
+
* // Prevent the same message from being posted within 60 seconds.
|
|
25
|
+
* new AntiDuplicationPolicy({ kv, expireIn: 60000 });
|
|
26
|
+
*
|
|
27
|
+
* // Only enforce the policy on messages with at least 50 characters.
|
|
28
|
+
* new AntiDuplicationPolicy({ kv, expireIn: 60000, minLength: 50 });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare class AntiDuplicationPolicy implements NPolicy {
|
|
32
|
+
private opts;
|
|
33
|
+
constructor(opts: AntiDuplicationPolicyOpts);
|
|
34
|
+
call(event: NostrEvent): Promise<NostrRelayOK>;
|
|
35
|
+
/**
|
|
36
|
+
* Get a "good enough" unique identifier for this content.
|
|
37
|
+
* This algorithm was chosen because it's very fast with a low chance of collisions.
|
|
38
|
+
* https://stackoverflow.com/a/8831937
|
|
39
|
+
*/
|
|
40
|
+
private static hashCode;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=AntiDuplicationPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AntiDuplicationPolicy.d.ts","sourceRoot":"","sources":["../AntiDuplicationPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,EAAE,EAAS,MAAM,UAAU,CAAC;AAErC,kDAAkD;AAClD,UAAU,yBAAyB;IACjC,qCAAqC;IACrC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;IAC5B,qGAAqG;IACrG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qJAAqJ;IACrJ,WAAW,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAAC;CACzC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,qBAAsB,YAAW,OAAO;IACvC,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,yBAAyB;IAE7C,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC;IA4BpD;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ;CASxB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AntiDuplicationPolicy = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Prevent messages with the exact same content from being submitted repeatedly.
|
|
6
|
+
*
|
|
7
|
+
* It stores a hashcode for each content in a Deno.Kv database and rate-limits them. Only messages that meet the minimum length criteria are selected.
|
|
8
|
+
* Each time a matching message is submitted, the timer will reset, so spammers sending the same message will only ever get the first one through.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* // Open a Deno.KV instance.
|
|
12
|
+
* const kv = await Deno.openKv();
|
|
13
|
+
*
|
|
14
|
+
* // Prevent the same message from being posted within 60 seconds.
|
|
15
|
+
* new AntiDuplicationPolicy({ kv, expireIn: 60000 });
|
|
16
|
+
*
|
|
17
|
+
* // Only enforce the policy on messages with at least 50 characters.
|
|
18
|
+
* new AntiDuplicationPolicy({ kv, expireIn: 60000, minLength: 50 });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
class AntiDuplicationPolicy {
|
|
22
|
+
opts;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
this.opts = opts;
|
|
25
|
+
}
|
|
26
|
+
async call(event) {
|
|
27
|
+
const { id, kind } = event;
|
|
28
|
+
const { kv, expireIn = 60000, minLength = 50, deobfuscate } = this.opts;
|
|
29
|
+
const content = deobfuscate?.(event) ?? event.content;
|
|
30
|
+
if (kind === 1 && content.length >= minLength) {
|
|
31
|
+
const hash = AntiDuplicationPolicy.hashCode(content);
|
|
32
|
+
const key = ['nostrify', 'policies', 'antiduplication', hash];
|
|
33
|
+
const { value } = await kv.get(key);
|
|
34
|
+
if (value) {
|
|
35
|
+
await kv.set(key, true, { expireIn });
|
|
36
|
+
return [
|
|
37
|
+
'OK',
|
|
38
|
+
id,
|
|
39
|
+
false,
|
|
40
|
+
'blocked: the same message has been repeated too many times',
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
await kv.set(key, true, { expireIn });
|
|
44
|
+
}
|
|
45
|
+
return ['OK', id, true, ''];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get a "good enough" unique identifier for this content.
|
|
49
|
+
* This algorithm was chosen because it's very fast with a low chance of collisions.
|
|
50
|
+
* https://stackoverflow.com/a/8831937
|
|
51
|
+
*/
|
|
52
|
+
static hashCode(str) {
|
|
53
|
+
let hash = 0;
|
|
54
|
+
for (let i = 0, len = str.length; i < len; i++) {
|
|
55
|
+
const chr = str.charCodeAt(i);
|
|
56
|
+
hash = (hash << 5) - hash + chr;
|
|
57
|
+
hash |= 0; // Convert to 32bit integer
|
|
58
|
+
}
|
|
59
|
+
return hash;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.AntiDuplicationPolicy = AntiDuplicationPolicy;
|
|
63
|
+
//# sourceMappingURL=AntiDuplicationPolicy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AntiDuplicationPolicy.js","sourceRoot":"","sources":["../AntiDuplicationPolicy.ts"],"names":[],"mappings":";;;AAeA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAa,qBAAqB;IACZ;IAApB,YAAoB,IAA+B;QAA/B,SAAI,GAAJ,IAAI,CAA2B;IAAG,CAAC;IAEvD,KAAK,CAAC,IAAI,CAAC,KAAiB;QAC1B,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;QAC3B,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,KAAK,EAAE,SAAS,GAAG,EAAE,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC;QAExE,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC;QAEtD,IAAI,IAAI,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,IAAI,SAAS,EAAE,CAAC;YAC9C,MAAM,IAAI,GAAG,qBAAqB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YACrD,MAAM,GAAG,GAAU,CAAC,UAAU,EAAE,UAAU,EAAE,iBAAiB,EAAE,IAAI,CAAC,CAAC;YAErE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAEpC,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;gBACtC,OAAO;oBACL,IAAI;oBACJ,EAAE;oBACF,KAAK;oBACL,4DAA4D;iBAC7D,CAAC;YACJ,CAAC;YAED,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;QACxC,CAAC;QAED,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,QAAQ,CAAC,GAAW;QACjC,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/C,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,GAAG,GAAG,CAAC;YAChC,IAAI,IAAI,CAAC,CAAC,CAAC,2BAA2B;QACxC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AA7CD,sDA6CC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
/** Similar to `PipePolicy`, but passes if at least one policy passes. */
|
|
3
|
+
export declare class AnyPolicy implements NPolicy {
|
|
4
|
+
private policies;
|
|
5
|
+
constructor(policies: NPolicy[]);
|
|
6
|
+
call(event: NostrEvent, signal?: AbortSignal): Promise<NostrRelayOK>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=AnyPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnyPolicy.d.ts","sourceRoot":"","sources":["../AnyPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAEpE,yEAAyE;AACzE,qBAAa,SAAU,YAAW,OAAO;IAC3B,OAAO,CAAC,QAAQ;gBAAR,QAAQ,EAAE,OAAO,EAAE;IAEjC,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;CAc3E"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AnyPolicy = void 0;
|
|
4
|
+
/** Similar to `PipePolicy`, but passes if at least one policy passes. */
|
|
5
|
+
class AnyPolicy {
|
|
6
|
+
policies;
|
|
7
|
+
constructor(policies) {
|
|
8
|
+
this.policies = policies;
|
|
9
|
+
}
|
|
10
|
+
async call(event, signal) {
|
|
11
|
+
let result = ['OK', event.id, false, 'blocked: no policy passed'];
|
|
12
|
+
for (const policy of this.policies) {
|
|
13
|
+
result = await policy.call(event, signal);
|
|
14
|
+
const ok = result[2];
|
|
15
|
+
if (ok) {
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.AnyPolicy = AnyPolicy;
|
|
23
|
+
//# sourceMappingURL=AnyPolicy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnyPolicy.js","sourceRoot":"","sources":["../AnyPolicy.ts"],"names":[],"mappings":";;;AAEA,yEAAyE;AACzE,MAAa,SAAS;IACA;IAApB,YAAoB,QAAmB;QAAnB,aAAQ,GAAR,QAAQ,CAAW;IAAG,CAAC;IAE3C,KAAK,CAAC,IAAI,CAAC,KAAiB,EAAE,MAAoB;QAChD,IAAI,MAAM,GAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,2BAA2B,CAAC,CAAC;QAEhF,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAE1C,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACrB,IAAI,EAAE,EAAE,CAAC;gBACP,OAAO,MAAM,CAAC;YAChB,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAjBD,8BAiBC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayOK, NPolicy, NStore } from '@nostrify/types';
|
|
2
|
+
/** Rejects events by authors without a kind 0, then optionally applies another policy to the kind 0. */
|
|
3
|
+
export declare class AuthorPolicy implements NPolicy {
|
|
4
|
+
private store;
|
|
5
|
+
private policy?;
|
|
6
|
+
constructor(store: NStore, policy?: NPolicy);
|
|
7
|
+
call(event: NostrEvent, signal?: AbortSignal): Promise<NostrRelayOK>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=AuthorPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthorPolicy.d.ts","sourceRoot":"","sources":["../AuthorPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE5E,wGAAwG;AACxG,qBAAa,YAAa,YAAW,OAAO;IAC9B,OAAO,CAAC,KAAK;IAAU,OAAO,CAAC,MAAM,CAAC;gBAA9B,KAAK,EAAE,MAAM,EAAU,MAAM,CAAC,EAAE,OAAO;IAErD,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;CAgB3E"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthorPolicy = void 0;
|
|
4
|
+
/** Rejects events by authors without a kind 0, then optionally applies another policy to the kind 0. */
|
|
5
|
+
class AuthorPolicy {
|
|
6
|
+
store;
|
|
7
|
+
policy;
|
|
8
|
+
constructor(store, policy) {
|
|
9
|
+
this.store = store;
|
|
10
|
+
this.policy = policy;
|
|
11
|
+
}
|
|
12
|
+
async call(event, signal) {
|
|
13
|
+
const author = event.kind === 0 ? event : await this.store
|
|
14
|
+
.query([{ kinds: [0], authors: [event.pubkey], limit: 1 }], { signal })
|
|
15
|
+
.then(([event]) => event);
|
|
16
|
+
if (!author) {
|
|
17
|
+
return ['OK', event.id, false, 'blocked: author is missing a kind 0 event'];
|
|
18
|
+
}
|
|
19
|
+
if (this.policy) {
|
|
20
|
+
const [, , ok, reason] = await this.policy.call(author, signal);
|
|
21
|
+
return ['OK', event.id, ok, reason];
|
|
22
|
+
}
|
|
23
|
+
return ['OK', event.id, true, ''];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.AuthorPolicy = AuthorPolicy;
|
|
27
|
+
//# sourceMappingURL=AuthorPolicy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthorPolicy.js","sourceRoot":"","sources":["../AuthorPolicy.ts"],"names":[],"mappings":";;;AAEA,wGAAwG;AACxG,MAAa,YAAY;IACH;IAAuB;IAA3C,YAAoB,KAAa,EAAU,MAAgB;QAAvC,UAAK,GAAL,KAAK,CAAQ;QAAU,WAAM,GAAN,MAAM,CAAU;IAAG,CAAC;IAE/D,KAAK,CAAC,IAAI,CAAC,KAAiB,EAAE,MAAoB;QAChD,MAAM,MAAM,GAA2B,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,KAAK;aAC/E,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC;aACtE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;QAE5B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,2CAA2C,CAAC,CAAC;QAC9E,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,CAAC,EAAE,AAAD,EAAG,EAAE,EAAE,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAChE,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;QACtC,CAAC;QAED,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IACpC,CAAC;CACF;AAnBD,oCAmBC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NPolicy, NProfilePointer, NStore } from '@nostrify/types';
|
|
2
|
+
import { AuthorPolicy } from './AuthorPolicy';
|
|
3
|
+
/** Options for `DomainPolicy`. */
|
|
4
|
+
interface DomainPolicyOpts {
|
|
5
|
+
/** Custom NIP-05 lookup function. */
|
|
6
|
+
lookup?(nip05: string, signal?: AbortSignal): Promise<NProfilePointer>;
|
|
7
|
+
/** List of domains to blacklist. Reject events from users with a NIP-05 matching any of these domains. */
|
|
8
|
+
blacklist?: string[];
|
|
9
|
+
/** List of domains to whitelist. If provided, only events from users with a valid NIP-05 on the given domains will be accepted. */
|
|
10
|
+
whitelist?: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Ban events unless their author has a valid NIP-05 name. Domains can also be whitelisted or blacklisted. */
|
|
13
|
+
export declare class DomainPolicy extends AuthorPolicy implements NPolicy {
|
|
14
|
+
constructor(store: NStore, opts?: DomainPolicyOpts);
|
|
15
|
+
/** Check if a domain is blacklisted, including subdomains of blacklisted domains. */
|
|
16
|
+
private static isDomainBlacklisted;
|
|
17
|
+
/** Default NIP-05 lookup method if one isn't provided by the caller. */
|
|
18
|
+
private static lookup;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=DomainPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DomainPolicy.d.ts","sourceRoot":"","sources":["../DomainPolicy.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEnE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,kCAAkC;AAClC,UAAU,gBAAgB;IACxB,qCAAqC;IACrC,MAAM,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACvE,0GAA0G;IAC1G,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,mIAAmI;IACnI,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,8GAA8G;AAC9G,qBAAa,YAAa,SAAQ,YAAa,YAAW,OAAO;gBACnD,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB;IAmDtD,qFAAqF;IACrF,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAmBlC,wEAAwE;IACxE,OAAO,CAAC,MAAM,CAAC,MAAM;CAMtB"}
|