@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 +0 -0
- package/dist/.setup/{channel-D0f0Q9uV.mjs → channel-D2d-FP8G.mjs} +115 -277
- package/dist/.setup/{config-schema-CkYd8ACn.mjs → config-schema-B3uU2qBS.mjs} +1 -3
- package/dist/.setup/setup-plugin-api-BhRAS2I1.mjs +2 -0
- package/dist/.setup/{setup-plugin-api-B7qtJE84.mjs → setup-plugin-api-Ci0XxWqb.mjs} +2 -1
- package/dist/api.js +34 -93
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +18 -130
- package/dist/setup-entry.js +1 -1
- package/dist/setup-plugin-api.js +1 -1
- package/package.json +4 -4
- package/dist/.setup/inbound-direct-dm-runtime-DHn9NoYN.mjs +0 -2
- package/dist/.setup/setup-plugin-api-B7i5rR6T.mjs +0 -2
- package/dist/.setup/state-account-id-CvBZ9s6P.mjs +0 -8
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 {
|
|
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
|
|
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
|
-
|
|
152
|
+
onMetric?.({
|
|
107
153
|
name,
|
|
108
154
|
value,
|
|
109
155
|
timestamp: Date.now(),
|
|
110
156
|
labels
|
|
111
157
|
});
|
|
112
|
-
|
|
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 (!
|
|
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 (!
|
|
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:
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
|
668
|
-
return finalizeEvent({
|
|
597
|
+
const event = finalizeEvent({
|
|
669
598
|
kind: 0,
|
|
670
|
-
content
|
|
599
|
+
content,
|
|
671
600
|
tags: [],
|
|
672
|
-
created_at:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 >=
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
1021
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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("
|
|
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
|
|
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((
|
|
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:
|
|
1551
|
+
resolveOutboundSessionRoute: resolveNostrOutboundSessionRoute
|
|
1702
1552
|
},
|
|
1703
1553
|
message: nostrMessageAdapter,
|
|
1704
|
-
status:
|
|
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 {
|
|
62
|
+
export { NostrProfileSchema as n, NostrConfigSchema as t };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
4
|
-
import { a as getNostrRuntime, i as contentToProfile, n as nostrPlugin, o as setNostrRuntime, r as publishNostrProfile, t as getNostrProfileState } from "./.setup/channel-
|
|
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
|
|
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
|
|
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 =
|
|
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 ??= { ...
|
|
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
|
|
250
|
-
...typeof
|
|
251
|
-
...typeof
|
|
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(
|
|
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
|
|
384
|
-
|
|
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 (
|
|
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
|
-
|
|
442
|
-
const
|
|
443
|
-
if (!
|
|
444
|
-
|
|
445
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1
|
+
import { n as nostrPlugin } from "./.setup/channel-D2d-FP8G.mjs";
|
|
2
2
|
export { nostrPlugin };
|
|
@@ -1,143 +1,31 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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-${
|
|
78
|
-
label
|
|
79
|
-
async detectLegacyState(
|
|
80
|
-
|
|
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(
|
|
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 = [
|
|
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 };
|
package/dist/setup-entry.js
CHANGED
|
@@ -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-
|
|
7
|
+
specifier: fileURLToPath(new URL(".setup/setup-plugin-api-BhRAS2I1.mjs", import.meta.url).href),
|
|
8
8
|
exportName: "nostrSetupPlugin"
|
|
9
9
|
},
|
|
10
10
|
secrets: {
|
package/dist/setup-plugin-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as nostrSetupPlugin } from "./.setup/setup-plugin-api-
|
|
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.
|
|
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.
|
|
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.
|
|
83
|
+
"pluginApi": ">=2026.9.8"
|
|
84
84
|
},
|
|
85
85
|
"build": {
|
|
86
|
-
"openclawVersion": "2026.9.
|
|
86
|
+
"openclawVersion": "2026.9.8"
|
|
87
87
|
},
|
|
88
88
|
"release": {
|
|
89
89
|
"publishToClawHub": true,
|
|
@@ -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 };
|