402-announce 1.1.0 → 1.1.2
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/build/announce.js +40 -11
- package/build/announce.js.map +1 -1
- package/build/event.d.ts +2 -1
- package/build/event.js +30 -2
- package/build/event.js.map +1 -1
- package/build/utils.d.ts +28 -0
- package/build/utils.js +126 -0
- package/build/utils.js.map +1 -1
- package/package.json +1 -1
package/build/announce.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { getPublicKey } from 'nostr-tools/pure';
|
|
2
1
|
import { Relay } from 'nostr-tools/relay';
|
|
3
2
|
import { buildAnnounceEvent } from './event.js';
|
|
4
|
-
import {
|
|
3
|
+
import { isPrivateHost } from './utils.js';
|
|
5
4
|
/**
|
|
6
5
|
* Publish a kind 31402 L402 service announcement to Nostr relays.
|
|
7
6
|
*
|
|
@@ -25,21 +24,51 @@ export async function announceService(config) {
|
|
|
25
24
|
if (!/^wss?:\/\//i.test(url)) {
|
|
26
25
|
throw new Error(`Invalid relay URL: ${url} — must start with wss:// or ws://`);
|
|
27
26
|
}
|
|
27
|
+
// H3: Reject private/loopback relay URLs (SSRF prevention)
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = new URL(url);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error(`Invalid relay URL: ${url}`);
|
|
34
|
+
}
|
|
35
|
+
if (isPrivateHost(parsed.hostname)) {
|
|
36
|
+
throw new Error(`Relay URL points to a private/loopback address: ${url}`);
|
|
37
|
+
}
|
|
38
|
+
// M4: Warn on insecure ws:// usage
|
|
39
|
+
if (url.startsWith('ws://')) {
|
|
40
|
+
console.warn(`[402-announce] Insecure WebSocket (ws://) relay: ${url} — use wss:// in production`);
|
|
41
|
+
}
|
|
28
42
|
}
|
|
29
|
-
//
|
|
30
|
-
const skBytes = hexToBytes(secretKey);
|
|
31
|
-
const pubkey = getPublicKey(skBytes);
|
|
32
|
-
skBytes.fill(0);
|
|
33
|
-
// Build and sign the event
|
|
43
|
+
// Build and sign the event (H2: no redundant key decode — pubkey comes from the event)
|
|
34
44
|
const event = buildAnnounceEvent(secretKey, config);
|
|
35
45
|
// Connect to relays in parallel and publish
|
|
36
46
|
const connectedRelays = [];
|
|
37
47
|
let accepted = 0;
|
|
38
48
|
const results = await Promise.allSettled(relays.map(async (url) => {
|
|
49
|
+
// H4: Track the relay reference before the race so it can be closed on timeout.
|
|
50
|
+
// Relay.connect() is started, then we race against the timeout. If the timeout
|
|
51
|
+
// fires first we close the relay once the connect promise eventually resolves,
|
|
52
|
+
// preventing the connection from leaking in the background.
|
|
53
|
+
const connectPromise = Relay.connect(url);
|
|
54
|
+
let timedOut = false;
|
|
55
|
+
let timerId;
|
|
39
56
|
const relay = await Promise.race([
|
|
40
|
-
|
|
41
|
-
new Promise((_, reject) =>
|
|
42
|
-
|
|
57
|
+
connectPromise.then((r) => { clearTimeout(timerId); return r; }),
|
|
58
|
+
new Promise((_, reject) => {
|
|
59
|
+
timerId = setTimeout(() => {
|
|
60
|
+
timedOut = true;
|
|
61
|
+
reject(new Error(`Relay connection timeout: ${url}`));
|
|
62
|
+
}, 10_000);
|
|
63
|
+
}),
|
|
64
|
+
]).catch(async (err) => {
|
|
65
|
+
// If the timeout fired, wait for the connect promise to settle so we
|
|
66
|
+
// can close any relay that connected after the deadline.
|
|
67
|
+
if (timedOut) {
|
|
68
|
+
connectPromise.then((r) => r.close()).catch(() => { });
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
});
|
|
43
72
|
connectedRelays.push(relay);
|
|
44
73
|
await relay.publish(event);
|
|
45
74
|
accepted++;
|
|
@@ -54,7 +83,7 @@ export async function announceService(config) {
|
|
|
54
83
|
}
|
|
55
84
|
return {
|
|
56
85
|
eventId: event.id,
|
|
57
|
-
pubkey,
|
|
86
|
+
pubkey: event.pubkey,
|
|
58
87
|
close() {
|
|
59
88
|
for (const relay of connectedRelays) {
|
|
60
89
|
try {
|
package/build/announce.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"announce.js","sourceRoot":"","sources":["../src/announce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"announce.js","sourceRoot":"","sources":["../src/announce.ts"],"names":[],"mappings":"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"}
|
package/build/event.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type { VerifiedEvent } from 'nostr-tools/pure';
|
|
|
3
3
|
/**
|
|
4
4
|
* Build and sign a kind 31402 Nostr event announcing an L402 service.
|
|
5
5
|
*
|
|
6
|
-
* The secret key
|
|
6
|
+
* The caller's secret key buffer is zeroed after signing. Library-internal
|
|
7
|
+
* copies made by @noble/curves are subject to GC timing.
|
|
7
8
|
*
|
|
8
9
|
* @param secretKeyHex - 64-character hex-encoded Nostr secret key
|
|
9
10
|
* @param config - Service announcement configuration
|
package/build/event.js
CHANGED
|
@@ -4,7 +4,8 @@ import { hexToBytes } from './utils.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Build and sign a kind 31402 Nostr event announcing an L402 service.
|
|
6
6
|
*
|
|
7
|
-
* The secret key
|
|
7
|
+
* The caller's secret key buffer is zeroed after signing. Library-internal
|
|
8
|
+
* copies made by @noble/curves are subject to GC timing.
|
|
8
9
|
*
|
|
9
10
|
* @param secretKeyHex - 64-character hex-encoded Nostr secret key
|
|
10
11
|
* @param config - Service announcement configuration
|
|
@@ -14,6 +15,29 @@ export function buildAnnounceEvent(secretKeyHex, config) {
|
|
|
14
15
|
if (!/^[0-9a-f]{64}$/i.test(secretKeyHex)) {
|
|
15
16
|
throw new Error('secretKey must be a 64-character hex string');
|
|
16
17
|
}
|
|
18
|
+
// M1: Validate url field
|
|
19
|
+
if (!config.url.startsWith('http://') && !config.url.startsWith('https://')) {
|
|
20
|
+
throw new Error('config.url must start with http:// or https://');
|
|
21
|
+
}
|
|
22
|
+
// M1: Validate picture field when present
|
|
23
|
+
if (config.picture !== undefined) {
|
|
24
|
+
if (!config.picture.startsWith('http://') && !config.picture.startsWith('https://')) {
|
|
25
|
+
throw new Error('config.picture must start with http:// or https://');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// M2: Validate identifier is non-empty and within length limit
|
|
29
|
+
if (config.identifier.trim().length === 0) {
|
|
30
|
+
throw new Error('config.identifier must not be empty or whitespace-only');
|
|
31
|
+
}
|
|
32
|
+
if (config.identifier.length > 256) {
|
|
33
|
+
throw new Error('config.identifier must not exceed 256 characters');
|
|
34
|
+
}
|
|
35
|
+
// M3: Validate all pricing entries
|
|
36
|
+
for (const p of config.pricing) {
|
|
37
|
+
if (!Number.isFinite(p.price) || p.price < 0) {
|
|
38
|
+
throw new Error(`config.pricing price must be a finite non-negative number, got: ${p.price}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
17
41
|
const sk = hexToBytes(secretKeyHex);
|
|
18
42
|
try {
|
|
19
43
|
const tags = [
|
|
@@ -43,10 +67,14 @@ export function buildAnnounceEvent(secretKeyHex, config) {
|
|
|
43
67
|
if (config.version) {
|
|
44
68
|
contentObj.version = config.version;
|
|
45
69
|
}
|
|
70
|
+
const content = JSON.stringify(contentObj);
|
|
71
|
+
if (Buffer.byteLength(content, 'utf8') > 65_536) {
|
|
72
|
+
throw new Error('Event content exceeds maximum size (64 KiB)');
|
|
73
|
+
}
|
|
46
74
|
const event = finalizeEvent({
|
|
47
75
|
kind: L402_ANNOUNCE_KIND,
|
|
48
76
|
tags,
|
|
49
|
-
content
|
|
77
|
+
content,
|
|
50
78
|
created_at: Math.floor(Date.now() / 1000),
|
|
51
79
|
}, sk);
|
|
52
80
|
return event;
|
package/build/event.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event.js","sourceRoot":"","sources":["../src/event.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAIvC
|
|
1
|
+
{"version":3,"file":"event.js","sourceRoot":"","sources":["../src/event.ts"],"names":[],"mappings":"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"}
|
package/build/utils.d.ts
CHANGED
|
@@ -1 +1,29 @@
|
|
|
1
1
|
export declare function hexToBytes(hex: string): Uint8Array;
|
|
2
|
+
/**
|
|
3
|
+
* Returns true if the hostname resolves to a loopback, link-local, or
|
|
4
|
+
* RFC-1918 private address. Used to prevent SSRF via relay URLs.
|
|
5
|
+
*
|
|
6
|
+
* Rejects:
|
|
7
|
+
* - localhost, localhost., *.localhost
|
|
8
|
+
* - 127.0.0.0/8 (IPv4 loopback)
|
|
9
|
+
* - 0.0.0.0/8 (RFC 1122 "this network")
|
|
10
|
+
* - ::1 (IPv6 loopback)
|
|
11
|
+
* - :: (IPv6 unspecified)
|
|
12
|
+
* - 169.254.0.0/16 (IPv4 link-local)
|
|
13
|
+
* - fe80::/10 (IPv6 link-local)
|
|
14
|
+
* - fc00::/7 (IPv6 unique-local / ULA)
|
|
15
|
+
* - 10.0.0.0/8 (RFC-1918)
|
|
16
|
+
* - 172.16.0.0/12 (RFC-1918)
|
|
17
|
+
* - 192.168.0.0/16 (RFC-1918)
|
|
18
|
+
* - ::ffff:<private> (IPv4-mapped IPv6)
|
|
19
|
+
* - ::<private> (IPv4-compatible IPv6, deprecated)
|
|
20
|
+
*
|
|
21
|
+
* Also rejects octal, hex, and shorthand IPv4 notations that could
|
|
22
|
+
* bypass naive decimal-only checks.
|
|
23
|
+
*
|
|
24
|
+
* Note: This checks the hostname string only. It does not perform DNS
|
|
25
|
+
* resolution, so a hostname that resolves to a private IP at connection
|
|
26
|
+
* time (DNS rebinding) is not caught. Deploy behind network-level
|
|
27
|
+
* egress controls in production.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isPrivateHost(hostname: string): boolean;
|
package/build/utils.js
CHANGED
|
@@ -1,8 +1,134 @@
|
|
|
1
1
|
export function hexToBytes(hex) {
|
|
2
|
+
if (hex.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(hex)) {
|
|
3
|
+
throw new Error('hexToBytes: input must be an even-length hex string');
|
|
4
|
+
}
|
|
2
5
|
const bytes = new Uint8Array(hex.length / 2);
|
|
3
6
|
for (let i = 0; i < hex.length; i += 2) {
|
|
4
7
|
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
5
8
|
}
|
|
6
9
|
return bytes;
|
|
7
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Returns true if the hostname resolves to a loopback, link-local, or
|
|
13
|
+
* RFC-1918 private address. Used to prevent SSRF via relay URLs.
|
|
14
|
+
*
|
|
15
|
+
* Rejects:
|
|
16
|
+
* - localhost, localhost., *.localhost
|
|
17
|
+
* - 127.0.0.0/8 (IPv4 loopback)
|
|
18
|
+
* - 0.0.0.0/8 (RFC 1122 "this network")
|
|
19
|
+
* - ::1 (IPv6 loopback)
|
|
20
|
+
* - :: (IPv6 unspecified)
|
|
21
|
+
* - 169.254.0.0/16 (IPv4 link-local)
|
|
22
|
+
* - fe80::/10 (IPv6 link-local)
|
|
23
|
+
* - fc00::/7 (IPv6 unique-local / ULA)
|
|
24
|
+
* - 10.0.0.0/8 (RFC-1918)
|
|
25
|
+
* - 172.16.0.0/12 (RFC-1918)
|
|
26
|
+
* - 192.168.0.0/16 (RFC-1918)
|
|
27
|
+
* - ::ffff:<private> (IPv4-mapped IPv6)
|
|
28
|
+
* - ::<private> (IPv4-compatible IPv6, deprecated)
|
|
29
|
+
*
|
|
30
|
+
* Also rejects octal, hex, and shorthand IPv4 notations that could
|
|
31
|
+
* bypass naive decimal-only checks.
|
|
32
|
+
*
|
|
33
|
+
* Note: This checks the hostname string only. It does not perform DNS
|
|
34
|
+
* resolution, so a hostname that resolves to a private IP at connection
|
|
35
|
+
* time (DNS rebinding) is not caught. Deploy behind network-level
|
|
36
|
+
* egress controls in production.
|
|
37
|
+
*/
|
|
38
|
+
export function isPrivateHost(hostname) {
|
|
39
|
+
const h = hostname.toLowerCase();
|
|
40
|
+
// Reject localhost, localhost., *.localhost, *.localhost.
|
|
41
|
+
if (h === 'localhost' || h === 'localhost.')
|
|
42
|
+
return true;
|
|
43
|
+
if (h.endsWith('.localhost') || h.endsWith('.localhost.'))
|
|
44
|
+
return true;
|
|
45
|
+
// Strip IPv6 brackets
|
|
46
|
+
const stripped = h.replace(/^\[|\]$/g, '');
|
|
47
|
+
// IPv6 loopback ::1 and unspecified ::
|
|
48
|
+
if (stripped === '::1' || stripped === '::')
|
|
49
|
+
return true;
|
|
50
|
+
// IPv6 link-local fe80::/10 (prefix fe80 through febf)
|
|
51
|
+
if (/^fe[89ab][0-9a-f]:/i.test(stripped))
|
|
52
|
+
return true;
|
|
53
|
+
// IPv6 unique-local fc00::/7 (fc00:: through fdff::)
|
|
54
|
+
if (/^f[cd][0-9a-f]{2}:/i.test(stripped))
|
|
55
|
+
return true;
|
|
56
|
+
// IPv4-mapped IPv6 — ::ffff:x.x.x.x or ::ffff:HHHH:HHHH
|
|
57
|
+
const v4mapped = stripped.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
58
|
+
if (v4mapped) {
|
|
59
|
+
return isPrivateIPv4(v4mapped[1]);
|
|
60
|
+
}
|
|
61
|
+
// ::ffff:HHHH:HHHH form (e.g. ::ffff:7f00:1 = 127.0.0.1)
|
|
62
|
+
const v4mappedHex = stripped.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
63
|
+
if (v4mappedHex) {
|
|
64
|
+
const hi = parseInt(v4mappedHex[1], 16);
|
|
65
|
+
const lo = parseInt(v4mappedHex[2], 16);
|
|
66
|
+
const ip = `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
|
|
67
|
+
return isPrivateIPv4(ip);
|
|
68
|
+
}
|
|
69
|
+
// IPv4-compatible IPv6 (deprecated) — ::x.x.x.x
|
|
70
|
+
const v4compat = stripped.match(/^::(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
71
|
+
if (v4compat) {
|
|
72
|
+
return isPrivateIPv4(v4compat[1]);
|
|
73
|
+
}
|
|
74
|
+
// Reject non-decimal IPv4 notations that could bypass checks:
|
|
75
|
+
// - Octal (leading zero): 0177.0.0.1
|
|
76
|
+
// - Hex: 0x7f.0.0.1 or 0x7f000001
|
|
77
|
+
// - Shorthand: 127.1 (two-part), 127.0.1 (three-part)
|
|
78
|
+
// - Decimal integer: 2130706433
|
|
79
|
+
// These are all valid in some URL parsers / OS network stacks.
|
|
80
|
+
// Reject hex IPv4 literals (e.g. 0x7f000001 or 0x7f.0.0.1) but NOT
|
|
81
|
+
// legitimate DNS names with labels starting with 0x (e.g. 0xchat.example).
|
|
82
|
+
// A hex IPv4 has all dot-separated parts matching hex/numeric patterns.
|
|
83
|
+
if (/^0x/i.test(h)) {
|
|
84
|
+
const hexParts = h.split('.');
|
|
85
|
+
if (hexParts.every(p => /^(0x[0-9a-f]+|\d+)$/i.test(p)))
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
// Reject pure decimal integer IPs (e.g. 2130706433)
|
|
89
|
+
if (/^\d{1,10}$/.test(h) && !h.includes('.'))
|
|
90
|
+
return true;
|
|
91
|
+
// Reject shorthand IPv4 (2 or 3 parts instead of 4).
|
|
92
|
+
// Intentionally conservative: rejects ALL shorthand numeric forms, not just
|
|
93
|
+
// private ranges, because some OS stacks interpret e.g. 10.1 as 10.0.0.1.
|
|
94
|
+
// Single-part all-numeric is already handled above.
|
|
95
|
+
const parts = h.split('.');
|
|
96
|
+
if (parts.length >= 2 && parts.length <= 3 && parts.every(p => /^\d+$/.test(p)))
|
|
97
|
+
return true;
|
|
98
|
+
// Parse dotted-decimal IPv4 (exactly 4 numeric parts)
|
|
99
|
+
const ipv4 = h.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
100
|
+
if (ipv4) {
|
|
101
|
+
const rawOctets = [ipv4[1], ipv4[2], ipv4[3], ipv4[4]];
|
|
102
|
+
// Reject leading zeros (octal notation bypass: 0177 = 127)
|
|
103
|
+
for (const octet of rawOctets) {
|
|
104
|
+
if (octet.length > 1 && octet.startsWith('0'))
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
// Reject out-of-range octets (not valid decimal IPv4)
|
|
108
|
+
if (rawOctets.some(o => Number(o) > 255))
|
|
109
|
+
return false;
|
|
110
|
+
return isPrivateIPv4(h);
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
/** Check a strict decimal dotted-quad IPv4 against private ranges. */
|
|
115
|
+
function isPrivateIPv4(ip) {
|
|
116
|
+
const parts = ip.split('.').map(Number);
|
|
117
|
+
if (parts.length !== 4 || parts.some(p => isNaN(p) || p < 0 || p > 255))
|
|
118
|
+
return false;
|
|
119
|
+
const [a, b] = parts;
|
|
120
|
+
if (a === 0)
|
|
121
|
+
return true; // 0.0.0.0/8
|
|
122
|
+
if (a === 127)
|
|
123
|
+
return true; // 127.0.0.0/8
|
|
124
|
+
if (a === 10)
|
|
125
|
+
return true; // 10.0.0.0/8
|
|
126
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
127
|
+
return true; // 172.16.0.0/12
|
|
128
|
+
if (a === 192 && b === 168)
|
|
129
|
+
return true; // 192.168.0.0/16
|
|
130
|
+
if (a === 169 && b === 254)
|
|
131
|
+
return true; // 169.254.0.0/16
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
8
134
|
//# sourceMappingURL=utils.js.map
|
package/build/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACtD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC"}
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED