@openclaw/nostr 2026.5.2 → 2026.5.3-beta.2

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.
Files changed (59) hide show
  1. package/dist/api.js +532 -0
  2. package/dist/channel-DfEqBtUh.js +1466 -0
  3. package/dist/channel-plugin-api.js +2 -0
  4. package/dist/config-schema-DIk4jlBg.js +64 -0
  5. package/dist/default-relays-DLwdWOTu.js +4 -0
  6. package/dist/inbound-direct-dm-runtime-22bZWcIW.js +2 -0
  7. package/dist/index.js +84 -0
  8. package/dist/runtime-api.js +2 -0
  9. package/dist/setup-api.js +2 -0
  10. package/dist/setup-entry.js +11 -0
  11. package/dist/setup-plugin-api.js +165 -0
  12. package/dist/setup-surface-DxAaUTyC.js +336 -0
  13. package/dist/test-api.js +2 -0
  14. package/package.json +15 -6
  15. package/api.ts +0 -10
  16. package/channel-plugin-api.ts +0 -1
  17. package/index.ts +0 -97
  18. package/runtime-api.ts +0 -6
  19. package/setup-api.ts +0 -1
  20. package/setup-entry.ts +0 -9
  21. package/setup-plugin-api.ts +0 -3
  22. package/src/channel-api.ts +0 -15
  23. package/src/channel.inbound.test.ts +0 -176
  24. package/src/channel.outbound.test.ts +0 -128
  25. package/src/channel.setup.ts +0 -231
  26. package/src/channel.test.ts +0 -519
  27. package/src/channel.ts +0 -207
  28. package/src/config-schema.ts +0 -98
  29. package/src/default-relays.ts +0 -1
  30. package/src/gateway.ts +0 -302
  31. package/src/inbound-direct-dm-runtime.ts +0 -1
  32. package/src/metrics.ts +0 -458
  33. package/src/nostr-bus.fuzz.test.ts +0 -360
  34. package/src/nostr-bus.inbound.test.ts +0 -526
  35. package/src/nostr-bus.integration.test.ts +0 -472
  36. package/src/nostr-bus.test.ts +0 -190
  37. package/src/nostr-bus.ts +0 -789
  38. package/src/nostr-key-utils.ts +0 -94
  39. package/src/nostr-profile-core.ts +0 -134
  40. package/src/nostr-profile-http-runtime.ts +0 -6
  41. package/src/nostr-profile-http.test.ts +0 -632
  42. package/src/nostr-profile-http.ts +0 -594
  43. package/src/nostr-profile-import.test.ts +0 -119
  44. package/src/nostr-profile-import.ts +0 -262
  45. package/src/nostr-profile-url-safety.ts +0 -21
  46. package/src/nostr-profile.fuzz.test.ts +0 -430
  47. package/src/nostr-profile.test.ts +0 -412
  48. package/src/nostr-profile.ts +0 -144
  49. package/src/nostr-state-store.test.ts +0 -237
  50. package/src/nostr-state-store.ts +0 -223
  51. package/src/runtime.ts +0 -9
  52. package/src/seen-tracker.ts +0 -289
  53. package/src/session-route.ts +0 -25
  54. package/src/setup-surface.ts +0 -265
  55. package/src/test-fixtures.ts +0 -45
  56. package/src/types.ts +0 -117
  57. package/test/setup.ts +0 -5
  58. package/test-api.ts +0 -1
  59. package/tsconfig.json +0 -16
