Skip to content

Commit d745d5b

Browse files
authored
feat: stricter type checking on tag types of events (#24)
1 parent a6e343a commit d745d5b

9 files changed

+85
-47
lines changed

core/protocol.d.ts

+19-13
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export interface NostrEvent<K extends EventKind = EventKind> {
3838
pubkey: PublicKey;
3939
created_at: Timestamp;
4040
kind: K;
41-
tags: TagFor<K>[];
42-
content: Stringified<EventContentFor<K>>;
41+
tags: [...Tags<K>, ...OptionalTag<K>[]];
42+
content: Stringified<EventContent<K>>;
4343
sig: Signature;
4444
}
4545

@@ -52,11 +52,11 @@ export type Signature = Brand<string, "EventSignature">;
5252

5353
export type EventSerializePrecursor<K extends EventKind = EventKind> = [
5454
header: 0,
55-
pubkey: PublicKey,
56-
created_at: Timestamp,
57-
kind: K,
58-
tags: TagFor<K>[],
59-
content: Stringified<EventContentFor<K>>,
55+
pubkey: NostrEvent<K>["pubkey"],
56+
created_at: NostrEvent<K>["created_at"],
57+
kind: NostrEvent<K>["kind"],
58+
tags: NostrEvent<K>["tags"],
59+
content: NostrEvent<K>["content"],
6060
];
6161

6262
// ----------------------
@@ -79,7 +79,12 @@ export type TagParams<T extends TagType> = TagRecord[T] extends [
7979
...infer P,
8080
] ? P
8181
: never;
82-
export type TagFor<K extends EventKind> = EventKindRecord[K]["Tag"];
82+
83+
export type Tags<K extends EventKind> = EventKindRecord[K] extends
84+
{ Tags: infer T extends Tag[] } ? T : [];
85+
86+
export type OptionalTag<K extends EventKind> = EventKindRecord[K] extends
87+
{ OptionalTag: infer T extends Tag } ? T | undefined : Tag | undefined;
8388

8489
// ----------------------
8590
// Communication
@@ -116,7 +121,7 @@ export type OkMessageContent<
116121
];
117122

118123
export type OkMessageBody<K extends EventKind, B extends boolean> = B extends
119-
true ? string : `${ResponsePrefixFor<K>}: ${string}`;
124+
true ? string : `${ResponsePrefix<K>}: ${string}`;
120125

121126
export type DefaultResponsePrefix =
122127
| "duplicate"
@@ -151,17 +156,18 @@ export type SubscriptionFilter<
151156
export type EventKind = keyof EventKindRecord & number;
152157

153158
export interface EventKindRecordEntry {
154-
Tag: Tag;
155159
Content: unknown;
160+
OptionalTag?: Tag;
161+
Tags?: Tag[];
156162
ResponsePrefix?: string;
157163
}
158164

159-
export type EventContent = EventContentFor<EventKind>;
165+
export type AnyEventContent = EventContent<EventKind>;
160166

161-
export type EventContentFor<K extends EventKind> = EventKindRecord[K] extends
167+
export type EventContent<K extends EventKind> = EventKindRecord[K] extends
162168
EventKindRecordEntry ? EventKindRecord[K]["Content"] : never;
163169

164-
export type ResponsePrefixFor<K extends EventKind = EventKind> =
170+
export type ResponsePrefix<K extends EventKind = EventKind> =
165171
EventKindRecord[K] extends { ResponsePrefix: infer P extends string } ? P
166172
: DefaultResponsePrefix;
167173

lib/events.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import type {
22
ClientToRelayMessage,
3-
EventContentFor,
3+
EventContent,
44
EventKind,
5-
TagFor,
5+
NostrEvent,
66
} from "../mod.ts";
77
import { Stringified } from "../core/types.ts";
88

99
export interface EventInit<K extends EventKind = EventKind> {
10-
kind: K;
11-
tags?: TagFor<K>[];
12-
content: EventContentFor<K> | Stringified<EventContentFor<K>>;
10+
kind: NostrEvent<K>["kind"];
11+
tags?: NostrEvent<K>["tags"];
12+
content: EventContent<K> | Stringified<EventContent<K>>;
1313
}
1414

1515
import type { Signer } from "./signs.ts";

