@openclaw/nostr 2026.7.2-beta.7 → 2026.7.34
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/{channel-O6-jXoT5.js → .setup/channel-B4SX7Iq6.mjs} +554 -877
- package/dist/{config-schema-CelPzg1N.js → .setup/config-schema-DIiXiBKr.mjs} +1 -1
- package/dist/.setup/inbound-direct-dm-runtime-EdLLVtkh.mjs +2 -0
- package/dist/{setup-adapter-BjKmSwdW.js → .setup/setup-adapter-DEU3o8MF.mjs} +1 -31
- package/dist/.setup/setup-plugin-api-DzlCumkz.mjs +2 -0
- package/dist/.setup/setup-plugin-api-csfJrRxD.mjs +111 -0
- package/dist/{setup-surface-BJCFpe02.js → .setup/setup-surface-DWa6-9ku.mjs} +70 -41
- package/dist/api.js +72 -61
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +3 -2
- package/dist/setup-api.js +1 -1
- package/dist/setup-entry.js +2 -1
- package/dist/setup-plugin-api.js +1 -112
- package/node_modules/nostr-tools/lib/cjs/abstract-pool.js +28 -59
- package/node_modules/nostr-tools/lib/cjs/abstract-pool.js.map +3 -3
- package/node_modules/nostr-tools/lib/cjs/abstract-relay.js +7 -44
- package/node_modules/nostr-tools/lib/cjs/abstract-relay.js.map +2 -2
- package/node_modules/nostr-tools/lib/cjs/core.js +1 -17
- package/node_modules/nostr-tools/lib/cjs/core.js.map +3 -3
- package/node_modules/nostr-tools/lib/cjs/filter.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/index.js +130 -172
- package/node_modules/nostr-tools/lib/cjs/index.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/kinds.js +1 -17
- package/node_modules/nostr-tools/lib/cjs/kinds.js.map +3 -3
- package/node_modules/nostr-tools/lib/cjs/nip04.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/nip10.js +5 -20
- package/node_modules/nostr-tools/lib/cjs/nip10.js.map +3 -3
- package/node_modules/nostr-tools/lib/cjs/nip13.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/nip17.js +23 -30
- package/node_modules/nostr-tools/lib/cjs/nip17.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/nip18.js +14 -21
- package/node_modules/nostr-tools/lib/cjs/nip18.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/nip19.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/nip21.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/nip22.js +7 -22
- package/node_modules/nostr-tools/lib/cjs/nip22.js.map +3 -3
- package/node_modules/nostr-tools/lib/cjs/nip25.js +14 -21
- package/node_modules/nostr-tools/lib/cjs/nip25.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/nip27.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/nip28.js +14 -21
- package/node_modules/nostr-tools/lib/cjs/nip28.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/nip29.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/nip30.js.map +2 -2
- package/node_modules/nostr-tools/lib/cjs/nip42.js.map +2 -2
- package/node_modules/nostr-tools/lib/cjs/nip44.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/nip45.js +9 -22
- package/node_modules/nostr-tools/lib/cjs/nip45.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/nip46.js +70 -111
- package/node_modules/nostr-tools/lib/cjs/nip46.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/nip47.js +18 -25
- package/node_modules/nostr-tools/lib/cjs/nip47.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/nip57.js +16 -23
- package/node_modules/nostr-tools/lib/cjs/nip57.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/nip58.js.map +2 -2
- package/node_modules/nostr-tools/lib/cjs/nip59.js +7 -16
- package/node_modules/nostr-tools/lib/cjs/nip59.js.map +3 -3
- package/node_modules/nostr-tools/lib/cjs/nip75.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/nip94.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/nip98.js +16 -23
- package/node_modules/nostr-tools/lib/cjs/nip98.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/nip99.js.map +2 -2
- package/node_modules/nostr-tools/lib/cjs/nipb0.js +17 -38
- package/node_modules/nostr-tools/lib/cjs/nipb0.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/nipb7.js +7 -13
- package/node_modules/nostr-tools/lib/cjs/nipb7.js.map +2 -2
- package/node_modules/nostr-tools/lib/cjs/pool.js +61 -102
- package/node_modules/nostr-tools/lib/cjs/pool.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/pure.js +14 -21
- package/node_modules/nostr-tools/lib/cjs/pure.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/references.js.map +1 -1
- package/node_modules/nostr-tools/lib/cjs/relay.js +42 -86
- package/node_modules/nostr-tools/lib/cjs/relay.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/signer.js +14 -21
- package/node_modules/nostr-tools/lib/cjs/signer.js.map +4 -4
- package/node_modules/nostr-tools/lib/cjs/utils.js +0 -10
- package/node_modules/nostr-tools/lib/cjs/utils.js.map +2 -2
- package/node_modules/nostr-tools/lib/cjs/wasm.js +2 -16
- package/node_modules/nostr-tools/lib/cjs/wasm.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/abstract-pool.js +28 -59
- package/node_modules/nostr-tools/lib/esm/abstract-pool.js.map +3 -3
- package/node_modules/nostr-tools/lib/esm/abstract-relay.js +7 -44
- package/node_modules/nostr-tools/lib/esm/abstract-relay.js.map +2 -2
- package/node_modules/nostr-tools/lib/esm/core.js +1 -15
- package/node_modules/nostr-tools/lib/esm/core.js.map +3 -3
- package/node_modules/nostr-tools/lib/esm/filter.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/index.js +80 -122
- package/node_modules/nostr-tools/lib/esm/index.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/kinds.js +1 -17
- package/node_modules/nostr-tools/lib/esm/kinds.js.map +3 -3
- package/node_modules/nostr-tools/lib/esm/nip04.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/nip10.js +5 -18
- package/node_modules/nostr-tools/lib/esm/nip10.js.map +3 -3
- package/node_modules/nostr-tools/lib/esm/nip13.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/nip17.js +8 -15
- package/node_modules/nostr-tools/lib/esm/nip17.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/nip18.js +8 -15
- package/node_modules/nostr-tools/lib/esm/nip18.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/nip19.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/nip21.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/nip22.js +7 -20
- package/node_modules/nostr-tools/lib/esm/nip22.js.map +3 -3
- package/node_modules/nostr-tools/lib/esm/nip25.js +8 -15
- package/node_modules/nostr-tools/lib/esm/nip25.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/nip27.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/nip28.js +8 -15
- package/node_modules/nostr-tools/lib/esm/nip28.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/nip29.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/nip30.js.map +2 -2
- package/node_modules/nostr-tools/lib/esm/nip42.js.map +2 -2
- package/node_modules/nostr-tools/lib/esm/nip44.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/nip45.js +10 -23
- package/node_modules/nostr-tools/lib/esm/nip45.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/nip46.js +54 -95
- package/node_modules/nostr-tools/lib/esm/nip46.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/nip47.js +8 -15
- package/node_modules/nostr-tools/lib/esm/nip47.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/nip57.js +10 -17
- package/node_modules/nostr-tools/lib/esm/nip57.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/nip58.js.map +2 -2
- package/node_modules/nostr-tools/lib/esm/nip59.js +1 -10
- package/node_modules/nostr-tools/lib/esm/nip59.js.map +3 -3
- package/node_modules/nostr-tools/lib/esm/nip75.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/nip94.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/nip98.js +8 -15
- package/node_modules/nostr-tools/lib/esm/nip98.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/nip99.js.map +2 -2
- package/node_modules/nostr-tools/lib/esm/nipb0.js +18 -39
- package/node_modules/nostr-tools/lib/esm/nipb0.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/nipb7.js +7 -13
- package/node_modules/nostr-tools/lib/esm/nipb7.js.map +2 -2
- package/node_modules/nostr-tools/lib/esm/pool.js +54 -95
- package/node_modules/nostr-tools/lib/esm/pool.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/pure.js +8 -15
- package/node_modules/nostr-tools/lib/esm/pure.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/references.js.map +1 -1
- package/node_modules/nostr-tools/lib/esm/relay.js +36 -80
- package/node_modules/nostr-tools/lib/esm/relay.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/signer.js +8 -15
- package/node_modules/nostr-tools/lib/esm/signer.js.map +4 -4
- package/node_modules/nostr-tools/lib/esm/utils.js +0 -10
- package/node_modules/nostr-tools/lib/esm/utils.js.map +2 -2
- package/node_modules/nostr-tools/lib/esm/wasm.js +3 -17
- package/node_modules/nostr-tools/lib/esm/wasm.js.map +4 -4
- package/node_modules/nostr-tools/lib/nostr.bundle.js +77 -121
- package/node_modules/nostr-tools/lib/nostr.bundle.js.map +4 -4
- package/node_modules/nostr-tools/lib/types/abstract-pool.d.ts +4 -7
- package/node_modules/nostr-tools/lib/types/abstract-relay.d.ts +2 -7
- package/node_modules/nostr-tools/lib/types/helpers.d.ts +2 -0
- package/node_modules/nostr-tools/lib/types/kinds.d.ts +0 -2
- package/node_modules/nostr-tools/lib/types/nipb0.d.ts +1 -0
- package/node_modules/nostr-tools/lib/types/nipb7.d.ts +1 -0
- package/node_modules/nostr-tools/lib/types/relay.d.ts +2 -2
- package/node_modules/nostr-tools/lib/types/utils.d.ts +0 -4
- package/node_modules/nostr-tools/package.json +1 -1
- package/openclaw.plugin.json +5 -0
- package/package.json +15 -41
- package/dist/inbound-direct-dm-runtime-DHn9NoYN.js +0 -2
- package/node_modules/nostr-tools/lib/types/nipb0.test.d.ts +0 -1
- /package/dist/{state-account-id-CvBZ9s6P.js → .setup/state-account-id-CvBZ9s6P.mjs} +0 -0
|
@@ -1,82 +1,60 @@
|
|
|
1
|
-
import { a as
|
|
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-
|
|
3
|
-
import {
|
|
4
|
-
import { t as normalizeNostrStateAccountId } from "./state-account-id-CvBZ9s6P.
|
|
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 {
|
|
9
|
-
import {
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
90
|
+
eventsReceived += value;
|
|
113
91
|
break;
|
|
114
92
|
case "event.processed":
|
|
115
|
-
|
|
93
|
+
eventsProcessed += value;
|
|
116
94
|
break;
|
|
117
95
|
case "event.duplicate":
|
|
118
|
-
|
|
96
|
+
eventsDuplicate += value;
|
|
119
97
|
break;
|
|
120
98
|
case "event.rejected.invalid_shape":
|
|
121
|
-
|
|
99
|
+
eventsRejected.invalidShape += value;
|
|
122
100
|
break;
|
|
123
101
|
case "event.rejected.wrong_kind":
|
|
124
|
-
|
|
102
|
+
eventsRejected.wrongKind += value;
|
|
125
103
|
break;
|
|
126
104
|
case "event.rejected.stale":
|
|
127
|
-
|
|
105
|
+
eventsRejected.stale += value;
|
|
128
106
|
break;
|
|
129
107
|
case "event.rejected.future":
|
|
130
|
-
|
|
108
|
+
eventsRejected.future += value;
|
|
131
109
|
break;
|
|
132
110
|
case "event.rejected.rate_limited":
|
|
133
|
-
|
|
111
|
+
eventsRejected.rateLimited += value;
|
|
134
112
|
break;
|
|
135
113
|
case "event.rejected.invalid_signature":
|
|
136
|
-
|
|
114
|
+
eventsRejected.invalidSignature += value;
|
|
137
115
|
break;
|
|
138
116
|
case "event.rejected.oversized_ciphertext":
|
|
139
|
-
|
|
117
|
+
eventsRejected.oversizedCiphertext += value;
|
|
140
118
|
break;
|
|
141
119
|
case "event.rejected.oversized_plaintext":
|
|
142
|
-
|
|
120
|
+
eventsRejected.oversizedPlaintext += value;
|
|
143
121
|
break;
|
|
144
122
|
case "event.rejected.decrypt_failed":
|
|
145
|
-
|
|
123
|
+
eventsRejected.decryptFailed += value;
|
|
146
124
|
break;
|
|
147
125
|
case "event.rejected.self_message":
|
|
148
|
-
|
|
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
|
-
|
|
176
|
+
rateLimiting.perSenderHits += value;
|
|
199
177
|
break;
|
|
200
178
|
case "rate_limit.global":
|
|
201
|
-
|
|
179
|
+
rateLimiting.globalHits += value;
|
|
202
180
|
break;
|
|
203
181
|
case "decrypt.success":
|
|
204
|
-
|
|
182
|
+
decrypt.success += value;
|
|
205
183
|
break;
|
|
206
184
|
case "decrypt.failure":
|
|
207
|
-
|
|
185
|
+
decrypt.failure += value;
|
|
208
186
|
break;
|
|
209
187
|
case "memory.seen_tracker_size":
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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([
|
|
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/
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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 (
|
|
1226
|
-
|
|
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
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1042
|
+
return;
|
|
1345
1043
|
} catch (err) {
|
|
1346
|
-
lastError = 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
|
|
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
|
|
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 {
|
|
1463
|
-
await
|
|
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
|
|
1483
|
-
if (!
|
|
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
|
-
|
|
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
|
|
1523
|
-
return { stop:
|
|
1524
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
1712
|
-
const trimmed =
|
|
1713
|
-
return trimmed.startsWith("npub1") ||
|
|
1388
|
+
looksLikeId: (input) => {
|
|
1389
|
+
const trimmed = input.trim();
|
|
1390
|
+
return trimmed.startsWith("npub1") || /^[0-9a-fA-F]{64}$/.test(trimmed);
|
|
1714
1391
|
},
|
|
1715
|
-
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:
|
|
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 {
|
|
1443
|
+
export { setNostrRuntime as a, getNostrRuntime as i, nostrPlugin as n, contentToProfile as o, publishNostrProfile as r, getNostrProfileState as t };
|