@kodelyth/nostr 2026.5.39 → 2026.5.42

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 (69) hide show
  1. package/README.md +142 -0
  2. package/api.ts +10 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/dist/api.js +522 -0
  5. package/dist/channel-CnPQxTzj.js +1467 -0
  6. package/dist/channel-plugin-api.js +2 -0
  7. package/dist/config-schema-KoL8Et_9.js +63 -0
  8. package/dist/default-relays-DLwdWOTu.js +4 -0
  9. package/dist/inbound-direct-dm-runtime-CeYGU_Fo.js +2 -0
  10. package/dist/index.js +81 -0
  11. package/dist/runtime-api.js +2 -0
  12. package/dist/setup-api.js +2 -0
  13. package/dist/setup-entry.js +11 -0
  14. package/dist/setup-plugin-api.js +166 -0
  15. package/dist/setup-surface-DFlfVW6j.js +337 -0
  16. package/dist/test-api.js +2 -0
  17. package/index.ts +95 -0
  18. package/klaw.plugin.json +2 -185
  19. package/package.json +4 -4
  20. package/runtime-api.ts +6 -0
  21. package/setup-api.ts +1 -0
  22. package/setup-entry.ts +9 -0
  23. package/setup-plugin-api.ts +3 -0
  24. package/src/channel-api.ts +11 -0
  25. package/src/channel.inbound.test.ts +187 -0
  26. package/src/channel.outbound.test.ts +163 -0
  27. package/src/channel.setup.ts +234 -0
  28. package/src/channel.test.ts +526 -0
  29. package/src/channel.ts +215 -0
  30. package/src/config-schema.ts +98 -0
  31. package/src/default-relays.ts +1 -0
  32. package/src/gateway.ts +321 -0
  33. package/src/inbound-direct-dm-runtime.ts +1 -0
  34. package/src/metrics.ts +458 -0
  35. package/src/nostr-bus.fuzz.test.ts +382 -0
  36. package/src/nostr-bus.inbound.test.ts +526 -0
  37. package/src/nostr-bus.integration.test.ts +477 -0
  38. package/src/nostr-bus.test.ts +231 -0
  39. package/src/nostr-bus.ts +789 -0
  40. package/src/nostr-key-utils.ts +94 -0
  41. package/src/nostr-profile-core.ts +134 -0
  42. package/src/nostr-profile-http-runtime.ts +6 -0
  43. package/src/nostr-profile-http.test.ts +632 -0
  44. package/src/nostr-profile-http.ts +583 -0
  45. package/src/nostr-profile-import.test.ts +119 -0
  46. package/src/nostr-profile-import.ts +262 -0
  47. package/src/nostr-profile-url-safety.ts +21 -0
  48. package/src/nostr-profile.fuzz.test.ts +430 -0
  49. package/src/nostr-profile.test.ts +415 -0
  50. package/src/nostr-profile.ts +144 -0
  51. package/src/nostr-state-store.test.ts +237 -0
  52. package/src/nostr-state-store.ts +206 -0
  53. package/src/runtime.ts +9 -0
  54. package/src/seen-tracker.ts +289 -0
  55. package/src/session-route.ts +25 -0
  56. package/src/setup-surface.ts +264 -0
  57. package/src/test-fixtures.ts +45 -0
  58. package/src/types.ts +117 -0
  59. package/test/setup.ts +5 -0
  60. package/test-api.ts +1 -0
  61. package/tsconfig.json +16 -0
  62. package/api.js +0 -7
  63. package/channel-plugin-api.js +0 -7
  64. package/index.js +0 -7
  65. package/runtime-api.js +0 -7
  66. package/setup-api.js +0 -7
  67. package/setup-entry.js +0 -7
  68. package/setup-plugin-api.js +0 -7
  69. package/test-api.js +0 -7
