Skip to content

Commit

Permalink
feat: stricter type checking on tag types of events (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
hasundue authored Oct 17, 2023
1 parent a6e343a commit d745d5b
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 47 deletions.
32 changes: 19 additions & 13 deletions core/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export interface NostrEvent<K extends EventKind = EventKind> {
pubkey: PublicKey;
created_at: Timestamp;
kind: K;
tags: TagFor<K>[];
content: Stringified<EventContentFor<K>>;
tags: [...Tags<K>, ...OptionalTag<K>[]];
content: Stringified<EventContent<K>>;
sig: Signature;
}

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

export type EventSerializePrecursor<K extends EventKind = EventKind> = [
header: 0,
pubkey: PublicKey,
created_at: Timestamp,
kind: K,
tags: TagFor<K>[],
content: Stringified<EventContentFor<K>>,
pubkey: NostrEvent<K>["pubkey"],
created_at: NostrEvent<K>["created_at"],
kind: NostrEvent<K>["kind"],
tags: NostrEvent<K>["tags"],
content: NostrEvent<K>["content"],
];

// ----------------------
Expand All @@ -79,7 +79,12 @@ export type TagParams<T extends TagType> = TagRecord[T] extends [
...infer P,
] ? P
: never;
export type TagFor<K extends EventKind> = EventKindRecord[K]["Tag"];

export type Tags<K extends EventKind> = EventKindRecord[K] extends
{ Tags: infer T extends Tag[] } ? T : [];

export type OptionalTag<K extends EventKind> = EventKindRecord[K] extends
{ OptionalTag: infer T extends Tag } ? T | undefined : Tag | undefined;

// ----------------------
// Communication
Expand Down Expand Up @@ -116,7 +121,7 @@ export type OkMessageContent<
];

export type OkMessageBody<K extends EventKind, B extends boolean> = B extends
true ? string : `${ResponsePrefixFor<K>}: ${string}`;
true ? string : `${ResponsePrefix<K>}: ${string}`;

export type DefaultResponsePrefix =
| "duplicate"
Expand Down Expand Up @@ -151,17 +156,18 @@ export type SubscriptionFilter<
export type EventKind = keyof EventKindRecord & number;

export interface EventKindRecordEntry {
Tag: Tag;
Content: unknown;
OptionalTag?: Tag;
Tags?: Tag[];
ResponsePrefix?: string;
}

export type EventContent = EventContentFor<EventKind>;
export type AnyEventContent = EventContent<EventKind>;

export type EventContentFor<K extends EventKind> = EventKindRecord[K] extends
export type EventContent<K extends EventKind> = EventKindRecord[K] extends
EventKindRecordEntry ? EventKindRecord[K]["Content"] : never;

export type ResponsePrefixFor<K extends EventKind = EventKind> =
export type ResponsePrefix<K extends EventKind = EventKind> =
EventKindRecord[K] extends { ResponsePrefix: infer P extends string } ? P
: DefaultResponsePrefix;

Expand Down
10 changes: 5 additions & 5 deletions lib/events.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type {
ClientToRelayMessage,
EventContentFor,
EventContent,
EventKind,
TagFor,
NostrEvent,
} from "../mod.ts";
import { Stringified } from "../core/types.ts";

export interface EventInit<K extends EventKind = EventKind> {
kind: K;
tags?: TagFor<K>[];
content: EventContentFor<K> | Stringified<EventContentFor<K>>;
kind: NostrEvent<K>["kind"];
tags?: NostrEvent<K>["tags"];
content: EventContent<K> | Stringified<EventContent<K>>;
}

