@orangecheck/nostr-core 0.1.1 → 0.2.0
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/index.d.mts +7 -10
- package/dist/index.d.ts +7 -10
- package/dist/index.js +29 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.test.ts +126 -4
- package/src/index.ts +90 -21
package/dist/index.d.mts
CHANGED
|
@@ -14,19 +14,15 @@ interface PublishResult {
|
|
|
14
14
|
reason?: string;
|
|
15
15
|
attempts: number;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
type IndexableTagName = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';
|
|
18
|
+
type IndexableTagFilter = `#${IndexableTagName}`;
|
|
19
|
+
interface Filter extends Partial<Record<IndexableTagFilter, string[]>> {
|
|
18
20
|
kinds?: number[];
|
|
19
21
|
authors?: string[];
|
|
20
22
|
ids?: string[];
|
|
21
23
|
limit?: number;
|
|
22
24
|
since?: number;
|
|
23
25
|
until?: number;
|
|
24
|
-
'#d'?: string[];
|
|
25
|
-
'#t'?: string[];
|
|
26
|
-
'#poll_id'?: string[];
|
|
27
|
-
'#voter'?: string[];
|
|
28
|
-
'#creator'?: string[];
|
|
29
|
-
[key: `#${string}`]: string[] | undefined;
|
|
30
26
|
}
|
|
31
27
|
interface QueryResult {
|
|
32
28
|
events: NostrEvent[];
|
|
@@ -37,7 +33,8 @@ interface QueryResult {
|
|
|
37
33
|
events: number;
|
|
38
34
|
}[];
|
|
39
35
|
}
|
|
40
|
-
|
|
41
|
-
declare function
|
|
36
|
+
type AuthSigner = (challenge: string, relayUrl: string) => NostrEvent | Promise<NostrEvent>;
|
|
37
|
+
declare function publishEvent(event: NostrEvent, relays?: readonly string[], timeoutMs?: number, auth?: AuthSigner): Promise<PublishResult[]>;
|
|
38
|
+
declare function queryEvents(filter: Filter, relays?: readonly string[], timeoutMs?: number, auth?: AuthSigner): Promise<QueryResult>;
|
|
42
39
|
|
|
43
|
-
export { DEFAULT_RELAYS, type Filter, type NostrEvent, type PublishResult, type QueryResult, publishEvent, queryEvents };
|
|
40
|
+
export { type AuthSigner, DEFAULT_RELAYS, type Filter, type IndexableTagFilter, type NostrEvent, type PublishResult, type QueryResult, publishEvent, queryEvents };
|
package/dist/index.d.ts
CHANGED
|
@@ -14,19 +14,15 @@ interface PublishResult {
|
|
|
14
14
|
reason?: string;
|
|
15
15
|
attempts: number;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
type IndexableTagName = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';
|
|
18
|
+
type IndexableTagFilter = `#${IndexableTagName}`;
|
|
19
|
+
interface Filter extends Partial<Record<IndexableTagFilter, string[]>> {
|
|
18
20
|
kinds?: number[];
|
|
19
21
|
authors?: string[];
|
|
20
22
|
ids?: string[];
|
|
21
23
|
limit?: number;
|
|
22
24
|
since?: number;
|
|
23
25
|
until?: number;
|
|
24
|
-
'#d'?: string[];
|
|
25
|
-
'#t'?: string[];
|
|
26
|
-
'#poll_id'?: string[];
|
|
27
|
-
'#voter'?: string[];
|
|
28
|
-
'#creator'?: string[];
|
|
29
|
-
[key: `#${string}`]: string[] | undefined;
|
|
30
26
|
}
|
|
31
27
|
interface QueryResult {
|
|
32
28
|
events: NostrEvent[];
|
|
@@ -37,7 +33,8 @@ interface QueryResult {
|
|
|
37
33
|
events: number;
|
|
38
34
|
}[];
|
|
39
35
|
}
|
|
40
|
-
|
|
41
|
-
declare function
|
|
36
|
+
type AuthSigner = (challenge: string, relayUrl: string) => NostrEvent | Promise<NostrEvent>;
|
|
37
|
+
declare function publishEvent(event: NostrEvent, relays?: readonly string[], timeoutMs?: number, auth?: AuthSigner): Promise<PublishResult[]>;
|
|
38
|
+
declare function queryEvents(filter: Filter, relays?: readonly string[], timeoutMs?: number, auth?: AuthSigner): Promise<QueryResult>;
|
|
42
39
|
|
|
43
|
-
export { DEFAULT_RELAYS, type Filter, type NostrEvent, type PublishResult, type QueryResult, publishEvent, queryEvents };
|
|
40
|
+
export { type AuthSigner, DEFAULT_RELAYS, type Filter, type IndexableTagFilter, type NostrEvent, type PublishResult, type QueryResult, publishEvent, queryEvents };
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ function parseFrame(raw) {
|
|
|
17
17
|
const arr = JSON.parse(raw);
|
|
18
18
|
if (!Array.isArray(arr) || arr.length === 0) return null;
|
|
19
19
|
const type = arr[0];
|
|
20
|
-
if (type === "OK" || type === "EVENT" || type === "EOSE" || type === "NOTICE" || type === "CLOSED") {
|
|
20
|
+
if (type === "OK" || type === "EVENT" || type === "EOSE" || type === "NOTICE" || type === "CLOSED" || type === "AUTH") {
|
|
21
21
|
return { type, payload: arr.slice(1) };
|
|
22
22
|
}
|
|
23
23
|
return null;
|
|
@@ -34,13 +34,13 @@ var DEFAULT_RETRY = {
|
|
|
34
34
|
function delay(ms) {
|
|
35
35
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
36
|
}
|
|
37
|
-
async function publishOne(url, event, retry) {
|
|
37
|
+
async function publishOne(url, event, retry, auth) {
|
|
38
38
|
let attempts = 0;
|
|
39
39
|
let backoff = retry.initialBackoffMs;
|
|
40
40
|
let lastReason;
|
|
41
41
|
while (attempts < retry.attempts) {
|
|
42
42
|
attempts++;
|
|
43
|
-
const attempt = await attemptPublish(url, event, retry.timeoutMs);
|
|
43
|
+
const attempt = await attemptPublish(url, event, retry.timeoutMs, auth);
|
|
44
44
|
if (attempt.ok) {
|
|
45
45
|
return {
|
|
46
46
|
relay: url,
|
|
@@ -64,9 +64,10 @@ async function publishOne(url, event, retry) {
|
|
|
64
64
|
...lastReason ? { reason: lastReason } : {}
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
function attemptPublish(url, event, timeoutMs) {
|
|
67
|
+
function attemptPublish(url, event, timeoutMs, auth) {
|
|
68
68
|
return new Promise((resolve) => {
|
|
69
69
|
let settled = false;
|
|
70
|
+
let authed = false;
|
|
70
71
|
let ws = null;
|
|
71
72
|
const timer = setTimeout(() => {
|
|
72
73
|
if (settled) return;
|
|
@@ -83,6 +84,16 @@ function attemptPublish(url, event, timeoutMs) {
|
|
|
83
84
|
ws.onmessage = (msg) => {
|
|
84
85
|
const frame = parseFrame(msg.data);
|
|
85
86
|
if (!frame) return;
|
|
87
|
+
if (frame.type === "AUTH" && auth && !authed) {
|
|
88
|
+
authed = true;
|
|
89
|
+
const challenge = String(frame.payload[0] ?? "");
|
|
90
|
+
void Promise.resolve(auth(challenge, url)).then((authEvent) => {
|
|
91
|
+
ws?.send(JSON.stringify(["AUTH", authEvent]));
|
|
92
|
+
ws?.send(JSON.stringify(["EVENT", event]));
|
|
93
|
+
}).catch(() => {
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
86
97
|
if (frame.type === "OK" && frame.payload[0] === event.id) {
|
|
87
98
|
if (settled) return;
|
|
88
99
|
settled = true;
|
|
@@ -124,11 +135,11 @@ function attemptPublish(url, event, timeoutMs) {
|
|
|
124
135
|
}
|
|
125
136
|
});
|
|
126
137
|
}
|
|
127
|
-
async function publishEvent(event, relays = DEFAULT_RELAYS, timeoutMs = 5e3) {
|
|
138
|
+
async function publishEvent(event, relays = DEFAULT_RELAYS, timeoutMs = 5e3, auth) {
|
|
128
139
|
const retry = { ...DEFAULT_RETRY, timeoutMs };
|
|
129
|
-
return Promise.all(relays.map((r) => publishOne(r, event, retry)));
|
|
140
|
+
return Promise.all(relays.map((r) => publishOne(r, event, retry, auth)));
|
|
130
141
|
}
|
|
131
|
-
async function queryEvents(filter, relays = DEFAULT_RELAYS, timeoutMs = 1500) {
|
|
142
|
+
async function queryEvents(filter, relays = DEFAULT_RELAYS, timeoutMs = 1500, auth) {
|
|
132
143
|
const subId = "ocnc-" + Math.random().toString(36).slice(2, 10);
|
|
133
144
|
const byId = /* @__PURE__ */ new Map();
|
|
134
145
|
const status = [];
|
|
@@ -136,6 +147,7 @@ async function queryEvents(filter, relays = DEFAULT_RELAYS, timeoutMs = 1500) {
|
|
|
136
147
|
relays.map(
|
|
137
148
|
(url) => new Promise((resolve) => {
|
|
138
149
|
let settled = false;
|
|
150
|
+
let authed = false;
|
|
139
151
|
let count = 0;
|
|
140
152
|
let reason;
|
|
141
153
|
let ws = null;
|
|
@@ -160,6 +172,16 @@ async function queryEvents(filter, relays = DEFAULT_RELAYS, timeoutMs = 1500) {
|
|
|
160
172
|
ws.onmessage = (msg) => {
|
|
161
173
|
const frame = parseFrame(msg.data);
|
|
162
174
|
if (!frame) return;
|
|
175
|
+
if (frame.type === "AUTH" && auth && !authed) {
|
|
176
|
+
authed = true;
|
|
177
|
+
const challenge = String(frame.payload[0] ?? "");
|
|
178
|
+
void Promise.resolve(auth(challenge, url)).then((authEvent) => {
|
|
179
|
+
ws?.send(JSON.stringify(["AUTH", authEvent]));
|
|
180
|
+
ws?.send(JSON.stringify(["REQ", subId, filter]));
|
|
181
|
+
}).catch(() => {
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
163
185
|
if (frame.type === "EVENT" && frame.payload[0] === subId) {
|
|
164
186
|
const event = frame.payload[1];
|
|
165
187
|
if (event && event.id) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @orangecheck/nostr-core\n *\n * Browser-compatible Nostr client used by every OrangeCheck family web\n * app. Raw NIP-01 over WebSocket against a list of relays. Every operation\n * races all relays in parallel and reports per-relay status so the caller\n * can distinguish \"nobody replied\" from \"one relay rejected.\" Retries with\n * exponential backoff on transport errors only.\n *\n * No dependencies — uses the platform `WebSocket` global. Works in any\n * runtime that ships a WHATWG WebSocket (browser, Node 22+, Deno, Bun,\n * Cloudflare Workers).\n *\n * Source-of-truth `DEFAULT_RELAYS` for the OC family. Co-publishes to four\n * public relays plus `wss://relay.ochk.io` (the family's first-party\n * kind-allowlisted relay — see https://github.com/orangecheck/oc-relay-infra).\n *\n * **Hard invariant:** `DEFAULT_RELAYS` MUST contain at least two entries,\n * and MUST NOT be `relay.ochk.io` alone. Enforced at the type level — a\n * future engineer simplifying to ours-only fails `tsc`. See `_validate`\n * below.\n */\n\n// ─────────────────────────────────────────────────────────────────────────\n// Build-time invariants + default relay set.\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Family-relay invariants applied to `DEFAULT_RELAYS`. The relay set must:\n * 1. Contain at least two relays — single-relay defaults are always wrong\n * because the family's BYPASS principle requires public-relay co-publish.\n * 2. Not be `wss://relay.ochk.io` alone — relay.ochk.io is additive, never\n * a single point of failure. See oc-relay-infra/BYPASS.md.\n *\n * If `T` violates either rule, this resolves to `never` and the assignment\n * below fails at `tsc` time.\n */\ntype ValidRelaySet<T extends readonly string[]> =\n T['length'] extends 0 | 1 ? never :\n T extends readonly ['wss://relay.ochk.io'] ? never :\n T;\n\nconst _RELAYS: ValidRelaySet<readonly [\n 'wss://relay.nostr.band',\n 'wss://nos.lol',\n 'wss://relay.primal.net',\n 'wss://offchain.pub',\n 'wss://relay.ochk.io',\n]> = [\n 'wss://relay.nostr.band',\n 'wss://nos.lol',\n 'wss://relay.primal.net',\n 'wss://offchain.pub',\n // First-party family relay — kind allowlist 30078–30086 + canonical\n // OC d-tag prefixes. Always co-published with public relays; never\n // the only copy. See https://github.com/orangecheck/oc-relay-infra.\n 'wss://relay.ochk.io',\n] as const;\n\n/**\n * Default relay set for OrangeCheck family Nostr publishes + queries.\n *\n * Frozen at runtime; consumers MAY pass an explicit `relays` arg to any\n * function in this package to override.\n *\n * @example\n * ```ts\n * import { DEFAULT_RELAYS, publishEvent } from '@orangecheck/nostr-core';\n *\n * const results = await publishEvent(myEvent);\n * // → publishes to all 5 relays in DEFAULT_RELAYS in parallel\n * console.log(`accepted on ${results.filter(r => r.ok).length}/${results.length}`);\n * ```\n */\nexport const DEFAULT_RELAYS: readonly string[] = Object.freeze([..._RELAYS]);\n\n// ─────────────────────────────────────────────────────────────────────────\n// Wire types — NIP-01 event + filter + result shapes.\n// ─────────────────────────────────────────────────────────────────────────\n\nexport interface NostrEvent {\n id: string;\n kind: number;\n pubkey: string;\n created_at: number;\n content: string;\n tags: string[][];\n sig: string;\n}\n\nexport interface PublishResult {\n relay: string;\n ok: boolean;\n reason?: string;\n attempts: number;\n}\n\nexport interface Filter {\n kinds?: number[];\n authors?: string[];\n ids?: string[];\n limit?: number;\n since?: number;\n until?: number;\n /** NIP-12 indexable `d`-tag filter. */\n '#d'?: string[];\n /** NIP-12 indexable single-letter tag filter. */\n '#t'?: string[];\n /** Used by OC Vote (kind 30081 ballots). */\n '#poll_id'?: string[];\n /** Used by OC Vote (kind 30081 ballots). */\n '#voter'?: string[];\n /** Used by OC Vote (kind 30080 polls). */\n '#creator'?: string[];\n /** Other indexable single-letter tags clients may filter on. */\n [key: `#${string}`]: string[] | undefined;\n}\n\nexport interface QueryResult {\n events: NostrEvent[];\n relayStatus: { relay: string; ok: boolean; reason?: string; events: number }[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Internal — frame parsing + retry config.\n// ─────────────────────────────────────────────────────────────────────────\n\ntype FrameType = 'OK' | 'EVENT' | 'EOSE' | 'NOTICE' | 'CLOSED';\ninterface RelayFrame {\n type: FrameType;\n payload: unknown[];\n}\n\nfunction parseFrame(raw: string): RelayFrame | null {\n try {\n const arr = JSON.parse(raw) as unknown[];\n if (!Array.isArray(arr) || arr.length === 0) return null;\n const type = arr[0];\n if (\n type === 'OK' ||\n type === 'EVENT' ||\n type === 'EOSE' ||\n type === 'NOTICE' ||\n type === 'CLOSED'\n ) {\n return { type: type as FrameType, payload: arr.slice(1) };\n }\n return null;\n } catch {\n return null;\n }\n}\n\ninterface RetryOptions {\n attempts: number;\n timeoutMs: number;\n initialBackoffMs: number;\n maxBackoffMs: number;\n}\n\nconst DEFAULT_RETRY: RetryOptions = {\n attempts: 3,\n timeoutMs: 5000,\n initialBackoffMs: 500,\n maxBackoffMs: 4000,\n};\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Publish — write an event to one or more relays in parallel, with retry.\n// ─────────────────────────────────────────────────────────────────────────\n\nasync function publishOne(\n url: string,\n event: NostrEvent,\n retry: RetryOptions\n): Promise<PublishResult> {\n let attempts = 0;\n let backoff = retry.initialBackoffMs;\n let lastReason: string | undefined;\n\n while (attempts < retry.attempts) {\n attempts++;\n const attempt = await attemptPublish(url, event, retry.timeoutMs);\n if (attempt.ok) {\n return {\n relay: url,\n ok: true,\n attempts,\n ...(attempt.reason ? { reason: attempt.reason } : {}),\n };\n }\n lastReason = attempt.reason;\n if (attempt.retryable && attempts < retry.attempts) {\n await delay(backoff);\n backoff = Math.min(backoff * 2, retry.maxBackoffMs);\n continue;\n }\n break;\n }\n return {\n relay: url,\n ok: false,\n attempts,\n ...(lastReason ? { reason: lastReason } : {}),\n };\n}\n\nfunction attemptPublish(\n url: string,\n event: NostrEvent,\n timeoutMs: number\n): Promise<{ ok: boolean; reason?: string; retryable: boolean }> {\n return new Promise((resolve) => {\n let settled = false;\n let ws: WebSocket | null = null;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n try {\n ws?.close();\n } catch {}\n resolve({ ok: false, reason: 'timeout', retryable: true });\n }, timeoutMs);\n try {\n ws = new WebSocket(url);\n ws.onopen = () => ws?.send(JSON.stringify(['EVENT', event]));\n ws.onmessage = (msg) => {\n const frame = parseFrame(msg.data as string);\n if (!frame) return;\n if (frame.type === 'OK' && frame.payload[0] === event.id) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n try {\n ws?.close();\n } catch {}\n const ok = frame.payload[1] === true;\n const reason = frame.payload[2] as string | undefined;\n resolve({\n ok,\n ...(reason ? { reason } : {}),\n retryable: false,\n });\n }\n };\n ws.onerror = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({ ok: false, reason: 'websocket_error', retryable: true });\n };\n ws.onclose = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({ ok: false, reason: 'closed_early', retryable: true });\n };\n } catch (err) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({\n ok: false,\n reason: err instanceof Error ? err.message : 'unknown',\n retryable: true,\n });\n }\n });\n}\n\n/**\n * Publish a NIP-01 event to all `relays` in parallel. Returns one\n * `PublishResult` per relay. Default timeout 5000ms.\n */\nexport async function publishEvent(\n event: NostrEvent,\n relays: readonly string[] = DEFAULT_RELAYS,\n timeoutMs = 5000\n): Promise<PublishResult[]> {\n const retry: RetryOptions = { ...DEFAULT_RETRY, timeoutMs };\n return Promise.all(relays.map((r) => publishOne(r, event, retry)));\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Query — REQ → EOSE → close. Races all relays; first to EOSE wins.\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Issue a NIP-01 REQ across all `relays` in parallel. Returns deduplicated\n * events sorted newest-first plus per-relay status.\n *\n * Default timeout 1500ms — short enough that a momentary blip on any one\n * relay (including relay.ochk.io) never holds up the racing reads. Pass an\n * explicit `timeoutMs` for slow filters or for use cases where waiting on\n * the slowest relay matters.\n */\nexport async function queryEvents(\n filter: Filter,\n relays: readonly string[] = DEFAULT_RELAYS,\n timeoutMs = 1500\n): Promise<QueryResult> {\n const subId = 'ocnc-' + Math.random().toString(36).slice(2, 10);\n const byId = new Map<string, NostrEvent>();\n const status: QueryResult['relayStatus'] = [];\n\n await Promise.all(\n relays.map(\n (url) =>\n new Promise<void>((resolve) => {\n let settled = false;\n let count = 0;\n let reason: string | undefined;\n let ws: WebSocket | null = null;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n try {\n ws?.close();\n } catch {}\n status.push({\n relay: url,\n ok: count > 0,\n reason: reason ?? 'timeout',\n events: count,\n });\n resolve();\n }, timeoutMs);\n try {\n ws = new WebSocket(url);\n ws.onopen = () => ws?.send(JSON.stringify(['REQ', subId, filter]));\n ws.onmessage = (msg) => {\n const frame = parseFrame(msg.data as string);\n if (!frame) return;\n if (frame.type === 'EVENT' && frame.payload[0] === subId) {\n const event = frame.payload[1] as NostrEvent | undefined;\n if (event && event.id) {\n byId.set(event.id, event);\n count++;\n }\n } else if (frame.type === 'EOSE' && frame.payload[0] === subId) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n try {\n ws?.send(JSON.stringify(['CLOSE', subId]));\n ws?.close();\n } catch {}\n status.push({ relay: url, ok: true, events: count });\n resolve();\n } else if (frame.type === 'NOTICE') {\n reason = String(frame.payload[0] ?? 'notice');\n }\n };\n ws.onerror = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: false,\n reason: 'ws_error',\n events: count,\n });\n resolve();\n };\n ws.onclose = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: count > 0,\n reason: reason ?? 'closed_early',\n events: count,\n });\n resolve();\n };\n } catch (err) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: false,\n reason: err instanceof Error ? err.message : 'unknown',\n events: count,\n });\n resolve();\n }\n })\n )\n );\n\n return {\n events: Array.from(byId.values()).sort((a, b) => b.created_at - a.created_at),\n relayStatus: status,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @orangecheck/nostr-core\n *\n * Browser-compatible Nostr client used by every OrangeCheck family web\n * app. Raw NIP-01 over WebSocket against a list of relays. Every operation\n * races all relays in parallel and reports per-relay status so the caller\n * can distinguish \"nobody replied\" from \"one relay rejected.\" Retries with\n * exponential backoff on transport errors only.\n *\n * No dependencies — uses the platform `WebSocket` global. Works in any\n * runtime that ships a WHATWG WebSocket (browser, Node 22+, Deno, Bun,\n * Cloudflare Workers).\n *\n * Source-of-truth `DEFAULT_RELAYS` for the OC family. Co-publishes to four\n * public relays plus `wss://relay.ochk.io` (the family's first-party\n * kind-allowlisted relay — see https://github.com/orangecheck/oc-relay-infra).\n *\n * **Hard invariant:** `DEFAULT_RELAYS` MUST contain at least two entries,\n * and MUST NOT be `relay.ochk.io` alone. Enforced at the type level — a\n * future engineer simplifying to ours-only fails `tsc`. See `_validate`\n * below.\n */\n\n// ─────────────────────────────────────────────────────────────────────────\n// Build-time invariants + default relay set.\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Family-relay invariants applied to `DEFAULT_RELAYS`. The relay set must:\n * 1. Contain at least two relays — single-relay defaults are always wrong\n * because the family's BYPASS principle requires public-relay co-publish.\n * 2. Not be `wss://relay.ochk.io` alone — relay.ochk.io is additive, never\n * a single point of failure. See oc-relay-infra/BYPASS.md.\n *\n * If `T` violates either rule, this resolves to `never` and the assignment\n * below fails at `tsc` time.\n */\ntype ValidRelaySet<T extends readonly string[]> =\n T['length'] extends 0 | 1 ? never :\n T extends readonly ['wss://relay.ochk.io'] ? never :\n T;\n\nconst _RELAYS: ValidRelaySet<readonly [\n 'wss://relay.nostr.band',\n 'wss://nos.lol',\n 'wss://relay.primal.net',\n 'wss://offchain.pub',\n 'wss://relay.ochk.io',\n]> = [\n 'wss://relay.nostr.band',\n 'wss://nos.lol',\n 'wss://relay.primal.net',\n 'wss://offchain.pub',\n // First-party family relay — kind allowlist 30078–30086 + canonical\n // OC d-tag prefixes. Always co-published with public relays; never\n // the only copy. See https://github.com/orangecheck/oc-relay-infra.\n 'wss://relay.ochk.io',\n] as const;\n\n/**\n * Default relay set for OrangeCheck family Nostr publishes + queries.\n *\n * Frozen at runtime; consumers MAY pass an explicit `relays` arg to any\n * function in this package to override.\n *\n * @example\n * ```ts\n * import { DEFAULT_RELAYS, publishEvent } from '@orangecheck/nostr-core';\n *\n * const results = await publishEvent(myEvent);\n * // → publishes to all 5 relays in DEFAULT_RELAYS in parallel\n * console.log(`accepted on ${results.filter(r => r.ok).length}/${results.length}`);\n * ```\n */\nexport const DEFAULT_RELAYS: readonly string[] = Object.freeze([..._RELAYS]);\n\n// ─────────────────────────────────────────────────────────────────────────\n// Wire types — NIP-01 event + filter + result shapes.\n// ─────────────────────────────────────────────────────────────────────────\n\nexport interface NostrEvent {\n id: string;\n kind: number;\n pubkey: string;\n created_at: number;\n content: string;\n tags: string[][];\n sig: string;\n}\n\nexport interface PublishResult {\n relay: string;\n ok: boolean;\n reason?: string;\n attempts: number;\n}\n\n/**\n * The tag names a relay is required to index.\n *\n * NIP-12 defines indexed tag filters over **single-letter** names only. A\n * filter on a multi-letter name (`#poll_id`, `#delegation`, `#address`) is\n * accepted by the wire format, forwarded, and then matches nothing on a\n * conforming relay — it fails silently and returns an empty set.\n *\n * This type used to name `'#poll_id'`, `'#voter'` and `'#creator'` as\n * legitimate filters (\"Used by OC Vote\"), with a `#${string}` catch-all that\n * permitted any other. That is how the same bug reached four codebases:\n * OC Vote's tally found zero ballots for every poll, OC Agent's revocation\n * lookup found zero revocations and therefore honoured revoked delegations,\n * and `@orangecheck/sdk`'s `check()` found zero attestations. Every one of\n * them was a filter the type system had blessed.\n *\n * Restricting the key space to single letters turns that class of bug into a\n * compile error. If you need to query by some value, the event has to carry\n * it in a single-letter tag — most often `t` — and that is a fact about\n * Nostr, not a preference.\n */\ntype IndexableTagName =\n | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'\n | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'\n | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M'\n | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';\n\n/** `#a` … `#Z` — the only tag filters a relay must honour. */\nexport type IndexableTagFilter = `#${IndexableTagName}`;\n\nexport interface Filter extends Partial<Record<IndexableTagFilter, string[]>> {\n kinds?: number[];\n authors?: string[];\n ids?: string[];\n limit?: number;\n since?: number;\n until?: number;\n}\n\nexport interface QueryResult {\n events: NostrEvent[];\n relayStatus: { relay: string; ok: boolean; reason?: string; events: number }[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Internal — frame parsing + retry config.\n// ─────────────────────────────────────────────────────────────────────────\n\ntype FrameType = 'OK' | 'EVENT' | 'EOSE' | 'NOTICE' | 'CLOSED' | 'AUTH';\ninterface RelayFrame {\n type: FrameType;\n payload: unknown[];\n}\n\n/**\n * Sign a [NIP-42](https://nips.nostr.com/42) AUTH challenge. Given the relay's\n * `challenge` string and `relayUrl`, return a signed kind-22242 event (tags\n * `[\"relay\", relayUrl]` + `[\"challenge\", challenge]`). Passed in by the caller so\n * this package stays dependency-free (no crypto here). Provide it only for\n * `auth-required` relays; absent it, an AUTH challenge is ignored (today's behavior).\n */\nexport type AuthSigner = (\n challenge: string,\n relayUrl: string\n) => NostrEvent | Promise<NostrEvent>;\n\nfunction parseFrame(raw: string): RelayFrame | null {\n try {\n const arr = JSON.parse(raw) as unknown[];\n if (!Array.isArray(arr) || arr.length === 0) return null;\n const type = arr[0];\n if (\n type === 'OK' ||\n type === 'EVENT' ||\n type === 'EOSE' ||\n type === 'NOTICE' ||\n type === 'CLOSED' ||\n type === 'AUTH'\n ) {\n return { type: type as FrameType, payload: arr.slice(1) };\n }\n return null;\n } catch {\n return null;\n }\n}\n\ninterface RetryOptions {\n attempts: number;\n timeoutMs: number;\n initialBackoffMs: number;\n maxBackoffMs: number;\n}\n\nconst DEFAULT_RETRY: RetryOptions = {\n attempts: 3,\n timeoutMs: 5000,\n initialBackoffMs: 500,\n maxBackoffMs: 4000,\n};\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Publish — write an event to one or more relays in parallel, with retry.\n// ─────────────────────────────────────────────────────────────────────────\n\nasync function publishOne(\n url: string,\n event: NostrEvent,\n retry: RetryOptions,\n auth?: AuthSigner\n): Promise<PublishResult> {\n let attempts = 0;\n let backoff = retry.initialBackoffMs;\n let lastReason: string | undefined;\n\n while (attempts < retry.attempts) {\n attempts++;\n const attempt = await attemptPublish(url, event, retry.timeoutMs, auth);\n if (attempt.ok) {\n return {\n relay: url,\n ok: true,\n attempts,\n ...(attempt.reason ? { reason: attempt.reason } : {}),\n };\n }\n lastReason = attempt.reason;\n if (attempt.retryable && attempts < retry.attempts) {\n await delay(backoff);\n backoff = Math.min(backoff * 2, retry.maxBackoffMs);\n continue;\n }\n break;\n }\n return {\n relay: url,\n ok: false,\n attempts,\n ...(lastReason ? { reason: lastReason } : {}),\n };\n}\n\nfunction attemptPublish(\n url: string,\n event: NostrEvent,\n timeoutMs: number,\n auth?: AuthSigner\n): Promise<{ ok: boolean; reason?: string; retryable: boolean }> {\n return new Promise((resolve) => {\n let settled = false;\n let authed = false; // a NIP-42 AUTH handshake is attempted at most once\n let ws: WebSocket | null = null;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n try {\n ws?.close();\n } catch {}\n resolve({ ok: false, reason: 'timeout', retryable: true });\n }, timeoutMs);\n try {\n ws = new WebSocket(url);\n ws.onopen = () => ws?.send(JSON.stringify(['EVENT', event]));\n ws.onmessage = (msg) => {\n const frame = parseFrame(msg.data as string);\n if (!frame) return;\n // NIP-42: an auth-required relay challenges before it accepts the\n // EVENT. Sign the challenge with the caller's signer, send AUTH,\n // then re-send the EVENT. Without an `auth` signer this is ignored.\n if (frame.type === 'AUTH' && auth && !authed) {\n authed = true;\n const challenge = String(frame.payload[0] ?? '');\n void Promise.resolve(auth(challenge, url))\n .then((authEvent) => {\n ws?.send(JSON.stringify(['AUTH', authEvent]));\n ws?.send(JSON.stringify(['EVENT', event]));\n })\n .catch(() => {\n /* signer failed → let the attempt time out / fail */\n });\n return;\n }\n if (frame.type === 'OK' && frame.payload[0] === event.id) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n try {\n ws?.close();\n } catch {}\n const ok = frame.payload[1] === true;\n const reason = frame.payload[2] as string | undefined;\n resolve({\n ok,\n ...(reason ? { reason } : {}),\n retryable: false,\n });\n }\n };\n ws.onerror = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({ ok: false, reason: 'websocket_error', retryable: true });\n };\n ws.onclose = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({ ok: false, reason: 'closed_early', retryable: true });\n };\n } catch (err) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({\n ok: false,\n reason: err instanceof Error ? err.message : 'unknown',\n retryable: true,\n });\n }\n });\n}\n\n/**\n * Publish a NIP-01 event to all `relays` in parallel. Returns one\n * `PublishResult` per relay. Default timeout 5000ms.\n */\nexport async function publishEvent(\n event: NostrEvent,\n relays: readonly string[] = DEFAULT_RELAYS,\n timeoutMs = 5000,\n auth?: AuthSigner\n): Promise<PublishResult[]> {\n const retry: RetryOptions = { ...DEFAULT_RETRY, timeoutMs };\n return Promise.all(relays.map((r) => publishOne(r, event, retry, auth)));\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Query — REQ → EOSE → close. Races all relays; first to EOSE wins.\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Issue a NIP-01 REQ across all `relays` in parallel. Returns deduplicated\n * events sorted newest-first plus per-relay status.\n *\n * Default timeout 1500ms — short enough that a momentary blip on any one\n * relay (including relay.ochk.io) never holds up the racing reads. Pass an\n * explicit `timeoutMs` for slow filters or for use cases where waiting on\n * the slowest relay matters.\n */\nexport async function queryEvents(\n filter: Filter,\n relays: readonly string[] = DEFAULT_RELAYS,\n timeoutMs = 1500,\n auth?: AuthSigner\n): Promise<QueryResult> {\n const subId = 'ocnc-' + Math.random().toString(36).slice(2, 10);\n const byId = new Map<string, NostrEvent>();\n const status: QueryResult['relayStatus'] = [];\n\n await Promise.all(\n relays.map(\n (url) =>\n new Promise<void>((resolve) => {\n let settled = false;\n let authed = false; // NIP-42 handshake attempted at most once\n let count = 0;\n let reason: string | undefined;\n let ws: WebSocket | null = null;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n try {\n ws?.close();\n } catch {}\n status.push({\n relay: url,\n ok: count > 0,\n reason: reason ?? 'timeout',\n events: count,\n });\n resolve();\n }, timeoutMs);\n try {\n ws = new WebSocket(url);\n ws.onopen = () => ws?.send(JSON.stringify(['REQ', subId, filter]));\n ws.onmessage = (msg) => {\n const frame = parseFrame(msg.data as string);\n if (!frame) return;\n // NIP-42: auth-required relay challenges before serving the\n // REQ — sign, send AUTH, then re-send the REQ. Ignored absent\n // an `auth` signer (today's behavior).\n if (frame.type === 'AUTH' && auth && !authed) {\n authed = true;\n const challenge = String(frame.payload[0] ?? '');\n void Promise.resolve(auth(challenge, url))\n .then((authEvent) => {\n ws?.send(JSON.stringify(['AUTH', authEvent]));\n ws?.send(JSON.stringify(['REQ', subId, filter]));\n })\n .catch(() => {\n /* signer failed → let the query time out */\n });\n return;\n }\n if (frame.type === 'EVENT' && frame.payload[0] === subId) {\n const event = frame.payload[1] as NostrEvent | undefined;\n if (event && event.id) {\n byId.set(event.id, event);\n count++;\n }\n } else if (frame.type === 'EOSE' && frame.payload[0] === subId) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n try {\n ws?.send(JSON.stringify(['CLOSE', subId]));\n ws?.close();\n } catch {}\n status.push({ relay: url, ok: true, events: count });\n resolve();\n } else if (frame.type === 'NOTICE') {\n reason = String(frame.payload[0] ?? 'notice');\n }\n };\n ws.onerror = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: false,\n reason: 'ws_error',\n events: count,\n });\n resolve();\n };\n ws.onclose = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: count > 0,\n reason: reason ?? 'closed_early',\n events: count,\n });\n resolve();\n };\n } catch (err) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: false,\n reason: err instanceof Error ? err.message : 'unknown',\n events: count,\n });\n resolve();\n }\n })\n )\n );\n\n return {\n events: Array.from(byId.values()).sort((a, b) => b.created_at - a.created_at),\n relayStatus: status,\n };\n}\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -15,7 +15,7 @@ function parseFrame(raw) {
|
|
|
15
15
|
const arr = JSON.parse(raw);
|
|
16
16
|
if (!Array.isArray(arr) || arr.length === 0) return null;
|
|
17
17
|
const type = arr[0];
|
|
18
|
-
if (type === "OK" || type === "EVENT" || type === "EOSE" || type === "NOTICE" || type === "CLOSED") {
|
|
18
|
+
if (type === "OK" || type === "EVENT" || type === "EOSE" || type === "NOTICE" || type === "CLOSED" || type === "AUTH") {
|
|
19
19
|
return { type, payload: arr.slice(1) };
|
|
20
20
|
}
|
|
21
21
|
return null;
|
|
@@ -32,13 +32,13 @@ var DEFAULT_RETRY = {
|
|
|
32
32
|
function delay(ms) {
|
|
33
33
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
34
|
}
|
|
35
|
-
async function publishOne(url, event, retry) {
|
|
35
|
+
async function publishOne(url, event, retry, auth) {
|
|
36
36
|
let attempts = 0;
|
|
37
37
|
let backoff = retry.initialBackoffMs;
|
|
38
38
|
let lastReason;
|
|
39
39
|
while (attempts < retry.attempts) {
|
|
40
40
|
attempts++;
|
|
41
|
-
const attempt = await attemptPublish(url, event, retry.timeoutMs);
|
|
41
|
+
const attempt = await attemptPublish(url, event, retry.timeoutMs, auth);
|
|
42
42
|
if (attempt.ok) {
|
|
43
43
|
return {
|
|
44
44
|
relay: url,
|
|
@@ -62,9 +62,10 @@ async function publishOne(url, event, retry) {
|
|
|
62
62
|
...lastReason ? { reason: lastReason } : {}
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
function attemptPublish(url, event, timeoutMs) {
|
|
65
|
+
function attemptPublish(url, event, timeoutMs, auth) {
|
|
66
66
|
return new Promise((resolve) => {
|
|
67
67
|
let settled = false;
|
|
68
|
+
let authed = false;
|
|
68
69
|
let ws = null;
|
|
69
70
|
const timer = setTimeout(() => {
|
|
70
71
|
if (settled) return;
|
|
@@ -81,6 +82,16 @@ function attemptPublish(url, event, timeoutMs) {
|
|
|
81
82
|
ws.onmessage = (msg) => {
|
|
82
83
|
const frame = parseFrame(msg.data);
|
|
83
84
|
if (!frame) return;
|
|
85
|
+
if (frame.type === "AUTH" && auth && !authed) {
|
|
86
|
+
authed = true;
|
|
87
|
+
const challenge = String(frame.payload[0] ?? "");
|
|
88
|
+
void Promise.resolve(auth(challenge, url)).then((authEvent) => {
|
|
89
|
+
ws?.send(JSON.stringify(["AUTH", authEvent]));
|
|
90
|
+
ws?.send(JSON.stringify(["EVENT", event]));
|
|
91
|
+
}).catch(() => {
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
84
95
|
if (frame.type === "OK" && frame.payload[0] === event.id) {
|
|
85
96
|
if (settled) return;
|
|
86
97
|
settled = true;
|
|
@@ -122,11 +133,11 @@ function attemptPublish(url, event, timeoutMs) {
|
|
|
122
133
|
}
|
|
123
134
|
});
|
|
124
135
|
}
|
|
125
|
-
async function publishEvent(event, relays = DEFAULT_RELAYS, timeoutMs = 5e3) {
|
|
136
|
+
async function publishEvent(event, relays = DEFAULT_RELAYS, timeoutMs = 5e3, auth) {
|
|
126
137
|
const retry = { ...DEFAULT_RETRY, timeoutMs };
|
|
127
|
-
return Promise.all(relays.map((r) => publishOne(r, event, retry)));
|
|
138
|
+
return Promise.all(relays.map((r) => publishOne(r, event, retry, auth)));
|
|
128
139
|
}
|
|
129
|
-
async function queryEvents(filter, relays = DEFAULT_RELAYS, timeoutMs = 1500) {
|
|
140
|
+
async function queryEvents(filter, relays = DEFAULT_RELAYS, timeoutMs = 1500, auth) {
|
|
130
141
|
const subId = "ocnc-" + Math.random().toString(36).slice(2, 10);
|
|
131
142
|
const byId = /* @__PURE__ */ new Map();
|
|
132
143
|
const status = [];
|
|
@@ -134,6 +145,7 @@ async function queryEvents(filter, relays = DEFAULT_RELAYS, timeoutMs = 1500) {
|
|
|
134
145
|
relays.map(
|
|
135
146
|
(url) => new Promise((resolve) => {
|
|
136
147
|
let settled = false;
|
|
148
|
+
let authed = false;
|
|
137
149
|
let count = 0;
|
|
138
150
|
let reason;
|
|
139
151
|
let ws = null;
|
|
@@ -158,6 +170,16 @@ async function queryEvents(filter, relays = DEFAULT_RELAYS, timeoutMs = 1500) {
|
|
|
158
170
|
ws.onmessage = (msg) => {
|
|
159
171
|
const frame = parseFrame(msg.data);
|
|
160
172
|
if (!frame) return;
|
|
173
|
+
if (frame.type === "AUTH" && auth && !authed) {
|
|
174
|
+
authed = true;
|
|
175
|
+
const challenge = String(frame.payload[0] ?? "");
|
|
176
|
+
void Promise.resolve(auth(challenge, url)).then((authEvent) => {
|
|
177
|
+
ws?.send(JSON.stringify(["AUTH", authEvent]));
|
|
178
|
+
ws?.send(JSON.stringify(["REQ", subId, filter]));
|
|
179
|
+
}).catch(() => {
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
161
183
|
if (frame.type === "EVENT" && frame.payload[0] === subId) {
|
|
162
184
|
const event = frame.payload[1];
|
|
163
185
|
if (event && event.id) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @orangecheck/nostr-core\n *\n * Browser-compatible Nostr client used by every OrangeCheck family web\n * app. Raw NIP-01 over WebSocket against a list of relays. Every operation\n * races all relays in parallel and reports per-relay status so the caller\n * can distinguish \"nobody replied\" from \"one relay rejected.\" Retries with\n * exponential backoff on transport errors only.\n *\n * No dependencies — uses the platform `WebSocket` global. Works in any\n * runtime that ships a WHATWG WebSocket (browser, Node 22+, Deno, Bun,\n * Cloudflare Workers).\n *\n * Source-of-truth `DEFAULT_RELAYS` for the OC family. Co-publishes to four\n * public relays plus `wss://relay.ochk.io` (the family's first-party\n * kind-allowlisted relay — see https://github.com/orangecheck/oc-relay-infra).\n *\n * **Hard invariant:** `DEFAULT_RELAYS` MUST contain at least two entries,\n * and MUST NOT be `relay.ochk.io` alone. Enforced at the type level — a\n * future engineer simplifying to ours-only fails `tsc`. See `_validate`\n * below.\n */\n\n// ─────────────────────────────────────────────────────────────────────────\n// Build-time invariants + default relay set.\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Family-relay invariants applied to `DEFAULT_RELAYS`. The relay set must:\n * 1. Contain at least two relays — single-relay defaults are always wrong\n * because the family's BYPASS principle requires public-relay co-publish.\n * 2. Not be `wss://relay.ochk.io` alone — relay.ochk.io is additive, never\n * a single point of failure. See oc-relay-infra/BYPASS.md.\n *\n * If `T` violates either rule, this resolves to `never` and the assignment\n * below fails at `tsc` time.\n */\ntype ValidRelaySet<T extends readonly string[]> =\n T['length'] extends 0 | 1 ? never :\n T extends readonly ['wss://relay.ochk.io'] ? never :\n T;\n\nconst _RELAYS: ValidRelaySet<readonly [\n 'wss://relay.nostr.band',\n 'wss://nos.lol',\n 'wss://relay.primal.net',\n 'wss://offchain.pub',\n 'wss://relay.ochk.io',\n]> = [\n 'wss://relay.nostr.band',\n 'wss://nos.lol',\n 'wss://relay.primal.net',\n 'wss://offchain.pub',\n // First-party family relay — kind allowlist 30078–30086 + canonical\n // OC d-tag prefixes. Always co-published with public relays; never\n // the only copy. See https://github.com/orangecheck/oc-relay-infra.\n 'wss://relay.ochk.io',\n] as const;\n\n/**\n * Default relay set for OrangeCheck family Nostr publishes + queries.\n *\n * Frozen at runtime; consumers MAY pass an explicit `relays` arg to any\n * function in this package to override.\n *\n * @example\n * ```ts\n * import { DEFAULT_RELAYS, publishEvent } from '@orangecheck/nostr-core';\n *\n * const results = await publishEvent(myEvent);\n * // → publishes to all 5 relays in DEFAULT_RELAYS in parallel\n * console.log(`accepted on ${results.filter(r => r.ok).length}/${results.length}`);\n * ```\n */\nexport const DEFAULT_RELAYS: readonly string[] = Object.freeze([..._RELAYS]);\n\n// ─────────────────────────────────────────────────────────────────────────\n// Wire types — NIP-01 event + filter + result shapes.\n// ─────────────────────────────────────────────────────────────────────────\n\nexport interface NostrEvent {\n id: string;\n kind: number;\n pubkey: string;\n created_at: number;\n content: string;\n tags: string[][];\n sig: string;\n}\n\nexport interface PublishResult {\n relay: string;\n ok: boolean;\n reason?: string;\n attempts: number;\n}\n\nexport interface Filter {\n kinds?: number[];\n authors?: string[];\n ids?: string[];\n limit?: number;\n since?: number;\n until?: number;\n /** NIP-12 indexable `d`-tag filter. */\n '#d'?: string[];\n /** NIP-12 indexable single-letter tag filter. */\n '#t'?: string[];\n /** Used by OC Vote (kind 30081 ballots). */\n '#poll_id'?: string[];\n /** Used by OC Vote (kind 30081 ballots). */\n '#voter'?: string[];\n /** Used by OC Vote (kind 30080 polls). */\n '#creator'?: string[];\n /** Other indexable single-letter tags clients may filter on. */\n [key: `#${string}`]: string[] | undefined;\n}\n\nexport interface QueryResult {\n events: NostrEvent[];\n relayStatus: { relay: string; ok: boolean; reason?: string; events: number }[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Internal — frame parsing + retry config.\n// ─────────────────────────────────────────────────────────────────────────\n\ntype FrameType = 'OK' | 'EVENT' | 'EOSE' | 'NOTICE' | 'CLOSED';\ninterface RelayFrame {\n type: FrameType;\n payload: unknown[];\n}\n\nfunction parseFrame(raw: string): RelayFrame | null {\n try {\n const arr = JSON.parse(raw) as unknown[];\n if (!Array.isArray(arr) || arr.length === 0) return null;\n const type = arr[0];\n if (\n type === 'OK' ||\n type === 'EVENT' ||\n type === 'EOSE' ||\n type === 'NOTICE' ||\n type === 'CLOSED'\n ) {\n return { type: type as FrameType, payload: arr.slice(1) };\n }\n return null;\n } catch {\n return null;\n }\n}\n\ninterface RetryOptions {\n attempts: number;\n timeoutMs: number;\n initialBackoffMs: number;\n maxBackoffMs: number;\n}\n\nconst DEFAULT_RETRY: RetryOptions = {\n attempts: 3,\n timeoutMs: 5000,\n initialBackoffMs: 500,\n maxBackoffMs: 4000,\n};\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Publish — write an event to one or more relays in parallel, with retry.\n// ─────────────────────────────────────────────────────────────────────────\n\nasync function publishOne(\n url: string,\n event: NostrEvent,\n retry: RetryOptions\n): Promise<PublishResult> {\n let attempts = 0;\n let backoff = retry.initialBackoffMs;\n let lastReason: string | undefined;\n\n while (attempts < retry.attempts) {\n attempts++;\n const attempt = await attemptPublish(url, event, retry.timeoutMs);\n if (attempt.ok) {\n return {\n relay: url,\n ok: true,\n attempts,\n ...(attempt.reason ? { reason: attempt.reason } : {}),\n };\n }\n lastReason = attempt.reason;\n if (attempt.retryable && attempts < retry.attempts) {\n await delay(backoff);\n backoff = Math.min(backoff * 2, retry.maxBackoffMs);\n continue;\n }\n break;\n }\n return {\n relay: url,\n ok: false,\n attempts,\n ...(lastReason ? { reason: lastReason } : {}),\n };\n}\n\nfunction attemptPublish(\n url: string,\n event: NostrEvent,\n timeoutMs: number\n): Promise<{ ok: boolean; reason?: string; retryable: boolean }> {\n return new Promise((resolve) => {\n let settled = false;\n let ws: WebSocket | null = null;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n try {\n ws?.close();\n } catch {}\n resolve({ ok: false, reason: 'timeout', retryable: true });\n }, timeoutMs);\n try {\n ws = new WebSocket(url);\n ws.onopen = () => ws?.send(JSON.stringify(['EVENT', event]));\n ws.onmessage = (msg) => {\n const frame = parseFrame(msg.data as string);\n if (!frame) return;\n if (frame.type === 'OK' && frame.payload[0] === event.id) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n try {\n ws?.close();\n } catch {}\n const ok = frame.payload[1] === true;\n const reason = frame.payload[2] as string | undefined;\n resolve({\n ok,\n ...(reason ? { reason } : {}),\n retryable: false,\n });\n }\n };\n ws.onerror = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({ ok: false, reason: 'websocket_error', retryable: true });\n };\n ws.onclose = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({ ok: false, reason: 'closed_early', retryable: true });\n };\n } catch (err) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({\n ok: false,\n reason: err instanceof Error ? err.message : 'unknown',\n retryable: true,\n });\n }\n });\n}\n\n/**\n * Publish a NIP-01 event to all `relays` in parallel. Returns one\n * `PublishResult` per relay. Default timeout 5000ms.\n */\nexport async function publishEvent(\n event: NostrEvent,\n relays: readonly string[] = DEFAULT_RELAYS,\n timeoutMs = 5000\n): Promise<PublishResult[]> {\n const retry: RetryOptions = { ...DEFAULT_RETRY, timeoutMs };\n return Promise.all(relays.map((r) => publishOne(r, event, retry)));\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Query — REQ → EOSE → close. Races all relays; first to EOSE wins.\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Issue a NIP-01 REQ across all `relays` in parallel. Returns deduplicated\n * events sorted newest-first plus per-relay status.\n *\n * Default timeout 1500ms — short enough that a momentary blip on any one\n * relay (including relay.ochk.io) never holds up the racing reads. Pass an\n * explicit `timeoutMs` for slow filters or for use cases where waiting on\n * the slowest relay matters.\n */\nexport async function queryEvents(\n filter: Filter,\n relays: readonly string[] = DEFAULT_RELAYS,\n timeoutMs = 1500\n): Promise<QueryResult> {\n const subId = 'ocnc-' + Math.random().toString(36).slice(2, 10);\n const byId = new Map<string, NostrEvent>();\n const status: QueryResult['relayStatus'] = [];\n\n await Promise.all(\n relays.map(\n (url) =>\n new Promise<void>((resolve) => {\n let settled = false;\n let count = 0;\n let reason: string | undefined;\n let ws: WebSocket | null = null;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n try {\n ws?.close();\n } catch {}\n status.push({\n relay: url,\n ok: count > 0,\n reason: reason ?? 'timeout',\n events: count,\n });\n resolve();\n }, timeoutMs);\n try {\n ws = new WebSocket(url);\n ws.onopen = () => ws?.send(JSON.stringify(['REQ', subId, filter]));\n ws.onmessage = (msg) => {\n const frame = parseFrame(msg.data as string);\n if (!frame) return;\n if (frame.type === 'EVENT' && frame.payload[0] === subId) {\n const event = frame.payload[1] as NostrEvent | undefined;\n if (event && event.id) {\n byId.set(event.id, event);\n count++;\n }\n } else if (frame.type === 'EOSE' && frame.payload[0] === subId) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n try {\n ws?.send(JSON.stringify(['CLOSE', subId]));\n ws?.close();\n } catch {}\n status.push({ relay: url, ok: true, events: count });\n resolve();\n } else if (frame.type === 'NOTICE') {\n reason = String(frame.payload[0] ?? 'notice');\n }\n };\n ws.onerror = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: false,\n reason: 'ws_error',\n events: count,\n });\n resolve();\n };\n ws.onclose = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: count > 0,\n reason: reason ?? 'closed_early',\n events: count,\n });\n resolve();\n };\n } catch (err) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: false,\n reason: err instanceof Error ? err.message : 'unknown',\n events: count,\n });\n resolve();\n }\n })\n )\n );\n\n return {\n events: Array.from(byId.values()).sort((a, b) => b.created_at - a.created_at),\n relayStatus: status,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @orangecheck/nostr-core\n *\n * Browser-compatible Nostr client used by every OrangeCheck family web\n * app. Raw NIP-01 over WebSocket against a list of relays. Every operation\n * races all relays in parallel and reports per-relay status so the caller\n * can distinguish \"nobody replied\" from \"one relay rejected.\" Retries with\n * exponential backoff on transport errors only.\n *\n * No dependencies — uses the platform `WebSocket` global. Works in any\n * runtime that ships a WHATWG WebSocket (browser, Node 22+, Deno, Bun,\n * Cloudflare Workers).\n *\n * Source-of-truth `DEFAULT_RELAYS` for the OC family. Co-publishes to four\n * public relays plus `wss://relay.ochk.io` (the family's first-party\n * kind-allowlisted relay — see https://github.com/orangecheck/oc-relay-infra).\n *\n * **Hard invariant:** `DEFAULT_RELAYS` MUST contain at least two entries,\n * and MUST NOT be `relay.ochk.io` alone. Enforced at the type level — a\n * future engineer simplifying to ours-only fails `tsc`. See `_validate`\n * below.\n */\n\n// ─────────────────────────────────────────────────────────────────────────\n// Build-time invariants + default relay set.\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Family-relay invariants applied to `DEFAULT_RELAYS`. The relay set must:\n * 1. Contain at least two relays — single-relay defaults are always wrong\n * because the family's BYPASS principle requires public-relay co-publish.\n * 2. Not be `wss://relay.ochk.io` alone — relay.ochk.io is additive, never\n * a single point of failure. See oc-relay-infra/BYPASS.md.\n *\n * If `T` violates either rule, this resolves to `never` and the assignment\n * below fails at `tsc` time.\n */\ntype ValidRelaySet<T extends readonly string[]> =\n T['length'] extends 0 | 1 ? never :\n T extends readonly ['wss://relay.ochk.io'] ? never :\n T;\n\nconst _RELAYS: ValidRelaySet<readonly [\n 'wss://relay.nostr.band',\n 'wss://nos.lol',\n 'wss://relay.primal.net',\n 'wss://offchain.pub',\n 'wss://relay.ochk.io',\n]> = [\n 'wss://relay.nostr.band',\n 'wss://nos.lol',\n 'wss://relay.primal.net',\n 'wss://offchain.pub',\n // First-party family relay — kind allowlist 30078–30086 + canonical\n // OC d-tag prefixes. Always co-published with public relays; never\n // the only copy. See https://github.com/orangecheck/oc-relay-infra.\n 'wss://relay.ochk.io',\n] as const;\n\n/**\n * Default relay set for OrangeCheck family Nostr publishes + queries.\n *\n * Frozen at runtime; consumers MAY pass an explicit `relays` arg to any\n * function in this package to override.\n *\n * @example\n * ```ts\n * import { DEFAULT_RELAYS, publishEvent } from '@orangecheck/nostr-core';\n *\n * const results = await publishEvent(myEvent);\n * // → publishes to all 5 relays in DEFAULT_RELAYS in parallel\n * console.log(`accepted on ${results.filter(r => r.ok).length}/${results.length}`);\n * ```\n */\nexport const DEFAULT_RELAYS: readonly string[] = Object.freeze([..._RELAYS]);\n\n// ─────────────────────────────────────────────────────────────────────────\n// Wire types — NIP-01 event + filter + result shapes.\n// ─────────────────────────────────────────────────────────────────────────\n\nexport interface NostrEvent {\n id: string;\n kind: number;\n pubkey: string;\n created_at: number;\n content: string;\n tags: string[][];\n sig: string;\n}\n\nexport interface PublishResult {\n relay: string;\n ok: boolean;\n reason?: string;\n attempts: number;\n}\n\n/**\n * The tag names a relay is required to index.\n *\n * NIP-12 defines indexed tag filters over **single-letter** names only. A\n * filter on a multi-letter name (`#poll_id`, `#delegation`, `#address`) is\n * accepted by the wire format, forwarded, and then matches nothing on a\n * conforming relay — it fails silently and returns an empty set.\n *\n * This type used to name `'#poll_id'`, `'#voter'` and `'#creator'` as\n * legitimate filters (\"Used by OC Vote\"), with a `#${string}` catch-all that\n * permitted any other. That is how the same bug reached four codebases:\n * OC Vote's tally found zero ballots for every poll, OC Agent's revocation\n * lookup found zero revocations and therefore honoured revoked delegations,\n * and `@orangecheck/sdk`'s `check()` found zero attestations. Every one of\n * them was a filter the type system had blessed.\n *\n * Restricting the key space to single letters turns that class of bug into a\n * compile error. If you need to query by some value, the event has to carry\n * it in a single-letter tag — most often `t` — and that is a fact about\n * Nostr, not a preference.\n */\ntype IndexableTagName =\n | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'\n | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'\n | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M'\n | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';\n\n/** `#a` … `#Z` — the only tag filters a relay must honour. */\nexport type IndexableTagFilter = `#${IndexableTagName}`;\n\nexport interface Filter extends Partial<Record<IndexableTagFilter, string[]>> {\n kinds?: number[];\n authors?: string[];\n ids?: string[];\n limit?: number;\n since?: number;\n until?: number;\n}\n\nexport interface QueryResult {\n events: NostrEvent[];\n relayStatus: { relay: string; ok: boolean; reason?: string; events: number }[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Internal — frame parsing + retry config.\n// ─────────────────────────────────────────────────────────────────────────\n\ntype FrameType = 'OK' | 'EVENT' | 'EOSE' | 'NOTICE' | 'CLOSED' | 'AUTH';\ninterface RelayFrame {\n type: FrameType;\n payload: unknown[];\n}\n\n/**\n * Sign a [NIP-42](https://nips.nostr.com/42) AUTH challenge. Given the relay's\n * `challenge` string and `relayUrl`, return a signed kind-22242 event (tags\n * `[\"relay\", relayUrl]` + `[\"challenge\", challenge]`). Passed in by the caller so\n * this package stays dependency-free (no crypto here). Provide it only for\n * `auth-required` relays; absent it, an AUTH challenge is ignored (today's behavior).\n */\nexport type AuthSigner = (\n challenge: string,\n relayUrl: string\n) => NostrEvent | Promise<NostrEvent>;\n\nfunction parseFrame(raw: string): RelayFrame | null {\n try {\n const arr = JSON.parse(raw) as unknown[];\n if (!Array.isArray(arr) || arr.length === 0) return null;\n const type = arr[0];\n if (\n type === 'OK' ||\n type === 'EVENT' ||\n type === 'EOSE' ||\n type === 'NOTICE' ||\n type === 'CLOSED' ||\n type === 'AUTH'\n ) {\n return { type: type as FrameType, payload: arr.slice(1) };\n }\n return null;\n } catch {\n return null;\n }\n}\n\ninterface RetryOptions {\n attempts: number;\n timeoutMs: number;\n initialBackoffMs: number;\n maxBackoffMs: number;\n}\n\nconst DEFAULT_RETRY: RetryOptions = {\n attempts: 3,\n timeoutMs: 5000,\n initialBackoffMs: 500,\n maxBackoffMs: 4000,\n};\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Publish — write an event to one or more relays in parallel, with retry.\n// ─────────────────────────────────────────────────────────────────────────\n\nasync function publishOne(\n url: string,\n event: NostrEvent,\n retry: RetryOptions,\n auth?: AuthSigner\n): Promise<PublishResult> {\n let attempts = 0;\n let backoff = retry.initialBackoffMs;\n let lastReason: string | undefined;\n\n while (attempts < retry.attempts) {\n attempts++;\n const attempt = await attemptPublish(url, event, retry.timeoutMs, auth);\n if (attempt.ok) {\n return {\n relay: url,\n ok: true,\n attempts,\n ...(attempt.reason ? { reason: attempt.reason } : {}),\n };\n }\n lastReason = attempt.reason;\n if (attempt.retryable && attempts < retry.attempts) {\n await delay(backoff);\n backoff = Math.min(backoff * 2, retry.maxBackoffMs);\n continue;\n }\n break;\n }\n return {\n relay: url,\n ok: false,\n attempts,\n ...(lastReason ? { reason: lastReason } : {}),\n };\n}\n\nfunction attemptPublish(\n url: string,\n event: NostrEvent,\n timeoutMs: number,\n auth?: AuthSigner\n): Promise<{ ok: boolean; reason?: string; retryable: boolean }> {\n return new Promise((resolve) => {\n let settled = false;\n let authed = false; // a NIP-42 AUTH handshake is attempted at most once\n let ws: WebSocket | null = null;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n try {\n ws?.close();\n } catch {}\n resolve({ ok: false, reason: 'timeout', retryable: true });\n }, timeoutMs);\n try {\n ws = new WebSocket(url);\n ws.onopen = () => ws?.send(JSON.stringify(['EVENT', event]));\n ws.onmessage = (msg) => {\n const frame = parseFrame(msg.data as string);\n if (!frame) return;\n // NIP-42: an auth-required relay challenges before it accepts the\n // EVENT. Sign the challenge with the caller's signer, send AUTH,\n // then re-send the EVENT. Without an `auth` signer this is ignored.\n if (frame.type === 'AUTH' && auth && !authed) {\n authed = true;\n const challenge = String(frame.payload[0] ?? '');\n void Promise.resolve(auth(challenge, url))\n .then((authEvent) => {\n ws?.send(JSON.stringify(['AUTH', authEvent]));\n ws?.send(JSON.stringify(['EVENT', event]));\n })\n .catch(() => {\n /* signer failed → let the attempt time out / fail */\n });\n return;\n }\n if (frame.type === 'OK' && frame.payload[0] === event.id) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n try {\n ws?.close();\n } catch {}\n const ok = frame.payload[1] === true;\n const reason = frame.payload[2] as string | undefined;\n resolve({\n ok,\n ...(reason ? { reason } : {}),\n retryable: false,\n });\n }\n };\n ws.onerror = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({ ok: false, reason: 'websocket_error', retryable: true });\n };\n ws.onclose = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({ ok: false, reason: 'closed_early', retryable: true });\n };\n } catch (err) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({\n ok: false,\n reason: err instanceof Error ? err.message : 'unknown',\n retryable: true,\n });\n }\n });\n}\n\n/**\n * Publish a NIP-01 event to all `relays` in parallel. Returns one\n * `PublishResult` per relay. Default timeout 5000ms.\n */\nexport async function publishEvent(\n event: NostrEvent,\n relays: readonly string[] = DEFAULT_RELAYS,\n timeoutMs = 5000,\n auth?: AuthSigner\n): Promise<PublishResult[]> {\n const retry: RetryOptions = { ...DEFAULT_RETRY, timeoutMs };\n return Promise.all(relays.map((r) => publishOne(r, event, retry, auth)));\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Query — REQ → EOSE → close. Races all relays; first to EOSE wins.\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Issue a NIP-01 REQ across all `relays` in parallel. Returns deduplicated\n * events sorted newest-first plus per-relay status.\n *\n * Default timeout 1500ms — short enough that a momentary blip on any one\n * relay (including relay.ochk.io) never holds up the racing reads. Pass an\n * explicit `timeoutMs` for slow filters or for use cases where waiting on\n * the slowest relay matters.\n */\nexport async function queryEvents(\n filter: Filter,\n relays: readonly string[] = DEFAULT_RELAYS,\n timeoutMs = 1500,\n auth?: AuthSigner\n): Promise<QueryResult> {\n const subId = 'ocnc-' + Math.random().toString(36).slice(2, 10);\n const byId = new Map<string, NostrEvent>();\n const status: QueryResult['relayStatus'] = [];\n\n await Promise.all(\n relays.map(\n (url) =>\n new Promise<void>((resolve) => {\n let settled = false;\n let authed = false; // NIP-42 handshake attempted at most once\n let count = 0;\n let reason: string | undefined;\n let ws: WebSocket | null = null;\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n try {\n ws?.close();\n } catch {}\n status.push({\n relay: url,\n ok: count > 0,\n reason: reason ?? 'timeout',\n events: count,\n });\n resolve();\n }, timeoutMs);\n try {\n ws = new WebSocket(url);\n ws.onopen = () => ws?.send(JSON.stringify(['REQ', subId, filter]));\n ws.onmessage = (msg) => {\n const frame = parseFrame(msg.data as string);\n if (!frame) return;\n // NIP-42: auth-required relay challenges before serving the\n // REQ — sign, send AUTH, then re-send the REQ. Ignored absent\n // an `auth` signer (today's behavior).\n if (frame.type === 'AUTH' && auth && !authed) {\n authed = true;\n const challenge = String(frame.payload[0] ?? '');\n void Promise.resolve(auth(challenge, url))\n .then((authEvent) => {\n ws?.send(JSON.stringify(['AUTH', authEvent]));\n ws?.send(JSON.stringify(['REQ', subId, filter]));\n })\n .catch(() => {\n /* signer failed → let the query time out */\n });\n return;\n }\n if (frame.type === 'EVENT' && frame.payload[0] === subId) {\n const event = frame.payload[1] as NostrEvent | undefined;\n if (event && event.id) {\n byId.set(event.id, event);\n count++;\n }\n } else if (frame.type === 'EOSE' && frame.payload[0] === subId) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n try {\n ws?.send(JSON.stringify(['CLOSE', subId]));\n ws?.close();\n } catch {}\n status.push({ relay: url, ok: true, events: count });\n resolve();\n } else if (frame.type === 'NOTICE') {\n reason = String(frame.payload[0] ?? 'notice');\n }\n };\n ws.onerror = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: false,\n reason: 'ws_error',\n events: count,\n });\n resolve();\n };\n ws.onclose = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: count > 0,\n reason: reason ?? 'closed_early',\n events: count,\n });\n resolve();\n };\n } catch (err) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n status.push({\n relay: url,\n ok: false,\n reason: err instanceof Error ? err.message : 'unknown',\n events: count,\n });\n resolve();\n }\n })\n )\n );\n\n return {\n events: Array.from(byId.values()).sort((a, b) => b.created_at - a.created_at),\n relayStatus: status,\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/nostr-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Browser-compatible Nostr client used by every OrangeCheck family web app. Raw NIP-01 over WebSocket against a list of relays. publishEvent / queryEvents / DEFAULT_RELAYS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nostr",
|
package/src/index.test.ts
CHANGED
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_RELAYS,
|
|
18
|
+
publishEvent,
|
|
19
|
+
queryEvents,
|
|
20
|
+
type Filter,
|
|
21
|
+
type NostrEvent,
|
|
22
|
+
} from './index';
|
|
17
23
|
|
|
18
24
|
// ── DEFAULT_RELAYS ───────────────────────────────────────────────────────
|
|
19
25
|
|
|
@@ -152,6 +158,33 @@ describe('queryEvents', () => {
|
|
|
152
158
|
});
|
|
153
159
|
});
|
|
154
160
|
|
|
161
|
+
describe('NIP-42 AUTH (additive)', () => {
|
|
162
|
+
it('publishEvent completes the AUTH handshake then publishes', async () => {
|
|
163
|
+
mockNextAuthPublish('chal-xyz');
|
|
164
|
+
let signedChallenge: string | undefined;
|
|
165
|
+
const results = await publishEvent(SAMPLE_EVENT, ['wss://auth'], 5000, (challenge, url) => {
|
|
166
|
+
signedChallenge = challenge;
|
|
167
|
+
return fakeAuthSigner(challenge, url);
|
|
168
|
+
});
|
|
169
|
+
expect(results[0]!.ok).toBe(true);
|
|
170
|
+
expect(signedChallenge).toBe('chal-xyz'); // the signer saw the relay's challenge
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('publishEvent WITHOUT an auth signer ignores the challenge and fails (no behavior change)', async () => {
|
|
174
|
+
mockNextAuthPublish('chal-xyz');
|
|
175
|
+
const results = await publishEvent(SAMPLE_EVENT, ['wss://auth'], 200);
|
|
176
|
+
expect(results[0]!.ok).toBe(false); // never authed → no OK → times out
|
|
177
|
+
}, 10_000);
|
|
178
|
+
|
|
179
|
+
it('queryEvents completes the AUTH handshake then serves the REQ', async () => {
|
|
180
|
+
const ev = { ...SAMPLE_EVENT, id: 'c'.repeat(64) };
|
|
181
|
+
mockNextAuthQuery('chal-q', [ev]);
|
|
182
|
+
const result = await queryEvents({ kinds: [30078] }, ['wss://auth'], 5000, fakeAuthSigner);
|
|
183
|
+
expect(result.events).toHaveLength(1);
|
|
184
|
+
expect(result.events[0]!.id).toBe('c'.repeat(64));
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
155
188
|
// ─────────────────────────────────────────────────────────────────────────
|
|
156
189
|
// Mock WebSocket harness
|
|
157
190
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -161,7 +194,10 @@ type Outcome =
|
|
|
161
194
|
| { kind: 'error' }
|
|
162
195
|
| { kind: 'query'; events: NostrEvent[] }
|
|
163
196
|
| { kind: 'query_error' }
|
|
164
|
-
| { kind: 'query_hang' }
|
|
197
|
+
| { kind: 'query_hang' }
|
|
198
|
+
// NIP-42: challenge first, then accept the re-sent EVENT/REQ after AUTH.
|
|
199
|
+
| { kind: 'auth_then_ok'; challenge: string }
|
|
200
|
+
| { kind: 'auth_then_query'; challenge: string; events: NostrEvent[] };
|
|
165
201
|
|
|
166
202
|
const outcomes: Outcome[] = [];
|
|
167
203
|
|
|
@@ -179,6 +215,30 @@ function mockNextQuery(detail: NostrEvent[] | 'error' | 'hang') {
|
|
|
179
215
|
else outcomes.push({ kind: 'query' as const, events: detail });
|
|
180
216
|
}
|
|
181
217
|
|
|
218
|
+
/** A relay that challenges with NIP-42 AUTH before accepting the publish. */
|
|
219
|
+
function mockNextAuthPublish(challenge: string) {
|
|
220
|
+
outcomes.push({ kind: 'auth_then_ok', challenge });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** A relay that challenges with NIP-42 AUTH before serving the query. */
|
|
224
|
+
function mockNextAuthQuery(challenge: string, events: NostrEvent[]) {
|
|
225
|
+
outcomes.push({ kind: 'auth_then_query', challenge, events });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** A throwaway kind-22242 AUTH signer for tests (no real crypto needed). */
|
|
229
|
+
const fakeAuthSigner = (challenge: string, relayUrl: string): NostrEvent => ({
|
|
230
|
+
id: 'a'.repeat(64),
|
|
231
|
+
kind: 22242,
|
|
232
|
+
pubkey: 'f'.repeat(64),
|
|
233
|
+
created_at: 1735689600,
|
|
234
|
+
content: '',
|
|
235
|
+
tags: [
|
|
236
|
+
['relay', relayUrl],
|
|
237
|
+
['challenge', challenge],
|
|
238
|
+
],
|
|
239
|
+
sig: 'cafebabe'.repeat(16),
|
|
240
|
+
});
|
|
241
|
+
|
|
182
242
|
let originalWebSocket: typeof globalThis.WebSocket | undefined;
|
|
183
243
|
|
|
184
244
|
function installMockWebSocket() {
|
|
@@ -198,6 +258,7 @@ function installMockWebSocket() {
|
|
|
198
258
|
onclose: (() => void) | null = null;
|
|
199
259
|
|
|
200
260
|
private outcome: Outcome | undefined;
|
|
261
|
+
private authChallenged = false; // NIP-42: challenge fired once per connection
|
|
201
262
|
|
|
202
263
|
constructor(public url: string) {
|
|
203
264
|
this.outcome = outcomes.shift();
|
|
@@ -208,11 +269,23 @@ function installMockWebSocket() {
|
|
|
208
269
|
send(raw: string) {
|
|
209
270
|
const frame = JSON.parse(raw) as unknown[];
|
|
210
271
|
const type = frame[0];
|
|
272
|
+
if (type === 'AUTH') {
|
|
273
|
+
// The client answered our NIP-42 challenge; nothing to do — the
|
|
274
|
+
// re-sent EVENT/REQ that follows is what we accept.
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
211
277
|
if (type === 'EVENT') {
|
|
212
278
|
const event = frame[1] as NostrEvent;
|
|
213
279
|
queueMicrotask(() => {
|
|
214
280
|
if (!this.outcome) return;
|
|
215
|
-
if (this.outcome.kind === '
|
|
281
|
+
if (this.outcome.kind === 'auth_then_ok') {
|
|
282
|
+
if (!this.authChallenged) {
|
|
283
|
+
this.authChallenged = true;
|
|
284
|
+
this.onmessage?.({ data: JSON.stringify(['AUTH', this.outcome.challenge]) });
|
|
285
|
+
} else {
|
|
286
|
+
this.onmessage?.({ data: JSON.stringify(['OK', event.id, true, '']) });
|
|
287
|
+
}
|
|
288
|
+
} else if (this.outcome.kind === 'publish_ok') {
|
|
216
289
|
this.onmessage?.({
|
|
217
290
|
data: JSON.stringify(['OK', event.id, this.outcome.ok, this.outcome.reason ?? '']),
|
|
218
291
|
});
|
|
@@ -224,7 +297,17 @@ function installMockWebSocket() {
|
|
|
224
297
|
const subId = frame[1] as string;
|
|
225
298
|
queueMicrotask(() => {
|
|
226
299
|
if (!this.outcome) return;
|
|
227
|
-
if (this.outcome.kind === '
|
|
300
|
+
if (this.outcome.kind === 'auth_then_query') {
|
|
301
|
+
if (!this.authChallenged) {
|
|
302
|
+
this.authChallenged = true;
|
|
303
|
+
this.onmessage?.({ data: JSON.stringify(['AUTH', this.outcome.challenge]) });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
for (const ev of this.outcome.events) {
|
|
307
|
+
this.onmessage?.({ data: JSON.stringify(['EVENT', subId, ev]) });
|
|
308
|
+
}
|
|
309
|
+
this.onmessage?.({ data: JSON.stringify(['EOSE', subId]) });
|
|
310
|
+
} else if (this.outcome.kind === 'query') {
|
|
228
311
|
for (const ev of this.outcome.events) {
|
|
229
312
|
this.onmessage?.({ data: JSON.stringify(['EVENT', subId, ev]) });
|
|
230
313
|
}
|
|
@@ -252,3 +335,42 @@ function restoreWebSocket() {
|
|
|
252
335
|
if (originalWebSocket) globalThis.WebSocket = originalWebSocket;
|
|
253
336
|
vi.unstubAllGlobals();
|
|
254
337
|
}
|
|
338
|
+
|
|
339
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
340
|
+
// Filter key space — NIP-12 indexed tags are single-letter only.
|
|
341
|
+
//
|
|
342
|
+
// These assertions are compile-time. They exist because the same silently-
|
|
343
|
+
// empty query shipped in four codebases (OC Vote's tally, OC Agent's
|
|
344
|
+
// revocation lookup, @orangecheck/sdk's check(), and this package's own type)
|
|
345
|
+
// while `Filter` still advertised '#poll_id' / '#voter' / '#creator' as valid.
|
|
346
|
+
// A multi-letter filter is not a style question — a conforming relay cannot
|
|
347
|
+
// match it, so the query returns nothing and the caller believes the answer.
|
|
348
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
349
|
+
describe('Filter key space', () => {
|
|
350
|
+
it('accepts single-letter indexed tag filters', () => {
|
|
351
|
+
const ok: Filter = { kinds: [30081], '#t': ['abc'], '#d': ['x'], '#p': ['y'] };
|
|
352
|
+
expect(ok['#t']).toEqual(['abc']);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('rejects multi-letter tag filters at compile time', () => {
|
|
356
|
+
const bad: Filter = {
|
|
357
|
+
kinds: [30081],
|
|
358
|
+
// @ts-expect-error '#poll_id' is not indexable — relays never match it.
|
|
359
|
+
'#poll_id': ['abc'],
|
|
360
|
+
};
|
|
361
|
+
// The value still exists at runtime; the point is that tsc refuses it.
|
|
362
|
+
expect((bad as Record<string, unknown>)['#poll_id']).toEqual(['abc']);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('rejects the other names that actually shipped', () => {
|
|
366
|
+
const a: Filter = {
|
|
367
|
+
// @ts-expect-error not indexable
|
|
368
|
+
'#delegation': ['x'],
|
|
369
|
+
};
|
|
370
|
+
const b: Filter = {
|
|
371
|
+
// @ts-expect-error not indexable
|
|
372
|
+
'#address': ['x'],
|
|
373
|
+
};
|
|
374
|
+
expect([a, b]).toHaveLength(2);
|
|
375
|
+
});
|
|
376
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -95,25 +95,43 @@ export interface PublishResult {
|
|
|
95
95
|
attempts: number;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The tag names a relay is required to index.
|
|
100
|
+
*
|
|
101
|
+
* NIP-12 defines indexed tag filters over **single-letter** names only. A
|
|
102
|
+
* filter on a multi-letter name (`#poll_id`, `#delegation`, `#address`) is
|
|
103
|
+
* accepted by the wire format, forwarded, and then matches nothing on a
|
|
104
|
+
* conforming relay — it fails silently and returns an empty set.
|
|
105
|
+
*
|
|
106
|
+
* This type used to name `'#poll_id'`, `'#voter'` and `'#creator'` as
|
|
107
|
+
* legitimate filters ("Used by OC Vote"), with a `#${string}` catch-all that
|
|
108
|
+
* permitted any other. That is how the same bug reached four codebases:
|
|
109
|
+
* OC Vote's tally found zero ballots for every poll, OC Agent's revocation
|
|
110
|
+
* lookup found zero revocations and therefore honoured revoked delegations,
|
|
111
|
+
* and `@orangecheck/sdk`'s `check()` found zero attestations. Every one of
|
|
112
|
+
* them was a filter the type system had blessed.
|
|
113
|
+
*
|
|
114
|
+
* Restricting the key space to single letters turns that class of bug into a
|
|
115
|
+
* compile error. If you need to query by some value, the event has to carry
|
|
116
|
+
* it in a single-letter tag — most often `t` — and that is a fact about
|
|
117
|
+
* Nostr, not a preference.
|
|
118
|
+
*/
|
|
119
|
+
type IndexableTagName =
|
|
120
|
+
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'
|
|
121
|
+
| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
|
|
122
|
+
| 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M'
|
|
123
|
+
| 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';
|
|
124
|
+
|
|
125
|
+
/** `#a` … `#Z` — the only tag filters a relay must honour. */
|
|
126
|
+
export type IndexableTagFilter = `#${IndexableTagName}`;
|
|
127
|
+
|
|
128
|
+
export interface Filter extends Partial<Record<IndexableTagFilter, string[]>> {
|
|
99
129
|
kinds?: number[];
|
|
100
130
|
authors?: string[];
|
|
101
131
|
ids?: string[];
|
|
102
132
|
limit?: number;
|
|
103
133
|
since?: number;
|
|
104
134
|
until?: number;
|
|
105
|
-
/** NIP-12 indexable `d`-tag filter. */
|
|
106
|
-
'#d'?: string[];
|
|
107
|
-
/** NIP-12 indexable single-letter tag filter. */
|
|
108
|
-
'#t'?: string[];
|
|
109
|
-
/** Used by OC Vote (kind 30081 ballots). */
|
|
110
|
-
'#poll_id'?: string[];
|
|
111
|
-
/** Used by OC Vote (kind 30081 ballots). */
|
|
112
|
-
'#voter'?: string[];
|
|
113
|
-
/** Used by OC Vote (kind 30080 polls). */
|
|
114
|
-
'#creator'?: string[];
|
|
115
|
-
/** Other indexable single-letter tags clients may filter on. */
|
|
116
|
-
[key: `#${string}`]: string[] | undefined;
|
|
117
135
|
}
|
|
118
136
|
|
|
119
137
|
export interface QueryResult {
|
|
@@ -125,12 +143,24 @@ export interface QueryResult {
|
|
|
125
143
|
// Internal — frame parsing + retry config.
|
|
126
144
|
// ─────────────────────────────────────────────────────────────────────────
|
|
127
145
|
|
|
128
|
-
type FrameType = 'OK' | 'EVENT' | 'EOSE' | 'NOTICE' | 'CLOSED';
|
|
146
|
+
type FrameType = 'OK' | 'EVENT' | 'EOSE' | 'NOTICE' | 'CLOSED' | 'AUTH';
|
|
129
147
|
interface RelayFrame {
|
|
130
148
|
type: FrameType;
|
|
131
149
|
payload: unknown[];
|
|
132
150
|
}
|
|
133
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Sign a [NIP-42](https://nips.nostr.com/42) AUTH challenge. Given the relay's
|
|
154
|
+
* `challenge` string and `relayUrl`, return a signed kind-22242 event (tags
|
|
155
|
+
* `["relay", relayUrl]` + `["challenge", challenge]`). Passed in by the caller so
|
|
156
|
+
* this package stays dependency-free (no crypto here). Provide it only for
|
|
157
|
+
* `auth-required` relays; absent it, an AUTH challenge is ignored (today's behavior).
|
|
158
|
+
*/
|
|
159
|
+
export type AuthSigner = (
|
|
160
|
+
challenge: string,
|
|
161
|
+
relayUrl: string
|
|
162
|
+
) => NostrEvent | Promise<NostrEvent>;
|
|
163
|
+
|
|
134
164
|
function parseFrame(raw: string): RelayFrame | null {
|
|
135
165
|
try {
|
|
136
166
|
const arr = JSON.parse(raw) as unknown[];
|
|
@@ -141,7 +171,8 @@ function parseFrame(raw: string): RelayFrame | null {
|
|
|
141
171
|
type === 'EVENT' ||
|
|
142
172
|
type === 'EOSE' ||
|
|
143
173
|
type === 'NOTICE' ||
|
|
144
|
-
type === 'CLOSED'
|
|
174
|
+
type === 'CLOSED' ||
|
|
175
|
+
type === 'AUTH'
|
|
145
176
|
) {
|
|
146
177
|
return { type: type as FrameType, payload: arr.slice(1) };
|
|
147
178
|
}
|
|
@@ -176,7 +207,8 @@ function delay(ms: number): Promise<void> {
|
|
|
176
207
|
async function publishOne(
|
|
177
208
|
url: string,
|
|
178
209
|
event: NostrEvent,
|
|
179
|
-
retry: RetryOptions
|
|
210
|
+
retry: RetryOptions,
|
|
211
|
+
auth?: AuthSigner
|
|
180
212
|
): Promise<PublishResult> {
|
|
181
213
|
let attempts = 0;
|
|
182
214
|
let backoff = retry.initialBackoffMs;
|
|
@@ -184,7 +216,7 @@ async function publishOne(
|
|
|
184
216
|
|
|
185
217
|
while (attempts < retry.attempts) {
|
|
186
218
|
attempts++;
|
|
187
|
-
const attempt = await attemptPublish(url, event, retry.timeoutMs);
|
|
219
|
+
const attempt = await attemptPublish(url, event, retry.timeoutMs, auth);
|
|
188
220
|
if (attempt.ok) {
|
|
189
221
|
return {
|
|
190
222
|
relay: url,
|
|
@@ -212,10 +244,12 @@ async function publishOne(
|
|
|
212
244
|
function attemptPublish(
|
|
213
245
|
url: string,
|
|
214
246
|
event: NostrEvent,
|
|
215
|
-
timeoutMs: number
|
|
247
|
+
timeoutMs: number,
|
|
248
|
+
auth?: AuthSigner
|
|
216
249
|
): Promise<{ ok: boolean; reason?: string; retryable: boolean }> {
|
|
217
250
|
return new Promise((resolve) => {
|
|
218
251
|
let settled = false;
|
|
252
|
+
let authed = false; // a NIP-42 AUTH handshake is attempted at most once
|
|
219
253
|
let ws: WebSocket | null = null;
|
|
220
254
|
const timer = setTimeout(() => {
|
|
221
255
|
if (settled) return;
|
|
@@ -231,6 +265,22 @@ function attemptPublish(
|
|
|
231
265
|
ws.onmessage = (msg) => {
|
|
232
266
|
const frame = parseFrame(msg.data as string);
|
|
233
267
|
if (!frame) return;
|
|
268
|
+
// NIP-42: an auth-required relay challenges before it accepts the
|
|
269
|
+
// EVENT. Sign the challenge with the caller's signer, send AUTH,
|
|
270
|
+
// then re-send the EVENT. Without an `auth` signer this is ignored.
|
|
271
|
+
if (frame.type === 'AUTH' && auth && !authed) {
|
|
272
|
+
authed = true;
|
|
273
|
+
const challenge = String(frame.payload[0] ?? '');
|
|
274
|
+
void Promise.resolve(auth(challenge, url))
|
|
275
|
+
.then((authEvent) => {
|
|
276
|
+
ws?.send(JSON.stringify(['AUTH', authEvent]));
|
|
277
|
+
ws?.send(JSON.stringify(['EVENT', event]));
|
|
278
|
+
})
|
|
279
|
+
.catch(() => {
|
|
280
|
+
/* signer failed → let the attempt time out / fail */
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
234
284
|
if (frame.type === 'OK' && frame.payload[0] === event.id) {
|
|
235
285
|
if (settled) return;
|
|
236
286
|
settled = true;
|
|
@@ -279,10 +329,11 @@ function attemptPublish(
|
|
|
279
329
|
export async function publishEvent(
|
|
280
330
|
event: NostrEvent,
|
|
281
331
|
relays: readonly string[] = DEFAULT_RELAYS,
|
|
282
|
-
timeoutMs = 5000
|
|
332
|
+
timeoutMs = 5000,
|
|
333
|
+
auth?: AuthSigner
|
|
283
334
|
): Promise<PublishResult[]> {
|
|
284
335
|
const retry: RetryOptions = { ...DEFAULT_RETRY, timeoutMs };
|
|
285
|
-
return Promise.all(relays.map((r) => publishOne(r, event, retry)));
|
|
336
|
+
return Promise.all(relays.map((r) => publishOne(r, event, retry, auth)));
|
|
286
337
|
}
|
|
287
338
|
|
|
288
339
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -301,7 +352,8 @@ export async function publishEvent(
|
|
|
301
352
|
export async function queryEvents(
|
|
302
353
|
filter: Filter,
|
|
303
354
|
relays: readonly string[] = DEFAULT_RELAYS,
|
|
304
|
-
timeoutMs = 1500
|
|
355
|
+
timeoutMs = 1500,
|
|
356
|
+
auth?: AuthSigner
|
|
305
357
|
): Promise<QueryResult> {
|
|
306
358
|
const subId = 'ocnc-' + Math.random().toString(36).slice(2, 10);
|
|
307
359
|
const byId = new Map<string, NostrEvent>();
|
|
@@ -312,6 +364,7 @@ export async function queryEvents(
|
|
|
312
364
|
(url) =>
|
|
313
365
|
new Promise<void>((resolve) => {
|
|
314
366
|
let settled = false;
|
|
367
|
+
let authed = false; // NIP-42 handshake attempted at most once
|
|
315
368
|
let count = 0;
|
|
316
369
|
let reason: string | undefined;
|
|
317
370
|
let ws: WebSocket | null = null;
|
|
@@ -335,6 +388,22 @@ export async function queryEvents(
|
|
|
335
388
|
ws.onmessage = (msg) => {
|
|
336
389
|
const frame = parseFrame(msg.data as string);
|
|
337
390
|
if (!frame) return;
|
|
391
|
+
// NIP-42: auth-required relay challenges before serving the
|
|
392
|
+
// REQ — sign, send AUTH, then re-send the REQ. Ignored absent
|
|
393
|
+
// an `auth` signer (today's behavior).
|
|
394
|
+
if (frame.type === 'AUTH' && auth && !authed) {
|
|
395
|
+
authed = true;
|
|
396
|
+
const challenge = String(frame.payload[0] ?? '');
|
|
397
|
+
void Promise.resolve(auth(challenge, url))
|
|
398
|
+
.then((authEvent) => {
|
|
399
|
+
ws?.send(JSON.stringify(['AUTH', authEvent]));
|
|
400
|
+
ws?.send(JSON.stringify(['REQ', subId, filter]));
|
|
401
|
+
})
|
|
402
|
+
.catch(() => {
|
|
403
|
+
/* signer failed → let the query time out */
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
338
407
|
if (frame.type === 'EVENT' && frame.payload[0] === subId) {
|
|
339
408
|
const event = frame.payload[1] as NostrEvent | undefined;
|
|
340
409
|
if (event && event.id) {
|