@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.
Files changed (2) hide show
  1. package/package.json +18 -0
  2. 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
+ }