lib/signs.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Brand, Stringified } from "../core/types.ts";
22
import type {
3-
EventContentFor,
3+
EventContent,
44
EventId,
55
EventKind,
66
EventSerializePrecursor,
@@ -47,8 +47,9 @@ export class Signer extends TransformStream<EventInit, NostrEvent> {
4747
created_at: Timestamp.now,
4848
tags: [],
4949
...event,
50-
content: JSON.stringify(event.content) as Stringified<EventContentFor<K>>,
51-
} satisfies UnsignedEvent<K>;
50+
content: JSON.stringify(event.content) as Stringified<EventContent<K>>,
51+
// TODO: Can we avoid this type assertion?
52+
} as UnsignedEvent<K>;
5253
const precursor = { ...unsigned, pubkey: PublicKey.from(this.nsec) };
5354
const hash = sha256(this.#encoder.encode(serialize(precursor)));
5455
return {

nips/01/protocol.d.ts

+12-14
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ declare module "../../core/protocol.d.ts" {
1919
Tag: "e" | "p" | "a" | "d";
2020
};
2121
}
22+
interface EventKindRecord {
23+
0: {
24+
Content: {
25+
name: string;
26+
about: string;
27+
picture: Url;
28+
};
29+
};
30+
1: {
31+
Content: string;
32+
};
33+
}
2234
interface TagRecord {
2335
/** Event ID */
2436
"e": [EventId, RelayUrl?];
@@ -46,18 +58,4 @@ declare module "../../core/protocol.d.ts" {
4658
EOSE: [SubscriptionId];
4759
NOTICE: [string];
4860
}
49-
interface EventKindRecord {
50-
0: {
51-
Tag: Tag;
52-
Content: {
53-
name: string;
54-
about: string;
55-
picture: Url;
56-
};
57-
};
58-
1: {
59-
Tag: Tag;
60-
Content: string;
61-
};
62-
}
6361
}

nips/02/protocol.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ declare module "../../core/protocol.d.ts" {
88
}
99
interface EventKindRecord {
1010
3: {
11-
Tag: ContactTag;
11+
OptionalTag: ContactTag;
1212
Content: "";
1313
};
1414
}

nips/02/protocol_test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it } from "../../lib/std/testing.ts";
22
import { assertType, Has } from "../../lib/std/testing.ts";
33
import { EventInit } from "../../lib/events.ts";
4-
import type { PublicKey } from "../../core/protocol.d.ts";
4+
import type { EventId, PublicKey } from "../../core/protocol.d.ts";
55
import "./protocol.d.ts";
66

77
describe("EventInit<3>", () => {
@@ -21,7 +21,7 @@ describe("EventInit<3>", () => {
2121
content: "",
2222
tags: [
2323
// @ts-expect-error: tag name should be "p"
24-
["e", "test" as PublicKey, "wss://nos.lol", "string"],
24+
["e", "test" as EventId, "wss://nos.lol", "string"],
2525
],
2626
} satisfies EventInit<3>;
2727
assertType<Has<typeof init, EventInit<3>>>(false);

nips/07/signs.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import type { Stringified } from "../../core/types.ts";
2-
import {
3-
EventContentFor,
4-
EventKind,
5-
NostrEvent,
6-
} from "../../core/protocol.d.ts";
2+
import { EventContent, EventKind, NostrEvent } from "../../core/protocol.d.ts";
73
import { EventInit } from "../../lib/events.ts";
84
import { Timestamp } from "../../lib/times.ts";
95
import { UnsignedEvent } from "./protocol.d.ts";
@@ -27,8 +23,9 @@ export class Signer extends TransformStream<EventInit, NostrEvent> {
2723
created_at: Timestamp.now,
2824
tags: [],
2925
...init,
30-
content: JSON.stringify(init.content) as Stringified<EventContentFor<K>>,
31-
} satisfies UnsignedEvent<K>;
26+
content: JSON.stringify(init.content) as Stringified<EventContent<K>>,
27+
// TODO: Can we avoid this type assertion?
28+
} as UnsignedEvent<K>;
3229
return window.nostr!.signEvent(unsigned);
3330
}
3431
}

nips/42/protocol.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ declare module "../../core/protocol.d.ts" {
88
ClientToRelayMessage: "AUTH";
99
RelayToClientMessage: "AUTH";
1010
EventKind: 22242;
11-
Tag: Tag<"relay" | "challenge">;
11+
Tag: "relay" | "challenge";
1212
};
1313
}
1414
interface RelayToClientMessageRecord {
@@ -19,7 +19,7 @@ declare module "../../core/protocol.d.ts" {
1919
}
2020
interface EventKindRecord {
2121
22242: {
22-
Tag: Tag<"relay" | "challenge">;
22+
Tags: [Tag<"relay">, Tag<"challenge">];
2323
Content: "";
2424
ResponsePrefix: "restricted";
2525
};

nips/42/protocol_test.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, it } from "../../lib/std/testing.ts";
2+
import { assertType, Has } from "../../lib/std/testing.ts";
3+
import { EventInit } from "../../lib/events.ts";
4+
import "./protocol.d.ts";
5+
6+
describe("EventInit<22242>", () => {
7+
it("can be valid", () => {
8+
const init = {
9+
kind: 22242,
10+
content: "",
11+
tags: [
12+
["relay", "wss://nos.lol", "string"],
13+
["challenge", "string"],
14+
],
15+
} satisfies EventInit<22242>;
16+
assertType<Has<typeof init, EventInit<22242>>>(true);
17+
});
18+
it("tags should not be empty", () => {
19+
const init = {
20+
kind: 22242,
21+
content: "",
22+
// @ts-expect-error tags should not be empty
23+
tags: [],
24+
} satisfies EventInit<22242>;
25+
assertType<Has<typeof init, EventInit<22242>>>(false);
26+
});
27+
it('tags should have "relay" and "challenge"', () => {
28+
const init = {
29+
kind: 22242,
30+
content: "",
31+
// @ts-expect-error tags should have "relay" and "challenge"
32+
tags: [["relay", "wss://nos.lol"]],
33+
} satisfies EventInit<22242>;
34+
assertType<Has<typeof init, EventInit<22242>>>(false);
35+
});
36+
});

0 commit comments

Comments
 (0)