@openclaw/nostr 2026.7.2-beta.7 → 2026.8.1-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,14 +1,14 @@
1
- import { s as resolveNostrAccount } from "./setup-surface-BJCFpe02.js";
1
+ import { s as resolveNostrAccount } from "./setup-surface-uA4hN27A.js";
2
2
  import { getPluginRuntimeGatewayRequestScope } from "./runtime-api.js";
3
- import { n as NostrProfileSchema } from "./config-schema-CelPzg1N.js";
4
- import { a as getNostrRuntime, i as contentToProfile, n as nostrPlugin, o as setNostrRuntime, r as publishNostrProfile, t as getNostrProfileState } from "./channel-O6-jXoT5.js";
3
+ import { n as NostrProfileSchema } from "./config-schema-CkYd8ACn.js";
4
+ import { a as getNostrRuntime, i as contentToProfile, n as nostrPlugin, o as setNostrRuntime, r as publishNostrProfile, t as getNostrProfileState } from "./channel-ity6au4a.js";
5
5
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
6
6
  import { z } from "zod";
7
7
  import { SimplePool } from "nostr-tools";
8
+ import { createFixedWindowRateLimiter as createFixedWindowRateLimiter$1 } from "openclaw/plugin-sdk/webhook-ingress";
8
9
  import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
9
10
  import { isBlockedHostnameOrIp, isLoopbackHost } from "openclaw/plugin-sdk/ssrf-runtime";
