@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
package/index.ts ADDED
@@ -0,0 +1,95 @@
1
+ import {
2
+ defineBundledChannelEntry,
3
+ loadBundledEntryExportSync,
4
+ } from "klaw/plugin-sdk/channel-entry-contract";
5
+ import type { KlawConfig, PluginRuntime, ResolvedNostrAccount } from "./api.js";
6
+
7
+ function createNostrProfileHttpHandler() {
8
+ return loadBundledEntryExportSync<
9
+ (params: Record<string, unknown>) => (ctx: unknown) => Promise<void> | void
10
+ >(import.meta.url, {
11
+ specifier: "./api.js",
12
+ exportName: "createNostrProfileHttpHandler",
13
+ });
14
+ }
15
+
16
+ function getNostrRuntime() {
17
+ return loadBundledEntryExportSync<() => PluginRuntime>(import.meta.url, {
18
+ specifier: "./api.js",
19
+ exportName: "getNostrRuntime",
20
+ })();
21
+ }
22
+
23
+ function resolveNostrAccount(params: { cfg: unknown; accountId: string }) {
24
+ return loadBundledEntryExportSync<
25
+ (params: { cfg: unknown; accountId: string }) => ResolvedNostrAccount
26
+ >(import.meta.url, {
27
+ specifier: "./api.js",
28
+ exportName: "resolveNostrAccount",
29
+ })(params);
30
+ }
31
+
32
+ export default defineBundledChannelEntry({
33
+ id: "nostr",
34
+ name: "Nostr",
35
+ description: "Nostr DM channel plugin via NIP-04",
36
+ importMetaUrl: import.meta.url,
37
+ plugin: {
38
+ specifier: "./channel-plugin-api.js",
39
+ exportName: "nostrPlugin",
40
+ },
41
+ runtime: {
42
+ specifier: "./api.js",
43
+ exportName: "setNostrRuntime",
44
+ },
45
+ registerFull(api) {
46
+ const httpHandler = createNostrProfileHttpHandler()({
47
+ getConfigProfile: (accountId: string) => {
48
+ const runtime = getNostrRuntime();
49
+ const cfg = runtime.config.current() as KlawConfig;
50
+ const account = resolveNostrAccount({ cfg, accountId });
51
+ return account.profile;
52
+ },
53
+ updateConfigProfile: async (_accountId: string, profile: unknown) => {
54
+ const runtime = getNostrRuntime();
55
+
56
+ await runtime.config.mutateConfigFile({
57
+ afterWrite: { mode: "auto" },
58
+ mutate: (draft) => {
59
+ const channels = (draft.channels ?? {}) as Record<string, unknown>;
60
+ const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
61
+
62
+ draft.channels = {
63
+ ...channels,
64
+ nostr: {
65
+ ...nostrConfig,
66
+ profile,
67
+ },
68
+ };
69
+ },
70
+ });
71
+ },
72
+ getAccountInfo: (accountId: string) => {
73
+ const runtime = getNostrRuntime();
74
+ const cfg = runtime.config.current() as KlawConfig;
75
+ const account = resolveNostrAccount({ cfg, accountId });
76
+ if (!account.configured || !account.publicKey) {
77
+ return null;
78
+ }
79
+ return {
80
+ pubkey: account.publicKey,
81
+ relays: account.relays,
82
+ };
83
+ },
84
+ log: api.logger,
85
+ });
86
+
87
+ api.registerHttpRoute({
88
+ path: "/api/channels/nostr",
89
+ auth: "gateway",
90
+ match: "prefix",
91
+ gatewayRuntimeScopeSurface: "trusted-operator",
92
+ handler: httpHandler,
93
+ });
94
+ },
95
+ });
package/klaw.plugin.json CHANGED
@@ -3,196 +3,13 @@
3
3
  "activation": {
4
4
  "onStartup": false
5
5
  },
6
- "channels": [
7
- "nostr"
8
- ],
6
+ "channels": ["nostr"],
9
7
  "channelEnvVars": {
10
- "nostr": [
11
- "NOSTR_PRIVATE_KEY"
12
- ]
8
+ "nostr": ["NOSTR_PRIVATE_KEY"]
13
9
  },
