@openclaw/nostr 2026.9.6 → 2026.9.8

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/assets/icon.png CHANGED
Binary file
@@ -1,15 +1,15 @@
1
1
  import { a as listNostrAccountIds, c as normalizePubkey, n as nostrSetupContract, o as resolveDefaultNostrAccountId, r as nostrSetupWizard, s as resolveNostrAccount } from "./setup-surface-Dx8QxWSP.mjs";
2
- import { a as collectStatusIssuesFromLastError, i as buildChannelConfigSchema, n as NostrProfileSchema, o as createDefaultChannelRuntimeState, r as DEFAULT_ACCOUNT_ID, s as formatPairingApproveHint, t as NostrConfigSchema } from "./config-schema-CkYd8ACn.mjs";
2
+ import { n as NostrProfileSchema, t as NostrConfigSchema } from "./config-schema-B3uU2qBS.mjs";
3
3
  import { c as validatePrivateKey, l as DEFAULT_RELAYS } from "./setup-adapter-KJ_iGKiG.mjs";
4
- import { t as normalizeNostrStateAccountId } from "./state-account-id-CvBZ9s6P.mjs";
5
4
  import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
6
5
  import { createScopedDmSecurityResolver, createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
7
6
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
8
7
  import { missingTargetError } from "openclaw/plugin-sdk/channel-feedback";
9
8
  import { DEFAULT_INGRESS_ADOPTION_STALL_MS, bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, createChannelMessageAdapterFromOutbound, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
9
+ import { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, formatPairingApproveHint } from "openclaw/plugin-sdk/channel-plugin-common";
10
10
  import { buildPassiveChannelStatusSummary, buildTrafficStatusSummary } from "openclaw/plugin-sdk/extension-shared";
11
- import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
12
- import { isRecord as isNostrIngressRecord, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
11
+ import { collectStatusIssuesFromLastError, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
12
+ import { isRecord, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
13
13
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
14
14
  import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
15
15
  import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
@@ -22,6 +22,7 @@ import { retryAsync } from "openclaw/plugin-sdk/retry-runtime";
22
22
  import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
23
23
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
24
24
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
25
+ import { withTimeout } from "openclaw/plugin-sdk/time-runtime";
25
26
  import { buildChannelOutboundSessionRoute, stripChannelTargetPrefix } from "openclaw/plugin-sdk/core";
26
27
  //#region extensions/nostr/src/metrics.ts
27
28
  function createZeroMetricsState() {
@@ -102,118 +103,59 @@ function createMetrics(onMetric) {
102
103
  }
103
104
  return relay;
104
105
  }
106
+ const relayMetric = (update) => (value, relayUrl) => {
107
+ if (relayUrl) update(getOrCreateRelay(relayUrl), value);
108
+ };
109
+ const updates = /* @__PURE__ */ new Map([
110
+ ["event.received", (value) => state.eventsReceived += value],
111
+ ["event.processed", (value) => state.eventsProcessed += value],
112
+ ["event.duplicate", (value) => state.eventsDuplicate += value],
113
+ ["event.rejected.invalid_shape", (value) => state.eventsRejected.invalidShape += value],
114
+ ["event.rejected.wrong_kind", (value) => state.eventsRejected.wrongKind += value],
115
+ ["event.rejected.stale", (value) => state.eventsRejected.stale += value],
116
+ ["event.rejected.future", (value) => state.eventsRejected.future += value],
117
+ ["event.rejected.rate_limited", (value) => state.eventsRejected.rateLimited += value],
118
+ ["event.rejected.invalid_signature", (value) => state.eventsRejected.invalidSignature += value],
119
+ ["event.rejected.oversized_ciphertext", (value) => state.eventsRejected.oversizedCiphertext += value],
120
+ ["event.rejected.oversized_plaintext", (value) => state.eventsRejected.oversizedPlaintext += value],
121
+ ["event.rejected.decrypt_failed", (value) => state.eventsRejected.decryptFailed += value],
122
+ ["event.rejected.self_message", (value) => state.eventsRejected.selfMessage += value],
123
+ ["rate_limit.per_sender", (value) => state.rateLimiting.perSenderHits += value],
124
+ ["rate_limit.global", (value) => state.rateLimiting.globalHits += value],
125
+ ["decrypt.success", (value) => state.decrypt.success += value],
126
+ ["decrypt.failure", (value) => state.decrypt.failure += value],
127
+ ["memory.seen_tracker_size", (value) => state.memory.seenTrackerSize = value],
128
+ ["memory.rate_limiter_entries", (value) => state.memory.rateLimiterEntries = value],
129
+ ["relay.connect", relayMetric((relay, value) => relay.connects += value)],
130
+ ["relay.disconnect", relayMetric((relay, value) => relay.disconnects += value)],
131
+ ["relay.reconnect", relayMetric((relay, value) => relay.reconnects += value)],
132
+ ["relay.error", relayMetric((relay, value) => relay.errors += value)],
133
+ ["relay.message.event", relayMetric((relay, value) => relay.messagesReceived.event += value)],
134
+ ["relay.message.eose", relayMetric((relay, value) => relay.messagesReceived.eose += value)],
135
+ ["relay.message.closed", relayMetric((relay, value) => relay.messagesReceived.closed += value)],
136
+ ["relay.message.notice", relayMetric((relay, value) => relay.messagesReceived.notice += value)],
137
+ ["relay.message.ok", relayMetric((relay, value) => relay.messagesReceived.ok += value)],
138
+ ["relay.message.auth", relayMetric((relay, value) => relay.messagesReceived.auth += value)],
139
+ ["relay.circuit_breaker.open", relayMetric((relay, value) => {
140
+ relay.circuitBreakerState = "open";
141
+ relay.circuitBreakerOpens += value;
142
+ })],
143
+ ["relay.circuit_breaker.close", relayMetric((relay, value) => {
144
+ relay.circuitBreakerState = "closed";
145
+ relay.circuitBreakerCloses += value;
146
+ })],
147
+ ["relay.circuit_breaker.half_open", relayMetric((relay) => {
148
+ relay.circuitBreakerState = "half_open";
149
+ })]
150
+ ]);
105
151
  function emit(name, value = 1, labels) {
106
- if (onMetric) onMetric({
152
+ onMetric?.({
107
153
  name,
108
154
  value,
109
155
  timestamp: Date.now(),
110
156
  labels
111
157
  });
112
- const relayUrl = labels?.relay;
113
- switch (name) {
114
- case "event.received":
115
- state.eventsReceived += value;
116
- break;
117
- case "event.processed":
118
- state.eventsProcessed += value;
119
- break;
120
- case "event.duplicate":
121
- state.eventsDuplicate += value;
122
- break;
123
- case "event.rejected.invalid_shape":
124
- state.eventsRejected.invalidShape += value;
125
- break;
126
- case "event.rejected.wrong_kind":
127
- state.eventsRejected.wrongKind += value;
128
- break;
129
- case "event.rejected.stale":
130
- state.eventsRejected.stale += value;
131
- break;
132
- case "event.rejected.future":
133
- state.eventsRejected.future += value;
134
- break;
135
- case "event.rejected.rate_limited":
136
- state.eventsRejected.rateLimited += value;
137
- break;
138
- case "event.rejected.invalid_signature":
139
- state.eventsRejected.invalidSignature += value;
140
- break;
141
- case "event.rejected.oversized_ciphertext":
142
- state.eventsRejected.oversizedCiphertext += value;
143
- break;
144
- case "event.rejected.oversized_plaintext":
145
- state.eventsRejected.oversizedPlaintext += value;
146
- break;
147
- case "event.rejected.decrypt_failed":
148
- state.eventsRejected.decryptFailed += value;
149
- break;
150
- case "event.rejected.self_message":
151
- state.eventsRejected.selfMessage += value;
152
- break;
153
- case "relay.connect":
154
- if (relayUrl) getOrCreateRelay(relayUrl).connects += value;
155
- break;
156
- case "relay.disconnect":
157
- if (relayUrl) getOrCreateRelay(relayUrl).disconnects += value;
158
- break;
159
- case "relay.reconnect":
160
- if (relayUrl) getOrCreateRelay(relayUrl).reconnects += value;
161
- break;
162
- case "relay.error":
163
- if (relayUrl) getOrCreateRelay(relayUrl).errors += value;
164
- break;
165
- case "relay.message.event":
166
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.event += value;
167
- break;
168
- case "relay.message.eose":
169
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.eose += value;
170
- break;
171
- case "relay.message.closed":
172
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.closed += value;
173
- break;
174
- case "relay.message.notice":
175
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.notice += value;
176
- break;
177
- case "relay.message.ok":
178
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.ok += value;
179
- break;
180
- case "relay.message.auth":
181
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.auth += value;
182
- break;
183
- case "relay.circuit_breaker.open":
184
- if (relayUrl) {
185
- const r = getOrCreateRelay(relayUrl);
186
- r.circuitBreakerState = "open";
187
- r.circuitBreakerOpens += value;
188
- }
189
- break;
190
- case "relay.circuit_breaker.close":
191
- if (relayUrl) {
192
- const r = getOrCreateRelay(relayUrl);
193
- r.circuitBreakerState = "closed";
194
- r.circuitBreakerCloses += value;
195
- }
196
- break;
197
- case "relay.circuit_breaker.half_open":
198
- if (relayUrl) getOrCreateRelay(relayUrl).circuitBreakerState = "half_open";
199
- break;
200
- case "rate_limit.per_sender":
201
- state.rateLimiting.perSenderHits += value;
202
- break;
203
- case "rate_limit.global":
204
- state.rateLimiting.globalHits += value;
205
- break;
206
- case "decrypt.success":
207
- state.decrypt.success += value;
208
- break;
209
- case "decrypt.failure":
210
- state.decrypt.failure += value;
211
- break;
212
- case "memory.seen_tracker_size":
213
- state.memory.seenTrackerSize = value;
214
- break;
215
- case "memory.rate_limiter_entries": state.memory.rateLimiterEntries = value;
216
- }
158
+ updates.get(name)?.(value, labels?.relay);
217
159
  }
218
160
  function getSnapshot() {
219
161
  return createMetricsSnapshot(state);
@@ -383,7 +325,7 @@ function requiredString(value, field) {
383
325
  throw new NostrIngressPermanentError("invalid-event", `Nostr event is missing ${field}.`);
384
326
  }
385
327
  function inspectNostrIngressEvent(event) {
386
- if (!isNostrIngressRecord(event)) throw new NostrIngressPermanentError("invalid-event", "Nostr event must be an object.");
328
+ if (!isRecord(event)) throw new NostrIngressPermanentError("invalid-event", "Nostr event must be an object.");
387
329
  return {
388
330
  eventId: requiredString(event.id, "id"),
389
331
  laneKey: `direct:${requiredString(event.pubkey, "pubkey")}`
@@ -432,8 +374,7 @@ function deserializeNostrIngressEvent(rawEvent, claimedId) {
432
374
  } catch (error) {
433
375
  throw new NostrIngressPermanentError("invalid-event", `Nostr ingress row ${claimedId} contains invalid JSON.`, { cause: error });
434
376
  }
435
- if (!isNostrIngressRecord(parsed)) throw new NostrIngressPermanentError("invalid-event", `Nostr ingress row ${claimedId} has an invalid event shape.`);
436
- if (typeof parsed.kind !== "number" || typeof parsed.created_at !== "number" || typeof parsed.content !== "string" || typeof parsed.sig !== "string" || !Array.isArray(parsed.tags)) throw new NostrIngressPermanentError("invalid-event", `Nostr ingress row ${claimedId} has an invalid event shape.`);
377
+ if (!isRecord(parsed) || typeof parsed.kind !== "number" || typeof parsed.created_at !== "number" || typeof parsed.content !== "string" || typeof parsed.sig !== "string" || !Array.isArray(parsed.tags)) throw new NostrIngressPermanentError("invalid-event", `Nostr ingress row ${claimedId} has an invalid event shape.`);
437
378
  return parsed;
438
379
  }
439
380
  function createNostrIngress(options) {
@@ -481,7 +422,7 @@ function createNostrIngress(options) {
481
422
  },
482
423
  createClaimError: (kind, claim) => new NostrIngressPermanentError("invalid-event", kind === "invalid-version" ? `Nostr ingress row ${claim.id} has an unsupported version.` : `Nostr ingress row ${claim.id} changed event identity.`)
483
424
  },
484
- deliver: (event, lifecycle) => options.deliver(event, lifecycle),
425
+ deliver: options.deliver,
485
426
  pollIntervalMs: options.pollIntervalMs ?? NOSTR_INGRESS_POLL_INTERVAL_MS,
486
427
  retention: {
487
428
  completedMaxEntries: 1e5,
@@ -611,6 +552,16 @@ function createNostrIngress(options) {
611
552
  }
612
553
  //#endregion
613
554
  //#region extensions/nostr/src/nostr-profile-core.ts
555
+ const PROFILE_FIELDS = [
556
+ ["name", "name"],
557
+ ["displayName", "display_name"],
558
+ ["about", "about"],
559
+ ["picture", "picture"],
560
+ ["banner", "banner"],
561
+ ["website", "website"],
562
+ ["nip05", "nip05"],
563
+ ["lud16", "lud16"]
564
+ ];
614
565
  /**
615
566
  * Convert our config profile schema to NIP-01 content format.
616
567
  * Strips undefined fields and validates URLs.
@@ -618,14 +569,10 @@ function createNostrIngress(options) {
618
569
  function profileToContent(profile) {
619
570
  const validated = NostrProfileSchema.parse(profile);
620
571
  const content = {};
621
- if (validated.name !== void 0) content.name = validated.name;
622
- if (validated.displayName !== void 0) content.display_name = validated.displayName;
623
- if (validated.about !== void 0) content.about = validated.about;
624
- if (validated.picture !== void 0) content.picture = validated.picture;
625
- if (validated.banner !== void 0) content.banner = validated.banner;
626
- if (validated.website !== void 0) content.website = validated.website;
627
- if (validated.nip05 !== void 0) content.nip05 = validated.nip05;
628
- if (validated.lud16 !== void 0) content.lud16 = validated.lud16;
572
+ for (const [configKey, contentKey] of PROFILE_FIELDS) {
573
+ const value = validated[configKey];
574
+ if (value !== void 0) content[contentKey] = value;
575
+ }
629
576
  return content;
630
577
  }
631
578
  /**
@@ -634,76 +581,36 @@ function profileToContent(profile) {
634
581
  */
635
582
  function contentToProfile(content) {
636
583
  const profile = {};
637
- if (content.name !== void 0) profile.name = content.name;
638
- if (content.display_name !== void 0) profile.displayName = content.display_name;
639
- if (content.about !== void 0) profile.about = content.about;
640
- if (content.picture !== void 0) profile.picture = content.picture;
641
- if (content.banner !== void 0) profile.banner = content.banner;
642
- if (content.website !== void 0) profile.website = content.website;
643
- if (content.nip05 !== void 0) profile.nip05 = content.nip05;
644
- if (content.lud16 !== void 0) profile.lud16 = content.lud16;
584
+ for (const [configKey, contentKey] of PROFILE_FIELDS) {
585
+ const value = content[contentKey];
586
+ if (value !== void 0) profile[configKey] = value;
587
+ }
645
588
  return profile;
646
589
  }
647
590
  //#endregion
648
591
  //#region extensions/nostr/src/nostr-profile.ts
649
- /**
650
- * Nostr Profile Management (NIP-01 kind:0)
651
- *
652
- * Profile events are "replaceable" - the latest created_at wins.
653
- * This module handles profile event creation and publishing.
654
- */
655
- /**
656
- * Create a signed kind:0 profile event.
657
- *
658
- * @param sk - Private key as Uint8Array (32 bytes)
659
- * @param profile - Profile data to include
660
- * @param lastPublishedAt - Previous profile timestamp (for monotonic guarantee)
661
- * @returns Signed Nostr event
662
- */
663
- function createProfileEvent(sk, profile, lastPublishedAt) {
664
- const content = profileToContent(profile);
665
- const contentJson = JSON.stringify(content);
592
+ const RELAY_PUBLISH_TIMEOUT_MS = 5e3;
593
+ /** Publish one signed kind:0 event, reporting each relay's result without retrying. */
594
+ async function publishProfile(pool, sk, relays, profile, lastPublishedAt) {
595
+ const content = JSON.stringify(profileToContent(profile));
666
596
  const now = Math.floor(Date.now() / 1e3);
667
- const createdAt = lastPublishedAt !== void 0 ? Math.max(now, lastPublishedAt + 1) : now;
668
- return finalizeEvent({
597
+ const event = finalizeEvent({
669
598
  kind: 0,
670
- content: contentJson,
599
+ content,
671
600
  tags: [],
672
- created_at: createdAt
601
+ created_at: lastPublishedAt !== void 0 ? Math.max(now, lastPublishedAt + 1) : now
673
602
  }, sk);
674
- }
675
- /** Per-relay publish timeout (ms) */
676
- const RELAY_PUBLISH_TIMEOUT_MS = 5e3;
677
- /**
678
- * Publish a profile event to multiple relays.
679
- *
680
- * Best-effort: publishes to all relays in parallel, reports per-relay results.
681
- * Does NOT retry automatically - caller should handle retries if needed.
682
- *
683
- * @param pool - SimplePool instance for relay connections
684
- * @param relays - Array of relay WebSocket URLs
685
- * @param event - Signed profile event (kind:0)
686
- * @returns Publish results with successes and failures
687
- */
688
- async function publishProfileEvent(pool, relays, event) {
689
603
  const successes = [];
690
604
  const failures = [];
691
605
  const publishPromises = relays.map(async (relay) => {
692
- let timer;
693
606
  try {
694
- const timeoutPromise = new Promise((_, reject) => {
695
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error("timeout")), RELAY_PUBLISH_TIMEOUT_MS);
696
- });
697
- await Promise.race([pool.publish([relay], event)[0], timeoutPromise]);
607
+ await withTimeout(Promise.resolve(pool.publish([relay], event)[0]), RELAY_PUBLISH_TIMEOUT_MS, { message: "timeout" });
698
608
  successes.push(relay);
699
609
  } catch (err) {
700
- const errorMessage = formatErrorMessage(err);
701
610
  failures.push({
702
611
  relay,
703
- error: errorMessage
612
+ error: formatErrorMessage(err)
704
613
  });
705
- } finally {
706
- if (timer) clearTimeout(timer);
707
614
  }
708
615
  });
709
616
  await Promise.all(publishPromises);
@@ -714,19 +621,6 @@ async function publishProfileEvent(pool, relays, event) {
714
621
  createdAt: event.created_at
715
622
  };
716
623
  }
717
- /**
718
- * Create and publish a profile event in one call.
719
- *
720
- * @param pool - SimplePool instance
721
- * @param sk - Private key as Uint8Array
722
- * @param relays - Array of relay URLs
723
- * @param profile - Profile data
724
- * @param lastPublishedAt - Previous timestamp for monotonic ordering
725
- * @returns Publish results
726
- */
727
- async function publishProfile(pool, sk, relays, profile, lastPublishedAt) {
728
- return publishProfileEvent(pool, relays, createProfileEvent(sk, profile, lastPublishedAt));
729
- }
730
624
  //#endregion
731
625
  //#region extensions/nostr/src/nostr-relay-subscription.ts
732
626
  const DEFAULT_EOSE_CONFIRM_DEADLINE_MS = 1e4;
@@ -792,6 +686,13 @@ function createNostrRelaySubscriptionGroup(options) {
792
686
  };
793
687
  }
794
688
  //#endregion
689
+ //#region extensions/nostr/src/state-account-id.ts
690
+ function normalizeNostrStateAccountId(accountId) {
691
+ const trimmed = accountId?.trim();
692
+ if (!trimmed) return "default";
693
+ return trimmed.replace(/[^a-z0-9._-]+/gi, "_");
694
+ }
695
+ //#endregion
795
696
  //#region extensions/nostr/src/nostr-state-store.ts
796
697
  const STORE_VERSION = 2;
797
698
  const PROFILE_STATE_VERSION = 1;
@@ -850,22 +751,19 @@ const STARTUP_LOOKBACK_SEC = 120;
850
751
  const STATE_PERSIST_DEBOUNCE_MS = 5e3;
851
752
  const NOSTR_INGRESS_ENVELOPE_OVERHEAD_BYTES = 16384;
852
753
  const NOSTR_INGRESS_MAX_PENDING_EVENTS = 1e3;
853
- const DEFAULT_INBOUND_GUARD_POLICY = createDirectDmPreCryptoGuardPolicy();
854
754
  const CIRCUIT_BREAKER_THRESHOLD = 5;
855
755
  const CIRCUIT_BREAKER_RESET_MS = 3e4;
856
756
  const HEALTH_WINDOW_MS = 6e4;
857
- function createCircuitBreaker(relay, metrics, threshold = CIRCUIT_BREAKER_THRESHOLD, resetMs = CIRCUIT_BREAKER_RESET_MS) {
757
+ function createCircuitBreaker(relay, metrics) {
858
758
  const state = {
859
759
  state: "closed",
860
760
  failures: 0,
861
- lastFailure: 0,
862
- lastSuccess: Date.now()
761
+ lastFailure: 0
863
762
  };
864
763
  return {
865
764
  canAttempt() {
866
- if (state.state === "closed") return true;
867
765
  if (state.state === "open") {
868
- if (Date.now() - state.lastFailure >= resetMs) {
766
+ if (Date.now() - state.lastFailure >= CIRCUIT_BREAKER_RESET_MS) {
869
767
  state.state = "half_open";
870
768
  metrics.emit("relay.circuit_breaker.half_open", 1, { relay });
871
769
  return true;
@@ -880,21 +778,14 @@ function createCircuitBreaker(relay, metrics, threshold = CIRCUIT_BREAKER_THRESH
880
778
  state.failures = 0;
881
779
  metrics.emit("relay.circuit_breaker.close", 1, { relay });
882
780
  } else if (state.state === "closed") state.failures = 0;
883
- state.lastSuccess = Date.now();
884
781
  },
885
782
  recordFailure() {
886
783
  state.failures++;
887
784
  state.lastFailure = Date.now();
888
- if (state.state === "half_open") {
889
- state.state = "open";
890
- metrics.emit("relay.circuit_breaker.open", 1, { relay });
891
- } else if (state.state === "closed" && state.failures >= threshold) {
785
+ if (state.state === "half_open" || state.state === "closed" && state.failures >= CIRCUIT_BREAKER_THRESHOLD) {
892
786
  state.state = "open";
893
787
  metrics.emit("relay.circuit_breaker.open", 1, { relay });
894
788
  }
895
- },
896
- getState() {
897
- return state.state;
898
789
  }
899
790
  };
900
791
  }
@@ -907,7 +798,6 @@ function createRelayHealthTracker() {
907
798
  successCount: 0,
908
799
  failureCount: 0,
909
800
  latencySum: 0,
910
- latencyCount: 0,
911
801
  lastSuccess: 0,
912
802
  lastFailure: 0
913
803
  };
@@ -920,7 +810,6 @@ function createRelayHealthTracker() {
920
810
  const s = getOrCreate(relay);
921
811
  s.successCount++;
922
812
  s.latencySum += latencyMs;
923
- s.latencyCount++;
924
813
  s.lastSuccess = Date.now();
925
814
  },
926
815
  recordFailure(relay) {
@@ -936,18 +825,16 @@ function createRelayHealthTracker() {
936
825
  const successRate = s.successCount / total;
937
826
  const now = Date.now();
938
827
  const recencyBonus = s.lastSuccess > s.lastFailure ? Math.max(0, 1 - (now - s.lastSuccess) / HEALTH_WINDOW_MS) * .2 : 0;
939
- const avgLatency = s.latencyCount > 0 ? s.latencySum / s.latencyCount : 1e3;
828
+ const avgLatency = s.successCount > 0 ? s.latencySum / s.successCount : 1e3;
940
829
  const latencyPenalty = Math.min(.2, avgLatency / 1e4);
941
830
  return Math.max(0, Math.min(1, successRate + recencyBonus - latencyPenalty));
942
831
  },
943
832
  getSortedRelays(relays) {
944
- return [...relays].toSorted((a, b) => this.getScore(b) - this.getScore(a));
833
+ return relays.toSorted((a, b) => this.getScore(b) - this.getScore(a));
945
834
  }
946
835
  };
947
836
  }
948
- /**
949
- * Start the Nostr DM bus - subscribes to NIP-04 encrypted DMs
950
- */
837
+ /** Subscribe to NIP-04 encrypted DMs. */
951
838
  async function startNostrBus(options) {
952
839
  const { privateKey, relays = DEFAULT_RELAYS, onMessage, authorizeSender, onError, onEose, onMetric } = options;
953
840
  const sk = validatePrivateKey(privateKey);
@@ -956,14 +843,7 @@ async function startNostrBus(options) {
956
843
  pool.onRelayConnectionSuccess = options.onConnect;
957
844
  const accountId = options.accountId ?? pk.slice(0, 16);
958
845
  const gatewayStartedAt = Math.floor(Date.now() / 1e3);
959
- const guardPolicy = createDirectDmPreCryptoGuardPolicy({
960
- ...DEFAULT_INBOUND_GUARD_POLICY,
961
- ...options.guardPolicy,
962
- rateLimit: {
963
- ...DEFAULT_INBOUND_GUARD_POLICY.rateLimit,
964
- ...options.guardPolicy?.rateLimit
965
- }
966
- });
846
+ const guardPolicy = createDirectDmPreCryptoGuardPolicy(options.guardPolicy);
967
847
  const metrics = onMetric ? createMetrics(onMetric) : createNoopMetrics();
968
848
  const circuitBreakers = /* @__PURE__ */ new Map();
969
849
  const healthTracker = createRelayHealthTracker();
@@ -1004,27 +884,15 @@ async function startNostrBus(options) {
1004
884
  const updateRateLimiterSizeMetric = () => {
1005
885
  metrics.emit("memory.rate_limiter_entries", perSenderRateLimiter.size() + globalRateLimiter.size());
1006
886
  };
1007
- const rejectIfGlobalRateLimited = () => {
1008
- updateRateLimiterSizeMetric();
1009
- if (globalRateLimiter.isRateLimited("global")) {
1010
- metrics.emit("rate_limit.global");
1011
- metrics.emit("event.rejected.rate_limited");
1012
- updateRateLimiterSizeMetric();
1013
- return true;
1014
- }
1015
- updateRateLimiterSizeMetric();
1016
- return false;
1017
- };
1018
- const rejectIfVerifiedSenderRateLimited = (senderPubkey) => {
887
+ const rejectIfRateLimited = (limiter, key, metric) => {
1019
888
  updateRateLimiterSizeMetric();
1020
- if (perSenderRateLimiter.isRateLimited(senderPubkey)) {
1021
- metrics.emit("rate_limit.per_sender");
889
+ const limited = limiter.isRateLimited(key);
890
+ if (limited) {
891
+ metrics.emit(metric);
1022
892
  metrics.emit("event.rejected.rate_limited");
1023
- updateRateLimiterSizeMetric();
1024
- return true;
1025
893
  }
1026
894
  updateRateLimiterSizeMetric();
1027
- return false;
895
+ return limited;
1028
896
  };
1029
897
  async function dispatchEvent(event, lifecycle) {
1030
898
  if (event.pubkey === pk) {
@@ -1039,12 +907,7 @@ async function startNostrBus(options) {
1039
907
  metrics.emit("event.rejected.wrong_kind");
1040
908
  return;
1041
909
  }
1042
- let targetsUs = false;
1043
- for (const tag of event.tags) if (tag[0] === "p" && tag[1] === pk) {
1044
- targetsUs = true;
1045
- break;
1046
- }
1047
- if (!targetsUs) {
910
+ if (!event.tags.some((tag) => tag[0] === "p" && tag[1] === pk)) {
1048
911
  metrics.emit("event.rejected.wrong_kind");
1049
912
  return;
1050
913
  }
@@ -1052,18 +915,18 @@ async function startNostrBus(options) {
1052
915
  await sendEncryptedDm(pool, sk, event.pubkey, text, relays, metrics, circuitBreakers, healthTracker, onError, { replyToEventId: event.id });
1053
916
  };
1054
917
  if (Buffer.byteLength(event.content, "utf8") > guardPolicy.maxCiphertextBytes) {
1055
- if (rejectIfGlobalRateLimited()) throw new Error(`Nostr event ${event.id} hit the global rate limit.`);
918
+ if (rejectIfRateLimited(globalRateLimiter, "global", "rate_limit.global")) throw new Error(`Nostr event ${event.id} hit the global rate limit.`);
1056
919
  metrics.emit("event.rejected.oversized_ciphertext");
1057
920
  return;
1058
921
  }
1059
- if (rejectIfGlobalRateLimited()) throw new Error(`Nostr event ${event.id} hit the global rate limit.`);
922
+ if (rejectIfRateLimited(globalRateLimiter, "global", "rate_limit.global")) throw new Error(`Nostr event ${event.id} hit the global rate limit.`);
1060
923
  if (!verifyEvent(event)) {
1061
924
  metrics.emit("event.rejected.invalid_signature");
1062
925
  const error = new NostrIngressPermanentError("invalid-signature", `Nostr event ${event.id} has an invalid signature.`);
1063
926
  onError?.(error, `event ${event.id}`);
1064
927
  throw error;
1065
928
  }
1066
- if (rejectIfVerifiedSenderRateLimited(event.pubkey)) throw new Error(`Nostr sender ${event.pubkey} hit the rate limit.`);
929
+ if (rejectIfRateLimited(perSenderRateLimiter, event.pubkey, "rate_limit.per_sender")) throw new Error(`Nostr sender ${event.pubkey} hit the rate limit.`);
1067
930
  if (authorizeSender) {
1068
931
  if (await authorizeSender({
1069
932
  senderPubkey: event.pubkey,
@@ -1137,9 +1000,6 @@ async function startNostrBus(options) {
1137
1000
  const cursor = durableCursor.recordTransientRejection(event);
1138
1001
  if (cursor !== void 0) await cursorWriter.persistNow(cursor);
1139
1002
  };
1140
- const recoverCursorPersistence = async () => {
1141
- await cursorWriter.flushUntilSuccess();
1142
- };
1143
1003
  const handleRelayEvent = async (event) => {
1144
1004
  metrics.emit("event.received");
1145
1005
  if (typeof event.created_at === "number" && event.created_at < since) {
@@ -1160,7 +1020,7 @@ async function startNostrBus(options) {
1160
1020
  } catch (cursorError) {
1161
1021
  onError?.(cursorError, "persist transient replay cursor");
1162
1022
  await stopRelays("cursor persistence failed");
1163
- await recoverCursorPersistence();
1023
+ await cursorWriter.flushUntilSuccess();
1164
1024
  }
1165
1025
  return;
1166
1026
  }
@@ -1173,7 +1033,7 @@ async function startNostrBus(options) {
1173
1033
  cursorPersistenceFailed = true;
1174
1034
  }
1175
1035
  await stopRelays("durable admission failed");
1176
- if (cursorPersistenceFailed) await recoverCursorPersistence();
1036
+ if (cursorPersistenceFailed) await cursorWriter.flushUntilSuccess();
1177
1037
  }
1178
1038
  };
1179
1039
  let backfillFinalizePromise;
@@ -1213,9 +1073,6 @@ async function startNostrBus(options) {
1213
1073
  await Promise.allSettled([stopRelays("startup failed"), ingress.stop()]);
1214
1074
  throw error;
1215
1075
  }
1216
- const sendDm = async (toPubkey, text, sendOptions) => {
1217
- return await sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circuitBreakers, healthTracker, onError, sendOptions);
1218
- };
1219
1076
  const publishProfile$1 = async (profile) => {
1220
1077
  const lastPublishedAt = (await readNostrProfileState({ accountId }))?.lastPublishedAt ?? void 0;
1221
1078
  const result = await publishProfile(pool, sk, relays, profile, lastPublishedAt);
@@ -1253,15 +1110,12 @@ async function startNostrBus(options) {
1253
1110
  return {
1254
1111
  close,
1255
1112
  publicKey: pk,
1256
- sendDm,
1113
+ sendDm: (toPubkey, text, sendOptions) => sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circuitBreakers, healthTracker, onError, sendOptions),
1257
1114
  getMetrics: () => metrics.getSnapshot(),
1258
1115
  publishProfile: publishProfile$1,
1259
1116
  getProfileState
1260
1117
  };
1261
1118
  }
1262
- /**
1263
- * Send an encrypted DM to a pubkey
1264
- */
1265
1119
  async function sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circuitBreakers, healthTracker, onError, options) {
1266
1120
  const ciphertext = encrypt(sk, toPubkey, text);
1267
1121
  const tags = [["p", toPubkey]];
@@ -1420,7 +1274,7 @@ const startNostrGatewayAccount = async (ctx) => {
1420
1274
  ctx.log?.warn?.(`[${account.accountId}] dropping Nostr DM after preflight drift (${senderPubkey}, ${resolvedAccess.senderAccess.reasonCode})`);
1421
1275
  return;
1422
1276
  }
1423
- const { dispatchInboundDirectDm } = await import("./inbound-direct-dm-runtime-DHn9NoYN.mjs");
1277
+ const { dispatchInboundDirectDm } = await import("openclaw/plugin-sdk/channel-inbound");
1424
1278
  await dispatchInboundDirectDm({
1425
1279
  channelRuntime,
1426
1280
  resolveChannelIngress: async (contextBinding) => {
@@ -1446,8 +1300,7 @@ const startNostrGatewayAccount = async (ctx) => {
1446
1300
  commandAuthorized: resolvedAccess.commandAccess.requested ? resolvedAccess.commandAccess.authorized : void 0,
1447
1301
  turnAdoptionLifecycle: bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle,
1448
1302
  deliver: async (payload) => {
1449
- const outboundText = payload && typeof payload === "object" && "text" in payload ? payload.text ?? "" : "";
1450
- const sanitizedText = sanitizeAssistantVisibleText(outboundText);
1303
+ const sanitizedText = sanitizeAssistantVisibleText(payload.text ?? "");
1451
1304
  if (!sanitizedText) return;
1452
1305
  const tableMode = runtime.channel.text.resolveMarkdownTableMode({
1453
1306
  cfg: ctx.cfg,
@@ -1627,10 +1480,7 @@ const nostrConfigAdapter = createTopLevelChannelConfigAdapter({
1627
1480
  "profile"
1628
1481
  ],
1629
1482
  resolveAllowFrom: (account) => account.config.allowFrom,
1630
- formatAllowFrom: (allowFrom) => normalizeStringEntries(allowFrom).map((entry) => {
1631
- if (entry === "*") return "*";
1632
- return normalizeNostrTarget(entry);
1633
- }).filter(Boolean)
1483
+ formatAllowFrom: (allowFrom) => normalizeStringEntries(allowFrom).map(normalizeNostrTarget).filter(Boolean)
1634
1484
  });
1635
1485
  const nostrMessageAdapter = createChannelMessageAdapterFromOutbound({
1636
1486
  id: "nostr",
@@ -1698,10 +1548,10 @@ const nostrPlugin = createChatChannelPlugin({
1698
1548
  },
1699
1549
  hint: NOSTR_TARGET_HINT
1700
1550
  },
1701
- resolveOutboundSessionRoute: (params) => resolveNostrOutboundSessionRoute(params)
1551
+ resolveOutboundSessionRoute: resolveNostrOutboundSessionRoute
1702
1552
  },
1703
1553
  message: nostrMessageAdapter,
1704
- status: { ...createComputedAccountStatusAdapter({
1554
+ status: createComputedAccountStatusAdapter({
1705
1555
  defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
1706
1556
  collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("nostr", accounts),
1707
1557
  buildChannelSummary: ({ snapshot }) => buildPassiveChannelStatusSummary(snapshot, { publicKey: snapshot.publicKey ?? null }),
@@ -1716,31 +1566,19 @@ const nostrPlugin = createChatChannelPlugin({
1716
1566
  ...buildTrafficStatusSummary(runtime)
1717
1567
  }
1718
1568
  })
1719
- }) },
1569
+ }),
1720
1570
  gateway: { startAccount: startNostrGatewayAccount }
1721
1571
  },
1722
1572
  pairing: { text: nostrPairingTextAdapter },
1723
1573
  security: { resolveDmPolicy: resolveNostrDmPolicy },
1724
1574
  outbound: nostrPluginOutboundAdapter
1725
1575
  });
1726
- /**
1727
- * Publish a profile (kind:0) for a Nostr account.
1728
- * @param accountId - Account ID (defaults to "default")
1729
- * @param profile - Profile data to publish
1730
- * @returns Publish results with successes and failures
1731
- * @throws Error if account is not running
1732
- */
1733
1576
  async function publishNostrProfile(accountId, profile) {
1734
1577
  const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
1735
1578
  const bus = getActiveNostrBuses().get(resolvedAccountId);
1736
1579
  if (!bus) throw new Error(`Nostr bus not running for account ${resolvedAccountId}`);
1737
1580
  return bus.publishProfile(profile);
1738
1581
  }
1739
- /**
1740
- * Get profile publish state for a Nostr account.
1741
- * @param accountId - Account ID (defaults to "default")
1742
- * @returns Profile publish state or null if account not running
1743
- */
1744
1582
  async function getNostrProfileState(accountId = DEFAULT_ACCOUNT_ID) {
1745
1583
  const bus = getActiveNostrBuses().get(accountId);
1746
1584
  if (!bus) return null;
@@ -1,5 +1,3 @@
1
- import { collectStatusIssuesFromLastError, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
2
- import { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, formatPairingApproveHint } from "openclaw/plugin-sdk/channel-plugin-common";
3
1
  import { AllowFromListSchema, DmPolicySchema, MarkdownConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
4
2
  import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";
5
3
  import { z } from "zod";
@@ -61,4 +59,4 @@ const NostrConfigSchema = z.object({
61
59
  profile: NostrProfileSchema.optional()
62
60
  });
63
61
  //#endregion
64
- export { collectStatusIssuesFromLastError as a, buildChannelConfigSchema as i, NostrProfileSchema as n, createDefaultChannelRuntimeState as o, DEFAULT_ACCOUNT_ID as r, formatPairingApproveHint as s, NostrConfigSchema as t };
62
+ export { NostrProfileSchema as n, NostrConfigSchema as t };
@@ -0,0 +1,2 @@
1
+ import { t as nostrSetupPlugin } from "./setup-plugin-api-Ci0XxWqb.mjs";
2
+ export { nostrSetupPlugin };
@@ -1,6 +1,7 @@
1
- import { i as buildChannelConfigSchema, t as NostrConfigSchema } from "./config-schema-CkYd8ACn.mjs";
1
+ import { t as NostrConfigSchema } from "./config-schema-B3uU2qBS.mjs";
2
2
  import { i as createNostrSetupStatus, l as DEFAULT_RELAYS, n as createNostrSetupAdapter, o as hasConfiguredNostrPrivateKey, r as createNostrSetupContract, s as resolveNostrPrivateKey } from "./setup-adapter-KJ_iGKiG.mjs";
3
3
  import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
4
+ import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-plugin-common";
4
5
  import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
5
6
  import { createDelegatedSetupWizardProxy } from "openclaw/plugin-sdk/setup-runtime";
6
7
  //#region extensions/nostr/src/channel.setup.ts
package/dist/api.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { s as resolveNostrAccount } from "./.setup/setup-surface-Dx8QxWSP.mjs";
2
2
  import { getPluginRuntimeGatewayRequestScope } from "./runtime-api.js";
3
- import { n as NostrProfileSchema } from "./.setup/config-schema-CkYd8ACn.mjs";
4
- import { a as getNostrRuntime, i as contentToProfile, n as nostrPlugin, o as setNostrRuntime, r as publishNostrProfile, t as getNostrProfileState } from "./.setup/channel-D0f0Q9uV.mjs";
3
+ import { n as NostrProfileSchema } from "./.setup/config-schema-B3uU2qBS.mjs";
4
+ import { a as getNostrRuntime, i as contentToProfile, n as nostrPlugin, o as setNostrRuntime, r as publishNostrProfile, t as getNostrProfileState } from "./.setup/channel-D2d-FP8G.mjs";
5
+ import { getPluginRuntimeGatewayRequestScope as getPluginRuntimeGatewayRequestScope$1 } from "openclaw/plugin-sdk/plugin-runtime";
5
6
  import { isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
6
7
  import { z } from "zod";
7
8
  import { SimplePool } from "nostr-tools";
8
- import { createFixedWindowRateLimiter as createFixedWindowRateLimiter$1 } from "openclaw/plugin-sdk/webhook-ingress";
9
+ import { createFixedWindowRateLimiter } from "openclaw/plugin-sdk/webhook-ingress";
9
10
  import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
10
11
  import { isBlockedHostnameOrIp, isLoopbackHost } from "openclaw/plugin-sdk/ssrf-runtime";
11
12
  import { readJsonBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-request-guards";
@@ -50,17 +51,7 @@ function validateUrlSafety(urlStr) {
50
51
  }
51
52
  //#endregion
52
53
  //#region extensions/nostr/src/nostr-profile-import.ts
53
- /**
54
- * Nostr Profile Import
55
- *
56
- * Fetches and verifies kind:0 profile events from relays.
57
- * Used to import existing profiles before editing.
58
- */
59
54
  const DEFAULT_TIMEOUT_MS = 5e3;
60
- /**
61
- * Sanitize URLs in an imported profile to prevent SSRF attacks.
62
- * Removes any URLs that don't pass SSRF validation.
63
- */
64
55
  function sanitizeProfileUrls(profile) {
65
56
  const result = { ...profile };
66
57
  for (const field of [
@@ -75,14 +66,7 @@ function sanitizeProfileUrls(profile) {
75
66
  }
76
67
  return result;
77
68
  }
78
- /**
79
- * Fetch the latest kind:0 profile event for a pubkey from relays.
80
- *
81
- * - Queries all relays in parallel
82
- * - Takes the event with the highest created_at
83
- * - Verifies the event signature
84
- * - Parses and returns the profile
85
- */
69
+ /** Import the latest verified kind:0 profile across the configured relays. */
86
70
  async function importProfileFromRelays(opts) {
87
71
  const { pubkey, relays } = opts;
88
72
  const timeoutMs = resolveTimerTimeoutMs(opts.timeoutMs, DEFAULT_TIMEOUT_MS);
@@ -179,13 +163,7 @@ async function importProfileFromRelays(opts) {
179
163
  pool.close(relays);
180
164
  }
181
165
  }
182
- /**
183
- * Merge imported profile with local profile.
184
- *
185
- * Strategy:
186
- * - For each field, prefer local if set, otherwise use imported
187
- * - This preserves user customizations while filling in missing data
188
- */
166
+ /** Preserve local customizations while filling missing fields from the imported profile. */
189
167
  function mergeProfiles(local, imported) {
190
168
  if (!imported) return local ?? {};
191
169
  if (!local) return imported;
@@ -202,18 +180,12 @@ function mergeProfiles(local, imported) {
202
180
  }
203
181
  //#endregion
204
182
  //#region extensions/nostr/src/nostr-profile-http.ts
205
- const profileRateLimiter = createFixedWindowRateLimiter$1({
183
+ const profileRateLimiter = createFixedWindowRateLimiter({
206
184
  windowMs: 6e4,
207
185
  maxRequests: 5,
208
186
  maxTrackedKeys: 2048
209
187
  });
210
- function checkRateLimit(accountId) {
211
- return !profileRateLimiter.isRateLimited(accountId);
212
- }
213
188
  const publishLocks = new KeyedAsyncQueue();
214
- async function withPublishLock(accountId, fn) {
215
- return await publishLocks.enqueue(accountId, fn);
216
- }
217
189
  const nip05FormatSchema = z.string().regex(/^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,}$/i, "Invalid NIP-05 format (user@domain.com)").optional();
218
190
  const lud16FormatSchema = z.string().regex(/^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,}$/i, "Invalid Lightning address format").optional();
219
191
  const ProfileUpdateSchema = NostrProfileSchema.extend({
@@ -228,14 +200,13 @@ const PROFILE_URL_FIELDS = [
228
200
  ];
229
201
  function normalizeProfileUpdateUrlInputs(value) {
230
202
  if (!isRecord(value)) return value;
231
- const record = value;
232
203
  let normalized;
233
204
  for (const field of PROFILE_URL_FIELDS) {
234
- const input = record[field];
205
+ const input = value[field];
235
206
  if (typeof input !== "string") continue;
236
207
  const next = normalizeNostrProfileUrlForRuntime(input);
237
208
  if (next !== input) {
238
- normalized ??= { ...record };
209
+ normalized ??= { ...value };
239
210
  normalized[field] = next;
240
211
  }
241
212
  }
@@ -243,12 +214,11 @@ function normalizeProfileUpdateUrlInputs(value) {
243
214
  }
244
215
  function restoreProfileUpdateUrlInputs(profile, value) {
245
216
  if (!isRecord(value)) return profile;
246
- const record = value;
247
217
  return {
248
218
  ...profile,
249
- ...typeof record.picture === "string" ? { picture: record.picture } : {},
250
- ...typeof record.banner === "string" ? { banner: record.banner } : {},
251
- ...typeof record.website === "string" ? { website: record.website } : {}
219
+ ...typeof value.picture === "string" ? { picture: value.picture } : {},
220
+ ...typeof value.banner === "string" ? { banner: value.banner } : {},
221
+ ...typeof value.website === "string" ? { website: value.website } : {}
252
222
  };
253
223
  }
254
224
  function sendJson(res, status, body) {
@@ -264,8 +234,7 @@ async function readJsonBody(req, maxBytes = 65536, timeoutMs = 3e4) {
264
234
  });
265
235
  if (result.ok) return result.value;
266
236
  if (result.code === "PAYLOAD_TOO_LARGE") throw new Error("Request body too large");
267
- if (result.code === "REQUEST_BODY_TIMEOUT") throw new Error(requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
268
- if (result.code === "CONNECTION_CLOSED") throw new Error(requestBodyErrorToText("CONNECTION_CLOSED"));
237
+ if (result.code === "REQUEST_BODY_TIMEOUT" || result.code === "CONNECTION_CLOSED") throw new Error(requestBodyErrorToText(result.code));
269
238
  throw new Error(result.code === "INVALID_JSON" ? "Invalid JSON" : result.error);
270
239
  }
271
240
  function parseAccountIdFromPath(pathname) {
@@ -361,7 +330,7 @@ function enforceLoopbackMutationGuards(ctx, req, res) {
361
330
  return true;
362
331
  }
363
332
  function enforceGatewayMutationScope(ctx, accountId, res) {
364
- const runtimeScopes = getPluginRuntimeGatewayRequestScope()?.client?.connect?.scopes;
333
+ const runtimeScopes = getPluginRuntimeGatewayRequestScope$1()?.client?.connect?.scopes;
365
334
  if ((Array.isArray(runtimeScopes) ? runtimeScopes : []).includes(PROFILE_MUTATION_SCOPE)) return true;
366
335
  ctx.log?.warn?.(`[${accountId}] Rejected profile mutation missing ${PROFILE_MUTATION_SCOPE}`);
367
336
  sendJson(res, 403, {
@@ -380,8 +349,10 @@ function createNostrProfileHttpHandler(ctx) {
380
349
  if (!(url.pathname.endsWith("/profile") || isImport)) return false;
381
350
  try {
382
351
  if (req.method === "GET" && !isImport) return await handleGetProfile(accountId, ctx, res);
383
- if (req.method === "PUT" && !isImport) return await handleUpdateProfile(accountId, ctx, req, res);
384
- if (req.method === "POST" && isImport) return await handleImportProfile(accountId, ctx, req, res);
352
+ if (req.method === "PUT" && !isImport || req.method === "POST" && isImport) {
353
+ if (!enforceGatewayMutationScope(ctx, accountId, res) || !enforceLoopbackMutationGuards(ctx, req, res)) return true;
354
+ return await (isImport ? handleImportProfile : handleUpdateProfile)(accountId, ctx, req, res);
355
+ }
385
356
  sendJson(res, 405, {
386
357
  ok: false,
387
358
  error: "Method not allowed"
@@ -409,9 +380,7 @@ async function handleGetProfile(accountId, ctx, res) {
409
380
  return true;
410
381
  }
411
382
  async function handleUpdateProfile(accountId, ctx, req, res) {
412
- if (!enforceGatewayMutationScope(ctx, accountId, res)) return true;
413
- if (!enforceLoopbackMutationGuards(ctx, req, res)) return true;
414
- if (!checkRateLimit(accountId)) {
383
+ if (profileRateLimiter.isRateLimited(accountId)) {
415
384
  sendJson(res, 429, {
416
385
  ok: false,
417
386
  error: "Rate limit exceeded (5 requests/minute)"
@@ -438,32 +407,14 @@ async function handleUpdateProfile(accountId, ctx, req, res) {
438
407
  return true;
439
408
  }
440
409
  const profile = restoreProfileUpdateUrlInputs(parseResult.data, body);
441
- if (profile.picture) {
442
- const pictureCheck = validateUrlSafety(profile.picture);
443
- if (!pictureCheck.ok) {
444
- sendJson(res, 400, {
445
- ok: false,
446
- error: `picture: ${pictureCheck.error}`
447
- });
448
- return true;
449
- }
450
- }
451
- if (profile.banner) {
452
- const bannerCheck = validateUrlSafety(profile.banner);
453
- if (!bannerCheck.ok) {
454
- sendJson(res, 400, {
455
- ok: false,
456
- error: `banner: ${bannerCheck.error}`
457
- });
458
- return true;
459
- }
460
- }
461
- if (profile.website) {
462
- const websiteCheck = validateUrlSafety(profile.website);
463
- if (!websiteCheck.ok) {
410
+ for (const field of PROFILE_URL_FIELDS) {
411
+ const url = profile[field];
412
+ if (!url) continue;
413
+ const check = validateUrlSafety(url);
414
+ if (!check.ok) {
464
415
  sendJson(res, 400, {
465
416
  ok: false,
466
- error: `website: ${websiteCheck.error}`
417
+ error: `${field}: ${check.error}`
467
418
  });
468
419
  return true;
469
420
  }
@@ -473,12 +424,12 @@ async function handleUpdateProfile(accountId, ctx, req, res) {
473
424
  ...profile
474
425
  };
475
426
  try {
476
- const result = await withPublishLock(accountId, async () => {
477
- await getPluginRuntimeGatewayRequestScope()?.revalidate?.();
427
+ const result = await publishLocks.enqueue(accountId, async () => {
428
+ await getPluginRuntimeGatewayRequestScope$1()?.revalidate?.();
478
429
  return await publishNostrProfile(accountId, mergedProfile);
479
430
  });
480
431
  if (result.successes.length > 0) {
481
- await getPluginRuntimeGatewayRequestScope()?.revalidate?.();
432
+ await getPluginRuntimeGatewayRequestScope$1()?.revalidate?.();
482
433
  await ctx.updateConfigProfile(accountId, mergedProfile);
483
434
  ctx.log?.info(`[${accountId}] Profile published to ${result.successes.length} relay(s)`);
484
435
  } else ctx.log?.warn(`[${accountId}] Profile publish failed on all relays`);
@@ -501,8 +452,6 @@ async function handleUpdateProfile(accountId, ctx, req, res) {
501
452
  return true;
502
453
  }
503
454
  async function handleImportProfile(accountId, ctx, req, res) {
504
- if (!enforceGatewayMutationScope(ctx, accountId, res)) return true;
505
- if (!enforceLoopbackMutationGuards(ctx, req, res)) return true;
506
455
  const accountInfo = ctx.getAccountInfo(accountId);
507
456
  if (!accountInfo) {
508
457
  sendJson(res, 404, {
@@ -524,7 +473,7 @@ async function handleImportProfile(accountId, ctx, req, res) {
524
473
  const body = await readJsonBody(req);
525
474
  if (typeof body === "object" && body !== null) autoMerge = body.autoMerge === true;
526
475
  } catch {}
527
- await getPluginRuntimeGatewayRequestScope()?.revalidate?.();
476
+ await getPluginRuntimeGatewayRequestScope$1()?.revalidate?.();
528
477
  ctx.log?.info(`[${accountId}] Importing profile for ${pubkey.slice(0, 8)}...`);
529
478
  const result = await importProfileFromRelays({
530
479
  pubkey,
@@ -539,26 +488,18 @@ async function handleImportProfile(accountId, ctx, req, res) {
539
488
  });
540
489
  return true;
541
490
  }
491
+ let merged;
542
492
  if (autoMerge && result.profile) {
543
- await getPluginRuntimeGatewayRequestScope()?.revalidate?.();
544
- const merged = mergeProfiles(ctx.getConfigProfile(accountId), result.profile);
493
+ await getPluginRuntimeGatewayRequestScope$1()?.revalidate?.();
494
+ merged = mergeProfiles(ctx.getConfigProfile(accountId), result.profile);
545
495
  await ctx.updateConfigProfile(accountId, merged);
546
496
  ctx.log?.info(`[${accountId}] Profile imported and merged`);
547
- sendJson(res, 200, {
548
- ok: true,
549
- imported: result.profile,
550
- merged,
551
- saved: true,
552
- event: result.event,
553
- sourceRelay: result.sourceRelay,
554
- relaysQueried: result.relaysQueried
555
- });
556
- return true;
557
497
  }
558
498
  sendJson(res, 200, {
559
499
  ok: true,
560
500
  imported: result.profile,
561
- saved: false,
501
+ ...merged ? { merged } : {},
502
+ saved: merged !== void 0,
562
503
  event: result.event,
563
504
  sourceRelay: result.sourceRelay,
564
505
  relaysQueried: result.relaysQueried
@@ -1,2 +1,2 @@
1
- import { n as nostrPlugin } from "./.setup/channel-D0f0Q9uV.mjs";
1
+ import { n as nostrPlugin } from "./.setup/channel-D2d-FP8G.mjs";
2
2
  export { nostrPlugin };
@@ -1,143 +1,31 @@
1
- import { t as normalizeNostrStateAccountId } from "./.setup/state-account-id-CvBZ9s6P.mjs";
2
- import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
1
+ import { extractErrorCode } from "openclaw/plugin-sdk/error-runtime";
3
2
  import fs from "node:fs/promises";
4
3
  import path from "node:path";
5
- import { archiveLegacyStateSource } from "openclaw/plugin-sdk/runtime-doctor-migrations";
6
4
  //#region extensions/nostr/doctor-contract-api.ts
7
- const BUS_STATE_NAMESPACE = "bus-state";
8
- const PROFILE_STATE_NAMESPACE = "profile-state";
9
- const MAX_NOSTR_STATE_ENTRIES = 256;
10
- function parseBusState(value) {
11
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
12
- const parsed = value;
13
- if (parsed.version !== 1 && parsed.version !== 2) return null;
14
- return {
15
- version: 2,
16
- lastProcessedAt: asFiniteNumber(parsed.lastProcessedAt) ?? null,
17
- gatewayStartedAt: asFiniteNumber(parsed.gatewayStartedAt) ?? null,
18
- recentEventIds: parsed.version === 2 && Array.isArray(parsed.recentEventIds) ? parsed.recentEventIds.filter((entry) => typeof entry === "string") : []
19
- };
20
- }
21
- function parseProfileState(value) {
22
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
23
- const parsed = value;
24
- if (parsed.version !== 1) return null;
25
- const rawResults = parsed.lastPublishResults;
26
- const lastPublishResults = {};
27
- if (rawResults && typeof rawResults === "object" && !Array.isArray(rawResults)) {
28
- for (const [relay, result] of Object.entries(rawResults)) if (result === "ok" || result === "failed" || result === "timeout") lastPublishResults[relay] = result;
29
- }
30
- return {
31
- version: 1,
32
- lastPublishedAt: asFiniteNumber(parsed.lastPublishedAt) ?? null,
33
- lastPublishedEventId: typeof parsed.lastPublishedEventId === "string" ? parsed.lastPublishedEventId : null,
34
- lastPublishResults: rawResults === null || Object.keys(lastPublishResults).length === 0 ? null : lastPublishResults
35
- };
36
- }
37
- async function readJsonFile(filePath) {
38
- return JSON.parse(await fs.readFile(filePath, "utf8"));
39
- }
40
- async function listLegacyFiles(params) {
41
- const dir = path.join(params.stateDir, "nostr");
42
- let entries;
43
- try {
44
- entries = await fs.readdir(dir, { withFileTypes: true });
45
- } catch {
46
- return [];
47
- }
48
- const suffix = ".json";
49
- const files = [];
50
- for (const entry of entries) {
51
- if (!entry.isFile() || !entry.name.startsWith(params.prefix) || !entry.name.endsWith(suffix)) continue;
52
- const rawAccountId = entry.name.slice(params.prefix.length, -5);
53
- const accountId = normalizeNostrStateAccountId(rawAccountId);
54
- const filePath = path.join(dir, entry.name);
5
+ function retiredNostrStateMigration(namespace, label) {
6
+ const warning = `${label} JSON imports were retired. Upgrade to OpenClaw 2026.9.5 and run openclaw doctor --fix before upgrading to the latest version. Legacy files were left untouched.`;
7
+ const hasLegacyState = async (stateDir) => {
55
8
  try {
56
- const value = params.parse(await readJsonFile(filePath));
57
- if (value) files.push({
58
- accountId,
59
- filePath,
60
- value
61
- });
62
- } catch {}
63
- }
64
- return files;
65
- }
66
- async function ensureStoreCapacity(params) {
67
- const existingKeys = new Set((await params.store.entries()).map((entry) => entry.key));
68
- const missingKeys = new Set(params.files.map((file) => file.accountId).filter((key) => !existingKeys.has(key)));
69
- if (missingKeys.size > params.maxEntries - existingKeys.size) {
70
- 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`);
71
- return null;
72
- }
73
- return existingKeys;
74
- }
75
- function createNostrStateMigration(options) {
9
+ return (await fs.readdir(path.join(stateDir, "nostr"))).some((name) => name.startsWith(`${namespace}-`) && name.endsWith(".json"));
10
+ } catch (error) {
11
+ if (extractErrorCode(error) === "ENOENT") return false;
12
+ throw error;
13
+ }
14
+ };
76
15
  return {
77
- id: `nostr-${options.namespace}-json-to-plugin-state`,
78
- label: options.label,
79
- async detectLegacyState(params) {
80
- const files = await listLegacyFiles({
81
- stateDir: params.stateDir,
82
- prefix: `${options.namespace}-`,
83
- parse: options.parse
84
- });
85
- if (files.length === 0) return null;
86
- return { preview: [`- ${options.label}: ${files.length} ${files.length === 1 ? "account" : "accounts"} -> plugin state (${options.namespace})`] };
16
+ id: `nostr-${namespace}-json-to-plugin-state`,
17
+ label,
18
+ async detectLegacyState({ stateDir }) {
19
+ return await hasLegacyState(stateDir) ? { preview: [warning] } : null;
87
20
  },
88
- async migrateLegacyState(params) {
89
- const changes = [];
90
- const warnings = [];
91
- const files = await listLegacyFiles({
92
- stateDir: params.stateDir,
93
- prefix: `${options.namespace}-`,
94
- parse: options.parse
95
- });
96
- const store = params.context.openPluginStateKeyedStore({
97
- namespace: options.namespace,
98
- maxEntries: MAX_NOSTR_STATE_ENTRIES
99
- });
100
- const existingKeys = await ensureStoreCapacity({
101
- files,
102
- store,
103
- maxEntries: MAX_NOSTR_STATE_ENTRIES,
104
- label: options.label,
105
- warnings
106
- });
107
- if (!existingKeys) return {
108
- changes,
109
- warnings
110
- };
111
- let imported = 0;
112
- for (const file of files) {
113
- if (!existingKeys.has(file.accountId)) {
114
- await store.register(file.accountId, file.value);
115
- existingKeys.add(file.accountId);
116
- imported++;
117
- }
118
- await archiveLegacyStateSource({
119
- filePath: file.filePath,
120
- label: options.label,
121
- changes,
122
- warnings
123
- });
124
- }
125
- if (imported > 0) changes.unshift(`Migrated ${imported} Nostr ${options.namespace} ${imported === 1 ? "entry" : "entries"} -> plugin state`);
21
+ async migrateLegacyState({ stateDir }) {
126
22
  return {
127
- changes,
128
- warnings
23
+ changes: [],
24
+ warnings: await hasLegacyState(stateDir) ? [warning] : []
129
25
  };
130
26
  }
131
27
  };
132
28
  }
133
- const stateMigrations = [createNostrStateMigration({
134
- namespace: BUS_STATE_NAMESPACE,
135
- label: "Nostr bus state",
136
- parse: parseBusState
137
- }), createNostrStateMigration({
138
- namespace: PROFILE_STATE_NAMESPACE,
139
- label: "Nostr profile state",
140
- parse: parseProfileState
141
- })];
29
+ const stateMigrations = [retiredNostrStateMigration("bus-state", "Nostr bus state"), retiredNostrStateMigration("profile-state", "Nostr profile state")];
142
30
  //#endregion
143
31
  export { stateMigrations };
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  var setup_entry_default = defineBundledChannelSetupEntry({
5
5
  importMetaUrl: import.meta.url,
6
6
  plugin: {
7
- specifier: fileURLToPath(new URL(".setup/setup-plugin-api-B7i5rR6T.mjs", import.meta.url).href),
7
+ specifier: fileURLToPath(new URL(".setup/setup-plugin-api-BhRAS2I1.mjs", import.meta.url).href),
8
8
  exportName: "nostrSetupPlugin"
9
9
  },
10
10
  secrets: {
@@ -1,2 +1,2 @@
1
- import { t as nostrSetupPlugin } from "./.setup/setup-plugin-api-B7qtJE84.mjs";
1
+ import { t as nostrSetupPlugin } from "./.setup/setup-plugin-api-Ci0XxWqb.mjs";
2
2
  export { nostrSetupPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nostr",
3
- "version": "2026.9.6",
3
+ "version": "2026.9.8",
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.6.5"
13
13
  },
14
14
  "peerDependencies": {
15
- "openclaw": ">=2026.9.6"
15
+ "openclaw": ">=2026.9.8"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "openclaw": {
@@ -80,10 +80,10 @@
80
80
  "minHostVersion": ">=2026.4.10"
81
81
  },
82
82
  "compat": {
83
- "pluginApi": ">=2026.9.6"
83
+ "pluginApi": ">=2026.9.8"
84
84
  },
85
85
  "build": {
86
- "openclawVersion": "2026.9.6"
86
+ "openclawVersion": "2026.9.8"
87
87
  },
88
88
  "release": {
89
89
  "publishToClawHub": true,
@@ -1,2 +0,0 @@
1
- import { dispatchInboundDirectDm } from "openclaw/plugin-sdk/channel-inbound";
2
- export { dispatchInboundDirectDm };
@@ -1,2 +0,0 @@
1
- import { t as nostrSetupPlugin } from "./setup-plugin-api-B7qtJE84.mjs";
2
- export { nostrSetupPlugin };
@@ -1,8 +0,0 @@
1
- //#region extensions/nostr/src/state-account-id.ts
2
- function normalizeNostrStateAccountId(accountId) {
3
- const trimmed = accountId?.trim();
4
- if (!trimmed) return "default";
5
- return trimmed.replace(/[^a-z0-9._-]+/gi, "_");
6
- }
7
- //#endregion
8
- export { normalizeNostrStateAccountId as t };