@openclaw/nostr 2026.9.8 → 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.
@@ -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-Dx8QxWSP.mjs";
2
- import { n as NostrProfileSchema, t as NostrConfigSchema } from "./config-schema-B3uU2qBS.mjs";
3
- import { c as validatePrivateKey, l as DEFAULT_RELAYS } from "./setup-adapter-KJ_iGKiG.mjs";
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
- var NostrIngressPermanentError = class extends Error {
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 = options.eoseConfirmDeadlineMs ?? DEFAULT_EOSE_CONFIRM_DEADLINE_MS;
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: confirmDeadlineMs + LIBRARY_EOSE_TIMEOUT_MARGIN_MS,
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(pool, sk, event.pubkey, text, relays, metrics, circuitBreakers, healthTracker, onError, { replyToEventId: event.id });
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: (toPubkey, text, sendOptions) => sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circuitBreakers, healthTracker, onError, sendOptions),
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 = parseNostrAccessGroupAllowFromEntry(trimmed);
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(normalized)
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
- /** Display name (NIP-01: display_name) - max 256 chars */
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, validatePrivateKey as c, createNostrSetupStatus as i, DEFAULT_RELAYS as l, createNostrSetupAdapter as n, hasConfiguredNostrPrivateKey as o, createNostrSetupContract as r, resolveNostrPrivateKey as s, buildNostrSetupPatch as t };
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 };
@@ -0,0 +1,2 @@
1
+ import { t as nostrSetupPlugin } from "./setup-plugin-api-C_HK3q8q.mjs";
2
+ export { nostrSetupPlugin };
@@ -1,45 +1,21 @@
1
- import { t as NostrConfigSchema } from "./config-schema-B3uU2qBS.mjs";
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";
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
- const privateKey = resolveNostrPrivateKey(nostrCfg?.privateKey);
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-Dx8QxWSP.mjs").then((n) => n.i)).nostrSetupWizard,
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 validatePrivateKey, i as createNostrSetupStatus, l as DEFAULT_RELAYS, n as createNostrSetupAdapter, o as hasConfiguredNostrPrivateKey, r as createNostrSetupContract, s as resolveNostrPrivateKey, t as buildNostrSetupPatch } from "./setup-adapter-KJ_iGKiG.mjs";
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.channels?.nostr;
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 nostrCfg = opts.cfg.channels?.nostr;
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
- accountId,
60
- name: normalizeOptionalString(nostrCfg?.name),
61
- enabled: baseEnabled,
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.channels?.nostr?.dmPolicy ?? "pairing",
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 = cfg.channels?.nostr?.relays;
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-Dx8QxWSP.mjs";
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-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";
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, readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
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 = parseNostrProfileUrl(urlStr);
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 Promise.race([Promise.all(relays.map((relay) => new Promise((resolve) => {
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
- }))), deadline]);
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
- if (Array.isArray(value)) return value[0];
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 enforceLoopbackMutationGuards(ctx, req, res) {
277
+ function resolveLoopbackMutationRejection(req) {
287
278
  const remoteAddress = req.socket.remoteAddress;
288
- if (!isLoopbackRemoteAddress(remoteAddress)) {
289
- ctx.log?.warn?.(`Rejected mutation from non-loopback remoteAddress=${String(remoteAddress)}`);
290
- sendJson(res, 403, {
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) || !enforceLoopbackMutationGuards(ctx, req, res)) return true;
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-D2d-FP8G.mjs";
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 httpHandler = createNostrProfileHttpHandler()({
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: getNostrRuntime().config.current(),
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: getNostrRuntime().config.current(),
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-Dx8QxWSP.mjs";
1
+ import { r as nostrSetupWizard, t as nostrSetupAdapter } from "./.setup/setup-surface-b-YWJSGV.mjs";
2
2
  export { nostrSetupAdapter, nostrSetupWizard };
@@ -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-BhRAS2I1.mjs", import.meta.url).href),
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: {
@@ -1,2 +1,2 @@
1
- import { t as nostrSetupPlugin } from "./.setup/setup-plugin-api-Ci0XxWqb.mjs";
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.9.8",
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.9.8"
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.9.8"
83
+ "pluginApi": ">=2026.10.1-beta.1"
84
84
  },
85
85
  "build": {
86
- "openclawVersion": "2026.9.8"
86
+ "openclawVersion": "2026.10.1-beta.1"
87
87
  },
88
88
  "release": {
89
89
  "publishToClawHub": true,
@@ -1,2 +0,0 @@
1
- import { t as nostrSetupPlugin } from "./setup-plugin-api-Ci0XxWqb.mjs";
2
- export { nostrSetupPlugin };