@nostrify/nostrify 0.46.10 → 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 +14 -0
- package/NIP98Client.test.ts +323 -0
- package/NIP98Client.ts +36 -0
- package/mod.ts +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.47.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add NIP98Client
|
|
8
|
+
|
|
9
|
+
## 0.46.11
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- export js files instead of TS files
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @nostrify/types@0.36.7
|
|
16
|
+
|
|
3
17
|
## 0.46.10
|
|
4
18
|
|
|
5
19
|
### 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.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./dist/mod.js",
|
|
6
6
|
"./ln": "./dist/ln/mod.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"@types/node": "^24.1.0",
|
|
21
21
|
"@scure/base": "^1.2.6",
|
|
22
22
|
"@std/encoding": "npm:@jsr/std__encoding@^0.224.1",
|
|
23
|
-
"@nostrify/types": "0.36.
|
|
23
|
+
"@nostrify/types": "0.36.7"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|