@openclaw/nostr 2026.7.2-beta.7 → 2026.7.33

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.
Files changed (159) hide show
  1. package/dist/{channel-O6-jXoT5.js → .setup/channel-B4SX7Iq6.mjs} +554 -877
  2. package/dist/{config-schema-CelPzg1N.js → .setup/config-schema-DIiXiBKr.mjs} +1 -1
  3. package/dist/.setup/inbound-direct-dm-runtime-EdLLVtkh.mjs +2 -0
  4. package/dist/{setup-adapter-BjKmSwdW.js → .setup/setup-adapter-DEU3o8MF.mjs} +1 -31
  5. package/dist/.setup/setup-plugin-api-DzlCumkz.mjs +2 -0
  6. package/dist/.setup/setup-plugin-api-csfJrRxD.mjs +111 -0
  7. package/dist/{setup-surface-BJCFpe02.js → .setup/setup-surface-DWa6-9ku.mjs} +70 -41
  8. package/dist/api.js +72 -61
  9. package/dist/channel-plugin-api.js +1 -1
  10. package/dist/doctor-contract-api.js +3 -2
  11. package/dist/setup-api.js +1 -1
  12. package/dist/setup-entry.js +2 -1
  13. package/dist/setup-plugin-api.js +1 -112
  14. package/node_modules/nostr-tools/lib/cjs/abstract-pool.js +28 -59
  15. package/node_modules/nostr-tools/lib/cjs/abstract-pool.js.map +3 -3
  16. package/node_modules/nostr-tools/lib/cjs/abstract-relay.js +7 -44
  17. package/node_modules/nostr-tools/lib/cjs/abstract-relay.js.map +2 -2
  18. package/node_modules/nostr-tools/lib/cjs/core.js +1 -17
  19. package/node_modules/nostr-tools/lib/cjs/core.js.map +3 -3
  20. package/node_modules/nostr-tools/lib/cjs/filter.js.map +1 -1
  21. package/node_modules/nostr-tools/lib/cjs/index.js +130 -172
  22. package/node_modules/nostr-tools/lib/cjs/index.js.map +4 -4
  23. package/node_modules/nostr-tools/lib/cjs/kinds.js +1 -17
  24. package/node_modules/nostr-tools/lib/cjs/kinds.js.map +3 -3
  25. package/node_modules/nostr-tools/lib/cjs/nip04.js.map +1 -1
  26. package/node_modules/nostr-tools/lib/cjs/nip10.js +5 -20
  27. package/node_modules/nostr-tools/lib/cjs/nip10.js.map +3 -3
  28. package/node_modules/nostr-tools/lib/cjs/nip13.js.map +1 -1
  29. package/node_modules/nostr-tools/lib/cjs/nip17.js +23 -30
  30. package/node_modules/nostr-tools/lib/cjs/nip17.js.map +4 -4
  31. package/node_modules/nostr-tools/lib/cjs/nip18.js +14 -21
  32. package/node_modules/nostr-tools/lib/cjs/nip18.js.map +4 -4
  33. package/node_modules/nostr-tools/lib/cjs/nip19.js.map +1 -1
  34. package/node_modules/nostr-tools/lib/cjs/nip21.js.map +1 -1
  35. package/node_modules/nostr-tools/lib/cjs/nip22.js +7 -22
  36. package/node_modules/nostr-tools/lib/cjs/nip22.js.map +3 -3
  37. package/node_modules/nostr-tools/lib/cjs/nip25.js +14 -21
  38. package/node_modules/nostr-tools/lib/cjs/nip25.js.map +4 -4
  39. package/node_modules/nostr-tools/lib/cjs/nip27.js.map +1 -1
  40. package/node_modules/nostr-tools/lib/cjs/nip28.js +14 -21
  41. package/node_modules/nostr-tools/lib/cjs/nip28.js.map +4 -4
  42. package/node_modules/nostr-tools/lib/cjs/nip29.js.map +1 -1
  43. package/node_modules/nostr-tools/lib/cjs/nip30.js.map +2 -2
  44. package/node_modules/nostr-tools/lib/cjs/nip42.js.map +2 -2
  45. package/node_modules/nostr-tools/lib/cjs/nip44.js.map +1 -1
  46. package/node_modules/nostr-tools/lib/cjs/nip45.js +9 -22
  47. package/node_modules/nostr-tools/lib/cjs/nip45.js.map +4 -4
  48. package/node_modules/nostr-tools/lib/cjs/nip46.js +70 -111
  49. package/node_modules/nostr-tools/lib/cjs/nip46.js.map +4 -4
  50. package/node_modules/nostr-tools/lib/cjs/nip47.js +18 -25
  51. package/node_modules/nostr-tools/lib/cjs/nip47.js.map +4 -4
  52. package/node_modules/nostr-tools/lib/cjs/nip57.js +16 -23
  53. package/node_modules/nostr-tools/lib/cjs/nip57.js.map +4 -4
  54. package/node_modules/nostr-tools/lib/cjs/nip58.js.map +2 -2
  55. package/node_modules/nostr-tools/lib/cjs/nip59.js +7 -16
  56. package/node_modules/nostr-tools/lib/cjs/nip59.js.map +3 -3
  57. package/node_modules/nostr-tools/lib/cjs/nip75.js.map +1 -1
  58. package/node_modules/nostr-tools/lib/cjs/nip94.js.map +1 -1
  59. package/node_modules/nostr-tools/lib/cjs/nip98.js +16 -23
  60. package/node_modules/nostr-tools/lib/cjs/nip98.js.map +4 -4
  61. package/node_modules/nostr-tools/lib/cjs/nip99.js.map +2 -2
  62. package/node_modules/nostr-tools/lib/cjs/nipb0.js +17 -38
  63. package/node_modules/nostr-tools/lib/cjs/nipb0.js.map +4 -4
  64. package/node_modules/nostr-tools/lib/cjs/nipb7.js +7 -13
  65. package/node_modules/nostr-tools/lib/cjs/nipb7.js.map +2 -2
  66. package/node_modules/nostr-tools/lib/cjs/pool.js +61 -102
  67. package/node_modules/nostr-tools/lib/cjs/pool.js.map +4 -4
  68. package/node_modules/nostr-tools/lib/cjs/pure.js +14 -21
  69. package/node_modules/nostr-tools/lib/cjs/pure.js.map +4 -4
  70. package/node_modules/nostr-tools/lib/cjs/references.js.map +1 -1
  71. package/node_modules/nostr-tools/lib/cjs/relay.js +42 -86
  72. package/node_modules/nostr-tools/lib/cjs/relay.js.map +4 -4
  73. package/node_modules/nostr-tools/lib/cjs/signer.js +14 -21
  74. package/node_modules/nostr-tools/lib/cjs/signer.js.map +4 -4
  75. package/node_modules/nostr-tools/lib/cjs/utils.js +0 -10
  76. package/node_modules/nostr-tools/lib/cjs/utils.js.map +2 -2
  77. package/node_modules/nostr-tools/lib/cjs/wasm.js +2 -16
  78. package/node_modules/nostr-tools/lib/cjs/wasm.js.map +4 -4
  79. package/node_modules/nostr-tools/lib/esm/abstract-pool.js +28 -59
  80. package/node_modules/nostr-tools/lib/esm/abstract-pool.js.map +3 -3
  81. package/node_modules/nostr-tools/lib/esm/abstract-relay.js +7 -44
  82. package/node_modules/nostr-tools/lib/esm/abstract-relay.js.map +2 -2
  83. package/node_modules/nostr-tools/lib/esm/core.js +1 -15
  84. package/node_modules/nostr-tools/lib/esm/core.js.map +3 -3
  85. package/node_modules/nostr-tools/lib/esm/filter.js.map +1 -1
  86. package/node_modules/nostr-tools/lib/esm/index.js +80 -122
  87. package/node_modules/nostr-tools/lib/esm/index.js.map +4 -4
  88. package/node_modules/nostr-tools/lib/esm/kinds.js +1 -17
  89. package/node_modules/nostr-tools/lib/esm/kinds.js.map +3 -3
  90. package/node_modules/nostr-tools/lib/esm/nip04.js.map +1 -1
  91. package/node_modules/nostr-tools/lib/esm/nip10.js +5 -18
  92. package/node_modules/nostr-tools/lib/esm/nip10.js.map +3 -3
  93. package/node_modules/nostr-tools/lib/esm/nip13.js.map +1 -1
  94. package/node_modules/nostr-tools/lib/esm/nip17.js +8 -15
  95. package/node_modules/nostr-tools/lib/esm/nip17.js.map +4 -4
  96. package/node_modules/nostr-tools/lib/esm/nip18.js +8 -15
  97. package/node_modules/nostr-tools/lib/esm/nip18.js.map +4 -4
  98. package/node_modules/nostr-tools/lib/esm/nip19.js.map +1 -1
  99. package/node_modules/nostr-tools/lib/esm/nip21.js.map +1 -1
  100. package/node_modules/nostr-tools/lib/esm/nip22.js +7 -20
  101. package/node_modules/nostr-tools/lib/esm/nip22.js.map +3 -3
  102. package/node_modules/nostr-tools/lib/esm/nip25.js +8 -15
  103. package/node_modules/nostr-tools/lib/esm/nip25.js.map +4 -4
  104. package/node_modules/nostr-tools/lib/esm/nip27.js.map +1 -1
  105. package/node_modules/nostr-tools/lib/esm/nip28.js +8 -15
  106. package/node_modules/nostr-tools/lib/esm/nip28.js.map +4 -4
  107. package/node_modules/nostr-tools/lib/esm/nip29.js.map +1 -1
  108. package/node_modules/nostr-tools/lib/esm/nip30.js.map +2 -2
  109. package/node_modules/nostr-tools/lib/esm/nip42.js.map +2 -2
  110. package/node_modules/nostr-tools/lib/esm/nip44.js.map +1 -1
  111. package/node_modules/nostr-tools/lib/esm/nip45.js +10 -23
  112. package/node_modules/nostr-tools/lib/esm/nip45.js.map +4 -4
  113. package/node_modules/nostr-tools/lib/esm/nip46.js +54 -95
  114. package/node_modules/nostr-tools/lib/esm/nip46.js.map +4 -4
  115. package/node_modules/nostr-tools/lib/esm/nip47.js +8 -15
  116. package/node_modules/nostr-tools/lib/esm/nip47.js.map +4 -4
  117. package/node_modules/nostr-tools/lib/esm/nip57.js +10 -17
  118. package/node_modules/nostr-tools/lib/esm/nip57.js.map +4 -4
  119. package/node_modules/nostr-tools/lib/esm/nip58.js.map +2 -2
  120. package/node_modules/nostr-tools/lib/esm/nip59.js +1 -10
  121. package/node_modules/nostr-tools/lib/esm/nip59.js.map +3 -3
  122. package/node_modules/nostr-tools/lib/esm/nip75.js.map +1 -1
  123. package/node_modules/nostr-tools/lib/esm/nip94.js.map +1 -1
  124. package/node_modules/nostr-tools/lib/esm/nip98.js +8 -15
  125. package/node_modules/nostr-tools/lib/esm/nip98.js.map +4 -4
  126. package/node_modules/nostr-tools/lib/esm/nip99.js.map +2 -2
  127. package/node_modules/nostr-tools/lib/esm/nipb0.js +18 -39
  128. package/node_modules/nostr-tools/lib/esm/nipb0.js.map +4 -4
  129. package/node_modules/nostr-tools/lib/esm/nipb7.js +7 -13
  130. package/node_modules/nostr-tools/lib/esm/nipb7.js.map +2 -2
  131. package/node_modules/nostr-tools/lib/esm/pool.js +54 -95
  132. package/node_modules/nostr-tools/lib/esm/pool.js.map +4 -4
  133. package/node_modules/nostr-tools/lib/esm/pure.js +8 -15
  134. package/node_modules/nostr-tools/lib/esm/pure.js.map +4 -4
  135. package/node_modules/nostr-tools/lib/esm/references.js.map +1 -1
  136. package/node_modules/nostr-tools/lib/esm/relay.js +36 -80
  137. package/node_modules/nostr-tools/lib/esm/relay.js.map +4 -4
  138. package/node_modules/nostr-tools/lib/esm/signer.js +8 -15
  139. package/node_modules/nostr-tools/lib/esm/signer.js.map +4 -4
  140. package/node_modules/nostr-tools/lib/esm/utils.js +0 -10
  141. package/node_modules/nostr-tools/lib/esm/utils.js.map +2 -2
  142. package/node_modules/nostr-tools/lib/esm/wasm.js +3 -17
  143. package/node_modules/nostr-tools/lib/esm/wasm.js.map +4 -4
  144. package/node_modules/nostr-tools/lib/nostr.bundle.js +77 -121
  145. package/node_modules/nostr-tools/lib/nostr.bundle.js.map +4 -4
  146. package/node_modules/nostr-tools/lib/types/abstract-pool.d.ts +4 -7
  147. package/node_modules/nostr-tools/lib/types/abstract-relay.d.ts +2 -7
  148. package/node_modules/nostr-tools/lib/types/helpers.d.ts +2 -0
  149. package/node_modules/nostr-tools/lib/types/kinds.d.ts +0 -2
  150. package/node_modules/nostr-tools/lib/types/nipb0.d.ts +1 -0
  151. package/node_modules/nostr-tools/lib/types/nipb7.d.ts +1 -0
  152. package/node_modules/nostr-tools/lib/types/relay.d.ts +2 -2
  153. package/node_modules/nostr-tools/lib/types/utils.d.ts +0 -4
  154. package/node_modules/nostr-tools/package.json +1 -1
  155. package/openclaw.plugin.json +5 -0
  156. package/package.json +15 -41
  157. package/dist/inbound-direct-dm-runtime-DHn9NoYN.js +0 -2
  158. package/node_modules/nostr-tools/lib/types/nipb0.test.d.ts +0 -1
  159. /package/dist/{state-account-id-CvBZ9s6P.js → .setup/state-account-id-CvBZ9s6P.mjs} +0 -0
