@openclaw/nostr 2026.5.7 → 2026.5.10-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/api.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { o as resolveNostrAccount } from "./setup-surface-DxAaUTyC.js";
2
2
  import { getPluginRuntimeGatewayRequestScope } from "./runtime-api.js";
3
- import { n as NostrProfileSchema } from "./config-schema-DIk4jlBg.js";
4
- import { a as setNostrRuntime, i as getNostrRuntime, n as nostrPlugin, o as contentToProfile, r as publishNostrProfile, t as getNostrProfileState } from "./channel-DfEqBtUh.js";
3
+ import { n as NostrProfileSchema } from "./config-schema-Clq-vlF1.js";
4
+ import { a as setNostrRuntime, i as getNostrRuntime, n as nostrPlugin, o as contentToProfile, r as publishNostrProfile, t as getNostrProfileState } from "./channel-BSOktL3g.js";
5
5
  import { z } from "openclaw/plugin-sdk/zod";
6
6
  import { SimplePool, verifyEvent } from "nostr-tools";
7
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, readStringValue } from "openclaw/plugin-sdk/text-runtime";
7
8
  import { readJsonBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-request-guards";
8
9
  import { createFixedWindowRateLimiter } from "openclaw/plugin-sdk/webhook-ingress";
9
10
  import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
@@ -178,17 +179,6 @@ function mergeProfiles(local, imported) {
178
179
  }
179
180
  //#endregion
180
181
  //#region extensions/nostr/src/nostr-profile-http.ts
181
- function readStringValue(value) {
182
- return typeof value === "string" ? value : void 0;
183
- }
184
- function normalizeOptionalLowercaseString(value) {
185
- if (typeof value !== "string") return;
186
- const trimmed = value.trim();
187
- return trimmed ? trimmed.toLowerCase() : void 0;
188
- }
189
- function normalizeLowercaseStringOrEmpty(value) {
190
- return normalizeOptionalLowercaseString(value) ?? "";
191
- }
192
182
  const profileRateLimiter = createFixedWindowRateLimiter({
193
183
  windowMs: 6e4,
194
184
  maxRequests: 5,
@@ -1,22 +1,23 @@
1
1
  import { a as resolveDefaultNostrAccountId, c as validatePrivateKey, i as listNostrAccountIds, n as nostrSetupWizard, o as resolveNostrAccount, s as normalizePubkey, t as nostrSetupAdapter } from "./setup-surface-DxAaUTyC.js";
2
- import { a as collectStatusIssuesFromLastError, c as formatPairingApproveHint, i as buildChannelConfigSchema, l as resolveInboundDirectDmAccessWithRuntime, n as NostrProfileSchema, o as createDefaultChannelRuntimeState, r as DEFAULT_ACCOUNT_ID, s as createPreCryptoDirectDmAuthorizer, t as NostrConfigSchema } from "./config-schema-DIk4jlBg.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-Clq-vlF1.js";
3
3
  import { t as DEFAULT_RELAYS } from "./default-relays-DLwdWOTu.js";
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";
7
+ import { createChannelMessageAdapterFromOutbound } from "openclaw/plugin-sdk/channel-message";
7
8
  import { buildPassiveChannelStatusSummary, buildTrafficStatusSummary, safeParseJsonWithSchema } from "openclaw/plugin-sdk/extension-shared";
8
9
  import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
10
+ import { z } from "openclaw/plugin-sdk/zod";
11
+ import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
9
12
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
10
13
  import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
11
14
  import { SimplePool, finalizeEvent, getPublicKey, verifyEvent } from "nostr-tools";
12
15
  import { decrypt, encrypt } from "nostr-tools/nip04";
13
16
  import { createDirectDmPreCryptoGuardPolicy } from "openclaw/plugin-sdk/direct-dm-guard-policy";
14
17
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
15
- import crypto from "node:crypto";
16
- import fs from "node:fs/promises";
17
18
  import os from "node:os";
18
19
  import path from "node:path";
19
- import { z } from "zod";
20
+ import { privateFileStore } from "openclaw/plugin-sdk/security-runtime";
20
21
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
21
22
  import { buildChannelOutboundSessionRoute, stripChannelTargetPrefix } from "openclaw/plugin-sdk/core";
22
23
  //#region extensions/nostr/src/metrics.ts
@@ -460,29 +461,22 @@ function safeParseState(raw) {
460
461
  async function readNostrBusState(params) {
461
462
  const filePath = resolveNostrStatePath(params.accountId, params.env);
462
463
  try {
463
- return safeParseState(await fs.readFile(filePath, "utf-8"));
464
- } catch (err) {
465
- if (err.code === "ENOENT") return null;
464
+ const raw = await privateFileStore(path.dirname(filePath)).readTextIfExists(path.basename(filePath));
465
+ if (raw === null) return null;
466
+ return safeParseState(raw);
467
+ } catch {
466
468
  return null;
467
469
  }
468
470
  }
469
471
  async function writeNostrBusState(params) {
470
472
  const filePath = resolveNostrStatePath(params.accountId, params.env);
471
- const dir = path.dirname(filePath);
472
- await fs.mkdir(dir, {
473
- recursive: true,
474
- mode: 448
475
- });
476
- const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
477
473
  const payload = {
478
474
  version: STORE_VERSION,
479
475
  lastProcessedAt: params.lastProcessedAt,
480
476
  gatewayStartedAt: params.gatewayStartedAt,
481
477
  recentEventIds: (params.recentEventIds ?? []).filter((x) => typeof x === "string")
482
478
  };
483
- await fs.writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, { encoding: "utf-8" });
484
- await fs.chmod(tmp, 384);
485
- await fs.rename(tmp, filePath);
479
+ await privateFileStore(path.dirname(filePath)).writeJson(path.basename(filePath), payload, { trailingNewline: true });
486
480
  }
487
481
  /**
488
482
  * Determine the `since` timestamp for subscription.
@@ -501,29 +495,22 @@ function safeParseProfileState(raw) {
501
495
  async function readNostrProfileState(params) {
502
496
  const filePath = resolveNostrProfileStatePath(params.accountId, params.env);
503
497
  try {
504
- return safeParseProfileState(await fs.readFile(filePath, "utf-8"));
505
- } catch (err) {
506
- if (err.code === "ENOENT") return null;
498
+ const raw = await privateFileStore(path.dirname(filePath)).readTextIfExists(path.basename(filePath));
499
+ if (raw === null) return null;
500
+ return safeParseProfileState(raw);
501
+ } catch {
507
502
  return null;
508
503
  }
509
504
  }
510
505
  async function writeNostrProfileState(params) {
511
506
  const filePath = resolveNostrProfileStatePath(params.accountId, params.env);
512
- const dir = path.dirname(filePath);
513
- await fs.mkdir(dir, {
514
- recursive: true,
515
- mode: 448
516
- });
517
- const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
518
507
  const payload = {
519
508
  version: PROFILE_STATE_VERSION,
520
509
  lastPublishedAt: params.lastPublishedAt,
521
510
  lastPublishedEventId: params.lastPublishedEventId,
522
511
  lastPublishResults: params.lastPublishResults
523
512
  };
524
- await fs.writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, { encoding: "utf-8" });
525
- await fs.chmod(tmp, 384);
526
- await fs.rename(tmp, filePath);
513
+ await privateFileStore(path.dirname(filePath)).writeJson(path.basename(filePath), payload, { trailingNewline: true });
527
514
  }
528
515
  //#endregion
529
516
  //#region extensions/nostr/src/seen-tracker.ts
@@ -1110,38 +1097,38 @@ async function sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circui
1110
1097
  //#region extensions/nostr/src/gateway.ts
1111
1098
  const activeBuses = /* @__PURE__ */ new Map();
1112
1099
  const metricsSnapshots = /* @__PURE__ */ new Map();
1100
+ const ACCESS_GROUP_PREFIX = "accessGroup:";
1101
+ function parseNostrAccessGroupAllowFromEntry(entry) {
1102
+ const trimmed = entry.trim();
1103
+ if (!trimmed.startsWith(ACCESS_GROUP_PREFIX)) return null;
1104
+ return trimmed.slice(12).trim() || null;
1105
+ }
1113
1106
  function normalizeNostrAllowEntry(entry) {
1114
1107
  const trimmed = entry.trim();
1115
1108
  if (!trimmed) return null;
1116
1109
  if (trimmed === "*") return "*";
1110
+ const accessGroup = parseNostrAccessGroupAllowFromEntry(trimmed);
1111
+ if (accessGroup) return `accessGroup:${accessGroup}`;
1117
1112
  try {
1118
1113
  return normalizePubkey(trimmed.replace(/^nostr:/i, ""));
1119
1114
  } catch {
1120
1115
  return null;
1121
1116
  }
1122
1117
  }
1123
- function isNostrSenderAllowed(senderPubkey, allowFrom) {
1124
- const normalizedSender = normalizePubkey(senderPubkey);
1125
- for (const entry of allowFrom) {
1126
- const normalized = normalizeNostrAllowEntry(entry);
1127
- if (normalized === "*" || normalized === normalizedSender) return true;
1118
+ function normalizeNostrSenderPubkey(value) {
1119
+ try {
1120
+ return normalizePubkey(value);
1121
+ } catch {
1122
+ return null;
1128
1123
  }
1129
- return false;
1130
- }
1131
- async function resolveNostrDirectAccess(params) {
1132
- return resolveInboundDirectDmAccessWithRuntime({
1133
- cfg: params.cfg,
1134
- channel: "nostr",
1135
- accountId: params.accountId,
1136
- dmPolicy: params.dmPolicy,
1137
- allowFrom: params.allowFrom,
1138
- senderId: params.senderPubkey,
1139
- rawBody: params.rawBody,
1140
- isSenderAllowed: isNostrSenderAllowed,
1141
- runtime: params.runtime,
1142
- modeWhenAccessGroupsOff: "configured"
1143
- });
1144
1124
  }
1125
+ const nostrIngressIdentity = {
1126
+ key: "nostr-pubkey",
1127
+ normalizeEntry: normalizeNostrAllowEntry,
1128
+ normalizeSubject: normalizeNostrSenderPubkey,
1129
+ sensitivity: "pii",
1130
+ entryIdPrefix: "nostr-entry"
1131
+ };
1145
1132
  const startNostrGatewayAccount = async (ctx) => {
1146
1133
  const account = ctx.account;
1147
1134
  ctx.setStatus({
@@ -1156,38 +1143,42 @@ const startNostrGatewayAccount = async (ctx) => {
1156
1143
  channel: "nostr",
1157
1144
  accountId: account.accountId
1158
1145
  });
1159
- const resolveInboundAccess = async (senderPubkey, rawBody) => await resolveNostrDirectAccess({
1160
- cfg: ctx.cfg,
1146
+ const resolveInboundAccess = async (senderPubkey, rawBody) => await resolveStableChannelMessageIngress({
1147
+ channelId: "nostr",
1161
1148
  accountId: account.accountId,
1149
+ identity: nostrIngressIdentity,
1150
+ cfg: ctx.cfg,
1151
+ useDefaultPairingStore: true,
1152
+ subject: { stableId: senderPubkey },
1153
+ conversation: {
1154
+ kind: "direct",
1155
+ id: senderPubkey
1156
+ },
1162
1157
  dmPolicy: account.config.dmPolicy ?? "pairing",
1163
1158
  allowFrom: account.config.allowFrom,
1164
- senderPubkey,
1165
- rawBody,
1166
- runtime: {
1167
- shouldComputeCommandAuthorized: runtime.channel.commands.shouldComputeCommandAuthorized,
1168
- resolveCommandAuthorizedFromAuthorizers: runtime.channel.commands.resolveCommandAuthorizedFromAuthorizers
1169
- }
1159
+ command: runtime.channel.commands.shouldComputeCommandAuthorized(rawBody, ctx.cfg) ? { modeWhenAccessGroupsOff: "configured" } : void 0
1170
1160
  });
1171
1161
  let busHandle = null;
1172
- const authorizeSender = createPreCryptoDirectDmAuthorizer({
1173
- resolveAccess: async (senderPubkey) => await resolveInboundAccess(senderPubkey, ""),
1174
- issuePairingChallenge: async ({ senderId, reply }) => {
1162
+ const authorizeSender = async (input) => {
1163
+ const resolved = await resolveInboundAccess(input.senderId, "");
1164
+ if (resolved.senderAccess.decision === "allow") return "allow";
1165
+ if (resolved.senderAccess.decision === "pairing") {
1175
1166
  await pairing.issueChallenge({
1176
- senderId,
1177
- senderIdLine: `Your Nostr pubkey: ${senderId}`,
1178
- sendPairingReply: reply,
1167
+ senderId: input.senderId,
1168
+ senderIdLine: `Your Nostr pubkey: ${input.senderId}`,
1169
+ sendPairingReply: input.reply,
1179
1170
  onCreated: () => {
1180
- ctx.log?.debug?.(`[${account.accountId}] nostr pairing request sender=${senderId}`);
1171
+ ctx.log?.debug?.(`[${account.accountId}] nostr pairing request sender=${input.senderId}`);
1181
1172
  },
1182
1173
  onReplyError: (err) => {
1183
- ctx.log?.warn?.(`[${account.accountId}] nostr pairing reply failed for ${senderId}: ${String(err)}`);
1174
+ ctx.log?.warn?.(`[${account.accountId}] nostr pairing reply failed for ${input.senderId}: ${String(err)}`);
1184
1175
  }
1185
1176
  });
1186
- },
1187
- onBlocked: ({ senderId, reason }) => {
1188
- ctx.log?.debug?.(`[${account.accountId}] blocked Nostr sender ${senderId} (${reason})`);
1177
+ return "pairing";
1189
1178
  }
1190
- });
1179
+ ctx.log?.debug?.(`[${account.accountId}] blocked Nostr sender ${input.senderId} (${resolved.senderAccess.reasonCode})`);
1180
+ return "block";
1181
+ };
1191
1182
  const bus = await startNostrBus({
1192
1183
  accountId: account.accountId,
1193
1184
  privateKey: account.privateKey,
@@ -1198,8 +1189,8 @@ const startNostrGatewayAccount = async (ctx) => {
1198
1189
  }),
1199
1190
  onMessage: async (senderPubkey, text, reply, meta) => {
1200
1191
  const resolvedAccess = await resolveInboundAccess(senderPubkey, text);
1201
- if (resolvedAccess.access.decision !== "allow") {
1202
- ctx.log?.warn?.(`[${account.accountId}] dropping Nostr DM after preflight drift (${senderPubkey}, ${resolvedAccess.access.reason})`);
1192
+ if (resolvedAccess.senderAccess.decision !== "allow") {
1193
+ ctx.log?.warn?.(`[${account.accountId}] dropping Nostr DM after preflight drift (${senderPubkey}, ${resolvedAccess.senderAccess.reasonCode})`);
1203
1194
  return;
1204
1195
  }
1205
1196
  const { dispatchInboundDirectDmWithRuntime } = await import("./inbound-direct-dm-runtime-22bZWcIW.js");
@@ -1220,7 +1211,7 @@ const startNostrGatewayAccount = async (ctx) => {
1220
1211
  rawBody: text,
1221
1212
  messageId: meta.eventId,
1222
1213
  timestamp: meta.createdAt * 1e3,
1223
- commandAuthorized: resolvedAccess.commandAuthorized,
1214
+ commandAuthorized: resolvedAccess.commandAccess.requested ? resolvedAccess.commandAccess.authorized : void 0,
1224
1215
  deliver: async (payload) => {
1225
1216
  const outboundText = payload && typeof payload === "object" && "text" in payload ? payload.text ?? "" : "";
1226
1217
  if (!outboundText.trim()) return;
@@ -1287,6 +1278,10 @@ const nostrPairingTextAdapter = {
1287
1278
  const nostrOutboundAdapter = {
1288
1279
  deliveryMode: "direct",
1289
1280
  textChunkLimit: 4e3,
1281
+ deliveryCapabilities: { durableFinal: {
1282
+ text: true,
1283
+ messageSendingHooks: true
1284
+ } },
1290
1285
  sendText: async ({ cfg, to, text, accountId }) => {
1291
1286
  const core = getNostrRuntime();
1292
1287
  const aid = accountId ?? resolveDefaultNostrAccountId(cfg);
@@ -1370,6 +1365,10 @@ const nostrConfigAdapter = createTopLevelChannelConfigAdapter({
1370
1365
  }
1371
1366
  }).filter(Boolean)
1372
1367
  });
1368
+ const nostrMessageAdapter = createChannelMessageAdapterFromOutbound({
1369
+ id: "nostr",
1370
+ outbound: nostrOutboundAdapter
1371
+ });
1373
1372
  const nostrPlugin = createChatChannelPlugin({
1374
1373
  base: {
1375
1374
  id: "nostr",
@@ -1418,6 +1417,7 @@ const nostrPlugin = createChatChannelPlugin({
1418
1417
  },
1419
1418
  resolveOutboundSessionRoute: (params) => resolveNostrOutboundSessionRoute(params)
1420
1419
  },
1420
+ message: nostrMessageAdapter,
1421
1421
  status: { ...createComputedAccountStatusAdapter({
1422
1422
  defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
1423
1423
  collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("nostr", accounts),
@@ -1,2 +1,2 @@
1
- import { n as nostrPlugin } from "./channel-DfEqBtUh.js";
1
+ import { n as nostrPlugin } from "./channel-BSOktL3g.js";
2
2
  export { nostrPlugin };
@@ -1,6 +1,5 @@
1
1
  import { collectStatusIssuesFromLastError, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
2
2
  import { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, formatPairingApproveHint } from "openclaw/plugin-sdk/channel-plugin-common";
3
- import { createPreCryptoDirectDmAuthorizer, resolveInboundDirectDmAccessWithRuntime } from "openclaw/plugin-sdk/direct-dm-access";
4
3
  import { AllowFromListSchema, DmPolicySchema, MarkdownConfigSchema } from "openclaw/plugin-sdk/channel-config-primitives";
5
4
  import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";
6
5
  import { z } from "openclaw/plugin-sdk/zod";
@@ -61,4 +60,4 @@ const NostrConfigSchema = z.object({
61
60
  profile: NostrProfileSchema.optional()
62
61
  });
63
62
  //#endregion
64
- export { collectStatusIssuesFromLastError as a, formatPairingApproveHint as c, buildChannelConfigSchema as i, resolveInboundDirectDmAccessWithRuntime as l, NostrProfileSchema as n, createDefaultChannelRuntimeState as o, DEFAULT_ACCOUNT_ID as r, createPreCryptoDirectDmAuthorizer as s, NostrConfigSchema as t };
63
+ export { collectStatusIssuesFromLastError as a, buildChannelConfigSchema as i, NostrProfileSchema as n, createDefaultChannelRuntimeState as o, DEFAULT_ACCOUNT_ID as r, formatPairingApproveHint as s, NostrConfigSchema as t };
@@ -1,4 +1,4 @@
1
- import { i as buildChannelConfigSchema, t as NostrConfigSchema } from "./config-schema-DIk4jlBg.js";
1
+ import { i as buildChannelConfigSchema, t as NostrConfigSchema } from "./config-schema-Clq-vlF1.js";
2
2
  import { t as DEFAULT_RELAYS } from "./default-relays-DLwdWOTu.js";
3
3
  import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
4
4
  import { patchTopLevelChannelConfigSection } from "openclaw/plugin-sdk/setup";
package/dist/test-api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as nostrPlugin } from "./channel-DfEqBtUh.js";
1
+ import { n as nostrPlugin } from "./channel-BSOktL3g.js";
2
2
  export { nostrPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nostr",
3
- "version": "2026.5.7",
3
+ "version": "2026.5.10-beta.1",
4
4
  "description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,15 +8,14 @@
8
8
  },
9
9
  "type": "module",
10
10
  "dependencies": {
11
- "nostr-tools": "^2.23.3",
12
- "zod": "^4.4.3"
11
+ "nostr-tools": "^2.23.3"
13
12
  },
14
13
  "devDependencies": {
15
14
  "@openclaw/plugin-sdk": "workspace:*",
16
15
  "openclaw": "workspace:*"
17
16
  },
18
17
  "peerDependencies": {
19
- "openclaw": ">=2026.5.7"
18
+ "openclaw": ">=2026.5.10-beta.1"
20
19
  },
21
20
  "peerDependenciesMeta": {
22
21
  "openclaw": {
@@ -54,10 +53,10 @@
54
53
  "minHostVersion": ">=2026.4.10"
55
54
  },
56
55
  "compat": {
57
- "pluginApi": ">=2026.5.7"
56
+ "pluginApi": ">=2026.5.10-beta.1"
58
57
  },
59
58
  "build": {
60
- "openclawVersion": "2026.5.7"
59
+ "openclawVersion": "2026.5.10-beta.1"
61
60
  },
62
61
  "release": {
63
62
  "publishToClawHub": true,