@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nostr",
3
- "version": "2026.5.2",
3
+ "version": "2026.5.3-beta.2",
4
4
  "description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,7 +16,7 @@
16
16
  "openclaw": "workspace:*"
17
17
  },
18
18
  "peerDependencies": {
19
- "openclaw": ">=2026.5.2"
19
+ "openclaw": ">=2026.5.3-beta.2"
20
20
  },
21
21
  "peerDependenciesMeta": {
22
22
  "openclaw": {
@@ -54,14 +54,23 @@
54
54
  "minHostVersion": ">=2026.4.10"
55
55
  },
56
56
  "compat": {
57
- "pluginApi": ">=2026.5.2"
57
+ "pluginApi": ">=2026.5.3-beta.2"
58
58
  },
59
59
  "build": {
60
- "openclawVersion": "2026.5.2"
60
+ "openclawVersion": "2026.5.3-beta.2"
61
61
  },
62
62
  "release": {
63
63
  "publishToClawHub": true,
64
64
  "publishToNpm": true
65
- }
66
- }
65
+ },
66
+ "runtimeExtensions": [
67
+ "./dist/index.js"
68
+ ],
69
+ "runtimeSetupEntry": "./dist/setup-entry.js"
70
+ },
71
+ "files": [
72
+ "dist/**",
73
+ "openclaw.plugin.json",
74
+ "README.md"
75
+ ]
67
76
  }