@@ -1,632 +0,0 @@
1
- /**
2
- * Tests for Nostr Profile HTTP Handler
3
- */
4
-
5
- import { IncomingMessage, ServerResponse } from "node:http";
6
- import { Socket } from "node:net";
7
- import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
8
- import {
9
- clearNostrProfileRateLimitStateForTest,
10
- createNostrProfileHttpHandler,
11
- getNostrProfileRateLimitStateSizeForTest,
12
- isNostrProfileRateLimitedForTest,
13
- type NostrProfileHttpContext,
14
- } from "./nostr-profile-http.js";
15
-
16
- const runtimeScopeMock = vi.hoisted(() => vi.fn());
17
-
18
- vi.mock("./nostr-profile-http-runtime.js", async () => {
19
- const webhookIngress = await import("openclaw/plugin-sdk/webhook-ingress");
20
- const requestGuards = await import("openclaw/plugin-sdk/webhook-request-guards");
21
- return {
22
- createFixedWindowRateLimiter: webhookIngress.createFixedWindowRateLimiter,
23
- readJsonBodyWithLimit: requestGuards.readJsonBodyWithLimit,
24
- requestBodyErrorToText: requestGuards.requestBodyErrorToText,
25
- getPluginRuntimeGatewayRequestScope: runtimeScopeMock,
26
- };
27
- });
28
-
29
- // Mock the channel exports
30
- vi.mock("./channel.js", () => ({
31
- publishNostrProfile: vi.fn(),
32
- getNostrProfileState: vi.fn(),
33
- }));
34
-
35
- // Mock the import module
36
- vi.mock("./nostr-profile-import.js", () => ({
37
- importProfileFromRelays: vi.fn(),
38
- mergeProfiles: vi.fn((local, imported) => ({ ...imported, ...local })),
39
- }));
40
-
41
- import { publishNostrProfile, getNostrProfileState } from "./channel.js";
42
- import { importProfileFromRelays } from "./nostr-profile-import.js";
43
- import { TEST_HEX_PUBLIC_KEY, TEST_SETUP_RELAY_URLS } from "./test-fixtures.js";
44
-
45
- // ============================================================================
46
- // Test Helpers
47
- // ============================================================================
48
-
49
- const TEST_PROFILE_RELAY_URL = TEST_SETUP_RELAY_URLS[0];
50
-
51
- afterAll(() => {
52
- runtimeScopeMock.mockReset();
53
- });
54
-
55
- function setGatewayRuntimeScopes(scopes: readonly string[] | undefined): void {
56
- if (!scopes) {
57
- runtimeScopeMock.mockReturnValue(undefined);
58
- return;
59
- }
60
- runtimeScopeMock.mockReturnValue({
61
- client: {
62
- connect: {
63
- scopes: [...scopes],
64
- },
65
- },
66
- });
67
- }
68
-
69
- function responseChunkText(chunk: unknown): string {
70
- if (typeof chunk === "string") {
71
- return chunk;
72
- }
73
- if (Buffer.isBuffer(chunk)) {
74
- return chunk.toString();
75
- }
76
- return "";
77
- }
78
-
79
- function createMockRequest(
80
- method: string,
81
- url: string,
82
- body?: unknown,
83
- opts?: { headers?: Record<string, string>; remoteAddress?: string },
84
- ): IncomingMessage {
85
- const socket = new Socket();
86
- Object.defineProperty(socket, "remoteAddress", {
87
- value: opts?.remoteAddress ?? "127.0.0.1",
88
- configurable: true,
89
- });
90
- const req = new IncomingMessage(socket);
91
- req.method = method;
92
- req.url = url;
93
- req.headers = { host: "localhost:3000", ...opts?.headers };
94
-
95
- if (body) {
96
- const bodyStr = JSON.stringify(body);
97
- process.nextTick(() => {
98
- req.emit("data", Buffer.from(bodyStr));
99
- req.emit("end");
100
- });
101
- } else {
102
- process.nextTick(() => {
103
- req.emit("end");
104
- });
105
- }
106
-
107
- return req;
108
- }
109
-
110
- type MockResponse = {
111
- _getData: () => string;
112
- _getStatusCode: () => number;
113
- write: (chunk: unknown) => boolean;
114
- end: (chunk?: unknown) => MockResponse;
115
- statusCode: number;
116
- };
117
-
118
- function createMockResponse(): MockResponse {
119
- let data = "";
120
- let statusCode = 200;
121
- const res = Object.assign(new ServerResponse({} as IncomingMessage), {
122
- _getData: () => data,
123
- _getStatusCode: () => statusCode,
124
- }) as MockResponse;
125
-
126
- res.write = function (chunk: unknown) {
127
- data += responseChunkText(chunk);
128
- return true;
129
- };
130
-
131
- res.end = function (chunk?: unknown) {
132
- if (chunk) {
133
- data += responseChunkText(chunk);
134
- }
135
- return this;
136
- };
137
-
138
- Object.defineProperty(res, "statusCode", {
139
- get: () => statusCode,
140
- set: (code: number) => {
141
- statusCode = code;
142
- },
143
- });
144
-
145
- return res;
146
- }
147
-
148
- function createMockContext(overrides?: Partial<NostrProfileHttpContext>): NostrProfileHttpContext {
149
- return {
150
- getConfigProfile: vi.fn().mockReturnValue(undefined),
151
- updateConfigProfile: vi.fn().mockResolvedValue(undefined),
152
- getAccountInfo: vi.fn().mockReturnValue({
153
- pubkey: TEST_HEX_PUBLIC_KEY,
154
- relays: [TEST_PROFILE_RELAY_URL],
155
- }),
156
- log: {
157
- info: vi.fn(),
158
- warn: vi.fn(),
159
- error: vi.fn(),
160
- },
161
- ...overrides,
162
- };
163
- }
164
-
165
- function createProfileHttpHarness(
166
- method: string,
167
- url: string,
168
- options?: {
169
- body?: unknown;
170
- ctx?: Partial<NostrProfileHttpContext>;
171
- req?: Parameters<typeof createMockRequest>[3];
172
- },
173
- ) {
174
- const ctx = createMockContext(options?.ctx);
175
- const handler = createNostrProfileHttpHandler(ctx);
176
- const req = createMockRequest(method, url, options?.body, options?.req);
177
- const res = createMockResponse();
178
-
179
- return {
180
- ctx,
181
- req,
182
- res,
183
- run: () => handler(req, res as unknown as ServerResponse),
184
- };
185
- }
186
-
187
- function expectOkResponse(res: MockResponse) {
188
- expect(res._getStatusCode()).toBe(200);
189
- const data = JSON.parse(res._getData());
190
- expect(data.ok).toBe(true);
191
- return data;
192
- }
193
-
194
- function mockSuccessfulProfileImport() {
195
- vi.mocked(importProfileFromRelays).mockResolvedValue({
196
- ok: true,
197
- profile: {
198
- name: "imported",
199
- displayName: "Imported User",
200
- },
201
- event: {
202
- id: "evt123",
203
- pubkey: TEST_HEX_PUBLIC_KEY,
204
- created_at: 1234567890,
205
- },
206
- relaysQueried: [TEST_PROFILE_RELAY_URL],
207
- sourceRelay: TEST_PROFILE_RELAY_URL,
208
- });
209
- }
210
-
211
- async function expectAdminScopeRejected(params: {
212
- scopes: readonly string[] | undefined;
213
- method: string;
214
- url: string;
215
- body: unknown;
216
- expectOperationNotCalled: () => void;
217
- }) {
218
- setGatewayRuntimeScopes(params.scopes);
219
- const { ctx, res, run } = createProfileHttpHarness(params.method, params.url, {
220
- body: params.body,
221
- });
222
-
223
- await run();
224
-
225
- expect(res._getStatusCode()).toBe(403);
226
- const data = JSON.parse(res._getData());
227
- expect(data.error).toBe("missing scope: operator.admin");
228
- params.expectOperationNotCalled();
229
- expect(ctx.updateConfigProfile).not.toHaveBeenCalled();
230
- }
231
-
232
- // ============================================================================
233
- // Tests
234
- // ============================================================================
235
-
236
- describe("nostr-profile-http", () => {
237
- beforeEach(() => {
238
- vi.clearAllMocks();
239
- clearNostrProfileRateLimitStateForTest();
240
- setGatewayRuntimeScopes(["operator.admin"]);
241
- });
242
-
243
- describe("route matching", () => {
244
- it("returns false for non-nostr paths", async () => {
245
- const { run } = createProfileHttpHarness("GET", "/api/channels/telegram/profile");
246
- const result = await run();
247
-
248
- expect(result).toBe(false);
249
- });
250
-
251
- it("returns false for paths without accountId", async () => {
252
- const { run } = createProfileHttpHarness("GET", "/api/channels/nostr/");
253
- const result = await run();
254
-
255
- expect(result).toBe(false);
256
- });
257
-
258
- it("handles /api/channels/nostr/:accountId/profile", async () => {
259
- const { run } = createProfileHttpHarness("GET", "/api/channels/nostr/default/profile");
260
-
261
- vi.mocked(getNostrProfileState).mockResolvedValue(null);
262
-
263
- const result = await run();
264
-
265
- expect(result).toBe(true);
266
- });
267
- });
268
-
269
- describe("GET /api/channels/nostr/:accountId/profile", () => {
270
- it("returns profile and publish state", async () => {
271
- const { res, run } = createProfileHttpHarness("GET", "/api/channels/nostr/default/profile", {
272
- ctx: {
273
- getConfigProfile: vi.fn().mockReturnValue({
274
- name: "testuser",
275
- displayName: "Test User",
276
- }),
277
- },
278
- });
279
-
280
- vi.mocked(getNostrProfileState).mockResolvedValue({
281
- lastPublishedAt: 1234567890,
282
- lastPublishedEventId: "abc123",
283
- lastPublishResults: { [TEST_PROFILE_RELAY_URL]: "ok" },
284
- });
285
-
286
- await run();
287
-
288
- expect(res._getStatusCode()).toBe(200);
289
- const data = JSON.parse(res._getData());
290
- expect(data.ok).toBe(true);
291
- expect(data.profile.name).toBe("testuser");
292
- expect(data.publishState.lastPublishedAt).toBe(1234567890);
293
- });
294
- });
295
-
296
- describe("PUT /api/channels/nostr/:accountId/profile", () => {
297
- function mockPublishSuccess() {
298
- vi.mocked(publishNostrProfile).mockResolvedValue({
299
- eventId: "event123",
300
- createdAt: 1234567890,
301
- successes: [TEST_PROFILE_RELAY_URL],
302
- failures: [],
303
- });
304
- }
305
-
306
- function expectBadRequestResponse(res: ReturnType<typeof createMockResponse>) {
307
- expect(res._getStatusCode()).toBe(400);
308
- const data = JSON.parse(res._getData());
309
- expect(data.ok).toBe(false);
310
- return data;
311
- }
312
-
313
- async function expectPrivatePictureRejected(pictureUrl: string) {
314
- const { res, run } = createProfileHttpHarness("PUT", "/api/channels/nostr/default/profile", {
315
- body: {
316
- name: "hacker",
317
- picture: pictureUrl,
318
- },
319
- });
320
-
321
- await run();
322
-
323
- const data = expectBadRequestResponse(res);
324
- expect(data.error).toContain("private");
325
- }
326
-
327
- it("validates profile and publishes", async () => {
328
- const { ctx, res, run } = createProfileHttpHarness(
329
- "PUT",
330
- "/api/channels/nostr/default/profile",
331
- {
332
- body: {
333
- name: "satoshi",
334
- displayName: "Satoshi Nakamoto",
335
- about: "Creator of Bitcoin",
336
- },
337
- },
338
- );
339
-
340
- mockPublishSuccess();
341
-
342
- await run();
343
-
344
- const data = expectOkResponse(res);
345
- expect(data.eventId).toBe("event123");
346
- expect(data.successes).toContain(TEST_PROFILE_RELAY_URL);
347
- expect(data.persisted).toBe(true);
348
- expect(ctx.updateConfigProfile).toHaveBeenCalled();
349
- });
350
-
351
- it("rejects profile mutation from non-loopback remote address", async () => {
352
- const { res, run } = createProfileHttpHarness("PUT", "/api/channels/nostr/default/profile", {
353
- body: { name: "attacker" },
354
- req: { remoteAddress: "198.51.100.10" },
355
- });
356
-
357
- await run();
358
- expect(res._getStatusCode()).toBe(403);
359
- });
360
-
361
- it("rejects cross-origin profile mutation attempts", async () => {
362
- const { res, run } = createProfileHttpHarness("PUT", "/api/channels/nostr/default/profile", {
363
- body: { name: "attacker" },
364
- req: { headers: { origin: "https://evil.example" } },
365
- });
366
-
367
- await run();
368
- expect(res._getStatusCode()).toBe(403);
369
- });
370
-
371
- it("rejects profile mutation with cross-site sec-fetch-site header", async () => {
372
- const { res, run } = createProfileHttpHarness("PUT", "/api/channels/nostr/default/profile", {
373
- body: { name: "attacker" },
374
- req: { headers: { "sec-fetch-site": "cross-site" } },
375
- });
376
-
377
- await run();
378
- expect(res._getStatusCode()).toBe(403);
379
- });
380
-
381
- it("rejects profile mutation when forwarded client ip is non-loopback", async () => {
382
- const { res, run } = createProfileHttpHarness("PUT", "/api/channels/nostr/default/profile", {
383
- body: { name: "attacker" },
384
- req: { headers: { "x-forwarded-for": "203.0.113.99, 127.0.0.1" } },
385
- });
386
-
387
- await run();
388
- expect(res._getStatusCode()).toBe(403);
389
- });
390
-
391
- it("rejects profile mutation when gateway caller is missing operator.admin", async () => {
392
- await expectAdminScopeRejected({
393
- scopes: ["operator.read"],
394
- method: "PUT",
395
- url: "/api/channels/nostr/default/profile",
396
- body: { name: "attacker" },
397
- expectOperationNotCalled: () => expect(publishNostrProfile).not.toHaveBeenCalled(),
398
- });
399
- });
400
-
401
- it("rejects profile mutation when gateway scope context is missing", async () => {
402
- await expectAdminScopeRejected({
403
- scopes: undefined,
404
- method: "PUT",
405
- url: "/api/channels/nostr/default/profile",
406
- body: { name: "attacker" },
407
- expectOperationNotCalled: () => expect(publishNostrProfile).not.toHaveBeenCalled(),
408
- });
409
- });
410
-
411
- it("rejects private IP in picture URL (SSRF protection)", async () => {
412
- await expectPrivatePictureRejected("https://127.0.0.1/evil.jpg");
413
- });
414
-
415
- it("rejects ISATAP-embedded private IPv4 in picture URL", async () => {
416
- await expectPrivatePictureRejected("https://[2001:db8:1234::5efe:127.0.0.1]/evil.jpg");
417
- });
418
-
419
- it("rejects non-https URLs", async () => {
420
- const { res, run } = createProfileHttpHarness("PUT", "/api/channels/nostr/default/profile", {
421
- body: {
422
- name: "test",
423
- picture: "http://example.com/pic.jpg",
424
- },
425
- });
426
-
427
- await run();
428
-
429
- const data = expectBadRequestResponse(res);
430
- // The schema validation catches non-https URLs before SSRF check
431
- expect(data.error).toBe("Validation failed");
432
- expect(data.details).toBeDefined();
433
- expect(data.details.some((d: string) => d.includes("https"))).toBe(true);
434
- });
435
-
436
- it("does not persist if all relays fail", async () => {
437
- const { ctx, res, run } = createProfileHttpHarness(
438
- "PUT",
439
- "/api/channels/nostr/default/profile",
440
- {
441
- body: {
442
- name: "test",
443
- },
444
- },
445
- );
446
-
447
- vi.mocked(publishNostrProfile).mockResolvedValue({
448
- eventId: "event123",
449
- createdAt: 1234567890,
450
- successes: [],
451
- failures: [{ relay: TEST_PROFILE_RELAY_URL, error: "timeout" }],
452
- });
453
-
454
- await run();
455
-
456
- expect(res._getStatusCode()).toBe(200);
457
- const data = JSON.parse(res._getData());
458
- expect(data.persisted).toBe(false);
459
- expect(ctx.updateConfigProfile).not.toHaveBeenCalled();
460
- });
461
-
462
- it("enforces rate limiting", async () => {
463
- mockPublishSuccess();
464
-
465
- // Make 6 requests (limit is 5/min)
466
- for (let i = 0; i < 6; i++) {
467
- const { res, run } = createProfileHttpHarness(
468
- "PUT",
469
- "/api/channels/nostr/rate-test/profile",
470
- {
471
- body: {
472
- name: `user${i}`,
473
- },
474
- },
475
- );
476
- await run();
477
-
478
- if (i < 5) {
479
- expectOkResponse(res);
480
- } else {
481
- expect(res._getStatusCode()).toBe(429);
482
- const data = JSON.parse(res._getData());
483
- expect(data.error).toContain("Rate limit");
484
- }
485
- }
486
- });
487
-
488
- it("caps tracked rate-limit keys to prevent unbounded growth", () => {
489
- const now = 1_000_000;
490
- for (let i = 0; i < 2_500; i += 1) {
491
- isNostrProfileRateLimitedForTest(`rate-cap-${i}`, now);
492
- }
493
- expect(getNostrProfileRateLimitStateSizeForTest()).toBeLessThanOrEqual(2_048);
494
- });
495
-
496
- it("prunes stale rate-limit keys after the window elapses", () => {
497
- const now = 2_000_000;
498
- for (let i = 0; i < 100; i += 1) {
499
- isNostrProfileRateLimitedForTest(`rate-stale-${i}`, now);
500
- }
501
- expect(getNostrProfileRateLimitStateSizeForTest()).toBe(100);
502
-
503
- isNostrProfileRateLimitedForTest("fresh", now + 60_001);
504
- expect(getNostrProfileRateLimitStateSizeForTest()).toBe(1);
505
- });
506
- });
507
-
508
- describe("POST /api/channels/nostr/:accountId/profile/import", () => {
509
- function expectImportSuccessResponse(res: ReturnType<typeof createMockResponse>) {
510
- const data = expectOkResponse(res);
511
- expect(data.imported.name).toBe("imported");
512
- return data;
513
- }
514
-
515
- it("imports profile from relays", async () => {
516
- const { res, run } = createProfileHttpHarness(
517
- "POST",
518
- "/api/channels/nostr/default/profile/import",
519
- { body: {} },
520
- );
521
-
522
- mockSuccessfulProfileImport();
523
-
524
- await run();
525
-
526
- const data = expectImportSuccessResponse(res);
527
- expect(data.saved).toBe(false); // autoMerge not requested
528
- });
529
-
530
- it("rejects import mutation from non-loopback remote address", async () => {
531
- const { res, run } = createProfileHttpHarness(
532
- "POST",
533
- "/api/channels/nostr/default/profile/import",
534
- {
535
- body: {},
536
- req: { remoteAddress: "203.0.113.10" },
537
- },
538
- );
539
-
540
- await run();
541
- expect(res._getStatusCode()).toBe(403);
542
- });
543
-
544
- it("rejects cross-origin import mutation attempts", async () => {
545
- const { res, run } = createProfileHttpHarness(
546
- "POST",
547
- "/api/channels/nostr/default/profile/import",
548
- {
549
- body: {},
550
- req: { headers: { origin: "https://evil.example" } },
551
- },
552
- );
553
-
554
- await run();
555
- expect(res._getStatusCode()).toBe(403);
556
- });
557
-
558
- it("rejects import mutation when x-real-ip is non-loopback", async () => {
559
- const { res, run } = createProfileHttpHarness(
560
- "POST",
561
- "/api/channels/nostr/default/profile/import",
562
- {
563
- body: {},
564
- req: { headers: { "x-real-ip": "198.51.100.55" } },
565
- },
566
- );
567
-
568
- await run();
569
- expect(res._getStatusCode()).toBe(403);
570
- });
571
-
572
- it("rejects profile import when gateway caller is missing operator.admin", async () => {
573
- await expectAdminScopeRejected({
574
- scopes: ["operator.read"],
575
- method: "POST",
576
- url: "/api/channels/nostr/default/profile/import",
577
- body: { autoMerge: true },
578
- expectOperationNotCalled: () => expect(importProfileFromRelays).not.toHaveBeenCalled(),
579
- });
580
- });
581
-
582
- it("rejects profile import when gateway scope context is missing", async () => {
583
- await expectAdminScopeRejected({
584
- scopes: undefined,
585
- method: "POST",
586
- url: "/api/channels/nostr/default/profile/import",
587
- body: { autoMerge: true },
588
- expectOperationNotCalled: () => expect(importProfileFromRelays).not.toHaveBeenCalled(),
589
- });
590
- });
591
-
592
- it("auto-merges when requested", async () => {
593
- const { ctx, res, run } = createProfileHttpHarness(
594
- "POST",
595
- "/api/channels/nostr/default/profile/import",
596
- {
597
- body: { autoMerge: true },
598
- ctx: {
599
- getConfigProfile: vi.fn().mockReturnValue({ about: "local bio" }),
600
- },
601
- },
602
- );
603
-
604
- mockSuccessfulProfileImport();
605
-
606
- await run();
607
-
608
- const data = expectImportSuccessResponse(res);
609
- expect(data.saved).toBe(true);
610
- expect(ctx.updateConfigProfile).toHaveBeenCalled();
611
- });
612
-
613
- it("returns error when account not found", async () => {
614
- const { res, run } = createProfileHttpHarness(
615
- "POST",
616
- "/api/channels/nostr/unknown/profile/import",
617
- {
618
- body: {},
619
- ctx: {
620
- getAccountInfo: vi.fn().mockReturnValue(null),
621
- },
622
- },
623
- );
624
-
625
- await run();
626
-
627
- expect(res._getStatusCode()).toBe(404);
628
- const data = JSON.parse(res._getData());
629
- expect(data.error).toContain("not found");
630
- });
631
- });
632
- });