soulprint-network 0.2.1 → 0.2.2
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/credentials/email.d.ts +33 -0
- package/dist/credentials/email.js +110 -0
- package/dist/credentials/github.d.ts +44 -0
- package/dist/credentials/github.js +148 -0
- package/dist/credentials/index.d.ts +34 -0
- package/dist/credentials/index.js +173 -0
- package/dist/credentials/phone.d.ts +42 -0
- package/dist/credentials/phone.js +99 -0
- package/dist/validator.js +85 -20
- package/package.json +6 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email OTP Credential Validator
|
|
3
|
+
*
|
|
4
|
+
* Flujo:
|
|
5
|
+
* 1. POST /credentials/email/start { did, email }
|
|
6
|
+
* → genera OTP de 6 dígitos, válido 10 min
|
|
7
|
+
* → envía email via SMTP (nodemailer)
|
|
8
|
+
* → retorna { sessionId }
|
|
9
|
+
*
|
|
10
|
+
* 2. POST /credentials/email/verify { sessionId, otp }
|
|
11
|
+
* → si el OTP coincide → emite attestation "EmailVerified" sobre el DID
|
|
12
|
+
* → retorna { credential: "EmailVerified", did }
|
|
13
|
+
*
|
|
14
|
+
* Configuración (env vars):
|
|
15
|
+
* SMTP_HOST — servidor SMTP (ej: smtp.gmail.com)
|
|
16
|
+
* SMTP_PORT — puerto (default 587)
|
|
17
|
+
* SMTP_USER — usuario SMTP
|
|
18
|
+
* SMTP_PASS — contraseña SMTP
|
|
19
|
+
* SMTP_FROM — remitente (default "noreply@soulprint.digital")
|
|
20
|
+
*
|
|
21
|
+
* En desarrollo sin SMTP configurado: usa Ethereal (catch-all fake SMTP)
|
|
22
|
+
* para testing — los emails no se envían pero son capturables.
|
|
23
|
+
*/
|
|
24
|
+
export declare function startEmailVerification(did: string, email: string): Promise<{
|
|
25
|
+
sessionId: string;
|
|
26
|
+
preview?: string;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function verifyEmailOTP(sessionId: string, otp: string): {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
did?: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
reason?: string;
|
|
33
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email OTP Credential Validator
|
|
3
|
+
*
|
|
4
|
+
* Flujo:
|
|
5
|
+
* 1. POST /credentials/email/start { did, email }
|
|
6
|
+
* → genera OTP de 6 dígitos, válido 10 min
|
|
7
|
+
* → envía email via SMTP (nodemailer)
|
|
8
|
+
* → retorna { sessionId }
|
|
9
|
+
*
|
|
10
|
+
* 2. POST /credentials/email/verify { sessionId, otp }
|
|
11
|
+
* → si el OTP coincide → emite attestation "EmailVerified" sobre el DID
|
|
12
|
+
* → retorna { credential: "EmailVerified", did }
|
|
13
|
+
*
|
|
14
|
+
* Configuración (env vars):
|
|
15
|
+
* SMTP_HOST — servidor SMTP (ej: smtp.gmail.com)
|
|
16
|
+
* SMTP_PORT — puerto (default 587)
|
|
17
|
+
* SMTP_USER — usuario SMTP
|
|
18
|
+
* SMTP_PASS — contraseña SMTP
|
|
19
|
+
* SMTP_FROM — remitente (default "noreply@soulprint.digital")
|
|
20
|
+
*
|
|
21
|
+
* En desarrollo sin SMTP configurado: usa Ethereal (catch-all fake SMTP)
|
|
22
|
+
* para testing — los emails no se envían pero son capturables.
|
|
23
|
+
*/
|
|
24
|
+
import nodemailer from "nodemailer";
|
|
25
|
+
import { randomBytes, randomInt } from "node:crypto";
|
|
26
|
+
// TTL de la sesión OTP (10 minutos)
|
|
27
|
+
const OTP_TTL_MS = 10 * 60 * 1000;
|
|
28
|
+
// Sessions en memoria (el nodo limpia las expiradas cada 5 min)
|
|
29
|
+
const emailSessions = new Map();
|
|
30
|
+
setInterval(() => {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
for (const [id, s] of emailSessions) {
|
|
33
|
+
if (s.expiresAt < now)
|
|
34
|
+
emailSessions.delete(id);
|
|
35
|
+
}
|
|
36
|
+
}, 5 * 60_000).unref();
|
|
37
|
+
// Transportador SMTP (lazy init)
|
|
38
|
+
let transport = null;
|
|
39
|
+
async function getTransport() {
|
|
40
|
+
if (transport)
|
|
41
|
+
return transport;
|
|
42
|
+
if (process.env.SMTP_HOST) {
|
|
43
|
+
transport = nodemailer.createTransport({
|
|
44
|
+
host: process.env.SMTP_HOST,
|
|
45
|
+
port: parseInt(process.env.SMTP_PORT ?? "587"),
|
|
46
|
+
secure: process.env.SMTP_PORT === "465",
|
|
47
|
+
auth: {
|
|
48
|
+
user: process.env.SMTP_USER,
|
|
49
|
+
pass: process.env.SMTP_PASS,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Dev: usa Ethereal (emails no se envían, se loguean)
|
|
55
|
+
const testAccount = await nodemailer.createTestAccount();
|
|
56
|
+
transport = nodemailer.createTransport({
|
|
57
|
+
host: "smtp.ethereal.email",
|
|
58
|
+
port: 587,
|
|
59
|
+
secure: false,
|
|
60
|
+
auth: { user: testAccount.user, pass: testAccount.pass },
|
|
61
|
+
});
|
|
62
|
+
console.log("[email-cred] Dev mode — usando Ethereal SMTP");
|
|
63
|
+
}
|
|
64
|
+
return transport;
|
|
65
|
+
}
|
|
66
|
+
export async function startEmailVerification(did, email) {
|
|
67
|
+
// Validar email básico
|
|
68
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
69
|
+
throw new Error("Invalid email address");
|
|
70
|
+
}
|
|
71
|
+
// Generar OTP de 6 dígitos con crypto.randomInt (CSPRNG)
|
|
72
|
+
const otp = randomInt(100000, 999999).toString();
|
|
73
|
+
const sessionId = randomBytes(16).toString("hex");
|
|
74
|
+
emailSessions.set(sessionId, {
|
|
75
|
+
did, email, otp,
|
|
76
|
+
expiresAt: Date.now() + OTP_TTL_MS,
|
|
77
|
+
attempts: 0,
|
|
78
|
+
});
|
|
79
|
+
const transporter = await getTransport();
|
|
80
|
+
const info = await transporter.sendMail({
|
|
81
|
+
from: process.env.SMTP_FROM ?? "noreply@soulprint.digital",
|
|
82
|
+
to: email,
|
|
83
|
+
subject: "Soulprint — Your verification code",
|
|
84
|
+
text: `Your Soulprint verification code is: ${otp}\n\nExpires in 10 minutes. Do not share this code.`,
|
|
85
|
+
html: `<p>Your Soulprint verification code is: <strong style="font-size:1.5em">${otp}</strong></p><p>Expires in 10 minutes. Do not share this code.</p>`,
|
|
86
|
+
});
|
|
87
|
+
const preview = nodemailer.getTestMessageUrl(info) || undefined;
|
|
88
|
+
if (preview)
|
|
89
|
+
console.log(`[email-cred] Preview: ${preview}`);
|
|
90
|
+
return { sessionId, preview };
|
|
91
|
+
}
|
|
92
|
+
export function verifyEmailOTP(sessionId, otp) {
|
|
93
|
+
const session = emailSessions.get(sessionId);
|
|
94
|
+
if (!session)
|
|
95
|
+
return { ok: false, reason: "Session not found or expired" };
|
|
96
|
+
if (Date.now() > session.expiresAt) {
|
|
97
|
+
emailSessions.delete(sessionId);
|
|
98
|
+
return { ok: false, reason: "OTP expired" };
|
|
99
|
+
}
|
|
100
|
+
session.attempts++;
|
|
101
|
+
if (session.attempts > 5) {
|
|
102
|
+
emailSessions.delete(sessionId);
|
|
103
|
+
return { ok: false, reason: "Too many attempts" };
|
|
104
|
+
}
|
|
105
|
+
if (otp.trim() !== session.otp)
|
|
106
|
+
return { ok: false, reason: "Invalid OTP" };
|
|
107
|
+
// OTP correcto — limpiar sesión y retornar
|
|
108
|
+
emailSessions.delete(sessionId);
|
|
109
|
+
return { ok: true, did: session.did, email: session.email };
|
|
110
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Credential Validator — native fetch only, no extra deps
|
|
3
|
+
*
|
|
4
|
+
* Usa el flujo estándar OAuth 2.0 de GitHub (open source, bien documentado).
|
|
5
|
+
* No requiere librerías extra — solo fetch nativo (disponible en Node 18+).
|
|
6
|
+
*
|
|
7
|
+
* Flujo:
|
|
8
|
+
* 1. GET /credentials/github/start?did=<did>
|
|
9
|
+
* → retorna { authUrl } — el cliente redirige al usuario a authUrl
|
|
10
|
+
* authUrl = "https://github.com/login/oauth/authorize?..."
|
|
11
|
+
*
|
|
12
|
+
* 2. GitHub redirige a /credentials/github/callback?code=<code>&state=<state>
|
|
13
|
+
* → intercambia code por access_token
|
|
14
|
+
* → obtiene perfil del usuario (id, login, email)
|
|
15
|
+
* → emite GitHubLinked credential
|
|
16
|
+
* → retorna { credential: "GitHubLinked", did, githubLogin }
|
|
17
|
+
*
|
|
18
|
+
* Configuración (env vars):
|
|
19
|
+
* GITHUB_CLIENT_ID — GitHub OAuth App Client ID
|
|
20
|
+
* GITHUB_CLIENT_SECRET — GitHub OAuth App Client Secret
|
|
21
|
+
* SOULPRINT_BASE_URL — URL pública del nodo (para callback)
|
|
22
|
+
* ej: "https://my-validator.example.com"
|
|
23
|
+
*
|
|
24
|
+
* Crear OAuth App: https://github.com/settings/applications/new
|
|
25
|
+
* - Homepage URL: tu dominio del nodo
|
|
26
|
+
* - Callback URL: https://tu-nodo/credentials/github/callback
|
|
27
|
+
*
|
|
28
|
+
* Para desarrollo sin GitHub App configurada:
|
|
29
|
+
* → el endpoint retorna instrucciones para crearla
|
|
30
|
+
*/
|
|
31
|
+
export declare function startGitHubOAuth(did: string): {
|
|
32
|
+
authUrl?: string;
|
|
33
|
+
state?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
setup?: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function handleGitHubCallback(code: string, state: string): Promise<{
|
|
38
|
+
ok: boolean;
|
|
39
|
+
did?: string;
|
|
40
|
+
githubId?: number;
|
|
41
|
+
githubLogin?: string;
|
|
42
|
+
email?: string;
|
|
43
|
+
reason?: string;
|
|
44
|
+
}>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Credential Validator — native fetch only, no extra deps
|
|
3
|
+
*
|
|
4
|
+
* Usa el flujo estándar OAuth 2.0 de GitHub (open source, bien documentado).
|
|
5
|
+
* No requiere librerías extra — solo fetch nativo (disponible en Node 18+).
|
|
6
|
+
*
|
|
7
|
+
* Flujo:
|
|
8
|
+
* 1. GET /credentials/github/start?did=<did>
|
|
9
|
+
* → retorna { authUrl } — el cliente redirige al usuario a authUrl
|
|
10
|
+
* authUrl = "https://github.com/login/oauth/authorize?..."
|
|
11
|
+
*
|
|
12
|
+
* 2. GitHub redirige a /credentials/github/callback?code=<code>&state=<state>
|
|
13
|
+
* → intercambia code por access_token
|
|
14
|
+
* → obtiene perfil del usuario (id, login, email)
|
|
15
|
+
* → emite GitHubLinked credential
|
|
16
|
+
* → retorna { credential: "GitHubLinked", did, githubLogin }
|
|
17
|
+
*
|
|
18
|
+
* Configuración (env vars):
|
|
19
|
+
* GITHUB_CLIENT_ID — GitHub OAuth App Client ID
|
|
20
|
+
* GITHUB_CLIENT_SECRET — GitHub OAuth App Client Secret
|
|
21
|
+
* SOULPRINT_BASE_URL — URL pública del nodo (para callback)
|
|
22
|
+
* ej: "https://my-validator.example.com"
|
|
23
|
+
*
|
|
24
|
+
* Crear OAuth App: https://github.com/settings/applications/new
|
|
25
|
+
* - Homepage URL: tu dominio del nodo
|
|
26
|
+
* - Callback URL: https://tu-nodo/credentials/github/callback
|
|
27
|
+
*
|
|
28
|
+
* Para desarrollo sin GitHub App configurada:
|
|
29
|
+
* → el endpoint retorna instrucciones para crearla
|
|
30
|
+
*/
|
|
31
|
+
import { randomBytes } from "node:crypto";
|
|
32
|
+
const STATE_TTL_MS = 10 * 60 * 1000; // 10 minutos para completar OAuth
|
|
33
|
+
const stateStore = new Map();
|
|
34
|
+
setInterval(() => {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const [s, v] of stateStore) {
|
|
37
|
+
if (v.expiresAt < now)
|
|
38
|
+
stateStore.delete(s);
|
|
39
|
+
}
|
|
40
|
+
}, 5 * 60_000).unref();
|
|
41
|
+
function getConfig() {
|
|
42
|
+
const clientId = process.env.GITHUB_CLIENT_ID;
|
|
43
|
+
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
|
|
44
|
+
const baseUrl = process.env.SOULPRINT_BASE_URL ?? "http://localhost:4888";
|
|
45
|
+
return { clientId, clientSecret, baseUrl, configured: !!(clientId && clientSecret) };
|
|
46
|
+
}
|
|
47
|
+
export function startGitHubOAuth(did) {
|
|
48
|
+
const cfg = getConfig();
|
|
49
|
+
if (!cfg.configured) {
|
|
50
|
+
return {
|
|
51
|
+
error: "GitHub OAuth not configured on this validator node.",
|
|
52
|
+
setup: [
|
|
53
|
+
"To enable GitHubLinked verification:",
|
|
54
|
+
"1. Create a GitHub OAuth App at https://github.com/settings/applications/new",
|
|
55
|
+
" - Homepage URL: your validator's public URL",
|
|
56
|
+
" - Callback URL: <SOULPRINT_BASE_URL>/credentials/github/callback",
|
|
57
|
+
"2. Set env vars: GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, SOULPRINT_BASE_URL",
|
|
58
|
+
"3. Restart the validator node",
|
|
59
|
+
].join("\n"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const state = randomBytes(16).toString("hex");
|
|
63
|
+
stateStore.set(state, { did, expiresAt: Date.now() + STATE_TTL_MS, used: false });
|
|
64
|
+
const params = new URLSearchParams({
|
|
65
|
+
client_id: cfg.clientId,
|
|
66
|
+
redirect_uri: `${cfg.baseUrl}/credentials/github/callback`,
|
|
67
|
+
scope: "read:user user:email",
|
|
68
|
+
state,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
authUrl: `https://github.com/login/oauth/authorize?${params}`,
|
|
72
|
+
state,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export async function handleGitHubCallback(code, state) {
|
|
76
|
+
const cfg = getConfig();
|
|
77
|
+
if (!cfg.configured)
|
|
78
|
+
return { ok: false, reason: "GitHub OAuth not configured" };
|
|
79
|
+
// Verificar state
|
|
80
|
+
const stored = stateStore.get(state);
|
|
81
|
+
if (!stored)
|
|
82
|
+
return { ok: false, reason: "Invalid or expired state" };
|
|
83
|
+
if (stored.used)
|
|
84
|
+
return { ok: false, reason: "State already used" };
|
|
85
|
+
if (Date.now() > stored.expiresAt) {
|
|
86
|
+
stateStore.delete(state);
|
|
87
|
+
return { ok: false, reason: "State expired" };
|
|
88
|
+
}
|
|
89
|
+
stored.used = true;
|
|
90
|
+
// Intercambiar code por access_token
|
|
91
|
+
const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: {
|
|
94
|
+
"Accept": "application/json",
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
client_id: cfg.clientId,
|
|
99
|
+
client_secret: cfg.clientSecret,
|
|
100
|
+
code,
|
|
101
|
+
redirect_uri: `${cfg.baseUrl}/credentials/github/callback`,
|
|
102
|
+
}),
|
|
103
|
+
signal: AbortSignal.timeout(10_000),
|
|
104
|
+
});
|
|
105
|
+
if (!tokenRes.ok)
|
|
106
|
+
return { ok: false, reason: `GitHub token exchange failed: HTTP ${tokenRes.status}` };
|
|
107
|
+
const tokenData = await tokenRes.json();
|
|
108
|
+
if (tokenData.error)
|
|
109
|
+
return { ok: false, reason: `GitHub OAuth error: ${tokenData.error_description ?? tokenData.error}` };
|
|
110
|
+
const accessToken = tokenData.access_token;
|
|
111
|
+
// Obtener perfil del usuario
|
|
112
|
+
const userRes = await fetch("https://api.github.com/user", {
|
|
113
|
+
headers: {
|
|
114
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
115
|
+
"Accept": "application/vnd.github+json",
|
|
116
|
+
"User-Agent": "Soulprint-Validator/0.2.1",
|
|
117
|
+
},
|
|
118
|
+
signal: AbortSignal.timeout(10_000),
|
|
119
|
+
});
|
|
120
|
+
if (!userRes.ok)
|
|
121
|
+
return { ok: false, reason: `GitHub user fetch failed: HTTP ${userRes.status}` };
|
|
122
|
+
const user = await userRes.json();
|
|
123
|
+
// Opcionalmente obtener email privado
|
|
124
|
+
let email;
|
|
125
|
+
try {
|
|
126
|
+
const emailRes = await fetch("https://api.github.com/user/emails", {
|
|
127
|
+
headers: {
|
|
128
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
129
|
+
"Accept": "application/vnd.github+json",
|
|
130
|
+
"User-Agent": "Soulprint-Validator/0.2.1",
|
|
131
|
+
},
|
|
132
|
+
signal: AbortSignal.timeout(5_000),
|
|
133
|
+
});
|
|
134
|
+
if (emailRes.ok) {
|
|
135
|
+
const emails = await emailRes.json();
|
|
136
|
+
email = emails.find(e => e.primary && e.verified)?.email;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch { /* email opcional */ }
|
|
140
|
+
stateStore.delete(state);
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
did: stored.did,
|
|
144
|
+
githubId: user.id,
|
|
145
|
+
githubLogin: user.login,
|
|
146
|
+
email,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential Router — integra todos los validators de credenciales
|
|
3
|
+
* con el servidor HTTP del nodo validador.
|
|
4
|
+
*
|
|
5
|
+
* Endpoints añadidos:
|
|
6
|
+
*
|
|
7
|
+
* Email:
|
|
8
|
+
* POST /credentials/email/start { did, email }
|
|
9
|
+
* POST /credentials/email/verify { sessionId, otp }
|
|
10
|
+
*
|
|
11
|
+
* Phone (TOTP — sin SMS, sin API key):
|
|
12
|
+
* POST /credentials/phone/start { did, phone }
|
|
13
|
+
* POST /credentials/phone/verify { sessionId, code }
|
|
14
|
+
*
|
|
15
|
+
* GitHub OAuth:
|
|
16
|
+
* GET /credentials/github/start ?did=<did>
|
|
17
|
+
* GET /credentials/github/callback?code=<code>&state=<state>
|
|
18
|
+
*
|
|
19
|
+
* Biometric (ya existe via /verify — solo se documenta aquí):
|
|
20
|
+
* POST /verify { spt, zkp } → emite BiometricBound credential implícita
|
|
21
|
+
*
|
|
22
|
+
* Una vez verificado, el validator emite una BotAttestation con:
|
|
23
|
+
* context: "credential:EmailVerified" | "credential:PhoneVerified" |
|
|
24
|
+
* "credential:GitHubLinked"
|
|
25
|
+
* Esta attestation se gossipea al resto de la red P2P automáticamente.
|
|
26
|
+
*/
|
|
27
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
28
|
+
import type { BotAttestation, SoulprintKeypair } from "soulprint-core";
|
|
29
|
+
export type CredentialContext = {
|
|
30
|
+
nodeKeypair: SoulprintKeypair;
|
|
31
|
+
signAttestation: (att: Omit<BotAttestation, "sig">) => BotAttestation;
|
|
32
|
+
gossip: (att: BotAttestation) => void;
|
|
33
|
+
};
|
|
34
|
+
export declare function handleCredentialRoute(req: IncomingMessage, res: ServerResponse, url: string, ctx: CredentialContext): Promise<boolean>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential Router — integra todos los validators de credenciales
|
|
3
|
+
* con el servidor HTTP del nodo validador.
|
|
4
|
+
*
|
|
5
|
+
* Endpoints añadidos:
|
|
6
|
+
*
|
|
7
|
+
* Email:
|
|
8
|
+
* POST /credentials/email/start { did, email }
|
|
9
|
+
* POST /credentials/email/verify { sessionId, otp }
|
|
10
|
+
*
|
|
11
|
+
* Phone (TOTP — sin SMS, sin API key):
|
|
12
|
+
* POST /credentials/phone/start { did, phone }
|
|
13
|
+
* POST /credentials/phone/verify { sessionId, code }
|
|
14
|
+
*
|
|
15
|
+
* GitHub OAuth:
|
|
16
|
+
* GET /credentials/github/start ?did=<did>
|
|
17
|
+
* GET /credentials/github/callback?code=<code>&state=<state>
|
|
18
|
+
*
|
|
19
|
+
* Biometric (ya existe via /verify — solo se documenta aquí):
|
|
20
|
+
* POST /verify { spt, zkp } → emite BiometricBound credential implícita
|
|
21
|
+
*
|
|
22
|
+
* Una vez verificado, el validator emite una BotAttestation con:
|
|
23
|
+
* context: "credential:EmailVerified" | "credential:PhoneVerified" |
|
|
24
|
+
* "credential:GitHubLinked"
|
|
25
|
+
* Esta attestation se gossipea al resto de la red P2P automáticamente.
|
|
26
|
+
*/
|
|
27
|
+
import { startEmailVerification, verifyEmailOTP, } from "./email.js";
|
|
28
|
+
import { startPhoneVerification, verifyPhoneTOTP, } from "./phone.js";
|
|
29
|
+
import { startGitHubOAuth, handleGitHubCallback, } from "./github.js";
|
|
30
|
+
// ── HTTP helpers ──────────────────────────────────────────────────────────────
|
|
31
|
+
function jsonResp(res, status, body) {
|
|
32
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
33
|
+
res.end(JSON.stringify(body));
|
|
34
|
+
}
|
|
35
|
+
async function readBody(req) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
let data = "";
|
|
38
|
+
req.on("data", c => { data += c; if (data.length > 32_768) {
|
|
39
|
+
req.destroy();
|
|
40
|
+
reject(new Error("Body too large"));
|
|
41
|
+
} });
|
|
42
|
+
req.on("end", () => { try {
|
|
43
|
+
resolve(JSON.parse(data));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
reject(new Error("Invalid JSON"));
|
|
47
|
+
} });
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// ── Router ────────────────────────────────────────────────────────────────────
|
|
51
|
+
export async function handleCredentialRoute(req, res, url, ctx) {
|
|
52
|
+
const method = req.method ?? "GET";
|
|
53
|
+
const qs = new URL(url, "http://localhost").searchParams;
|
|
54
|
+
// ── Email ──────────────────────────────────────────────────────────────────
|
|
55
|
+
if (url.startsWith("/credentials/email/start") && method === "POST") {
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = await readBody(req);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
return jsonResp(res, 400, { error: e.message }), true;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const result = await startEmailVerification(body.did, body.email);
|
|
65
|
+
jsonResp(res, 200, { ok: true, ...result, message: "OTP sent to your email. Check your inbox." });
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
jsonResp(res, 400, { ok: false, error: e.message });
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (url.startsWith("/credentials/email/verify") && method === "POST") {
|
|
73
|
+
let body;
|
|
74
|
+
try {
|
|
75
|
+
body = await readBody(req);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return jsonResp(res, 400, { error: e.message }), true;
|
|
79
|
+
}
|
|
80
|
+
const result = verifyEmailOTP(body.sessionId, body.otp);
|
|
81
|
+
if (!result.ok)
|
|
82
|
+
return jsonResp(res, 403, { ok: false, reason: result.reason }), true;
|
|
83
|
+
const att = ctx.signAttestation({
|
|
84
|
+
issuer_did: ctx.nodeKeypair.did,
|
|
85
|
+
target_did: result.did,
|
|
86
|
+
value: 1,
|
|
87
|
+
context: "credential:EmailVerified",
|
|
88
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
89
|
+
});
|
|
90
|
+
ctx.gossip(att);
|
|
91
|
+
jsonResp(res, 200, { ok: true, credential: "EmailVerified", did: result.did, email: result.email, attestation: att });
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
// ── Phone ──────────────────────────────────────────────────────────────────
|
|
95
|
+
if (url.startsWith("/credentials/phone/start") && method === "POST") {
|
|
96
|
+
let body;
|
|
97
|
+
try {
|
|
98
|
+
body = await readBody(req);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return jsonResp(res, 400, { error: e.message }), true;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const result = startPhoneVerification(body.did, body.phone);
|
|
105
|
+
jsonResp(res, 200, { ok: true, ...result });
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
jsonResp(res, 400, { ok: false, error: e.message });
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (url.startsWith("/credentials/phone/verify") && method === "POST") {
|
|
113
|
+
let body;
|
|
114
|
+
try {
|
|
115
|
+
body = await readBody(req);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
return jsonResp(res, 400, { error: e.message }), true;
|
|
119
|
+
}
|
|
120
|
+
const result = verifyPhoneTOTP(body.sessionId, body.code);
|
|
121
|
+
if (!result.ok)
|
|
122
|
+
return jsonResp(res, 403, { ok: false, reason: result.reason }), true;
|
|
123
|
+
const att = ctx.signAttestation({
|
|
124
|
+
issuer_did: ctx.nodeKeypair.did,
|
|
125
|
+
target_did: result.did,
|
|
126
|
+
value: 1,
|
|
127
|
+
context: "credential:PhoneVerified",
|
|
128
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
129
|
+
});
|
|
130
|
+
ctx.gossip(att);
|
|
131
|
+
jsonResp(res, 200, { ok: true, credential: "PhoneVerified", did: result.did, phone: result.phone, attestation: att });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
// ── GitHub OAuth ───────────────────────────────────────────────────────────
|
|
135
|
+
if (url.startsWith("/credentials/github/start") && method === "GET") {
|
|
136
|
+
const did = qs.get("did");
|
|
137
|
+
if (!did)
|
|
138
|
+
return jsonResp(res, 400, { error: "Missing query param: did" }), true;
|
|
139
|
+
const result = startGitHubOAuth(did);
|
|
140
|
+
if (result.error)
|
|
141
|
+
return jsonResp(res, 503, { ok: false, ...result }), true;
|
|
142
|
+
// Redirect al usuario a GitHub
|
|
143
|
+
res.writeHead(302, { Location: result.authUrl });
|
|
144
|
+
res.end();
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (url.startsWith("/credentials/github/callback") && method === "GET") {
|
|
148
|
+
const code = qs.get("code");
|
|
149
|
+
const state = qs.get("state");
|
|
150
|
+
if (!code || !state)
|
|
151
|
+
return jsonResp(res, 400, { error: "Missing code or state" }), true;
|
|
152
|
+
const result = await handleGitHubCallback(code, state);
|
|
153
|
+
if (!result.ok)
|
|
154
|
+
return jsonResp(res, 403, { ok: false, reason: result.reason }), true;
|
|
155
|
+
const att = ctx.signAttestation({
|
|
156
|
+
issuer_did: ctx.nodeKeypair.did,
|
|
157
|
+
target_did: result.did,
|
|
158
|
+
value: 1,
|
|
159
|
+
context: "credential:GitHubLinked",
|
|
160
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
161
|
+
});
|
|
162
|
+
ctx.gossip(att);
|
|
163
|
+
jsonResp(res, 200, {
|
|
164
|
+
ok: true,
|
|
165
|
+
credential: "GitHubLinked",
|
|
166
|
+
did: result.did,
|
|
167
|
+
github: { id: result.githubId, login: result.githubLogin },
|
|
168
|
+
attestation: att,
|
|
169
|
+
});
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false; // no es una ruta de credenciales
|
|
173
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phone TOTP Credential Validator — sin servicio externo
|
|
3
|
+
*
|
|
4
|
+
* Usa TOTP (RFC 6238) — el mismo estándar de Google Authenticator / Authy.
|
|
5
|
+
* No requiere SMS, no requiere API key, funciona completamente offline.
|
|
6
|
+
*
|
|
7
|
+
* Flujo:
|
|
8
|
+
* 1. POST /credentials/phone/start { did, phone }
|
|
9
|
+
* → genera un TOTP secret único para este DID
|
|
10
|
+
* → retorna { sessionId, totpUri, qrData }
|
|
11
|
+
* totpUri = "otpauth://totp/Soulprint:+57..."
|
|
12
|
+
* qrData = string para generar QR (usar qrencode o cualquier lib)
|
|
13
|
+
*
|
|
14
|
+
* 2. El usuario escanea el QR con Google Authenticator / Authy
|
|
15
|
+
* y envía el código de 6 dígitos
|
|
16
|
+
*
|
|
17
|
+
* 3. POST /credentials/phone/verify { sessionId, code }
|
|
18
|
+
* → si el TOTP es válido → emite PhoneVerified
|
|
19
|
+
* → retorna { credential: "PhoneVerified", did }
|
|
20
|
+
*
|
|
21
|
+
* ¿Por qué TOTP en lugar de SMS?
|
|
22
|
+
* - 100% open source (RFC 6238, sin dependencias externas)
|
|
23
|
+
* - 0 costo — no necesita Twilio, Vonage, etc.
|
|
24
|
+
* - Compatible con cualquier TOTP app (Google Auth, Authy, Aegis, etc.)
|
|
25
|
+
* - El usuario confirma que controla su dispositivo (equivalente a "phone verified")
|
|
26
|
+
* - Funciona offline en el validador
|
|
27
|
+
* - P2P friendly: cualquier nodo puede verificar sin shared state
|
|
28
|
+
*
|
|
29
|
+
* Configuración: ninguna — funciona out of the box.
|
|
30
|
+
*/
|
|
31
|
+
export declare function startPhoneVerification(did: string, phone: string): {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
totpUri: string;
|
|
34
|
+
qrData: string;
|
|
35
|
+
instructions: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function verifyPhoneTOTP(sessionId: string, code: string): {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
did?: string;
|
|
40
|
+
phone?: string;
|
|
41
|
+
reason?: string;
|
|
42
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phone TOTP Credential Validator — sin servicio externo
|
|
3
|
+
*
|
|
4
|
+
* Usa TOTP (RFC 6238) — el mismo estándar de Google Authenticator / Authy.
|
|
5
|
+
* No requiere SMS, no requiere API key, funciona completamente offline.
|
|
6
|
+
*
|
|
7
|
+
* Flujo:
|
|
8
|
+
* 1. POST /credentials/phone/start { did, phone }
|
|
9
|
+
* → genera un TOTP secret único para este DID
|
|
10
|
+
* → retorna { sessionId, totpUri, qrData }
|
|
11
|
+
* totpUri = "otpauth://totp/Soulprint:+57..."
|
|
12
|
+
* qrData = string para generar QR (usar qrencode o cualquier lib)
|
|
13
|
+
*
|
|
14
|
+
* 2. El usuario escanea el QR con Google Authenticator / Authy
|
|
15
|
+
* y envía el código de 6 dígitos
|
|
16
|
+
*
|
|
17
|
+
* 3. POST /credentials/phone/verify { sessionId, code }
|
|
18
|
+
* → si el TOTP es válido → emite PhoneVerified
|
|
19
|
+
* → retorna { credential: "PhoneVerified", did }
|
|
20
|
+
*
|
|
21
|
+
* ¿Por qué TOTP en lugar de SMS?
|
|
22
|
+
* - 100% open source (RFC 6238, sin dependencias externas)
|
|
23
|
+
* - 0 costo — no necesita Twilio, Vonage, etc.
|
|
24
|
+
* - Compatible con cualquier TOTP app (Google Auth, Authy, Aegis, etc.)
|
|
25
|
+
* - El usuario confirma que controla su dispositivo (equivalente a "phone verified")
|
|
26
|
+
* - Funciona offline en el validador
|
|
27
|
+
* - P2P friendly: cualquier nodo puede verificar sin shared state
|
|
28
|
+
*
|
|
29
|
+
* Configuración: ninguna — funciona out of the box.
|
|
30
|
+
*/
|
|
31
|
+
import { TOTP } from "otpauth";
|
|
32
|
+
import { randomBytes } from "node:crypto";
|
|
33
|
+
const SESSION_TTL_MS = 15 * 60 * 1000; // 15 minutos para completar la verificación
|
|
34
|
+
const phoneSessions = new Map();
|
|
35
|
+
setInterval(() => {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
for (const [id, s] of phoneSessions) {
|
|
38
|
+
if (s.expiresAt < now)
|
|
39
|
+
phoneSessions.delete(id);
|
|
40
|
+
}
|
|
41
|
+
}, 5 * 60_000).unref();
|
|
42
|
+
export function startPhoneVerification(did, phone) {
|
|
43
|
+
// Validar formato básico de teléfono (E.164)
|
|
44
|
+
const cleanPhone = phone.replace(/\s/g, "");
|
|
45
|
+
if (!/^\+?[1-9]\d{7,14}$/.test(cleanPhone)) {
|
|
46
|
+
throw new Error("Invalid phone number. Use E.164 format: +573001234567");
|
|
47
|
+
}
|
|
48
|
+
const sessionId = randomBytes(16).toString("hex");
|
|
49
|
+
// Generar TOTP secret único para este DID + sesión
|
|
50
|
+
const secret = randomBytes(20).toString("base64").replace(/[^A-Z2-7]/gi, "A").slice(0, 32).toUpperCase();
|
|
51
|
+
const totp = new TOTP({
|
|
52
|
+
issuer: "Soulprint",
|
|
53
|
+
label: cleanPhone,
|
|
54
|
+
algorithm: "SHA1",
|
|
55
|
+
digits: 6,
|
|
56
|
+
period: 30,
|
|
57
|
+
secret,
|
|
58
|
+
});
|
|
59
|
+
const totpUri = totp.toString();
|
|
60
|
+
phoneSessions.set(sessionId, {
|
|
61
|
+
did, phone: cleanPhone, totp, totpUri, secret,
|
|
62
|
+
expiresAt: Date.now() + SESSION_TTL_MS,
|
|
63
|
+
verified: false, attempts: 0,
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
sessionId,
|
|
67
|
+
totpUri,
|
|
68
|
+
qrData: totpUri, // el cliente puede generar el QR con cualquier lib
|
|
69
|
+
instructions: [
|
|
70
|
+
"1. Open Google Authenticator, Authy, or any TOTP app",
|
|
71
|
+
"2. Tap '+' → 'Scan QR code' and scan the QR code",
|
|
72
|
+
" Or tap 'Enter setup key' and paste: " + totpUri,
|
|
73
|
+
"3. Enter the 6-digit code that appears in the app",
|
|
74
|
+
"4. POST /credentials/phone/verify with your sessionId and code",
|
|
75
|
+
"",
|
|
76
|
+
"This proves you control a real device (equivalent to phone verification).",
|
|
77
|
+
].join("\n"),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function verifyPhoneTOTP(sessionId, code) {
|
|
81
|
+
const session = phoneSessions.get(sessionId);
|
|
82
|
+
if (!session)
|
|
83
|
+
return { ok: false, reason: "Session not found or expired" };
|
|
84
|
+
if (Date.now() > session.expiresAt) {
|
|
85
|
+
phoneSessions.delete(sessionId);
|
|
86
|
+
return { ok: false, reason: "Session expired" };
|
|
87
|
+
}
|
|
88
|
+
session.attempts++;
|
|
89
|
+
if (session.attempts > 10) {
|
|
90
|
+
phoneSessions.delete(sessionId);
|
|
91
|
+
return { ok: false, reason: "Too many attempts" };
|
|
92
|
+
}
|
|
93
|
+
// TOTP valida con ventana de ±1 período (30s) para tolerancia de clock drift
|
|
94
|
+
const isValid = session.totp.validate({ token: code.trim(), window: 1 }) !== null;
|
|
95
|
+
if (!isValid)
|
|
96
|
+
return { ok: false, reason: "Invalid or expired TOTP code. Try the current code in your authenticator app." };
|
|
97
|
+
phoneSessions.delete(sessionId);
|
|
98
|
+
return { ok: true, did: session.did, phone: session.phone };
|
|
99
|
+
}
|
package/dist/validator.js
CHANGED
|
@@ -2,8 +2,9 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { generateKeypair, keypairFromPrivateKey, decodeToken, sign, verifyAttestation, computeReputation, defaultReputation, PROTOCOL, } from "soulprint-core";
|
|
5
|
+
import { generateKeypair, keypairFromPrivateKey, decodeToken, sign, verifyAttestation, computeReputation, defaultReputation, PROTOCOL, checkFarming, recordApprovedGain, recordFarmingStrike, loadAuditStore, exportAuditStore, } from "soulprint-core";
|
|
6
6
|
import { verifyProof, deserializeProof } from "soulprint-zkp";
|
|
7
|
+
import { handleCredentialRoute } from "./credentials/index.js";
|
|
7
8
|
import { publishAttestationP2P, onAttestationReceived, getP2PStats, } from "./p2p.js";
|
|
8
9
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
9
10
|
const PORT = parseInt(process.env.SOULPRINT_PORT ?? String(PROTOCOL.DEFAULT_HTTP_PORT));
|
|
@@ -12,6 +13,7 @@ const KEYPAIR_FILE = join(NODE_DIR, "node-identity.json");
|
|
|
12
13
|
const NULLIFIER_DB = join(NODE_DIR, "nullifiers.json");
|
|
13
14
|
const REPUTE_DB = join(NODE_DIR, "reputation.json");
|
|
14
15
|
const PEERS_DB = join(NODE_DIR, "peers.json");
|
|
16
|
+
const AUDIT_DB = join(NODE_DIR, "audit.json");
|
|
15
17
|
const VERSION = "0.2.0";
|
|
16
18
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
17
19
|
// ── Protocol constants (inamovibles — no cambiar directamente aquí) ───────────
|
|
@@ -82,6 +84,15 @@ function loadReputation() {
|
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
function saveReputation() { writeFileSync(REPUTE_DB, JSON.stringify(repStore, null, 2)); }
|
|
87
|
+
// ── Audit store (anti-farming) ────────────────────────────────────────────────
|
|
88
|
+
function loadAudit() {
|
|
89
|
+
if (existsSync(AUDIT_DB))
|
|
90
|
+
try {
|
|
91
|
+
loadAuditStore(JSON.parse(readFileSync(AUDIT_DB, "utf8")));
|
|
92
|
+
}
|
|
93
|
+
catch { /* empty */ }
|
|
94
|
+
}
|
|
95
|
+
function saveAudit() { writeFileSync(AUDIT_DB, JSON.stringify(exportAuditStore(), null, 2)); }
|
|
85
96
|
/**
|
|
86
97
|
* Obtiene la reputación de un DID.
|
|
87
98
|
* Si no existe, retorna la reputación neutral (score=10).
|
|
@@ -360,16 +371,46 @@ async function handleAttest(req, res, ip) {
|
|
|
360
371
|
return json(res, 403, { error: "Invalid attestation signature" });
|
|
361
372
|
}
|
|
362
373
|
// ── Aplicar y persistir ───────────────────────────────────────────────────
|
|
363
|
-
|
|
374
|
+
// ── ANTI-FARMING CHECK (solo para attestations positivas) ─────────────────
|
|
375
|
+
// Si detectamos farming, convertimos el +1 en -1 automáticamente.
|
|
376
|
+
// Las attestations negativas no se chequean (una penalización real no hace farming).
|
|
377
|
+
let finalAtt = att;
|
|
378
|
+
if (att.value === 1 && !from_peer) {
|
|
379
|
+
const existing = repStore[att.target_did];
|
|
380
|
+
const prevAtts = existing?.attestations ?? [];
|
|
381
|
+
// Reconstruir sesión desde el contexto de la attestation
|
|
382
|
+
const session = {
|
|
383
|
+
did: att.target_did,
|
|
384
|
+
startTime: (att.timestamp - 60) * 1000, // estimar inicio de sesión 60s antes
|
|
385
|
+
events: [], // no tenemos eventos individuales aquí — se evalúa en withTracking()
|
|
386
|
+
issuerDid: att.issuer_did,
|
|
387
|
+
};
|
|
388
|
+
const farmResult = checkFarming(session, prevAtts);
|
|
389
|
+
if (farmResult.isFarming) {
|
|
390
|
+
console.warn(`[anti-farming] 🚫 Farming detectado para ${att.target_did.slice(0, 20)}...`, `\n Razón: ${farmResult.reason}`, `\n Convirtiendo +1 → -1 automáticamente`);
|
|
391
|
+
// Penalizar en lugar de recompensar
|
|
392
|
+
finalAtt = { ...att, value: -1, context: `farming-penalty:${att.context}` };
|
|
393
|
+
recordFarmingStrike(att.target_did);
|
|
394
|
+
saveAudit();
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
// Registrar ganancia aprobada para el tracking de velocidad
|
|
398
|
+
recordApprovedGain(att.target_did);
|
|
399
|
+
saveAudit();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const updatedRep = applyAttestation(finalAtt);
|
|
364
403
|
// ── Gossip a los peers (async, fire-and-forget) ───────────────────────────
|
|
365
404
|
if (!from_peer) {
|
|
366
|
-
gossipAttestation(
|
|
405
|
+
gossipAttestation(finalAtt, undefined);
|
|
367
406
|
}
|
|
368
407
|
json(res, 200, {
|
|
369
408
|
ok: true,
|
|
370
|
-
target_did:
|
|
409
|
+
target_did: finalAtt.target_did,
|
|
371
410
|
reputation: updatedRep,
|
|
372
411
|
gossiped_to: from_peer ? 0 : peers.length,
|
|
412
|
+
farming_detected: finalAtt.value !== att.value,
|
|
413
|
+
...(finalAtt.value !== att.value ? { farming_reason: finalAtt.context } : {}),
|
|
373
414
|
});
|
|
374
415
|
}
|
|
375
416
|
// ── POST /peers/register ──────────────────────────────────────────────────────
|
|
@@ -487,10 +528,21 @@ export function startValidatorNode(port = PORT) {
|
|
|
487
528
|
loadNullifiers();
|
|
488
529
|
loadReputation();
|
|
489
530
|
loadPeers();
|
|
531
|
+
loadAudit();
|
|
490
532
|
const nodeKeypair = loadOrCreateNodeKeypair();
|
|
533
|
+
// ── Credential context (para el router de credenciales) ───────────────────
|
|
534
|
+
const credentialCtx = {
|
|
535
|
+
nodeKeypair,
|
|
536
|
+
signAttestation: (att) => {
|
|
537
|
+
const sig = sign(att, nodeKeypair.privateKey);
|
|
538
|
+
return { ...att, sig };
|
|
539
|
+
},
|
|
540
|
+
gossip: (att) => gossipAttestation(att, undefined),
|
|
541
|
+
};
|
|
491
542
|
const server = createServer(async (req, res) => {
|
|
492
543
|
const ip = getIP(req);
|
|
493
|
-
const url = req.url
|
|
544
|
+
const url = req.url ?? "/";
|
|
545
|
+
const cleanUrl = url.split("?")[0];
|
|
494
546
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
495
547
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
496
548
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
@@ -499,22 +551,28 @@ export function startValidatorNode(port = PORT) {
|
|
|
499
551
|
res.end();
|
|
500
552
|
return;
|
|
501
553
|
}
|
|
502
|
-
|
|
554
|
+
// ── Credential routes (email, phone, github) ───────────────────────────
|
|
555
|
+
if (cleanUrl.startsWith("/credentials/")) {
|
|
556
|
+
const handled = await handleCredentialRoute(req, res, url, credentialCtx);
|
|
557
|
+
if (handled)
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (cleanUrl === "/info" && req.method === "GET")
|
|
503
561
|
return handleInfo(res, nodeKeypair);
|
|
504
|
-
if (
|
|
562
|
+
if (cleanUrl === "/protocol" && req.method === "GET")
|
|
505
563
|
return handleProtocol(res);
|
|
506
|
-
if (
|
|
564
|
+
if (cleanUrl === "/verify" && req.method === "POST")
|
|
507
565
|
return handleVerify(req, res, nodeKeypair, ip);
|
|
508
|
-
if (
|
|
566
|
+
if (cleanUrl === "/reputation/attest" && req.method === "POST")
|
|
509
567
|
return handleAttest(req, res, ip);
|
|
510
|
-
if (
|
|
568
|
+
if (cleanUrl === "/peers/register" && req.method === "POST")
|
|
511
569
|
return handlePeerRegister(req, res);
|
|
512
|
-
if (
|
|
570
|
+
if (cleanUrl === "/peers" && req.method === "GET")
|
|
513
571
|
return handleGetPeers(res);
|
|
514
|
-
if (
|
|
515
|
-
return handleGetReputation(res, decodeURIComponent(
|
|
516
|
-
if (
|
|
517
|
-
return handleNullifierCheck(res, decodeURIComponent(
|
|
572
|
+
if (cleanUrl.startsWith("/reputation/") && req.method === "GET")
|
|
573
|
+
return handleGetReputation(res, decodeURIComponent(cleanUrl.replace("/reputation/", "")));
|
|
574
|
+
if (cleanUrl.startsWith("/nullifier/") && req.method === "GET")
|
|
575
|
+
return handleNullifierCheck(res, decodeURIComponent(cleanUrl.replace("/nullifier/", "")));
|
|
518
576
|
json(res, 404, { error: "Not found" });
|
|
519
577
|
});
|
|
520
578
|
server.listen(port, () => {
|
|
@@ -524,14 +582,21 @@ export function startValidatorNode(port = PORT) {
|
|
|
524
582
|
console.log(` Nullifiers: ${Object.keys(nullifiers).length}`);
|
|
525
583
|
console.log(` Reputations: ${Object.keys(repStore).length}`);
|
|
526
584
|
console.log(` Known peers: ${peers.length}`);
|
|
527
|
-
console.log(`\n
|
|
585
|
+
console.log(`\n Core endpoints:`);
|
|
586
|
+
console.log(` POST /verify verify ZK proof + co-sign`);
|
|
528
587
|
console.log(` GET /info node info`);
|
|
588
|
+
console.log(` GET /protocol protocol constants (immutable)`);
|
|
529
589
|
console.log(` GET /nullifier/:n anti-sybil check`);
|
|
530
|
-
console.log(` POST /reputation/attest issue
|
|
590
|
+
console.log(` POST /reputation/attest issue attestation (anti-farming ON)`);
|
|
531
591
|
console.log(` GET /reputation/:did get bot reputation`);
|
|
532
|
-
console.log(
|
|
533
|
-
console.log(`
|
|
534
|
-
console.log(
|
|
592
|
+
console.log(`\n Credential validators (open source, no API keys needed):`);
|
|
593
|
+
console.log(` POST /credentials/email/start → email OTP (nodemailer)`);
|
|
594
|
+
console.log(` POST /credentials/email/verify`);
|
|
595
|
+
console.log(` POST /credentials/phone/start → TOTP device proof (otpauth)`);
|
|
596
|
+
console.log(` POST /credentials/phone/verify`);
|
|
597
|
+
console.log(` GET /credentials/github/start → GitHub OAuth (native fetch)`);
|
|
598
|
+
console.log(` GET /credentials/github/callback`);
|
|
599
|
+
console.log(`\n Anti-farming: ON — max +1/day, pattern detection, cooldowns\n`);
|
|
535
600
|
});
|
|
536
601
|
return server;
|
|
537
602
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soulprint-network",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Soulprint validator node — HTTP server that verifies ZK proofs, co-signs SPTs, anti-Sybil registry",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -40,12 +40,16 @@
|
|
|
40
40
|
"@libp2p/ping": "2.0.37",
|
|
41
41
|
"@libp2p/tcp": "10.1.19",
|
|
42
42
|
"libp2p": "2.10.0",
|
|
43
|
+
"nodemailer": "^8.0.1",
|
|
44
|
+
"otpauth": "^9.5.0",
|
|
45
|
+
"otplib": "^13.3.0",
|
|
43
46
|
"uint8arrays": "5.1.0",
|
|
44
|
-
"soulprint-core": "0.1.
|
|
47
|
+
"soulprint-core": "0.1.5",
|
|
45
48
|
"soulprint-zkp": "0.1.3"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@types/node": "^20.0.0",
|
|
52
|
+
"@types/nodemailer": "^7.0.11",
|
|
49
53
|
"typescript": "^5.4.0"
|
|
50
54
|
},
|
|
51
55
|
"engines": {
|