soulprint-network 0.3.4 → 0.3.6

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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "codeHash": "35b73961e9b592ba5c735379ce44c5ef8dda1db97da632afcb275da4e571a065",
3
- "codeHashHex": "0x35b73961e9b592ba5c735379ce44c5ef8dda1db97da632afcb275da4e571a065",
4
- "computedAt": "2026-02-24T20:55:30.860Z",
2
+ "codeHash": "9aeeff8b0b506a6e06a6c297165b8b397c0c1652e9e2844e05aabe36c14b490c",
3
+ "codeHashHex": "0x9aeeff8b0b506a6e06a6c297165b8b397c0c1652e9e2844e05aabe36c14b490c",
4
+ "computedAt": "2026-02-24T21:50:18.197Z",
5
5
  "fileCount": 17,
6
6
  "files": [
7
7
  "blockchain/blockchain-anchor.ts",
@@ -32,7 +32,7 @@ export declare function getCodeIntegrity(): CodeIntegrityInfo;
32
32
  * @param approvedHashes Lista de hashes aprobados por governance
33
33
  * @returns true si el hash actual está en la lista de aprobados
34
34
  */
35
- export declare function isCodeApproved(approvedHashes: string[]): boolean;
35
+ export declare function isCodeApproved(approvedHashes: (string | null | undefined)[]): boolean;
36
36
  /**
37
37
  * Computa un hash rápido del propio binario en tiempo real (fallback).
38
38
  * Menos preciso que el hash del código fuente pero no requiere build step.
@@ -68,9 +68,13 @@ export function getCodeIntegrity() {
68
68
  export function isCodeApproved(approvedHashes) {
69
69
  const info = getCodeIntegrity();
70
70
  if (!info.available)
71
- return false; // si no hay hash, mejor denegar
71
+ return false;
72
72
  const h = info.codeHash.toLowerCase().replace("0x", "");
73
- return approvedHashes.some(a => a.toLowerCase().replace("0x", "") === h);
73
+ return approvedHashes.some(a => {
74
+ if (a == null || typeof a !== "string")
75
+ return false;
76
+ return a.toLowerCase().replace("0x", "") === h;
77
+ });
74
78
  }
75
79
  /**
76
80
  * Computa un hash rápido del propio binario en tiempo real (fallback).
package/dist/validator.js CHANGED
@@ -2,7 +2,7 @@ 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, PROTOCOL_HASH, isProtocolHashCompatible, checkFarming, recordApprovedGain, recordFarmingStrike, loadAuditStore, exportAuditStore, } from "soulprint-core";
5
+ import { generateKeypair, keypairFromPrivateKey, decodeToken, sign, createToken, TOKEN_LIFETIME_SECONDS, TOKEN_RENEW_PREEMPTIVE_SECS, TOKEN_RENEW_GRACE_SECS, TOKEN_RENEW_COOLDOWN_SECS, verifyAttestation, computeReputation, defaultReputation, PROTOCOL, PROTOCOL_HASH, isProtocolHashCompatible, computeTotalScoreWithFloor, checkFarming, recordApprovedGain, recordFarmingStrike, loadAuditStore, exportAuditStore, } from "soulprint-core";
6
6
  import { verifyProof, deserializeProof } from "soulprint-zkp";
7
7
  import { handleCredentialRoute } from "./credentials/index.js";
8
8
  import { encryptGossip, decryptGossip } from "./crypto/gossip-cipher.js";
@@ -591,6 +591,109 @@ async function handleVerify(req, res, nodeKeypair, ip) {
591
591
  verified_at: now,
592
592
  });
593
593
  }
594
+ // ── POST /token/renew ─────────────────────────────────────────────────────────
595
+ /**
596
+ * Auto-renueva un SPT próximo a expirar o recién expirado.
597
+ *
598
+ * REGLAS:
599
+ * • Token dentro del período de pre-renew (< 1h restante) → renovar
600
+ * • Token expirado hace < 7 días → renovar (grace period)
601
+ * • Token expirado hace > 7 días → denegar, requiere re-verificación completa
602
+ * • Score actual < VERIFIED_SCORE_FLOOR → denegar
603
+ * • Cooldown: un mismo DID no puede renovar más de 1 vez cada 60s
604
+ *
605
+ * Body: { spt: "<token_actual>" }
606
+ * Respuesta: { spt: "<token_nuevo>", expires_in: <segundos>, renewed: true }
607
+ */
608
+ async function handleTokenRenew(req, res, nodeKeypair) {
609
+ const body = await readBody(req);
610
+ if (!body?.spt)
611
+ return json(res, 400, { error: "Required: spt" });
612
+ // Decodificar sin verificar expiración (queremos ver el DID aunque esté expirado)
613
+ const token = decodeToken(body.spt);
614
+ if (!token)
615
+ return json(res, 401, { error: "Invalid SPT — cannot decode" });
616
+ const nowSecs = Math.floor(Date.now() / 1000);
617
+ const secsUntilExpiry = token.expires - nowSecs;
618
+ const secsAfterExpiry = nowSecs - token.expires;
619
+ // Ventana de renovación permitida
620
+ const TOKEN_LIFETIME = TOKEN_LIFETIME_SECONDS;
621
+ const RENEW_PREEMPT = TOKEN_RENEW_PREEMPTIVE_SECS;
622
+ const RENEW_GRACE = TOKEN_RENEW_GRACE_SECS;
623
+ const RENEW_COOLDOWN = TOKEN_RENEW_COOLDOWN_SECS;
624
+ // ── Verificar ventana de tiempo ──────────────────────────────────────────
625
+ const isExpired = secsUntilExpiry <= 0;
626
+ const inPreemptWindow = !isExpired && secsUntilExpiry <= RENEW_PREEMPT;
627
+ const inGraceWindow = isExpired && secsAfterExpiry <= RENEW_GRACE;
628
+ if (!inPreemptWindow && !inGraceWindow) {
629
+ if (!isExpired) {
630
+ return json(res, 400, {
631
+ error: "Token válido — no necesita renovación aún",
632
+ expires_in: secsUntilExpiry,
633
+ renew_after: secsUntilExpiry - RENEW_PREEMPT,
634
+ });
635
+ }
636
+ return json(res, 401, {
637
+ error: "Token expirado hace más de 7 días — requiere re-verificación completa",
638
+ expired_ago: secsAfterExpiry,
639
+ max_grace: RENEW_GRACE,
640
+ });
641
+ }
642
+ // ── Anti-spam: cooldown por DID ──────────────────────────────────────────
643
+ const lastRenewKey = `renew:${token.did}`;
644
+ const lastRenew = repStore[token.did]?._lastRenew ?? 0;
645
+ if (nowSecs - lastRenew < RENEW_COOLDOWN) {
646
+ return json(res, 429, {
647
+ error: "Renovación muy frecuente — espera 60s entre renovaciones",
648
+ retry_in: RENEW_COOLDOWN - (nowSecs - lastRenew),
649
+ });
650
+ }
651
+ // ── Verificar que el DID sigue registrado en el estado P2P ───────────────
652
+ const nullifierPair = Object.entries(nullifiers).find(([, n]) => n.did === token.did);
653
+ const nullifierEntry = nullifierPair?.[1];
654
+ if (!nullifierEntry) {
655
+ return json(res, 403, {
656
+ error: "DID no registrado en este nodo — requiere re-verificación",
657
+ did: token.did,
658
+ });
659
+ }
660
+ const nullifierHash = nullifierPair[0];
661
+ // ── Verificar score actual (puede haber bajado desde el último token) ────
662
+ const repEntry = repStore[token.did];
663
+ const currentRep = repEntry
664
+ ? computeTotalScoreWithFloor(repEntry.identityScore ?? 0, repEntry.score ?? 0, repEntry.hasDocumentVerified ?? false)
665
+ : 0;
666
+ const scoreFloor = PROTOCOL.VERIFIED_SCORE_FLOOR ?? 52;
667
+ if (currentRep < scoreFloor) {
668
+ return json(res, 403, {
669
+ error: "Score por debajo del floor — renovación denegada",
670
+ score: currentRep,
671
+ floor: scoreFloor,
672
+ hint: "El bot necesita más attestations positivas",
673
+ });
674
+ }
675
+ // ── Todo OK → emitir nuevo SPT ───────────────────────────────────────────
676
+ // Mantener las mismas credenciales y score del token original
677
+ const newSpt = createToken(nodeKeypair, nullifierHash, // nullifier hash (fuente de verdad)
678
+ (token.credentials ?? []), {
679
+ lifetimeSeconds: TOKEN_LIFETIME,
680
+ country: token.country,
681
+ });
682
+ // Registrar timestamp de renovación (anti-spam)
683
+ if (repStore[token.did]) {
684
+ repStore[token.did]._lastRenew = nowSecs;
685
+ saveReputation();
686
+ }
687
+ console.log(`[renew] ✅ ${token.did.slice(0, 20)}... → nuevo SPT (${isExpired ? "post-grace" : "pre-emptivo"})`);
688
+ return json(res, 200, {
689
+ spt: newSpt,
690
+ expires_in: TOKEN_LIFETIME,
691
+ renewed: true,
692
+ method: isExpired ? "grace_window" : "preemptive",
693
+ old_expired: isExpired,
694
+ node_did: nodeKeypair.did,
695
+ });
696
+ }
594
697
  // ── GET /nullifier/:hash ──────────────────────────────────────────────────────
595
698
  function handleNullifierCheck(res, nullifier) {
596
699
  if (!/^(0x)?[0-9a-fA-F]{1,128}$/.test(nullifier))
@@ -739,6 +842,8 @@ export function startValidatorNode(port = PORT) {
739
842
  return handleProtocol(res);
740
843
  if (cleanUrl === "/verify" && req.method === "POST")
741
844
  return handleVerify(req, res, nodeKeypair, ip);
845
+ if (cleanUrl === "/token/renew" && req.method === "POST")
846
+ return handleTokenRenew(req, res, nodeKeypair);
742
847
  if (cleanUrl === "/reputation/attest" && req.method === "POST")
743
848
  return handleAttest(req, res, ip);
744
849
  if (cleanUrl === "/peers/register" && req.method === "POST")
@@ -924,6 +1029,7 @@ export function startValidatorNode(port = PORT) {
924
1029
  console.log(` Known peers: ${peers.length}`);
925
1030
  console.log(`\n Core endpoints:`);
926
1031
  console.log(` POST /verify verify ZK proof + co-sign`);
1032
+ console.log(` POST /token/renew auto-renew SPT (pre-emptivo 1h / grace 7d)`);
927
1033
  console.log(` GET /health code integrity + governance status`);
928
1034
  console.log(` GET /info node info`);
929
1035
  console.log(` GET /protocol protocol constants (immutable)`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "soulprint-network",
3
- "version": "0.3.4",
4
- "description": "Soulprint validator node \u2014 HTTP server that verifies ZK proofs, co-signs SPTs, anti-Sybil registry",
3
+ "version": "0.3.6",
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",
7
7
  "bin": {
@@ -68,4 +68,4 @@
68
68
  "types": "./dist/index.d.ts"
69
69
  }
70
70
  }
71
- }
71
+ }