@openclaw/nostr 2026.5.30-beta.1 → 2026.5.31-beta.1

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/dist/api.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { o as resolveNostrAccount } from "./setup-surface-CVEYWXAG.js";
2
2
  import { getPluginRuntimeGatewayRequestScope } from "./runtime-api.js";
3
3
  import { n as NostrProfileSchema } from "./config-schema-DIiXiBKr.js";
4
- import { a as setNostrRuntime, i as getNostrRuntime, n as nostrPlugin, o as contentToProfile, r as publishNostrProfile, t as getNostrProfileState } from "./channel-UK7t4qb8.js";
4
+ import { a as setNostrRuntime, i as getNostrRuntime, n as nostrPlugin, o as contentToProfile, r as publishNostrProfile, t as getNostrProfileState } from "./channel-nsm56KpS.js";
5
5
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
6
6
  import { z } from "zod";
7
7
  import { SimplePool, verifyEvent } from "nostr-tools";
8
+ import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
8
9
  import { readJsonBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-request-guards";
9
10
  import { createFixedWindowRateLimiter } from "openclaw/plugin-sdk/webhook-ingress";
10
- import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
11
11
  import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
12
12
  //#region extensions/nostr/src/nostr-profile-url-safety.ts
13
13
  function validateUrlSafety(urlStr) {
@@ -5,10 +5,9 @@ import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
5
5
  import { createScopedDmSecurityResolver, createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
6
6
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
7
7
  import { createChannelMessageAdapterFromOutbound } from "openclaw/plugin-sdk/channel-outbound";
8
- import { buildPassiveChannelStatusSummary, buildTrafficStatusSummary, runStoppablePassiveMonitor, safeParseJsonWithSchema } from "openclaw/plugin-sdk/extension-shared";
8
+ import { buildPassiveChannelStatusSummary, buildTrafficStatusSummary, runStoppablePassiveMonitor } from "openclaw/plugin-sdk/extension-shared";
9
9
  import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
10
10
  import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
11
- import { z } from "zod";
12
11
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
13
12
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
14
13
  import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
@@ -16,10 +15,8 @@ import { SimplePool, finalizeEvent, getPublicKey, verifyEvent } from "nostr-tool
16
15
  import { decrypt, encrypt } from "nostr-tools/nip04";
17
16
  import { createDirectDmPreCryptoGuardPolicy } from "openclaw/plugin-sdk/direct-dm-guard-policy";
18
17
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
19
- import os from "node:os";
20
- import path from "node:path";
21
- import { privateFileStore } from "openclaw/plugin-sdk/security-runtime";
22
18
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
19
+ import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
23
20
  import { buildChannelOutboundSessionRoute, stripChannelTargetPrefix } from "openclaw/plugin-sdk/core";
24
21
  //#region extensions/nostr/src/metrics.ts
25
22
  /**
@@ -410,78 +407,40 @@ const { setRuntime: setNostrRuntime, getRuntime: getNostrRuntime } = createPlugi
410
407
  //#region extensions/nostr/src/nostr-state-store.ts
411
408
  const STORE_VERSION = 2;
412
409
  const PROFILE_STATE_VERSION = 1;
413
- const NullableFiniteNumberSchema = z.number().finite().nullable().catch(null);
414
- const NostrBusStateV1Schema = z.object({
415
- version: z.literal(1),
416
- lastProcessedAt: NullableFiniteNumberSchema,
417
- gatewayStartedAt: NullableFiniteNumberSchema
418
- });
419
- const NostrBusStateSchema = z.object({
420
- version: z.literal(2),
421
- lastProcessedAt: NullableFiniteNumberSchema,
422
- gatewayStartedAt: NullableFiniteNumberSchema,
423
- recentEventIds: z.array(z.unknown()).catch([]).transform((ids) => ids.filter((id) => typeof id === "string"))
424
- });
425
- const NostrProfileStateSchema = z.object({
426
- version: z.literal(1),
427
- lastPublishedAt: NullableFiniteNumberSchema,
428
- lastPublishedEventId: z.string().nullable().catch(null),
429
- lastPublishResults: z.record(z.string(), z.enum([
430
- "ok",
431
- "failed",
432
- "timeout"
433
- ])).nullable().catch(null)
434
- });
435
410
  function normalizeAccountId(accountId) {
436
411
  const trimmed = accountId?.trim();
437
412
  if (!trimmed) return "default";
438
413
  return trimmed.replace(/[^a-z0-9._-]+/gi, "_");
439
414
  }
440
- function resolveNostrStatePath(accountId, env = process.env) {
441
- const stateDir = getNostrRuntime().state.resolveStateDir(env, os.homedir);
442
- const normalized = normalizeAccountId(accountId);
443
- return path.join(stateDir, "nostr", `bus-state-${normalized}.json`);
444
- }
445
- function resolveNostrProfileStatePath(accountId, env = process.env) {
446
- const stateDir = getNostrRuntime().state.resolveStateDir(env, os.homedir);
447
- const normalized = normalizeAccountId(accountId);
448
- return path.join(stateDir, "nostr", `profile-state-${normalized}.json`);
415
+ function openNostrBusStateStore(env) {
416
+ return getNostrRuntime().state.openKeyedStore({
417
+ namespace: "bus-state",
418
+ maxEntries: 256,
419
+ ...env ? { env } : {}
420
+ });
449
421
  }
450
- function safeParseState(raw) {
451
- const parsedV2 = safeParseJsonWithSchema(NostrBusStateSchema, raw);
452
- if (parsedV2) return parsedV2;
453
- const parsedV1 = safeParseJsonWithSchema(NostrBusStateV1Schema, raw);
454
- if (!parsedV1) return null;
455
- return {
456
- version: 2,
457
- lastProcessedAt: parsedV1.lastProcessedAt,
458
- gatewayStartedAt: parsedV1.gatewayStartedAt,
459
- recentEventIds: []
460
- };
422
+ function openNostrProfileStateStore(env) {
423
+ return getNostrRuntime().state.openKeyedStore({
424
+ namespace: "profile-state",
425
+ maxEntries: 256,
426
+ ...env ? { env } : {}
427
+ });
461
428
  }
462
429
  async function readNostrBusState(params) {
463
- const filePath = resolveNostrStatePath(params.accountId, params.env);
464
- try {
465
- const raw = await privateFileStore(path.dirname(filePath)).readTextIfExists(path.basename(filePath));
466
- if (raw === null) return null;
467
- return safeParseState(raw);
468
- } catch {
469
- return null;
470
- }
430
+ return await openNostrBusStateStore(params.env).lookup(normalizeAccountId(params.accountId)) ?? null;
471
431
  }
472
432
  async function writeNostrBusState(params) {
473
- const filePath = resolveNostrStatePath(params.accountId, params.env);
474
433
  const payload = {
475
434
  version: STORE_VERSION,
476
435
  lastProcessedAt: params.lastProcessedAt,
477
436
  gatewayStartedAt: params.gatewayStartedAt,
478
437
  recentEventIds: (params.recentEventIds ?? []).filter((x) => typeof x === "string")
479
438
  };
480
- await privateFileStore(path.dirname(filePath)).writeJson(path.basename(filePath), payload, { trailingNewline: true });
439
+ await openNostrBusStateStore(params.env).register(normalizeAccountId(params.accountId), payload);
481
440
  }
482
441
  /**
483
442
  * Determine the `since` timestamp for subscription.
484
- * Returns the later of: lastProcessedAt or gatewayStartedAt (both from disk),
443
+ * Returns the later of: lastProcessedAt or gatewayStartedAt (both from state),
485
444
  * falling back to `now` for fresh starts.
486
445
  */
487
446
  function computeSinceTimestamp(state, nowSec = Math.floor(Date.now() / 1e3)) {
@@ -490,36 +449,29 @@ function computeSinceTimestamp(state, nowSec = Math.floor(Date.now() / 1e3)) {
490
449
  if (candidates.length === 0) return nowSec;
491
450
  return Math.max(...candidates);
492
451
  }
493
- function safeParseProfileState(raw) {
494
- return safeParseJsonWithSchema(NostrProfileStateSchema, raw);
495
- }
496
452
  async function readNostrProfileState(params) {
497
- const filePath = resolveNostrProfileStatePath(params.accountId, params.env);
498
- try {
499
- const raw = await privateFileStore(path.dirname(filePath)).readTextIfExists(path.basename(filePath));
500
- if (raw === null) return null;
501
- return safeParseProfileState(raw);
502
- } catch {
503
- return null;
504
- }
453
+ return await openNostrProfileStateStore(params.env).lookup(normalizeAccountId(params.accountId)) ?? null;
505
454
  }
506
455
  async function writeNostrProfileState(params) {
507
- const filePath = resolveNostrProfileStatePath(params.accountId, params.env);
508
456
  const payload = {
509
457
  version: PROFILE_STATE_VERSION,
510
458
  lastPublishedAt: params.lastPublishedAt,
511
459
  lastPublishedEventId: params.lastPublishedEventId,
512
460
  lastPublishResults: params.lastPublishResults
513
461
  };
514
- await privateFileStore(path.dirname(filePath)).writeJson(path.basename(filePath), payload, { trailingNewline: true });
462
+ await openNostrProfileStateStore(params.env).register(normalizeAccountId(params.accountId), payload);
515
463
  }
516
464
  //#endregion
517
465
  //#region extensions/nostr/src/seen-tracker.ts
518
466
  /**
467
+ * LRU-based seen event tracker with TTL support.
468
+ * Prevents unbounded memory growth under high load or abuse.
469
+ */
470
+ /**
519
471
  * Create a new seen tracker with LRU eviction and TTL expiration.
520
472
  */
521
473
  function createSeenTracker(options) {
522
- const maxEntries = options?.maxEntries ?? 1e5;
474
+ const maxEntries = resolveIntegerOption(options?.maxEntries, 1e5, { min: 1 });
523
475
  const ttlMs = options?.ttlMs ?? 3600 * 1e3;
524
476
  const pruneIntervalMs = options?.pruneIntervalMs ?? 600 * 1e3;
525
477
  const entries = /* @__PURE__ */ new Map();
@@ -1,2 +1,2 @@
1
- import { n as nostrPlugin } from "./channel-UK7t4qb8.js";
1
+ import { n as nostrPlugin } from "./channel-nsm56KpS.js";
2
2
  export { nostrPlugin };
@@ -0,0 +1,211 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ //#region extensions/nostr/doctor-contract-api.ts
4
+ const BUS_STATE_NAMESPACE = "bus-state";
5
+ const PROFILE_STATE_NAMESPACE = "profile-state";
6
+ const MAX_NOSTR_STATE_ENTRIES = 256;
7
+ function normalizeAccountId(accountId) {
8
+ const trimmed = accountId?.trim();
9
+ if (!trimmed) return "default";
10
+ return trimmed.replace(/[^a-z0-9._-]+/gi, "_");
11
+ }
12
+ function finiteNumberOrNull(value) {
13
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
14
+ }
15
+ function parseBusState(value) {
16
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
17
+ const parsed = value;
18
+ if (parsed.version !== 1 && parsed.version !== 2) return null;
19
+ return {
20
+ version: 2,
21
+ lastProcessedAt: finiteNumberOrNull(parsed.lastProcessedAt),
22
+ gatewayStartedAt: finiteNumberOrNull(parsed.gatewayStartedAt),
23
+ recentEventIds: parsed.version === 2 && Array.isArray(parsed.recentEventIds) ? parsed.recentEventIds.filter((entry) => typeof entry === "string") : []
24
+ };
25
+ }
26
+ function parseProfileState(value) {
27
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
28
+ const parsed = value;
29
+ if (parsed.version !== 1) return null;
30
+ const rawResults = parsed.lastPublishResults;
31
+ const lastPublishResults = {};
32
+ if (rawResults && typeof rawResults === "object" && !Array.isArray(rawResults)) {
33
+ for (const [relay, result] of Object.entries(rawResults)) if (result === "ok" || result === "failed" || result === "timeout") lastPublishResults[relay] = result;
34
+ }
35
+ return {
36
+ version: 1,
37
+ lastPublishedAt: finiteNumberOrNull(parsed.lastPublishedAt),
38
+ lastPublishedEventId: typeof parsed.lastPublishedEventId === "string" ? parsed.lastPublishedEventId : null,
39
+ lastPublishResults: rawResults === null || Object.keys(lastPublishResults).length === 0 ? null : lastPublishResults
40
+ };
41
+ }
42
+ async function fileExists(filePath) {
43
+ try {
44
+ return (await fs.stat(filePath)).isFile();
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+ async function readJsonFile(filePath) {
50
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
51
+ }
52
+ async function listLegacyFiles(params) {
53
+ const dir = path.join(params.stateDir, "nostr");
54
+ let entries = [];
55
+ try {
56
+ entries = await fs.readdir(dir, { withFileTypes: true });
57
+ } catch {
58
+ return [];
59
+ }
60
+ const suffix = ".json";
61
+ const files = [];
62
+ for (const entry of entries) {
63
+ if (!entry.isFile() || !entry.name.startsWith(params.prefix) || !entry.name.endsWith(suffix)) continue;
64
+ const accountId = normalizeAccountId(entry.name.slice(params.prefix.length, -5));
65
+ const filePath = path.join(dir, entry.name);
66
+ try {
67
+ const value = params.parse(await readJsonFile(filePath));
68
+ if (value) files.push({
69
+ accountId,
70
+ filePath,
71
+ value
72
+ });
73
+ } catch {}
74
+ }
75
+ return files;
76
+ }
77
+ async function archiveLegacySource(params) {
78
+ const archivedPath = `${params.filePath}.migrated`;
79
+ if (await fileExists(archivedPath)) {
80
+ params.warnings.push(`Left migrated ${params.label} source in place because ${archivedPath} already exists`);
81
+ return;
82
+ }
83
+ try {
84
+ await fs.rename(params.filePath, archivedPath);
85
+ params.changes.push(`Archived ${params.label} legacy source -> ${archivedPath}`);
86
+ } catch (err) {
87
+ params.warnings.push(`Failed archiving ${params.label} legacy source: ${String(err)}`);
88
+ }
89
+ }
90
+ async function ensureStoreCapacity(params) {
91
+ const existingKeys = new Set((await params.store.entries()).map((entry) => entry.key));
92
+ const missingKeys = new Set(params.files.map((file) => file.accountId).filter((key) => !existingKeys.has(key)));
93
+ if (missingKeys.size > params.maxEntries - existingKeys.size) {
94
+ params.warnings.push(`Skipped migrating ${params.label} because plugin state has room for ${params.maxEntries - existingKeys.size} of ${missingKeys.size} missing entries; left legacy sources in place`);
95
+ return null;
96
+ }
97
+ return existingKeys;
98
+ }
99
+ const stateMigrations = [{
100
+ id: "nostr-bus-state-json-to-plugin-state",
101
+ label: "Nostr bus state",
102
+ async detectLegacyState(params) {
103
+ const files = await listLegacyFiles({
104
+ stateDir: params.stateDir,
105
+ prefix: "bus-state-",
106
+ parse: parseBusState
107
+ });
108
+ if (files.length === 0) return null;
109
+ return { preview: [`- Nostr bus state: ${files.length} ${files.length === 1 ? "account" : "accounts"} -> plugin state (${BUS_STATE_NAMESPACE})`] };
110
+ },
111
+ async migrateLegacyState(params) {
112
+ const changes = [];
113
+ const warnings = [];
114
+ const files = await listLegacyFiles({
115
+ stateDir: params.stateDir,
116
+ prefix: "bus-state-",
117
+ parse: parseBusState
118
+ });
119
+ const store = params.context.openPluginStateKeyedStore({
120
+ namespace: BUS_STATE_NAMESPACE,
121
+ maxEntries: MAX_NOSTR_STATE_ENTRIES
122
+ });
123
+ const existingKeys = await ensureStoreCapacity({
124
+ files,
125
+ store,
126
+ maxEntries: MAX_NOSTR_STATE_ENTRIES,
127
+ label: "Nostr bus state",
128
+ warnings
129
+ });
130
+ if (!existingKeys) return {
131
+ changes,
132
+ warnings
133
+ };
134
+ let imported = 0;
135
+ for (const file of files) {
136
+ if (!existingKeys.has(file.accountId)) {
137
+ await store.register(file.accountId, file.value);
138
+ existingKeys.add(file.accountId);
139
+ imported++;
140
+ }
141
+ await archiveLegacySource({
142
+ filePath: file.filePath,
143
+ label: "Nostr bus state",
144
+ changes,
145
+ warnings
146
+ });
147
+ }
148
+ if (imported > 0) changes.unshift(`Migrated ${imported} Nostr bus-state ${imported === 1 ? "entry" : "entries"} -> plugin state`);
149
+ return {
150
+ changes,
151
+ warnings
152
+ };
153
+ }
154
+ }, {
155
+ id: "nostr-profile-state-json-to-plugin-state",
156
+ label: "Nostr profile state",
157
+ async detectLegacyState(params) {
158
+ const files = await listLegacyFiles({
159
+ stateDir: params.stateDir,
160
+ prefix: "profile-state-",
161
+ parse: parseProfileState
162
+ });
163
+ if (files.length === 0) return null;
164
+ return { preview: [`- Nostr profile state: ${files.length} ${files.length === 1 ? "account" : "accounts"} -> plugin state (${PROFILE_STATE_NAMESPACE})`] };
165
+ },
166
+ async migrateLegacyState(params) {
167
+ const changes = [];
168
+ const warnings = [];
169
+ const files = await listLegacyFiles({
170
+ stateDir: params.stateDir,
171
+ prefix: "profile-state-",
172
+ parse: parseProfileState
173
+ });
174
+ const store = params.context.openPluginStateKeyedStore({
175
+ namespace: PROFILE_STATE_NAMESPACE,
176
+ maxEntries: MAX_NOSTR_STATE_ENTRIES
177
+ });
178
+ const existingKeys = await ensureStoreCapacity({
179
+ files,
180
+ store,
181
+ maxEntries: MAX_NOSTR_STATE_ENTRIES,
182
+ label: "Nostr profile state",
183
+ warnings
184
+ });
185
+ if (!existingKeys) return {
186
+ changes,
187
+ warnings
188
+ };
189
+ let imported = 0;
190
+ for (const file of files) {
191
+ if (!existingKeys.has(file.accountId)) {
192
+ await store.register(file.accountId, file.value);
193
+ existingKeys.add(file.accountId);
194
+ imported++;
195
+ }
196
+ await archiveLegacySource({
197
+ filePath: file.filePath,
198
+ label: "Nostr profile state",
199
+ changes,
200
+ warnings
201
+ });
202
+ }
203
+ if (imported > 0) changes.unshift(`Migrated ${imported} Nostr profile-state ${imported === 1 ? "entry" : "entries"} -> plugin state`);
204
+ return {
205
+ changes,
206
+ warnings
207
+ };
208
+ }
209
+ }];
210
+ //#endregion
211
+ export { stateMigrations };
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@openclaw/nostr",
3
- "version": "2026.5.30-beta.1",
3
+ "version": "2026.5.31-beta.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/nostr",
9
- "version": "2026.5.30-beta.1",
9
+ "version": "2026.5.31-beta.1",
10
10
  "dependencies": {
11
11
  "nostr-tools": "2.23.5",
12
12
  "zod": "4.4.3"
13
13
  },
14
14
  "peerDependencies": {
15
- "openclaw": ">=2026.5.30-beta.1"
15
+ "openclaw": ">=2026.5.31-beta.1"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "openclaw": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nostr",
3
- "version": "2026.5.30-beta.1",
3
+ "version": "2026.5.31-beta.1",
4
4
  "description": "OpenClaw Nostr channel plugin for NIP-04 encrypted direct messages.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,7 +12,7 @@
12
12
  "zod": "4.4.3"
13
13
  },
14
14
  "peerDependencies": {
15
- "openclaw": ">=2026.5.30-beta.1"
15
+ "openclaw": ">=2026.5.31-beta.1"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "openclaw": {
@@ -50,10 +50,10 @@
50
50
  "minHostVersion": ">=2026.4.10"
51
51
  },
52
52
  "compat": {
53
- "pluginApi": ">=2026.5.30-beta.1"
53
+ "pluginApi": ">=2026.5.31-beta.1"
54
54
  },
55
55
  "build": {
56
- "openclawVersion": "2026.5.30-beta.1"
56
+ "openclawVersion": "2026.5.31-beta.1"
57
57
  },
58
58
  "release": {
59
59
  "publishToClawHub": true,