402-announce 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -15
- package/build/announce.js +37 -11
- package/build/announce.js.map +1 -1
- package/build/event.d.ts +2 -1
- package/build/event.js +25 -1
- package/build/event.js.map +1 -1
- package/build/types.d.ts +4 -0
- package/build/types.js.map +1 -1
- package/build/utils.d.ts +16 -0
- package/build/utils.js +46 -0
- package/build/utils.js.map +1 -1
- package/llms-full.txt +222 -0
- package/llms.txt +36 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -6,6 +6,22 @@ Announce HTTP 402 services on Nostr for decentralised discovery. Supports both L
|
|
|
6
6
|
|
|
7
7
|
Publishes **kind 31402** parameterised replaceable events so that AI agents (and any Nostr client) can discover paid APIs without a central registry.
|
|
8
8
|
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
flowchart LR
|
|
13
|
+
A[Your API] -->|"npm install<br/>402-announce"| B[402-announce]
|
|
14
|
+
B -->|"kind 31402<br/>signed event"| C[(Nostr Relays)]
|
|
15
|
+
C -->|subscribe & filter| D[402-mcp]
|
|
16
|
+
D -->|discover + pay| A
|
|
17
|
+
|
|
18
|
+
style B fill:#f59e0b,color:#000
|
|
19
|
+
style C fill:#8b5cf6,color:#fff
|
|
20
|
+
style D fill:#3b82f6,color:#fff
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Your API publishes a service announcement to Nostr relays. AI agents (via [402-mcp](https://github.com/TheCryptoDonkey/402-mcp)) discover it, pay the invoice, and consume the API. No central registry required.
|
|
24
|
+
|
|
9
25
|
## Quick start
|
|
10
26
|
|
|
11
27
|
```bash
|
|
@@ -40,22 +56,64 @@ console.log('From pubkey:', handle.pubkey)
|
|
|
40
56
|
handle.close()
|
|
41
57
|
```
|
|
42
58
|
|
|
59
|
+
## Announce flow
|
|
60
|
+
|
|
61
|
+
```mermaid
|
|
62
|
+
flowchart TD
|
|
63
|
+
A[announceService config] --> B{Validate}
|
|
64
|
+
B -->|"secretKey: 64-char hex"| C[Derive pubkey]
|
|
65
|
+
B -->|"relays: wss:// URLs"| C
|
|
66
|
+
C --> D[Build kind 31402 event]
|
|
67
|
+
D --> E[Sign with secret key]
|
|
68
|
+
E --> F[Zeroise key bytes]
|
|
69
|
+
F --> G["Connect relays (parallel, 10s timeout)"]
|
|
70
|
+
G --> H{Each relay}
|
|
71
|
+
H -->|success| I[Publish event]
|
|
72
|
+
H -->|failure| J[Log warning, continue]
|
|
73
|
+
I --> K["Return { eventId, pubkey, close() }"]
|
|
74
|
+
J --> K
|
|
75
|
+
```
|
|
76
|
+
|
|
43
77
|
## Event format
|
|
44
78
|
|
|
45
|
-
Each announcement is a **kind 31402** parameterised replaceable event. The combination of `pubkey` + `d` tag uniquely identifies a listing
|
|
79
|
+
Each announcement is a **kind 31402** parameterised replaceable event. The combination of `pubkey` + `d` tag uniquely identifies a listing — publishing again with the same values updates the existing listing.
|
|
80
|
+
|
|
81
|
+
```mermaid
|
|
82
|
+
graph TB
|
|
83
|
+
subgraph "Kind 31402 Event"
|
|
84
|
+
direction TB
|
|
85
|
+
subgraph Tags
|
|
86
|
+
D["d: jokes-api"]
|
|
87
|
+
N["name: Jokes API"]
|
|
88
|
+
U["url: https://jokes.example.com"]
|
|
89
|
+
AB["about: A joke-telling service"]
|
|
90
|
+
PMI1["pmi: bitcoin-lightning-bolt11"]
|
|
91
|
+
PMI2["pmi: bitcoin-cashu"]
|
|
92
|
+
P["price: get_joke, 1, sats"]
|
|
93
|
+
T["t: comedy"]
|
|
94
|
+
end
|
|
95
|
+
subgraph Content["Content (JSON)"]
|
|
96
|
+
CAP["capabilities: [{ name, description }]"]
|
|
97
|
+
VER["version: 1.0.0"]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
style Tags fill:#1e293b,color:#e2e8f0
|
|
102
|
+
style Content fill:#1e293b,color:#e2e8f0
|
|
103
|
+
```
|
|
46
104
|
|
|
47
105
|
### Tags
|
|
48
106
|
|
|
49
|
-
| Tag | Description | Example |
|
|
50
|
-
|
|
51
|
-
| `d` | Unique identifier for this listing | `jokes-api` |
|
|
52
|
-
| `name` | Human-readable service name | `Jokes API` |
|
|
53
|
-
| `url` | HTTP endpoint for the 402 service | `https://jokes.example.com` |
|
|
54
|
-
| `about` | Short description | `A joke-telling service` |
|
|
55
|
-
| `pmi` | Payment method identifier (repeatable) | `bitcoin-lightning-bolt11` |
|
|
56
|
-
| `price` | Capability pricing (repeatable) | `get_joke`, `1`, `sats` |
|
|
57
|
-
| `t` | Topic tag for search/filtering (repeatable) | `comedy` |
|
|
58
|
-
| `picture` |
|
|
107
|
+
| Tag | Required | Description | Example |
|
|
108
|
+
|-----------|----------|----------------------------------------------|-----------------------------------|
|
|
109
|
+
| `d` | yes | Unique identifier for this listing | `jokes-api` |
|
|
110
|
+
| `name` | yes | Human-readable service name | `Jokes API` |
|
|
111
|
+
| `url` | yes | HTTP endpoint for the 402 service | `https://jokes.example.com` |
|
|
112
|
+
| `about` | yes | Short description | `A joke-telling service` |
|
|
113
|
+
| `pmi` | yes | Payment method identifier (repeatable) | `bitcoin-lightning-bolt11` |
|
|
114
|
+
| `price` | yes | Capability pricing (repeatable) | `get_joke`, `1`, `sats` |
|
|
115
|
+
| `t` | no | Topic tag for search/filtering (repeatable) | `comedy` |
|
|
116
|
+
| `picture` | no | Icon URL | `https://example.com/icon.png` |
|
|
59
117
|
|
|
60
118
|
### Content
|
|
61
119
|
|
|
@@ -70,10 +128,85 @@ The event content is a JSON object with optional fields:
|
|
|
70
128
|
}
|
|
71
129
|
```
|
|
72
130
|
|
|
131
|
+
## API
|
|
132
|
+
|
|
133
|
+
### `announceService(config): Promise<Announcement>`
|
|
134
|
+
|
|
135
|
+
High-level function that builds, signs, and publishes the event to multiple relays.
|
|
136
|
+
|
|
137
|
+
**Returns** an `Announcement` handle:
|
|
138
|
+
- `eventId` — the published Nostr event ID
|
|
139
|
+
- `pubkey` — the Nostr pubkey derived from your secret key
|
|
140
|
+
- `close()` — disconnect from all relays
|
|
141
|
+
|
|
142
|
+
### `buildAnnounceEvent(secretKey, config): VerifiedEvent`
|
|
143
|
+
|
|
144
|
+
Lower-level function that builds and signs the event without publishing. Useful if you manage relay connections yourself.
|
|
145
|
+
|
|
146
|
+
### Config options
|
|
147
|
+
|
|
148
|
+
| Field | Type | Required | Description |
|
|
149
|
+
|------------------|-------------------|----------|------------------------------------------------|
|
|
150
|
+
| `secretKey` | `string` | yes | 64-char hex Nostr secret key |
|
|
151
|
+
| `relays` | `string[]` | yes | Relay URLs (`wss://` or `ws://`) |
|
|
152
|
+
| `identifier` | `string` | yes | Unique listing ID (Nostr `d` tag) |
|
|
153
|
+
| `name` | `string` | yes | Human-readable service name |
|
|
154
|
+
| `url` | `string` | yes | HTTP endpoint for the service |
|
|
155
|
+
| `about` | `string` | yes | Short description |
|
|
156
|
+
| `pricing` | `PricingDef[]` | yes | Per-capability pricing |
|
|
157
|
+
| `paymentMethods` | `string[]` | yes | Accepted payment methods |
|
|
158
|
+
| `picture` | `string` | no | Icon URL |
|
|
159
|
+
| `topics` | `string[]` | no | Topic tags for filtering |
|
|
160
|
+
| `capabilities` | `CapabilityDef[]` | no | Capability details (stored in event content) |
|
|
161
|
+
| `version` | `string` | no | Service version (stored in event content) |
|
|
162
|
+
|
|
163
|
+
## How it fits together
|
|
164
|
+
|
|
165
|
+
```mermaid
|
|
166
|
+
flowchart LR
|
|
167
|
+
subgraph "Your Server"
|
|
168
|
+
API[Your API]
|
|
169
|
+
TB[toll-booth<br/>L402 paywall]
|
|
170
|
+
AN[402-announce]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
subgraph "Nostr Network"
|
|
174
|
+
R1[(Relay 1)]
|
|
175
|
+
R2[(Relay 2)]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
subgraph "AI Agent"
|
|
179
|
+
MCP[402-mcp<br/>discovery + payment]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
API --> TB
|
|
183
|
+
TB -->|"protects"| API
|
|
184
|
+
AN -->|"kind 31402"| R1
|
|
185
|
+
AN -->|"kind 31402"| R2
|
|
186
|
+
R1 -->|"subscribe"| MCP
|
|
187
|
+
R2 -->|"subscribe"| MCP
|
|
188
|
+
MCP -->|"HTTP + L402 token"| TB
|
|
189
|
+
|
|
190
|
+
style TB fill:#ef4444,color:#fff
|
|
191
|
+
style AN fill:#f59e0b,color:#000
|
|
192
|
+
style MCP fill:#3b82f6,color:#fff
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
1. **[toll-booth](https://github.com/TheCryptoDonkey/toll-booth)** wraps your API with an L402 paywall
|
|
196
|
+
2. **402-announce** publishes a kind 31402 event describing the service, pricing, and payment methods
|
|
197
|
+
3. **[402-mcp](https://github.com/TheCryptoDonkey/402-mcp)** discovers the announcement, pays the invoice, and calls your API
|
|
198
|
+
|
|
199
|
+
## Security
|
|
200
|
+
|
|
201
|
+
- Secret key bytes are zeroised immediately after signing (in both `announceService` and `buildAnnounceEvent`)
|
|
202
|
+
- Never log or persist the secret key — pass it in and let the library handle cleanup
|
|
203
|
+
- Relay connections use a 10-second timeout to prevent hanging
|
|
204
|
+
- Individual relay failures are logged as warnings but don't reject the promise
|
|
205
|
+
|
|
73
206
|
## What it does
|
|
74
207
|
|
|
75
208
|
- Builds and signs kind 31402 Nostr events
|
|
76
|
-
- Publishes to one or more Nostr relays
|
|
209
|
+
- Publishes to one or more Nostr relays in parallel
|
|
77
210
|
- Zeroises secret key bytes after use
|
|
78
211
|
- Degrades gracefully when individual relays fail
|
|
79
212
|
- Provides a `close()` handle for clean disconnection
|
|
@@ -81,16 +214,16 @@ The event content is a JSON object with optional fields:
|
|
|
81
214
|
## What it does not do
|
|
82
215
|
|
|
83
216
|
- Does not run an L402 paywall (use [toll-booth](https://github.com/TheCryptoDonkey/toll-booth) for that)
|
|
84
|
-
- Does not subscribe to or search for announcements (use [
|
|
217
|
+
- Does not subscribe to or search for announcements (use [402-mcp](https://github.com/TheCryptoDonkey/402-mcp) for that)
|
|
85
218
|
- Does not handle payments or token verification
|
|
86
219
|
|
|
87
220
|
## Ecosystem
|
|
88
221
|
|
|
89
222
|
| Package | Purpose |
|
|
90
223
|
|---------|---------|
|
|
91
|
-
| [toll-booth](https://github.com/TheCryptoDonkey/toll-booth) | L402 middleware
|
|
224
|
+
| [toll-booth](https://github.com/TheCryptoDonkey/toll-booth) | L402 middleware — any API becomes a toll booth in minutes |
|
|
92
225
|
| [satgate](https://github.com/TheCryptoDonkey/satsgate) | Production L402 gateway with Lightning and Cashu support |
|
|
93
|
-
| [
|
|
226
|
+
| [402-mcp](https://github.com/TheCryptoDonkey/402-mcp) | MCP server for AI agents to discover, pay, and consume 402 APIs |
|
|
94
227
|
|
|
95
228
|
## Licence
|
|
96
229
|
|
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,48 @@ 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;
|
|
39
55
|
const relay = await Promise.race([
|
|
40
|
-
|
|
41
|
-
new Promise((_, reject) => setTimeout(() =>
|
|
42
|
-
|
|
56
|
+
connectPromise,
|
|
57
|
+
new Promise((_, reject) => setTimeout(() => {
|
|
58
|
+
timedOut = true;
|
|
59
|
+
reject(new Error(`Relay connection timeout: ${url}`));
|
|
60
|
+
}, 10_000)),
|
|
61
|
+
]).catch(async (err) => {
|
|
62
|
+
// If the timeout fired, wait for the connect promise to settle so we
|
|
63
|
+
// can close any relay that connected after the deadline.
|
|
64
|
+
if (timedOut) {
|
|
65
|
+
connectPromise.then((r) => r.close()).catch(() => { });
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
});
|
|
43
69
|
connectedRelays.push(relay);
|
|
44
70
|
await relay.publish(event);
|
|
45
71
|
accepted++;
|
|
@@ -54,7 +80,7 @@ export async function announceService(config) {
|
|
|
54
80
|
}
|
|
55
81
|
return {
|
|
56
82
|
eventId: event.id,
|
|
57
|
-
pubkey,
|
|
83
|
+
pubkey: event.pubkey,
|
|
58
84
|
close() {
|
|
59
85
|
for (const relay of connectedRelays) {
|
|
60
86
|
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 = [
|
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/types.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export interface PricingDef {
|
|
|
11
11
|
export interface CapabilityDef {
|
|
12
12
|
name: string;
|
|
13
13
|
description: string;
|
|
14
|
+
/** Optional JSON Schema describing the capability's input parameters. */
|
|
15
|
+
schema?: unknown;
|
|
16
|
+
/** Optional JSON Schema describing the capability's output. */
|
|
17
|
+
outputSchema?: unknown;
|
|
14
18
|
}
|
|
15
19
|
/** Configuration for announceService(). */
|
|
16
20
|
export interface AnnounceConfig {
|
package/build/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA0DA,gEAAgE;AAChE,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAA"}
|
package/build/utils.d.ts
CHANGED
|
@@ -1 +1,17 @@
|
|
|
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
|
|
8
|
+
* - 127.0.0.0/8 (IPv4 loopback)
|
|
9
|
+
* - ::1 (IPv6 loopback)
|
|
10
|
+
* - 0.0.0.0
|
|
11
|
+
* - 169.254.0.0/16 (IPv4 link-local)
|
|
12
|
+
* - fe80::/10 (IPv6 link-local)
|
|
13
|
+
* - 10.0.0.0/8 (RFC-1918)
|
|
14
|
+
* - 172.16.0.0/12 (RFC-1918)
|
|
15
|
+
* - 192.168.0.0/16 (RFC-1918)
|
|
16
|
+
*/
|
|
17
|
+
export declare function isPrivateHost(hostname: string): boolean;
|
package/build/utils.js
CHANGED
|
@@ -5,4 +5,50 @@ export function hexToBytes(hex) {
|
|
|
5
5
|
}
|
|
6
6
|
return bytes;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Returns true if the hostname resolves to a loopback, link-local, or
|
|
10
|
+
* RFC-1918 private address. Used to prevent SSRF via relay URLs.
|
|
11
|
+
*
|
|
12
|
+
* Rejects:
|
|
13
|
+
* - localhost
|
|
14
|
+
* - 127.0.0.0/8 (IPv4 loopback)
|
|
15
|
+
* - ::1 (IPv6 loopback)
|
|
16
|
+
* - 0.0.0.0
|
|
17
|
+
* - 169.254.0.0/16 (IPv4 link-local)
|
|
18
|
+
* - fe80::/10 (IPv6 link-local)
|
|
19
|
+
* - 10.0.0.0/8 (RFC-1918)
|
|
20
|
+
* - 172.16.0.0/12 (RFC-1918)
|
|
21
|
+
* - 192.168.0.0/16 (RFC-1918)
|
|
22
|
+
*/
|
|
23
|
+
export function isPrivateHost(hostname) {
|
|
24
|
+
const h = hostname.toLowerCase();
|
|
25
|
+
// Reject literal "localhost"
|
|
26
|
+
if (h === 'localhost')
|
|
27
|
+
return true;
|
|
28
|
+
// IPv6 loopback ::1 (may appear as [::1] in URLs — strip brackets)
|
|
29
|
+
const stripped = h.replace(/^\[|\]$/g, '');
|
|
30
|
+
if (stripped === '::1')
|
|
31
|
+
return true;
|
|
32
|
+
// IPv6 link-local fe80::/10 (prefix fe80 through febf)
|
|
33
|
+
if (/^fe[89ab][0-9a-f]:/i.test(stripped))
|
|
34
|
+
return true;
|
|
35
|
+
// Parse dotted-decimal IPv4
|
|
36
|
+
const ipv4 = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
37
|
+
if (ipv4) {
|
|
38
|
+
const [, a, b, c] = ipv4.map(Number);
|
|
39
|
+
if (a === 127)
|
|
40
|
+
return true; // 127.0.0.0/8
|
|
41
|
+
if (a === 0 && b === 0 && c === 0)
|
|
42
|
+
return true; // 0.0.0.0
|
|
43
|
+
if (a === 10)
|
|
44
|
+
return true; // 10.0.0.0/8
|
|
45
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
46
|
+
return true; // 172.16.0.0/12
|
|
47
|
+
if (a === 192 && b === 168)
|
|
48
|
+
return true; // 192.168.0.0/16
|
|
49
|
+
if (a === 169 && b === 254)
|
|
50
|
+
return true; // 169.254.0.0/16
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
8
54
|
//# 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":"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;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,MAAM,CAAC,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAA;IAEhC,6BAA6B;IAC7B,IAAI,CAAC,KAAK,WAAW;QAAE,OAAO,IAAI,CAAA;IAElC,mEAAmE;IACnE,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;IAC1C,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,IAAI,CAAA;IAEnC,uDAAuD;IACvD,IAAI,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAA;IAErD,4BAA4B;IAC5B,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAA;IACpE,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACpC,IAAI,CAAC,KAAK,GAAG;YAAE,OAAO,IAAI,CAAA,CAAsC,cAAc;QAC9E,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA,CAAkB,UAAU;QAC1E,IAAI,CAAC,KAAK,EAAE;YAAE,OAAO,IAAI,CAAA,CAAuC,aAAa;QAC7E,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;YAAE,OAAO,IAAI,CAAA,CAAe,gBAAgB;QAC/E,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;YAAE,OAAO,IAAI,CAAA,CAAyB,iBAAiB;QACjF,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;YAAE,OAAO,IAAI,CAAA,CAAyB,iBAAiB;IACnF,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC"}
|
package/llms-full.txt
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# 402-announce — Full Reference
|
|
2
|
+
|
|
3
|
+
> Announce HTTP 402 services on Nostr for decentralised discovery.
|
|
4
|
+
> Supports both L402 and x402 payment protocols.
|
|
5
|
+
> Source: https://github.com/TheCryptoDonkey/402-announce
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
402-announce is a TypeScript library that publishes kind 31402 Nostr events describing paid HTTP APIs. These events enable decentralised service discovery — AI agents and Nostr clients can find, evaluate, and pay for APIs without a central registry.
|
|
10
|
+
|
|
11
|
+
The library handles event construction, signing, and multi-relay publication. Secret key material is zeroised after use.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install 402-announce
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Node.js >= 18. ESM only. Single runtime dependency: nostr-tools.
|
|
20
|
+
|
|
21
|
+
## API Reference
|
|
22
|
+
|
|
23
|
+
### announceService(config: AnnounceConfig): Promise<Announcement>
|
|
24
|
+
|
|
25
|
+
High-level function: builds a kind 31402 event, signs it, connects to relays in parallel, and publishes.
|
|
26
|
+
|
|
27
|
+
**Parameters:**
|
|
28
|
+
|
|
29
|
+
AnnounceConfig:
|
|
30
|
+
- secretKey (string, required): 64-character hex-encoded Nostr secret key
|
|
31
|
+
- relays (string[], required): Relay URLs (must start with wss:// or ws://)
|
|
32
|
+
- identifier (string, required): Unique listing ID, used as the Nostr `d` tag. Same pubkey + identifier = same listing.
|
|
33
|
+
- name (string, required): Human-readable service name
|
|
34
|
+
- url (string, required): HTTP endpoint where the 402 service is accessible
|
|
35
|
+
- about (string, required): Short description of the service
|
|
36
|
+
- pricing (PricingDef[], required): Per-capability pricing. Each entry: { capability, price, currency }
|
|
37
|
+
- paymentMethods (string[], required): Accepted payment method identifiers
|
|
38
|
+
- picture (string, optional): Icon URL
|
|
39
|
+
- topics (string[], optional): Topic tags for search/filtering
|
|
40
|
+
- capabilities (CapabilityDef[], optional): Capability details stored in event content
|
|
41
|
+
- version (string, optional): Service version stored in event content
|
|
42
|
+
|
|
43
|
+
PricingDef:
|
|
44
|
+
- capability (string): Capability name (e.g. "get_joke", "chat", "inference")
|
|
45
|
+
- price (number): Price amount
|
|
46
|
+
- currency (string): Currency unit (e.g. "sats", "USD")
|
|
47
|
+
|
|
48
|
+
CapabilityDef:
|
|
49
|
+
- name (string): Capability name
|
|
50
|
+
- description (string): Human-readable description
|
|
51
|
+
|
|
52
|
+
**Returns** Announcement:
|
|
53
|
+
- eventId (string): The published Nostr event ID
|
|
54
|
+
- pubkey (string): Nostr pubkey derived from the secret key
|
|
55
|
+
- close(): void — Disconnect from all connected relays
|
|
56
|
+
|
|
57
|
+
**Behaviour:**
|
|
58
|
+
- Validates secretKey (must be 64-char hex) and relay URLs (must be wss:// or ws://)
|
|
59
|
+
- Connects to all relays in parallel with a 10-second timeout per relay
|
|
60
|
+
- Individual relay failures are logged as warnings but do not reject the promise
|
|
61
|
+
- If no relay accepts the event, a warning is emitted (but the promise still resolves)
|
|
62
|
+
- Secret key bytes are zeroised after signing
|
|
63
|
+
|
|
64
|
+
**Throws:**
|
|
65
|
+
- If secretKey is not a 64-character hex string
|
|
66
|
+
- If relays array is empty
|
|
67
|
+
- If any relay URL does not start with wss:// or ws://
|
|
68
|
+
|
|
69
|
+
### buildAnnounceEvent(secretKeyHex: string, config: Omit<AnnounceConfig, 'relays'>): VerifiedEvent
|
|
70
|
+
|
|
71
|
+
Lower-level function: builds and signs the kind 31402 event without publishing. Use this if you manage relay connections yourself.
|
|
72
|
+
|
|
73
|
+
**Returns** a nostr-tools VerifiedEvent ready for relay publication.
|
|
74
|
+
|
|
75
|
+
Secret key bytes are zeroised after signing (via try/finally).
|
|
76
|
+
|
|
77
|
+
### Constants
|
|
78
|
+
|
|
79
|
+
- L402_ANNOUNCE_KIND = 31402 — The Nostr event kind for service announcements
|
|
80
|
+
|
|
81
|
+
### Exported types
|
|
82
|
+
|
|
83
|
+
- AnnounceConfig — Configuration for announceService()
|
|
84
|
+
- Announcement — Handle returned by announceService()
|
|
85
|
+
- PricingDef — Pricing for a specific capability
|
|
86
|
+
- CapabilityDef — Capability with description
|
|
87
|
+
|
|
88
|
+
## Event Format Specification
|
|
89
|
+
|
|
90
|
+
### Kind 31402 — Parameterised Replaceable Event
|
|
91
|
+
|
|
92
|
+
A kind 31402 event is a Nostr parameterised replaceable event (NIP-33). This means:
|
|
93
|
+
- The combination of pubkey + kind + d-tag uniquely identifies a listing
|
|
94
|
+
- Publishing a new event with the same pubkey + kind + d-tag replaces the previous one
|
|
95
|
+
- This allows services to update their announcements (e.g. change pricing) without creating duplicates
|
|
96
|
+
|
|
97
|
+
### Tag structure
|
|
98
|
+
|
|
99
|
+
Required tags:
|
|
100
|
+
["d", "<identifier>"] — Unique listing identifier
|
|
101
|
+
["name", "<service name>"] — Human-readable name
|
|
102
|
+
["url", "<endpoint URL>"] — HTTP endpoint
|
|
103
|
+
["about", "<description>"] — Short description
|
|
104
|
+
["pmi", "<payment method>"] — Payment method identifier (one tag per method)
|
|
105
|
+
["price", "<capability>", "<amount>", "<currency>"] — Pricing (one tag per capability)
|
|
106
|
+
|
|
107
|
+
Optional tags:
|
|
108
|
+
["t", "<topic>"] — Topic tag for filtering (one tag per topic)
|
|
109
|
+
["picture", "<icon URL>"] — Service icon
|
|
110
|
+
|
|
111
|
+
### Content field
|
|
112
|
+
|
|
113
|
+
JSON object with optional fields:
|
|
114
|
+
{
|
|
115
|
+
"capabilities": [
|
|
116
|
+
{ "name": "get_joke", "description": "Returns a random joke" }
|
|
117
|
+
],
|
|
118
|
+
"version": "1.0.0"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
### Example event
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
"kind": 31402,
|
|
125
|
+
"pubkey": "ab1234...ef5678",
|
|
126
|
+
"created_at": 1710000000,
|
|
127
|
+
"tags": [
|
|
128
|
+
["d", "jokes-api"],
|
|
129
|
+
["name", "Jokes API"],
|
|
130
|
+
["url", "https://jokes.example.com"],
|
|
131
|
+
["about", "A joke-telling service behind an L402 paywall"],
|
|
132
|
+
["pmi", "bitcoin-lightning-bolt11"],
|
|
133
|
+
["pmi", "bitcoin-cashu"],
|
|
134
|
+
["price", "get_joke", "1", "sats"],
|
|
135
|
+
["t", "comedy"],
|
|
136
|
+
["t", "ai"]
|
|
137
|
+
],
|
|
138
|
+
"content": "{\"capabilities\":[{\"name\":\"get_joke\",\"description\":\"Returns a random joke\"}],\"version\":\"1.0.0\"}",
|
|
139
|
+
"id": "...",
|
|
140
|
+
"sig": "..."
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
## Payment Method Identifiers
|
|
144
|
+
|
|
145
|
+
Known payment method identifiers for the `pmi` tag and `paymentMethods` config:
|
|
146
|
+
|
|
147
|
+
bitcoin-lightning-bolt11 — Lightning Network BOLT-11 invoices
|
|
148
|
+
bitcoin-cashu — Cashu ecash tokens
|
|
149
|
+
bitcoin-onchain — On-chain Bitcoin
|
|
150
|
+
x402-evm — x402 protocol on EVM chains
|
|
151
|
+
x402-solana — x402 protocol on Solana
|
|
152
|
+
|
|
153
|
+
## Integration Patterns
|
|
154
|
+
|
|
155
|
+
### Producer side: toll-booth + 402-announce
|
|
156
|
+
|
|
157
|
+
A typical server setup protects an API with toll-booth and announces it with 402-announce:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { createTollBooth } from 'toll-booth'
|
|
161
|
+
import { announceService } from '402-announce'
|
|
162
|
+
|
|
163
|
+
// 1. Protect your API with a paywall
|
|
164
|
+
const booth = createTollBooth({ /* payment config */ })
|
|
165
|
+
app.use('/api', booth.middleware)
|
|
166
|
+
|
|
167
|
+
// 2. Announce it on Nostr
|
|
168
|
+
const handle = await announceService({
|
|
169
|
+
secretKey: process.env.NOSTR_SECRET_KEY,
|
|
170
|
+
relays: ['wss://relay.damus.io'],
|
|
171
|
+
identifier: 'my-api',
|
|
172
|
+
name: 'My API',
|
|
173
|
+
url: 'https://api.example.com',
|
|
174
|
+
about: 'A paid API service',
|
|
175
|
+
pricing: [{ capability: 'query', price: 10, currency: 'sats' }],
|
|
176
|
+
paymentMethods: ['bitcoin-lightning-bolt11'],
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// 3. Clean up on shutdown
|
|
180
|
+
process.on('SIGTERM', () => handle.close())
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Consumer side: 402-mcp
|
|
184
|
+
|
|
185
|
+
AI agents use 402-mcp to discover and consume announced services:
|
|
186
|
+
|
|
187
|
+
1. 402-mcp subscribes to kind 31402 events on configured relays
|
|
188
|
+
2. Agent searches for services by topic, capability, or payment method
|
|
189
|
+
3. 402-mcp handles L402 payment negotiation automatically
|
|
190
|
+
4. Agent receives the API response
|
|
191
|
+
|
|
192
|
+
The agent never needs to know about Nostr or payment protocols — 402-mcp abstracts it all.
|
|
193
|
+
|
|
194
|
+
### Updating a listing
|
|
195
|
+
|
|
196
|
+
Because kind 31402 is a parameterised replaceable event, calling announceService() again with the same identifier and pubkey replaces the previous listing. Use this to update pricing, add capabilities, or change the endpoint URL.
|
|
197
|
+
|
|
198
|
+
## Security Considerations
|
|
199
|
+
|
|
200
|
+
- **Key zeroisation**: Secret key bytes (Uint8Array) are filled with zeros immediately after signing, in both announceService() and buildAnnounceEvent(). This limits the window during which key material exists in memory.
|
|
201
|
+
- **Key handling**: Pass the secret key as a hex string. The library converts to bytes, signs, and zeroises. Never log or persist the key yourself.
|
|
202
|
+
- **Relay trust**: Relays are untrusted infrastructure. Events are cryptographically signed — relay operators cannot forge announcements. However, relays can choose not to store or serve your events.
|
|
203
|
+
- **Connection timeouts**: Relay connections time out after 10 seconds to prevent indefinite hangs.
|
|
204
|
+
- **Graceful degradation**: If some relays fail, the event is still published to the others. A warning is emitted only if no relay accepts the event.
|
|
205
|
+
|
|
206
|
+
## Error Handling
|
|
207
|
+
|
|
208
|
+
The library throws synchronously for invalid configuration:
|
|
209
|
+
- secretKey not 64-char hex → Error
|
|
210
|
+
- Empty relays array → Error
|
|
211
|
+
- Invalid relay URL (not wss:// or ws://) → Error
|
|
212
|
+
|
|
213
|
+
Relay-level failures during publication are handled gracefully:
|
|
214
|
+
- Connection timeouts → warning logged, other relays still tried
|
|
215
|
+
- Publish rejections → warning logged, other relays still tried
|
|
216
|
+
- All relays fail → warning logged, promise resolves with eventId from the signed event
|
|
217
|
+
|
|
218
|
+
## Source
|
|
219
|
+
|
|
220
|
+
Repository: https://github.com/TheCryptoDonkey/402-announce
|
|
221
|
+
Licence: MIT
|
|
222
|
+
Runtime dependency: nostr-tools
|
package/llms.txt
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# 402-announce
|
|
2
|
+
|
|
3
|
+
> Announce HTTP 402 services on Nostr for decentralised discovery
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
402-announce publishes kind 31402 parameterised replaceable Nostr events that describe paid HTTP APIs. AI agents and Nostr clients discover these announcements to find services they can pay for and consume — no central registry needed.
|
|
8
|
+
|
|
9
|
+
## Key concepts
|
|
10
|
+
|
|
11
|
+
- **Kind 31402**: Nostr event kind for L402/x402 service announcements. Parameterised replaceable — same pubkey + identifier updates the existing listing.
|
|
12
|
+
- **Payment methods**: Services declare accepted payment methods via `pmi` tags (e.g. `bitcoin-lightning-bolt11`, `bitcoin-cashu`).
|
|
13
|
+
- **Pricing**: Per-capability pricing via `price` tags (capability name, amount, currency).
|
|
14
|
+
- **Decentralised discovery**: Events are published to standard Nostr relays. Any client can subscribe and filter.
|
|
15
|
+
|
|
16
|
+
## API surface
|
|
17
|
+
|
|
18
|
+
Two exports:
|
|
19
|
+
|
|
20
|
+
- `announceService(config)` — build, sign, and publish to relays. Returns `{ eventId, pubkey, close() }`.
|
|
21
|
+
- `buildAnnounceEvent(secretKey, config)` — build and sign without publishing. Returns a `VerifiedEvent`.
|
|
22
|
+
|
|
23
|
+
## Ecosystem position
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
toll-booth (paywall) → protects your API
|
|
27
|
+
402-announce (this) → tells the world your API exists
|
|
28
|
+
402-mcp (discovery) → AI agents find, pay, and consume your API
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Details
|
|
32
|
+
|
|
33
|
+
For full API reference, event format, integration patterns, and examples see:
|
|
34
|
+
- llms-full.txt (comprehensive reference)
|
|
35
|
+
- README.md (human-oriented documentation with diagrams)
|
|
36
|
+
- Source: https://github.com/TheCryptoDonkey/402-announce
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "402-announce",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Announce HTTP 402 services (L402 and x402) on Nostr for decentralised discovery. Kind 31402 parameterised replaceable events.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"build",
|
|
17
17
|
"LICENSE",
|
|
18
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"llms.txt",
|
|
20
|
+
"llms-full.txt"
|
|
19
21
|
],
|
|
20
22
|
"scripts": {
|
|
21
23
|
"build": "tsc",
|