14
10
  "configSchema": {
15
11
  "type": "object",
16
12
  "additionalProperties": false,
17
13
  "properties": {}
18
- },
19
- "channelConfigs": {
20
- "nostr": {
21
- "schema": {
22
- "$schema": "http://json-schema.org/draft-07/schema#",
23
- "type": "object",
24
- "properties": {
25
- "name": {
26
- "type": "string"
27
- },
28
- "defaultAccount": {
29
- "type": "string"
30
- },
31
- "enabled": {
32
- "type": "boolean"
33
- },
34
- "markdown": {
35
- "type": "object",
36
- "properties": {
37
- "tables": {
38
- "type": "string",
39
- "enum": [
40
- "off",
41
- "bullets",
42
- "code",
43
- "block"
44
- ]
45
- }
46
- },
47
- "additionalProperties": false
48
- },
49
- "privateKey": {
50
- "anyOf": [
51
- {
52
- "type": "string"
53
- },
54
- {
55
- "oneOf": [
56
- {
57
- "type": "object",
58
- "properties": {
59
- "source": {
60
- "type": "string",
61
- "const": "env"
62
- },
63
- "provider": {
64
- "type": "string",
65
- "pattern": "^[a-z][a-z0-9_-]{0,63}$"
66
- },
67
- "id": {
68
- "type": "string",
69
- "pattern": "^[A-Z][A-Z0-9_]{0,127}$"
70
- }
71
- },
72
- "required": [
73
- "source",
74
- "provider",
75
- "id"
76
- ],
77
- "additionalProperties": false
78
- },
79
- {
80
- "type": "object",
81
- "properties": {
82
- "source": {
83
- "type": "string",
84
- "const": "file"
85
- },
86
- "provider": {
87
- "type": "string",
88
- "pattern": "^[a-z][a-z0-9_-]{0,63}$"
89
- },
90
- "id": {
91
- "type": "string"
92
- }
93
- },
94
- "required": [
95
- "source",
96
- "provider",
97
- "id"
98
- ],
99
- "additionalProperties": false
100
- },
101
- {
102
- "type": "object",
103
- "properties": {
104
- "source": {
105
- "type": "string",
106
- "const": "exec"
107
- },
108
- "provider": {
109
- "type": "string",
110
- "pattern": "^[a-z][a-z0-9_-]{0,63}$"
111
- },
112
- "id": {
113
- "type": "string"
114
- }
115
- },
116
- "required": [
117
- "source",
118
- "provider",
119
- "id"
120
- ],
121
- "additionalProperties": false
122
- }
123
- ]
124
- }
125
- ]
126
- },
127
- "relays": {
128
- "type": "array",
129
- "items": {
130
- "type": "string"
131
- }
132
- },
133
- "dmPolicy": {
134
- "type": "string",
135
- "enum": [
136
- "pairing",
137
- "allowlist",
138
- "open",
139
- "disabled"
140
- ]
141
- },
142
- "allowFrom": {
143
- "type": "array",
144
- "items": {
145
- "anyOf": [
146
- {
147
- "type": "string"
148
- },
149
- {
150
- "type": "number"
151
- }
152
- ]
153
- }
154
- },
155
- "profile": {
156
- "type": "object",
157
- "properties": {
158
- "name": {
159
- "type": "string",
160
- "maxLength": 256
161
- },
162
- "displayName": {
163
- "type": "string",
164
- "maxLength": 256
165
- },
166
- "about": {
167
- "type": "string",
168
- "maxLength": 2000
169
- },
170
- "picture": {
171
- "type": "string",
172
- "format": "uri"
173
- },
174
- "banner": {
175
- "type": "string",
176
- "format": "uri"
177
- },
178
- "website": {
179
- "type": "string",
180
- "format": "uri"
181
- },
182
- "nip05": {
183
- "type": "string"
184
- },
185
- "lud16": {
186
- "type": "string"
187
- }
188
- },
189
- "additionalProperties": false
190
- }
191
- },
192
- "additionalProperties": false
193
- },
194
- "label": "Nostr",
195
- "description": "Decentralized protocol; encrypted DMs via NIP-04."
196
- }
197
14
  }
198
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodelyth/nostr",
3
- "version": "2026.5.39",
3
+ "version": "2026.5.42",
4
4
  "description": "Klaw Nostr channel plugin for NIP-04 encrypted DMs",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "@kodelyth/plugin-sdk": "1.0.1",