10
11
  import { readJsonBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-request-guards";
11
- import { createFixedWindowRateLimiter } from "openclaw/plugin-sdk/webhook-ingress";
12
12
  import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
13
13
  //#region extensions/nostr/src/nostr-profile-url-safety.ts
14
14
  function validateUrlSafety(urlStr) {
@@ -184,7 +184,7 @@ function mergeProfiles(local, imported) {
184
184
  }
185
185
  //#endregion
186
186
  //#region extensions/nostr/src/nostr-profile-http.ts
187
- const profileRateLimiter = createFixedWindowRateLimiter({
187
+ const profileRateLimiter = createFixedWindowRateLimiter$1({
188
188
  windowMs: 6e4,
189
189
  maxRequests: 5,
190
190
  maxTrackedKeys: 2048
@@ -1,6 +1,6 @@
1
- import { a as listNostrAccountIds, c as normalizePubkey, l as validatePrivateKey, n as nostrSetupContract, o as resolveDefaultNostrAccountId, r as nostrSetupWizard, s as resolveNostrAccount, t as nostrSetupAdapter } from "./setup-surface-BJCFpe02.js";
2
- import { a as collectStatusIssuesFromLastError, i as buildChannelConfigSchema, n as NostrProfileSchema, o as createDefaultChannelRuntimeState, r as DEFAULT_ACCOUNT_ID, s as formatPairingApproveHint, t as NostrConfigSchema } from "./config-schema-CelPzg1N.js";
3
- import { a as DEFAULT_RELAYS } from "./setup-adapter-BjKmSwdW.js";
1
+ import { a as listNostrAccountIds, c as normalizePubkey, l as validatePrivateKey, n as nostrSetupContract, o as resolveDefaultNostrAccountId, r as nostrSetupWizard, s as resolveNostrAccount } from "./setup-surface-uA4hN27A.js";
2
+ import { a as collectStatusIssuesFromLastError, i as buildChannelConfigSchema, n as NostrProfileSchema, o as createDefaultChannelRuntimeState, r as DEFAULT_ACCOUNT_ID, s as formatPairingApproveHint, t as NostrConfigSchema } from "./config-schema-CkYd8ACn.js";
3
+ import { c as DEFAULT_RELAYS } from "./setup-adapter-ClKW2fkz.js";
4
4
  import { t as normalizeNostrStateAccountId } from "./state-account-id-CvBZ9s6P.js";
5
5
  import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
6
6
  import { createScopedDmSecurityResolver, createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
@@ -9,14 +9,18 @@ import { missingTargetError } from "openclaw/plugin-sdk/channel-feedback";
9
9
  import { DEFAULT_INGRESS_ADOPTION_STALL_MS, bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, createChannelMessageAdapterFromOutbound, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
10
10
  import { buildPassiveChannelStatusSummary, buildTrafficStatusSummary } from "openclaw/plugin-sdk/extension-shared";
11
11
  import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
12
- import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
12
+ import { isRecord as isNostrIngressRecord, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
13
13
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
14
14
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
15
15
  import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
16
+ import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
16
17
  import { chunkTextForOutbound, sanitizeAssistantVisibleText, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
17
18
  import { SimplePool, finalizeEvent, getPublicKey, verifyEvent } from "nostr-tools";
18
19
  import { decrypt, encrypt } from "nostr-tools/nip04";
19
20
  import { createDirectDmPreCryptoGuardPolicy } from "openclaw/plugin-sdk/direct-dm-guard-policy";
21
+ import { createFixedWindowRateLimiter } from "openclaw/plugin-sdk/webhook-ingress";
22
+ import { retryAsync } from "openclaw/plugin-sdk/retry-runtime";
23
+ import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
20
24
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
21
25
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
22
26
  import { buildChannelOutboundSessionRoute, stripChannelTargetPrefix } from "openclaw/plugin-sdk/core";
@@ -288,19 +292,16 @@ function createNostrCursorStateWriter(options) {
288
292
  let activeFlush;
289
293
  let recoveryFlush;
290
294
  const writeWithRetry = async (cursor) => {
291
- let lastError;
292
- for (const delayMs of CURSOR_WRITE_RETRY_MS) {
293
- if (delayMs > 0) await new Promise((resolve) => {
294
- setTimeout(resolve, delayMs);
295
+ try {
296
+ await retryAsync(() => options.write(cursor), {
297
+ attempts: CURSOR_WRITE_RETRY_MS.length,
298
+ minDelayMs: 0,
299
+ delayMs: ({ attempt }) => CURSOR_WRITE_RETRY_MS[attempt] ?? 0,
300
+ sleep: (delayMs) => sleepWithAbort(delayMs)
295
301
  });
296
- try {
297
- await options.write(cursor);
298
- return;
299
- } catch (error) {
300
- lastError = error;
301
- }
302
+ } catch (error) {
303
+ throw new Error("Nostr cursor state write failed.", { cause: error });
302
304
  }
303
- throw new Error("Nostr cursor state write failed.", { cause: lastError });
304
305
  };
305
306
  const enqueueWrite = (cursor) => {
306
307
  const write = writeTail.then(() => writeWithRetry(cursor));
@@ -382,9 +383,6 @@ var NostrIngressPermanentError = class extends Error {
382
383
  this.name = "NostrIngressPermanentError";
383
384
  }
384
385
  };
385
- function isNostrIngressRecord(value) {
386
- return typeof value === "object" && value !== null && !Array.isArray(value);
387
- }
388
386
  function requiredString(value, field) {
389
387
  if (typeof value === "string" && value.trim()) return value;
390
388
  throw new NostrIngressPermanentError("invalid-event", `Nostr event is missing ${field}.`);
@@ -550,12 +548,8 @@ function createNostrIngress(options) {
550
548
  const pending = await getQueue().listPending({ limit: options.maxPendingEvents });
551
549
  const claims = await getQueue().listClaims();
552
550
  if (pending.length + claims.length >= options.maxPendingEvents) throw new NostrIngressAdmissionRejectedError("backpressure", "Nostr event exceeds the durable ingress backlog.");
553
- let lastError;
554
- for (const delayMs of NOSTR_INGRESS_APPEND_RETRY_MS) {
555
- if (delayMs > 0) await new Promise((resolve) => {
556
- setTimeout(resolve, delayMs);
557
- });
558
- try {
551
+ try {
552
+ return await retryAsync(async () => {
559
553
  const result = await getQueue().enqueue(prepared.facts.eventId, prepared.payload, {
560
554
  receivedAt: prepared.receivedAt,
561
555
  laneKey: prepared.facts.laneKey
@@ -563,11 +557,15 @@ function createNostrIngress(options) {
563
557
  options.afterDurableAppend(prepared.event);
564
558
  monitor.requestDrain();
565
559
  return result.kind === "accepted" ? "accepted" : "duplicate";
566
- } catch (error) {
567
- lastError = error;
568
- }
560
+ }, {
561
+ attempts: NOSTR_INGRESS_APPEND_RETRY_MS.length,
562
+ minDelayMs: 0,
563
+ delayMs: ({ attempt }) => NOSTR_INGRESS_APPEND_RETRY_MS[attempt] ?? 0,
564
+ sleep: (delayMs) => sleepWithAbort(delayMs)
565
+ });
566
+ } catch (error) {
567
+ throw new Error(`Nostr durable admission failed: ${formatErrorMessage(error)}`, { cause: error });
569
568
  }
570
- throw new Error(`Nostr durable admission failed: ${formatErrorMessage(lastError)}`, { cause: lastError });
571
569
  };
572
570
  return {
573
571
  ready: async () => {
@@ -744,48 +742,6 @@ async function publishProfile(pool, sk, relays, profile, lastPublishedAt) {
744
742
  return publishProfileEvent(pool, relays, createProfileEvent(sk, profile, lastPublishedAt));
745
743
  }
746
744
  //#endregion
747
- //#region extensions/nostr/src/nostr-rate-limiter.ts
748
- function createFixedWindowRateLimiter(params) {
749
- const windowMs = Math.max(1, Math.floor(params.windowMs));
750
- const maxRequests = Math.max(1, Math.floor(params.maxRequests));
751
- const maxTrackedKeys = Math.max(1, Math.floor(params.maxTrackedKeys));
752
- const state = /* @__PURE__ */ new Map();
753
- const touch = (key, value) => {
754
- state.delete(key);
755
- state.set(key, value);
756
- };
757
- const prune = (nowMs) => {
758
- for (const [key, entry] of state) if (nowMs - entry.windowStartMs >= windowMs) state.delete(key);
759
- while (state.size > maxTrackedKeys) {
760
- const oldest = state.keys().next().value;
761
- if (!oldest) break;
762
- state.delete(oldest);
763
- }
764
- };
765
- return {
766
- isRateLimited: (key, nowMs = Date.now()) => {
767
- if (!key) return false;
768
- prune(nowMs);
769
- const existing = state.get(key);
770
- if (!existing || nowMs - existing.windowStartMs >= windowMs) {
771
- touch(key, {
772
- count: 1,
773
- windowStartMs: nowMs
774
- });
775
- return false;
776
- }
777
- const nextCount = existing.count + 1;
778
- touch(key, {
779
- count: nextCount,
780
- windowStartMs: existing.windowStartMs
781
- });
782
- return nextCount > maxRequests;
783
- },
784
- size: () => state.size,
785
- clear: () => state.clear()
786
- };
787
- }
788
- //#endregion
789
745
  //#region extensions/nostr/src/nostr-relay-subscription.ts
790
746
  const DEFAULT_EOSE_CONFIRM_DEADLINE_MS = 1e4;
791
747
  const LIBRARY_EOSE_TIMEOUT_MARGIN_MS = 1e3;
@@ -1359,8 +1315,10 @@ async function sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circui
1359
1315
  //#endregion
1360
1316
  //#region extensions/nostr/src/gateway.ts
1361
1317
  const activeBuses = /* @__PURE__ */ new Map();
1362
- const metricsSnapshots = /* @__PURE__ */ new Map();
1363
1318
  const ACCESS_GROUP_PREFIX = "accessGroup:";
1319
+ function normalizeRelayLifecycleKey(relay) {
1320
+ return new URL(relay).toString();
1321
+ }
1364
1322
  function parseNostrAccessGroupAllowFromEntry(entry) {
1365
1323
  const trimmed = entry.trim();
1366
1324
  if (!trimmed.startsWith(ACCESS_GROUP_PREFIX)) return null;
@@ -1396,17 +1354,20 @@ const startNostrGatewayAccount = async (ctx) => {
1396
1354
  const account = ctx.account;
1397
1355
  ctx.setStatus({
1398
1356
  accountId: account.accountId,
1399
- publicKey: account.publicKey
1357
+ publicKey: account.publicKey,
1358
+ lifecycle: "starting"
1400
1359
  });
1401
1360
  ctx.log?.info?.(`[${account.accountId}] starting Nostr provider (pubkey: ${account.publicKey})`);
1402
1361
  if (!account.configured) throw new Error("Nostr private key not configured");
1362
+ const channelRuntime = ctx.channelRuntime;
1363
+ if (!channelRuntime?.inbound?.buildContext) throw new Error("Nostr requires its registered channel runtime context builder");
1403
1364
  const runtime = getNostrRuntime();
1404
1365
  const pairing = createChannelPairingController({
1405
1366
  core: runtime,
1406
1367
  channel: "nostr",
1407
1368
  accountId: account.accountId
1408
1369
  });
1409
- const resolveInboundAccess = async (senderPubkey, rawBody) => await resolveStableChannelMessageIngress({
1370
+ const resolveInboundAccess = async (senderPubkey, rawBody, contextBinding) => await resolveStableChannelMessageIngress({
1410
1371
  channelId: "nostr",
1411
1372
  accountId: account.accountId,
1412
1373
  identity: nostrIngressIdentity,
@@ -1417,11 +1378,12 @@ const startNostrGatewayAccount = async (ctx) => {
1417
1378
  kind: "direct",
1418
1379
  id: senderPubkey
1419
1380
  },
1381
+ contextBinding,
1420
1382
  dmPolicy: account.config.dmPolicy ?? "pairing",
1421
1383
  allowFrom: account.config.allowFrom,
1422
1384
  command: runtime.channel.commands.shouldComputeCommandAuthorized(rawBody, ctx.cfg) ? { modeWhenAccessGroupsOff: "configured" } : void 0
1423
1385
  });
1424
- let busHandle = null;
1386
+ const connectedRelays = /* @__PURE__ */ new Set();
1425
1387
  const authorizeSender = async (input) => {
1426
1388
  const resolved = await resolveInboundAccess(input.senderId, "");
1427
1389
  if (resolved.senderAccess.decision === "allow") return "allow";
@@ -1461,6 +1423,12 @@ const startNostrGatewayAccount = async (ctx) => {
1461
1423
  }
1462
1424
  const { dispatchInboundDirectDm } = await import("./inbound-direct-dm-runtime-DHn9NoYN.js");
1463
1425
  await dispatchInboundDirectDm({
1426
+ channelRuntime,
1427
+ resolveChannelIngress: async (contextBinding) => {
1428
+ const exactAccess = await resolveInboundAccess(senderPubkey, text, contextBinding);
1429
+ if (!exactAccess.senderAccess.allowed) throw new Error(`Nostr sender authorization changed before dispatch (${senderPubkey})`);
1430
+ return exactAccess;
1431
+ },
1464
1432
  cfg: ctx.cfg,
1465
1433
  channel: "nostr",
1466
1434
  channelLabel: "Nostr",
@@ -1501,9 +1469,17 @@ const startNostrGatewayAccount = async (ctx) => {
1501
1469
  ctx.log?.error?.(`[${account.accountId}] Nostr error (${context}): ${error.message}`);
1502
1470
  },
1503
1471
  onConnect: (relay) => {
1472
+ connectedRelays.add(normalizeRelayLifecycleKey(relay));
1473
+ ctx.setStatus(channelReadyPatch({ accountId: account.accountId }));
1504
1474
  ctx.log?.debug?.(`[${account.accountId}] Connected to relay: ${relay}`);
1505
1475
  },
1506
1476
  onDisconnect: (relay) => {
1477
+ connectedRelays.delete(normalizeRelayLifecycleKey(relay));
1478
+ if (connectedRelays.size === 0) ctx.setStatus({
1479
+ accountId: account.accountId,
1480
+ connected: false,
1481
+ lifecycle: "recovering"
1482
+ });
1507
1483
  ctx.log?.debug?.(`[${account.accountId}] Disconnected from relay: ${relay}`);
1508
1484
  },
1509
1485
  onEose: (relays) => {
@@ -1514,17 +1490,13 @@ const startNostrGatewayAccount = async (ctx) => {
1514
1490
  else if (event.name === "relay.circuit_breaker.open") ctx.log?.warn?.(`[${account.accountId}] Circuit breaker opened for relay: ${event.labels?.relay}`);
1515
1491
  else if (event.name === "relay.circuit_breaker.close") ctx.log?.info?.(`[${account.accountId}] Circuit breaker closed for relay: ${event.labels?.relay}`);
1516
1492
  else if (event.name === "relay.error") ctx.log?.debug?.(`[${account.accountId}] Relay error: ${event.labels?.relay}`);
1517
- if (busHandle) metricsSnapshots.set(account.accountId, busHandle.getMetrics());
1518
1493
  }
1519
1494
  });
1520
- busHandle = bus;
1521
1495
  activeBuses.set(account.accountId, bus);
1522
1496
  ctx.log?.info?.(`[${account.accountId}] Nostr provider started with ${account.relays.length} configured relay(s)`);
1523
1497
  return { stop: async () => {
1524
- await bus.close();
1525
- if (busHandle === bus) busHandle = null;
1526
1498
  if (activeBuses.get(account.accountId) === bus) activeBuses.delete(account.accountId);
1527
- metricsSnapshots.delete(account.accountId);
1499
+ await bus.close();
1528
1500
  ctx.log?.info?.(`[${account.accountId}] Nostr provider stopped`);
1529
1501
  } };
1530
1502
  },
@@ -1618,6 +1590,14 @@ function normalizeNostrTarget(target) {
1618
1590
  return target.trim();
1619
1591
  }
1620
1592
  }
1593
+ function inferNostrTargetChatType(target) {
1594
+ try {
1595
+ normalizePubkey(stripNostrTargetPrefix(target));
1596
+ return "direct";
1597
+ } catch {
1598
+ return;
1599
+ }
1600
+ }
1621
1601
  const resolveNostrDmPolicy = createScopedDmSecurityResolver({
1622
1602
  channelKey: "nostr",
1623
1603
  resolvePolicy: (account) => account.config.dmPolicy,
@@ -1692,7 +1672,6 @@ const nostrPlugin = createChatChannelPlugin({
1692
1672
  },
1693
1673
  reload: { configPrefixes: ["channels.nostr"] },
1694
1674
  configSchema: buildChannelConfigSchema(NostrConfigSchema),
1695
- setup: nostrSetupAdapter,
1696
1675
  setupContract: nostrSetupContract,
1697
1676
  setupWizard: nostrSetupWizard,
1698
1677
  config: {
@@ -1707,6 +1686,7 @@ const nostrPlugin = createChatChannelPlugin({
1707
1686
  messaging: {
1708
1687
  targetPrefixes: ["nostr"],
1709
1688
  normalizeTarget: normalizeNostrTarget,
1689
+ inferTargetChatType: ({ to }) => inferNostrTargetChatType(to),
1710
1690
  targetResolver: {
1711
1691
  looksLikeId: (input, normalized) => {
1712
1692
  const trimmed = normalized?.trim() || stripNostrTargetPrefix(input);
@@ -1,2 +1,2 @@
1
- import { n as nostrPlugin } from "./channel-O6-jXoT5.js";
1
+ import { n as nostrPlugin } from "./channel-ity6au4a.js";
2
2
  export { nostrPlugin };
@@ -46,6 +46,7 @@ const NostrConfigSchema = z.object({
46
46
  defaultAccount: z.string().optional(),
47
47
  /** Whether this channel is enabled */
48
48
  enabled: z.boolean().optional(),
49
+ configWrites: z.boolean().optional(),
49
50
  /** Markdown formatting overrides (tables). */
50
51
  markdown: MarkdownConfigSchema,
51
52
  /** Private key in hex or nsec bech32 format */
@@ -1,22 +1,20 @@
1
1
  import { t as normalizeNostrStateAccountId } from "./state-account-id-CvBZ9s6P.js";
2
+ import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
- import { archiveLegacyStateSource } from "openclaw/plugin-sdk/runtime-doctor";
5
+ import { archiveLegacyStateSource } from "openclaw/plugin-sdk/runtime-doctor-migrations";
5
6
  //#region extensions/nostr/doctor-contract-api.ts
6
7
  const BUS_STATE_NAMESPACE = "bus-state";
7
8
  const PROFILE_STATE_NAMESPACE = "profile-state";
8
9
  const MAX_NOSTR_STATE_ENTRIES = 256;
9
- function finiteNumberOrNull(value) {
10
- return typeof value === "number" && Number.isFinite(value) ? value : null;
11
- }
12
10
  function parseBusState(value) {
13
11
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
14
12
  const parsed = value;
15
13
  if (parsed.version !== 1 && parsed.version !== 2) return null;
16
14
  return {
17
15
  version: 2,
18
- lastProcessedAt: finiteNumberOrNull(parsed.lastProcessedAt),
19
- gatewayStartedAt: finiteNumberOrNull(parsed.gatewayStartedAt),
16
+ lastProcessedAt: asFiniteNumber(parsed.lastProcessedAt) ?? null,
17
+ gatewayStartedAt: asFiniteNumber(parsed.gatewayStartedAt) ?? null,
20
18
  recentEventIds: parsed.version === 2 && Array.isArray(parsed.recentEventIds) ? parsed.recentEventIds.filter((entry) => typeof entry === "string") : []
21
19
  };
22
20
  }
@@ -31,7 +29,7 @@ function parseProfileState(value) {
31
29
  }
32
30
  return {
33
31
  version: 1,
34
- lastPublishedAt: finiteNumberOrNull(parsed.lastPublishedAt),
32
+ lastPublishedAt: asFiniteNumber(parsed.lastPublishedAt) ?? null,
35
33
  lastPublishedEventId: typeof parsed.lastPublishedEventId === "string" ? parsed.lastPublishedEventId : null,
36
34
  lastPublishResults: rawResults === null || Object.keys(lastPublishResults).length === 0 ? null : lastPublishResults
37
35
  };
package/dist/index.js CHANGED
@@ -27,6 +27,10 @@ var nostr_default = defineBundledChannelEntry({
27
27
  specifier: "./channel-plugin-api.js",
28
28
  exportName: "nostrPlugin"
29
29
  },
30
+ secrets: {
31
+ specifier: "./secret-contract-api.js",
32
+ exportName: "channelSecrets"
33
+ },
30
34
  runtime: {
31
35
  specifier: "./api.js",
32
36
  exportName: "setNostrRuntime"
@@ -0,0 +1,37 @@
1
+ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import { collectSecretInputAssignment, createChannelSecretTargetRegistryEntries, getChannelRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
3
+ //#region extensions/nostr/src/secret-contract.ts
4
+ const secretTargetRegistryEntries = createChannelSecretTargetRegistryEntries({
5
+ channelKey: "nostr",
6
+ channel: ["privateKey"]
7
+ });
8
+ function collectRuntimeConfigAssignments(params) {
9
+ const nostr = getChannelRecord(params.config, "nostr");
10
+ if (!nostr) return;
11
+ const accountId = normalizeAccountId(typeof nostr.defaultAccount === "string" ? nostr.defaultAccount : void 0);
12
+ collectSecretInputAssignment({
13
+ value: nostr.privateKey,
14
+ path: "channels.nostr.privateKey",
15
+ expected: "string",
16
+ defaults: params.defaults,
17
+ context: params.context,
18
+ active: nostr.enabled !== false,
19
+ inactiveReason: "Nostr channel is disabled.",
20
+ owner: {
21
+ ownerKind: "account",
22
+ ownerId: `nostr:${accountId}`,
23
+ requiredForGateway: false,
24
+ disposition: "isolate",
25
+ contract: nostr
26
+ },
27
+ apply: (value) => {
28
+ nostr.privateKey = value;
29
+ }
30
+ });
31
+ }
32
+ const channelSecrets = {
33
+ secretTargetRegistryEntries,
34
+ collectRuntimeConfigAssignments
35
+ };
36
+ //#endregion
37
+ export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries };
@@ -1,10 +1,22 @@
1
1
  import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
2
+ import { hasConfiguredSecretInput, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
2
3
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
3
- import { patchTopLevelChannelConfigSection, splitSetupEntries } from "openclaw/plugin-sdk/setup";
4
+ import { createSetupTranslator, createStandardChannelSetupStatus, patchTopLevelChannelConfigSection, splitSetupEntries } from "openclaw/plugin-sdk/setup";
4
5
  import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
5
6
  //#region extensions/nostr/src/default-relays.ts
6
7
  const DEFAULT_RELAYS = ["wss://relay.damus.io", "wss://nos.lol"];
7
8
  //#endregion
9
+ //#region extensions/nostr/src/private-key.ts
10
+ const NOSTR_PRIVATE_KEY_ENV_VAR = "NOSTR_PRIVATE_KEY";
11
+ function hasConfiguredNostrPrivateKey(value) {
12
+ return hasConfiguredSecretInput(value) || Boolean(process.env["NOSTR_PRIVATE_KEY"]?.trim());
13
+ }
14
+ function resolveNostrPrivateKey(value) {
15
+ const configured = normalizeSecretInputString(value);
16
+ if (configured || hasConfiguredSecretInput(value)) return configured ?? "";
17
+ return process.env["NOSTR_PRIVATE_KEY"]?.trim() ?? "";
18
+ }
19
+ //#endregion
8
20
  //#region extensions/nostr/src/setup-adapter.ts
9
21
  const channel = "nostr";
10
22
  function buildNostrSetupPatch(accountId, patch) {
@@ -41,25 +53,23 @@ function createNostrSetupAdapter(params) {
41
53
  patch: buildNostrSetupPatch(accountId, name?.trim() ? { name: name.trim() } : {})
42
54
  }),
43
55
  validateInput: ({ input }) => {
44
- const typedInput = input;
45
- if (!typedInput.useEnv) {
46
- const privateKey = typedInput.privateKey?.trim();
56
+ if (!input.useEnv) {
57
+ const privateKey = input.privateKey?.trim();
47
58
  if (!privateKey) return "Nostr requires --private-key or --use-env.";
48
59
  if (!params.validatePrivateKey(privateKey)) return "Nostr private key must be valid nsec or 64-character hex.";
49
60
  }
50
- if (typedInput.relayUrls?.trim()) return parseRelayUrls(typedInput.relayUrls).error ?? null;
61
+ if (input.relayUrls?.trim()) return parseRelayUrls(input.relayUrls).error ?? null;
51
62
  return null;
52
63
  },
53
64
  applyAccountConfig: ({ cfg, accountId, input }) => {
54
- const typedInput = input;
55
- const relayResult = typedInput.relayUrls?.trim() ? parseRelayUrls(typedInput.relayUrls) : { relays: [] };
65
+ const relayResult = input.relayUrls?.trim() ? parseRelayUrls(input.relayUrls) : { relays: [] };
56
66
  return patchTopLevelChannelConfigSection({
57
67
  cfg,
58
68
  channel,
59
69
  enabled: true,
60
- clearFields: typedInput.useEnv ? ["privateKey"] : void 0,
70
+ clearFields: input.useEnv ? ["privateKey"] : void 0,
61
71
  patch: buildNostrSetupPatch(accountId, {
62
- ...typedInput.useEnv ? {} : { privateKey: typedInput.privateKey?.trim() },
72
+ ...input.useEnv ? {} : { privateKey: input.privateKey?.trim() },
63
73
  ...relayResult.relays.length > 0 ? { relays: relayResult.relays } : {}
64
74
  })
65
75
  });
@@ -89,11 +99,32 @@ function createNostrSetupContract(adapter) {
89
99
  cli: {
90
100
  flags: "--use-env",
91
101
  description: "Use NOSTR_PRIVATE_KEY"
92
- }
102
+ },
103
+ envVars: [NOSTR_PRIVATE_KEY_ENV_VAR]
93
104
  }
94
105
  },
95
- legacyAdapter: adapter
106
+ adapter
107
+ });
108
+ }
109
+ function createNostrSetupStatus(resolveAccount) {
110
+ const t = createSetupTranslator();
111
+ return createStandardChannelSetupStatus({
112
+ channelLabel: "Nostr",
113
+ configuredLabel: t("wizard.channels.statusConfigured"),
114
+ unconfiguredLabel: t("wizard.channels.statusNeedsPrivateKey"),
115
+ configuredHint: t("wizard.channels.statusConfigured"),
116
+ unconfiguredHint: t("wizard.channels.statusNeedsPrivateKey"),
117
+ configuredScore: 1,
118
+ unconfiguredScore: 0,
119
+ includeStatusLine: true,
120
+ resolveConfigured: ({ cfg, accountId }) => resolveAccount({
121
+ cfg,
122
+ accountId
123
+ }).configured,
124
+ resolveExtraStatusLines: ({ cfg }) => {
125
+ return [`Relays: ${resolveAccount({ cfg }).relays.length || DEFAULT_RELAYS.length}`];
126
+ }
96
127
  });
97
128
  }
98
129
  //#endregion
99
- export { DEFAULT_RELAYS as a, parseRelayUrls as i, createNostrSetupAdapter as n, createNostrSetupContract as r, buildNostrSetupPatch as t };
130
+ export { parseRelayUrls as a, DEFAULT_RELAYS as c, createNostrSetupStatus as i, createNostrSetupAdapter as n, hasConfiguredNostrPrivateKey as o, createNostrSetupContract as r, resolveNostrPrivateKey as s, buildNostrSetupPatch as t };
package/dist/setup-api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { r as nostrSetupWizard, t as nostrSetupAdapter } from "./setup-surface-BJCFpe02.js";
1
+ import { r as nostrSetupWizard, t as nostrSetupAdapter } from "./setup-surface-uA4hN27A.js";
2
2
  export { nostrSetupAdapter, nostrSetupWizard };
@@ -5,6 +5,10 @@ var setup_entry_default = defineBundledChannelSetupEntry({
5
5
  plugin: {
6
6
  specifier: "./setup-plugin-api.js",
7
7
  exportName: "nostrSetupPlugin"
8
+ },
9
+ secrets: {
10
+ specifier: "./secret-contract-api.js",
11
+ exportName: "channelSecrets"
8
12
  }
9
13
  });
10
14
  //#endregion
@@ -1,27 +1,21 @@
1
- import { i as buildChannelConfigSchema, t as NostrConfigSchema } from "./config-schema-CelPzg1N.js";
2
- import { a as DEFAULT_RELAYS, n as createNostrSetupAdapter, r as createNostrSetupContract } from "./setup-adapter-BjKmSwdW.js";
1
+ import { i as buildChannelConfigSchema, t as NostrConfigSchema } from "./config-schema-CkYd8ACn.js";
2
+ import { c as DEFAULT_RELAYS, i as createNostrSetupStatus, n as createNostrSetupAdapter, o as hasConfiguredNostrPrivateKey, r as createNostrSetupContract, s as resolveNostrPrivateKey } from "./setup-adapter-ClKW2fkz.js";
3
3
  import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
4
- import { DEFAULT_ACCOUNT_ID, createDelegatedSetupWizardProxy, createSetupTranslator, createStandardChannelSetupStatus } from "openclaw/plugin-sdk/setup-runtime";
4
+ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
5
+ import { createDelegatedSetupWizardProxy } from "openclaw/plugin-sdk/setup-runtime";
5
6
  //#region extensions/nostr/src/channel.setup.ts
6
- const t = createSetupTranslator();
7
7
  const channel = "nostr";
8
8
  function getNostrConfig(cfg) {
9
9
  return cfg.channels?.nostr;
10
10
  }
11
- function listSetupNostrAccountIds(cfg) {
12
- const nostrCfg = getNostrConfig(cfg);
13
- if (!(typeof nostrCfg?.privateKey === "string" ? nostrCfg.privateKey.trim() : "")) return [];
14
- return [resolveDefaultSetupNostrAccountId(cfg)];
15
- }
16
11
  function resolveDefaultSetupNostrAccountId(cfg) {
17
- const configured = getNostrConfig(cfg)?.defaultAccount;
18
- return typeof configured === "string" && configured.trim() ? configured.trim() : DEFAULT_ACCOUNT_ID;
12
+ return normalizeAccountId(getNostrConfig(cfg)?.defaultAccount);
19
13
  }
20
14
  function resolveSetupNostrAccount(params) {
21
15
  const nostrCfg = getNostrConfig(params.cfg);
22
- const accountId = params.accountId?.trim() || resolveDefaultSetupNostrAccountId(params.cfg);
23
- const privateKey = typeof nostrCfg?.privateKey === "string" ? nostrCfg.privateKey.trim() : "";
24
- const configured = Boolean(privateKey);
16
+ const accountId = normalizeAccountId(params.accountId ?? resolveDefaultSetupNostrAccountId(params.cfg));
17
+ const privateKey = resolveNostrPrivateKey(nostrCfg?.privateKey);
18
+ const configured = hasConfiguredNostrPrivateKey(nostrCfg?.privateKey);
25
19
  return {
26
20
  accountId,
27
21
  name: typeof nostrCfg?.name === "string" ? nostrCfg.name : void 0,
@@ -42,34 +36,10 @@ function resolveSetupNostrAccount(params) {
42
36
  }
43
37
  };
44
38
  }
45
- function looksLikeNostrPrivateKey(privateKey) {
46
- return privateKey.startsWith("nsec1") || privateKey.startsWith("NSEC1") || /^[0-9a-fA-F]{64}$/.test(privateKey);
47
- }
48
- const nostrSetupAdapter = createNostrSetupAdapter({
49
- resolveAccountId: (cfg, accountId) => accountId?.trim() || resolveDefaultSetupNostrAccountId(cfg),
50
- validatePrivateKey: looksLikeNostrPrivateKey
51
- });
52
- const nostrSetupContract = createNostrSetupContract(nostrSetupAdapter);
53
39
  const nostrSetupWizard = createDelegatedSetupWizardProxy({
54
40
  channel,
55
- loadWizard: async () => (await import("./setup-surface-BJCFpe02.js").then((n) => n.i)).nostrSetupWizard,
56
- status: { ...createStandardChannelSetupStatus({
57
- channelLabel: "Nostr",
58
- configuredLabel: t("wizard.channels.statusConfigured"),
59
- unconfiguredLabel: t("wizard.channels.statusNeedsPrivateKey"),
60
- configuredHint: t("wizard.channels.statusConfigured"),
61
- unconfiguredHint: t("wizard.channels.statusNeedsPrivateKey"),
62
- configuredScore: 1,
63
- unconfiguredScore: 0,
64
- includeStatusLine: true,
65
- resolveConfigured: ({ cfg, accountId }) => resolveSetupNostrAccount({
66
- cfg,
67
- accountId
68
- }).configured,
69
- resolveExtraStatusLines: ({ cfg }) => {
70
- return [`Relays: ${resolveSetupNostrAccount({ cfg }).relays.length || DEFAULT_RELAYS.length}`];
71
- }
72
- }) },
41
+ loadWizard: async () => (await import("./setup-surface-uA4hN27A.js").then((n) => n.i)).nostrSetupWizard,
42
+ status: createNostrSetupStatus(resolveSetupNostrAccount),
73
43
  resolveShouldPromptAccountIds: () => false,
74
44
  delegatePrepare: true,
75
45
  delegateFinalize: true
@@ -91,11 +61,13 @@ const nostrSetupPlugin = {
91
61
  },
92
62
  reload: { configPrefixes: ["channels.nostr"] },
93
63
  configSchema: buildChannelConfigSchema(NostrConfigSchema),
94
- setup: nostrSetupAdapter,
95
- setupContract: nostrSetupContract,
64
+ setupContract: createNostrSetupContract(createNostrSetupAdapter({
65
+ resolveAccountId: (cfg, accountId) => accountId?.trim() || resolveDefaultSetupNostrAccountId(cfg),
66
+ validatePrivateKey: (privateKey) => /^(?:nsec1|NSEC1)|^[0-9a-fA-F]{64}$/u.test(privateKey)
67
+ })),
96
68
  setupWizard: nostrSetupWizard,
97
69
  config: {
98
- listAccountIds: listSetupNostrAccountIds,
70
+ listAccountIds: (cfg) => resolveSetupNostrAccount({ cfg }).configured ? [resolveDefaultSetupNostrAccountId(cfg)] : [],
99
71
  resolveAccount: (cfg, accountId) => resolveSetupNostrAccount({
100
72
  cfg,
101
73
  accountId
@@ -1,11 +1,11 @@
1
- import { a as DEFAULT_RELAYS, i as parseRelayUrls, n as createNostrSetupAdapter, r as createNostrSetupContract, t as buildNostrSetupPatch } from "./setup-adapter-BjKmSwdW.js";
1
+ import { a as parseRelayUrls, c as DEFAULT_RELAYS, i as createNostrSetupStatus, n as createNostrSetupAdapter, o as hasConfiguredNostrPrivateKey, r as createNostrSetupContract, s as resolveNostrPrivateKey, t as buildNostrSetupPatch } from "./setup-adapter-ClKW2fkz.js";
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";
5
5
  import { getPublicKey, nip19 } from "nostr-tools";
6
6
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
7
7
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1 } from "openclaw/plugin-sdk/routing";
8
- import { createSetupTranslator, createStandardChannelSetupStatus, createTopLevelChannelDmPolicy, createTopLevelChannelParsedAllowFromPrompt, defineTokenCredential, formatDocsLink, mergeAllowFromEntries, parseSetupEntriesWithParser, patchTopLevelChannelConfigSection, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
8
+ import { createSetupTranslator, createTopLevelChannelDmPolicy, createTopLevelChannelParsedAllowFromPrompt, defineTokenCredential, formatDocsLink, mergeAllowFromEntries, parseSetupEntriesWithParser, patchTopLevelChannelConfigSection, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
9
9
  //#region \0rolldown/runtime.js
10
10
  var __defProp = Object.defineProperty;
11
11
  var __exportAll = (all, no_symbols) => {
@@ -59,7 +59,7 @@ const { listAccountIds: listNostrAccountIds, resolveDefaultAccountId: resolveDef
59
59
  fallbackAccountIdWhenEmpty: false,
60
60
  resolveImplicitAccountId: (cfg) => {
61
61
  const account = cfg.channels?.nostr;
62
- return normalizeSecretInputString(account?.privateKey) ? normalizeOptionalAccountId(account?.defaultAccount) ?? DEFAULT_ACCOUNT_ID : void 0;
62
+ return hasConfiguredNostrPrivateKey(account?.privateKey) ? normalizeOptionalAccountId(account?.defaultAccount) ?? DEFAULT_ACCOUNT_ID : void 0;
63
63
  }
64
64
  });
65
65
  /**
@@ -69,8 +69,8 @@ function resolveNostrAccount(opts) {
69
69
  const accountId = normalizeAccountId(opts.accountId ?? resolveDefaultNostrAccountId(opts.cfg));
70
70
  const nostrCfg = opts.cfg.channels?.nostr;
71
71
  const baseEnabled = nostrCfg?.enabled !== false;
72
- const privateKey = normalizeSecretInputString(nostrCfg?.privateKey) ?? "";
73
- const configured = Boolean(privateKey);
72
+ const privateKey = resolveNostrPrivateKey(nostrCfg?.privateKey);
73
+ const configured = hasConfiguredNostrPrivateKey(nostrCfg?.privateKey);
74
74
  let publicKey = "";
75
75
  if (privateKey) try {
76
76
  publicKey = getPublicKeyFromPrivate(privateKey);
@@ -162,20 +162,7 @@ const nostrSetupWizard = {
162
162
  channel,
163
163
  resolveAccountIdForConfigure: ({ accountOverride, defaultAccountId }) => accountOverride?.trim() || defaultAccountId,
164
164
  resolveShouldPromptAccountIds: () => false,
165
- status: createStandardChannelSetupStatus({
166
- channelLabel: "Nostr",
167
- configuredLabel: t("wizard.channels.statusConfigured"),
168
- unconfiguredLabel: t("wizard.channels.statusNeedsPrivateKey"),
169
- configuredHint: t("wizard.channels.statusConfigured"),
170
- unconfiguredHint: t("wizard.channels.statusNeedsPrivateKey"),
171
- configuredScore: 1,
172
- unconfiguredScore: 0,
173
- includeStatusLine: true,
174
- resolveConfigured: ({ cfg }) => resolveNostrAccount({ cfg }).configured,
175
- resolveExtraStatusLines: ({ cfg }) => {
176
- return [`Relays: ${resolveNostrAccount({ cfg }).relays.length || DEFAULT_RELAYS.length}`];
177
- }
178
- }),
165
+ status: createNostrSetupStatus(resolveNostrAccount),
179
166
  introNote: {
180
167
  title: t("wizard.nostr.setupTitle"),
181
168
  lines: NOSTR_SETUP_HELP_LINES
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "id": "nostr",
3
+ "doctorContract": {
4
+ "stateMigrations": true
5
+ },
3
6
  "name": "Nostr",
4
7
  "description": "OpenClaw Nostr channel plugin for NIP-04 encrypted direct messages.",
5
8
  "activation": {
@@ -28,6 +31,9 @@
28
31
  "enabled": {
29
32
  "type": "boolean"
30
33
  },
34
+ "configWrites": {
35
+ "type": "boolean"
36
+ },
31
37
  "markdown": {
32
38
  "type": "object",
33
39
  "properties": {
@@ -73,6 +79,29 @@
73
79
  ],
74
80
  "additionalProperties": false
75
81
  },
82
+ {
83
+ "type": "object",
84
+ "properties": {
85
+ "source": {
86
+ "type": "string",
87
+ "const": "store"
88
+ },
89
+ "provider": {
90
+ "type": "string",
91
+ "pattern": "^[a-z][a-z0-9_-]{0,63}$"
92
+ },
93
+ "id": {
94
+ "type": "string",
95
+ "pattern": "^[A-Z][A-Z0-9_]{0,127}$"
96
+ }
97
+ },
98
+ "required": [
99
+ "source",
100
+ "provider",
101
+ "id"
102
+ ],
103
+ "additionalProperties": false
104
+ },
76
105
  {
77
106
  "type": "object",
78
107
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nostr",
3
- "version": "2026.7.2-beta.7",
3
+ "version": "2026.8.1-beta.3",
4
4
  "description": "OpenClaw Nostr channel plugin for NIP-04 encrypted direct messages.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,7 +12,7 @@
12
12
  "zod": "4.4.3"
13
13
  },
14
14
  "peerDependencies": {
15
- "openclaw": ">=2026.7.2-beta.7"
15
+ "openclaw": ">=2026.8.1-beta.3"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "openclaw": {
@@ -65,7 +65,10 @@
65
65
  "cli": {
66
66
  "flags": "--use-env",
67
67
  "description": "Use NOSTR_PRIVATE_KEY"
68
- }
68
+ },
69
+ "envVars": [
70
+ "NOSTR_PRIVATE_KEY"
71
+ ]
69
72
  }
70
73
  ]
71
74
  }
@@ -76,10 +79,10 @@
76
79
  "minHostVersion": ">=2026.4.10"
77
80
  },
78
81
  "compat": {
79
- "pluginApi": ">=2026.7.2-beta.7"
82
+ "pluginApi": ">=2026.8.1-beta.3"
80
83
  },
81
84
  "build": {
82
- "openclawVersion": "2026.7.2-beta.7"
85
+ "openclawVersion": "2026.8.1-beta.3"
83
86
  },
84
87
  "release": {
85
88
  "publishToClawHub": true,