@@ -0,0 +1,94 @@
1
+ import { getPublicKey, nip19 } from "nostr-tools";
2
+
3
+ /**
4
+ * Validate and normalize a private key (accepts hex or nsec format)
5
+ */
6
+ export function validatePrivateKey(key: string): Uint8Array {
7
+ const trimmed = key.trim();
8
+
9
+ // Handle nsec (bech32) format
10
+ if (trimmed.startsWith("nsec1")) {
11
+ const decoded = nip19.decode(trimmed);
12
+ if (decoded.type !== "nsec") {
13
+ throw new Error("Invalid nsec key: wrong type");
14
+ }
15
+ return decoded.data;
16
+ }
17
+
18
+ // Handle hex format
19
+ if (!/^[0-9a-fA-F]{64}$/.test(trimmed)) {
20
+ throw new Error("Private key must be 64 hex characters or nsec bech32 format");
21
+ }
22
+
23
+ // Convert hex string to Uint8Array
24
+ const bytes = new Uint8Array(32);
25
+ for (let i = 0; i < 32; i++) {
26
+ bytes[i] = Number.parseInt(trimmed.slice(i * 2, i * 2 + 2), 16);
27
+ }
28
+ return bytes;
29
+ }
30
+
31
+ /**
32
+ * Get public key from private key (hex or nsec format)
33
+ */
34
+ export function getPublicKeyFromPrivate(privateKey: string): string {
35
+ const sk = validatePrivateKey(privateKey);
36
+ return getPublicKey(sk);
37
+ }
38
+
39
+ /**
40
+ * Check if a string looks like a valid Nostr pubkey (hex or npub)
41
+ */
42
+ export function isValidPubkey(input: string): boolean {
43
+ if (typeof input !== "string") {
44
+ return false;
45
+ }
46
+ const trimmed = input.trim();
47
+
48
+ // npub format
49
+ if (trimmed.startsWith("npub1")) {
50
+ try {
51
+ const decoded = nip19.decode(trimmed);
52
+ return decoded.type === "npub";
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ // Hex format
59
+ return /^[0-9a-fA-F]{64}$/.test(trimmed);
60
+ }
61
+
62
+ /**
63
+ * Normalize a pubkey to hex format (accepts npub or hex)
64
+ */
65
+ export function normalizePubkey(input: string): string {
66
+ const trimmed = input.trim();
67
+
68
+ // npub format - decode to hex
69
+ if (trimmed.startsWith("npub1")) {
70
+ const decoded = nip19.decode(trimmed);
71
+ if (decoded.type !== "npub") {
72
+ throw new Error("Invalid npub key");
73
+ }
74
+ // Convert Uint8Array to hex string
75
+ return Array.from(decoded.data as unknown as Uint8Array)
76
+ .map((b) => b.toString(16).padStart(2, "0"))
77
+ .join("");
78
+ }
79
+
80
+ // Already hex - validate and return lowercase
81
+ if (!/^[0-9a-fA-F]{64}$/.test(trimmed)) {
82
+ throw new Error("Pubkey must be 64 hex characters or npub format");
83
+ }
84
+ return trimmed.toLowerCase();
85
+ }
86
+
87
+ /**
88
+ * Convert a hex pubkey to npub format
89
+ */
90
+ export function pubkeyToNpub(hexPubkey: string): string {
91
+ const normalized = normalizePubkey(hexPubkey);
92
+ // npubEncode expects a hex string, not Uint8Array
93
+ return nip19.npubEncode(normalized);
94
+ }
@@ -0,0 +1,134 @@
1
+ import { type NostrProfile, NostrProfileSchema } from "./config-schema.js";
2
+
3
+ /** NIP-01 profile content (JSON inside kind:0 event). */
4
+ export interface ProfileContent {
5
+ name?: string;
6
+ display_name?: string;
7
+ about?: string;
8
+ picture?: string;
9
+ banner?: string;
10
+ website?: string;
11
+ nip05?: string;
12
+ lud16?: string;
13
+ }
14
+
15
+ /**
16
+ * Convert our config profile schema to NIP-01 content format.
17
+ * Strips undefined fields and validates URLs.
18
+ */
19
+ export function profileToContent(profile: NostrProfile): ProfileContent {
20
+ const validated = NostrProfileSchema.parse(profile);
21
+
22
+ const content: ProfileContent = {};
23
+
24
+ if (validated.name !== undefined) {
25
+ content.name = validated.name;
26
+ }
27
+ if (validated.displayName !== undefined) {
28
+ content.display_name = validated.displayName;
29
+ }
30
+ if (validated.about !== undefined) {
31
+ content.about = validated.about;
32
+ }
33
+ if (validated.picture !== undefined) {
34
+ content.picture = validated.picture;
35
+ }
36
+ if (validated.banner !== undefined) {
37
+ content.banner = validated.banner;
38
+ }
39
+ if (validated.website !== undefined) {
40
+ content.website = validated.website;
41
+ }
42
+ if (validated.nip05 !== undefined) {
43
+ content.nip05 = validated.nip05;
44
+ }
45
+ if (validated.lud16 !== undefined) {
46
+ content.lud16 = validated.lud16;
47
+ }
48
+
49
+ return content;
50
+ }
51
+
52
+ /**
53
+ * Convert NIP-01 content format back to our config profile schema.
54
+ * Useful for importing existing profiles from relays.
55
+ */
56
+ export function contentToProfile(content: ProfileContent): NostrProfile {
57
+ const profile: NostrProfile = {};
58
+
59
+ if (content.name !== undefined) {
60
+ profile.name = content.name;
61
+ }
62
+ if (content.display_name !== undefined) {
63
+ profile.displayName = content.display_name;
64
+ }
65
+ if (content.about !== undefined) {
66
+ profile.about = content.about;
67
+ }
68
+ if (content.picture !== undefined) {
69
+ profile.picture = content.picture;
70
+ }
71
+ if (content.banner !== undefined) {
72
+ profile.banner = content.banner;
73
+ }
74
+ if (content.website !== undefined) {
75
+ profile.website = content.website;
76
+ }
77
+ if (content.nip05 !== undefined) {
78
+ profile.nip05 = content.nip05;
79
+ }
80
+ if (content.lud16 !== undefined) {
81
+ profile.lud16 = content.lud16;
82
+ }
83
+
84
+ return profile;
85
+ }
86
+
87
+ /**
88
+ * Validate a profile without throwing (returns result object).
89
+ */
90
+ export function validateProfile(profile: unknown): {
91
+ valid: boolean;
92
+ profile?: NostrProfile;
93
+ errors?: string[];
94
+ } {
95
+ const result = NostrProfileSchema.safeParse(profile);
96
+
97
+ if (result.success) {
98
+ return { valid: true, profile: result.data };
99
+ }
100
+
101
+ return {
102
+ valid: false,
103
+ errors: result.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Sanitize profile text fields to prevent XSS when displaying in UI.
109
+ * Escapes HTML special characters.
110
+ */
111
+ export function sanitizeProfileForDisplay(profile: NostrProfile): NostrProfile {
112
+ const escapeHtml = (str: string | undefined): string | undefined => {
113
+ if (str === undefined) {
114
+ return undefined;
115
+ }
116
+ return str
117
+ .replace(/&/g, "&amp;")
118
+ .replace(/</g, "&lt;")
119
+ .replace(/>/g, "&gt;")
120
+ .replace(/"/g, "&quot;")
121
+ .replace(/'/g, "&#039;");
122
+ };
123
+
124
+ return {
125
+ name: escapeHtml(profile.name),
126
+ displayName: escapeHtml(profile.displayName),
127
+ about: escapeHtml(profile.about),
128
+ picture: profile.picture,
129
+ banner: profile.banner,
130
+ website: profile.website,
131
+ nip05: escapeHtml(profile.nip05),
132
+ lud16: escapeHtml(profile.lud16),
133
+ };
134
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ readJsonBodyWithLimit,
3
+ requestBodyErrorToText,
4
+ } from "klaw/plugin-sdk/webhook-request-guards";
5
+ export { createFixedWindowRateLimiter } from "klaw/plugin-sdk/webhook-ingress";
6
+ export { getPluginRuntimeGatewayRequestScope } from "../runtime-api.js";