package/api.ts DELETED
@@ -1,10 +0,0 @@
1
- export {
2
- getPluginRuntimeGatewayRequestScope,
3
- type OpenClawConfig,
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,97 +0,0 @@
1
- import {
2
- defineBundledChannelEntry,
3
- loadBundledEntryExportSync,
4
- } from "openclaw/plugin-sdk/channel-entry-contract";
5
- import type { OpenClawConfig, 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 OpenClawConfig;
50
- const account = resolveNostrAccount({ cfg, accountId });
51
- return account.profile;
52
- },
53
- updateConfigProfile: async (accountId: string, profile: unknown) => {
54
- const runtime = getNostrRuntime();
55
- const cfg = runtime.config.current() as OpenClawConfig;
56
-
57
- const channels = (cfg.channels ?? {}) as Record<string, unknown>;
58
- const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
59
-
60
- await runtime.config.replaceConfigFile({
61
- nextConfig: {
62
- ...cfg,
63
- channels: {
64
- ...channels,
65
- nostr: {
66
- ...nostrConfig,
67
- profile,
68
- },
69
- },
70
- },
71
- afterWrite: { mode: "auto" },
72
- });
73
- },
74
- getAccountInfo: (accountId: string) => {
75
- const runtime = getNostrRuntime();
76
- const cfg = runtime.config.current() as OpenClawConfig;
77
- const account = resolveNostrAccount({ cfg, accountId });
78
- if (!account.configured || !account.publicKey) {
79
- return null;
80
- }
81
- return {
82
- pubkey: account.publicKey,
83
- relays: account.relays,
84
- };
85
- },
86
- log: api.logger,
87
- });
88
-
89
- api.registerHttpRoute({
90
- path: "/api/channels/nostr",
91
- auth: "gateway",
92
- match: "prefix",
93
- gatewayRuntimeScopeSurface: "trusted-operator",
94
- handler: httpHandler,
95
- });
96
- },
97
- });
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 { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
5
- export { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime";
6
- export type { PluginRuntime } from "openclaw/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 "openclaw/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,15 +0,0 @@
1
- export {
2
- buildChannelConfigSchema,
3
- DEFAULT_ACCOUNT_ID,
4
- formatPairingApproveHint,
5
- type ChannelPlugin,
6
- } from "openclaw/plugin-sdk/channel-plugin-common";
7
- export type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-contract";
8
- export {
9
- collectStatusIssuesFromLastError,
10
- createDefaultChannelRuntimeState,
11
- } from "openclaw/plugin-sdk/status-helpers";
12
- export {
13
- createPreCryptoDirectDmAuthorizer,
14
- resolveInboundDirectDmAccessWithRuntime,
15
- } from "openclaw/plugin-sdk/direct-dm-access";
@@ -1,176 +0,0 @@
1
- import { createStartAccountContext } from "openclaw/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
- describe("nostr inbound gateway path", () => {
105
- afterEach(() => {
106
- mocks.normalizePubkey.mockClear();
107
- mocks.startNostrBus.mockReset();
108
- });
109
-
110
- it("issues a pairing reply before decrypt for unknown senders", async () => {
111
- const { cleanup } = await startGatewayHarness({
112
- account: buildResolvedNostrAccount({
113
- config: { dmPolicy: "pairing", allowFrom: [] },
114
- }),
115
- });
116
-
117
- const options = mocks.startNostrBus.mock.calls[0]?.[0] as {
118
- authorizeSender: (params: {
119
- senderPubkey: string;
120
- reply: (text: string) => Promise<void>;
121
- }) => Promise<string>;
122
- };
123
- const sendPairingReply = vi.fn(async (_text: string) => {});
124
-
125
- await expect(
126
- options.authorizeSender({
127
- senderPubkey: "nostr:UNKNOWN-SENDER",
128
- reply: sendPairingReply,
129
- }),
130
- ).resolves.toBe("pairing");
131
- expect(sendPairingReply).toHaveBeenCalledTimes(1);
132
- expect(sendPairingReply.mock.calls[0]?.[0]).toContain("Pairing code:");
133
-
134
- cleanup.stop();
135
- });
136
-
137
- it("routes allowed DMs through the standard reply pipeline", async () => {
138
- const { harness, cleanup } = await startGatewayHarness({
139
- account: buildResolvedNostrAccount({
140
- publicKey: "bot-pubkey",
141
- config: { dmPolicy: "allowlist", allowFrom: ["nostr:sender-pubkey"] },
142
- }),
143
- cfg: {
144
- session: { store: { type: "jsonl" } },
145
- commands: { useAccessGroups: true },
146
- } as never,
147
- });
148
-
149
- const options = mocks.startNostrBus.mock.calls[0]?.[0] as {
150
- onMessage: (
151
- senderPubkey: string,
152
- text: string,
153
- reply: (text: string) => Promise<void>,
154
- meta: { eventId: string; createdAt: number },
155
- ) => Promise<void>;
156
- };
157
- const sendReply = vi.fn(async (_text: string) => {});
158
-
159
- await options.onMessage("sender-pubkey", "hello from nostr", sendReply, {
160
- eventId: "event-123",
161
- createdAt: 1_710_000_000,
162
- });
163
-
164
- expect(harness.recordInboundSession).toHaveBeenCalledTimes(1);
165
- expect(harness.dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
166
- expect(harness.dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0]?.ctx).toMatchObject({
167
- BodyForAgent: "hello from nostr",
168
- SenderId: "sender-pubkey",
169
- MessageSid: "event-123",
170
- CommandAuthorized: true,
171
- });
172
- expect(sendReply).toHaveBeenCalledWith("converted:|a|b|");
173
-
174
- cleanup.stop();
175
- });
176
- });
@@ -1,128 +0,0 @@
1
- import { createStartAccountContext } from "openclaw/plugin-sdk/channel-test-helpers";
2
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
3
- import { afterEach, describe, expect, it, vi } from "vitest";
4
- import type { PluginRuntime } from "../runtime-api.js";
5
- import { nostrOutboundAdapter, startNostrGatewayAccount } from "./gateway.js";
6
- import { setNostrRuntime } from "./runtime.js";
7
- import { TEST_RESOLVED_PRIVATE_KEY, buildResolvedNostrAccount } from "./test-fixtures.js";
8
-
9
- const mocks = vi.hoisted(() => ({
10
- normalizePubkey: vi.fn((value: string) => `normalized-${value.toLowerCase()}`),
11
- startNostrBus: vi.fn(),
12
- }));
13
-
14
- vi.mock("./nostr-bus.js", () => ({
15
- DEFAULT_RELAYS: ["wss://relay.example.com"],
16
- startNostrBus: mocks.startNostrBus,
17
- }));
18
-
19
- vi.mock("./nostr-key-utils.js", () => ({
20
- getPublicKeyFromPrivate: vi.fn(() => "pubkey"),
21
- normalizePubkey: mocks.normalizePubkey,
22
- }));
23
-
24
- function createCfg() {
25
- return {
26
- channels: {
27
- nostr: {
28
- privateKey: TEST_RESOLVED_PRIVATE_KEY, // pragma: allowlist secret
29
- },
30
- },
31
- };
32
- }
33
-
34
- function installOutboundRuntime(convertMarkdownTables = vi.fn((text: string) => text)) {
35
- const resolveMarkdownTableMode = vi.fn(() => "off");
36
- setNostrRuntime({
37
- channel: {
38
- text: {
39
- resolveMarkdownTableMode,
40
- convertMarkdownTables,
41
- },
42
- },
43
- reply: {},
44
- } as unknown as PluginRuntime);
45
- return { resolveMarkdownTableMode, convertMarkdownTables };
46
- }
47
-
48
- async function startOutboundAccount(accountId?: string) {
49
- const sendDm = vi.fn(async () => {});
50
- const bus = {
51
- sendDm,
52
- close: vi.fn(),
53
- getMetrics: vi.fn(() => ({ counters: {} })),
54
- publishProfile: vi.fn(),
55
- getProfileState: vi.fn(async () => null),
56
- };
57
- mocks.startNostrBus.mockResolvedValueOnce(bus as unknown);
58
-
59
- const cleanup = (await startNostrGatewayAccount(
60
- createStartAccountContext({
61
- account: buildResolvedNostrAccount(accountId ? { accountId } : undefined),
62
- }),
63
- )) as { stop: () => void };
64
-
65
- return { cleanup, sendDm };
66
- }
67
-
68
- describe("nostr outbound cfg threading", () => {
69
- afterEach(() => {
70
- mocks.normalizePubkey.mockClear();
71
- mocks.startNostrBus.mockReset();
72
- });
73
-
74
- it("uses resolved cfg when converting markdown tables before send", async () => {
75
- const { resolveMarkdownTableMode, convertMarkdownTables } = installOutboundRuntime(
76
- vi.fn((text: string) => `converted:${text}`),
77
- );
78
- const { cleanup, sendDm } = await startOutboundAccount();
79
-
80
- const cfg = createCfg();
81
- await nostrOutboundAdapter.sendText({
82
- cfg: cfg as OpenClawConfig,
83
- to: "NPUB123",
84
- text: "|a|b|",
85
- accountId: "default",
86
- });
87
-
88
- expect(resolveMarkdownTableMode).toHaveBeenCalledWith({
89
- cfg,
90
- channel: "nostr",
91
- accountId: "default",
92
- });
93
- expect(convertMarkdownTables).toHaveBeenCalledWith("|a|b|", "off");
94
- expect(mocks.normalizePubkey).toHaveBeenCalledWith("NPUB123");
95
- expect(sendDm).toHaveBeenCalledWith("normalized-npub123", "converted:|a|b|");
96
-
97
- cleanup.stop();
98
- });
99
-
100
- it("uses the configured defaultAccount when accountId is omitted", async () => {
101
- const { resolveMarkdownTableMode } = installOutboundRuntime();
102
- const { cleanup, sendDm } = await startOutboundAccount("work");
103
-
104
- const cfg = {
105
- channels: {
106
- nostr: {
107
- privateKey: TEST_RESOLVED_PRIVATE_KEY, // pragma: allowlist secret
108
- defaultAccount: "work",
109
- },
110
- },
111
- };
112
-
113
- await nostrOutboundAdapter.sendText({
114
- cfg: cfg as OpenClawConfig,
115
- to: "NPUB123",
116
- text: "hello",
117
- });
118
-
119
- expect(resolveMarkdownTableMode).toHaveBeenCalledWith({
120
- cfg,
121
- channel: "nostr",
122
- accountId: "work",
123
- });
124
- expect(sendDm).toHaveBeenCalledWith("normalized-npub123", "hello");
125
-
126
- cleanup.stop();
127
- });
128
- });