@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
@@ -1,237 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- import type { PluginRuntime } from "../runtime-api.js";
6
- import {
7
- readNostrBusState,
8
- readNostrProfileState,
9
- writeNostrBusState,
10
- writeNostrProfileState,
11
- computeSinceTimestamp,
12
- } from "./nostr-state-store.js";
13
- import { setNostrRuntime } from "./runtime.js";
14
-
15
- async function withTempStateDir<T>(fn: (dir: string) => Promise<T>) {
16
- const previous = process.env.OPENCLAW_STATE_DIR;
17
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-nostr-"));
18
- process.env.OPENCLAW_STATE_DIR = dir;
19
- setNostrRuntime({
20
- state: {
21
- resolveStateDir: (env, homedir) => {
22
- const stateEnv = env ?? process.env;
23
- const override = stateEnv.OPENCLAW_STATE_DIR?.trim();
24
- if (override) {
25
- return override;
26
- }
27
- const resolveHome = homedir ?? os.homedir;
28
- return path.join(resolveHome(), ".openclaw");
29
- },
30
- },
31
- } as PluginRuntime);
32
- try {
33
- return await fn(dir);
34
- } finally {
35
- if (previous === undefined) {
36
- delete process.env.OPENCLAW_STATE_DIR;
37
- } else {
38
- process.env.OPENCLAW_STATE_DIR = previous;
39
- }
40
- await fs.rm(dir, { recursive: true, force: true });
41
- }
42
- }
43
-
44
- describe("nostr bus state store", () => {
45
- it("persists and reloads state across restarts", async () => {
46
- await withTempStateDir(async () => {
47
- // Fresh start - no state
48
- expect(await readNostrBusState({ accountId: "test-bot" })).toBeNull();
49
-
50
- // Write state
51
- await writeNostrBusState({
52
- accountId: "test-bot",
53
- lastProcessedAt: 1700000000,
54
- gatewayStartedAt: 1700000100,
55
- });
56
-
57
- // Read it back
58
- const state = await readNostrBusState({ accountId: "test-bot" });
59
- expect(state).toEqual({
60
- version: 2,
61
- lastProcessedAt: 1700000000,
62
- gatewayStartedAt: 1700000100,
63
- recentEventIds: [],
64
- });
65
- });
66
- });
67
-
68
- it("isolates state by accountId", async () => {
69
- await withTempStateDir(async () => {
70
- await writeNostrBusState({
71
- accountId: "bot-a",
72
- lastProcessedAt: 1000,
73
- gatewayStartedAt: 1000,
74
- });
75
- await writeNostrBusState({
76
- accountId: "bot-b",
77
- lastProcessedAt: 2000,
78
- gatewayStartedAt: 2000,
79
- });
80
-
81
- const stateA = await readNostrBusState({ accountId: "bot-a" });
82
- const stateB = await readNostrBusState({ accountId: "bot-b" });
83
-
84
- expect(stateA?.lastProcessedAt).toBe(1000);
85
- expect(stateB?.lastProcessedAt).toBe(2000);
86
- });
87
- });
88
-
89
- it("upgrades v1 bus state files on read", async () => {
90
- await withTempStateDir(async (dir) => {
91
- const filePath = path.join(dir, "nostr", "bus-state-test-bot.json");
92
- await fs.mkdir(path.dirname(filePath), { recursive: true });
93
- await fs.writeFile(
94
- filePath,
95
- JSON.stringify({
96
- version: 1,
97
- lastProcessedAt: 1700000000,
98
- gatewayStartedAt: 1700000100,
99
- }),
100
- "utf-8",
101
- );
102
-
103
- const state = await readNostrBusState({ accountId: "test-bot" });
104
- expect(state).toEqual({
105
- version: 2,
106
- lastProcessedAt: 1700000000,
107
- gatewayStartedAt: 1700000100,
108
- recentEventIds: [],
109
- });
110
- });
111
- });
112
-
113
- it("drops malformed recent event ids while keeping the state", async () => {
114
- await withTempStateDir(async (dir) => {
115
- const filePath = path.join(dir, "nostr", "bus-state-test-bot.json");
116
- await fs.mkdir(path.dirname(filePath), { recursive: true });
117
- await fs.writeFile(
118
- filePath,
119
- JSON.stringify({
120
- version: 2,
121
- lastProcessedAt: 1700000000,
122
- gatewayStartedAt: 1700000100,
123
- recentEventIds: ["evt-1", 2, null],
124
- }),
125
- "utf-8",
126
- );
127
-
128
- const state = await readNostrBusState({ accountId: "test-bot" });
129
- expect(state).toEqual({
130
- version: 2,
131
- lastProcessedAt: 1700000000,
132
- gatewayStartedAt: 1700000100,
133
- recentEventIds: ["evt-1"],
134
- });
135
- });
136
- });
137
- });
138
-
139
- describe("nostr profile state store", () => {
140
- it("persists and reloads profile publish state", async () => {
141
- await withTempStateDir(async () => {
142
- await writeNostrProfileState({
143
- accountId: "test-bot",
144
- lastPublishedAt: 1700000000,
145
- lastPublishedEventId: "evt-1",
146
- lastPublishResults: {
147
- "wss://relay.example": "ok",
148
- },
149
- });
150
-
151
- const state = await readNostrProfileState({ accountId: "test-bot" });
152
- expect(state).toEqual({
153
- version: 1,
154
- lastPublishedAt: 1700000000,
155
- lastPublishedEventId: "evt-1",
156
- lastPublishResults: {
157
- "wss://relay.example": "ok",
158
- },
159
- });
160
- });
161
- });
162
-
163
- it("drops malformed relay results while keeping valid state fields", async () => {
164
- await withTempStateDir(async (dir) => {
165
- const filePath = path.join(dir, "nostr", "profile-state-test-bot.json");
166
- await fs.mkdir(path.dirname(filePath), { recursive: true });
167
- await fs.writeFile(
168
- filePath,
169
- JSON.stringify({
170
- version: 1,
171
- lastPublishedAt: 1700000000,
172
- lastPublishedEventId: "evt-1",
173
- lastPublishResults: {
174
- "wss://relay.example": "ok",
175
- "wss://relay.bad": "unknown",
176
- },
177
- }),
178
- "utf-8",
179
- );
180
-
181
- const state = await readNostrProfileState({ accountId: "test-bot" });
182
- expect(state).toEqual({
183
- version: 1,
184
- lastPublishedAt: 1700000000,
185
- lastPublishedEventId: "evt-1",
186
- lastPublishResults: null,
187
- });
188
- });
189
- });
190
- });
191
-
192
- describe("computeSinceTimestamp", () => {
193
- it("returns now for null state (fresh start)", () => {
194
- const now = 1700000000;
195
- expect(computeSinceTimestamp(null, now)).toBe(now);
196
- });
197
-
198
- it("uses lastProcessedAt when available", () => {
199
- const state: Parameters<typeof computeSinceTimestamp>[0] = {
200
- version: 2,
201
- lastProcessedAt: 1699999000,
202
- gatewayStartedAt: null,
203
- recentEventIds: [],
204
- };
205
- expect(computeSinceTimestamp(state, 1700000000)).toBe(1699999000);
206
- });
207
-
208
- it("uses gatewayStartedAt when lastProcessedAt is null", () => {
209
- const state: Parameters<typeof computeSinceTimestamp>[0] = {
210
- version: 2,
211
- lastProcessedAt: null,
212
- gatewayStartedAt: 1699998000,
213
- recentEventIds: [],
214
- };
215
- expect(computeSinceTimestamp(state, 1700000000)).toBe(1699998000);
216
- });
217
-
218
- it("uses the max of both timestamps", () => {
219
- const state: Parameters<typeof computeSinceTimestamp>[0] = {
220
- version: 2,
221
- lastProcessedAt: 1699999000,
222
- gatewayStartedAt: 1699998000,
223
- recentEventIds: [],
224
- };
225
- expect(computeSinceTimestamp(state, 1700000000)).toBe(1699999000);
226
- });
227
-
228
- it("falls back to now if both are null", () => {
229
- const state: Parameters<typeof computeSinceTimestamp>[0] = {
230
- version: 2,
231
- lastProcessedAt: null,
232
- gatewayStartedAt: null,
233
- recentEventIds: [],
234
- };
235
- expect(computeSinceTimestamp(state, 1700000000)).toBe(1700000000);
236
- });
237
- });
@@ -1,223 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { safeParseJsonWithSchema } from "openclaw/plugin-sdk/extension-shared";
6
- import { z } from "zod";
7
- import { getNostrRuntime } from "./runtime.js";
8
-
9
- const STORE_VERSION = 2;
10
- const PROFILE_STATE_VERSION = 1;
11
-
12
- type _NostrBusStateV1 = {
13
- version: 1;
14
- /** Unix timestamp (seconds) of the last processed event */
15
- lastProcessedAt: number | null;
16
- /** Gateway startup timestamp (seconds) - events before this are old */
17
- gatewayStartedAt: number | null;
18
- };
19
-
20
- type NostrBusState = {
21
- version: 2;
22
- /** Unix timestamp (seconds) of the last processed event */
23
- lastProcessedAt: number | null;
24
- /** Gateway startup timestamp (seconds) - events before this are old */
25
- gatewayStartedAt: number | null;
26
- /** Recent processed event IDs for overlap dedupe across restarts */
27
- recentEventIds: string[];
28
- };
29
-
30
- /** Profile publish state (separate from bus state) */
31
- type NostrProfileState = {
32
- version: 1;
33
- /** Unix timestamp (seconds) of last successful profile publish */
34
- lastPublishedAt: number | null;
35
- /** Event ID of the last published profile */
36
- lastPublishedEventId: string | null;
37
- /** Per-relay publish results from last attempt */
38
- lastPublishResults: Record<string, "ok" | "failed" | "timeout"> | null;
39
- };
40
-
41
- const NullableFiniteNumberSchema = z.number().finite().nullable().catch(null);
42
- const NostrBusStateV1Schema = z.object({
43
- version: z.literal(1),
44
- lastProcessedAt: NullableFiniteNumberSchema,
45
- gatewayStartedAt: NullableFiniteNumberSchema,
46
- });
47
-
48
- const NostrBusStateSchema = z.object({
49
- version: z.literal(2),
50
- lastProcessedAt: NullableFiniteNumberSchema,
51
- gatewayStartedAt: NullableFiniteNumberSchema,
52
- recentEventIds: z
53
- .array(z.unknown())
54
- .catch([])
55
- .transform((ids) => ids.filter((id): id is string => typeof id === "string")),
56
- });
57
-
58
- const NostrProfileStateSchema = z.object({
59
- version: z.literal(1),
60
- lastPublishedAt: NullableFiniteNumberSchema,
61
- lastPublishedEventId: z.string().nullable().catch(null),
62
- lastPublishResults: z
63
- .record(z.string(), z.enum(["ok", "failed", "timeout"]))
64
- .nullable()
65
- .catch(null),
66
- });
67
-
68
- function normalizeAccountId(accountId?: string): string {
69
- const trimmed = accountId?.trim();
70
- if (!trimmed) {
71
- return "default";
72
- }
73
- return trimmed.replace(/[^a-z0-9._-]+/gi, "_");
74
- }
75
-
76
- function resolveNostrStatePath(accountId?: string, env: NodeJS.ProcessEnv = process.env): string {
77
- const stateDir = getNostrRuntime().state.resolveStateDir(env, os.homedir);
78
- const normalized = normalizeAccountId(accountId);
79
- return path.join(stateDir, "nostr", `bus-state-${normalized}.json`);
80
- }
81
-
82
- function resolveNostrProfileStatePath(
83
- accountId?: string,
84
- env: NodeJS.ProcessEnv = process.env,
85
- ): string {
86
- const stateDir = getNostrRuntime().state.resolveStateDir(env, os.homedir);
87
- const normalized = normalizeAccountId(accountId);
88
- return path.join(stateDir, "nostr", `profile-state-${normalized}.json`);
89
- }
90
-
91
- function safeParseState(raw: string): NostrBusState | null {
92
- const parsedV2 = safeParseJsonWithSchema(NostrBusStateSchema, raw);
93
- if (parsedV2) {
94
- return parsedV2;
95
- }
96
-
97
- const parsedV1 = safeParseJsonWithSchema(NostrBusStateV1Schema, raw);
98
- if (!parsedV1) {
99
- return null;
100
- }
101
-
102
- // Back-compat: v1 state files
103
- return {
104
- version: 2,
105
- lastProcessedAt: parsedV1.lastProcessedAt,
106
- gatewayStartedAt: parsedV1.gatewayStartedAt,
107
- recentEventIds: [],
108
- };
109
- }
110
-
111
- export async function readNostrBusState(params: {
112
- accountId?: string;
113
- env?: NodeJS.ProcessEnv;
114
- }): Promise<NostrBusState | null> {
115
- const filePath = resolveNostrStatePath(params.accountId, params.env);
116
- try {
117
- const raw = await fs.readFile(filePath, "utf-8");
118
- return safeParseState(raw);
119
- } catch (err) {
120
- const code = (err as { code?: string }).code;
121
- if (code === "ENOENT") {
122
- return null;
123
- }
124
- return null;
125
- }
126
- }
127
-
128
- export async function writeNostrBusState(params: {
129
- accountId?: string;
130
- lastProcessedAt: number;
131
- gatewayStartedAt: number;
132
- recentEventIds?: string[];
133
- env?: NodeJS.ProcessEnv;
134
- }): Promise<void> {
135
- const filePath = resolveNostrStatePath(params.accountId, params.env);
136
- const dir = path.dirname(filePath);
137
- await fs.mkdir(dir, { recursive: true, mode: 0o700 });
138
- const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
139
- const payload: NostrBusState = {
140
- version: STORE_VERSION,
141
- lastProcessedAt: params.lastProcessedAt,
142
- gatewayStartedAt: params.gatewayStartedAt,
143
- recentEventIds: (params.recentEventIds ?? []).filter((x): x is string => typeof x === "string"),
144
- };
145
- await fs.writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, {
146
- encoding: "utf-8",
147
- });
148
- await fs.chmod(tmp, 0o600);
149
- await fs.rename(tmp, filePath);
150
- }
151
-
152
- /**
153
- * Determine the `since` timestamp for subscription.
154
- * Returns the later of: lastProcessedAt or gatewayStartedAt (both from disk),
155
- * falling back to `now` for fresh starts.
156
- */
157
- export function computeSinceTimestamp(
158
- state: NostrBusState | null,
159
- nowSec: number = Math.floor(Date.now() / 1000),
160
- ): number {
161
- if (!state) {
162
- return nowSec;
163
- }
164
-
165
- // Use the most recent timestamp we have
166
- const candidates = [state.lastProcessedAt, state.gatewayStartedAt].filter(
167
- (t): t is number => t !== null && t > 0,
168
- );
169
-
170
- if (candidates.length === 0) {
171
- return nowSec;
172
- }
173
- return Math.max(...candidates);
174
- }
175
-
176
- // ============================================================================
177
- // Profile State Management
178
- // ============================================================================
179
-
180
- function safeParseProfileState(raw: string): NostrProfileState | null {
181
- return safeParseJsonWithSchema(NostrProfileStateSchema, raw);
182
- }
183
-
184
- export async function readNostrProfileState(params: {
185
- accountId?: string;
186
- env?: NodeJS.ProcessEnv;
187
- }): Promise<NostrProfileState | null> {
188
- const filePath = resolveNostrProfileStatePath(params.accountId, params.env);
189
- try {
190
- const raw = await fs.readFile(filePath, "utf-8");
191
- return safeParseProfileState(raw);
192
- } catch (err) {
193
- const code = (err as { code?: string }).code;
194
- if (code === "ENOENT") {
195
- return null;
196
- }
197
- return null;
198
- }
199
- }
200
-
201
- export async function writeNostrProfileState(params: {
202
- accountId?: string;
203
- lastPublishedAt: number;
204
- lastPublishedEventId: string;
205
- lastPublishResults: Record<string, "ok" | "failed" | "timeout">;
206
- env?: NodeJS.ProcessEnv;
207
- }): Promise<void> {
208
- const filePath = resolveNostrProfileStatePath(params.accountId, params.env);
209
- const dir = path.dirname(filePath);
210
- await fs.mkdir(dir, { recursive: true, mode: 0o700 });
211
- const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
212
- const payload: NostrProfileState = {
213
- version: PROFILE_STATE_VERSION,
214
- lastPublishedAt: params.lastPublishedAt,
215
- lastPublishedEventId: params.lastPublishedEventId,
216
- lastPublishResults: params.lastPublishResults,
217
- };
218
- await fs.writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, {
219
- encoding: "utf-8",
220
- });
221
- await fs.chmod(tmp, 0o600);
222
- await fs.rename(tmp, filePath);
223
- }
package/src/runtime.ts DELETED
@@ -1,9 +0,0 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk/core";
2
- import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
3
-
4
- const { setRuntime: setNostrRuntime, getRuntime: getNostrRuntime } =
5
- createPluginRuntimeStore<PluginRuntime>({
6
- pluginId: "nostr",
7
- errorMessage: "Nostr runtime not initialized",
8
- });
9
- export { getNostrRuntime, setNostrRuntime };