import type { Signer } from "./signs.ts";
Expand Down
7 changes: 4 additions & 3 deletions lib/signs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Brand, Stringified } from "../core/types.ts";
import type {
EventContentFor,
EventContent,
EventId,
EventKind,
EventSerializePrecursor,
Expand Down Expand Up @@ -47,8 +47,9 @@ export class Signer extends TransformStream<EventInit, NostrEvent> {
created_at: Timestamp.now,
tags: [],
...event,
content: JSON.stringify(event.content) as Stringified<EventContentFor<K>>,
} satisfies UnsignedEvent<K>;
content: JSON.stringify(event.content) as Stringified<EventContent<K>>,
// TODO: Can we avoid this type assertion?
} as UnsignedEvent<K>;
const precursor = { ...unsigned, pubkey: PublicKey.from(this.nsec) };
const hash = sha256(this.#encoder.encode(serialize(precursor)));
return {
Expand Down
26 changes: 12 additions & 14 deletions nips/01/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ declare module "../../core/protocol.d.ts" {
Tag: "e" | "p" | "a" | "d";
};
}
interface EventKindRecord {
0: {
Content: {
name: string;
about: string;
picture: Url;
};
};
1: {
Content: string;
};
}
interface TagRecord {
/** Event ID */
"e": [EventId, RelayUrl?];
Expand Down Expand Up @@ -46,18 +58,4 @@ declare module "../../core/protocol.d.ts" {
EOSE: [SubscriptionId];
NOTICE: [string];
}
interface EventKindRecord {
0: {
Tag: Tag;
Content: {
name: string;
about: string;
picture: Url;
};
};
1: {
Tag: Tag;
Content: string;
};
}
}
2 changes: 1 addition & 1 deletion nips/02/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ declare module "../../core/protocol.d.ts" {
}
interface EventKindRecord {
3: {
Tag: ContactTag;
OptionalTag: ContactTag;
Content: "";
};
}
Expand Down
4 changes: 2 additions & 2 deletions nips/02/protocol_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it } from "../../lib/std/testing.ts";
import { assertType, Has } from "../../lib/std/testing.ts";
import { EventInit } from "../../lib/events.ts";
import type { PublicKey } from "../../core/protocol.d.ts";
import type { EventId, PublicKey } from "../../core/protocol.d.ts";
import "./protocol.d.ts";

describe("EventInit<3>", () => {
Expand All @@ -21,7 +21,7 @@ describe("EventInit<3>", () => {
content: "",
tags: [
// @ts-expect-error: tag name should be "p"
["e", "test" as PublicKey, "wss://nos.lol", "string"],
["e", "test" as EventId, "wss://nos.lol", "string"],
],
} satisfies EventInit<3>;
assertType<Has<typeof init, EventInit<3>>>(false);
Expand Down
11 changes: 4 additions & 7 deletions nips/07/signs.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import type { Stringified } from "../../core/types.ts";
import {
EventContentFor,
EventKind,
NostrEvent,
} from "../../core/protocol.d.ts";
import { EventContent, EventKind, NostrEvent } from "../../core/protocol.d.ts";
import { EventInit } from "../../lib/events.ts";
import { Timestamp } from "../../lib/times.ts";
import { UnsignedEvent } from "./protocol.d.ts";
Expand All @@ -27,8 +23,9 @@ export class Signer extends TransformStream<EventInit, NostrEvent> {
created_at: Timestamp.now,
tags: [],
...init,
content: JSON.stringify(init.content) as Stringified<EventContentFor<K>>,
} satisfies UnsignedEvent<K>;
content: JSON.stringify(init.content) as Stringified<EventContent<K>>,
// TODO: Can we avoid this type assertion?
} as UnsignedEvent<K>;
return window.nostr!.signEvent(unsigned);
}
}
4 changes: 2 additions & 2 deletions nips/42/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ declare module "../../core/protocol.d.ts" {
ClientToRelayMessage: "AUTH";
RelayToClientMessage: "AUTH";
EventKind: 22242;
Tag: Tag<"relay" | "challenge">;
Tag: "relay" | "challenge";
};
}
interface RelayToClientMessageRecord {
Expand All @@ -19,7 +19,7 @@ declare module "../../core/protocol.d.ts" {
}
interface EventKindRecord {
22242: {
Tag: Tag<"relay" | "challenge">;
Tags: [Tag<"relay">, Tag<"challenge">];
Content: "";
ResponsePrefix: "restricted";
};
Expand Down
36 changes: 36 additions & 0 deletions nips/42/protocol_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, it } from "../../lib/std/testing.ts";
import { assertType, Has } from "../../lib/std/testing.ts";
import { EventInit } from "../../lib/events.ts";
import "./protocol.d.ts";

describe("EventInit<22242>", () => {
it("can be valid", () => {
const init = {
kind: 22242,
content: "",
tags: [
["relay", "wss://nos.lol", "string"],
["challenge", "string"],
],
} satisfies EventInit<22242>;
assertType<Has<typeof init, EventInit<22242>>>(true);
});
it("tags should not be empty", () => {
const init = {
kind: 22242,
content: "",
// @ts-expect-error tags should not be empty
tags: [],
} satisfies EventInit<22242>;
assertType<Has<typeof init, EventInit<22242>>>(false);
});
it('tags should have "relay" and "challenge"', () => {
const init = {
kind: 22242,
content: "",
// @ts-expect-error tags should have "relay" and "challenge"
tags: [["relay", "wss://nos.lol"]],
} satisfies EventInit<22242>;
assertType<Has<typeof init, EventInit<22242>>>(false);
});
});

0 comments on commit d745d5b

Please sign in to comment.