openshard-registry 1.0.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 +18 -0
- package/package.json +18 -0
- package/src/index.js +172 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @openshard/registry
|
|
2
|
+
|
|
3
|
+
> Peer discovery and liveness monitoring service for the OpenShard network.
|
|
4
|
+
|
|
5
|
+
The **OpenShard Registry** is a lightweight, high-performance Fastify server that tracks active seller nodes, advertises AI offerings, and prunes offline peers automatically.
|
|
6
|
+
|
|
7
|
+
## API Endpoints
|
|
8
|
+
|
|
9
|
+
- `POST /peers/register`: Registers a new seller node with its public endpoint, peer ID, merchant address, and available AI offerings.
|
|
10
|
+
- `POST /peers/heartbeat`: Periodic liveness ping sent by active sellers (pruned after 90 seconds of inactivity).
|
|
11
|
+
- `GET /peers`: Lists all live, healthy peers sorted by recency and filtered by supported models.
|
|
12
|
+
- `GET /metrics`: Observability endpoint exporting standard Prometheus metrics.
|
|
13
|
+
|
|
14
|
+
## Running
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm run dev --workspace=packages/registry
|
|
18
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openshard-registry",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Discovery & registry service — lets buyers find sellers",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "node --watch src/index.js",
|
|
9
|
+
"start": "node src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"openshard-shared": "*",
|
|
13
|
+
"@fastify/cors": "^10.0.1",
|
|
14
|
+
"fastify": "^5.2.1"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"license": "ISC"
|
|
18
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import cors from '@fastify/cors';
|
|
3
|
+
import { REGISTRY_PORT, isAlive, errorBody, PrometheusRegistry } from 'openshard-shared';
|
|
4
|
+
|
|
5
|
+
// ─── In-memory peer store ─────────────────────────────────────────────────────
|
|
6
|
+
const peers = new Map();
|
|
7
|
+
const _startTime = Date.now();
|
|
8
|
+
|
|
9
|
+
// ─── Prometheus metrics ───────────────────────────────────────────────────────
|
|
10
|
+
const reg = new PrometheusRegistry();
|
|
11
|
+
const m = {
|
|
12
|
+
peersTotal: reg.gauge('openshard_registry_peers_total', 'Total registered peers'),
|
|
13
|
+
peersAlive: reg.gauge('openshard_registry_peers_alive', 'Live peers (seen within 60s)'),
|
|
14
|
+
registrations: reg.counter('openshard_registry_registrations_total', 'Total peer registration calls'),
|
|
15
|
+
heartbeats: reg.counter('openshard_registry_heartbeats_total', 'Total heartbeat calls'),
|
|
16
|
+
uptimeSeconds: reg.gauge('openshard_registry_uptime_seconds', 'Registry uptime in seconds'),
|
|
17
|
+
pruned: reg.counter('openshard_registry_pruned_total', 'Total stale peers pruned')
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// ─── Prune dead peers every 90 seconds ───────────────────────────────────────
|
|
21
|
+
setInterval(() => {
|
|
22
|
+
let pruned = 0;
|
|
23
|
+
for (const [id, peer] of peers) {
|
|
24
|
+
if (!isAlive(peer)) {
|
|
25
|
+
console.log(`[registry] pruning stale peer: ${id.slice(0, 12)}…`);
|
|
26
|
+
peers.delete(id);
|
|
27
|
+
pruned++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (pruned > 0) m.pruned.inc({}, pruned);
|
|
31
|
+
}, 90_000);
|
|
32
|
+
|
|
33
|
+
// ─── Fastify instance ─────────────────────────────────────────────────────────
|
|
34
|
+
const app = Fastify({ logger: true });
|
|
35
|
+
app.addHook('onRequest', (req, reply, done) => {
|
|
36
|
+
reply.header('Access-Control-Allow-Private-Network', 'true');
|
|
37
|
+
reply.header('Access-Control-Allow-Origin', req.headers.origin || '*');
|
|
38
|
+
reply.header('Access-Control-Allow-Credentials', 'true');
|
|
39
|
+
reply.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
|
|
40
|
+
reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-anteseed-pin-peer, anthropic-version, x-402-payment-auth, x-402-channel-id, x-openshard-buyer-address');
|
|
41
|
+
if (req.method === 'OPTIONS') {
|
|
42
|
+
return reply.status(200).send();
|
|
43
|
+
}
|
|
44
|
+
done();
|
|
45
|
+
});
|
|
46
|
+
await app.register(cors, { origin: true, credentials: true });
|
|
47
|
+
|
|
48
|
+
// ── GET /health ───────────────────────────────────────────────────────────────
|
|
49
|
+
app.get('/health', async () => ({
|
|
50
|
+
status: 'ok',
|
|
51
|
+
peers: peers.size,
|
|
52
|
+
alive: [...peers.values()].filter(isAlive).length
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// ── GET /metrics (Prometheus) ────────────────────────────────────────────────
|
|
56
|
+
app.get('/metrics', async (req, reply) => {
|
|
57
|
+
const alive = [...peers.values()].filter(isAlive).length;
|
|
58
|
+
m.peersTotal.set({}, peers.size);
|
|
59
|
+
m.peersAlive.set({}, alive);
|
|
60
|
+
m.uptimeSeconds.set({}, Math.floor((Date.now() - _startTime) / 1000));
|
|
61
|
+
reply.header('content-type', 'text/plain; version=0.0.4; charset=utf-8');
|
|
62
|
+
return reply.send(reg.format());
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── POST /peers/register ──────────────────────────────────────────────────────
|
|
66
|
+
app.post('/peers/register', {
|
|
67
|
+
schema: {
|
|
68
|
+
body: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
required: ['peerId', 'endpoint', 'offerings'],
|
|
71
|
+
properties: {
|
|
72
|
+
peerId: { type: 'string' },
|
|
73
|
+
endpoint: { type: 'string' },
|
|
74
|
+
offerings: { type: 'array' },
|
|
75
|
+
merchantAddress: { type: 'string' }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, async (req, reply) => {
|
|
80
|
+
const { peerId, endpoint, offerings, merchantAddress } = req.body;
|
|
81
|
+
|
|
82
|
+
if (!peerId || !endpoint || !Array.isArray(offerings)) {
|
|
83
|
+
return reply.status(400).send(errorBody('bad_request', 'peerId, endpoint, and offerings are required'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
const existing = peers.get(peerId);
|
|
88
|
+
|
|
89
|
+
peers.set(peerId, {
|
|
90
|
+
peerId,
|
|
91
|
+
endpoint,
|
|
92
|
+
offerings,
|
|
93
|
+
merchantAddress: merchantAddress ?? '0xYOUR_MERCHANT_ADDRESS_HERE',
|
|
94
|
+
registeredAt: existing?.registeredAt ?? now,
|
|
95
|
+
lastSeen: now
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
m.registrations.inc();
|
|
99
|
+
app.log.info(`[registry] registered: ${peerId.slice(0, 12)}… (${offerings.length} offerings)`);
|
|
100
|
+
return { ok: true, peerId };
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── POST /peers/heartbeat ─────────────────────────────────────────────────────
|
|
104
|
+
app.post('/peers/heartbeat', {
|
|
105
|
+
schema: {
|
|
106
|
+
body: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
required: ['peerId'],
|
|
109
|
+
properties: { peerId: { type: 'string' } }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}, async (req, reply) => {
|
|
113
|
+
const { peerId } = req.body;
|
|
114
|
+
const peer = peers.get(peerId);
|
|
115
|
+
|
|
116
|
+
if (!peer) {
|
|
117
|
+
return reply.status(404).send(errorBody('not_found', 'Peer not registered. Call /peers/register first.'));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
peer.lastSeen = Date.now();
|
|
121
|
+
m.heartbeats.inc();
|
|
122
|
+
return { ok: true };
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── GET /peers ────────────────────────────────────────────────────────────────
|
|
126
|
+
app.get('/peers', async (req) => {
|
|
127
|
+
const { model } = req.query;
|
|
128
|
+
let results = [...peers.values()].filter(isAlive);
|
|
129
|
+
|
|
130
|
+
if (model) {
|
|
131
|
+
results = results.filter(p =>
|
|
132
|
+
p.offerings?.some(o => o.services?.includes(model))
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Sort by most recently seen
|
|
137
|
+
results.sort((a, b) => b.lastSeen - a.lastSeen);
|
|
138
|
+
return { peers: results };
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── GET /peers/:peerId ────────────────────────────────────────────────────────
|
|
142
|
+
app.get('/peers/:peerId', async (req, reply) => {
|
|
143
|
+
const peer = peers.get(req.params.peerId);
|
|
144
|
+
if (!peer || !isAlive(peer)) {
|
|
145
|
+
return reply.status(404).send(errorBody('not_found', 'Peer not found or offline'));
|
|
146
|
+
}
|
|
147
|
+
return peer;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── DELETE /peers/:peerId ─────────────────────────────────────────────────────
|
|
151
|
+
app.delete('/peers/:peerId', async (req, reply) => {
|
|
152
|
+
if (!peers.has(req.params.peerId)) {
|
|
153
|
+
return reply.status(404).send(errorBody('not_found', 'Peer not found'));
|
|
154
|
+
}
|
|
155
|
+
peers.delete(req.params.peerId);
|
|
156
|
+
return { ok: true };
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
160
|
+
try {
|
|
161
|
+
await app.listen({ port: REGISTRY_PORT, host: '0.0.0.0' });
|
|
162
|
+
console.log(`
|
|
163
|
+
🐜 OpenShard Registry http://0.0.0.0:${REGISTRY_PORT}
|
|
164
|
+
|
|
165
|
+
GET /peers list live peers
|
|
166
|
+
GET /metrics Prometheus metrics
|
|
167
|
+
GET /health liveness
|
|
168
|
+
`);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
app.log.error(err);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|