@@ -1,82 +1,60 @@
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";
4
- import { t as normalizeNostrStateAccountId } from "./state-account-id-CvBZ9s6P.js";
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-DWa6-9ku.mjs";
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-DIiXiBKr.mjs";
3
+ import { i as DEFAULT_RELAYS } from "./setup-adapter-DEU3o8MF.mjs";
4
+ import { t as normalizeNostrStateAccountId } from "./state-account-id-CvBZ9s6P.mjs";
5
5
  import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
6
6
  import { createScopedDmSecurityResolver, createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
7
7
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
8
- import { missingTargetError } from "openclaw/plugin-sdk/channel-feedback";
9
- import { DEFAULT_INGRESS_ADOPTION_STALL_MS, bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, createChannelMessageAdapterFromOutbound, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
10
- import { buildPassiveChannelStatusSummary, buildTrafficStatusSummary } from "openclaw/plugin-sdk/extension-shared";
8
+ import { createChannelMessageAdapterFromOutbound } from "openclaw/plugin-sdk/channel-outbound";
9
+ import { buildPassiveChannelStatusSummary, buildTrafficStatusSummary, runStoppablePassiveMonitor } from "openclaw/plugin-sdk/extension-shared";
11
10
  import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
12
11
  import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
13
12
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
14
13
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
15
14
  import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
16
- import { chunkTextForOutbound, sanitizeAssistantVisibleText, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
17
15
  import { SimplePool, finalizeEvent, getPublicKey, verifyEvent } from "nostr-tools";
18
16
  import { decrypt, encrypt } from "nostr-tools/nip04";
19
17
  import { createDirectDmPreCryptoGuardPolicy } from "openclaw/plugin-sdk/direct-dm-guard-policy";
20
18
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
21
19
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
20
+ import { resolveIntegerOption, resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
22
21
  import { buildChannelOutboundSessionRoute, stripChannelTargetPrefix } from "openclaw/plugin-sdk/core";
23
22
  //#region extensions/nostr/src/metrics.ts
24
- function createZeroMetricsState() {
25
- return {
26
- eventsReceived: 0,
27
- eventsProcessed: 0,
28
- eventsDuplicate: 0,
29
- eventsRejected: {
30
- invalidShape: 0,
31
- wrongKind: 0,
32
- stale: 0,
33
- future: 0,
34
- rateLimited: 0,
35
- invalidSignature: 0,
36
- oversizedCiphertext: 0,
37
- oversizedPlaintext: 0,
38
- decryptFailed: 0,
39
- selfMessage: 0
40
- },
41
- relays: /* @__PURE__ */ new Map(),
42
- rateLimiting: {
43
- perSenderHits: 0,
44
- globalHits: 0
45
- },
46
- decrypt: {
47
- success: 0,
48
- failure: 0
49
- },
50
- memory: {
51
- seenTrackerSize: 0,
52
- rateLimiterEntries: 0
53
- }
54
- };
55
- }
56
- function createMetricsSnapshot(state, snapshotAt) {
57
- const relays = {};
58
- for (const [url, stats] of state.relays) relays[url] = {
59
- ...stats,
60
- messagesReceived: { ...stats.messagesReceived }
61
- };
62
- return {
63
- ...state,
64
- eventsRejected: { ...state.eventsRejected },
65
- relays,
66
- rateLimiting: { ...state.rateLimiting },
67
- decrypt: { ...state.decrypt },
68
- memory: { ...state.memory },
69
- snapshotAt: snapshotAt ?? Date.now()
70
- };
71
- }
72
23
  /**
73
24
  * Create a metrics collector instance.
74
25
  * Optionally pass an onMetric callback to receive real-time metric events.
75
26
  */
76
27
  function createMetrics(onMetric) {
77
- let state = createZeroMetricsState();
28
+ let eventsReceived = 0;
29
+ let eventsProcessed = 0;
30
+ let eventsDuplicate = 0;
31
+ const eventsRejected = {
32
+ invalidShape: 0,
33
+ wrongKind: 0,
34
+ stale: 0,
35
+ future: 0,
36
+ rateLimited: 0,
37
+ invalidSignature: 0,
38
+ oversizedCiphertext: 0,
39
+ oversizedPlaintext: 0,
40
+ decryptFailed: 0,
41
+ selfMessage: 0
42
+ };
43
+ const relays = /* @__PURE__ */ new Map();
44
+ const rateLimiting = {
45
+ perSenderHits: 0,
46
+ globalHits: 0
47
+ };
48
+ const decrypt = {
49
+ success: 0,
50
+ failure: 0
51
+ };
52
+ const memory = {
53
+ seenTrackerSize: 0,
54
+ rateLimiterEntries: 0
55
+ };
78
56
  function getOrCreateRelay(url) {
79
- let relay = state.relays.get(url);
57
+ let relay = relays.get(url);
80
58
  if (!relay) {
81
59
  relay = {
82
60
  connects: 0,
@@ -95,7 +73,7 @@ function createMetrics(onMetric) {
95
73
  circuitBreakerOpens: 0,
96
74
  circuitBreakerCloses: 0
97
75
  };
98
- state.relays.set(url, relay);
76
+ relays.set(url, relay);
99
77
  }
100
78
  return relay;
101
79
  }
@@ -109,43 +87,43 @@ function createMetrics(onMetric) {
109
87
  const relayUrl = labels?.relay;
110
88
  switch (name) {
111
89
  case "event.received":
112
- state.eventsReceived += value;
90
+ eventsReceived += value;
113
91
  break;
114
92
  case "event.processed":
115
- state.eventsProcessed += value;
93
+ eventsProcessed += value;
116
94
  break;
117
95
  case "event.duplicate":
118
- state.eventsDuplicate += value;
96
+ eventsDuplicate += value;
119
97
  break;
120
98
  case "event.rejected.invalid_shape":
121
- state.eventsRejected.invalidShape += value;
99
+ eventsRejected.invalidShape += value;
122
100
  break;
123
101
  case "event.rejected.wrong_kind":
124
- state.eventsRejected.wrongKind += value;
102
+ eventsRejected.wrongKind += value;
125
103
  break;
126
104
  case "event.rejected.stale":
127
- state.eventsRejected.stale += value;
105
+ eventsRejected.stale += value;
128
106
  break;
129
107
  case "event.rejected.future":
130
- state.eventsRejected.future += value;
108
+ eventsRejected.future += value;
131
109
  break;
132
110
  case "event.rejected.rate_limited":
133
- state.eventsRejected.rateLimited += value;
111
+ eventsRejected.rateLimited += value;
134
112
  break;
135
113
  case "event.rejected.invalid_signature":
136
- state.eventsRejected.invalidSignature += value;
114
+ eventsRejected.invalidSignature += value;
137
115
  break;
138
116
  case "event.rejected.oversized_ciphertext":
139
- state.eventsRejected.oversizedCiphertext += value;
117
+ eventsRejected.oversizedCiphertext += value;
140
118
  break;
141
119
  case "event.rejected.oversized_plaintext":
142
- state.eventsRejected.oversizedPlaintext += value;
120
+ eventsRejected.oversizedPlaintext += value;
143
121
  break;
144
122
  case "event.rejected.decrypt_failed":
145
- state.eventsRejected.decryptFailed += value;
123
+ eventsRejected.decryptFailed += value;
146
124
  break;
147
125
  case "event.rejected.self_message":
148
- state.eventsRejected.selfMessage += value;
126
+ eventsRejected.selfMessage += value;
149
127
  break;
150
128
  case "relay.connect":
151
129
  if (relayUrl) getOrCreateRelay(relayUrl).connects += value;
@@ -195,30 +173,64 @@ function createMetrics(onMetric) {
195
173
  if (relayUrl) getOrCreateRelay(relayUrl).circuitBreakerState = "half_open";
196
174
  break;
197
175
  case "rate_limit.per_sender":
198
- state.rateLimiting.perSenderHits += value;
176
+ rateLimiting.perSenderHits += value;
199
177
  break;
200
178
  case "rate_limit.global":
201
- state.rateLimiting.globalHits += value;
179
+ rateLimiting.globalHits += value;
202
180
  break;
203
181
  case "decrypt.success":
204
- state.decrypt.success += value;
182
+ decrypt.success += value;
205
183
  break;
206
184
  case "decrypt.failure":
207
- state.decrypt.failure += value;
185
+ decrypt.failure += value;
208
186
  break;
209
187
  case "memory.seen_tracker_size":
210
- state.memory.seenTrackerSize = value;
211
- break;
212
- case "memory.rate_limiter_entries":
213
- state.memory.rateLimiterEntries = value;
188
+ memory.seenTrackerSize = value;
214
189
  break;
190
+ case "memory.rate_limiter_entries": memory.rateLimiterEntries = value;
215
191
  }
216
192
  }
217
193
  function getSnapshot() {
218
- return createMetricsSnapshot(state);
194
+ const relaysObj = {};
195
+ for (const [url, stats] of relays) relaysObj[url] = {
196
+ ...stats,
197
+ messagesReceived: { ...stats.messagesReceived }
198
+ };
199
+ return {
200
+ eventsReceived,
201
+ eventsProcessed,
202
+ eventsDuplicate,
203
+ eventsRejected: { ...eventsRejected },
204
+ relays: relaysObj,
205
+ rateLimiting: { ...rateLimiting },
206
+ decrypt: { ...decrypt },
207
+ memory: { ...memory },
208
+ snapshotAt: Date.now()
209
+ };
219
210
  }
220
211
  function reset() {
221
- state = createZeroMetricsState();
212
+ eventsReceived = 0;
213
+ eventsProcessed = 0;
214
+ eventsDuplicate = 0;
215
+ Object.assign(eventsRejected, {
216
+ invalidShape: 0,
217
+ wrongKind: 0,
218
+ stale: 0,
219
+ future: 0,
220
+ rateLimited: 0,
221
+ invalidSignature: 0,
222
+ oversizedCiphertext: 0,
223
+ oversizedPlaintext: 0,
224
+ decryptFailed: 0,
225
+ selfMessage: 0
226
+ });
227
+ relays.clear();
228
+ rateLimiting.perSenderHits = 0;
229
+ rateLimiting.globalHits = 0;
230
+ decrypt.success = 0;
231
+ decrypt.failure = 0;
232
+ memory.seenTrackerSize = 0;
233
+ memory.rateLimiterEntries = 0;
222
234
  }
223
235
  return {
224
236
  emit,
@@ -230,7 +242,37 @@ function createMetrics(onMetric) {
230
242
  * Create a no-op metrics instance (for when metrics are disabled).
231
243
  */
232
244
  function createNoopMetrics() {
233
- const emptySnapshot = createMetricsSnapshot(createZeroMetricsState(), 0);
245
+ const emptySnapshot = {
246
+ eventsReceived: 0,
247
+ eventsProcessed: 0,
248
+ eventsDuplicate: 0,
249
+ eventsRejected: {
250
+ invalidShape: 0,
251
+ wrongKind: 0,
252
+ stale: 0,
253
+ future: 0,
254
+ rateLimited: 0,
255
+ invalidSignature: 0,
256
+ oversizedCiphertext: 0,
257
+ oversizedPlaintext: 0,
258
+ decryptFailed: 0,
259
+ selfMessage: 0
260
+ },
261
+ relays: {},
262
+ rateLimiting: {
263
+ perSenderHits: 0,
264
+ globalHits: 0
265
+ },
266
+ decrypt: {
267
+ success: 0,
268
+ failure: 0
269
+ },
270
+ memory: {
271
+ seenTrackerSize: 0,
272
+ rateLimiterEntries: 0
273
+ },
274
+ snapshotAt: 0
275
+ };
234
276
  return {
235
277
  emit: () => {},
236
278
  getSnapshot: () => ({
@@ -241,382 +283,6 @@ function createNoopMetrics() {
241
283
  };
242
284
  }
243
285
  //#endregion
244
- //#region extensions/nostr/src/nostr-cursor.ts
245
- const CURSOR_WRITE_RETRY_MS = [
246
- 0,
247
- 100,
248
- 300
249
- ];
250
- const CURSOR_RECOVERY_RETRY_MS = 1e3;
251
- /** Tracks the largest EOSE-safe Nostr timestamp without skipping undurable relay events. */
252
- function createNostrDurableCursor(options) {
253
- const nowSec = options.nowSec ?? (() => Math.floor(Date.now() / 1e3));
254
- let durableCandidate;
255
- let transientReplayCeiling;
256
- let backfillComplete = false;
257
- const safeCandidate = () => {
258
- if (durableCandidate === void 0) return;
259
- return Math.min(durableCandidate, transientReplayCeiling ?? Number.MAX_SAFE_INTEGER);
260
- };
261
- return {
262
- recordDurableAppend: (event) => {
263
- if (!Number.isSafeInteger(event.created_at)) return;
264
- const previousSafeCandidate = safeCandidate();
265
- durableCandidate = Math.max(durableCandidate ?? 0, Math.min(event.created_at, nowSec()));
266
- const nextSafeCandidate = safeCandidate();
267
- return backfillComplete && nextSafeCandidate !== previousSafeCandidate ? nextSafeCandidate : void 0;
268
- },
269
- recordTransientRejection: (event) => {
270
- if (!Number.isSafeInteger(event.created_at) || event.created_at < options.since) return;
271
- const previousSafeCandidate = safeCandidate();
272
- transientReplayCeiling = Math.min(transientReplayCeiling ?? Number.MAX_SAFE_INTEGER, event.created_at + options.replayOverlapSec);
273
- const nextSafeCandidate = safeCandidate();
274
- return backfillComplete && nextSafeCandidate !== previousSafeCandidate ? nextSafeCandidate : void 0;
275
- },
276
- markBackfillComplete: () => {
277
- backfillComplete = true;
278
- return safeCandidate();
279
- }
280
- };
281
- }
282
- /** Serializes cursor writes so a safety rewind always lands after older progress writes. */
283
- function createNostrCursorStateWriter(options) {
284
- let desiredCursor = Math.max(options.minimumCursor, options.initialCursor);
285
- let dirty = false;
286
- let timer;
287
- let writeTail = Promise.resolve();
288
- let activeFlush;
289
- let recoveryFlush;
290
- 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
- });
296
- try {
297
- await options.write(cursor);
298
- return;
299
- } catch (error) {
300
- lastError = error;
301
- }
302
- }
303
- throw new Error("Nostr cursor state write failed.", { cause: lastError });
304
- };
305
- const enqueueWrite = (cursor) => {
306
- const write = writeTail.then(() => writeWithRetry(cursor));
307
- writeTail = write.catch(() => void 0);
308
- return write;
309
- };
310
- const clearTimer = () => {
311
- if (timer) {
312
- clearTimeout(timer);
313
- timer = void 0;
314
- }
315
- };
316
- const runFlush = async () => {
317
- for (;;) {
318
- if (!dirty) {
319
- await writeTail;
320
- if (!dirty) return;
321
- }
322
- dirty = false;
323
- const cursor = desiredCursor;
324
- try {
325
- await enqueueWrite(cursor);
326
- } catch (error) {
327
- dirty = true;
328
- throw error;
329
- }
330
- }
331
- };
332
- const flush = () => {
333
- clearTimer();
334
- if (activeFlush) return activeFlush;
335
- const tracked = runFlush().finally(() => {
336
- if (activeFlush === tracked) activeFlush = void 0;
337
- });
338
- activeFlush = tracked;
339
- return tracked;
340
- };
341
- const setDesiredCursor = (cursor) => {
342
- desiredCursor = Math.max(options.minimumCursor, cursor);
343
- dirty = true;
344
- };
345
- return {
346
- schedule: (cursor) => {
347
- setDesiredCursor(cursor);
348
- clearTimer();
349
- timer = setTimeout(() => {
350
- timer = void 0;
351
- flush().catch((error) => options.onBackgroundError?.(error));
352
- }, options.debounceMs);
353
- timer.unref?.();
354
- },
355
- persistNow: async (cursor) => {
356
- setDesiredCursor(cursor);
357
- await flush();
358
- },
359
- flush,
360
- flushUntilSuccess: () => {
361
- recoveryFlush ??= (async () => {
362
- for (;;) try {
363
- await flush();
364
- return;
365
- } catch (error) {
366
- options.onBackgroundError?.(error);
367
- await new Promise((resolve) => {
368
- setTimeout(resolve, options.recoveryRetryMs ?? CURSOR_RECOVERY_RETRY_MS);
369
- });
370
- }
371
- })().finally(() => {
372
- recoveryFlush = void 0;
373
- });
374
- return recoveryFlush;
375
- }
376
- };
377
- }
378
- var NostrIngressPermanentError = class extends Error {
379
- constructor(reason, message, options) {
380
- super(message, options);
381
- this.reason = reason;
382
- this.name = "NostrIngressPermanentError";
383
- }
384
- };
385
- function isNostrIngressRecord(value) {
386
- return typeof value === "object" && value !== null && !Array.isArray(value);
387
- }
388
- function requiredString(value, field) {
389
- if (typeof value === "string" && value.trim()) return value;
390
- throw new NostrIngressPermanentError("invalid-event", `Nostr event is missing ${field}.`);
391
- }
392
- function inspectNostrIngressEvent(event) {
393
- if (!isNostrIngressRecord(event)) throw new NostrIngressPermanentError("invalid-event", "Nostr event must be an object.");
394
- return {
395
- eventId: requiredString(event.id, "id"),
396
- laneKey: `direct:${requiredString(event.pubkey, "pubkey")}`
397
- };
398
- }
399
- /** Convert the retired persisted LRU seed into durable completion tombstones. */
400
- async function migrateNostrLegacyRecentEventIds(params) {
401
- const migratedAt = params.migratedAt ?? Date.now();
402
- let migrated = 0;
403
- for (const eventId of new Set(params.eventIds)) {
404
- if (!eventId.trim()) continue;
405
- const result = await params.queue.enqueue(eventId, {
406
- version: 1,
407
- receivedAt: migratedAt,
408
- rawEvent: ""
409
- }, {
410
- receivedAt: migratedAt,
411
- laneKey: `legacy:${eventId}`
412
- });
413
- if (result.kind === "accepted" || result.kind === "pending" && result.record.payload.rawEvent === "") {
414
- if (!await params.queue.complete(eventId, { completedAt: migratedAt })) throw new Error(`Failed to migrate Nostr replay event ${eventId}.`);
415
- }
416
- migrated += 1;
417
- }
418
- return migrated;
419
- }
420
- //#endregion
421
- //#region extensions/nostr/src/runtime.ts
422
- const { setRuntime: setNostrRuntime, getRuntime: getNostrRuntime } = createPluginRuntimeStore({
423
- pluginId: "nostr",
424
- errorMessage: "Nostr runtime not initialized"
425
- });
426
- //#endregion
427
- //#region extensions/nostr/src/nostr-ingress.ts
428
- const NOSTR_INGRESS_POLL_INTERVAL_MS = 500;
429
- const NOSTR_INGRESS_APPEND_RETRY_MS = [
430
- 0,
431
- 100,
432
- 300
433
- ];
434
- const NostrIngressAdmissionRejectedError = createChannelIngressError("NostrIngressAdmissionRejectedError", { withReason: true });
435
- function deserializeNostrIngressEvent(rawEvent, claimedId) {
436
- let parsed;
437
- try {
438
- parsed = JSON.parse(rawEvent);
439
- } catch (error) {
440
- throw new NostrIngressPermanentError("invalid-event", `Nostr ingress row ${claimedId} contains invalid JSON.`, { cause: error });
441
- }
442
- if (!isNostrIngressRecord(parsed)) throw new NostrIngressPermanentError("invalid-event", `Nostr ingress row ${claimedId} has an invalid event shape.`);
443
- if (typeof parsed.kind !== "number" || typeof parsed.created_at !== "number" || typeof parsed.content !== "string" || typeof parsed.sig !== "string" || !Array.isArray(parsed.tags)) throw new NostrIngressPermanentError("invalid-event", `Nostr ingress row ${claimedId} has an invalid event shape.`);
444
- return parsed;
445
- }
446
- function createNostrIngress(options) {
447
- let queue = options.queue;
448
- let admissionFailure;
449
- let admissionWindowStartedAt = Date.now();
450
- let admissionWindowCount = 0;
451
- let queuedAdmissions = 0;
452
- let stopping = false;
453
- let stopTask;
454
- const createStoppedError = () => /* @__PURE__ */ new Error("Nostr ingress stopped");
455
- const getQueue = () => {
456
- queue ??= getNostrRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
457
- return queue;
458
- };
459
- const monitor = createChannelIngressMonitor({
460
- queue: getQueue,
461
- inspect: (event) => {
462
- const facts = inspectNostrIngressEvent(event);
463
- return {
464
- eventId: facts.eventId,
465
- laneKey: facts.laneKey
466
- };
467
- },
468
- payload: {
469
- version: 1,
470
- serialize: (event, { receivedAt }) => ({
471
- receivedAt,
472
- rawEvent: JSON.stringify(event)
473
- }),
474
- deserialize: (body, { claim }) => deserializeNostrIngressEvent(body.rawEvent, claim.id),
475
- encode: ({ body }) => ({
476
- version: 1,
477
- ...body
478
- }),
479
- decode: (payload, { claim }) => {
480
- if (typeof payload !== "object" || payload === null || typeof payload.rawEvent !== "string") throw new NostrIngressPermanentError("invalid-event", `Nostr ingress row ${claim.id} has an invalid payload.`);
481
- return {
482
- version: payload.version,
483
- body: {
484
- receivedAt: typeof payload.receivedAt === "number" ? payload.receivedAt : 0,
485
- rawEvent: payload.rawEvent
486
- }
487
- };
488
- },
489
- createClaimError: (kind, claim) => new NostrIngressPermanentError("invalid-event", kind === "invalid-version" ? `Nostr ingress row ${claim.id} has an unsupported version.` : `Nostr ingress row ${claim.id} changed event identity.`)
490
- },
491
- deliver: (event, lifecycle) => options.deliver(event, lifecycle),
492
- pollIntervalMs: options.pollIntervalMs ?? NOSTR_INGRESS_POLL_INTERVAL_MS,
493
- retention: {
494
- completedMaxEntries: 1e5,
495
- failedMaxEntries: 1e5
496
- },
497
- drain: {
498
- adoptionStallTimeoutMs: options.adoptionStallTimeoutMs ?? DEFAULT_INGRESS_ADOPTION_STALL_MS,
499
- resolveNonRetryableFailure: (error) => error instanceof NostrIngressPermanentError ? {
500
- reason: error.reason,
501
- message: error.message
502
- } : null,
503
- onLog: (message) => options.onError?.(new Error(message), "ingress drain")
504
- },
505
- createStoppedError,
506
- onError: (error) => options.onError?.(error, "ingress drain")
507
- });
508
- const monitorStart = (async () => {
509
- monitor.ensureQueueAvailable();
510
- await migrateNostrLegacyRecentEventIds({
511
- queue: getQueue(),
512
- eventIds: options.legacyEventIds ?? []
513
- });
514
- if (!stopping) monitor.start();
515
- })();
516
- monitorStart.catch((error) => options.onError?.(error, "ingress drain"));
517
- let admissionTail = Promise.resolve();
518
- const prepareAdmission = (event) => {
519
- const facts = inspectNostrIngressEvent(event);
520
- const receivedAt = Date.now();
521
- if (receivedAt - admissionWindowStartedAt >= options.admissionRateLimit.windowMs) {
522
- admissionWindowStartedAt = receivedAt;
523
- admissionWindowCount = 0;
524
- }
525
- if (admissionWindowCount >= options.admissionRateLimit.maxEvents) throw new NostrIngressAdmissionRejectedError("rate-limited", "Nostr event exceeds the durable admission rate.");
526
- admissionWindowCount += 1;
527
- if (queuedAdmissions >= options.maxQueuedAdmissions) throw new NostrIngressAdmissionRejectedError("backpressure", "Nostr event exceeds the in-memory admission backlog.");
528
- let payload;
529
- let serializedPayload;
530
- try {
531
- payload = {
532
- version: 1,
533
- receivedAt,
534
- rawEvent: JSON.stringify(event)
535
- };
536
- serializedPayload = JSON.stringify(payload);
537
- } catch (error) {
538
- throw new NostrIngressPermanentError("invalid-event", "Nostr event could not be serialized for durable ingress.", { cause: error });
539
- }
540
- if (Buffer.byteLength(serializedPayload, "utf8") > options.maxSerializedPayloadBytes) throw new NostrIngressAdmissionRejectedError("oversized-event", "Nostr event exceeds the durable ingress size limit.");
541
- return {
542
- event,
543
- facts,
544
- receivedAt,
545
- payload
546
- };
547
- };
548
- const admitOnce = async (prepared) => {
549
- await monitorStart;
550
- const pending = await getQueue().listPending({ limit: options.maxPendingEvents });
551
- const claims = await getQueue().listClaims();
552
- 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 {
559
- const result = await getQueue().enqueue(prepared.facts.eventId, prepared.payload, {
560
- receivedAt: prepared.receivedAt,
561
- laneKey: prepared.facts.laneKey
562
- });
563
- options.afterDurableAppend(prepared.event);
564
- monitor.requestDrain();
565
- return result.kind === "accepted" ? "accepted" : "duplicate";
566
- } catch (error) {
567
- lastError = error;
568
- }
569
- }
570
- throw new Error(`Nostr durable admission failed: ${formatErrorMessage(lastError)}`, { cause: lastError });
571
- };
572
- return {
573
- ready: async () => {
574
- await monitorStart;
575
- },
576
- receive: (event) => {
577
- if (stopping) return Promise.reject(createStoppedError());
578
- let prepared;
579
- try {
580
- prepared = prepareAdmission(event);
581
- } catch (error) {
582
- return Promise.reject(error);
583
- }
584
- queuedAdmissions += 1;
585
- const settledAdmission = admissionTail.then(async () => {
586
- if (admissionFailure) throw admissionFailure;
587
- try {
588
- return await admitOnce(prepared);
589
- } catch (error) {
590
- if (error instanceof NostrIngressAdmissionRejectedError || error instanceof NostrIngressPermanentError) throw error;
591
- admissionFailure = error instanceof Error ? error : new Error(formatErrorMessage(error), { cause: error });
592
- throw admissionFailure;
593
- }
594
- }).finally(() => {
595
- queuedAdmissions -= 1;
596
- });
597
- admissionTail = settledAdmission.then(() => void 0, () => void 0);
598
- return settledAdmission;
599
- },
600
- stop: () => {
601
- if (stopTask) return stopTask;
602
- stopping = true;
603
- const pauseTask = monitor.pause();
604
- stopTask = (async () => {
605
- await admissionTail;
606
- await monitorStart.catch(() => void 0);
607
- await pauseTask;
608
- await monitor.stop();
609
- })();
610
- return stopTask;
611
- },
612
- waitForIdle: async () => {
613
- await admissionTail;
614
- await monitorStart;
615
- await monitor.waitForIdle();
616
- }
617
- };
618
- }
619
- //#endregion
620
286
  //#region extensions/nostr/src/nostr-profile-core.ts
621
287
  /**
622
288
  * Convert our config profile schema to NIP-01 content format.
@@ -652,16 +318,6 @@ function contentToProfile(content) {
652
318
  return profile;
653
319
  }
654
320
  //#endregion
655
- //#region extensions/nostr/src/relay-publish.ts
656
- const CONNECTION_FAILURE_PREFIX = "connection failure: ";
657
- async function publishNostrEventToRelay(pool, relay, event) {
658
- const publishPromise = pool.publish([relay], event)[0];
659
- if (!publishPromise) throw new Error(`Failed to create publish promise for relay ${relay}`);
660
- const result = await publishPromise;
661
- if (result.startsWith(CONNECTION_FAILURE_PREFIX)) throw new Error(result.slice(20));
662
- return result;
663
- }
664
- //#endregion
665
321
  //#region extensions/nostr/src/nostr-profile.ts
666
322
  /**
667
323
  * Nostr Profile Management (NIP-01 kind:0)
@@ -681,11 +337,12 @@ function createProfileEvent(sk, profile, lastPublishedAt) {
681
337
  const content = profileToContent(profile);
682
338
  const contentJson = JSON.stringify(content);
683
339
  const now = Math.floor(Date.now() / 1e3);
340
+ const createdAt = lastPublishedAt !== void 0 ? Math.max(now, lastPublishedAt + 1) : now;
684
341
  return finalizeEvent({
685
342
  kind: 0,
686
343
  content: contentJson,
687
344
  tags: [],
688
- created_at: lastPublishedAt !== void 0 ? Math.max(now, lastPublishedAt + 1) : now
345
+ created_at: createdAt
689
346
  }, sk);
690
347
  }
691
348
  /** Per-relay publish timeout (ms) */
@@ -710,7 +367,7 @@ async function publishProfileEvent(pool, relays, event) {
710
367
  const timeoutPromise = new Promise((_, reject) => {
711
368
  timer = setTimeout(() => reject(/* @__PURE__ */ new Error("timeout")), RELAY_PUBLISH_TIMEOUT_MS);
712
369
  });
713
- await Promise.race([publishNostrEventToRelay(pool, relay, event), timeoutPromise]);
370
+ await Promise.race([...pool.publish([relay], event), timeoutPromise]);
714
371
  successes.push(relay);
715
372
  } catch (err) {
716
373
  const errorMessage = formatErrorMessage(err);
@@ -744,111 +401,11 @@ async function publishProfile(pool, sk, relays, profile, lastPublishedAt) {
744
401
  return publishProfileEvent(pool, relays, createProfileEvent(sk, profile, lastPublishedAt));
745
402
  }
746
403
  //#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
- //#region extensions/nostr/src/nostr-relay-subscription.ts
790
- const DEFAULT_EOSE_CONFIRM_DEADLINE_MS = 1e4;
791
- const LIBRARY_EOSE_TIMEOUT_MARGIN_MS = 1e3;
792
- /** Separates real relay EOSE frames from nostr-tools timeout/close synthesis. */
793
- function createNostrRelaySubscriptionGroup(options) {
794
- const relays = [...new Set(options.relays)];
795
- const subscriptions = [];
796
- const deadlineTimers = /* @__PURE__ */ new Set();
797
- let locallyClosing = false;
798
- const backfillStatus = new Map(relays.map((relay) => [relay, "pending"]));
799
- const confirmDeadlineMs = options.eoseConfirmDeadlineMs ?? DEFAULT_EOSE_CONFIRM_DEADLINE_MS;
800
- const settleBackfill = (relay, status) => {
801
- if (backfillStatus.get(relay) !== "pending") return;
802
- backfillStatus.set(relay, status);
803
- if ([...backfillStatus.values()].some((value) => value === "pending")) return;
804
- if ([...backfillStatus.values()].every((value) => value === "confirmed")) options.onBackfillComplete(relays);
805
- };
806
- const clearDeadlines = () => {
807
- for (const timer of deadlineTimers) clearTimeout(timer);
808
- deadlineTimers.clear();
809
- };
810
- return {
811
- start: () => {
812
- for (const relay of relays) {
813
- let relayClosed = false;
814
- let deadlineReached = false;
815
- const deadlineTimer = setTimeout(() => {
816
- deadlineTimers.delete(deadlineTimer);
817
- deadlineReached = true;
818
- settleBackfill(relay, "incomplete");
819
- }, confirmDeadlineMs);
820
- deadlineTimer.unref?.();
821
- deadlineTimers.add(deadlineTimer);
822
- subscriptions.push(options.pool.subscribeMany([relay], options.filter, {
823
- onevent: options.onEvent,
824
- oneose: () => {
825
- queueMicrotask(() => {
826
- if (!relayClosed && !deadlineReached) {
827
- clearTimeout(deadlineTimer);
828
- deadlineTimers.delete(deadlineTimer);
829
- settleBackfill(relay, "confirmed");
830
- }
831
- });
832
- },
833
- onclose: (reasons) => {
834
- relayClosed = true;
835
- clearTimeout(deadlineTimer);
836
- deadlineTimers.delete(deadlineTimer);
837
- settleBackfill(relay, "incomplete");
838
- if (!locallyClosing && !options.abort.aborted) options.onClose(relay, reasons.map(({ reason }) => reason));
839
- },
840
- maxWait: confirmDeadlineMs + LIBRARY_EOSE_TIMEOUT_MARGIN_MS,
841
- abort: options.abort
842
- }));
843
- }
844
- },
845
- close: async (reason) => {
846
- locallyClosing = true;
847
- clearDeadlines();
848
- await Promise.all(subscriptions.map(async (subscription) => subscription.close(reason)));
849
- }
850
- };
851
- }
404
+ //#region extensions/nostr/src/runtime.ts
405
+ const { setRuntime: setNostrRuntime, getRuntime: getNostrRuntime } = createPluginRuntimeStore({
406
+ pluginId: "nostr",
407
+ errorMessage: "Nostr runtime not initialized"
408
+ });
852
409
  //#endregion
853
410
  //#region extensions/nostr/src/nostr-state-store.ts
854
411
  const STORE_VERSION = 2;
@@ -903,15 +460,213 @@ async function writeNostrProfileState(params) {
903
460
  await openNostrProfileStateStore(params.env).register(normalizeNostrStateAccountId(params.accountId), payload);
904
461
  }
905
462
  //#endregion
463
+ //#region extensions/nostr/src/seen-tracker.ts
464
+ /**
465
+ * LRU-based seen event tracker with TTL support.
466
+ * Prevents unbounded memory growth under high load or abuse.
467
+ */
468
+ /**
469
+ * Create a new seen tracker with LRU eviction and TTL expiration.
470
+ */
471
+ function createSeenTracker(options) {
472
+ const maxEntries = resolveIntegerOption(options?.maxEntries, 1e5, { min: 1 });
473
+ const ttlMs = resolvePositiveTimerTimeoutMs(options?.ttlMs, 36e5);
474
+ const pruneIntervalMs = resolvePositiveTimerTimeoutMs(options?.pruneIntervalMs, 6e5);
475
+ const entries = /* @__PURE__ */ new Map();
476
+ let head = null;
477
+ let tail = null;
478
+ function moveToFront(id) {
479
+ const entry = entries.get(id);
480
+ if (!entry) return;
481
+ if (head === id) return;
482
+ if (entry.prev) {
483
+ const prevEntry = entries.get(entry.prev);
484
+ if (prevEntry) prevEntry.next = entry.next;
485
+ }
486
+ if (entry.next) {
487
+ const nextEntry = entries.get(entry.next);
488
+ if (nextEntry) nextEntry.prev = entry.prev;
489
+ }
490
+ if (tail === id) tail = entry.prev;
491
+ entry.prev = null;
492
+ entry.next = head;
493
+ if (head) {
494
+ const headEntry = entries.get(head);
495
+ if (headEntry) headEntry.prev = id;
496
+ }
497
+ head = id;
498
+ if (!tail) tail = id;
499
+ }
500
+ function removeFromList(id) {
501
+ const entry = entries.get(id);
502
+ if (!entry) return;
503
+ if (entry.prev) {
504
+ const prevEntry = entries.get(entry.prev);
505
+ if (prevEntry) prevEntry.next = entry.next;
506
+ } else head = entry.next;
507
+ if (entry.next) {
508
+ const nextEntry = entries.get(entry.next);
509
+ if (nextEntry) nextEntry.prev = entry.prev;
510
+ } else tail = entry.prev;
511
+ }
512
+ function evictLRU() {
513
+ if (!tail) return;
514
+ const idToEvict = tail;
515
+ removeFromList(idToEvict);
516
+ entries.delete(idToEvict);
517
+ }
518
+ function insertAtFront(id, seenAt) {
519
+ const newEntry = {
520
+ seenAt,
521
+ prev: null,
522
+ next: head
523
+ };
524
+ if (head) {
525
+ const headEntry = entries.get(head);
526
+ if (headEntry) headEntry.prev = id;
527
+ }
528
+ entries.set(id, newEntry);
529
+ head = id;
530
+ if (!tail) tail = id;
531
+ }
532
+ function pruneExpired() {
533
+ const now = Date.now();
534
+ const toDelete = [];
535
+ for (const [id, entry] of entries) if (now - entry.seenAt > ttlMs) toDelete.push(id);
536
+ for (const id of toDelete) {
537
+ removeFromList(id);
538
+ entries.delete(id);
539
+ }
540
+ }
541
+ let pruneTimer;
542
+ if (pruneIntervalMs > 0) {
543
+ pruneTimer = setInterval(pruneExpired, pruneIntervalMs);
544
+ if (pruneTimer.unref) pruneTimer.unref();
545
+ }
546
+ function add(id) {
547
+ const now = Date.now();
548
+ const existing = entries.get(id);
549
+ if (existing) {
550
+ existing.seenAt = now;
551
+ moveToFront(id);
552
+ return;
553
+ }
554
+ while (entries.size >= maxEntries) evictLRU();
555
+ insertAtFront(id, now);
556
+ }
557
+ function has(id) {
558
+ const entry = entries.get(id);
559
+ if (!entry) {
560
+ add(id);
561
+ return false;
562
+ }
563
+ if (Date.now() - entry.seenAt > ttlMs) {
564
+ removeFromList(id);
565
+ entries.delete(id);
566
+ add(id);
567
+ return false;
568
+ }
569
+ entry.seenAt = Date.now();
570
+ moveToFront(id);
571
+ return true;
572
+ }
573
+ function peek(id) {
574
+ const entry = entries.get(id);
575
+ if (!entry) return false;
576
+ if (Date.now() - entry.seenAt > ttlMs) {
577
+ removeFromList(id);
578
+ entries.delete(id);
579
+ return false;
580
+ }
581
+ return true;
582
+ }
583
+ function deleteEntry(id) {
584
+ if (entries.has(id)) {
585
+ removeFromList(id);
586
+ entries.delete(id);
587
+ }
588
+ }
589
+ function clear() {
590
+ entries.clear();
591
+ head = null;
592
+ tail = null;
593
+ }
594
+ function size() {
595
+ return entries.size;
596
+ }
597
+ function stop() {
598
+ if (pruneTimer) {
599
+ clearInterval(pruneTimer);
600
+ pruneTimer = void 0;
601
+ }
602
+ }
603
+ function seed(ids) {
604
+ const now = Date.now();
605
+ for (let i = ids.length - 1; i >= 0; i--) {
606
+ const id = ids[i];
607
+ if (!entries.has(id) && entries.size < maxEntries) insertAtFront(id, now);
608
+ }
609
+ }
610
+ return {
611
+ has,
612
+ add,
613
+ peek,
614
+ delete: deleteEntry,
615
+ clear,
616
+ size,
617
+ stop,
618
+ seed
619
+ };
620
+ }
621
+ //#endregion
906
622
  //#region extensions/nostr/src/nostr-bus.ts
907
623
  const STARTUP_LOOKBACK_SEC = 120;
624
+ const MAX_PERSISTED_EVENT_IDS = 5e3;
908
625
  const STATE_PERSIST_DEBOUNCE_MS = 5e3;
909
- const NOSTR_INGRESS_ENVELOPE_OVERHEAD_BYTES = 16 * 1024;
910
- const NOSTR_INGRESS_MAX_PENDING_EVENTS = 1e3;
911
626
  const DEFAULT_INBOUND_GUARD_POLICY = createDirectDmPreCryptoGuardPolicy();
912
627
  const CIRCUIT_BREAKER_THRESHOLD = 5;
913
628
  const CIRCUIT_BREAKER_RESET_MS = 3e4;
914
629
  const HEALTH_WINDOW_MS = 6e4;
630
+ function createFixedWindowRateLimiter(params) {
631
+ const windowMs = Math.max(1, Math.floor(params.windowMs));
632
+ const maxRequests = Math.max(1, Math.floor(params.maxRequests));
633
+ const maxTrackedKeys = Math.max(1, Math.floor(params.maxTrackedKeys));
634
+ const state = /* @__PURE__ */ new Map();
635
+ const touch = (key, value) => {
636
+ state.delete(key);
637
+ state.set(key, value);
638
+ };
639
+ const prune = (nowMs) => {
640
+ for (const [key, entry] of state) if (nowMs - entry.windowStartMs >= windowMs) state.delete(key);
641
+ while (state.size > maxTrackedKeys) {
642
+ const oldest = state.keys().next().value;
643
+ if (!oldest) break;
644
+ state.delete(oldest);
645
+ }
646
+ };
647
+ return {
648
+ isRateLimited: (key, nowMs = Date.now()) => {
649
+ if (!key) return false;
650
+ prune(nowMs);
651
+ const existing = state.get(key);
652
+ if (!existing || nowMs - existing.windowStartMs >= windowMs) {
653
+ touch(key, {
654
+ count: 1,
655
+ windowStartMs: nowMs
656
+ });
657
+ return false;
658
+ }
659
+ const nextCount = existing.count + 1;
660
+ touch(key, {
661
+ count: nextCount,
662
+ windowStartMs: existing.windowStartMs
663
+ });
664
+ return nextCount > maxRequests;
665
+ },
666
+ size: () => state.size,
667
+ clear: () => state.clear()
668
+ };
669
+ }
915
670
  function createCircuitBreaker(relay, metrics, threshold = CIRCUIT_BREAKER_THRESHOLD, resetMs = CIRCUIT_BREAKER_RESET_MS) {
916
671
  const state = {
917
672
  state: "closed",
@@ -1007,11 +762,10 @@ function createRelayHealthTracker() {
1007
762
  * Start the Nostr DM bus - subscribes to NIP-04 encrypted DMs
1008
763
  */
1009
764
  async function startNostrBus(options) {
1010
- const { privateKey, relays = DEFAULT_RELAYS, onMessage, authorizeSender, onError, onEose, onMetric } = options;
765
+ const { privateKey, relays = DEFAULT_RELAYS, onMessage, authorizeSender, onError, onEose, onMetric, maxSeenEntries = 1e5, seenTtlMs = 36e5 } = options;
1011
766
  const sk = validatePrivateKey(privateKey);
1012
767
  const pk = getPublicKey(sk);
1013
768
  const pool = new SimplePool();
1014
- pool.onRelayConnectionSuccess = options.onConnect;
1015
769
  const accountId = options.accountId ?? pk.slice(0, 16);
1016
770
  const gatewayStartedAt = Math.floor(Date.now() / 1e3);
1017
771
  const guardPolicy = createDirectDmPreCryptoGuardPolicy({
@@ -1023,32 +777,41 @@ async function startNostrBus(options) {
1023
777
  }
1024
778
  });
1025
779
  const metrics = onMetric ? createMetrics(onMetric) : createNoopMetrics();
780
+ const seen = createSeenTracker({
781
+ maxEntries: maxSeenEntries,
782
+ ttlMs: seenTtlMs
783
+ });
1026
784
  const circuitBreakers = /* @__PURE__ */ new Map();
1027
785
  const healthTracker = createRelayHealthTracker();
1028
786
  for (const relay of relays) circuitBreakers.set(relay, createCircuitBreaker(relay, metrics));
1029
787
  const state = await readNostrBusState({ accountId });
1030
788
  const baseSince = computeSinceTimestamp(state, gatewayStartedAt);
1031
789
  const since = Math.max(0, baseSince - STARTUP_LOOKBACK_SEC);
1032
- const cursorStartedAt = state?.gatewayStartedAt ?? gatewayStartedAt;
1033
- const initialCursor = Math.max(baseSince, state?.lastProcessedAt ?? cursorStartedAt);
1034
- const cursorWriter = createNostrCursorStateWriter({
1035
- initialCursor,
1036
- minimumCursor: baseSince,
1037
- debounceMs: STATE_PERSIST_DEBOUNCE_MS,
1038
- write: async (cursor) => {
1039
- await writeNostrBusState({
1040
- accountId,
1041
- lastProcessedAt: cursor,
1042
- gatewayStartedAt: cursorStartedAt,
1043
- recentEventIds: []
1044
- });
1045
- },
1046
- onBackgroundError: (error) => onError?.(error, "persist state")
1047
- });
1048
- const durableCursor = createNostrDurableCursor({
1049
- since,
1050
- replayOverlapSec: STARTUP_LOOKBACK_SEC
790
+ if (state?.recentEventIds?.length) seen.seed(state.recentEventIds);
791
+ await writeNostrBusState({
792
+ accountId,
793
+ lastProcessedAt: state?.lastProcessedAt ?? gatewayStartedAt,
794
+ gatewayStartedAt,
795
+ recentEventIds: state?.recentEventIds ?? []
1051
796
  });
797
+ let pendingWrite;
798
+ let lastProcessedAt = state?.lastProcessedAt ?? gatewayStartedAt;
799
+ let recentEventIds = (state?.recentEventIds ?? []).slice(-5e3);
800
+ function scheduleStatePersist(eventCreatedAt, eventId) {
801
+ lastProcessedAt = Math.max(lastProcessedAt, eventCreatedAt);
802
+ recentEventIds.push(eventId);
803
+ if (recentEventIds.length > MAX_PERSISTED_EVENT_IDS) recentEventIds = recentEventIds.slice(-5e3);
804
+ if (pendingWrite) clearTimeout(pendingWrite);
805
+ pendingWrite = setTimeout(() => {
806
+ writeNostrBusState({
807
+ accountId,
808
+ lastProcessedAt,
809
+ gatewayStartedAt,
810
+ recentEventIds
811
+ }).catch((err) => onError?.(err, "persist state"));
812
+ }, STATE_PERSIST_DEBOUNCE_MS);
813
+ }
814
+ const inflight = /* @__PURE__ */ new Set();
1052
815
  const perSenderRateLimiter = createFixedWindowRateLimiter({
1053
816
  windowMs: guardPolicy.rateLimit.windowMs,
1054
817
  maxRequests: guardPolicy.rateLimit.maxPerSenderPerWindow,
@@ -1062,217 +825,147 @@ async function startNostrBus(options) {
1062
825
  const updateRateLimiterSizeMetric = () => {
1063
826
  metrics.emit("memory.rate_limiter_entries", perSenderRateLimiter.size() + globalRateLimiter.size());
1064
827
  };
1065
- const rejectIfGlobalRateLimited = () => {
1066
- updateRateLimiterSizeMetric();
1067
- if (globalRateLimiter.isRateLimited("global")) {
1068
- metrics.emit("rate_limit.global");
1069
- metrics.emit("event.rejected.rate_limited");
1070
- updateRateLimiterSizeMetric();
1071
- return true;
1072
- }
1073
- updateRateLimiterSizeMetric();
1074
- return false;
1075
- };
1076
- const rejectIfVerifiedSenderRateLimited = (senderPubkey) => {
1077
- updateRateLimiterSizeMetric();
1078
- if (perSenderRateLimiter.isRateLimited(senderPubkey)) {
1079
- metrics.emit("rate_limit.per_sender");
1080
- metrics.emit("event.rejected.rate_limited");
1081
- updateRateLimiterSizeMetric();
1082
- return true;
1083
- }
1084
- updateRateLimiterSizeMetric();
1085
- return false;
1086
- };
1087
- async function dispatchEvent(event, lifecycle) {
1088
- if (event.pubkey === pk) {
1089
- metrics.emit("event.rejected.self_message");
1090
- return;
1091
- }
1092
- if (event.created_at > Math.floor(Date.now() / 1e3) + guardPolicy.maxFutureSkewSec) {
1093
- metrics.emit("event.rejected.future");
1094
- throw new Error(`Nostr event ${event.id} is too far in the future.`);
1095
- }
1096
- if (!guardPolicy.allowedKinds.includes(event.kind)) {
1097
- metrics.emit("event.rejected.wrong_kind");
1098
- return;
1099
- }
1100
- let targetsUs = false;
1101
- for (const tag of event.tags) if (tag[0] === "p" && tag[1] === pk) {
1102
- targetsUs = true;
1103
- break;
1104
- }
1105
- if (!targetsUs) {
1106
- metrics.emit("event.rejected.wrong_kind");
1107
- return;
1108
- }
1109
- const replyTo = async (text) => {
1110
- await sendEncryptedDm(pool, sk, event.pubkey, text, relays, metrics, circuitBreakers, healthTracker, onError, event.id);
1111
- };
1112
- if (Buffer.byteLength(event.content, "utf8") > guardPolicy.maxCiphertextBytes) {
1113
- if (rejectIfGlobalRateLimited()) throw new Error(`Nostr event ${event.id} hit the global rate limit.`);
1114
- metrics.emit("event.rejected.oversized_ciphertext");
1115
- return;
1116
- }
1117
- if (rejectIfGlobalRateLimited()) throw new Error(`Nostr event ${event.id} hit the global rate limit.`);
1118
- if (!verifyEvent(event)) {
1119
- metrics.emit("event.rejected.invalid_signature");
1120
- const error = new NostrIngressPermanentError("invalid-signature", `Nostr event ${event.id} has an invalid signature.`);
1121
- onError?.(error, `event ${event.id}`);
1122
- throw error;
1123
- }
1124
- if (rejectIfVerifiedSenderRateLimited(event.pubkey)) throw new Error(`Nostr sender ${event.pubkey} hit the rate limit.`);
1125
- if (authorizeSender) {
1126
- if (await authorizeSender({
1127
- senderPubkey: event.pubkey,
1128
- reply: replyTo
1129
- }) !== "allow") return;
1130
- }
1131
- let plaintext;
828
+ async function handleEvent(event) {
1132
829
  try {
1133
- plaintext = decrypt(sk, event.pubkey, event.content);
1134
- metrics.emit("decrypt.success");
1135
- } catch (error) {
1136
- metrics.emit("decrypt.failure");
1137
- metrics.emit("event.rejected.decrypt_failed");
1138
- onError?.(error, `decrypt from ${event.pubkey}`);
1139
- throw new NostrIngressPermanentError("decrypt-failed", `Nostr event ${event.id} could not be decrypted.`, { cause: error });
1140
- }
1141
- if (Buffer.byteLength(plaintext, "utf8") > guardPolicy.maxPlaintextBytes) {
1142
- metrics.emit("event.rejected.oversized_plaintext");
1143
- return;
1144
- }
1145
- if (lifecycle.abortSignal.aborted) throw new Error(`Nostr event ${event.id} stopped before dispatch.`);
1146
- await onMessage(event.pubkey, plaintext, replyTo, {
1147
- eventId: event.id,
1148
- createdAt: event.created_at
1149
- }, lifecycle);
1150
- metrics.emit("event.processed");
1151
- }
1152
- const dmFilter = {
1153
- kinds: [4],
1154
- "#p": [pk],
1155
- since
1156
- };
1157
- const relayAbort = new AbortController();
1158
- let relaySubscriptions;
1159
- let relayStopPromise;
1160
- const stopRelays = (reason) => {
1161
- relayStopPromise ??= (async () => {
1162
- relayAbort.abort(reason);
1163
- try {
1164
- await relaySubscriptions?.close(reason);
1165
- } catch (error) {
1166
- onError?.(error, "close subscription");
1167
- } finally {
1168
- try {
1169
- pool.close(relays);
1170
- } catch (error) {
1171
- onError?.(error, "close relay pool");
1172
- }
830
+ metrics.emit("event.received");
831
+ if (seen.peek(event.id) || inflight.has(event.id)) {
832
+ metrics.emit("event.duplicate");
833
+ return;
1173
834
  }
1174
- })();
1175
- return relayStopPromise;
1176
- };
1177
- const ingress = createNostrIngress({
1178
- accountId,
1179
- legacyEventIds: state?.recentEventIds ?? [],
1180
- maxSerializedPayloadBytes: guardPolicy.maxCiphertextBytes + NOSTR_INGRESS_ENVELOPE_OVERHEAD_BYTES,
1181
- maxPendingEvents: NOSTR_INGRESS_MAX_PENDING_EVENTS,
1182
- maxQueuedAdmissions: guardPolicy.rateLimit.maxGlobalPerWindow,
1183
- admissionRateLimit: {
1184
- windowMs: guardPolicy.rateLimit.windowMs,
1185
- maxEvents: guardPolicy.rateLimit.maxGlobalPerWindow
1186
- },
1187
- afterDurableAppend: (event) => {
1188
- const cursor = durableCursor.recordDurableAppend(event);
1189
- if (cursor !== void 0) cursorWriter.schedule(cursor);
1190
- },
1191
- deliver: dispatchEvent,
1192
- onError
1193
- });
1194
- const persistTransientReplayCursor = async (event) => {
1195
- const cursor = durableCursor.recordTransientRejection(event);
1196
- if (cursor !== void 0) await cursorWriter.persistNow(cursor);
1197
- };
1198
- const recoverCursorPersistence = async () => {
1199
- await cursorWriter.flushUntilSuccess();
1200
- };
1201
- const handleRelayEvent = async (event) => {
1202
- metrics.emit("event.received");
1203
- if (typeof event.created_at === "number" && event.created_at < since) {
1204
- metrics.emit("event.rejected.stale");
1205
- return;
1206
- }
1207
- try {
1208
- if (await ingress.receive(event) === "duplicate") metrics.emit("event.duplicate");
1209
- } catch (error) {
1210
- onError?.(error, `durable admission for event ${event.id}`);
1211
- if (error instanceof NostrIngressAdmissionRejectedError) {
1212
- if (error.reason === "rate-limited") {
835
+ inflight.add(event.id);
836
+ const markSeen = () => {
837
+ seen.add(event.id);
838
+ metrics.emit("memory.seen_tracker_size", seen.size());
839
+ };
840
+ const rejectAndMarkSeen = (metric) => {
841
+ markSeen();
842
+ metrics.emit(metric);
843
+ };
844
+ if (event.pubkey === pk) {
845
+ rejectAndMarkSeen("event.rejected.self_message");
846
+ return;
847
+ }
848
+ if (event.created_at < since) {
849
+ rejectAndMarkSeen("event.rejected.stale");
850
+ return;
851
+ }
852
+ if (event.created_at > Math.floor(Date.now() / 1e3) + guardPolicy.maxFutureSkewSec) {
853
+ metrics.emit("event.rejected.future");
854
+ return;
855
+ }
856
+ if (!guardPolicy.allowedKinds.includes(event.kind)) {
857
+ rejectAndMarkSeen("event.rejected.wrong_kind");
858
+ return;
859
+ }
860
+ let targetsUs = false;
861
+ for (const t of event.tags) if (t[0] === "p" && t[1] === pk) {
862
+ targetsUs = true;
863
+ break;
864
+ }
865
+ if (!targetsUs) {
866
+ rejectAndMarkSeen("event.rejected.wrong_kind");
867
+ return;
868
+ }
869
+ const replyTo = async (text) => {
870
+ await sendEncryptedDm(pool, sk, event.pubkey, text, relays, metrics, circuitBreakers, healthTracker, onError);
871
+ };
872
+ const rejectIfGlobalRateLimited = () => {
873
+ updateRateLimiterSizeMetric();
874
+ if (globalRateLimiter.isRateLimited("global")) {
1213
875
  metrics.emit("rate_limit.global");
1214
876
  metrics.emit("event.rejected.rate_limited");
877
+ updateRateLimiterSizeMetric();
878
+ return true;
1215
879
  }
1216
- if (error.reason !== "oversized-event") try {
1217
- await persistTransientReplayCursor(event);
1218
- } catch (cursorError) {
1219
- onError?.(cursorError, "persist transient replay cursor");
1220
- await stopRelays("cursor persistence failed");
1221
- await recoverCursorPersistence();
880
+ updateRateLimiterSizeMetric();
881
+ return false;
882
+ };
883
+ const rejectIfVerifiedSenderRateLimited = () => {
884
+ updateRateLimiterSizeMetric();
885
+ if (perSenderRateLimiter.isRateLimited(event.pubkey)) {
886
+ metrics.emit("rate_limit.per_sender");
887
+ metrics.emit("event.rejected.rate_limited");
888
+ updateRateLimiterSizeMetric();
889
+ return true;
1222
890
  }
891
+ updateRateLimiterSizeMetric();
892
+ return false;
893
+ };
894
+ if (Buffer.byteLength(event.content, "utf8") > guardPolicy.maxCiphertextBytes) {
895
+ if (rejectIfGlobalRateLimited()) return;
896
+ rejectAndMarkSeen("event.rejected.oversized_ciphertext");
1223
897
  return;
1224
898
  }
1225
- if (error instanceof NostrIngressPermanentError) return;
1226
- let cursorPersistenceFailed = false;
899
+ if (rejectIfGlobalRateLimited()) return;
900
+ if (!verifyEvent(event)) {
901
+ rejectAndMarkSeen("event.rejected.invalid_signature");
902
+ onError?.(/* @__PURE__ */ new Error("Invalid signature"), `event ${event.id}`);
903
+ return;
904
+ }
905
+ if (rejectIfVerifiedSenderRateLimited()) return;
906
+ if (authorizeSender) {
907
+ if (await authorizeSender({
908
+ senderPubkey: event.pubkey,
909
+ reply: replyTo
910
+ }) !== "allow") {
911
+ markSeen();
912
+ return;
913
+ }
914
+ }
915
+ let plaintext;
1227
916
  try {
1228
- await persistTransientReplayCursor(event);
1229
- } catch (cursorError) {
1230
- onError?.(cursorError, "persist transient replay cursor");
1231
- cursorPersistenceFailed = true;
917
+ plaintext = decrypt(sk, event.pubkey, event.content);
918
+ metrics.emit("decrypt.success");
919
+ } catch (err) {
920
+ markSeen();
921
+ metrics.emit("decrypt.failure");
922
+ metrics.emit("event.rejected.decrypt_failed");
923
+ onError?.(err, `decrypt from ${event.pubkey}`);
924
+ return;
1232
925
  }
1233
- await stopRelays("durable admission failed");
1234
- if (cursorPersistenceFailed) await recoverCursorPersistence();
926
+ if (Buffer.byteLength(plaintext, "utf8") > guardPolicy.maxPlaintextBytes) {
927
+ markSeen();
928
+ metrics.emit("event.rejected.oversized_plaintext");
929
+ return;
930
+ }
931
+ await onMessage(event.pubkey, plaintext, replyTo, {
932
+ eventId: event.id,
933
+ createdAt: event.created_at
934
+ });
935
+ markSeen();
936
+ metrics.emit("event.processed");
937
+ scheduleStatePersist(event.created_at, event.id);
938
+ } catch (err) {
939
+ onError?.(err, `event ${event.id}`);
940
+ } finally {
941
+ inflight.delete(event.id);
1235
942
  }
943
+ }
944
+ const dmFilter = {
945
+ kinds: [4],
946
+ "#p": [pk],
947
+ since
1236
948
  };
1237
- let backfillFinalizePromise;
1238
- try {
1239
- await ingress.ready();
1240
- await writeNostrBusState({
1241
- accountId,
1242
- lastProcessedAt: initialCursor,
1243
- gatewayStartedAt: cursorStartedAt,
1244
- recentEventIds: []
1245
- });
1246
- relaySubscriptions = createNostrRelaySubscriptionGroup({
1247
- pool,
1248
- relays,
1249
- filter: dmFilter,
1250
- abort: relayAbort.signal,
1251
- onEvent: (event) => {
1252
- const task = handleRelayEvent(event);
1253
- if (options.trackIngressTask) options.trackIngressTask(task.then(() => ingress.waitForIdle()));
1254
- },
1255
- onBackfillComplete: (confirmedRelays) => {
1256
- backfillFinalizePromise ??= ingress.waitForIdle().then(() => {
1257
- const cursor = durableCursor.markBackfillComplete();
1258
- if (cursor !== void 0) cursorWriter.schedule(cursor);
1259
- for (const relay of confirmedRelays) metrics.emit("relay.message.eose", 1, { relay });
1260
- onEose?.(confirmedRelays.join(", "));
1261
- }).catch((error) => onError?.(error, "finalize relay backfill"));
1262
- },
1263
- onClose: (relay, reasons) => {
949
+ const relayAbort = new AbortController();
950
+ const sub = pool.subscribeMany(relays, dmFilter, {
951
+ onevent: (event) => {
952
+ handleEvent(event);
953
+ },
954
+ oneose: () => {
955
+ for (const relay of relays) metrics.emit("relay.message.eose", 1, { relay });
956
+ onEose?.(relays.join(", "));
957
+ },
958
+ onclose: (reason) => {
959
+ for (const relay of relays) {
1264
960
  metrics.emit("relay.message.closed", 1, { relay });
1265
961
  options.onDisconnect?.(relay);
1266
- onError?.(/* @__PURE__ */ new Error(`Subscription closed: ${reasons.join(", ")}`), "subscription");
1267
962
  }
1268
- });
1269
- relaySubscriptions.start();
1270
- } catch (error) {
1271
- await Promise.allSettled([stopRelays("startup failed"), ingress.stop()]);
1272
- throw error;
1273
- }
963
+ onError?.(/* @__PURE__ */ new Error(`Subscription closed: ${reason.join(", ")}`), "subscription");
964
+ },
965
+ abort: relayAbort.signal
966
+ });
1274
967
  const sendDm = async (toPubkey, text) => {
1275
- return await sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circuitBreakers, healthTracker, onError);
968
+ await sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circuitBreakers, healthTracker, onError);
1276
969
  };
1277
970
  const publishProfile$1 = async (profile) => {
1278
971
  const lastPublishedAt = (await readNostrProfileState({ accountId }))?.lastPublishedAt ?? void 0;
@@ -1296,20 +989,25 @@ async function startNostrBus(options) {
1296
989
  lastPublishResults: stateLocal?.lastPublishResults ?? null
1297
990
  };
1298
991
  };
1299
- let closePromise;
1300
- const close = () => {
1301
- closePromise ??= (async () => {
1302
- await stopRelays("closed by caller");
1303
- await ingress.stop();
1304
- await backfillFinalizePromise;
1305
- await cursorWriter.flushUntilSuccess();
992
+ return {
993
+ close: () => {
994
+ relayAbort.abort("closed by caller");
995
+ Promise.resolve(sub.close("closed by caller")).catch((err) => onError?.(err, "close subscription")).finally(() => {
996
+ pool.close(relays);
997
+ });
998
+ seen.stop();
1306
999
  perSenderRateLimiter.clear();
1307
1000
  globalRateLimiter.clear();
1308
- })();
1309
- return closePromise;
1310
- };
1311
- return {
1312
- close,
1001
+ if (pendingWrite) {
1002
+ clearTimeout(pendingWrite);
1003
+ writeNostrBusState({
1004
+ accountId,
1005
+ lastProcessedAt,
1006
+ gatewayStartedAt,
1007
+ recentEventIds
1008
+ }).catch((err) => onError?.(err, "persist state on close"));
1009
+ }
1010
+ },
1313
1011
  publicKey: pk,
1314
1012
  sendDm,
1315
1013
  getMetrics: () => metrics.getSnapshot(),
@@ -1320,14 +1018,12 @@ async function startNostrBus(options) {
1320
1018
  /**
1321
1019
  * Send an encrypted DM to a pubkey
1322
1020
  */
1323
- async function sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circuitBreakers, healthTracker, onError, replyToEventId) {
1021
+ async function sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circuitBreakers, healthTracker, onError) {
1324
1022
  const ciphertext = encrypt(sk, toPubkey, text);
1325
- const tags = [["p", toPubkey]];
1326
- if (replyToEventId) tags.push(["e", replyToEventId]);
1327
1023
  const reply = finalizeEvent({
1328
1024
  kind: 4,
1329
1025
  content: ciphertext,
1330
- tags,
1026
+ tags: [["p", toPubkey]],
1331
1027
  created_at: Math.floor(Date.now() / 1e3)
1332
1028
  }, sk);
1333
1029
  const sortedRelays = healthTracker.getSortedRelays(relays);
@@ -1337,13 +1033,15 @@ async function sendEncryptedDm(pool, sk, toPubkey, text, relays, metrics, circui
1337
1033
  if (cb && !cb.canAttempt()) continue;
1338
1034
  const startTime = Date.now();
1339
1035
  try {
1340
- await publishNostrEventToRelay(pool, relay, reply);
1036
+ const publishPromises = pool.publish([relay], reply);
1037
+ if (publishPromises.length === 0) throw new Error(`Failed to create publish promise for relay ${relay}`);
1038
+ await publishPromises[0];
1341
1039
  const latency = Date.now() - startTime;
1342
1040
  cb?.recordSuccess();
1343
1041
  healthTracker.recordSuccess(relay, latency);
1344
- return reply.id;
1042
+ return;
1345
1043
  } catch (err) {
1346
- lastError = err instanceof Error ? err : new Error(String(err));
1044
+ lastError = err;
1347
1045
  const latency = Date.now() - startTime;
1348
1046
  cb?.recordFailure();
1349
1047
  healthTracker.recordFailure(relay);
@@ -1442,7 +1140,7 @@ const startNostrGatewayAccount = async (ctx) => {
1442
1140
  ctx.log?.debug?.(`[${account.accountId}] blocked Nostr sender ${input.senderId} (${resolved.senderAccess.reasonCode})`);
1443
1141
  return "block";
1444
1142
  };
1445
- await runPassiveAccountLifecycle({
1143
+ await runStoppablePassiveMonitor({
1446
1144
  abortSignal: ctx.abortSignal,
1447
1145
  start: async () => {
1448
1146
  const bus = await startNostrBus({
@@ -1453,15 +1151,16 @@ const startNostrGatewayAccount = async (ctx) => {
1453
1151
  senderId: senderPubkey,
1454
1152
  reply
1455
1153
  }),
1456
- onMessage: async (senderPubkey, text, reply, meta, lifecycle) => {
1154
+ onMessage: async (senderPubkey, text, reply, meta) => {
1457
1155
  const resolvedAccess = await resolveInboundAccess(senderPubkey, text);
1458
1156
  if (resolvedAccess.senderAccess.decision !== "allow") {
1459
1157
  ctx.log?.warn?.(`[${account.accountId}] dropping Nostr DM after preflight drift (${senderPubkey}, ${resolvedAccess.senderAccess.reasonCode})`);
1460
1158
  return;
1461
1159
  }
1462
- const { dispatchInboundDirectDm } = await import("./inbound-direct-dm-runtime-DHn9NoYN.js");
1463
- await dispatchInboundDirectDm({
1160
+ const { dispatchInboundDirectDmWithRuntime } = await import("./inbound-direct-dm-runtime-EdLLVtkh.mjs");
1161
+ await dispatchInboundDirectDmWithRuntime({
1464
1162
  cfg: ctx.cfg,
1163
+ runtime,
1465
1164
  channel: "nostr",
1466
1165
  channelLabel: "Nostr",
1467
1166
  accountId: account.accountId,
@@ -1477,17 +1176,15 @@ const startNostrGatewayAccount = async (ctx) => {
1477
1176
  messageId: meta.eventId,
1478
1177
  timestamp: meta.createdAt * 1e3,
1479
1178
  commandAuthorized: resolvedAccess.commandAccess.requested ? resolvedAccess.commandAccess.authorized : void 0,
1480
- turnAdoptionLifecycle: bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle,
1481
1179
  deliver: async (payload) => {
1482
- const sanitizedText = sanitizeAssistantVisibleText(payload && typeof payload === "object" && "text" in payload ? payload.text ?? "" : "");
1483
- if (!sanitizedText) return;
1180
+ const outboundText = payload && typeof payload === "object" && "text" in payload ? payload.text ?? "" : "";
1181
+ if (!outboundText.trim()) return;
1484
1182
  const tableMode = runtime.channel.text.resolveMarkdownTableMode({
1485
1183
  cfg: ctx.cfg,
1486
1184
  channel: "nostr",
1487
1185
  accountId: account.accountId
1488
1186
  });
1489
- const message = stripMarkdown(runtime.channel.text.convertMarkdownTables(sanitizedText, tableMode));
1490
- if (message) await reply(message);
1187
+ await reply(runtime.channel.text.convertMarkdownTables(outboundText, tableMode));
1491
1188
  },
1492
1189
  onRecordError: (err) => {
1493
1190
  ctx.log?.error?.(`[${account.accountId}] failed recording Nostr inbound session: ${String(err)}`);
@@ -1517,19 +1214,19 @@ const startNostrGatewayAccount = async (ctx) => {
1517
1214
  if (busHandle) metricsSnapshots.set(account.accountId, busHandle.getMetrics());
1518
1215
  }
1519
1216
  });
1217
+ let stopped = false;
1520
1218
  busHandle = bus;
1521
1219
  activeBuses.set(account.accountId, bus);
1522
- ctx.log?.info?.(`[${account.accountId}] Nostr provider started with ${account.relays.length} configured relay(s)`);
1523
- return { stop: async () => {
1524
- await bus.close();
1220
+ ctx.log?.info?.(`[${account.accountId}] Nostr provider started, connected to ${account.relays.length} relay(s)`);
1221
+ return { stop: () => {
1222
+ if (stopped) return;
1223
+ stopped = true;
1224
+ bus.close();
1525
1225
  if (busHandle === bus) busHandle = null;
1526
1226
  if (activeBuses.get(account.accountId) === bus) activeBuses.delete(account.accountId);
1527
1227
  metricsSnapshots.delete(account.accountId);
1528
1228
  ctx.log?.info?.(`[${account.accountId}] Nostr provider stopped`);
1529
1229
  } };
1530
- },
1531
- stop: async (monitor) => {
1532
- await monitor.stop();
1533
1230
  }
1534
1231
  });
1535
1232
  };
@@ -1551,8 +1248,6 @@ const nostrPairingTextAdapter = {
1551
1248
  const nostrOutboundAdapter = {
1552
1249
  deliveryMode: "direct",
1553
1250
  textChunkLimit: 4e3,
1554
- chunker: chunkTextForOutbound,
1555
- sanitizeText: ({ text }) => sanitizeAssistantVisibleText(text),
1556
1251
  deliveryCapabilities: { durableFinal: {
1557
1252
  text: true,
1558
1253
  messageSendingHooks: true
@@ -1567,12 +1262,12 @@ const nostrOutboundAdapter = {
1567
1262
  channel: "nostr",
1568
1263
  accountId: aid
1569
1264
  });
1570
- const message = stripMarkdown(core.channel.text.convertMarkdownTables(text ?? "", tableMode));
1571
- if (!message) throw new Error("Nostr send requires non-empty text after markdown stripping.");
1265
+ const message = core.channel.text.convertMarkdownTables(text ?? "", tableMode);
1572
1266
  const normalizedTo = normalizePubkey(to);
1267
+ await bus.sendDm(normalizedTo, message);
1573
1268
  return attachChannelToResult("nostr", {
1574
1269
  to: normalizedTo,
1575
- messageId: await bus.sendDm(normalizedTo, message)
1270
+ messageId: `nostr-${Date.now()}`
1576
1271
  });
1577
1272
  }
1578
1273
  };
@@ -1606,18 +1301,6 @@ function resolveNostrOutboundSessionRoute(params) {
1606
1301
  }
1607
1302
  //#endregion
1608
1303
  //#region extensions/nostr/src/channel.ts
1609
- const NOSTR_TARGET_HINT = "<npub|hex pubkey|nostr:npub...>";
1610
- function stripNostrTargetPrefix(target) {
1611
- return target.trim().replace(/^nostr:/i, "");
1612
- }
1613
- function normalizeNostrTarget(target) {
1614
- const cleaned = stripNostrTargetPrefix(target);
1615
- try {
1616
- return normalizePubkey(cleaned);
1617
- } catch {
1618
- return target.trim();
1619
- }
1620
- }
1621
1304
  const resolveNostrDmPolicy = createScopedDmSecurityResolver({
1622
1305
  channelKey: "nostr",
1623
1306
  resolvePolicy: (account) => account.config.dmPolicy,
@@ -1625,7 +1308,13 @@ const resolveNostrDmPolicy = createScopedDmSecurityResolver({
1625
1308
  policyPathSuffix: "dmPolicy",
1626
1309
  defaultPolicy: "pairing",
1627
1310
  approveHint: formatPairingApproveHint("nostr"),
1628
- normalizeEntry: normalizeNostrTarget
1311
+ normalizeEntry: (raw) => {
1312
+ try {
1313
+ return normalizePubkey(raw.trim().replace(/^nostr:/i, ""));
1314
+ } catch {
1315
+ return raw.trim();
1316
+ }
1317
+ }
1629
1318
  });
1630
1319
  const nostrConfigAdapter = createTopLevelChannelConfigAdapter({
1631
1320
  sectionKey: "nostr",
@@ -1645,35 +1334,17 @@ const nostrConfigAdapter = createTopLevelChannelConfigAdapter({
1645
1334
  resolveAllowFrom: (account) => account.config.allowFrom,
1646
1335
  formatAllowFrom: (allowFrom) => normalizeStringEntries(allowFrom).map((entry) => {
1647
1336
  if (entry === "*") return "*";
1648
- return normalizeNostrTarget(entry);
1337
+ try {
1338
+ return normalizePubkey(entry);
1339
+ } catch {
1340
+ return entry;
1341
+ }
1649
1342
  }).filter(Boolean)
1650
1343
  });
1651
1344
  const nostrMessageAdapter = createChannelMessageAdapterFromOutbound({
1652
1345
  id: "nostr",
1653
1346
  outbound: nostrOutboundAdapter
1654
1347
  });
1655
- const nostrPluginOutboundAdapter = {
1656
- ...nostrOutboundAdapter,
1657
- resolveTarget: ({ to }) => {
1658
- const trimmed = to?.trim() ?? "";
1659
- if (!trimmed) return {
1660
- ok: false,
1661
- error: missingTargetError("Nostr", NOSTR_TARGET_HINT)
1662
- };
1663
- const normalized = normalizeNostrTarget(trimmed);
1664
- try {
1665
- return {
1666
- ok: true,
1667
- to: normalizePubkey(normalized)
1668
- };
1669
- } catch {
1670
- return {
1671
- ok: false,
1672
- error: /* @__PURE__ */ new Error("Nostr target must be a 64-character hex pubkey or npub value")
1673
- };
1674
- }
1675
- }
1676
- };
1677
1348
  const nostrPlugin = createChatChannelPlugin({
1678
1349
  base: {
1679
1350
  id: "nostr",
@@ -1693,7 +1364,6 @@ const nostrPlugin = createChatChannelPlugin({
1693
1364
  reload: { configPrefixes: ["channels.nostr"] },
1694
1365
  configSchema: buildChannelConfigSchema(NostrConfigSchema),
1695
1366
  setup: nostrSetupAdapter,
1696
- setupContract: nostrSetupContract,
1697
1367
  setupWizard: nostrSetupWizard,
1698
1368
  config: {
1699
1369
  ...nostrConfigAdapter,
@@ -1706,13 +1376,20 @@ const nostrPlugin = createChatChannelPlugin({
1706
1376
  },
1707
1377
  messaging: {
1708
1378
  targetPrefixes: ["nostr"],
1709
- normalizeTarget: normalizeNostrTarget,
1379
+ normalizeTarget: (target) => {
1380
+ const cleaned = target.trim().replace(/^nostr:/i, "");
1381
+ try {
1382
+ return normalizePubkey(cleaned);
1383
+ } catch {
1384
+ return cleaned;
1385
+ }
1386
+ },
1710
1387
  targetResolver: {
1711
- looksLikeId: (input, normalized) => {
1712
- const trimmed = normalized?.trim() || stripNostrTargetPrefix(input);
1713
- return trimmed.startsWith("npub1") || trimmed.startsWith("NPUB1") || /^[0-9a-fA-F]{64}$/.test(trimmed);
1388
+ looksLikeId: (input) => {
1389
+ const trimmed = input.trim();
1390
+ return trimmed.startsWith("npub1") || /^[0-9a-fA-F]{64}$/.test(trimmed);
1714
1391
  },
1715
- hint: NOSTR_TARGET_HINT
1392
+ hint: "<npub|hex pubkey|nostr:npub...>"
1716
1393
  },
1717
1394
  resolveOutboundSessionRoute: (params) => resolveNostrOutboundSessionRoute(params)
1718
1395
  },
@@ -1737,7 +1414,7 @@ const nostrPlugin = createChatChannelPlugin({
1737
1414
  },
1738
1415
  pairing: { text: nostrPairingTextAdapter },
1739
1416
  security: { resolveDmPolicy: resolveNostrDmPolicy },
1740
- outbound: nostrPluginOutboundAdapter
1417
+ outbound: nostrOutboundAdapter
1741
1418
  });
1742
1419
  /**
1743
1420
  * Publish a profile (kind:0) for a Nostr account.
@@ -1763,4 +1440,4 @@ async function getNostrProfileState(accountId = DEFAULT_ACCOUNT_ID) {
1763
1440
  return bus.getProfileState();
1764
1441
  }
1765
1442
  //#endregion
1766
- export { getNostrRuntime as a, contentToProfile as i, nostrPlugin as n, setNostrRuntime as o, publishNostrProfile as r, getNostrProfileState as t };
1443
+ export { setNostrRuntime as a, getNostrRuntime as i, nostrPlugin as n, contentToProfile as o, publishNostrProfile as r, getNostrProfileState as t };