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