@0xopenseeddev/registry 1.0.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/package.json +18 -0
- package/src/index.js +161 -0
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@0xopenseeddev/registry",
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"@0xopenseeddev/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,161 @@
|
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import cors from '@fastify/cors';
|
|
3
|
+
import { REGISTRY_PORT, isAlive, errorBody, PrometheusRegistry } from '@0xopenseeddev/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('openseed_registry_peers_total', 'Total registered peers'),
|
|
13
|
+
peersAlive: reg.gauge('openseed_registry_peers_alive', 'Live peers (seen within 60s)'),
|
|
14
|
+
registrations: reg.counter('openseed_registry_registrations_total', 'Total peer registration calls'),
|
|
15
|
+
heartbeats: reg.counter('openseed_registry_heartbeats_total', 'Total heartbeat calls'),
|
|
16
|
+
uptimeSeconds: reg.gauge('openseed_registry_uptime_seconds', 'Registry uptime in seconds'),
|
|
17
|
+
pruned: reg.counter('openseed_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
|
+
await app.register(cors, { origin: '*' });
|
|
36
|
+
|
|
37
|
+
// ── GET /health ───────────────────────────────────────────────────────────────
|
|
38
|
+
app.get('/health', async () => ({
|
|
39
|
+
status: 'ok',
|
|
40
|
+
peers: peers.size,
|
|
41
|
+
alive: [...peers.values()].filter(isAlive).length
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// ── GET /metrics (Prometheus) ────────────────────────────────────────────────
|
|
45
|
+
app.get('/metrics', async (req, reply) => {
|
|
46
|
+
const alive = [...peers.values()].filter(isAlive).length;
|
|
47
|
+
m.peersTotal.set({}, peers.size);
|
|
48
|
+
m.peersAlive.set({}, alive);
|
|
49
|
+
m.uptimeSeconds.set({}, Math.floor((Date.now() - _startTime) / 1000));
|
|
50
|
+
reply.header('content-type', 'text/plain; version=0.0.4; charset=utf-8');
|
|
51
|
+
return reply.send(reg.format());
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ── POST /peers/register ──────────────────────────────────────────────────────
|
|
55
|
+
app.post('/peers/register', {
|
|
56
|
+
schema: {
|
|
57
|
+
body: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
required: ['peerId', 'endpoint', 'offerings'],
|
|
60
|
+
properties: {
|
|
61
|
+
peerId: { type: 'string' },
|
|
62
|
+
endpoint: { type: 'string' },
|
|
63
|
+
offerings: { type: 'array' },
|
|
64
|
+
merchantAddress: { type: 'string' }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}, async (req, reply) => {
|
|
69
|
+
const { peerId, endpoint, offerings, merchantAddress } = req.body;
|
|
70
|
+
|
|
71
|
+
if (!peerId || !endpoint || !Array.isArray(offerings)) {
|
|
72
|
+
return reply.status(400).send(errorBody('bad_request', 'peerId, endpoint, and offerings are required'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const existing = peers.get(peerId);
|
|
77
|
+
|
|
78
|
+
peers.set(peerId, {
|
|
79
|
+
peerId,
|
|
80
|
+
endpoint,
|
|
81
|
+
offerings,
|
|
82
|
+
merchantAddress: merchantAddress ?? '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
|
|
83
|
+
registeredAt: existing?.registeredAt ?? now,
|
|
84
|
+
lastSeen: now
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
m.registrations.inc();
|
|
88
|
+
app.log.info(`[registry] registered: ${peerId.slice(0, 12)}… (${offerings.length} offerings)`);
|
|
89
|
+
return { ok: true, peerId };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── POST /peers/heartbeat ─────────────────────────────────────────────────────
|
|
93
|
+
app.post('/peers/heartbeat', {
|
|
94
|
+
schema: {
|
|
95
|
+
body: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
required: ['peerId'],
|
|
98
|
+
properties: { peerId: { type: 'string' } }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, async (req, reply) => {
|
|
102
|
+
const { peerId } = req.body;
|
|
103
|
+
const peer = peers.get(peerId);
|
|
104
|
+
|
|
105
|
+
if (!peer) {
|
|
106
|
+
return reply.status(404).send(errorBody('not_found', 'Peer not registered. Call /peers/register first.'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
peer.lastSeen = Date.now();
|
|
110
|
+
m.heartbeats.inc();
|
|
111
|
+
return { ok: true };
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── GET /peers ────────────────────────────────────────────────────────────────
|
|
115
|
+
app.get('/peers', async (req) => {
|
|
116
|
+
const { model } = req.query;
|
|
117
|
+
let results = [...peers.values()].filter(isAlive);
|
|
118
|
+
|
|
119
|
+
if (model) {
|
|
120
|
+
results = results.filter(p =>
|
|
121
|
+
p.offerings?.some(o => o.services?.includes(model))
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Sort by most recently seen
|
|
126
|
+
results.sort((a, b) => b.lastSeen - a.lastSeen);
|
|
127
|
+
return { peers: results };
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── GET /peers/:peerId ────────────────────────────────────────────────────────
|
|
131
|
+
app.get('/peers/:peerId', async (req, reply) => {
|
|
132
|
+
const peer = peers.get(req.params.peerId);
|
|
133
|
+
if (!peer || !isAlive(peer)) {
|
|
134
|
+
return reply.status(404).send(errorBody('not_found', 'Peer not found or offline'));
|
|
135
|
+
}
|
|
136
|
+
return peer;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── DELETE /peers/:peerId ─────────────────────────────────────────────────────
|
|
140
|
+
app.delete('/peers/:peerId', async (req, reply) => {
|
|
141
|
+
if (!peers.has(req.params.peerId)) {
|
|
142
|
+
return reply.status(404).send(errorBody('not_found', 'Peer not found'));
|
|
143
|
+
}
|
|
144
|
+
peers.delete(req.params.peerId);
|
|
145
|
+
return { ok: true };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
149
|
+
try {
|
|
150
|
+
await app.listen({ port: REGISTRY_PORT, host: '0.0.0.0' });
|
|
151
|
+
console.log(`
|
|
152
|
+
🐜 OpenSeed Registry http://0.0.0.0:${REGISTRY_PORT}
|
|
153
|
+
|
|
154
|
+
GET /peers list live peers
|
|
155
|
+
GET /metrics Prometheus metrics
|
|
156
|
+
GET /health liveness
|
|
157
|
+
`);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
app.log.error(err);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|