@nostrify/nostrify 0.46.11 → 0.47.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.47.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add NIP98Client
8
+
3
9
  ## 0.46.11
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,323 @@
1
+ // deno-lint-ignore-file require-await
2
+ import { test } from "node:test";
3
+ import { deepStrictEqual, ok } from "node:assert";
4
+ import { generateSecretKey } from "nostr-tools";
5
+
6
+ import { NIP98Client } from "./NIP98Client.ts";
7
+ import { NSecSigner } from "./NSecSigner.ts";
8
+ import { N64 } from "./utils/N64.ts";
9
+ import { NIP98 } from "./NIP98.ts";
10
+
11
+ await test("NIP98Client.fetch - basic GET request", async () => {
12
+ const secretKey = generateSecretKey();
13
+ const signer = new NSecSigner(secretKey);
14
+
15
+ // Mock fetch function to capture the request
16
+ let capturedRequest: Request | undefined;
17
+ const mockFetch: typeof globalThis.fetch = async (
18
+ input: string | URL | Request,
19
+ init?: RequestInit,
20
+ ): Promise<Response> => {
21
+ const request = new Request(input, init);
22
+ capturedRequest = request.clone();
23
+ return new Response("success", { status: 200 });
24
+ };
25
+
26
+ const client = new NIP98Client({ signer, fetch: mockFetch });
27
+
28
+ const response = await client.fetch("https://example.com/api");
29
+
30
+ deepStrictEqual(response.status, 200);
31
+ deepStrictEqual(await response.text(), "success");
32
+
33
+ // Verify the Authorization header was added
34
+ const authHeader = capturedRequest?.headers.get("Authorization");
35
+ ok(authHeader && authHeader.includes("Nostr "));
36
+
37
+ // Verify the token can be decoded and is valid
38
+ const token = authHeader!.replace("Nostr ", "");
39
+ const event = N64.decodeEvent(token);
40
+
41
+ deepStrictEqual(event.kind, 27235);
42
+ deepStrictEqual(event.pubkey, await signer.getPublicKey());
43
+
44
+ // Verify the event tags contain the correct method and URL
45
+ const methodTag = event.tags.find(([name]) => name === "method");
46
+ const urlTag = event.tags.find(([name]) => name === "u");
47
+
48
+ deepStrictEqual(methodTag?.[1], "GET");
49
+ deepStrictEqual(urlTag?.[1], "https://example.com/api");
50
+ });
51
+
52
+ await test("NIP98Client.fetch - POST request with body", async () => {
53
+ const secretKey = generateSecretKey();
54
+ const signer = new NSecSigner(secretKey);
55
+
56
+ let capturedRequest: Request | undefined;
57
+ const mockFetch: typeof globalThis.fetch = async (
58
+ input: string | URL | Request,
59
+ init?: RequestInit,
60
+ ): Promise<Response> => {
61
+ const request = new Request(input, init);
62
+ capturedRequest = request.clone();
63
+ return new Response("created", { status: 201 });
64
+ };
65
+
66
+ const client = new NIP98Client({ signer, fetch: mockFetch });
67
+
68
+ const requestBody = JSON.stringify({ message: "Hello, world!" });
69
+ const response = await client.fetch("https://example.com/api", {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: requestBody,
73
+ });
74
+
75
+ deepStrictEqual(response.status, 201);
76
+ deepStrictEqual(await response.text(), "created");
77
+
78
+ // Verify the Authorization header was added
79
+ const authHeader = capturedRequest?.headers.get("Authorization");
80
+ ok(authHeader && authHeader.includes("Nostr "));
81
+
82
+ // Verify the token contains payload hash for POST request
83
+ const token = authHeader!.replace("Nostr ", "");
84
+ const event = N64.decodeEvent(token);
85
+
86
+ deepStrictEqual(event.kind, 27235);
87
+
88
+ const methodTag = event.tags.find(([name]) => name === "method");
89
+ const urlTag = event.tags.find(([name]) => name === "u");
90
+ const payloadTag = event.tags.find(([name]) => name === "payload");
91
+
92
+ deepStrictEqual(methodTag?.[1], "POST");
93
+ deepStrictEqual(urlTag?.[1], "https://example.com/api");
94
+ deepStrictEqual(typeof payloadTag?.[1], "string"); // Should have payload hash
95
+ });
96
+
97
+ await test("NIP98Client.fetch - with Request object input", async () => {
98
+ const secretKey = generateSecretKey();
99
+ const signer = new NSecSigner(secretKey);
100
+
101
+ let capturedRequest: Request | undefined;
102
+ const mockFetch: typeof globalThis.fetch = async (
103
+ input: string | URL | Request,
104
+ init?: RequestInit,
105
+ ): Promise<Response> => {
106
+ const request = new Request(input, init);
107
+ capturedRequest = request.clone();
108
+ return new Response("ok", { status: 200 });
109
+ };
110
+
111
+ const client = new NIP98Client({ signer, fetch: mockFetch });
112
+
113
+ const originalRequest = new Request("https://example.com/test", {
114
+ method: "PUT",
115
+ headers: { "Custom-Header": "custom-value" },
116
+ body: "test data",
117
+ });
118
+
119
+ const response = await client.fetch(originalRequest);
120
+
121
+ deepStrictEqual(response.status, 200);
122
+
123
+ // Verify original headers are preserved
124
+ deepStrictEqual(
125
+ capturedRequest?.headers.get("Custom-Header"),
126
+ "custom-value",
127
+ );
128
+
129
+ // Verify Authorization header was added
130
+ const authHeader = capturedRequest?.headers.get("Authorization");
131
+ ok(authHeader && authHeader.includes("Nostr "));
132
+
133
+ // Verify the event details
134
+ const token = authHeader!.replace("Nostr ", "");
135
+ const event = N64.decodeEvent(token);
136
+
137
+ const methodTag = event.tags.find(([name]) => name === "method");
138
+ const urlTag = event.tags.find(([name]) => name === "u");
139
+
140
+ deepStrictEqual(methodTag?.[1], "PUT");
141
+ deepStrictEqual(urlTag?.[1], "https://example.com/test");
142
+ });
143
+
144
+ await test("NIP98Client.fetch - with URL object input", async () => {
145
+ const secretKey = generateSecretKey();
146
+ const signer = new NSecSigner(secretKey);
147
+
148
+ let capturedRequest: Request | undefined;
149
+ const mockFetch: typeof globalThis.fetch = async (
150
+ input: string | URL | Request,
151
+ init?: RequestInit,
152
+ ): Promise<Response> => {
153
+ const request = new Request(input, init);
154
+ capturedRequest = request.clone();
155
+ return new Response("ok", { status: 200 });
156
+ };
157
+
158
+ const client = new NIP98Client({ signer, fetch: mockFetch });
159
+
160
+ const url = new URL("https://example.com/url-test");
161
+ const response = await client.fetch(url, { method: "DELETE" });
162
+
163
+ deepStrictEqual(response.status, 200);
164
+
165
+ const authHeader = capturedRequest?.headers.get("Authorization");
166
+ const token = authHeader!.replace("Nostr ", "");
167
+ const event = N64.decodeEvent(token);
168
+
169
+ const methodTag = event.tags.find(([name]) => name === "method");
170
+ const urlTag = event.tags.find(([name]) => name === "u");
171
+
172
+ deepStrictEqual(methodTag?.[1], "DELETE");
173
+ deepStrictEqual(urlTag?.[1], "https://example.com/url-test");
174
+ });
175
+
176
+ await test("NIP98Client.fetch - uses default fetch when not provided", async () => {
177
+ const secretKey = generateSecretKey();
178
+ const signer = new NSecSigner(secretKey);
179
+
180
+ // Mock the global fetch to verify it's called
181
+ const originalFetch = globalThis.fetch;
182
+ let fetchCalled = false;
183
+
184
+ globalThis.fetch = (async (
185
+ input: string | URL | Request,
186
+ init?: RequestInit,
187
+ ): Promise<Response> => {
188
+ fetchCalled = true;
189
+ const request = new Request(input, init);
190
+
191
+ // Verify the Authorization header is present
192
+ const authHeader = request.headers.get("Authorization");
193
+ ok(authHeader && authHeader.includes("Nostr "));
194
+
195
+ return new Response("default fetch used", { status: 200 });
196
+ }) as typeof globalThis.fetch;
197
+
198
+ try {
199
+ const client = new NIP98Client({ signer });
200
+ const response = await client.fetch("https://example.com/default");
201
+
202
+ deepStrictEqual(fetchCalled, true);
203
+ deepStrictEqual(response.status, 200);
204
+ deepStrictEqual(await response.text(), "default fetch used");
205
+ } finally {
206
+ // Restore original fetch
207
+ globalThis.fetch = originalFetch;
208
+ }
209
+ });
210
+
211
+ await test("NIP98Client.fetch - preserves existing headers", async () => {
212
+ const secretKey = generateSecretKey();
213
+ const signer = new NSecSigner(secretKey);
214
+
215
+ let capturedRequest: Request | undefined;
216
+ const mockFetch: typeof globalThis.fetch = async (
217
+ input: string | URL | Request,
218
+ init?: RequestInit,
219
+ ): Promise<Response> => {
220
+ const request = new Request(input, init);
221
+ capturedRequest = request.clone();
222
+ return new Response("ok", { status: 200 });
223
+ };
224
+
225
+ const client = new NIP98Client({ signer, fetch: mockFetch });
226
+
227
+ await client.fetch("https://example.com/headers", {
228
+ headers: {
229
+ "User-Agent": "test-agent",
230
+ "Accept": "application/json",
231
+ "X-Custom": "custom-value",
232
+ },
233
+ });
234
+
235
+ // Verify all original headers are preserved
236
+ deepStrictEqual(capturedRequest?.headers.get("User-Agent"), "test-agent");
237
+ deepStrictEqual(capturedRequest?.headers.get("Accept"), "application/json");
238
+ deepStrictEqual(capturedRequest?.headers.get("X-Custom"), "custom-value");
239
+
240
+ // Verify Authorization header was added
241
+ ok(capturedRequest?.headers.get("Authorization")?.includes("Nostr "));
242
+ });
243
+
244
+ await test("NIP98Client.fetch - event can be verified with NIP98.verify", async () => {
245
+ const secretKey = generateSecretKey();
246
+ const signer = new NSecSigner(secretKey);
247
+
248
+ let capturedRequest: Request | undefined;
249
+ const mockFetch: typeof globalThis.fetch = async (
250
+ input: string | URL | Request,
251
+ init?: RequestInit,
252
+ ): Promise<Response> => {
253
+ const request = new Request(input, init);
254
+ capturedRequest = request.clone();
255
+ return new Response("verified", { status: 200 });
256
+ };
257
+
258
+ const client = new NIP98Client({ signer, fetch: mockFetch });
259
+
260
+ await client.fetch("https://example.com/verify", {
261
+ method: "POST",
262
+ body: "test payload",
263
+ });
264
+
265
+ // Verify the request can be verified using NIP98.verify
266
+ const verifiedEvent = await NIP98.verify(capturedRequest!);
267
+
268
+ deepStrictEqual(verifiedEvent.kind, 27235);
269
+ deepStrictEqual(verifiedEvent.pubkey, await signer.getPublicKey());
270
+
271
+ const methodTag = verifiedEvent.tags.find(([name]) => name === "method");
272
+ const urlTag = verifiedEvent.tags.find(([name]) => name === "u");
273
+
274
+ deepStrictEqual(methodTag?.[1], "POST");
275
+ deepStrictEqual(urlTag?.[1], "https://example.com/verify");
276
+ });
277
+
278
+ await test("NIP98Client.fetch - handles different HTTP methods", async () => {
279
+ const secretKey = generateSecretKey();
280
+ const signer = new NSecSigner(secretKey);
281
+
282
+ const methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
283
+
284
+ for (const method of methods) {
285
+ let capturedRequest: Request | undefined;
286
+ const mockFetch: typeof globalThis.fetch = async (
287
+ input: string | URL | Request,
288
+ init?: RequestInit,
289
+ ): Promise<Response> => {
290
+ const request = new Request(input, init);
291
+ capturedRequest = request.clone();
292
+ return new Response("ok", { status: 200 });
293
+ };
294
+
295
+ const client = new NIP98Client({ signer, fetch: mockFetch });
296
+
297
+ await client.fetch(`https://example.com/${method.toLowerCase()}`, {
298
+ method,
299
+ body: ["POST", "PUT", "PATCH"].includes(method) ? "test body" : undefined,
300
+ });
301
+
302
+ const authHeader = capturedRequest?.headers.get("Authorization");
303
+ const token = authHeader!.replace("Nostr ", "");
304
+ const event = N64.decodeEvent(token);
305
+
306
+ const methodTag = event.tags.find(([name]) => name === "method");
307
+ deepStrictEqual(
308
+ methodTag?.[1],
309
+ method,
310
+ `Method tag should match for ${method}`,
311
+ );
312
+
313
+ // Check if payload tag is present for methods that should have it
314
+ const payloadTag = event.tags.find(([name]) => name === "payload");
315
+ if (["POST", "PUT", "PATCH"].includes(method)) {
316
+ deepStrictEqual(
317
+ typeof payloadTag?.[1],
318
+ "string",
319
+ `Payload tag should be present for ${method}`,
320
+ );
321
+ }
322
+ }
323
+ });
package/NIP98Client.ts ADDED
@@ -0,0 +1,36 @@
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/mod.ts CHANGED
@@ -5,6 +5,7 @@ export { NConnectSigner, type NConnectSignerOpts } from './NConnectSigner.ts';
5
5
  export { NIP05 } from './NIP05.ts';
6
6
  export { NIP50 } from './NIP50.ts';
7
7
  export { NIP98 } from './NIP98.ts';
8
+ export { NIP98Client, type NIP98ClientOpts } from './NIP98Client.ts';
8
9
  export { NKinds } from './NKinds.ts';
9
10
  export { NPool, type NPoolOpts } from './NPool.ts';
10
11
  export { NRelay1, type NRelay1Opts } from './NRelay1.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nostrify/nostrify",
3
- "version": "0.46.11",
3
+ "version": "0.47.0",
4
4
  "exports": {
5
5
  ".": "./dist/mod.js",
6
6
  "./ln": "./dist/ln/mod.js",