@openclaw/nostr 2026.9.7 → 2026.10.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.setup/{channel-D2d-FP8G.mjs → channel-BaBU_Q15.mjs} +60 -76
- package/dist/.setup/{config-schema-B3uU2qBS.mjs → config-schema-HuEK1L2K.mjs} +1 -15
- package/dist/.setup/{setup-adapter-KJ_iGKiG.mjs → setup-adapter-I55hhpDp.mjs} +29 -1
- package/dist/.setup/setup-plugin-api-BUsavy-i.mjs +2 -0
- package/dist/.setup/{setup-plugin-api-Ci0XxWqb.mjs → setup-plugin-api-C_HK3q8q.mjs} +4 -28
- package/dist/.setup/{setup-surface-Dx8QxWSP.mjs → setup-surface-b-YWJSGV.mjs} +11 -31
- package/dist/api.js +25 -61
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +1 -0
- package/dist/index.js +6 -21
- package/dist/setup-api.js +1 -1
- package/dist/setup-entry.js +1 -1
- package/dist/setup-plugin-api.js +1 -1
- package/package.json +4 -4
- package/dist/.setup/setup-plugin-api-BhRAS2I1.mjs +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as listNostrAccountIds, c as normalizePubkey, n as nostrSetupContract, o as resolveDefaultNostrAccountId, r as nostrSetupWizard, s as resolveNostrAccount } from "./setup-surface-
|
|
2
|
-
import { n as NostrProfileSchema, t as NostrConfigSchema } from "./config-schema-
|
|
3
|
-
import {
|
|
1
|
+
import { a as listNostrAccountIds, c as normalizePubkey, n as nostrSetupContract, o as resolveDefaultNostrAccountId, r as nostrSetupWizard, s as resolveNostrAccount } from "./setup-surface-b-YWJSGV.mjs";
|
|
2
|
+
import { n as NostrProfileSchema, t as NostrConfigSchema } from "./config-schema-HuEK1L2K.mjs";
|
|
3
|
+
import { l as validatePrivateKey, u as DEFAULT_RELAYS } from "./setup-adapter-I55hhpDp.mjs";
|
|
4
4
|
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
5
5
|
import { createScopedDmSecurityResolver, createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
6
6
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
@@ -10,6 +10,7 @@ import { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, formatPairingApproveHint
|
|
|
10
10
|
import { buildPassiveChannelStatusSummary, buildTrafficStatusSummary } from "openclaw/plugin-sdk/extension-shared";
|
|
11
11
|
import { collectStatusIssuesFromLastError, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
12
12
|
import { isRecord, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
13
|
+
import { parseAccessGroupAllowFromEntry } from "openclaw/plugin-sdk/access-groups";
|
|
13
14
|
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
|
14
15
|
import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
|
|
15
16
|
import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
|
@@ -169,9 +170,6 @@ function createMetrics(onMetric) {
|
|
|
169
170
|
reset
|
|
170
171
|
};
|
|
171
172
|
}
|
|
172
|
-
/**
|
|
173
|
-
* Create a no-op metrics instance (for when metrics are disabled).
|
|
174
|
-
*/
|
|
175
173
|
function createNoopMetrics() {
|
|
176
174
|
const emptySnapshot = createMetricsSnapshot(createZeroMetricsState(), 0);
|
|
177
175
|
return {
|
|
@@ -313,13 +311,7 @@ function createNostrCursorStateWriter(options) {
|
|
|
313
311
|
}
|
|
314
312
|
};
|
|
315
313
|
}
|
|
316
|
-
|
|
317
|
-
constructor(reason, message, options) {
|
|
318
|
-
super(message, options);
|
|
319
|
-
this.reason = reason;
|
|
320
|
-
this.name = "NostrIngressPermanentError";
|
|
321
|
-
}
|
|
322
|
-
};
|
|
314
|
+
const NostrIngressPermanentError = createChannelIngressError("NostrIngressPermanentError", { withReason: true });
|
|
323
315
|
function requiredString(value, field) {
|
|
324
316
|
if (typeof value === "string" && value.trim()) return value;
|
|
325
317
|
throw new NostrIngressPermanentError("invalid-event", `Nostr event is missing ${field}.`);
|
|
@@ -624,7 +616,6 @@ async function publishProfile(pool, sk, relays, profile, lastPublishedAt) {
|
|
|
624
616
|
//#endregion
|
|
625
617
|
//#region extensions/nostr/src/nostr-relay-subscription.ts
|
|
626
618
|
const DEFAULT_EOSE_CONFIRM_DEADLINE_MS = 1e4;
|
|
627
|
-
const LIBRARY_EOSE_TIMEOUT_MARGIN_MS = 1e3;
|
|
628
619
|
/** Separates real relay EOSE frames from nostr-tools timeout/close synthesis. */
|
|
629
620
|
function createNostrRelaySubscriptionGroup(options) {
|
|
630
621
|
const relays = [...new Set(options.relays)];
|
|
@@ -632,7 +623,7 @@ function createNostrRelaySubscriptionGroup(options) {
|
|
|
632
623
|
const deadlineTimers = /* @__PURE__ */ new Set();
|
|
633
624
|
let locallyClosing = false;
|
|
634
625
|
const backfillStatus = new Map(relays.map((relay) => [relay, "pending"]));
|
|
635
|
-
const confirmDeadlineMs =
|
|
626
|
+
const confirmDeadlineMs = DEFAULT_EOSE_CONFIRM_DEADLINE_MS;
|
|
636
627
|
const settleBackfill = (relay, status) => {
|
|
637
628
|
if (backfillStatus.get(relay) !== "pending") return;
|
|
638
629
|
backfillStatus.set(relay, status);
|
|
@@ -673,7 +664,7 @@ function createNostrRelaySubscriptionGroup(options) {
|
|
|
673
664
|
settleBackfill(relay, "incomplete");
|
|
674
665
|
if (!locallyClosing && !options.abort.aborted) options.onClose(relay, reasons.map(({ reason }) => reason));
|
|
675
666
|
},
|
|
676
|
-
maxWait:
|
|
667
|
+
maxWait: 11e3,
|
|
677
668
|
abort: options.abort
|
|
678
669
|
}));
|
|
679
670
|
}
|
|
@@ -912,7 +903,7 @@ async function startNostrBus(options) {
|
|
|
912
903
|
return;
|
|
913
904
|
}
|
|
914
905
|
const replyTo = async (text) => {
|
|
915
|
-
await sendEncryptedDm(
|
|
906
|
+
await sendEncryptedDm(event.pubkey, text, { replyToEventId: event.id });
|
|
916
907
|
};
|
|
917
908
|
if (Buffer.byteLength(event.content, "utf8") > guardPolicy.maxCiphertextBytes) {
|
|
918
909
|
if (rejectIfRateLimited(globalRateLimiter, "global", "rate_limit.global")) throw new Error(`Nostr event ${event.id} hit the global rate limit.`);
|
|
@@ -1107,81 +1098,75 @@ async function startNostrBus(options) {
|
|
|
1107
1098
|
})();
|
|
1108
1099
|
return closePromise;
|
|
1109
1100
|
};
|
|
1101
|
+
async function sendEncryptedDm(toPubkey, text, sendOptions) {
|
|
1102
|
+
const ciphertext = encrypt(sk, toPubkey, text);
|
|
1103
|
+
const tags = [["p", toPubkey]];
|
|
1104
|
+
if (sendOptions?.replyToEventId) tags.push(["e", sendOptions.replyToEventId]);
|
|
1105
|
+
const reply = finalizeEvent({
|
|
1106
|
+
kind: 4,
|
|
1107
|
+
content: ciphertext,
|
|
1108
|
+
tags,
|
|
1109
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1110
|
+
}, sk);
|
|
1111
|
+
const sortedRelays = healthTracker.getSortedRelays(relays);
|
|
1112
|
+
let lastError;
|
|
1113
|
+
for (const relay of sortedRelays) {
|
|
1114
|
+
sendOptions?.assertDirectAdapterHandoff?.();
|
|
1115
|
+
const cb = circuitBreakers.get(relay);
|
|
1116
|
+
if (cb && !cb.canAttempt()) continue;
|
|
1117
|
+
const startTime = Date.now();
|
|
1118
|
+
const recordFailure = (err) => {
|
|
1119
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1120
|
+
const latency = Date.now() - startTime;
|
|
1121
|
+
cb?.recordFailure();
|
|
1122
|
+
healthTracker.recordFailure(relay);
|
|
1123
|
+
metrics.emit("relay.error", 1, {
|
|
1124
|
+
relay,
|
|
1125
|
+
latency
|
|
1126
|
+
});
|
|
1127
|
+
onError?.(lastError, `publish to ${relay}`);
|
|
1128
|
+
};
|
|
1129
|
+
const connection = await pool.ensureRelay(relay, { connectionTimeout: pool.maxWaitForConnection }).catch((err) => {
|
|
1130
|
+
recordFailure(/* @__PURE__ */ new Error(`connection failure: ${String(err)}`));
|
|
1131
|
+
});
|
|
1132
|
+
if (!connection) continue;
|
|
1133
|
+
sendOptions?.assertDirectAdapterHandoff?.();
|
|
1134
|
+
if (sendOptions?.onPlatformSendDispatch) {
|
|
1135
|
+
await sendOptions.onPlatformSendDispatch();
|
|
1136
|
+
sendOptions.assertDirectAdapterHandoff?.();
|
|
1137
|
+
}
|
|
1138
|
+
try {
|
|
1139
|
+
await connection.publish(reply);
|
|
1140
|
+
const latency = Date.now() - startTime;
|
|
1141
|
+
cb?.recordSuccess();
|
|
1142
|
+
healthTracker.recordSuccess(relay, latency);
|
|
1143
|
+
return reply.id;
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
recordFailure(err);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
throw new Error(`Failed to publish to any relay: ${lastError?.message}`);
|
|
1149
|
+
}
|
|
1110
1150
|
return {
|
|
1111
1151
|
close,
|
|
1112
1152
|
publicKey: pk,
|
|
1113
|
-
sendDm:
|
|
1153
|
+
sendDm: sendEncryptedDm,
|
|
1114
1154
|
getMetrics: () => metrics.getSnapshot(),
|
|
1115
1155
|
publishProfile: publishProfile$1,
|
|
1116
1156
|
getProfileState
|
|
1117
1157
|
};
|
|
1118
1158
|
}
|
|
1119
|
-
async function sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circuitBreakers, healthTracker, onError, options) {
|
|
1120
|
-
const ciphertext = encrypt(sk, toPubkey, text);
|
|
1121
|
-
const tags = [["p", toPubkey]];
|
|
1122
|
-
if (options?.replyToEventId) tags.push(["e", options.replyToEventId]);
|
|
1123
|
-
const reply = finalizeEvent({
|
|
1124
|
-
kind: 4,
|
|
1125
|
-
content: ciphertext,
|
|
1126
|
-
tags,
|
|
1127
|
-
created_at: Math.floor(Date.now() / 1e3)
|
|
1128
|
-
}, sk);
|
|
1129
|
-
const sortedRelays = healthTracker.getSortedRelays(relays);
|
|
1130
|
-
let lastError;
|
|
1131
|
-
for (const relay of sortedRelays) {
|
|
1132
|
-
options?.assertDirectAdapterHandoff?.();
|
|
1133
|
-
const cb = circuitBreakers.get(relay);
|
|
1134
|
-
if (cb && !cb.canAttempt()) continue;
|
|
1135
|
-
const startTime = Date.now();
|
|
1136
|
-
const recordFailure = (err) => {
|
|
1137
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1138
|
-
const latency = Date.now() - startTime;
|
|
1139
|
-
cb?.recordFailure();
|
|
1140
|
-
healthTracker.recordFailure(relay);
|
|
1141
|
-
metrics.emit("relay.error", 1, {
|
|
1142
|
-
relay,
|
|
1143
|
-
latency
|
|
1144
|
-
});
|
|
1145
|
-
onError?.(lastError, `publish to ${relay}`);
|
|
1146
|
-
};
|
|
1147
|
-
const connection = await pool.ensureRelay(relay, { connectionTimeout: pool.maxWaitForConnection }).catch((err) => {
|
|
1148
|
-
recordFailure(/* @__PURE__ */ new Error(`connection failure: ${String(err)}`));
|
|
1149
|
-
});
|
|
1150
|
-
if (!connection) continue;
|
|
1151
|
-
options?.assertDirectAdapterHandoff?.();
|
|
1152
|
-
if (options?.onPlatformSendDispatch) {
|
|
1153
|
-
await options.onPlatformSendDispatch();
|
|
1154
|
-
options.assertDirectAdapterHandoff?.();
|
|
1155
|
-
}
|
|
1156
|
-
try {
|
|
1157
|
-
await connection.publish(reply);
|
|
1158
|
-
const latency = Date.now() - startTime;
|
|
1159
|
-
cb?.recordSuccess();
|
|
1160
|
-
healthTracker.recordSuccess(relay, latency);
|
|
1161
|
-
return reply.id;
|
|
1162
|
-
} catch (err) {
|
|
1163
|
-
recordFailure(err);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
throw new Error(`Failed to publish to any relay: ${lastError?.message}`);
|
|
1167
|
-
}
|
|
1168
1159
|
//#endregion
|
|
1169
1160
|
//#region extensions/nostr/src/gateway.ts
|
|
1170
1161
|
const activeBuses = /* @__PURE__ */ new Map();
|
|
1171
|
-
const ACCESS_GROUP_PREFIX = "accessGroup:";
|
|
1172
1162
|
function normalizeRelayLifecycleKey(relay) {
|
|
1173
1163
|
return new URL(relay).toString();
|
|
1174
1164
|
}
|
|
1175
|
-
function parseNostrAccessGroupAllowFromEntry(entry) {
|
|
1176
|
-
const trimmed = entry.trim();
|
|
1177
|
-
if (!trimmed.startsWith(ACCESS_GROUP_PREFIX)) return null;
|
|
1178
|
-
return trimmed.slice(12).trim() || null;
|
|
1179
|
-
}
|
|
1180
1165
|
function normalizeNostrAllowEntry(entry) {
|
|
1181
1166
|
const trimmed = entry.trim();
|
|
1182
1167
|
if (!trimmed) return null;
|
|
1183
1168
|
if (trimmed === "*") return "*";
|
|
1184
|
-
const accessGroup =
|
|
1169
|
+
const accessGroup = parseAccessGroupAllowFromEntry(trimmed);
|
|
1185
1170
|
if (accessGroup) return `accessGroup:${accessGroup}`;
|
|
1186
1171
|
try {
|
|
1187
1172
|
return normalizePubkey(trimmed.replace(/^nostr:/i, ""));
|
|
@@ -1494,11 +1479,10 @@ const nostrPluginOutboundAdapter = {
|
|
|
1494
1479
|
ok: false,
|
|
1495
1480
|
error: missingTargetError("Nostr", NOSTR_TARGET_HINT)
|
|
1496
1481
|
};
|
|
1497
|
-
const normalized = normalizeNostrTarget(trimmed);
|
|
1498
1482
|
try {
|
|
1499
1483
|
return {
|
|
1500
1484
|
ok: true,
|
|
1501
|
-
to: normalizePubkey(
|
|
1485
|
+
to: normalizePubkey(stripNostrTargetPrefix(trimmed))
|
|
1502
1486
|
};
|
|
1503
1487
|
} catch {
|
|
1504
1488
|
return {
|
|
@@ -2,9 +2,6 @@ import { AllowFromListSchema, DmPolicySchema, MarkdownConfigSchema } from "openc
|
|
|
2
2
|
import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
//#region extensions/nostr/src/config-schema.ts
|
|
5
|
-
/**
|
|
6
|
-
* Validates https:// URLs only (no javascript:, data:, file:, etc.)
|
|
7
|
-
*/
|
|
8
5
|
const safeUrlSchema = z.string().url().refine((url) => {
|
|
9
6
|
try {
|
|
10
7
|
return new URL(url).protocol === "https:";
|
|
@@ -17,32 +14,22 @@ const safeUrlSchema = z.string().url().refine((url) => {
|
|
|
17
14
|
* https://github.com/nostr-protocol/nips/blob/master/01.md
|
|
18
15
|
*/
|
|
19
16
|
const NostrProfileSchema = z.object({
|
|
20
|
-
/** Username (NIP-01: name) - max 256 chars */
|
|
21
17
|
name: z.string().max(256).optional(),
|
|
22
|
-
/**
|
|
18
|
+
/** NIP-01 display_name. */
|
|
23
19
|
displayName: z.string().max(256).optional(),
|
|
24
|
-
/** Bio/description (NIP-01: about) - max 2000 chars */
|
|
25
20
|
about: z.string().max(2e3).optional(),
|
|
26
|
-
/** Profile picture URL (must be https) */
|
|
27
21
|
picture: safeUrlSchema.optional(),
|
|
28
|
-
/** Banner image URL (must be https) */
|
|
29
22
|
banner: safeUrlSchema.optional(),
|
|
30
|
-
/** Website URL (must be https) */
|
|
31
23
|
website: safeUrlSchema.optional(),
|
|
32
24
|
/** NIP-05 identifier (e.g., "user@example.com") */
|
|
33
25
|
nip05: z.string().optional(),
|
|
34
26
|
/** Lightning address (LUD-16) */
|
|
35
27
|
lud16: z.string().optional()
|
|
36
28
|
});
|
|
37
|
-
/**
|
|
38
|
-
* Zod schema for channels.nostr.* configuration
|
|
39
|
-
*/
|
|
40
29
|
const NostrConfigSchema = z.object({
|
|
41
|
-
/** Account name (optional display name) */
|
|
42
30
|
name: z.string().optional(),
|
|
43
31
|
/** Optional default account id for routing/account selection. */
|
|
44
32
|
defaultAccount: z.string().optional(),
|
|
45
|
-
/** Whether this channel is enabled */
|
|
46
33
|
enabled: z.boolean().optional(),
|
|
47
34
|
configWrites: z.boolean().optional(),
|
|
48
35
|
/** Markdown formatting overrides (tables). */
|
|
@@ -51,7 +38,6 @@ const NostrConfigSchema = z.object({
|
|
|
51
38
|
privateKey: buildSecretInputSchema().optional(),
|
|
52
39
|
/** WebSocket relay URLs to connect to */
|
|
53
40
|
relays: z.array(z.string()).optional(),
|
|
54
|
-
/** DM access policy: pairing, allowlist, open, or disabled */
|
|
55
41
|
dmPolicy: DmPolicySchema.optional(),
|
|
56
42
|
/** Allowed sender pubkeys (npub or hex format) */
|
|
57
43
|
allowFrom: AllowFromListSchema,
|
|
@@ -41,6 +41,34 @@ function resolveNostrPrivateKey(value) {
|
|
|
41
41
|
return process.env["NOSTR_PRIVATE_KEY"]?.trim() ?? "";
|
|
42
42
|
}
|
|
43
43
|
//#endregion
|
|
44
|
+
//#region extensions/nostr/src/accounts.ts
|
|
45
|
+
function getNostrConfig(cfg) {
|
|
46
|
+
return cfg.channels?.nostr;
|
|
47
|
+
}
|
|
48
|
+
function resolveNostrAccountBase(cfg, accountId) {
|
|
49
|
+
const nostrCfg = getNostrConfig(cfg);
|
|
50
|
+
const privateKey = resolveNostrPrivateKey(nostrCfg?.privateKey);
|
|
51
|
+
return {
|
|
52
|
+
accountId,
|
|
53
|
+
name: typeof nostrCfg?.name === "string" ? nostrCfg.name : void 0,
|
|
54
|
+
enabled: nostrCfg?.enabled !== false,
|
|
55
|
+
configured: hasConfiguredNostrPrivateKey(nostrCfg?.privateKey),
|
|
56
|
+
privateKey,
|
|
57
|
+
publicKey: "",
|
|
58
|
+
relays: nostrCfg?.relays ?? DEFAULT_RELAYS,
|
|
59
|
+
profile: nostrCfg?.profile,
|
|
60
|
+
config: {
|
|
61
|
+
enabled: nostrCfg?.enabled,
|
|
62
|
+
name: nostrCfg?.name,
|
|
63
|
+
privateKey: nostrCfg?.privateKey,
|
|
64
|
+
relays: nostrCfg?.relays,
|
|
65
|
+
dmPolicy: nostrCfg?.dmPolicy,
|
|
66
|
+
allowFrom: nostrCfg?.allowFrom,
|
|
67
|
+
profile: nostrCfg?.profile
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
44
72
|
//#region extensions/nostr/src/setup-adapter.ts
|
|
45
73
|
const channel = "nostr";
|
|
46
74
|
function buildNostrSetupPatch(accountId, patch) {
|
|
@@ -155,4 +183,4 @@ function createNostrSetupStatus(resolveAccount) {
|
|
|
155
183
|
});
|
|
156
184
|
}
|
|
157
185
|
//#endregion
|
|
158
|
-
export { parseRelayUrls as a,
|
|
186
|
+
export { parseRelayUrls as a, hasConfiguredNostrPrivateKey as c, createNostrSetupStatus as i, validatePrivateKey as l, createNostrSetupAdapter as n, getNostrConfig as o, createNostrSetupContract as r, resolveNostrAccountBase as s, buildNostrSetupPatch as t, DEFAULT_RELAYS as u };
|
|
@@ -1,45 +1,21 @@
|
|
|
1
|
-
import { t as NostrConfigSchema } from "./config-schema-
|
|
2
|
-
import { i as createNostrSetupStatus,
|
|
1
|
+
import { t as NostrConfigSchema } from "./config-schema-HuEK1L2K.mjs";
|
|
2
|
+
import { i as createNostrSetupStatus, n as createNostrSetupAdapter, o as getNostrConfig, r as createNostrSetupContract, s as resolveNostrAccountBase } from "./setup-adapter-I55hhpDp.mjs";
|
|
3
3
|
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
4
4
|
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-plugin-common";
|
|
5
5
|
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
6
6
|
import { createDelegatedSetupWizardProxy } from "openclaw/plugin-sdk/setup-runtime";
|
|
7
7
|
//#region extensions/nostr/src/channel.setup.ts
|
|
8
8
|
const channel = "nostr";
|
|
9
|
-
function getNostrConfig(cfg) {
|
|
10
|
-
return cfg.channels?.nostr;
|
|
11
|
-
}
|
|
12
9
|
function resolveDefaultSetupNostrAccountId(cfg) {
|
|
13
10
|
return normalizeAccountId(getNostrConfig(cfg)?.defaultAccount);
|
|
14
11
|
}
|
|
15
12
|
function resolveSetupNostrAccount(params) {
|
|
16
|
-
const nostrCfg = getNostrConfig(params.cfg);
|
|
17
13
|
const accountId = normalizeAccountId(params.accountId ?? resolveDefaultSetupNostrAccountId(params.cfg));
|
|
18
|
-
|
|
19
|
-
const configured = hasConfiguredNostrPrivateKey(nostrCfg?.privateKey);
|
|
20
|
-
return {
|
|
21
|
-
accountId,
|
|
22
|
-
name: typeof nostrCfg?.name === "string" ? nostrCfg.name : void 0,
|
|
23
|
-
enabled: nostrCfg?.enabled !== false,
|
|
24
|
-
configured,
|
|
25
|
-
privateKey,
|
|
26
|
-
publicKey: "",
|
|
27
|
-
relays: nostrCfg?.relays ?? DEFAULT_RELAYS,
|
|
28
|
-
profile: nostrCfg?.profile,
|
|
29
|
-
config: {
|
|
30
|
-
enabled: nostrCfg?.enabled,
|
|
31
|
-
name: nostrCfg?.name,
|
|
32
|
-
privateKey: nostrCfg?.privateKey,
|
|
33
|
-
relays: nostrCfg?.relays,
|
|
34
|
-
dmPolicy: nostrCfg?.dmPolicy,
|
|
35
|
-
allowFrom: nostrCfg?.allowFrom,
|
|
36
|
-
profile: nostrCfg?.profile
|
|
37
|
-
}
|
|
38
|
-
};
|
|
14
|
+
return resolveNostrAccountBase(params.cfg, accountId);
|
|
39
15
|
}
|
|
40
16
|
const nostrSetupWizard = createDelegatedSetupWizardProxy({
|
|
41
17
|
channel,
|
|
42
|
-
loadWizard: async () => (await import("./setup-surface-
|
|
18
|
+
loadWizard: async () => (await import("./setup-surface-b-YWJSGV.mjs").then((n) => n.i)).nostrSetupWizard,
|
|
43
19
|
status: createNostrSetupStatus(resolveSetupNostrAccount),
|
|
44
20
|
resolveShouldPromptAccountIds: () => false,
|
|
45
21
|
delegatePrepare: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as parseRelayUrls, c as
|
|
1
|
+
import { a as parseRelayUrls, c as hasConfiguredNostrPrivateKey, i as createNostrSetupStatus, l as validatePrivateKey, n as createNostrSetupAdapter, o as getNostrConfig, r as createNostrSetupContract, s as resolveNostrAccountBase, t as buildNostrSetupPatch, u as DEFAULT_RELAYS } from "./setup-adapter-I55hhpDp.mjs";
|
|
2
2
|
import { createAccountListHelpers } from "openclaw/plugin-sdk/account-helpers";
|
|
3
3
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
4
|
import { hasConfiguredSecretInput, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
@@ -38,41 +38,21 @@ function normalizePubkey(input) {
|
|
|
38
38
|
const { listAccountIds: listNostrAccountIds, resolveDefaultAccountId: resolveDefaultNostrAccountId } = createAccountListHelpers("nostr", {
|
|
39
39
|
fallbackAccountIdWhenEmpty: false,
|
|
40
40
|
resolveImplicitAccountId: (cfg) => {
|
|
41
|
-
const account = cfg
|
|
41
|
+
const account = getNostrConfig(cfg);
|
|
42
42
|
return hasConfiguredNostrPrivateKey(account?.privateKey) ? normalizeOptionalAccountId(account?.defaultAccount) ?? DEFAULT_ACCOUNT_ID : void 0;
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
|
-
/**
|
|
46
|
-
* Resolve a Nostr account from config
|
|
47
|
-
*/
|
|
48
45
|
function resolveNostrAccount(opts) {
|
|
49
|
-
const accountId = normalizeAccountId(opts.accountId ?? resolveDefaultNostrAccountId(opts.cfg));
|
|
50
|
-
const
|
|
51
|
-
const baseEnabled = nostrCfg?.enabled !== false;
|
|
52
|
-
const privateKey = resolveNostrPrivateKey(nostrCfg?.privateKey);
|
|
53
|
-
const configured = hasConfiguredNostrPrivateKey(nostrCfg?.privateKey);
|
|
46
|
+
const accountId = normalizeAccountId(opts.accountId ?? resolveDefaultNostrAccountId({ channels: { nostr: getNostrConfig(opts.cfg) } }));
|
|
47
|
+
const account = resolveNostrAccountBase(opts.cfg, accountId);
|
|
54
48
|
let publicKey = "";
|
|
55
|
-
if (privateKey) try {
|
|
56
|
-
publicKey = getPublicKeyFromPrivate(privateKey);
|
|
49
|
+
if (account.privateKey) try {
|
|
50
|
+
publicKey = getPublicKeyFromPrivate(account.privateKey);
|
|
57
51
|
} catch {}
|
|
58
52
|
return {
|
|
59
|
-
|
|
60
|
-
name: normalizeOptionalString(
|
|
61
|
-
|
|
62
|
-
configured,
|
|
63
|
-
privateKey,
|
|
64
|
-
publicKey,
|
|
65
|
-
relays: nostrCfg?.relays ?? DEFAULT_RELAYS,
|
|
66
|
-
profile: nostrCfg?.profile,
|
|
67
|
-
config: {
|
|
68
|
-
enabled: nostrCfg?.enabled,
|
|
69
|
-
name: nostrCfg?.name,
|
|
70
|
-
privateKey: nostrCfg?.privateKey,
|
|
71
|
-
relays: nostrCfg?.relays,
|
|
72
|
-
dmPolicy: nostrCfg?.dmPolicy,
|
|
73
|
-
allowFrom: nostrCfg?.allowFrom,
|
|
74
|
-
profile: nostrCfg?.profile
|
|
75
|
-
}
|
|
53
|
+
...account,
|
|
54
|
+
name: normalizeOptionalString(account.config.name),
|
|
55
|
+
publicKey
|
|
76
56
|
};
|
|
77
57
|
}
|
|
78
58
|
//#endregion
|
|
@@ -124,7 +104,7 @@ const nostrDmPolicy = createTopLevelChannelDmPolicy({
|
|
|
124
104
|
channel,
|
|
125
105
|
policyKey: "channels.nostr.dmPolicy",
|
|
126
106
|
allowFromKey: "channels.nostr.allowFrom",
|
|
127
|
-
getCurrent: (cfg) => cfg
|
|
107
|
+
getCurrent: (cfg) => getNostrConfig(cfg)?.dmPolicy ?? "pairing",
|
|
128
108
|
promptAllowFrom: promptNostrAllowFrom
|
|
129
109
|
});
|
|
130
110
|
const nostrSetupAdapter = createNostrSetupAdapter({ resolveAccountId: (cfg, accountId) => accountId?.trim() || resolveDefaultNostrAccountId(cfg) });
|
|
@@ -195,7 +175,7 @@ const nostrSetupWizard = {
|
|
|
195
175
|
cfg,
|
|
196
176
|
accountId
|
|
197
177
|
});
|
|
198
|
-
const configuredRelays =
|
|
178
|
+
const configuredRelays = account.config.relays;
|
|
199
179
|
return (configuredRelays && configuredRelays.length > 0 ? account.relays : []).join(", ");
|
|
200
180
|
},
|
|
201
181
|
keepPrompt: (value) => t("wizard.nostr.relayUrlsKeep", { value }),
|
package/dist/api.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { s as resolveNostrAccount } from "./.setup/setup-surface-
|
|
1
|
+
import { s as resolveNostrAccount } from "./.setup/setup-surface-b-YWJSGV.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-HuEK1L2K.mjs";
|
|
4
|
+
import { a as getNostrRuntime, i as contentToProfile, n as nostrPlugin, o as setNostrRuntime, r as publishNostrProfile, t as getNostrProfileState } from "./.setup/channel-BaBU_Q15.mjs";
|
|
5
5
|
import { getPluginRuntimeGatewayRequestScope as getPluginRuntimeGatewayRequestScope$1 } from "openclaw/plugin-sdk/plugin-runtime";
|
|
6
|
-
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString
|
|
6
|
+
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { SimplePool } from "nostr-tools";
|
|
9
9
|
import { createFixedWindowRateLimiter } from "openclaw/plugin-sdk/webhook-ingress";
|
|
10
|
+
import { raceWithTimeout } from "openclaw/plugin-sdk/time-runtime";
|
|
10
11
|
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
|
11
12
|
import { isBlockedHostnameOrIp, isLoopbackHost } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
12
13
|
import { readJsonBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
@@ -26,12 +27,9 @@ function normalizeNostrProfileUrlForRuntime(urlStr) {
|
|
|
26
27
|
return `${prefix}${high.toString(16)}:${low.toString(16)}${suffix}`;
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
|
-
function parseNostrProfileUrl(urlStr) {
|
|
30
|
-
return new URL(normalizeNostrProfileUrlForRuntime(urlStr));
|
|
31
|
-
}
|
|
32
30
|
function validateUrlSafety(urlStr) {
|
|
33
31
|
try {
|
|
34
|
-
const url =
|
|
32
|
+
const url = new URL(normalizeNostrProfileUrlForRuntime(urlStr));
|
|
35
33
|
if (url.protocol !== "https:") return {
|
|
36
34
|
ok: false,
|
|
37
35
|
error: "URL must use https:// protocol"
|
|
@@ -82,15 +80,10 @@ async function importProfileFromRelays(opts) {
|
|
|
82
80
|
};
|
|
83
81
|
const pool = new SimplePool();
|
|
84
82
|
const relaysQueried = [...relays];
|
|
85
|
-
let deadlineTimer;
|
|
86
|
-
const deadline = new Promise((resolve) => {
|
|
87
|
-
deadlineTimer = setTimeout(resolve, timeoutMs);
|
|
88
|
-
deadlineTimer.unref?.();
|
|
89
|
-
});
|
|
90
83
|
const subscriptions = [];
|
|
91
84
|
try {
|
|
92
85
|
const events = [];
|
|
93
|
-
await
|
|
86
|
+
await raceWithTimeout(() => Promise.all(relays.map((relay) => new Promise((resolve) => {
|
|
94
87
|
const subscription = pool.subscribeMany([relay], {
|
|
95
88
|
kinds: [0],
|
|
96
89
|
authors: [pubkey],
|
|
@@ -110,7 +103,7 @@ async function importProfileFromRelays(opts) {
|
|
|
110
103
|
}
|
|
111
104
|
});
|
|
112
105
|
subscriptions.push(subscription);
|
|
113
|
-
}))),
|
|
106
|
+
}))), timeoutMs, () => void 0, { ref: false });
|
|
114
107
|
if (events.length === 0) return {
|
|
115
108
|
ok: false,
|
|
116
109
|
error: "No profile found on any relay",
|
|
@@ -158,7 +151,6 @@ async function importProfileFromRelays(opts) {
|
|
|
158
151
|
sourceRelay: bestEvent.relay
|
|
159
152
|
};
|
|
160
153
|
} finally {
|
|
161
|
-
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
162
154
|
for (const subscription of subscriptions) subscription.close();
|
|
163
155
|
pool.close(relays);
|
|
164
156
|
}
|
|
@@ -258,8 +250,7 @@ function isLoopbackOriginLike(value) {
|
|
|
258
250
|
}
|
|
259
251
|
}
|
|
260
252
|
function firstHeaderValue(value) {
|
|
261
|
-
|
|
262
|
-
return readStringValue(value);
|
|
253
|
+
return Array.isArray(value) ? value[0] : value;
|
|
263
254
|
}
|
|
264
255
|
function normalizeIpCandidate(raw) {
|
|
265
256
|
const unquoted = raw.trim().replace(/^"|"$/g, "");
|
|
@@ -283,51 +274,15 @@ function hasNonLoopbackForwardedClient(req) {
|
|
|
283
274
|
}
|
|
284
275
|
return false;
|
|
285
276
|
}
|
|
286
|
-
function
|
|
277
|
+
function resolveLoopbackMutationRejection(req) {
|
|
287
278
|
const remoteAddress = req.socket.remoteAddress;
|
|
288
|
-
if (!isLoopbackRemoteAddress(remoteAddress)) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
ok: false,
|
|
292
|
-
error: "Forbidden"
|
|
293
|
-
});
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
if (hasNonLoopbackForwardedClient(req)) {
|
|
297
|
-
ctx.log?.warn?.("Rejected mutation with non-loopback forwarded client headers");
|
|
298
|
-
sendJson(res, 403, {
|
|
299
|
-
ok: false,
|
|
300
|
-
error: "Forbidden"
|
|
301
|
-
});
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
if (normalizeOptionalLowercaseString(firstHeaderValue(req.headers["sec-fetch-site"])) === "cross-site") {
|
|
305
|
-
ctx.log?.warn?.("Rejected mutation with cross-site sec-fetch-site header");
|
|
306
|
-
sendJson(res, 403, {
|
|
307
|
-
ok: false,
|
|
308
|
-
error: "Forbidden"
|
|
309
|
-
});
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
279
|
+
if (!isLoopbackRemoteAddress(remoteAddress)) return `Rejected mutation from non-loopback remoteAddress=${String(remoteAddress)}`;
|
|
280
|
+
if (hasNonLoopbackForwardedClient(req)) return "Rejected mutation with non-loopback forwarded client headers";
|
|
281
|
+
if (normalizeOptionalLowercaseString(firstHeaderValue(req.headers["sec-fetch-site"])) === "cross-site") return "Rejected mutation with cross-site sec-fetch-site header";
|
|
312
282
|
const origin = firstHeaderValue(req.headers.origin);
|
|
313
|
-
if (typeof origin === "string" && !isLoopbackOriginLike(origin)) {
|
|
314
|
-
ctx.log?.warn?.(`Rejected mutation with non-loopback origin=${origin}`);
|
|
315
|
-
sendJson(res, 403, {
|
|
316
|
-
ok: false,
|
|
317
|
-
error: "Forbidden"
|
|
318
|
-
});
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
283
|
+
if (typeof origin === "string" && !isLoopbackOriginLike(origin)) return `Rejected mutation with non-loopback origin=${origin}`;
|
|
321
284
|
const referer = firstHeaderValue(req.headers.referer ?? req.headers.referrer);
|
|
322
|
-
if (typeof referer === "string" && !isLoopbackOriginLike(referer)) {
|
|
323
|
-
ctx.log?.warn?.(`Rejected mutation with non-loopback referer=${referer}`);
|
|
324
|
-
sendJson(res, 403, {
|
|
325
|
-
ok: false,
|
|
326
|
-
error: "Forbidden"
|
|
327
|
-
});
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
330
|
-
return true;
|
|
285
|
+
if (typeof referer === "string" && !isLoopbackOriginLike(referer)) return `Rejected mutation with non-loopback referer=${referer}`;
|
|
331
286
|
}
|
|
332
287
|
function enforceGatewayMutationScope(ctx, accountId, res) {
|
|
333
288
|
const runtimeScopes = getPluginRuntimeGatewayRequestScope$1()?.client?.connect?.scopes;
|
|
@@ -350,7 +305,16 @@ function createNostrProfileHttpHandler(ctx) {
|
|
|
350
305
|
try {
|
|
351
306
|
if (req.method === "GET" && !isImport) return await handleGetProfile(accountId, ctx, res);
|
|
352
307
|
if (req.method === "PUT" && !isImport || req.method === "POST" && isImport) {
|
|
353
|
-
if (!enforceGatewayMutationScope(ctx, accountId, res)
|
|
308
|
+
if (!enforceGatewayMutationScope(ctx, accountId, res)) return true;
|
|
309
|
+
const rejection = resolveLoopbackMutationRejection(req);
|
|
310
|
+
if (rejection) {
|
|
311
|
+
ctx.log?.warn?.(rejection);
|
|
312
|
+
sendJson(res, 403, {
|
|
313
|
+
ok: false,
|
|
314
|
+
error: "Forbidden"
|
|
315
|
+
});
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
354
318
|
return await (isImport ? handleImportProfile : handleUpdateProfile)(accountId, ctx, req, res);
|
|
355
319
|
}
|
|
356
320
|
sendJson(res, 405, {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as nostrPlugin } from "./.setup/channel-
|
|
1
|
+
import { n as nostrPlugin } from "./.setup/channel-BaBU_Q15.mjs";
|
|
2
2
|
export { nostrPlugin };
|
|
@@ -15,6 +15,7 @@ function retiredNostrStateMigration(namespace, label) {
|
|
|
15
15
|
return {
|
|
16
16
|
id: `nostr-${namespace}-json-to-plugin-state`,
|
|
17
17
|
label,
|
|
18
|
+
collectBackupResources: () => [],
|
|
18
19
|
async detectLegacyState({ stateDir }) {
|
|
19
20
|
return await hasLegacyState(stateDir) ? { preview: [warning] } : null;
|
|
20
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,5 @@
|
|
|
1
1
|
import { defineBundledChannelEntry, loadBundledEntryExportSync } from "openclaw/plugin-sdk/channel-entry-contract";
|
|
2
2
|
//#region extensions/nostr/index.ts
|
|
3
|
-
function createNostrProfileHttpHandler() {
|
|
4
|
-
return loadBundledEntryExportSync(import.meta.url, {
|
|
5
|
-
specifier: "./api.js",
|
|
6
|
-
exportName: "createNostrProfileHttpHandler"
|
|
7
|
-
});
|
|
8
|
-
}
|
|
9
|
-
function getNostrRuntime() {
|
|
10
|
-
return loadBundledEntryExportSync(import.meta.url, {
|
|
11
|
-
specifier: "./api.js",
|
|
12
|
-
exportName: "getNostrRuntime"
|
|
13
|
-
})();
|
|
14
|
-
}
|
|
15
|
-
function resolveNostrAccount(params) {
|
|
16
|
-
return loadBundledEntryExportSync(import.meta.url, {
|
|
17
|
-
specifier: "./api.js",
|
|
18
|
-
exportName: "resolveNostrAccount"
|
|
19
|
-
})(params);
|
|
20
|
-
}
|
|
21
3
|
var nostr_default = defineBundledChannelEntry({
|
|
22
4
|
id: "nostr",
|
|
23
5
|
name: "Nostr",
|
|
@@ -36,10 +18,12 @@ var nostr_default = defineBundledChannelEntry({
|
|
|
36
18
|
exportName: "setNostrRuntime"
|
|
37
19
|
},
|
|
38
20
|
registerFull(api) {
|
|
39
|
-
const
|
|
21
|
+
const { createNostrProfileHttpHandler, getNostrRuntime, resolveNostrAccount } = loadBundledEntryExportSync(import.meta.url, { specifier: "./api.js" });
|
|
22
|
+
const httpHandler = createNostrProfileHttpHandler({
|
|
40
23
|
getConfigProfile: (accountId) => {
|
|
24
|
+
const cfg = getNostrRuntime().config.current();
|
|
41
25
|
return resolveNostrAccount({
|
|
42
|
-
cfg
|
|
26
|
+
cfg,
|
|
43
27
|
accountId
|
|
44
28
|
}).profile;
|
|
45
29
|
},
|
|
@@ -60,8 +44,9 @@ var nostr_default = defineBundledChannelEntry({
|
|
|
60
44
|
});
|
|
61
45
|
},
|
|
62
46
|
getAccountInfo: (accountId) => {
|
|
47
|
+
const cfg = getNostrRuntime().config.current();
|
|
63
48
|
const account = resolveNostrAccount({
|
|
64
|
-
cfg
|
|
49
|
+
cfg,
|
|
65
50
|
accountId
|
|
66
51
|
});
|
|
67
52
|
if (!account.configured || !account.publicKey) return null;
|
package/dist/setup-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { r as nostrSetupWizard, t as nostrSetupAdapter } from "./.setup/setup-surface-
|
|
1
|
+
import { r as nostrSetupWizard, t as nostrSetupAdapter } from "./.setup/setup-surface-b-YWJSGV.mjs";
|
|
2
2
|
export { nostrSetupAdapter, nostrSetupWizard };
|
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-BUsavy-i.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-C_HK3q8q.mjs";
|
|
2
2
|
export { nostrSetupPlugin };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/nostr",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.10.1-beta.1",
|
|
4
4
|
"description": "OpenClaw Nostr channel plugin for NIP-04 encrypted direct messages.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"zod": "4.6.5"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"openclaw": ">=2026.
|
|
15
|
+
"openclaw": ">=2026.10.1-beta.1"
|
|
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.
|
|
83
|
+
"pluginApi": ">=2026.10.1-beta.1"
|
|
84
84
|
},
|
|
85
85
|
"build": {
|
|
86
|
-
"openclawVersion": "2026.
|
|
86
|
+
"openclawVersion": "2026.10.1-beta.1"
|
|
87
87
|
},
|
|
88
88
|
"release": {
|
|
89
89
|
"publishToClawHub": true,
|