@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,412 +0,0 @@
1
- import { verifyEvent, getPublicKey } from "nostr-tools";
2
- import { describe, expect, it, vi, beforeEach } from "vitest";
3
- import type { NostrProfile } from "./config-schema.js";
4
- import {
5
- createProfileEvent,
6
- profileToContent,
7
- contentToProfile,
8
- validateProfile,
9
- sanitizeProfileForDisplay,
10
- type ProfileContent,
11
- } from "./nostr-profile.js";
12
- import { TEST_HEX_PRIVATE_KEY_BYTES } from "./test-fixtures.js";
13
-
14
- const TEST_PUBKEY = getPublicKey(TEST_HEX_PRIVATE_KEY_BYTES);
15
-
16
- function createTestProfileEvent(profile: NostrProfile, lastPublishedAt?: number) {
17
- return createProfileEvent(TEST_HEX_PRIVATE_KEY_BYTES, profile, lastPublishedAt);
18
- }
19
-
20
- // ============================================================================
21
- // Profile Content Conversion Tests
22
- // ============================================================================
23
-
24
- describe("profileToContent", () => {
25
- it("converts full profile to NIP-01 content format", () => {
26
- const profile: NostrProfile = {
27
- name: "testuser",
28
- displayName: "Test User",
29
- about: "A test user for unit testing",
30
- picture: "https://example.com/avatar.png",
31
- banner: "https://example.com/banner.png",
32
- website: "https://example.com",
33
- nip05: "testuser@example.com",
34
- lud16: "testuser@walletofsatoshi.com",
35
- };
36
-
37
- const content = profileToContent(profile);
38
-
39
- expect(content.name).toBe("testuser");
40
- expect(content.display_name).toBe("Test User");
41
- expect(content.about).toBe("A test user for unit testing");
42
- expect(content.picture).toBe("https://example.com/avatar.png");
43
- expect(content.banner).toBe("https://example.com/banner.png");
44
- expect(content.website).toBe("https://example.com");
45
- expect(content.nip05).toBe("testuser@example.com");
46
- expect(content.lud16).toBe("testuser@walletofsatoshi.com");
47
- });
48
-
49
- it("omits undefined fields from content", () => {
50
- const profile: NostrProfile = {
51
- name: "minimaluser",
52
- };
53
-
54
- const content = profileToContent(profile);
55
-
56
- expect(content.name).toBe("minimaluser");
57
- expect("display_name" in content).toBe(false);
58
- expect("about" in content).toBe(false);
59
- expect("picture" in content).toBe(false);
60
- });
61
-
62
- it("handles empty profile", () => {
63
- const profile: NostrProfile = {};
64
- const content = profileToContent(profile);
65
- expect(Object.keys(content)).toHaveLength(0);
66
- });
67
- });
68
-
69
- describe("contentToProfile", () => {
70
- it("converts NIP-01 content to profile format", () => {
71
- const content: ProfileContent = {
72
- name: "testuser",
73
- display_name: "Test User",
74
- about: "A test user",
75
- picture: "https://example.com/avatar.png",
76
- nip05: "test@example.com",
77
- };
78
-
79
- const profile = contentToProfile(content);
80
-
81
- expect(profile.name).toBe("testuser");
82
- expect(profile.displayName).toBe("Test User");
83
- expect(profile.about).toBe("A test user");
84
- expect(profile.picture).toBe("https://example.com/avatar.png");
85
- expect(profile.nip05).toBe("test@example.com");
86
- });
87
-
88
- it("handles empty content", () => {
89
- const content: ProfileContent = {};
90
- const profile = contentToProfile(content);
91
- expect(
92
- Object.keys(profile).filter((k) => profile[k as keyof NostrProfile] !== undefined),
93
- ).toHaveLength(0);
94
- });
95
-
96
- it("round-trips profile data", () => {
97
- const original: NostrProfile = {
98
- name: "roundtrip",
99
- displayName: "Round Trip Test",
100
- about: "Testing round-trip conversion",
101
- };
102
-
103
- const content = profileToContent(original);
104
- const restored = contentToProfile(content);
105
-
106
- expect(restored.name).toBe(original.name);
107
- expect(restored.displayName).toBe(original.displayName);
108
- expect(restored.about).toBe(original.about);
109
- });
110
- });
111
-
112
- // ============================================================================
113
- // Event Creation Tests
114
- // ============================================================================
115
-
116
- describe("createProfileEvent", () => {
117
- beforeEach(() => {
118
- vi.useFakeTimers();
119
- vi.setSystemTime(new Date("2024-01-15T12:00:00Z"));
120
- });
121
-
122
- it("creates a valid kind:0 event", () => {
123
- const profile: NostrProfile = {
124
- name: "testbot",
125
- about: "A test bot",
126
- };
127
-
128
- const event = createTestProfileEvent(profile);
129
-
130
- expect(event.kind).toBe(0);
131
- expect(event.pubkey).toBe(TEST_PUBKEY);
132
- expect(event.tags).toEqual([]);
133
- expect(event.id).toMatch(/^[0-9a-f]{64}$/);
134
- expect(event.sig).toMatch(/^[0-9a-f]{128}$/);
135
- });
136
-
137
- it("includes profile content as JSON in event content", () => {
138
- const profile: NostrProfile = {
139
- name: "jsontest",
140
- displayName: "JSON Test User",
141
- about: "Testing JSON serialization",
142
- };
143
-
144
- const event = createTestProfileEvent(profile);
145
- const parsedContent = JSON.parse(event.content) as ProfileContent;
146
-
147
- expect(parsedContent.name).toBe("jsontest");
148
- expect(parsedContent.display_name).toBe("JSON Test User");
149
- expect(parsedContent.about).toBe("Testing JSON serialization");
150
- });
151
-
152
- it("produces a verifiable signature", () => {
153
- const profile: NostrProfile = { name: "signaturetest" };
154
- const event = createTestProfileEvent(profile);
155
-
156
- expect(verifyEvent(event)).toBe(true);
157
- });
158
-
159
- it("uses current timestamp when no lastPublishedAt provided", () => {
160
- const profile: NostrProfile = { name: "timestamptest" };
161
- const event = createTestProfileEvent(profile);
162
-
163
- const expectedTimestamp = Math.floor(Date.now() / 1000);
164
- expect(event.created_at).toBe(expectedTimestamp);
165
- });
166
-
167
- it("ensures monotonic timestamp when lastPublishedAt is in the future", () => {
168
- // Current time is 2024-01-15T12:00:00Z = 1705320000
169
- const futureTimestamp = 1705320000 + 3600; // 1 hour in the future
170
- const profile: NostrProfile = { name: "monotonictest" };
171
-
172
- const event = createTestProfileEvent(profile, futureTimestamp);
173
-
174
- expect(event.created_at).toBe(futureTimestamp + 1);
175
- });
176
-
177
- it("uses current time when lastPublishedAt is in the past", () => {
178
- const pastTimestamp = 1705320000 - 3600; // 1 hour in the past
179
- const profile: NostrProfile = { name: "pasttest" };
180
-
181
- const event = createTestProfileEvent(profile, pastTimestamp);
182
-
183
- const expectedTimestamp = Math.floor(Date.now() / 1000);
184
- expect(event.created_at).toBe(expectedTimestamp);
185
- });
186
-
187
- vi.useRealTimers();
188
- });
189
-
190
- // ============================================================================
191
- // Profile Validation Tests
192
- // ============================================================================
193
-
194
- describe("validateProfile", () => {
195
- it("validates a correct profile", () => {
196
- const profile = {
197
- name: "validuser",
198
- about: "A valid user",
199
- picture: "https://example.com/pic.png",
200
- };
201
-
202
- const result = validateProfile(profile);
203
-
204
- expect(result.valid).toBe(true);
205
- expect(result.profile).toBeDefined();
206
- expect(result.errors).toBeUndefined();
207
- });
208
-
209
- it("rejects profile with invalid URL", () => {
210
- const profile = {
211
- name: "invalidurl",
212
- picture: "http://insecure.example.com/pic.png", // HTTP not HTTPS
213
- };
214
-
215
- const result = validateProfile(profile);
216
-
217
- expect(result.valid).toBe(false);
218
- expect(result.errors).toBeDefined();
219
- expect(result.errors!.some((e) => e.includes("https://"))).toBe(true);
220
- });
221
-
222
- it("rejects profile with javascript: URL", () => {
223
- const profile = {
224
- name: "xssattempt",
225
- picture: "javascript:alert('xss')",
226
- };
227
-
228
- const result = validateProfile(profile);
229
-
230
- expect(result.valid).toBe(false);
231
- });
232
-
233
- it("rejects profile with data: URL", () => {
234
- const profile = {
235
- name: "dataurl",
236
- picture: "data:image/png;base64,abc123",
237
- };
238
-
239
- const result = validateProfile(profile);
240
-
241
- expect(result.valid).toBe(false);
242
- });
243
-
244
- it("rejects name exceeding 256 characters", () => {
245
- const profile = {
246
- name: "a".repeat(257),
247
- };
248
-
249
- const result = validateProfile(profile);
250
-
251
- expect(result.valid).toBe(false);
252
- expect(result.errors!.some((e) => e.includes("256"))).toBe(true);
253
- });
254
-
255
- it("rejects about exceeding 2000 characters", () => {
256
- const profile = {
257
- about: "a".repeat(2001),
258
- };
259
-
260
- const result = validateProfile(profile);
261
-
262
- expect(result.valid).toBe(false);
263
- expect(result.errors!.some((e) => e.includes("2000"))).toBe(true);
264
- });
265
-
266
- it("accepts empty profile", () => {
267
- const result = validateProfile({});
268
- expect(result.valid).toBe(true);
269
- });
270
-
271
- it("rejects null input", () => {
272
- const result = validateProfile(null);
273
- expect(result.valid).toBe(false);
274
- });
275
-
276
- it("rejects non-object input", () => {
277
- const result = validateProfile("not an object");
278
- expect(result.valid).toBe(false);
279
- });
280
- });
281
-
282
- // ============================================================================
283
- // Sanitization Tests
284
- // ============================================================================
285
-
286
- describe("sanitizeProfileForDisplay", () => {
287
- it("escapes HTML in name field", () => {
288
- const profile: NostrProfile = {
289
- name: "<script>alert('xss')</script>",
290
- };
291
-
292
- const sanitized = sanitizeProfileForDisplay(profile);
293
-
294
- expect(sanitized.name).toBe("&lt;script&gt;alert(&#039;xss&#039;)&lt;/script&gt;");
295
- });
296
-
297
- it("escapes HTML in about field", () => {
298
- const profile: NostrProfile = {
299
- about: 'Check out <img src="x" onerror="alert(1)">',
300
- };
301
-
302
- const sanitized = sanitizeProfileForDisplay(profile);
303
-
304
- expect(sanitized.about).toBe(
305
- "Check out &lt;img src=&quot;x&quot; onerror=&quot;alert(1)&quot;&gt;",
306
- );
307
- });
308
-
309
- it("preserves URLs without modification", () => {
310
- const profile: NostrProfile = {
311
- picture: "https://example.com/pic.png",
312
- website: "https://example.com",
313
- };
314
-
315
- const sanitized = sanitizeProfileForDisplay(profile);
316
-
317
- expect(sanitized.picture).toBe("https://example.com/pic.png");
318
- expect(sanitized.website).toBe("https://example.com");
319
- });
320
-
321
- it("handles undefined fields", () => {
322
- const profile: NostrProfile = {
323
- name: "test",
324
- };
325
-
326
- const sanitized = sanitizeProfileForDisplay(profile);
327
-
328
- expect(sanitized.name).toBe("test");
329
- expect(sanitized.about).toBeUndefined();
330
- expect(sanitized.picture).toBeUndefined();
331
- });
332
-
333
- it("escapes ampersands", () => {
334
- const profile: NostrProfile = {
335
- name: "Tom & Jerry",
336
- };
337
-
338
- const sanitized = sanitizeProfileForDisplay(profile);
339
-
340
- expect(sanitized.name).toBe("Tom &amp; Jerry");
341
- });
342
-
343
- it("escapes quotes", () => {
344
- const profile: NostrProfile = {
345
- about: 'Say "hello" to everyone',
346
- };
347
-
348
- const sanitized = sanitizeProfileForDisplay(profile);
349
-
350
- expect(sanitized.about).toBe("Say &quot;hello&quot; to everyone");
351
- });
352
- });
353
-
354
- // ============================================================================
355
- // Edge Cases
356
- // ============================================================================
357
-
358
- describe("edge cases", () => {
359
- it("handles emoji in profile fields", () => {
360
- const profile: NostrProfile = {
361
- name: "🤖 Bot",
362
- about: "I am a 🤖 robot! 🎉",
363
- };
364
-
365
- const content = profileToContent(profile);
366
- expect(content.name).toBe("🤖 Bot");
367
- expect(content.about).toBe("I am a 🤖 robot! 🎉");
368
-
369
- const event = createTestProfileEvent(profile);
370
- const parsed = JSON.parse(event.content) as ProfileContent;
371
- expect(parsed.name).toBe("🤖 Bot");
372
- });
373
-
374
- it("handles unicode in profile fields", () => {
375
- const profile: NostrProfile = {
376
- name: "日本語ユーザー",
377
- about: "Привет мир! 你好世界!",
378
- };
379
-
380
- const content = profileToContent(profile);
381
- expect(content.name).toBe("日本語ユーザー");
382
-
383
- const event = createTestProfileEvent(profile);
384
- expect(verifyEvent(event)).toBe(true);
385
- });
386
-
387
- it("handles newlines in about field", () => {
388
- const profile: NostrProfile = {
389
- about: "Line 1\nLine 2\nLine 3",
390
- };
391
-
392
- const content = profileToContent(profile);
393
- expect(content.about).toBe("Line 1\nLine 2\nLine 3");
394
-
395
- const event = createTestProfileEvent(profile);
396
- const parsed = JSON.parse(event.content) as ProfileContent;
397
- expect(parsed.about).toBe("Line 1\nLine 2\nLine 3");
398
- });
399
-
400
- it("handles maximum length fields", () => {
401
- const profile: NostrProfile = {
402
- name: "a".repeat(256),
403
- about: "b".repeat(2000),
404
- };
405
-
406
- const result = validateProfile(profile);
407
- expect(result.valid).toBe(true);
408
-
409
- const event = createTestProfileEvent(profile);
410
- expect(verifyEvent(event)).toBe(true);
411
- });
412
- });
@@ -1,144 +0,0 @@
1
- /**
2
- * Nostr Profile Management (NIP-01 kind:0)
3
- *
4
- * Profile events are "replaceable" - the latest created_at wins.
5
- * This module handles profile event creation and publishing.
6
- */
7
-
8
- import { finalizeEvent, SimplePool, type Event } from "nostr-tools";
9
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
10
- import type { NostrProfile } from "./config-schema.js";
11
- import { profileToContent } from "./nostr-profile-core.js";
12
- export {
13
- contentToProfile,
14
- profileToContent,
15
- sanitizeProfileForDisplay,
16
- validateProfile,
17
- type ProfileContent,
18
- } from "./nostr-profile-core.js";
19
-
20
- // ============================================================================
21
- // Types
22
- // ============================================================================
23
-
24
- /** Result of a profile publish attempt */
25
- export interface ProfilePublishResult {
26
- /** Event ID of the published profile */
27
- eventId: string;
28
- /** Relays that successfully received the event */
29
- successes: string[];
30
- /** Relays that failed with their error messages */
31
- failures: Array<{ relay: string; error: string }>;
32
- /** Unix timestamp when the event was created */
33
- createdAt: number;
34
- }
35
-
36
- // ============================================================================
37
- // Event Creation
38
- // ============================================================================
39
-
40
- /**
41
- * Create a signed kind:0 profile event.
42
- *
43
- * @param sk - Private key as Uint8Array (32 bytes)
44
- * @param profile - Profile data to include
45
- * @param lastPublishedAt - Previous profile timestamp (for monotonic guarantee)
46
- * @returns Signed Nostr event
47
- */
48
- export function createProfileEvent(
49
- sk: Uint8Array,
50
- profile: NostrProfile,
51
- lastPublishedAt?: number,
52
- ): Event {
53
- const content = profileToContent(profile);
54
- const contentJson = JSON.stringify(content);
55
-
56
- // Ensure monotonic timestamp (new event > previous)
57
- const now = Math.floor(Date.now() / 1000);
58
- const createdAt = lastPublishedAt !== undefined ? Math.max(now, lastPublishedAt + 1) : now;
59
-
60
- const event = finalizeEvent(
61
- {
62
- kind: 0,
63
- content: contentJson,
64
- tags: [],
65
- created_at: createdAt,
66
- },
67
- sk,
68
- );
69
-
70
- return event;
71
- }
72
-
73
- // ============================================================================
74
- // Profile Publishing
75
- // ============================================================================
76
-
77
- /** Per-relay publish timeout (ms) */
78
- const RELAY_PUBLISH_TIMEOUT_MS = 5000;
79
-
80
- /**
81
- * Publish a profile event to multiple relays.
82
- *
83
- * Best-effort: publishes to all relays in parallel, reports per-relay results.
84
- * Does NOT retry automatically - caller should handle retries if needed.
85
- *
86
- * @param pool - SimplePool instance for relay connections
87
- * @param relays - Array of relay WebSocket URLs
88
- * @param event - Signed profile event (kind:0)
89
- * @returns Publish results with successes and failures
90
- */
91
- async function publishProfileEvent(
92
- pool: SimplePool,
93
- relays: string[],
94
- event: Event,
95
- ): Promise<ProfilePublishResult> {
96
- const successes: string[] = [];
97
- const failures: Array<{ relay: string; error: string }> = [];
98
-
99
- // Publish to each relay in parallel with timeout
100
- const publishPromises = relays.map(async (relay) => {
101
- try {
102
- const timeoutPromise = new Promise<never>((_, reject) => {
103
- setTimeout(() => reject(new Error("timeout")), RELAY_PUBLISH_TIMEOUT_MS);
104
- });
105
-
106
- await Promise.race([...pool.publish([relay], event), timeoutPromise]);
107
-
108
- successes.push(relay);
109
- } catch (err) {
110
- const errorMessage = formatErrorMessage(err);
111
- failures.push({ relay, error: errorMessage });
112
- }
113
- });
114
-
115
- await Promise.all(publishPromises);
116
-
117
- return {
118
- eventId: event.id,
119
- successes,
120
- failures,
121
- createdAt: event.created_at,
122
- };
123
- }
124
-
125
- /**
126
- * Create and publish a profile event in one call.
127
- *
128
- * @param pool - SimplePool instance
129
- * @param sk - Private key as Uint8Array
130
- * @param relays - Array of relay URLs
131
- * @param profile - Profile data
132
- * @param lastPublishedAt - Previous timestamp for monotonic ordering
133
- * @returns Publish results
134
- */
135
- export async function publishProfile(
136
- pool: SimplePool,
137
- sk: Uint8Array,
138
- relays: string[],
139
- profile: NostrProfile,
140
- lastPublishedAt?: number,
141
- ): Promise<ProfilePublishResult> {
142
- const event = createProfileEvent(sk, profile, lastPublishedAt);
143
- return publishProfileEvent(pool, relays, event);
144
- }