@openclaw/nostr 2026.2.14 → 2026.2.17
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.
- package/CHANGELOG.md +18 -0
- package/index.ts +1 -1
- package/package.json +1 -1
- package/src/channel.ts +6 -24
- package/src/metrics.ts +20 -40
- package/src/nostr-profile-http.test.ts +19 -28
- package/src/nostr-state-store.test.ts +9 -7
- package/src/seen-tracker.ts +23 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.2.17
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.2.16
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.2.15
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
3
21
|
## 2026.2.14
|
|
4
22
|
|
|
5
23
|
### Changes
|
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
-
import type { NostrProfile } from "./src/config-schema.js";
|
|
4
3
|
import { nostrPlugin } from "./src/channel.js";
|
|
4
|
+
import type { NostrProfile } from "./src/config-schema.js";
|
|
5
5
|
import { createNostrProfileHttpHandler } from "./src/nostr-profile-http.js";
|
|
6
6
|
import { setNostrRuntime, getNostrRuntime } from "./src/runtime.js";
|
|
7
7
|
import { resolveNostrAccount } from "./src/types.js";
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildChannelConfigSchema,
|
|
3
|
+
collectStatusIssuesFromLastError,
|
|
4
|
+
createDefaultChannelRuntimeState,
|
|
3
5
|
DEFAULT_ACCOUNT_ID,
|
|
4
6
|
formatPairingApproveHint,
|
|
5
7
|
type ChannelPlugin,
|
|
6
8
|
} from "openclaw/plugin-sdk";
|
|
7
9
|
import type { NostrProfile } from "./config-schema.js";
|
|
8
|
-
import type { MetricEvent, MetricsSnapshot } from "./metrics.js";
|
|
9
|
-
import type { ProfilePublishResult } from "./nostr-profile.js";
|
|
10
10
|
import { NostrConfigSchema } from "./config-schema.js";
|
|
11
|
+
import type { MetricEvent, MetricsSnapshot } from "./metrics.js";
|
|
11
12
|
import { normalizePubkey, startNostrBus, type NostrBusHandle } from "./nostr-bus.js";
|
|
13
|
+
import type { ProfilePublishResult } from "./nostr-profile.js";
|
|
12
14
|
import { getNostrRuntime } from "./runtime.js";
|
|
13
15
|
import {
|
|
14
16
|
listNostrAccountIds,
|
|
@@ -157,28 +159,8 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
157
159
|
},
|
|
158
160
|
|
|
159
161
|
status: {
|
|
160
|
-
defaultRuntime:
|
|
161
|
-
|
|
162
|
-
running: false,
|
|
163
|
-
lastStartAt: null,
|
|
164
|
-
lastStopAt: null,
|
|
165
|
-
lastError: null,
|
|
166
|
-
},
|
|
167
|
-
collectStatusIssues: (accounts) =>
|
|
168
|
-
accounts.flatMap((account) => {
|
|
169
|
-
const lastError = typeof account.lastError === "string" ? account.lastError.trim() : "";
|
|
170
|
-
if (!lastError) {
|
|
171
|
-
return [];
|
|
172
|
-
}
|
|
173
|
-
return [
|
|
174
|
-
{
|
|
175
|
-
channel: "nostr",
|
|
176
|
-
accountId: account.accountId,
|
|
177
|
-
kind: "runtime" as const,
|
|
178
|
-
message: `Channel error: ${lastError}`,
|
|
179
|
-
},
|
|
180
|
-
];
|
|
181
|
-
}),
|
|
162
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
|
163
|
+
collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("nostr", accounts),
|
|
182
164
|
buildChannelSummary: ({ snapshot }) => ({
|
|
183
165
|
configured: snapshot.configured ?? false,
|
|
184
166
|
publicKey: snapshot.publicKey ?? null,
|
package/src/metrics.ts
CHANGED
|
@@ -50,6 +50,24 @@ export type MetricName =
|
|
|
50
50
|
| DecryptMetricName
|
|
51
51
|
| MemoryMetricName;
|
|
52
52
|
|
|
53
|
+
type RelayMetrics = {
|
|
54
|
+
connects: number;
|
|
55
|
+
disconnects: number;
|
|
56
|
+
reconnects: number;
|
|
57
|
+
errors: number;
|
|
58
|
+
messagesReceived: {
|
|
59
|
+
event: number;
|
|
60
|
+
eose: number;
|
|
61
|
+
closed: number;
|
|
62
|
+
notice: number;
|
|
63
|
+
ok: number;
|
|
64
|
+
auth: number;
|
|
65
|
+
};
|
|
66
|
+
circuitBreakerState: "closed" | "open" | "half_open";
|
|
67
|
+
circuitBreakerOpens: number;
|
|
68
|
+
circuitBreakerCloses: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
53
71
|
// ============================================================================
|
|
54
72
|
// Metric Event
|
|
55
73
|
// ============================================================================
|
|
@@ -93,26 +111,7 @@ export interface MetricsSnapshot {
|
|
|
93
111
|
};
|
|
94
112
|
|
|
95
113
|
/** Relay stats by URL */
|
|
96
|
-
relays: Record<
|
|
97
|
-
string,
|
|
98
|
-
{
|
|
99
|
-
connects: number;
|
|
100
|
-
disconnects: number;
|
|
101
|
-
reconnects: number;
|
|
102
|
-
errors: number;
|
|
103
|
-
messagesReceived: {
|
|
104
|
-
event: number;
|
|
105
|
-
eose: number;
|
|
106
|
-
closed: number;
|
|
107
|
-
notice: number;
|
|
108
|
-
ok: number;
|
|
109
|
-
auth: number;
|
|
110
|
-
};
|
|
111
|
-
circuitBreakerState: "closed" | "open" | "half_open";
|
|
112
|
-
circuitBreakerOpens: number;
|
|
113
|
-
circuitBreakerCloses: number;
|
|
114
|
-
}
|
|
115
|
-
>;
|
|
114
|
+
relays: Record<string, RelayMetrics>;
|
|
116
115
|
|
|
117
116
|
/** Rate limiting stats */
|
|
118
117
|
rateLimiting: {
|
|
@@ -174,26 +173,7 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics {
|
|
|
174
173
|
};
|
|
175
174
|
|
|
176
175
|
// Per-relay stats
|
|
177
|
-
const relays = new Map<
|
|
178
|
-
string,
|
|
179
|
-
{
|
|
180
|
-
connects: number;
|
|
181
|
-
disconnects: number;
|
|
182
|
-
reconnects: number;
|
|
183
|
-
errors: number;
|
|
184
|
-
messagesReceived: {
|
|
185
|
-
event: number;
|
|
186
|
-
eose: number;
|
|
187
|
-
closed: number;
|
|
188
|
-
notice: number;
|
|
189
|
-
ok: number;
|
|
190
|
-
auth: number;
|
|
191
|
-
};
|
|
192
|
-
circuitBreakerState: "closed" | "open" | "half_open";
|
|
193
|
-
circuitBreakerOpens: number;
|
|
194
|
-
circuitBreakerCloses: number;
|
|
195
|
-
}
|
|
196
|
-
>();
|
|
176
|
+
const relays = new Map<string, RelayMetrics>();
|
|
197
177
|
|
|
198
178
|
// Rate limiting stats
|
|
199
179
|
const rateLimiting = {
|
|
@@ -112,6 +112,23 @@ function createMockContext(overrides?: Partial<NostrProfileHttpContext>): NostrP
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function mockSuccessfulProfileImport() {
|
|
116
|
+
vi.mocked(importProfileFromRelays).mockResolvedValue({
|
|
117
|
+
ok: true,
|
|
118
|
+
profile: {
|
|
119
|
+
name: "imported",
|
|
120
|
+
displayName: "Imported User",
|
|
121
|
+
},
|
|
122
|
+
event: {
|
|
123
|
+
id: "evt123",
|
|
124
|
+
pubkey: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
|
125
|
+
created_at: 1234567890,
|
|
126
|
+
},
|
|
127
|
+
relaysQueried: ["wss://relay.damus.io"],
|
|
128
|
+
sourceRelay: "wss://relay.damus.io",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
// ============================================================================
|
|
116
133
|
// Tests
|
|
117
134
|
// ============================================================================
|
|
@@ -342,20 +359,7 @@ describe("nostr-profile-http", () => {
|
|
|
342
359
|
const req = createMockRequest("POST", "/api/channels/nostr/default/profile/import", {});
|
|
343
360
|
const res = createMockResponse();
|
|
344
361
|
|
|
345
|
-
|
|
346
|
-
ok: true,
|
|
347
|
-
profile: {
|
|
348
|
-
name: "imported",
|
|
349
|
-
displayName: "Imported User",
|
|
350
|
-
},
|
|
351
|
-
event: {
|
|
352
|
-
id: "evt123",
|
|
353
|
-
pubkey: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
|
354
|
-
created_at: 1234567890,
|
|
355
|
-
},
|
|
356
|
-
relaysQueried: ["wss://relay.damus.io"],
|
|
357
|
-
sourceRelay: "wss://relay.damus.io",
|
|
358
|
-
});
|
|
362
|
+
mockSuccessfulProfileImport();
|
|
359
363
|
|
|
360
364
|
await handler(req, res);
|
|
361
365
|
|
|
@@ -406,20 +410,7 @@ describe("nostr-profile-http", () => {
|
|
|
406
410
|
});
|
|
407
411
|
const res = createMockResponse();
|
|
408
412
|
|
|
409
|
-
|
|
410
|
-
ok: true,
|
|
411
|
-
profile: {
|
|
412
|
-
name: "imported",
|
|
413
|
-
displayName: "Imported User",
|
|
414
|
-
},
|
|
415
|
-
event: {
|
|
416
|
-
id: "evt123",
|
|
417
|
-
pubkey: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
|
418
|
-
created_at: 1234567890,
|
|
419
|
-
},
|
|
420
|
-
relaysQueried: ["wss://relay.damus.io"],
|
|
421
|
-
sourceRelay: "wss://relay.damus.io",
|
|
422
|
-
});
|
|
413
|
+
mockSuccessfulProfileImport();
|
|
423
414
|
|
|
424
415
|
await handler(req, res);
|
|
425
416
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import {
|
|
7
7
|
readNostrBusState,
|
|
@@ -17,11 +17,13 @@ async function withTempStateDir<T>(fn: (dir: string) => Promise<T>) {
|
|
|
17
17
|
setNostrRuntime({
|
|
18
18
|
state: {
|
|
19
19
|
resolveStateDir: (env, homedir) => {
|
|
20
|
-
const
|
|
20
|
+
const stateEnv = env ?? process.env;
|
|
21
|
+
const override = stateEnv.OPENCLAW_STATE_DIR?.trim() || stateEnv.CLAWDBOT_STATE_DIR?.trim();
|
|
21
22
|
if (override) {
|
|
22
23
|
return override;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
const resolveHome = homedir ?? os.homedir;
|
|
26
|
+
return path.join(resolveHome(), ".openclaw");
|
|
25
27
|
},
|
|
26
28
|
},
|
|
27
29
|
} as PluginRuntime);
|
|
@@ -90,7 +92,7 @@ describe("computeSinceTimestamp", () => {
|
|
|
90
92
|
});
|
|
91
93
|
|
|
92
94
|
it("uses lastProcessedAt when available", () => {
|
|
93
|
-
const state = {
|
|
95
|
+
const state: Parameters<typeof computeSinceTimestamp>[0] = {
|
|
94
96
|
version: 2,
|
|
95
97
|
lastProcessedAt: 1699999000,
|
|
96
98
|
gatewayStartedAt: null,
|
|
@@ -100,7 +102,7 @@ describe("computeSinceTimestamp", () => {
|
|
|
100
102
|
});
|
|
101
103
|
|
|
102
104
|
it("uses gatewayStartedAt when lastProcessedAt is null", () => {
|
|
103
|
-
const state = {
|
|
105
|
+
const state: Parameters<typeof computeSinceTimestamp>[0] = {
|
|
104
106
|
version: 2,
|
|
105
107
|
lastProcessedAt: null,
|
|
106
108
|
gatewayStartedAt: 1699998000,
|
|
@@ -110,7 +112,7 @@ describe("computeSinceTimestamp", () => {
|
|
|
110
112
|
});
|
|
111
113
|
|
|
112
114
|
it("uses the max of both timestamps", () => {
|
|
113
|
-
const state = {
|
|
115
|
+
const state: Parameters<typeof computeSinceTimestamp>[0] = {
|
|
114
116
|
version: 2,
|
|
115
117
|
lastProcessedAt: 1699999000,
|
|
116
118
|
gatewayStartedAt: 1699998000,
|
|
@@ -120,7 +122,7 @@ describe("computeSinceTimestamp", () => {
|
|
|
120
122
|
});
|
|
121
123
|
|
|
122
124
|
it("falls back to now if both are null", () => {
|
|
123
|
-
const state = {
|
|
125
|
+
const state: Parameters<typeof computeSinceTimestamp>[0] = {
|
|
124
126
|
version: 2,
|
|
125
127
|
lastProcessedAt: null,
|
|
126
128
|
gatewayStartedAt: null,
|
package/src/seen-tracker.ts
CHANGED
|
@@ -137,6 +137,27 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
|
|
|
137
137
|
entries.delete(idToEvict);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
function insertAtFront(id: string, seenAt: number): void {
|
|
141
|
+
const newEntry: Entry = {
|
|
142
|
+
seenAt,
|
|
143
|
+
prev: null,
|
|
144
|
+
next: head,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (head) {
|
|
148
|
+
const headEntry = entries.get(head);
|
|
149
|
+
if (headEntry) {
|
|
150
|
+
headEntry.prev = id;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
entries.set(id, newEntry);
|
|
155
|
+
head = id;
|
|
156
|
+
if (!tail) {
|
|
157
|
+
tail = id;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
140
161
|
// Prune expired entries
|
|
141
162
|
function pruneExpired(): void {
|
|
142
163
|
const now = Date.now();
|
|
@@ -180,25 +201,7 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
|
|
|
180
201
|
evictLRU();
|
|
181
202
|
}
|
|
182
203
|
|
|
183
|
-
|
|
184
|
-
const newEntry: Entry = {
|
|
185
|
-
seenAt: now,
|
|
186
|
-
prev: null,
|
|
187
|
-
next: head,
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
if (head) {
|
|
191
|
-
const headEntry = entries.get(head);
|
|
192
|
-
if (headEntry) {
|
|
193
|
-
headEntry.prev = id;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
entries.set(id, newEntry);
|
|
198
|
-
head = id;
|
|
199
|
-
if (!tail) {
|
|
200
|
-
tail = id;
|
|
201
|
-
}
|
|
204
|
+
insertAtFront(id, now);
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
function has(id: string): boolean {
|
|
@@ -268,24 +271,7 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
|
|
|
268
271
|
for (let i = ids.length - 1; i >= 0; i--) {
|
|
269
272
|
const id = ids[i];
|
|
270
273
|
if (!entries.has(id) && entries.size < maxEntries) {
|
|
271
|
-
|
|
272
|
-
seenAt: now,
|
|
273
|
-
prev: null,
|
|
274
|
-
next: head,
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
if (head) {
|
|
278
|
-
const headEntry = entries.get(head);
|
|
279
|
-
if (headEntry) {
|
|
280
|
-
headEntry.prev = id;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
entries.set(id, newEntry);
|
|
285
|
-
head = id;
|
|
286
|
-
if (!tail) {
|
|
287
|
-
tail = id;
|
|
288
|
-
}
|
|
274
|
+
insertAtFront(id, now);
|
|
289
275
|
}
|
|
290
276
|
}
|
|
291
277
|
}
|