@kodelyth/nostr 2026.5.42 → 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 (47) hide show
  1. package/klaw.plugin.json +185 -2
  2. package/package.json +19 -6
  3. package/api.ts +0 -10
  4. package/channel-plugin-api.ts +0 -1
  5. package/index.ts +0 -95
  6. package/runtime-api.ts +0 -6
  7. package/setup-api.ts +0 -1
  8. package/setup-entry.ts +0 -9
  9. package/setup-plugin-api.ts +0 -3
  10. package/src/channel-api.ts +0 -11
  11. package/src/channel.inbound.test.ts +0 -187
  12. package/src/channel.outbound.test.ts +0 -163
  13. package/src/channel.setup.ts +0 -234
  14. package/src/channel.test.ts +0 -526
  15. package/src/channel.ts +0 -215
  16. package/src/config-schema.ts +0 -98
  17. package/src/default-relays.ts +0 -1
  18. package/src/gateway.ts +0 -321
  19. package/src/inbound-direct-dm-runtime.ts +0 -1
  20. package/src/metrics.ts +0 -458
  21. package/src/nostr-bus.fuzz.test.ts +0 -382
  22. package/src/nostr-bus.inbound.test.ts +0 -526
  23. package/src/nostr-bus.integration.test.ts +0 -477
  24. package/src/nostr-bus.test.ts +0 -231
  25. package/src/nostr-bus.ts +0 -789
  26. package/src/nostr-key-utils.ts +0 -94
  27. package/src/nostr-profile-core.ts +0 -134
  28. package/src/nostr-profile-http-runtime.ts +0 -6
  29. package/src/nostr-profile-http.test.ts +0 -632
  30. package/src/nostr-profile-http.ts +0 -583
  31. package/src/nostr-profile-import.test.ts +0 -119
  32. package/src/nostr-profile-import.ts +0 -262
  33. package/src/nostr-profile-url-safety.ts +0 -21
  34. package/src/nostr-profile.fuzz.test.ts +0 -430
  35. package/src/nostr-profile.test.ts +0 -415
  36. package/src/nostr-profile.ts +0 -144
  37. package/src/nostr-state-store.test.ts +0 -237
  38. package/src/nostr-state-store.ts +0 -206
  39. package/src/runtime.ts +0 -9
  40. package/src/seen-tracker.ts +0 -289
  41. package/src/session-route.ts +0 -25
  42. package/src/setup-surface.ts +0 -264
  43. package/src/test-fixtures.ts +0 -45
  44. package/src/types.ts +0 -117
  45. package/test/setup.ts +0 -5
  46. package/test-api.ts +0 -1
  47. package/tsconfig.json +0 -16
package/klaw.plugin.json CHANGED
@@ -3,13 +3,196 @@
3
3
  "activation": {
4
4
  "onStartup": false
5
5
  },
6
- "channels": ["nostr"],
6
+ "channels": [
7
+ "nostr"
8
+ ],
7
9
  "channelEnvVars": {
8
- "nostr": ["NOSTR_PRIVATE_KEY"]
10
+ "nostr": [
11
+ "NOSTR_PRIVATE_KEY"
12
+ ]
9
13
  },
10
14
  "configSchema": {
11
15
  "type": "object",
12
16
  "additionalProperties": false,
13
17
  "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
+ }
14
197
  }
15
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodelyth/nostr",
3
- "version": "2026.5.42",
3
+ "version": "2026.6.2",
4
4
  "description": "Klaw Nostr channel plugin for NIP-04 encrypted DMs",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,15 +12,19 @@
12
12
  "zod": "4.4.3"
13
13
  },
14
14
  "devDependencies": {
15
- "@kodelyth/plugin-sdk": "1.0.1",
16
- "@kodelyth/klaw": "2026.5.42"
15
+ "@kodelyth/plugin-sdk": "workspace:*",
16
+ "@kodelyth/klaw": "workspace:*"
17
17
  },
18
18
  "peerDependencies": {
19
- "@kodelyth/klaw": ">=2026.5.19"
19
+ "@kodelyth/klaw": ">=2026.5.19",
20
+ "klaw": ">=2026.5.39"
20
21
  },
21
22
  "peerDependenciesMeta": {
22
23
  "@kodelyth/klaw": {
23
24
  "optional": true
25
+ },
26
+ "klaw": {
27
+ "optional": true
24
28
  }
25
29
  },
26
30
  "klaw": {
@@ -62,6 +66,15 @@
62
66
  "release": {
63
67
  "publishToClawHub": true,
64
68
  "publishToNpm": true
65
- }
66
- }
69
+ },
70
+ "runtimeExtensions": [
71
+ "./dist/index.js"
72
+ ],
73
+ "runtimeSetupEntry": "./dist/setup-entry.js"
74
+ },
75
+ "files": [
76
+ "dist/**",
77
+ "klaw.plugin.json",
78
+ "README.md"
79
+ ]
67
80
  }
package/api.ts DELETED
@@ -1,10 +0,0 @@
1
- export {
2
- getPluginRuntimeGatewayRequestScope,
3
- type KlawConfig,
4
- type PluginRuntime,
5
- } from "./runtime-api.js";
6
- export { nostrPlugin } from "./src/channel.js";
7
- export { createNostrProfileHttpHandler } from "./src/nostr-profile-http.js";
8
- export { getNostrRuntime, setNostrRuntime } from "./src/runtime.js";
9
- export { resolveNostrAccount } from "./src/types.js";
10
- export type { ResolvedNostrAccount } from "./src/types.js";
@@ -1 +0,0 @@
1
- export { nostrPlugin } from "./src/channel.js";
package/index.ts DELETED
@@ -1,95 +0,0 @@
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/runtime-api.ts DELETED
@@ -1,6 +0,0 @@
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 DELETED
@@ -1 +0,0 @@
1
- export { nostrSetupAdapter, nostrSetupWizard } from "./src/setup-surface.js";
package/setup-entry.ts DELETED
@@ -1,9 +0,0 @@
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
- });
@@ -1,3 +0,0 @@
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";
@@ -1,11 +0,0 @@
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";
@@ -1,187 +0,0 @@
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
- });