soulprint-core 0.1.3 → 0.1.5
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/anti-farming.d.ts +77 -0
- package/dist/anti-farming.js +192 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +17 -0
- package/dist/protocol-constants.d.ts +95 -0
- package/dist/protocol-constants.js +139 -0
- package/package.json +1 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soulprint Anti-Farming Module
|
|
3
|
+
*
|
|
4
|
+
* Detecta y penaliza intentos de farmeo de puntos de reputación.
|
|
5
|
+
*
|
|
6
|
+
* REGLAS INAMOVIBLES (definidas en PROTOCOL):
|
|
7
|
+
* - Un DID solo puede ganar MAX +1 punto por día en total (todos los validadores)
|
|
8
|
+
* - Velocidad máxima: +2 puntos en 7 días vía attestations positivas
|
|
9
|
+
* - Sesiones deben durar al menos MIN_SESSION_SECONDS para ser elegibles a reward
|
|
10
|
+
* - Patrones regulares (robot-like) → detectados como farming → -1
|
|
11
|
+
* - Nuevos DIDs (< PROBATION_DAYS) ganan 0 hasta acumular 2 attestations válidas
|
|
12
|
+
*/
|
|
13
|
+
import { BotAttestation } from "./index.js";
|
|
14
|
+
export declare const FARMING_RULES: Readonly<{
|
|
15
|
+
/** Max puntos que un DID puede ganar en 24h via attestations positivas */
|
|
16
|
+
MAX_GAIN_PER_DAY: 1;
|
|
17
|
+
/** Max puntos acumulados en 7 días */
|
|
18
|
+
MAX_GAIN_PER_WEEK: 2;
|
|
19
|
+
/** Segundos mínimos que debe durar una sesión para ser elegible a reward */
|
|
20
|
+
MIN_SESSION_SECONDS: 30;
|
|
21
|
+
/** Días de probación para DIDs nuevos (cuentan 0 hasta 2 attestations) */
|
|
22
|
+
PROBATION_DAYS: 7;
|
|
23
|
+
/** Max attestations positivas de un MISMO servicio a un DID en 24h */
|
|
24
|
+
MAX_SAME_ISSUER_PER_DAY: 1;
|
|
25
|
+
/** Entropía mínima del patrón de tools para no ser detectado como farming
|
|
26
|
+
* (número mínimo de tools distintos reales para reward) */
|
|
27
|
+
MIN_TOOL_ENTROPY: 4;
|
|
28
|
+
/** Tiempo mínimo entre dos calls a la misma tool (ms) para que no sea robot */
|
|
29
|
+
MIN_TOOL_INTERVAL_MS: 2000;
|
|
30
|
+
/** Si se detecta farming → emitir -1 automático en lugar de +1 */
|
|
31
|
+
FARMING_PENALTY: -1;
|
|
32
|
+
}>;
|
|
33
|
+
export interface SessionEvent {
|
|
34
|
+
tool: string;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
}
|
|
37
|
+
export interface SessionContext {
|
|
38
|
+
did: string;
|
|
39
|
+
startTime: number;
|
|
40
|
+
events: SessionEvent[];
|
|
41
|
+
issuerDid: string;
|
|
42
|
+
}
|
|
43
|
+
export interface FarmingCheckResult {
|
|
44
|
+
isFarming: boolean;
|
|
45
|
+
reason?: string;
|
|
46
|
+
penalty: -1 | 0;
|
|
47
|
+
details?: Record<string, any>;
|
|
48
|
+
}
|
|
49
|
+
export interface DIDAuditEntry {
|
|
50
|
+
dailyGain: number;
|
|
51
|
+
weeklyGain: number;
|
|
52
|
+
dayStart: number;
|
|
53
|
+
weekStart: number;
|
|
54
|
+
firstSeen: number;
|
|
55
|
+
attestCount: number;
|
|
56
|
+
farmingStrikes: number;
|
|
57
|
+
}
|
|
58
|
+
export declare function getOrCreateAudit(did: string): DIDAuditEntry;
|
|
59
|
+
export declare function loadAuditStore(data: Record<string, DIDAuditEntry>): void;
|
|
60
|
+
export declare function exportAuditStore(): Record<string, DIDAuditEntry>;
|
|
61
|
+
/**
|
|
62
|
+
* Analiza una sesión ANTES de emitir la attestation.
|
|
63
|
+
* Si detecta farming, retorna isFarming=true con razón específica.
|
|
64
|
+
*
|
|
65
|
+
* El validator debe llamar esta función ANTES de applyAttestation().
|
|
66
|
+
* Si isFarming=true, emitir -1 en lugar de +1.
|
|
67
|
+
*/
|
|
68
|
+
export declare function checkFarming(session: SessionContext, existingAtts: BotAttestation[]): FarmingCheckResult;
|
|
69
|
+
/**
|
|
70
|
+
* Registra una attestation positiva aprobada en el audit store.
|
|
71
|
+
* Llamar DESPUÉS de confirmar que no es farming y aplicar el +1.
|
|
72
|
+
*/
|
|
73
|
+
export declare function recordApprovedGain(did: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Registra un strike de farming en el audit store.
|
|
76
|
+
*/
|
|
77
|
+
export declare function recordFarmingStrike(did: string): void;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Soulprint Anti-Farming Module
|
|
4
|
+
*
|
|
5
|
+
* Detecta y penaliza intentos de farmeo de puntos de reputación.
|
|
6
|
+
*
|
|
7
|
+
* REGLAS INAMOVIBLES (definidas en PROTOCOL):
|
|
8
|
+
* - Un DID solo puede ganar MAX +1 punto por día en total (todos los validadores)
|
|
9
|
+
* - Velocidad máxima: +2 puntos en 7 días vía attestations positivas
|
|
10
|
+
* - Sesiones deben durar al menos MIN_SESSION_SECONDS para ser elegibles a reward
|
|
11
|
+
* - Patrones regulares (robot-like) → detectados como farming → -1
|
|
12
|
+
* - Nuevos DIDs (< PROBATION_DAYS) ganan 0 hasta acumular 2 attestations válidas
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FARMING_RULES = void 0;
|
|
16
|
+
exports.getOrCreateAudit = getOrCreateAudit;
|
|
17
|
+
exports.loadAuditStore = loadAuditStore;
|
|
18
|
+
exports.exportAuditStore = exportAuditStore;
|
|
19
|
+
exports.checkFarming = checkFarming;
|
|
20
|
+
exports.recordApprovedGain = recordApprovedGain;
|
|
21
|
+
exports.recordFarmingStrike = recordFarmingStrike;
|
|
22
|
+
// ── Anti-farming constants (parte del PROTOCOL) ──────────────────────────────
|
|
23
|
+
exports.FARMING_RULES = Object.freeze({
|
|
24
|
+
/** Max puntos que un DID puede ganar en 24h via attestations positivas */
|
|
25
|
+
MAX_GAIN_PER_DAY: 1,
|
|
26
|
+
/** Max puntos acumulados en 7 días */
|
|
27
|
+
MAX_GAIN_PER_WEEK: 2,
|
|
28
|
+
/** Segundos mínimos que debe durar una sesión para ser elegible a reward */
|
|
29
|
+
MIN_SESSION_SECONDS: 30,
|
|
30
|
+
/** Días de probación para DIDs nuevos (cuentan 0 hasta 2 attestations) */
|
|
31
|
+
PROBATION_DAYS: 7,
|
|
32
|
+
/** Max attestations positivas de un MISMO servicio a un DID en 24h */
|
|
33
|
+
MAX_SAME_ISSUER_PER_DAY: 1,
|
|
34
|
+
/** Entropía mínima del patrón de tools para no ser detectado como farming
|
|
35
|
+
* (número mínimo de tools distintos reales para reward) */
|
|
36
|
+
MIN_TOOL_ENTROPY: 4,
|
|
37
|
+
/** Tiempo mínimo entre dos calls a la misma tool (ms) para que no sea robot */
|
|
38
|
+
MIN_TOOL_INTERVAL_MS: 2000,
|
|
39
|
+
/** Si se detecta farming → emitir -1 automático en lugar de +1 */
|
|
40
|
+
FARMING_PENALTY: -1,
|
|
41
|
+
});
|
|
42
|
+
// ── In-memory audit store (el validator lo persiste en disco) ────────────────
|
|
43
|
+
const auditStore = new Map();
|
|
44
|
+
function getOrCreateAudit(did) {
|
|
45
|
+
if (!auditStore.has(did)) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
auditStore.set(did, {
|
|
48
|
+
dailyGain: 0,
|
|
49
|
+
weeklyGain: 0,
|
|
50
|
+
dayStart: startOfDay(now),
|
|
51
|
+
weekStart: startOfWeek(now),
|
|
52
|
+
firstSeen: now,
|
|
53
|
+
attestCount: 0,
|
|
54
|
+
farmingStrikes: 0,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return auditStore.get(did);
|
|
58
|
+
}
|
|
59
|
+
function loadAuditStore(data) {
|
|
60
|
+
for (const [did, entry] of Object.entries(data)) {
|
|
61
|
+
auditStore.set(did, entry);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function exportAuditStore() {
|
|
65
|
+
return Object.fromEntries(auditStore.entries());
|
|
66
|
+
}
|
|
67
|
+
// ── Core farming detection ────────────────────────────────────────────────────
|
|
68
|
+
/**
|
|
69
|
+
* Analiza una sesión ANTES de emitir la attestation.
|
|
70
|
+
* Si detecta farming, retorna isFarming=true con razón específica.
|
|
71
|
+
*
|
|
72
|
+
* El validator debe llamar esta función ANTES de applyAttestation().
|
|
73
|
+
* Si isFarming=true, emitir -1 en lugar de +1.
|
|
74
|
+
*/
|
|
75
|
+
function checkFarming(session, existingAtts) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const audit = getOrCreateAudit(session.did);
|
|
78
|
+
resetDailyIfNeeded(audit, now);
|
|
79
|
+
// ── Regla 1: Sesión demasiado corta ──────────────────────────────────────
|
|
80
|
+
const sessionDuration = (now - session.startTime) / 1000;
|
|
81
|
+
if (sessionDuration < exports.FARMING_RULES.MIN_SESSION_SECONDS) {
|
|
82
|
+
return farming(`Session too short: ${sessionDuration.toFixed(1)}s < ${exports.FARMING_RULES.MIN_SESSION_SECONDS}s`, audit);
|
|
83
|
+
}
|
|
84
|
+
// ── Regla 2: Velocidad diaria excedida ───────────────────────────────────
|
|
85
|
+
if (audit.dailyGain >= exports.FARMING_RULES.MAX_GAIN_PER_DAY) {
|
|
86
|
+
return farming(`Daily gain cap reached: +${audit.dailyGain} today (max ${exports.FARMING_RULES.MAX_GAIN_PER_DAY})`, audit);
|
|
87
|
+
}
|
|
88
|
+
// ── Regla 3: Velocidad semanal excedida ──────────────────────────────────
|
|
89
|
+
if (audit.weeklyGain >= exports.FARMING_RULES.MAX_GAIN_PER_WEEK) {
|
|
90
|
+
return farming(`Weekly gain cap reached: +${audit.weeklyGain} this week (max ${exports.FARMING_RULES.MAX_GAIN_PER_WEEK})`, audit);
|
|
91
|
+
}
|
|
92
|
+
// ── Regla 4: Mismo emisor ya attestó hoy ─────────────────────────────────
|
|
93
|
+
const todayStart = startOfDay(now);
|
|
94
|
+
const sameIssuerToday = existingAtts.filter(a => a.issuer_did === session.issuerDid &&
|
|
95
|
+
a.value === 1 &&
|
|
96
|
+
a.timestamp * 1000 >= todayStart).length;
|
|
97
|
+
if (sameIssuerToday >= exports.FARMING_RULES.MAX_SAME_ISSUER_PER_DAY) {
|
|
98
|
+
return farming(`Same issuer already attested today: ${session.issuerDid.slice(0, 20)}... (max ${exports.FARMING_RULES.MAX_SAME_ISSUER_PER_DAY}/day)`, audit);
|
|
99
|
+
}
|
|
100
|
+
// ── Regla 5: Entropía de herramientas insuficiente ───────────────────────
|
|
101
|
+
const uniqueTools = new Set(session.events.map(e => e.tool));
|
|
102
|
+
if (uniqueTools.size < exports.FARMING_RULES.MIN_TOOL_ENTROPY) {
|
|
103
|
+
return farming(`Tool entropy too low: ${uniqueTools.size} distinct tools (min ${exports.FARMING_RULES.MIN_TOOL_ENTROPY})`, audit);
|
|
104
|
+
}
|
|
105
|
+
// ── Regla 6: Patrón robótico (intervalos demasiado regulares) ────────────
|
|
106
|
+
if (isRoboticPattern(session.events)) {
|
|
107
|
+
return farming("Robotic call pattern detected (regular intervals suggest automation)", audit);
|
|
108
|
+
}
|
|
109
|
+
// ── Regla 7: DID en probación con menos de 2 attestations ───────────────
|
|
110
|
+
const daysOld = (now - audit.firstSeen) / (1000 * 60 * 60 * 24);
|
|
111
|
+
if (daysOld < exports.FARMING_RULES.PROBATION_DAYS && audit.attestCount < 2) {
|
|
112
|
+
return farming(`DID in probation: ${daysOld.toFixed(1)} days old, ${audit.attestCount} attestations (need 2+ to earn points in first 7 days)`, audit);
|
|
113
|
+
}
|
|
114
|
+
// ── Sin farming detectado → OK ────────────────────────────────────────────
|
|
115
|
+
return { isFarming: false, penalty: 0 };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Registra una attestation positiva aprobada en el audit store.
|
|
119
|
+
* Llamar DESPUÉS de confirmar que no es farming y aplicar el +1.
|
|
120
|
+
*/
|
|
121
|
+
function recordApprovedGain(did) {
|
|
122
|
+
const audit = getOrCreateAudit(did);
|
|
123
|
+
resetDailyIfNeeded(audit, Date.now());
|
|
124
|
+
audit.dailyGain++;
|
|
125
|
+
audit.weeklyGain++;
|
|
126
|
+
audit.attestCount++;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Registra un strike de farming en el audit store.
|
|
130
|
+
*/
|
|
131
|
+
function recordFarmingStrike(did) {
|
|
132
|
+
const audit = getOrCreateAudit(did);
|
|
133
|
+
audit.farmingStrikes++;
|
|
134
|
+
}
|
|
135
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
136
|
+
function farming(reason, audit) {
|
|
137
|
+
audit.farmingStrikes++;
|
|
138
|
+
return {
|
|
139
|
+
isFarming: true,
|
|
140
|
+
reason,
|
|
141
|
+
penalty: exports.FARMING_RULES.FARMING_PENALTY,
|
|
142
|
+
details: {
|
|
143
|
+
dailyGain: audit.dailyGain,
|
|
144
|
+
weeklyGain: audit.weeklyGain,
|
|
145
|
+
farmingStrikes: audit.farmingStrikes,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Detecta patrones robóticos: intervalos entre calls demasiado regulares.
|
|
151
|
+
* Un humano real varía sus tiempos; un bot suele tener intervals constantes.
|
|
152
|
+
*/
|
|
153
|
+
function isRoboticPattern(events) {
|
|
154
|
+
if (events.length < 4)
|
|
155
|
+
return false;
|
|
156
|
+
const intervals = [];
|
|
157
|
+
for (let i = 1; i < events.length; i++) {
|
|
158
|
+
intervals.push(events[i].timestamp - events[i - 1].timestamp);
|
|
159
|
+
}
|
|
160
|
+
// Si todos los intervalos son casi iguales (stddev < 10% de la media) → robótico
|
|
161
|
+
const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
162
|
+
const stddev = Math.sqrt(intervals.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / intervals.length);
|
|
163
|
+
// Adicionalmente: intervalos muy cortos (< MIN_TOOL_INTERVAL_MS) → farming
|
|
164
|
+
const tooFast = intervals.filter(i => i < exports.FARMING_RULES.MIN_TOOL_INTERVAL_MS).length;
|
|
165
|
+
if (tooFast > intervals.length * 0.5)
|
|
166
|
+
return true; // >50% calls muy rápidos
|
|
167
|
+
return mean > 0 && (stddev / mean) < 0.10; // coeficiente de variación < 10%
|
|
168
|
+
}
|
|
169
|
+
function resetDailyIfNeeded(audit, now) {
|
|
170
|
+
const todayStart = startOfDay(now);
|
|
171
|
+
if (audit.dayStart < todayStart) {
|
|
172
|
+
audit.dailyGain = 0;
|
|
173
|
+
audit.dayStart = todayStart;
|
|
174
|
+
}
|
|
175
|
+
const weekStart = startOfWeek(now);
|
|
176
|
+
if (audit.weekStart < weekStart) {
|
|
177
|
+
audit.weeklyGain = 0;
|
|
178
|
+
audit.weekStart = weekStart;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function startOfDay(ms) {
|
|
182
|
+
const d = new Date(ms);
|
|
183
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
184
|
+
return d.getTime();
|
|
185
|
+
}
|
|
186
|
+
function startOfWeek(ms) {
|
|
187
|
+
const d = new Date(ms);
|
|
188
|
+
const day = d.getUTCDay();
|
|
189
|
+
d.setUTCDate(d.getUTCDate() - day);
|
|
190
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
191
|
+
return d.getTime();
|
|
192
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -100,3 +100,6 @@ export declare function calculateScore(credentials: CredentialType[]): number;
|
|
|
100
100
|
*/
|
|
101
101
|
export declare function calculateTotalScore(credentials: CredentialType[], botRep?: BotReputation): number;
|
|
102
102
|
export declare function calculateLevel(credentials: CredentialType[]): TrustLevel;
|
|
103
|
+
export { PROTOCOL, isProtocolCompatible, clampMinScore, computeTotalScoreWithFloor, withRetry, } from "./protocol-constants.js";
|
|
104
|
+
export { FARMING_RULES, checkFarming, recordApprovedGain, recordFarmingStrike, getOrCreateAudit, loadAuditStore, exportAuditStore, } from "./anti-farming.js";
|
|
105
|
+
export type { SessionContext, FarmingCheckResult, DIDAuditEntry } from "./anti-farming.js";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.exportAuditStore = exports.loadAuditStore = exports.getOrCreateAudit = exports.recordFarmingStrike = exports.recordApprovedGain = exports.checkFarming = exports.FARMING_RULES = exports.withRetry = exports.computeTotalScoreWithFloor = exports.clampMinScore = exports.isProtocolCompatible = exports.PROTOCOL = void 0;
|
|
6
7
|
exports.generateKeypair = generateKeypair;
|
|
7
8
|
exports.keypairFromPrivateKey = keypairFromPrivateKey;
|
|
8
9
|
exports.deriveNullifier = deriveNullifier;
|
|
@@ -203,3 +204,19 @@ function calculateLevel(credentials) {
|
|
|
203
204
|
return "EmailVerified";
|
|
204
205
|
return "Unverified";
|
|
205
206
|
}
|
|
207
|
+
// ── Protocol Constants (re-export) ────────────────────────────────────────────
|
|
208
|
+
var protocol_constants_js_1 = require("./protocol-constants.js");
|
|
209
|
+
Object.defineProperty(exports, "PROTOCOL", { enumerable: true, get: function () { return protocol_constants_js_1.PROTOCOL; } });
|
|
210
|
+
Object.defineProperty(exports, "isProtocolCompatible", { enumerable: true, get: function () { return protocol_constants_js_1.isProtocolCompatible; } });
|
|
211
|
+
Object.defineProperty(exports, "clampMinScore", { enumerable: true, get: function () { return protocol_constants_js_1.clampMinScore; } });
|
|
212
|
+
Object.defineProperty(exports, "computeTotalScoreWithFloor", { enumerable: true, get: function () { return protocol_constants_js_1.computeTotalScoreWithFloor; } });
|
|
213
|
+
Object.defineProperty(exports, "withRetry", { enumerable: true, get: function () { return protocol_constants_js_1.withRetry; } });
|
|
214
|
+
// ── Anti-Farming (re-export) ──────────────────────────────────────────────────
|
|
215
|
+
var anti_farming_js_1 = require("./anti-farming.js");
|
|
216
|
+
Object.defineProperty(exports, "FARMING_RULES", { enumerable: true, get: function () { return anti_farming_js_1.FARMING_RULES; } });
|
|
217
|
+
Object.defineProperty(exports, "checkFarming", { enumerable: true, get: function () { return anti_farming_js_1.checkFarming; } });
|
|
218
|
+
Object.defineProperty(exports, "recordApprovedGain", { enumerable: true, get: function () { return anti_farming_js_1.recordApprovedGain; } });
|
|
219
|
+
Object.defineProperty(exports, "recordFarmingStrike", { enumerable: true, get: function () { return anti_farming_js_1.recordFarmingStrike; } });
|
|
220
|
+
Object.defineProperty(exports, "getOrCreateAudit", { enumerable: true, get: function () { return anti_farming_js_1.getOrCreateAudit; } });
|
|
221
|
+
Object.defineProperty(exports, "loadAuditStore", { enumerable: true, get: function () { return anti_farming_js_1.loadAuditStore; } });
|
|
222
|
+
Object.defineProperty(exports, "exportAuditStore", { enumerable: true, get: function () { return anti_farming_js_1.exportAuditStore; } });
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soulprint Protocol Constants — SIP v0.1
|
|
3
|
+
*
|
|
4
|
+
* Estas constantes son INMUTABLES a nivel de protocolo.
|
|
5
|
+
* Todos los nodos de la red deben operar con los mismos valores.
|
|
6
|
+
* Cambiarlas requiere un nuevo SIP (Soulprint Improvement Proposal)
|
|
7
|
+
* y una actualización de versión de protocolo.
|
|
8
|
+
*
|
|
9
|
+
* Object.freeze() garantiza que no se modifiquen en runtime.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PROTOCOL: Readonly<{
|
|
12
|
+
/** Versión del protocolo SIP. Nodos con versiones distintas se rechazan. */
|
|
13
|
+
readonly VERSION: "sip/0.1";
|
|
14
|
+
/** Score máximo posible (identidad + reputación). */
|
|
15
|
+
readonly MAX_SCORE: 100;
|
|
16
|
+
/** Máximo de la sub-puntuación de identidad humana. */
|
|
17
|
+
readonly IDENTITY_MAX: 80;
|
|
18
|
+
/** Máximo de la sub-puntuación de reputación de bot. */
|
|
19
|
+
readonly REPUTATION_MAX: 20;
|
|
20
|
+
/** Reputación neutral inicial para todo bot nuevo. */
|
|
21
|
+
readonly DEFAULT_REPUTATION: 10;
|
|
22
|
+
/**
|
|
23
|
+
* PISO MÍNIMO DE SCORE — ningún threshold de servicio puede estar por debajo.
|
|
24
|
+
* Si un servicio configura minScore < SCORE_FLOOR, se clampea a este valor.
|
|
25
|
+
* Garantiza que endpoints protegidos siempre exigen identidad mínima.
|
|
26
|
+
*/
|
|
27
|
+
readonly SCORE_FLOOR: 65;
|
|
28
|
+
/**
|
|
29
|
+
* Piso de score para identidades con DocumentVerified.
|
|
30
|
+
* Una persona con documento verificado nunca puede quedar por debajo de este
|
|
31
|
+
* valor total, sin importar cuántas attestaciones negativas reciba.
|
|
32
|
+
*/
|
|
33
|
+
readonly VERIFIED_SCORE_FLOOR: 52;
|
|
34
|
+
/**
|
|
35
|
+
* Score mínimo de un servicio para poder emitir attestations.
|
|
36
|
+
* Servicio sin este score no puede calificar el comportamiento de bots.
|
|
37
|
+
*/
|
|
38
|
+
readonly MIN_ATTESTER_SCORE: 65;
|
|
39
|
+
/** Número máximo de reintentos al verificar un token con un nodo validador. */
|
|
40
|
+
readonly VERIFY_RETRY_MAX: 3;
|
|
41
|
+
/** Delay base en ms para el backoff exponencial de reintentos. */
|
|
42
|
+
readonly VERIFY_RETRY_BASE_MS: 500;
|
|
43
|
+
/** Delay máximo entre reintentos (cap del backoff). */
|
|
44
|
+
readonly VERIFY_RETRY_MAX_MS: 8000;
|
|
45
|
+
/** Jitter máximo en ms para evitar thundering herd entre clientes. */
|
|
46
|
+
readonly VERIFY_RETRY_JITTER_MS: 200;
|
|
47
|
+
/** Tiempo máximo que puede tener una attestation para ser aceptada (segundos). */
|
|
48
|
+
readonly ATT_MAX_AGE_SECONDS: 3600;
|
|
49
|
+
/** Clock skew máximo permitido entre cliente y nodo validador (segundos). */
|
|
50
|
+
readonly CLOCK_SKEW_MAX_SECONDS: 300;
|
|
51
|
+
/** Tiempo de vida por defecto de un SPT token (6 meses en segundos). */
|
|
52
|
+
readonly TOKEN_DEFAULT_LIFETIME_SECONDS: number;
|
|
53
|
+
/** Puerto HTTP por defecto del nodo validador. */
|
|
54
|
+
readonly DEFAULT_HTTP_PORT: 4888;
|
|
55
|
+
/** Puerto P2P por defecto del nodo validador (HTTP + 2000). */
|
|
56
|
+
readonly DEFAULT_P2P_PORT: 6888;
|
|
57
|
+
/** Timeout en ms para el gossip HTTP entre nodos. */
|
|
58
|
+
readonly GOSSIP_TIMEOUT_MS: 3000;
|
|
59
|
+
/** Requests máximos por minuto por IP en el nodo validador. */
|
|
60
|
+
readonly RATE_LIMIT_MAX: 100;
|
|
61
|
+
/** Ventana de tiempo del rate limiter (ms). */
|
|
62
|
+
readonly RATE_LIMIT_WINDOW_MS: 60000;
|
|
63
|
+
}>;
|
|
64
|
+
export type ProtocolConstants = typeof PROTOCOL;
|
|
65
|
+
/**
|
|
66
|
+
* Verifica que este nodo es compatible con la versión de protocolo recibida.
|
|
67
|
+
* Retorna true si son compatibles, false si hay que rechazar la conexión.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isProtocolCompatible(remoteVersion: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Clampea un minScore al floor del protocolo.
|
|
72
|
+
* Ningún servicio puede exigir menos del SCORE_FLOOR.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* clampMinScore(40) // → 65 (clamped al floor)
|
|
76
|
+
* clampMinScore(80) // → 80 (ya está por encima del floor)
|
|
77
|
+
* clampMinScore(0) // → 65 (clamped al floor)
|
|
78
|
+
*/
|
|
79
|
+
export declare function clampMinScore(requested: number): number;
|
|
80
|
+
/**
|
|
81
|
+
* Calcula el score total respetando el floor de identidad verificada.
|
|
82
|
+
* Si el bot tiene DocumentVerified, su score total nunca puede bajar
|
|
83
|
+
* de VERIFIED_SCORE_FLOOR sin importar las attestaciones negativas.
|
|
84
|
+
*/
|
|
85
|
+
export declare function computeTotalScoreWithFloor(identityScore: number, reputationScore: number, hasDocumentVerified: boolean): number;
|
|
86
|
+
/**
|
|
87
|
+
* Retry con backoff exponencial + jitter para llamadas a nodos validadores.
|
|
88
|
+
* Respeta VERIFY_RETRY_MAX, VERIFY_RETRY_BASE_MS y VERIFY_RETRY_MAX_MS.
|
|
89
|
+
*
|
|
90
|
+
* @param fn - Función async que puede fallar
|
|
91
|
+
* @param label - Label para logs
|
|
92
|
+
* @returns - Resultado de fn al tener éxito
|
|
93
|
+
* @throws - Error del último intento si todos fallan
|
|
94
|
+
*/
|
|
95
|
+
export declare function withRetry<T>(fn: () => Promise<T>, label?: string): Promise<T>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Soulprint Protocol Constants — SIP v0.1
|
|
4
|
+
*
|
|
5
|
+
* Estas constantes son INMUTABLES a nivel de protocolo.
|
|
6
|
+
* Todos los nodos de la red deben operar con los mismos valores.
|
|
7
|
+
* Cambiarlas requiere un nuevo SIP (Soulprint Improvement Proposal)
|
|
8
|
+
* y una actualización de versión de protocolo.
|
|
9
|
+
*
|
|
10
|
+
* Object.freeze() garantiza que no se modifiquen en runtime.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.PROTOCOL = void 0;
|
|
14
|
+
exports.isProtocolCompatible = isProtocolCompatible;
|
|
15
|
+
exports.clampMinScore = clampMinScore;
|
|
16
|
+
exports.computeTotalScoreWithFloor = computeTotalScoreWithFloor;
|
|
17
|
+
exports.withRetry = withRetry;
|
|
18
|
+
exports.PROTOCOL = Object.freeze({
|
|
19
|
+
// ── Versión del protocolo ──────────────────────────────────────────────────
|
|
20
|
+
/** Versión del protocolo SIP. Nodos con versiones distintas se rechazan. */
|
|
21
|
+
VERSION: "sip/0.1",
|
|
22
|
+
// ── Score limits ───────────────────────────────────────────────────────────
|
|
23
|
+
/** Score máximo posible (identidad + reputación). */
|
|
24
|
+
MAX_SCORE: 100,
|
|
25
|
+
/** Máximo de la sub-puntuación de identidad humana. */
|
|
26
|
+
IDENTITY_MAX: 80,
|
|
27
|
+
/** Máximo de la sub-puntuación de reputación de bot. */
|
|
28
|
+
REPUTATION_MAX: 20,
|
|
29
|
+
/** Reputación neutral inicial para todo bot nuevo. */
|
|
30
|
+
DEFAULT_REPUTATION: 10,
|
|
31
|
+
// ── Score floors (INAMOVIBLES) ─────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* PISO MÍNIMO DE SCORE — ningún threshold de servicio puede estar por debajo.
|
|
34
|
+
* Si un servicio configura minScore < SCORE_FLOOR, se clampea a este valor.
|
|
35
|
+
* Garantiza que endpoints protegidos siempre exigen identidad mínima.
|
|
36
|
+
*/
|
|
37
|
+
SCORE_FLOOR: 65,
|
|
38
|
+
/**
|
|
39
|
+
* Piso de score para identidades con DocumentVerified.
|
|
40
|
+
* Una persona con documento verificado nunca puede quedar por debajo de este
|
|
41
|
+
* valor total, sin importar cuántas attestaciones negativas reciba.
|
|
42
|
+
*/
|
|
43
|
+
VERIFIED_SCORE_FLOOR: 52,
|
|
44
|
+
/**
|
|
45
|
+
* Score mínimo de un servicio para poder emitir attestations.
|
|
46
|
+
* Servicio sin este score no puede calificar el comportamiento de bots.
|
|
47
|
+
*/
|
|
48
|
+
MIN_ATTESTER_SCORE: 65,
|
|
49
|
+
// ── Retry logic ────────────────────────────────────────────────────────────
|
|
50
|
+
/** Número máximo de reintentos al verificar un token con un nodo validador. */
|
|
51
|
+
VERIFY_RETRY_MAX: 3,
|
|
52
|
+
/** Delay base en ms para el backoff exponencial de reintentos. */
|
|
53
|
+
VERIFY_RETRY_BASE_MS: 500,
|
|
54
|
+
/** Delay máximo entre reintentos (cap del backoff). */
|
|
55
|
+
VERIFY_RETRY_MAX_MS: 8000,
|
|
56
|
+
/** Jitter máximo en ms para evitar thundering herd entre clientes. */
|
|
57
|
+
VERIFY_RETRY_JITTER_MS: 200,
|
|
58
|
+
// ── Attestation rules ──────────────────────────────────────────────────────
|
|
59
|
+
/** Tiempo máximo que puede tener una attestation para ser aceptada (segundos). */
|
|
60
|
+
ATT_MAX_AGE_SECONDS: 3600,
|
|
61
|
+
/** Clock skew máximo permitido entre cliente y nodo validador (segundos). */
|
|
62
|
+
CLOCK_SKEW_MAX_SECONDS: 300,
|
|
63
|
+
// ── Token lifetime ─────────────────────────────────────────────────────────
|
|
64
|
+
/** Tiempo de vida por defecto de un SPT token (6 meses en segundos). */
|
|
65
|
+
TOKEN_DEFAULT_LIFETIME_SECONDS: 60 * 60 * 24 * 180,
|
|
66
|
+
// ── Network ────────────────────────────────────────────────────────────────
|
|
67
|
+
/** Puerto HTTP por defecto del nodo validador. */
|
|
68
|
+
DEFAULT_HTTP_PORT: 4888,
|
|
69
|
+
/** Puerto P2P por defecto del nodo validador (HTTP + 2000). */
|
|
70
|
+
DEFAULT_P2P_PORT: 6888,
|
|
71
|
+
/** Timeout en ms para el gossip HTTP entre nodos. */
|
|
72
|
+
GOSSIP_TIMEOUT_MS: 3000,
|
|
73
|
+
/** Requests máximos por minuto por IP en el nodo validador. */
|
|
74
|
+
RATE_LIMIT_MAX: 100,
|
|
75
|
+
/** Ventana de tiempo del rate limiter (ms). */
|
|
76
|
+
RATE_LIMIT_WINDOW_MS: 60_000,
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Verifica que este nodo es compatible con la versión de protocolo recibida.
|
|
80
|
+
* Retorna true si son compatibles, false si hay que rechazar la conexión.
|
|
81
|
+
*/
|
|
82
|
+
function isProtocolCompatible(remoteVersion) {
|
|
83
|
+
// Por ahora: solo aceptamos la misma versión exacta
|
|
84
|
+
// En el futuro: parsear semver y aceptar minor patches compatibles
|
|
85
|
+
return remoteVersion === exports.PROTOCOL.VERSION;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clampea un minScore al floor del protocolo.
|
|
89
|
+
* Ningún servicio puede exigir menos del SCORE_FLOOR.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* clampMinScore(40) // → 65 (clamped al floor)
|
|
93
|
+
* clampMinScore(80) // → 80 (ya está por encima del floor)
|
|
94
|
+
* clampMinScore(0) // → 65 (clamped al floor)
|
|
95
|
+
*/
|
|
96
|
+
function clampMinScore(requested) {
|
|
97
|
+
return Math.max(exports.PROTOCOL.SCORE_FLOOR, Math.min(exports.PROTOCOL.MAX_SCORE, requested));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Calcula el score total respetando el floor de identidad verificada.
|
|
101
|
+
* Si el bot tiene DocumentVerified, su score total nunca puede bajar
|
|
102
|
+
* de VERIFIED_SCORE_FLOOR sin importar las attestaciones negativas.
|
|
103
|
+
*/
|
|
104
|
+
function computeTotalScoreWithFloor(identityScore, reputationScore, hasDocumentVerified) {
|
|
105
|
+
const raw = Math.min(exports.PROTOCOL.MAX_SCORE, identityScore + reputationScore);
|
|
106
|
+
if (hasDocumentVerified) {
|
|
107
|
+
return Math.max(exports.PROTOCOL.VERIFIED_SCORE_FLOOR, raw);
|
|
108
|
+
}
|
|
109
|
+
return raw;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Retry con backoff exponencial + jitter para llamadas a nodos validadores.
|
|
113
|
+
* Respeta VERIFY_RETRY_MAX, VERIFY_RETRY_BASE_MS y VERIFY_RETRY_MAX_MS.
|
|
114
|
+
*
|
|
115
|
+
* @param fn - Función async que puede fallar
|
|
116
|
+
* @param label - Label para logs
|
|
117
|
+
* @returns - Resultado de fn al tener éxito
|
|
118
|
+
* @throws - Error del último intento si todos fallan
|
|
119
|
+
*/
|
|
120
|
+
async function withRetry(fn, label = "soulprint-verify") {
|
|
121
|
+
let lastError = new Error("No attempts made");
|
|
122
|
+
for (let attempt = 1; attempt <= exports.PROTOCOL.VERIFY_RETRY_MAX; attempt++) {
|
|
123
|
+
try {
|
|
124
|
+
return await fn();
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
128
|
+
if (attempt === exports.PROTOCOL.VERIFY_RETRY_MAX)
|
|
129
|
+
break;
|
|
130
|
+
// Backoff exponencial con jitter
|
|
131
|
+
const baseDelay = exports.PROTOCOL.VERIFY_RETRY_BASE_MS * Math.pow(2, attempt - 1);
|
|
132
|
+
const jitter = Math.random() * exports.PROTOCOL.VERIFY_RETRY_JITTER_MS;
|
|
133
|
+
const delay = Math.min(baseDelay + jitter, exports.PROTOCOL.VERIFY_RETRY_MAX_MS);
|
|
134
|
+
console.warn(`[${label}] Attempt ${attempt}/${exports.PROTOCOL.VERIFY_RETRY_MAX} failed: ${lastError.message}. Retrying in ${Math.round(delay)}ms...`);
|
|
135
|
+
await new Promise(r => setTimeout(r, delay));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`[${label}] All ${exports.PROTOCOL.VERIFY_RETRY_MAX} attempts failed. Last error: ${lastError.message}`);
|
|
139
|
+
}
|