@nostrify/nostrify 0.48.2 → 0.48.3

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.
@@ -1,100 +0,0 @@
1
- import type { NostrEvent, NostrSigner } from '@nostrify/types';
2
-
3
- /**
4
- * NIP-07-compatible signer that proxies to browser extension, normalizing behavior across different implementations.
5
- *
6
- * This signer delegates all operations to the browser's `window.nostr` object,
7
- * which is typically provided by browser extensions like Alby, nos2x, etc.
8
- *
9
- * Usage:
10
- *
11
- * ```ts
12
- * const signer = new NBrowserSigner();
13
- * const pubkey = await signer.getPublicKey();
14
- * const event = await signer.signEvent({ kind: 1, content: 'Hello, world!', tags: [], created_at: 0 });
15
- * ```
16
- */
17
- export class NBrowserSigner implements NostrSigner {
18
- private get nostr(): NostrSigner {
19
- const nostr = (globalThis as { nostr?: NostrSigner }).nostr;
20
- if (!nostr) {
21
- throw new Error('Browser extension not available');
22
- }
23
- return nostr;
24
- }
25
-
26
- async getPublicKey(): Promise<string> {
27
- const pubkey = await this.nostr.getPublicKey();
28
- if (typeof pubkey !== 'string') {
29
- throw new Error(`Nostr public key retrieval failed: expected string, got ${JSON.stringify(pubkey)}`);
30
- }
31
- return pubkey;
32
- }
33
-
34
- async signEvent(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> {
35
- const signed = await this.nostr.signEvent(event);
36
- if (typeof signed !== 'object' || !signed.id || !signed.pubkey || !signed.sig) {
37
- throw new Error(
38
- `Nostr event signing failed: expected object with id, pubkey, and sig, got ${JSON.stringify(signed)}`,
39
- );
40
- }
41
- return signed;
42
- }
43
-
44
- async getRelays(): Promise<Record<string, { read: boolean; write: boolean }>> {
45
- if (!this.nostr.getRelays) {
46
- return {};
47
- }
48
- const relays = await this.nostr.getRelays();
49
- if (typeof relays !== 'object' || relays === null) {
50
- throw new Error(`Nostr getRelays failed: expected object, got ${JSON.stringify(relays)}`);
51
- }
52
- return this.nostr.getRelays();
53
- }
54
-
55
- get nip04(): NostrSigner['nip04'] {
56
- const nostr = this.nostr;
57
- if (!nostr.nip04) {
58
- return undefined;
59
- }
60
- return {
61
- encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
62
- const encrypted = await nostr.nip04!.encrypt(pubkey, plaintext);
63
- if (typeof encrypted !== 'string') {
64
- throw new Error(`NIP-04 encryption failed: expected string result, got ${JSON.stringify(encrypted)}`);
65
- }
66
- return encrypted;
67
- },
68
- decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
69
- const decrypted = await nostr.nip04!.decrypt(pubkey, ciphertext);
70
- if (typeof decrypted !== 'string') {
71
- throw new Error(`NIP-04 decryption failed: expected string result, got ${JSON.stringify(decrypted)}`);
72
- }
73
- return decrypted;
74
- },
75
- };
76
- }
77
-
78
- get nip44(): NostrSigner['nip44'] {
79
- const nostr = this.nostr;
80
- if (!nostr.nip44) {
81
- return undefined;
82
- }
83
- return {
84
- encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
85
- const encrypted = await nostr.nip44!.encrypt(pubkey, plaintext);
86
- if (typeof encrypted !== 'string') {
87
- throw new Error(`NIP-44 encryption failed: expected string result, got ${JSON.stringify(encrypted)}`);
88
- }
89
- return encrypted;
90
- },
91
- decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
92
- const decrypted = await nostr.nip44!.decrypt(pubkey, ciphertext);
93
- if (typeof decrypted !== 'string') {
94
- throw new Error(`NIP-44 decryption failed: expected string result, got ${JSON.stringify(decrypted)}`);
95
- }
96
- return decrypted;
97
- },
98
- };
99
- }
100
- }
package/dist/NCache.ts DELETED
@@ -1,73 +0,0 @@
1
- // deno-lint-ignore-file require-await
2
-
3
- import type { NostrEvent, NostrFilter, NostrRelayCOUNT, NStore } from '@nostrify/types';
4
- import { LRUCache } from 'lru-cache';
5
- import { matchFilters } from 'nostr-tools';
6
-
7
- import { NSet } from './NSet.ts';
8
-
9
- /**
10
- * Nostr LRU cache based on [`npm:lru-cache`](https://www.npmjs.com/package/lru-cache).
11
- * It implements both `NStore` and `NSet` interfaces.
12
- *
13
- * ```ts
14
- * // Accepts the options of `npm:lru-cache`:
15
- * const cache = new NCache({ max: 1000 });
16
- *
17
- * // Events can be added like a regular `Set`:
18
- * cache.add(event1);
19
- * cache.add(event2);
20
- *
21
- * // Can be queried like `NStore`:
22
- * const events = await cache.query([{ kinds: [1] }]);
23
- *
24
- * // Can be iterated like `NSet`:
25
- * for (const event of cache) {
26
- * console.log(event);
27
- * }
28
- * ```
29
- */
30
- class NCache extends NSet implements NStore {
31
- constructor(
32
- ...args: ConstructorParameters<typeof LRUCache<string, NostrEvent>>
33
- ) {
34
- super(new LRUCache<string, NostrEvent>(...args) as Map<string, NostrEvent>);
35
- }
36
-
37
- async event(event: NostrEvent): Promise<void> {
38
- this.add(event);
39
- }
40
-
41
- async query(filters: NostrFilter[]): Promise<NostrEvent[]> {
42
- const events: NostrEvent[] = [];
43
-
44
- for (const event of this) {
45
- if (matchFilters(filters, event)) {
46
- this.cache.get(event.id);
47
- events.push(event);
48
- }
49
- }
50
-
51
- return events;
52
- }
53
-
54
- async remove(filters: NostrFilter[]): Promise<void> {
55
- for (const event of this) {
56
- if (matchFilters(filters, event)) {
57
- this.delete(event);
58
- }
59
- }
60
- }
61
-
62
- async count(filters: NostrFilter[]): Promise<NostrRelayCOUNT[2]> {
63
- const events = await this.query(filters);
64
- return {
65
- count: events.length,
66
- approximate: false,
67
- };
68
- }
69
-
70
- override [Symbol.toStringTag] = 'NCache';
71
- }
72
-
73
- export { NCache };
@@ -1,188 +0,0 @@
1
- // deno-lint-ignore-file require-await
2
-
3
- import { z } from 'zod';
4
-
5
- import type { NostrConnectRequest, NostrConnectResponse, NostrEvent, NostrSigner, NRelay } from '@nostrify/types';
6
-
7
- import { NSchema as n } from './NSchema.ts';
8
-
9
- /** Options for `NConnectSigner`. */
10
- export interface NConnectSignerOpts {
11
- /** Relay to facilitate connection. */
12
- relay: NRelay;
13
- /** Remote pubkey to sign as. */
14
- pubkey: string;
15
- /** Local signer to sign the request events. */
16
- signer: NostrSigner;
17
- /** Timeout for requests. */
18
- timeout?: number;
19
- /** Encryption to use when encrypting local messages. Decryption is automatic. */
20
- encryption?: 'nip04' | 'nip44';
21
- }
22
-
23
- /** [NIP-46](https://github.com/nostr-protocol/nips/blob/master/46.md) remote signer through a relay. */
24
- export class NConnectSigner implements NostrSigner {
25
- private relay: NRelay;
26
- private pubkey: string;
27
- private signer: NostrSigner;
28
- private timeout?: number;
29
- private encryption: 'nip04' | 'nip44';
30
-
31
- constructor(
32
- { relay, pubkey, signer, timeout, encryption = 'nip44' }: NConnectSignerOpts,
33
- ) {
34
- this.relay = relay;
35
- this.pubkey = pubkey;
36
- this.signer = signer;
37
- this.timeout = timeout;
38
- this.encryption = encryption;
39
- }
40
-
41
- async getPublicKey(): Promise<string> {
42
- return this.cmd('get_public_key', []);
43
- }
44
-
45
- async signEvent(
46
- event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>,
47
- ): Promise<NostrEvent> {
48
- const result = await this.cmd('sign_event', [JSON.stringify(event)]);
49
- return n.json().pipe(n.event()).parse(result);
50
- }
51
-
52
- async getRelays(): Promise<
53
- Record<string, { read: boolean; write: boolean }>
54
- > {
55
- const result = await this.cmd('get_relays', []);
56
-
57
- return n
58
- .json()
59
- .pipe(
60
- z.record(
61
- z.string(),
62
- z.object({ read: z.boolean(), write: z.boolean() }),
63
- ),
64
- )
65
- .parse(result) as Record<string, { read: boolean; write: boolean }>; // FIXME: hack!
66
- }
67
-
68
- readonly nip04 = {
69
- encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
70
- return this.cmd('nip04_encrypt', [pubkey, plaintext]);
71
- },
72
-
73
- decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
74
- return this.cmd('nip04_decrypt', [pubkey, ciphertext]);
75
- },
76
- };
77
-
78
- readonly nip44 = {
79
- encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
80
- return this.cmd('nip44_encrypt', [pubkey, plaintext]);
81
- },
82
-
83
- decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
84
- return this.cmd('nip44_decrypt', [pubkey, ciphertext]);
85
- },
86
- };
87
-
88
- /** Send a `connect` command to the relay. It should respond with `ack`. */
89
- async connect(secret?: string): Promise<string> {
90
- const params: string[] = [this.pubkey];
91
-
92
- if (secret) {
93
- params.push(secret);
94
- }
95
-
96
- return this.cmd('connect', params);
97
- }
98
-
99
- /** Send a `ping` command to the signer. It should respond with `pong`. */
100
- async ping(): Promise<string> {
101
- return this.cmd('ping', []);
102
- }
103
-
104
- /** High-level RPC method. Returns the string result, or throws on error. */
105
- private async cmd(method: string, params: string[]): Promise<string> {
106
- const signal = typeof this.timeout === 'number' ? AbortSignal.timeout(this.timeout) : undefined;
107
-
108
- const { result, error } = await this.send(
109
- { id: crypto.randomUUID(), method, params },
110
- { signal },
111
- );
112
-
113
- if (error) {
114
- throw new Error(error);
115
- }
116
-
117
- return result;
118
- }
119
-
120
- /** Low-level send method. Deals directly with connect request/response. */
121
- private async send(
122
- request: NostrConnectRequest,
123
- opts: { signal?: AbortSignal } = {},
124
- ): Promise<NostrConnectResponse> {
125
- const { signal } = opts;
126
-
127
- const event = await this.signer.signEvent({
128
- kind: 24133,
129
- content: await this.encrypt(this.pubkey, JSON.stringify(request)),
130
- created_at: Math.floor(Date.now() / 1000),
131
- tags: [['p', this.pubkey]],
132
- });
133
-
134
- const local = await this.signer.getPublicKey();
135
-
136
- const req = this.relay.req(
137
- [{ kinds: [24133], authors: [this.pubkey], '#p': [local] }],
138
- { signal },
139
- );
140
-
141
- // Ensure the REQ is opened before sending the EVENT
142
- const promise = new Promise<NostrConnectResponse>((resolve, reject) => {
143
- (async () => {
144
- try {
145
- for await (const msg of req) {
146
- if (msg[0] === 'CLOSED') throw new Error('Subscription closed');
147
- if (msg[0] === 'EVENT') {
148
- const event = msg[2];
149
- const decrypted = await this.decrypt(this.pubkey, event.content);
150
- const response = n.json().pipe(n.connectResponse()).parse(
151
- decrypted,
152
- );
153
- if (response.id === request.id) {
154
- resolve(response);
155
- return;
156
- }
157
- }
158
- }
159
- } catch (error) {
160
- reject(error);
161
- }
162
- })();
163
- });
164
-
165
- await this.relay.event(event, { signal });
166
- return promise;
167
- }
168
-
169
- /** Local encrypt depending on settings. */
170
- private async encrypt(pubkey: string, plaintext: string): Promise<string> {
171
- switch (this.encryption) {
172
- case 'nip04':
173
- return this.signer.nip04!.encrypt(pubkey, plaintext);
174
- case 'nip44':
175
- return this.signer.nip44!.encrypt(pubkey, plaintext);
176
- }
177
- }
178
-
179
- /** Local decrypt depending on settings. */
180
- private async decrypt(pubkey: string, ciphertext: string): Promise<string> {
181
- switch (this.encryption) {
182
- case 'nip04':
183
- return this.signer.nip04!.decrypt(pubkey, ciphertext);
184
- case 'nip44':
185
- return this.signer.nip44!.decrypt(pubkey, ciphertext);
186
- }
187
- }
188
- }
package/dist/NIP05.ts DELETED
@@ -1,51 +0,0 @@
1
- import type { NProfilePointer } from "@nostrify/types";
2
-
3
- import { NSchema as n, z } from "./NSchema.ts";
4
-
5
- interface LookupOpts {
6
- fetch?: typeof fetch;
7
- signal?: AbortSignal;
8
- }
9
-
10
- export class NIP05 {
11
- /** NIP-05 value regex. */
12
- static regex(): RegExp {
13
- return /^(?:([\w.+-]+)@)?([\w.-]+)$/;
14
- }
15
-
16
- /** Nostr pubkey with relays object. */
17
- private static profilePointerSchema(): z.ZodType<NProfilePointer> {
18
- return z.object({
19
- pubkey: n.id(),
20
- relays: n.relayUrl().array().optional(),
21
- }) as z.ZodType<NProfilePointer>;
22
- }
23
-
24
- /** Resolve NIP-05 name to a profile pointer. */
25
- static async lookup(
26
- nip05: string,
27
- opts?: LookupOpts,
28
- ): Promise<NProfilePointer> {
29
- const { fetch = globalThis.fetch.bind(globalThis), signal } = opts ?? {};
30
-
31
- const match = nip05.match(NIP05.regex());
32
- if (!match) throw new Error(`NIP-05: invalid name ${nip05}`);
33
-
34
- const [_, name = "_", domain] = match;
35
-
36
- const url = new URL("/.well-known/nostr.json", `https://${domain}/`);
37
- url.searchParams.set("name", name);
38
-
39
- const response = await fetch(url, { signal });
40
- const json = await response.json();
41
-
42
- try {
43
- const pubkey = json.names[name];
44
- const relays = json.relays?.[pubkey];
45
-
46
- return NIP05.profilePointerSchema().parse({ pubkey, relays });
47
- } catch {
48
- throw new Error(`NIP-05: no match for ${nip05}`);
49
- }
50
- }
51
- }
package/dist/NIP50.ts DELETED
@@ -1,24 +0,0 @@
1
- type SearchToken = string | { key: string; value: string };
2
-
3
- /** [NIP-50](https://github.com/nostr-protocol/nips/blob/master/50.md) search functionality. */
4
- export class NIP50 {
5
- static parseInput(input: string): SearchToken[] {
6
- const regex = /(\B-\w+:[^\s"]+)|(\b\w+:[^\s"]+)|(".*?")|(\S+)/g;
7
-
8
- const tokens: SearchToken[] = [];
9
- let match: RegExpExecArray | null;
10
-
11
- while ((match = regex.exec(input)) !== null) {
12
- if (match[1] || match[2]) {
13
- const [key, ...values] = (match[1] || match[2]).split(':');
14
- tokens.push({ key, value: values.join(':') });
15
- } else if (match[3]) {
16
- tokens.push(match[3].replace(/"/g, ''));
17
- } else if (match[4]) {
18
- tokens.push(match[4]);
19
- }
20
- }
21
-
22
- return tokens;
23
- }
24
- }
package/dist/NIP98.ts DELETED
@@ -1,111 +0,0 @@
1
- import type { NostrEvent } from "@nostrify/types";
2
- import { toHex } from "@smithy/util-hex-encoding";
3
- import { verifyEvent as _verifyEvent } from "nostr-tools";
4
-
5
- import { N64 } from "./utils/N64.ts";
6
-
7
- /** [NIP-98](https://github.com/nostr-protocol/nips/blob/master/98.md) HTTP auth. */
8
- export class NIP98 {
9
- /** Generate an auth event template from a Request. */
10
- static async template(
11
- request: Request,
12
- opts?: { validatePayload?: boolean },
13
- ): Promise<Omit<NostrEvent, "id" | "pubkey" | "sig">> {
14
- const {
15
- validatePayload = ["POST", "PUT", "PATCH"].includes(request.method),
16
- } = opts ?? {};
17
- const { method, url } = request;
18
-
19
- const tags = [
20
- ["method", method],
21
- ["u", url],
22
- ];
23
-
24
- if (validatePayload) {
25
- const buffer = await request.clone().arrayBuffer();
26
- const digest = await crypto.subtle.digest("SHA-256", buffer);
27
-
28
- tags.push(["payload", toHex(new Uint8Array(digest))]);
29
- }
30
-
31
- return {
32
- kind: 27235,
33
- content: "",
34
- tags,
35
- created_at: Math.floor(Date.now() / 1000),
36
- };
37
- }
38
-
39
- /** Compare the auth event with the request, throwing a human-readable error if validation fails. */
40
- static async verify(
41
- request: Request,
42
- opts?: {
43
- maxAge?: number;
44
- validatePayload?: boolean;
45
- verifyEvent?: (event: NostrEvent) => boolean;
46
- },
47
- ): Promise<NostrEvent> {
48
- const {
49
- maxAge = 60_000,
50
- validatePayload = ["POST", "PUT", "PATCH"].includes(request.method),
51
- verifyEvent = _verifyEvent,
52
- } = opts ?? {};
53
-
54
- const header = request.headers.get("authorization");
55
- if (!header) {
56
- throw new Error("Missing Nostr authorization header");
57
- }
58
-
59
- const token = header.match(/^Nostr (.+)$/)?.[1];
60
- if (!token) {
61
- throw new Error("Missing Nostr authorization token");
62
- }
63
-
64
- let event: NostrEvent;
65
- try {
66
- event = N64.decodeEvent(token);
67
- } catch (e) {
68
- if (
69
- e instanceof TypeError &&
70
- e.message.includes("Incorrect padding on base64")
71
- ) {
72
- throw new Error("Invalid token");
73
- } else {
74
- throw e;
75
- }
76
- }
77
-
78
- if (!verifyEvent(event)) {
79
- throw new Error("Event signature is invalid");
80
- }
81
-
82
- const age = Date.now() - (event.created_at * 1_000);
83
- const u = event.tags.find(([name]) => name === "u")?.[1];
84
- const method = event.tags.find(([name]) => name === "method")?.[1];
85
- const payload = event.tags.find(([name]) => name === "payload")?.[1];
86
-
87
- if (event.kind !== 27235) {
88
- throw new Error("Event must be kind 27235");
89
- }
90
- if (u !== request.url) {
91
- throw new Error("Event URL does not match request URL");
92
- }
93
- if (method !== request.method) {
94
- throw new Error("Event method does not match HTTP request method");
95
- }
96
- if (age >= maxAge) {
97
- throw new Error("Event expired");
98
- }
99
- if (validatePayload && payload !== undefined) {
100
- const buffer = await request.clone().arrayBuffer();
101
- const digest = await crypto.subtle.digest("SHA-256", buffer);
102
- const hexed = toHex(new Uint8Array(digest));
103
-
104
- if (hexed !== payload) {
105
- throw new Error("Event payload does not match request body");
106
- }
107
- }
108
-
109
- return event;
110
- }
111
- }
@@ -1,36 +0,0 @@
1
- import { type NostrSigner } from '@nostrify/types';
2
- import { NIP98 } from './NIP98.ts';
3
- import { N64 } from './utils/N64.ts';
4
-
5
- export interface NIP98ClientOpts {
6
- signer: NostrSigner;
7
- fetch?: typeof globalThis.fetch;
8
- }
9
-
10
- /** Wraps a fetch request with NIP98 authentication */
11
- export class NIP98Client {
12
- private signer: NostrSigner;
13
- private customFetch: typeof globalThis.fetch;
14
-
15
- constructor(opts: NIP98ClientOpts) {
16
- this.signer = opts.signer;
17
- this.customFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
18
- }
19
-
20
- /** Performs a fetch request with NIP98 authentication */
21
- async fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
22
- // Normalize to a Request object
23
- const request = new Request(input, init);
24
-
25
- // Create the NIP98 token
26
- const template = await NIP98.template(request);
27
- const event = await this.signer.signEvent(template);
28
- const token = N64.encodeEvent(event);
29
-
30
- // Add the Authorization header
31
- request.headers.set('Authorization', `Nostr ${token}`);
32
-
33
- // Call the custom fetch function
34
- return this.customFetch(request);
35
- }
36
- }
package/dist/NKinds.ts DELETED
@@ -1,26 +0,0 @@
1
- export class NKinds {
2
- /** Events are **regular**, which means they're all expected to be stored by relays. */
3
- static regular(kind: number): boolean {
4
- return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind);
5
- }
6
-
7
- /** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */
8
- static replaceable(kind: number): boolean {
9
- return (10000 <= kind && kind < 20000) || [0, 3].includes(kind);
10
- }
11
-
12
- /** Events are **ephemeral**, which means they are not expected to be stored by relays. */
13
- static ephemeral(kind: number): boolean {
14
- return 20000 <= kind && kind < 30000;
15
- }
16
-
17
- /** Events are **addressable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */
18
- static addressable(kind: number): boolean {
19
- return 30000 <= kind && kind < 40000;
20
- }
21
-
22
- /** @deprecated Use `NKinds.addressable()` instead. */
23
- static parameterizedReplaceable(kind: number): boolean {
24
- return NKinds.addressable(kind);
25
- }
26
- }