16
- "@kodelyth/klaw": "2026.5.41"
16
+ "@kodelyth/klaw": "2026.5.42"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "@kodelyth/klaw": ">=2026.5.19"
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "klaw": {
27
27
  "extensions": [
28
- "./index.js"
28
+ "./index.ts"
29
29
  ],
30
- "setupEntry": "./setup-entry.js",
30
+ "setupEntry": "./setup-entry.ts",
31
31
  "channel": {
32
32
  "id": "nostr",
33
33
  "label": "Nostr",
package/runtime-api.ts ADDED
@@ -0,0 +1,6 @@
1
+ // Private runtime barrel for the bundled Nostr extension.
2
+ // Keep this barrel thin and aligned with the local extension surface.
3
+
4
+ export type { KlawConfig } from "klaw/plugin-sdk/config-contracts";
5
+ export { getPluginRuntimeGatewayRequestScope } from "klaw/plugin-sdk/plugin-runtime";
6
+ export type { PluginRuntime } from "klaw/plugin-sdk/runtime-store";
package/setup-api.ts ADDED
@@ -0,0 +1 @@
1
+ export { nostrSetupAdapter, nostrSetupWizard } from "./src/setup-surface.js";
package/setup-entry.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineBundledChannelSetupEntry } from "klaw/plugin-sdk/channel-entry-contract";
2
+
3
+ export default defineBundledChannelSetupEntry({
4
+ importMetaUrl: import.meta.url,
5
+ plugin: {
6
+ specifier: "./setup-plugin-api.js",
7
+ exportName: "nostrSetupPlugin",
8
+ },
9
+ });
@@ -0,0 +1,3 @@
1
+ // Keep bundled setup entry imports narrow so setup loads do not pull the
2
+ // broader Nostr runtime plugin surface.
3
+ export { nostrSetupPlugin } from "./src/channel.setup.js";
@@ -0,0 +1,11 @@
1
+ export {
2
+ buildChannelConfigSchema,
3
+ DEFAULT_ACCOUNT_ID,
4
+ formatPairingApproveHint,
5
+ type ChannelPlugin,
6
+ } from "klaw/plugin-sdk/channel-plugin-common";
7
+ export type { ChannelOutboundAdapter } from "klaw/plugin-sdk/channel-contract";
8
+ export {
9
+ collectStatusIssuesFromLastError,
10
+ createDefaultChannelRuntimeState,
11
+ } from "klaw/plugin-sdk/status-helpers";
@@ -0,0 +1,187 @@
1
+ import { createStartAccountContext } from "klaw/plugin-sdk/channel-test-helpers";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import type { PluginRuntime } from "../runtime-api.js";
4
+ import { startNostrGatewayAccount } from "./gateway.js";
5
+ import { setNostrRuntime } from "./runtime.js";
6
+ import { buildResolvedNostrAccount } from "./test-fixtures.js";
7
+
8
+ const mocks = vi.hoisted(() => ({
9
+ normalizePubkey: vi.fn((value: string) =>
10
+ value
11
+ .trim()
12
+ .replace(/^nostr:/i, "")
13
+ .toLowerCase(),
14
+ ),
15
+ startNostrBus: vi.fn(),
16
+ }));
17
+
18
+ vi.mock("./nostr-bus.js", () => ({
19
+ DEFAULT_RELAYS: ["wss://relay.example.com"],
20
+ startNostrBus: mocks.startNostrBus,
21
+ }));
22
+
23
+ vi.mock("./nostr-key-utils.js", () => ({
24
+ getPublicKeyFromPrivate: vi.fn(() => "bot-pubkey"),
25
+ normalizePubkey: mocks.normalizePubkey,
26
+ }));
27
+
28
+ function createMockBus() {
29
+ return {
30
+ sendDm: vi.fn(async () => {}),
31
+ close: vi.fn(),
32
+ getMetrics: vi.fn(() => ({ counters: {} })),
33
+ publishProfile: vi.fn(),
34
+ getProfileState: vi.fn(async () => null),
35
+ };
36
+ }
37
+
38
+ function createRuntimeHarness() {
39
+ const recordInboundSession = vi.fn(async () => {});
40
+ const dispatchReplyWithBufferedBlockDispatcher = vi.fn(async ({ dispatcherOptions }) => {
41
+ await dispatcherOptions.deliver({ text: "|a|b|" });
42
+ });
43
+ const runtime = {
44
+ channel: {
45
+ text: {
46
+ resolveMarkdownTableMode: vi.fn(() => "off"),
47
+ convertMarkdownTables: vi.fn((text: string) => `converted:${text}`),
48
+ },
49
+ commands: {
50
+ shouldComputeCommandAuthorized: vi.fn(() => true),
51
+ resolveCommandAuthorizedFromAuthorizers: vi.fn(() => true),
52
+ },
53
+ routing: {
54
+ resolveAgentRoute: vi.fn(({ accountId, peer }) => ({
55
+ agentId: "agent-nostr",
56
+ accountId,
57
+ sessionKey: `nostr:${peer.id}`,
58
+ })),
59
+ },
60
+ session: {
61
+ resolveStorePath: vi.fn(() => "/tmp/nostr-session-store"),
62
+ readSessionUpdatedAt: vi.fn(() => undefined),
63
+ recordInboundSession,
64
+ },
65
+ reply: {
66
+ formatAgentEnvelope: vi.fn(({ body }) => `envelope:${body}`),
67
+ resolveEnvelopeFormatOptions: vi.fn(() => ({ mode: "agent" })),
68
+ finalizeInboundContext: vi.fn((ctx) => ctx),
69
+ dispatchReplyWithBufferedBlockDispatcher,
70
+ },
71
+ pairing: {
72
+ readAllowFromStore: vi.fn(async () => []),
73
+ upsertPairingRequest: vi.fn(async () => ({ code: "PAIR1234", created: true })),
74
+ },
75
+ },
76
+ } as unknown as PluginRuntime;
77
+
78
+ return {
79
+ runtime,
80
+ recordInboundSession,
81
+ dispatchReplyWithBufferedBlockDispatcher,
82
+ };
83
+ }
84
+
85
+ async function startGatewayHarness(params: {
86
+ account: ReturnType<typeof buildResolvedNostrAccount>;
87
+ cfg?: Parameters<typeof createStartAccountContext>[0]["cfg"];
88
+ }) {
89
+ const harness = createRuntimeHarness();
90
+ const bus = createMockBus();
91
+ setNostrRuntime(harness.runtime);
92
+ mocks.startNostrBus.mockResolvedValueOnce(bus as never);
93
+
94
+ const cleanup = (await startNostrGatewayAccount(
95
+ createStartAccountContext({
96
+ account: params.account,
97
+ cfg: params.cfg,
98
+ }),
99
+ )) as { stop: () => void };
100
+
101
+ return { harness, bus, cleanup };
102
+ }
103
+
104
+ function mockCallArg(mock: ReturnType<typeof vi.fn>, callIndex = 0, argIndex = 0): unknown {
105
+ const call = mock.mock.calls[callIndex];
106
+ if (!call) {
107
+ throw new Error(`Expected mock call ${callIndex}`);
108
+ }
109
+ return call[argIndex];
110
+ }
111
+
112
+ describe("nostr inbound gateway path", () => {
113
+ afterEach(() => {
114
+ mocks.normalizePubkey.mockClear();
115
+ mocks.startNostrBus.mockReset();
116
+ });
117
+
118
+ it("issues a pairing reply before decrypt for unknown senders", async () => {
119
+ const { cleanup } = await startGatewayHarness({
120
+ account: buildResolvedNostrAccount({
121
+ config: { dmPolicy: "pairing", allowFrom: [] },
122
+ }),
123
+ });
124
+
125
+ const options = mockCallArg(mocks.startNostrBus) as {
126
+ authorizeSender: (params: {
127
+ senderPubkey: string;
128
+ reply: (text: string) => Promise<void>;
129
+ }) => Promise<string>;
130
+ };
131
+ const sendPairingReply = vi.fn(async (_text: string) => {});
132
+
133
+ await expect(
134
+ options.authorizeSender({
135
+ senderPubkey: "nostr:UNKNOWN-SENDER",
136
+ reply: sendPairingReply,
137
+ }),
138
+ ).resolves.toBe("pairing");
139
+ expect(sendPairingReply).toHaveBeenCalledTimes(1);
140
+ expect(mockCallArg(sendPairingReply)).toContain("Pairing code:");
141
+
142
+ cleanup.stop();
143
+ });
144
+
145
+ it("routes allowed DMs through the standard reply pipeline", async () => {
146
+ const { harness, cleanup } = await startGatewayHarness({
147
+ account: buildResolvedNostrAccount({
148
+ publicKey: "bot-pubkey",
149
+ config: { dmPolicy: "allowlist", allowFrom: ["nostr:sender-pubkey"] },
150
+ }),
151
+ cfg: {
152
+ session: { store: { type: "jsonl" } },
153
+ commands: { useAccessGroups: true },
154
+ } as never,
155
+ });
156
+
157
+ const options = mockCallArg(mocks.startNostrBus) as {
158
+ onMessage: (
159
+ senderPubkey: string,
160
+ text: string,
161
+ reply: (text: string) => Promise<void>,
162
+ meta: { eventId: string; createdAt: number },
163
+ ) => Promise<void>;
164
+ };
165
+ const sendReply = vi.fn(async (_text: string) => {});
166
+
167
+ await options.onMessage("sender-pubkey", "hello from nostr", sendReply, {
168
+ eventId: "event-123",
169
+ createdAt: 1_710_000_000,
170
+ });
171
+
172
+ expect(harness.recordInboundSession).toHaveBeenCalledTimes(1);
173
+ expect(harness.dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
174
+ const ctx = (
175
+ mockCallArg(harness.dispatchReplyWithBufferedBlockDispatcher) as {
176
+ ctx?: Record<string, unknown>;
177
+ }
178
+ ).ctx;
179
+ expect(ctx?.BodyForAgent).toBe("hello from nostr");
180
+ expect(ctx?.SenderId).toBe("sender-pubkey");
181
+ expect(ctx?.MessageSid).toBe("event-123");
182
+ expect(ctx?.CommandAuthorized).toBe(true);
183
+ expect(sendReply).toHaveBeenCalledWith("converted:|a|b|");
184
+
185
+ cleanup.stop();
186
+ });
187
+ });