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