soulprint-network 0.1.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/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +7 -0
- package/dist/validator.d.ts +12 -0
- package/dist/validator.js +238 -0
- package/package.json +47 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BOOTSTRAP_NODES = exports.getNodeInfo = exports.submitToNode = exports.startValidatorNode = void 0;
|
|
4
|
+
var validator_js_1 = require("./validator.js");
|
|
5
|
+
Object.defineProperty(exports, "startValidatorNode", { enumerable: true, get: function () { return validator_js_1.startValidatorNode; } });
|
|
6
|
+
Object.defineProperty(exports, "submitToNode", { enumerable: true, get: function () { return validator_js_1.submitToNode; } });
|
|
7
|
+
Object.defineProperty(exports, "getNodeInfo", { enumerable: true, get: function () { return validator_js_1.getNodeInfo; } });
|
|
8
|
+
Object.defineProperty(exports, "BOOTSTRAP_NODES", { enumerable: true, get: function () { return validator_js_1.BOOTSTRAP_NODES; } });
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
// Entrypoint del nodo validador — ejecutar con: node dist/server.js
|
|
5
|
+
const validator_js_1 = require("./validator.js");
|
|
6
|
+
const port = parseInt(process.env.SOULPRINT_PORT ?? "4888");
|
|
7
|
+
(0, validator_js_1.startValidatorNode)(port);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
export declare function startValidatorNode(port?: number): import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
3
|
+
export interface NodeVerifyResult {
|
|
4
|
+
valid: boolean;
|
|
5
|
+
co_signature: string;
|
|
6
|
+
nullifier: string;
|
|
7
|
+
node_did: string;
|
|
8
|
+
anti_sybil: "new" | "existing";
|
|
9
|
+
}
|
|
10
|
+
export declare function submitToNode(nodeUrl: string, spt: string, zkProof: string): Promise<NodeVerifyResult>;
|
|
11
|
+
export declare function getNodeInfo(nodeUrl: string): Promise<any>;
|
|
12
|
+
export declare const BOOTSTRAP_NODES: string[];
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BOOTSTRAP_NODES = void 0;
|
|
4
|
+
exports.startValidatorNode = startValidatorNode;
|
|
5
|
+
exports.submitToNode = submitToNode;
|
|
6
|
+
exports.getNodeInfo = getNodeInfo;
|
|
7
|
+
const node_http_1 = require("node:http");
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const node_os_1 = require("node:os");
|
|
11
|
+
const soulprint_core_1 = require("soulprint-core");
|
|
12
|
+
const soulprint_zkp_1 = require("soulprint-zkp");
|
|
13
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
14
|
+
const PORT = parseInt(process.env.SOULPRINT_PORT ?? "4888");
|
|
15
|
+
const NODE_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".soulprint", "node");
|
|
16
|
+
const KEYPAIR_FILE = (0, node_path_1.join)(NODE_DIR, "node-identity.json");
|
|
17
|
+
const NULLIFIER_DB = (0, node_path_1.join)(NODE_DIR, "nullifiers.json");
|
|
18
|
+
const VERSION = "0.1.0";
|
|
19
|
+
const MAX_BODY_BYTES = 64 * 1024; // 64KB max
|
|
20
|
+
const RATE_LIMIT_MS = 60_000; // 1 min window
|
|
21
|
+
const RATE_LIMIT_MAX = 10; // 10 req/min/IP
|
|
22
|
+
const CLOCK_SKEW_MAX = 300; // ±5 min
|
|
23
|
+
// ── Rate limiter ──────────────────────────────────────────────────────────────
|
|
24
|
+
const rateLimits = new Map();
|
|
25
|
+
function checkRateLimit(ip) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const entry = rateLimits.get(ip);
|
|
28
|
+
if (!entry || now > entry.resetAt) {
|
|
29
|
+
rateLimits.set(ip, { count: 1, resetAt: now + RATE_LIMIT_MS });
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (entry.count >= RATE_LIMIT_MAX)
|
|
33
|
+
return false;
|
|
34
|
+
entry.count++;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
setInterval(() => {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
for (const [ip, e] of rateLimits)
|
|
40
|
+
if (now > e.resetAt)
|
|
41
|
+
rateLimits.delete(ip);
|
|
42
|
+
}, 5 * 60_000).unref();
|
|
43
|
+
// ── Nullifier registry ────────────────────────────────────────────────────────
|
|
44
|
+
let nullifiers = {};
|
|
45
|
+
function loadNullifiers() {
|
|
46
|
+
if ((0, node_fs_1.existsSync)(NULLIFIER_DB)) {
|
|
47
|
+
try {
|
|
48
|
+
nullifiers = JSON.parse((0, node_fs_1.readFileSync)(NULLIFIER_DB, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
nullifiers = {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function saveNullifiers() { (0, node_fs_1.writeFileSync)(NULLIFIER_DB, JSON.stringify(nullifiers, null, 2)); }
|
|
56
|
+
// ── Node keypair ──────────────────────────────────────────────────────────────
|
|
57
|
+
function loadOrCreateNodeKeypair() {
|
|
58
|
+
if (!(0, node_fs_1.existsSync)(NODE_DIR))
|
|
59
|
+
(0, node_fs_1.mkdirSync)(NODE_DIR, { recursive: true, mode: 0o700 });
|
|
60
|
+
if ((0, node_fs_1.existsSync)(KEYPAIR_FILE)) {
|
|
61
|
+
try {
|
|
62
|
+
const s = JSON.parse((0, node_fs_1.readFileSync)(KEYPAIR_FILE, "utf8"));
|
|
63
|
+
return (0, soulprint_core_1.keypairFromPrivateKey)(new Uint8Array(Buffer.from(s.privateKey, "hex")));
|
|
64
|
+
}
|
|
65
|
+
catch { /* regenerar */ }
|
|
66
|
+
}
|
|
67
|
+
const kp = (0, soulprint_core_1.generateKeypair)();
|
|
68
|
+
(0, node_fs_1.writeFileSync)(KEYPAIR_FILE, JSON.stringify({ did: kp.did, privateKey: Buffer.from(kp.privateKey).toString("hex"), created: new Date().toISOString() }), { mode: 0o600 });
|
|
69
|
+
console.log(`✅ Nuevo nodo: ${kp.did}`);
|
|
70
|
+
return kp;
|
|
71
|
+
}
|
|
72
|
+
// ── HTTP helpers ──────────────────────────────────────────────────────────────
|
|
73
|
+
const SECURITY_HEADERS = {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"X-Soulprint-Node": VERSION,
|
|
76
|
+
"X-Content-Type-Options": "nosniff",
|
|
77
|
+
"X-Frame-Options": "DENY",
|
|
78
|
+
};
|
|
79
|
+
function json(res, status, body) {
|
|
80
|
+
res.writeHead(status, SECURITY_HEADERS);
|
|
81
|
+
res.end(JSON.stringify(body));
|
|
82
|
+
}
|
|
83
|
+
async function readBody(req) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
let data = "", size = 0;
|
|
86
|
+
req.on("data", chunk => {
|
|
87
|
+
size += chunk.length;
|
|
88
|
+
if (size > MAX_BODY_BYTES) {
|
|
89
|
+
req.destroy();
|
|
90
|
+
reject(new Error("Request too large (max 64KB)"));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
data += chunk;
|
|
94
|
+
});
|
|
95
|
+
req.on("end", () => { try {
|
|
96
|
+
resolve(JSON.parse(data));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
reject(new Error("Invalid JSON"));
|
|
100
|
+
} });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function getIP(req) {
|
|
104
|
+
const fwd = req.headers["x-forwarded-for"];
|
|
105
|
+
return (typeof fwd === "string" ? fwd.split(",")[0].trim() : req.socket.remoteAddress) ?? "unknown";
|
|
106
|
+
}
|
|
107
|
+
// ── GET /info ─────────────────────────────────────────────────────────────────
|
|
108
|
+
function handleInfo(res, nodeKeypair) {
|
|
109
|
+
json(res, 200, {
|
|
110
|
+
node_did: nodeKeypair.did,
|
|
111
|
+
version: VERSION,
|
|
112
|
+
protocol: "sip/0.1",
|
|
113
|
+
total_verified: Object.keys(nullifiers).length,
|
|
114
|
+
supported_countries: ["CO"],
|
|
115
|
+
capabilities: ["zk-verify", "anti-sybil", "co-sign"],
|
|
116
|
+
rate_limit: `${RATE_LIMIT_MAX} req/min per IP`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// ── POST /verify ──────────────────────────────────────────────────────────────
|
|
120
|
+
async function handleVerify(req, res, nodeKeypair, ip) {
|
|
121
|
+
if (!checkRateLimit(ip))
|
|
122
|
+
return json(res, 429, { error: "Rate limit exceeded. Try again in 1 minute." });
|
|
123
|
+
let body;
|
|
124
|
+
try {
|
|
125
|
+
body = await readBody(req);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return json(res, 400, { error: e.message });
|
|
129
|
+
}
|
|
130
|
+
const { spt, zkp } = body ?? {};
|
|
131
|
+
if (!spt || !zkp)
|
|
132
|
+
return json(res, 400, { error: "Missing required fields: spt, zkp" });
|
|
133
|
+
if (typeof spt !== "string" || typeof zkp !== "string")
|
|
134
|
+
return json(res, 400, { error: "spt and zkp must be strings" });
|
|
135
|
+
// Verify SPT
|
|
136
|
+
const token = (0, soulprint_core_1.decodeToken)(spt);
|
|
137
|
+
if (!token)
|
|
138
|
+
return json(res, 401, { error: "Invalid or expired SPT" });
|
|
139
|
+
// Clock skew check
|
|
140
|
+
const now = Math.floor(Date.now() / 1000);
|
|
141
|
+
if (Math.abs(token.issued - now) > CLOCK_SKEW_MAX) {
|
|
142
|
+
return json(res, 400, { error: "Clock skew too large", max_skew_seconds: CLOCK_SKEW_MAX });
|
|
143
|
+
}
|
|
144
|
+
// Verify ZK proof
|
|
145
|
+
let zkResult;
|
|
146
|
+
try {
|
|
147
|
+
const proof = (0, soulprint_zkp_1.deserializeProof)(zkp);
|
|
148
|
+
zkResult = await (0, soulprint_zkp_1.verifyProof)(proof);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
return json(res, 400, { error: `ZK proof error: ${e.message?.slice(0, 100)}` });
|
|
152
|
+
}
|
|
153
|
+
if (!zkResult.valid)
|
|
154
|
+
return json(res, 403, { error: "ZK proof is not valid" });
|
|
155
|
+
if (!zkResult.nullifier)
|
|
156
|
+
return json(res, 400, { error: "No nullifier in ZK proof" });
|
|
157
|
+
// Anti-Sybil
|
|
158
|
+
const existing = nullifiers[zkResult.nullifier];
|
|
159
|
+
let antiSybil = "new";
|
|
160
|
+
if (existing) {
|
|
161
|
+
if (existing.did !== token.did) {
|
|
162
|
+
return json(res, 409, {
|
|
163
|
+
error: "Anti-Sybil: this nullifier is already registered with a different DID",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
antiSybil = "existing";
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
nullifiers[zkResult.nullifier] = { did: token.did, verified_at: now };
|
|
170
|
+
saveNullifiers();
|
|
171
|
+
}
|
|
172
|
+
const coSig = (0, soulprint_core_1.sign)({ nullifier: zkResult.nullifier, did: token.did, timestamp: now }, nodeKeypair.privateKey);
|
|
173
|
+
json(res, 200, {
|
|
174
|
+
valid: true,
|
|
175
|
+
anti_sybil: antiSybil,
|
|
176
|
+
nullifier: zkResult.nullifier,
|
|
177
|
+
node_did: nodeKeypair.did,
|
|
178
|
+
co_signature: coSig,
|
|
179
|
+
verified_at: now,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// ── GET /nullifier/:hash ──────────────────────────────────────────────────────
|
|
183
|
+
function handleNullifierCheck(res, nullifier) {
|
|
184
|
+
if (!/^(0x)?[0-9a-fA-F]{1,128}$/.test(nullifier))
|
|
185
|
+
return json(res, 400, { error: "Invalid nullifier format" });
|
|
186
|
+
const entry = nullifiers[nullifier];
|
|
187
|
+
if (!entry)
|
|
188
|
+
return json(res, 404, { registered: false });
|
|
189
|
+
json(res, 200, { registered: true, verified_at: entry.verified_at });
|
|
190
|
+
}
|
|
191
|
+
// ── Server ────────────────────────────────────────────────────────────────────
|
|
192
|
+
function startValidatorNode(port = PORT) {
|
|
193
|
+
loadNullifiers();
|
|
194
|
+
const nodeKeypair = loadOrCreateNodeKeypair();
|
|
195
|
+
const server = (0, node_http_1.createServer)(async (req, res) => {
|
|
196
|
+
const ip = getIP(req);
|
|
197
|
+
const url = req.url ?? "/";
|
|
198
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
199
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
200
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
201
|
+
if (req.method === "OPTIONS") {
|
|
202
|
+
res.writeHead(204);
|
|
203
|
+
res.end();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (url === "/info" && req.method === "GET")
|
|
207
|
+
return handleInfo(res, nodeKeypair);
|
|
208
|
+
if (url === "/verify" && req.method === "POST")
|
|
209
|
+
return handleVerify(req, res, nodeKeypair, ip);
|
|
210
|
+
if (url.startsWith("/nullifier/") && req.method === "GET")
|
|
211
|
+
return handleNullifierCheck(res, decodeURIComponent(url.replace("/nullifier/", "")));
|
|
212
|
+
json(res, 404, { error: "Not found" });
|
|
213
|
+
});
|
|
214
|
+
server.listen(port, () => {
|
|
215
|
+
console.log(`🌐 Soulprint Validator Node v${VERSION}`);
|
|
216
|
+
console.log(` Node DID: ${nodeKeypair.did}`);
|
|
217
|
+
console.log(` Listening on http://0.0.0.0:${port}`);
|
|
218
|
+
console.log(` Nullifiers registered: ${Object.keys(nullifiers).length}`);
|
|
219
|
+
console.log(` Rate limit: ${RATE_LIMIT_MAX} req/min per IP`);
|
|
220
|
+
console.log(`\n POST /verify verify ZK proof + co-sign SPT`);
|
|
221
|
+
console.log(` GET /info node info`);
|
|
222
|
+
console.log(` GET /nullifier/:n check nullifier`);
|
|
223
|
+
console.log(`\n Anyone can run a Soulprint node. More nodes = more security.`);
|
|
224
|
+
});
|
|
225
|
+
return server;
|
|
226
|
+
}
|
|
227
|
+
async function submitToNode(nodeUrl, spt, zkProof) {
|
|
228
|
+
const res = await fetch(`${nodeUrl}/verify`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ spt, zkp: zkProof }) });
|
|
229
|
+
const data = await res.json();
|
|
230
|
+
if (!res.ok)
|
|
231
|
+
throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
232
|
+
return data;
|
|
233
|
+
}
|
|
234
|
+
async function getNodeInfo(nodeUrl) {
|
|
235
|
+
return (await fetch(`${nodeUrl}/info`)).json();
|
|
236
|
+
}
|
|
237
|
+
// Bootstrap nodes — add yours via PR: https://github.com/manuelariasfz/soulprint
|
|
238
|
+
exports.BOOTSTRAP_NODES = [];
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "soulprint-network",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Soulprint validator node — HTTP server that verifies ZK proofs, co-signs SPTs, anti-Sybil registry",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"soulprint-node": "dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/manuelariasfz/soulprint",
|
|
20
|
+
"directory": "packages/network"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/manuelariasfz/soulprint#readme",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"soulprint",
|
|
25
|
+
"validator-node",
|
|
26
|
+
"kyc",
|
|
27
|
+
"anti-sybil",
|
|
28
|
+
"zero-knowledge",
|
|
29
|
+
"decentralized"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"soulprint-core": "0.1.0",
|
|
34
|
+
"soulprint-zkp": "0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.4.0",
|
|
38
|
+
"@types/node": "^20.0.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"start": "node dist/server.js"
|
|
46
|
+
}
|
|
47
|
+
}
|