soulprint-network 0.4.0 → 0.4.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/blockchain/PeerRegistryClient.d.ts +40 -0
- package/dist/blockchain/PeerRegistryClient.js +125 -0
- package/dist/blockchain/addresses.json +3 -0
- package/dist/blockchain/protocol-thresholds-client.d.ts +49 -0
- package/dist/blockchain/protocol-thresholds-client.js +102 -0
- package/dist/code-hash.json +6 -4
- package/dist/server.js +39 -1
- package/dist/validator.d.ts +14 -1
- package/dist/validator.js +113 -13
- package/package.json +9 -9
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const PEER_REGISTRY_RPC = "https://sepolia.base.org";
|
|
2
|
+
export declare const PEER_REGISTRY_ADDRESS: string;
|
|
3
|
+
export interface PeerEntry {
|
|
4
|
+
peerDid: string;
|
|
5
|
+
peerId: string;
|
|
6
|
+
multiaddr: string;
|
|
7
|
+
score: number;
|
|
8
|
+
lastSeen: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class PeerRegistryClient {
|
|
11
|
+
private provider;
|
|
12
|
+
private contract;
|
|
13
|
+
private wallet?;
|
|
14
|
+
private cache;
|
|
15
|
+
private cacheAt;
|
|
16
|
+
private cacheTTLMs;
|
|
17
|
+
private address;
|
|
18
|
+
constructor(opts?: {
|
|
19
|
+
rpc?: string;
|
|
20
|
+
address?: string;
|
|
21
|
+
privateKey?: string;
|
|
22
|
+
cacheTTLMs?: number;
|
|
23
|
+
});
|
|
24
|
+
/** Get all registered peers (cached, TTL 5 min) */
|
|
25
|
+
getAllPeers(): Promise<PeerEntry[]>;
|
|
26
|
+
/** Force refresh from blockchain */
|
|
27
|
+
refreshPeers(): Promise<PeerEntry[]>;
|
|
28
|
+
/** Register this node on-chain. Non-blocking — logs warning on failure. */
|
|
29
|
+
registerSelf(opts: {
|
|
30
|
+
peerDid: string;
|
|
31
|
+
peerId: string;
|
|
32
|
+
multiaddr: string;
|
|
33
|
+
score?: number;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
/** Returns multiaddrs of all peers (for bootstrap) */
|
|
36
|
+
getBootstrapMultiaddrs(): Promise<string[]>;
|
|
37
|
+
get contractAddress(): string;
|
|
38
|
+
}
|
|
39
|
+
/** Singleton instance (read-only) */
|
|
40
|
+
export declare const peerRegistryClient: PeerRegistryClient;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerRegistryClient
|
|
3
|
+
* On-chain peer discovery for Soulprint validator nodes (Base Sepolia).
|
|
4
|
+
*
|
|
5
|
+
* - On startup: reads all peers from contract → used as bootstrap multiaddrs
|
|
6
|
+
* - On startup: registers self (DID, peerId, multiaddr, score=0)
|
|
7
|
+
* - Cache TTL 5 min, refreshPeers() to invalidate
|
|
8
|
+
* - Non-blocking: RPC failures log a warning and continue
|
|
9
|
+
*/
|
|
10
|
+
import { ethers } from "ethers";
|
|
11
|
+
export const PEER_REGISTRY_RPC = "https://sepolia.base.org";
|
|
12
|
+
// Load address from addresses.json
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
function loadAddress() {
|
|
18
|
+
try {
|
|
19
|
+
const f = join(__dirname, "addresses.json");
|
|
20
|
+
const d = JSON.parse(readFileSync(f, "utf8"));
|
|
21
|
+
return d["PeerRegistry"] ?? "";
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const PEER_REGISTRY_ADDRESS = loadAddress();
|
|
28
|
+
const ABI = [
|
|
29
|
+
"function registerPeer(string peerDid, string peerId, string multiaddr, uint256 score) external",
|
|
30
|
+
"function removePeer(string peerDid) external",
|
|
31
|
+
"function getPeer(string peerDid) external view returns (string did, string peerId, string multiaddr, uint256 score, uint256 lastSeen)",
|
|
32
|
+
"function getAllPeers() external view returns (tuple(string peerDid, string peerId, string multiaddr, uint256 score, uint256 lastSeen, address registrant)[])",
|
|
33
|
+
"function peerCount() external view returns (uint256)",
|
|
34
|
+
"event PeerRegistered(string indexed peerDid, string peerId, string multiaddr, uint256 score, address indexed registrant, uint256 timestamp)",
|
|
35
|
+
"event PeerUpdated(string indexed peerDid, string peerId, string multiaddr, uint256 score, address indexed registrant, uint256 timestamp)",
|
|
36
|
+
"event PeerRemoved(string indexed peerDid, address indexed removedBy, uint256 timestamp)",
|
|
37
|
+
];
|
|
38
|
+
export class PeerRegistryClient {
|
|
39
|
+
provider;
|
|
40
|
+
contract;
|
|
41
|
+
wallet;
|
|
42
|
+
cache = null;
|
|
43
|
+
cacheAt = 0;
|
|
44
|
+
cacheTTLMs;
|
|
45
|
+
address;
|
|
46
|
+
constructor(opts) {
|
|
47
|
+
this.address = opts?.address ?? PEER_REGISTRY_ADDRESS;
|
|
48
|
+
this.cacheTTLMs = opts?.cacheTTLMs ?? 5 * 60 * 1000; // 5 min
|
|
49
|
+
const rpc = opts?.rpc ?? PEER_REGISTRY_RPC;
|
|
50
|
+
this.provider = new ethers.JsonRpcProvider(rpc);
|
|
51
|
+
if (!this.address) {
|
|
52
|
+
console.warn("[peer-registry] ⚠️ No contract address — peer registry disabled");
|
|
53
|
+
this.contract = null;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (opts?.privateKey) {
|
|
57
|
+
this.wallet = new ethers.Wallet(opts.privateKey, this.provider);
|
|
58
|
+
this.contract = new ethers.Contract(this.address, ABI, this.wallet);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.contract = new ethers.Contract(this.address, ABI, this.provider);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Get all registered peers (cached, TTL 5 min) */
|
|
65
|
+
async getAllPeers() {
|
|
66
|
+
if (this.cache && Date.now() - this.cacheAt < this.cacheTTLMs) {
|
|
67
|
+
return this.cache;
|
|
68
|
+
}
|
|
69
|
+
return this.refreshPeers();
|
|
70
|
+
}
|
|
71
|
+
/** Force refresh from blockchain */
|
|
72
|
+
async refreshPeers() {
|
|
73
|
+
if (!this.address) {
|
|
74
|
+
console.warn("[peer-registry] ⚠️ No contract address configured — skipping peer fetch");
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const raw = await this.contract.getAllPeers();
|
|
79
|
+
this.cache = raw.map((p) => ({
|
|
80
|
+
peerDid: p.peerDid,
|
|
81
|
+
peerId: p.peerId,
|
|
82
|
+
multiaddr: p.multiaddr,
|
|
83
|
+
score: Number(p.score),
|
|
84
|
+
lastSeen: Number(p.lastSeen),
|
|
85
|
+
}));
|
|
86
|
+
this.cacheAt = Date.now();
|
|
87
|
+
return this.cache;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.warn(`[peer-registry] ⚠️ Could not fetch peers from chain: ${err.shortMessage ?? err.message}`);
|
|
91
|
+
return this.cache ?? [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Register this node on-chain. Non-blocking — logs warning on failure. */
|
|
95
|
+
async registerSelf(opts) {
|
|
96
|
+
if (!this.wallet) {
|
|
97
|
+
console.warn("[peer-registry] ⚠️ No private key — cannot register self on-chain");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!this.address) {
|
|
101
|
+
console.warn("[peer-registry] ⚠️ No contract address — skipping self-registration");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const feeData = await this.provider.getFeeData();
|
|
106
|
+
const tx = await this.contract.registerPeer(opts.peerDid, opts.peerId, opts.multiaddr, BigInt(opts.score ?? 0), { gasPrice: feeData.gasPrice });
|
|
107
|
+
console.log(`[peer-registry] 📡 Registering self on-chain... tx: ${tx.hash}`);
|
|
108
|
+
await tx.wait();
|
|
109
|
+
console.log(`[peer-registry] ✅ Registered: did=${opts.peerDid.slice(0, 20)}... multiaddr=${opts.multiaddr}`);
|
|
110
|
+
// Invalidate cache so next getAllPeers() reflects the new entry
|
|
111
|
+
this.cache = null;
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
console.warn(`[peer-registry] ⚠️ Self-registration failed (non-fatal): ${err.shortMessage ?? err.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Returns multiaddrs of all peers (for bootstrap) */
|
|
118
|
+
async getBootstrapMultiaddrs() {
|
|
119
|
+
const peers = await this.getAllPeers();
|
|
120
|
+
return peers.map(p => p.multiaddr).filter(Boolean);
|
|
121
|
+
}
|
|
122
|
+
get contractAddress() { return this.address; }
|
|
123
|
+
}
|
|
124
|
+
/** Singleton instance (read-only) */
|
|
125
|
+
export const peerRegistryClient = new PeerRegistryClient();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProtocolThresholdsClient
|
|
3
|
+
* Lee los thresholds del protocolo desde Base Sepolia.
|
|
4
|
+
* - Cache con TTL (10 min por defecto)
|
|
5
|
+
* - Fallback transparente a constantes locales si la blockchain no es accesible
|
|
6
|
+
* - Solo el superAdmin puede actualizar on-chain; aquí solo leemos
|
|
7
|
+
*/
|
|
8
|
+
import { ethers } from "ethers";
|
|
9
|
+
export declare const PROTOCOL_THRESHOLDS_ADDRESS = "0xD8f78d65b35806101672A49801b57F743f2D2ab1";
|
|
10
|
+
export declare const PROTOCOL_THRESHOLDS_CHAIN = "Base Sepolia (chainId: 84532)";
|
|
11
|
+
export declare const PROTOCOL_THRESHOLDS_RPC = "https://sepolia.base.org";
|
|
12
|
+
export interface ProtocolThresholds {
|
|
13
|
+
SCORE_FLOOR: number;
|
|
14
|
+
VERIFIED_SCORE_FLOOR: number;
|
|
15
|
+
MIN_ATTESTER_SCORE: number;
|
|
16
|
+
FACE_SIM_DOC_SELFIE: number;
|
|
17
|
+
FACE_SIM_SELFIE_SELFIE: number;
|
|
18
|
+
DEFAULT_REPUTATION: number;
|
|
19
|
+
IDENTITY_MAX: number;
|
|
20
|
+
REPUTATION_MAX: number;
|
|
21
|
+
VERIFY_RETRY_MAX: number;
|
|
22
|
+
source: "blockchain" | "local_fallback";
|
|
23
|
+
loadedAt: number;
|
|
24
|
+
superAdmin?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class ProtocolThresholdsClient {
|
|
27
|
+
private provider;
|
|
28
|
+
private contract;
|
|
29
|
+
private cache;
|
|
30
|
+
private cacheTTLMs;
|
|
31
|
+
private address;
|
|
32
|
+
constructor(opts?: {
|
|
33
|
+
rpc?: string;
|
|
34
|
+
address?: string;
|
|
35
|
+
cacheTTLMs?: number;
|
|
36
|
+
});
|
|
37
|
+
/** Carga thresholds desde blockchain — fallback transparente a local */
|
|
38
|
+
load(): Promise<ProtocolThresholds>;
|
|
39
|
+
/** Invalida la cache — el siguiente load() irá a blockchain */
|
|
40
|
+
invalidate(): void;
|
|
41
|
+
/** Lee un threshold individual (con cache) */
|
|
42
|
+
get(name: keyof Omit<ProtocolThresholds, "source" | "loadedAt" | "superAdmin">): Promise<number>;
|
|
43
|
+
/** Devuelve la dirección del contrato */
|
|
44
|
+
get contractAddress(): string;
|
|
45
|
+
/** Acceso directo al contrato para uso en tests */
|
|
46
|
+
get rawContract(): ethers.Contract;
|
|
47
|
+
}
|
|
48
|
+
/** Instancia global (singleton) para uso en el validador */
|
|
49
|
+
export declare const thresholdsClient: ProtocolThresholdsClient;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProtocolThresholdsClient
|
|
3
|
+
* Lee los thresholds del protocolo desde Base Sepolia.
|
|
4
|
+
* - Cache con TTL (10 min por defecto)
|
|
5
|
+
* - Fallback transparente a constantes locales si la blockchain no es accesible
|
|
6
|
+
* - Solo el superAdmin puede actualizar on-chain; aquí solo leemos
|
|
7
|
+
*/
|
|
8
|
+
import { ethers } from "ethers";
|
|
9
|
+
import { PROTOCOL } from "soulprint-core";
|
|
10
|
+
export const PROTOCOL_THRESHOLDS_ADDRESS = "0xD8f78d65b35806101672A49801b57F743f2D2ab1";
|
|
11
|
+
export const PROTOCOL_THRESHOLDS_CHAIN = "Base Sepolia (chainId: 84532)";
|
|
12
|
+
export const PROTOCOL_THRESHOLDS_RPC = "https://sepolia.base.org";
|
|
13
|
+
const ABI = [
|
|
14
|
+
"function getThreshold(string calldata name) external view returns (uint256)",
|
|
15
|
+
"function superAdmin() external view returns (address)",
|
|
16
|
+
"function pendingSuperAdmin() external view returns (address)",
|
|
17
|
+
"function getAll() external view returns (string[] memory names, uint256[] memory values)",
|
|
18
|
+
"event ThresholdUpdated(bytes32 indexed key, uint256 oldValue, uint256 newValue, address indexed by, uint256 timestamp)",
|
|
19
|
+
];
|
|
20
|
+
// Valores locales como fallback
|
|
21
|
+
function localFallback(source = "local_fallback") {
|
|
22
|
+
return {
|
|
23
|
+
SCORE_FLOOR: PROTOCOL.SCORE_FLOOR,
|
|
24
|
+
VERIFIED_SCORE_FLOOR: PROTOCOL.VERIFIED_SCORE_FLOOR,
|
|
25
|
+
MIN_ATTESTER_SCORE: PROTOCOL.MIN_ATTESTER_SCORE,
|
|
26
|
+
FACE_SIM_DOC_SELFIE: Math.round(PROTOCOL.FACE_SIM_DOC_SELFIE * 1000),
|
|
27
|
+
FACE_SIM_SELFIE_SELFIE: Math.round(PROTOCOL.FACE_SIM_SELFIE_SELFIE * 1000),
|
|
28
|
+
DEFAULT_REPUTATION: PROTOCOL.DEFAULT_REPUTATION,
|
|
29
|
+
IDENTITY_MAX: PROTOCOL.IDENTITY_MAX,
|
|
30
|
+
REPUTATION_MAX: PROTOCOL.REPUTATION_MAX,
|
|
31
|
+
VERIFY_RETRY_MAX: PROTOCOL.VERIFY_RETRY_MAX,
|
|
32
|
+
source,
|
|
33
|
+
loadedAt: Date.now(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export class ProtocolThresholdsClient {
|
|
37
|
+
provider;
|
|
38
|
+
contract;
|
|
39
|
+
cache = null;
|
|
40
|
+
cacheTTLMs;
|
|
41
|
+
address;
|
|
42
|
+
constructor(opts) {
|
|
43
|
+
this.address = opts?.address ?? PROTOCOL_THRESHOLDS_ADDRESS;
|
|
44
|
+
this.cacheTTLMs = opts?.cacheTTLMs ?? 10 * 60 * 1000; // 10 min
|
|
45
|
+
const rpc = opts?.rpc ?? PROTOCOL_THRESHOLDS_RPC;
|
|
46
|
+
this.provider = new ethers.JsonRpcProvider(rpc);
|
|
47
|
+
this.contract = new ethers.Contract(this.address, ABI, this.provider);
|
|
48
|
+
}
|
|
49
|
+
/** Carga thresholds desde blockchain — fallback transparente a local */
|
|
50
|
+
async load() {
|
|
51
|
+
// Cache hit
|
|
52
|
+
if (this.cache && Date.now() - this.cache.loadedAt < this.cacheTTLMs) {
|
|
53
|
+
return this.cache;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const [admin, floor, vfloor, minAtt, faceSim, faceSimSS, defRep, idMax, repMax, retryMax] = await Promise.all([
|
|
57
|
+
this.contract.superAdmin(),
|
|
58
|
+
this.contract.getThreshold("SCORE_FLOOR"),
|
|
59
|
+
this.contract.getThreshold("VERIFIED_SCORE_FLOOR"),
|
|
60
|
+
this.contract.getThreshold("MIN_ATTESTER_SCORE"),
|
|
61
|
+
this.contract.getThreshold("FACE_SIM_DOC_SELFIE"),
|
|
62
|
+
this.contract.getThreshold("FACE_SIM_SELFIE_SELFIE"),
|
|
63
|
+
this.contract.getThreshold("DEFAULT_REPUTATION"),
|
|
64
|
+
this.contract.getThreshold("IDENTITY_MAX"),
|
|
65
|
+
this.contract.getThreshold("REPUTATION_MAX"),
|
|
66
|
+
this.contract.getThreshold("VERIFY_RETRY_MAX"),
|
|
67
|
+
]);
|
|
68
|
+
this.cache = {
|
|
69
|
+
SCORE_FLOOR: Number(floor),
|
|
70
|
+
VERIFIED_SCORE_FLOOR: Number(vfloor),
|
|
71
|
+
MIN_ATTESTER_SCORE: Number(minAtt),
|
|
72
|
+
FACE_SIM_DOC_SELFIE: Number(faceSim),
|
|
73
|
+
FACE_SIM_SELFIE_SELFIE: Number(faceSimSS),
|
|
74
|
+
DEFAULT_REPUTATION: Number(defRep),
|
|
75
|
+
IDENTITY_MAX: Number(idMax),
|
|
76
|
+
REPUTATION_MAX: Number(repMax),
|
|
77
|
+
VERIFY_RETRY_MAX: Number(retryMax),
|
|
78
|
+
source: "blockchain",
|
|
79
|
+
loadedAt: Date.now(),
|
|
80
|
+
superAdmin: admin,
|
|
81
|
+
};
|
|
82
|
+
return this.cache;
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.warn(`[thresholds] ⚠️ Blockchain no disponible — usando fallback local (${err.shortMessage ?? err.message})`);
|
|
86
|
+
return localFallback("local_fallback");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Invalida la cache — el siguiente load() irá a blockchain */
|
|
90
|
+
invalidate() { this.cache = null; }
|
|
91
|
+
/** Lee un threshold individual (con cache) */
|
|
92
|
+
async get(name) {
|
|
93
|
+
const t = await this.load();
|
|
94
|
+
return t[name];
|
|
95
|
+
}
|
|
96
|
+
/** Devuelve la dirección del contrato */
|
|
97
|
+
get contractAddress() { return this.address; }
|
|
98
|
+
/** Acceso directo al contrato para uso en tests */
|
|
99
|
+
get rawContract() { return this.contract; }
|
|
100
|
+
}
|
|
101
|
+
/** Instancia global (singleton) para uso en el validador */
|
|
102
|
+
export const thresholdsClient = new ProtocolThresholdsClient();
|
package/dist/code-hash.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codeHash": "
|
|
3
|
-
"codeHashHex": "
|
|
4
|
-
"computedAt": "2026-
|
|
5
|
-
"fileCount":
|
|
2
|
+
"codeHash": "740841a4e4069e7576a21b2da277ae7bb5b65bed028121cbc3277c865f58544f",
|
|
3
|
+
"codeHashHex": "0x740841a4e4069e7576a21b2da277ae7bb5b65bed028121cbc3277c865f58544f",
|
|
4
|
+
"computedAt": "2026-03-01T01:46:41.787Z",
|
|
5
|
+
"fileCount": 21,
|
|
6
6
|
"files": [
|
|
7
|
+
"blockchain/PeerRegistryClient.ts",
|
|
7
8
|
"blockchain/blockchain-anchor.ts",
|
|
8
9
|
"blockchain/blockchain-client.ts",
|
|
10
|
+
"blockchain/protocol-thresholds-client.ts",
|
|
9
11
|
"code-integrity.ts",
|
|
10
12
|
"consensus/attestation-consensus.ts",
|
|
11
13
|
"consensus/index.ts",
|
package/dist/server.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* 1. HTTP server (port 4888) — clientes y legado
|
|
7
7
|
* 2. libp2p P2P node (port 6888) — Kademlia DHT + GossipSub + mDNS
|
|
8
8
|
*/
|
|
9
|
-
import { startValidatorNode, setP2PNode } from "./validator.js";
|
|
9
|
+
import { startValidatorNode, setP2PNode, setPeerRegistryClient } from "./validator.js";
|
|
10
10
|
import { createSoulprintP2PNode, MAINNET_BOOTSTRAP, stopP2PNode } from "./p2p.js";
|
|
11
|
+
import { PeerRegistryClient } from "./blockchain/PeerRegistryClient.js";
|
|
11
12
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
12
13
|
const HTTP_PORT = parseInt(process.env.SOULPRINT_PORT ?? "4888");
|
|
13
14
|
const P2P_PORT = parseInt(process.env.SOULPRINT_P2P_PORT ?? String(HTTP_PORT + 2000));
|
|
@@ -42,6 +43,43 @@ catch (err) {
|
|
|
42
43
|
console.warn(`⚠️ P2P no disponible — solo HTTP gossip activo`);
|
|
43
44
|
console.warn(` Error: ${err?.message ?? String(err)}\n`);
|
|
44
45
|
}
|
|
46
|
+
// ─── On-chain PeerRegistry (auto-registro) ────────────────────────────────────
|
|
47
|
+
const adminPrivateKey = process.env.ADMIN_PRIVATE_KEY;
|
|
48
|
+
const peerRegistry = new PeerRegistryClient({
|
|
49
|
+
privateKey: adminPrivateKey,
|
|
50
|
+
});
|
|
51
|
+
setPeerRegistryClient(peerRegistry);
|
|
52
|
+
// Register self after P2P is ready (non-blocking)
|
|
53
|
+
setTimeout(async () => {
|
|
54
|
+
try {
|
|
55
|
+
if (!adminPrivateKey) {
|
|
56
|
+
console.warn("[peer-registry] ⚠️ ADMIN_PRIVATE_KEY not set — skipping on-chain registration");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Get node identity from validator (fetched via HTTP)
|
|
60
|
+
const infoRes = await fetch(`http://localhost:${HTTP_PORT}/health`, { signal: AbortSignal.timeout(5000) });
|
|
61
|
+
const info = await infoRes.json();
|
|
62
|
+
// Get P2P multiaddr (first non-localhost if available)
|
|
63
|
+
let multiaddr = `http://localhost:${HTTP_PORT}`;
|
|
64
|
+
if (p2pNode) {
|
|
65
|
+
const addrs = p2pNode.getMultiaddrs().map((m) => m.toString());
|
|
66
|
+
const publicAddr = addrs.find(a => !a.includes("127.0.0.1") && !a.includes("/ip4/0.0.0.0"));
|
|
67
|
+
multiaddr = publicAddr ?? addrs[0] ?? multiaddr;
|
|
68
|
+
}
|
|
69
|
+
// Use node DID from keypair (read from health endpoint) and P2P peerId
|
|
70
|
+
const nodeDid = globalThis._nodeDid ?? `did:soulprint:node:${Date.now()}`;
|
|
71
|
+
const nodePeer = p2pNode?.peerId?.toString() ?? "";
|
|
72
|
+
await peerRegistry.registerSelf({
|
|
73
|
+
peerDid: nodeDid,
|
|
74
|
+
peerId: nodePeer,
|
|
75
|
+
multiaddr,
|
|
76
|
+
score: 0,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
console.warn(`[peer-registry] ⚠️ Could not register self: ${e.message}`);
|
|
81
|
+
}
|
|
82
|
+
}, 3_000);
|
|
45
83
|
// ─── HTTP Bootstrap Peers (auto-registro) ─────────────────────────────────────
|
|
46
84
|
// SOULPRINT_BOOTSTRAP_HTTP=http://node1:4888,http://node2:4888
|
|
47
85
|
// Registra peers HTTP automáticamente al arrancar (útil en WSL2 / Docker / cloud)
|
package/dist/validator.d.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { BotAttestation, BotReputation } from "soulprint-core";
|
|
3
3
|
import { type SoulprintP2PNode } from "./p2p.js";
|
|
4
|
+
import { PeerRegistryClient } from "./blockchain/PeerRegistryClient.js";
|
|
5
|
+
export declare function getLiveThresholds(): {
|
|
6
|
+
SCORE_FLOOR: number;
|
|
7
|
+
VERIFIED_SCORE_FLOOR: number;
|
|
8
|
+
MIN_ATTESTER_SCORE: number;
|
|
9
|
+
FACE_SIM_DOC_SELFIE: number;
|
|
10
|
+
FACE_SIM_SELFIE_SELFIE: number;
|
|
11
|
+
DEFAULT_REPUTATION: number;
|
|
12
|
+
IDENTITY_MAX: number;
|
|
13
|
+
REPUTATION_MAX: number;
|
|
14
|
+
source: "blockchain" | "local_fallback";
|
|
15
|
+
};
|
|
16
|
+
export declare function setPeerRegistryClient(client: PeerRegistryClient): void;
|
|
4
17
|
/**
|
|
5
18
|
* Inyecta el nodo libp2p al validador.
|
|
6
19
|
* Cuando se llama:
|
|
@@ -13,7 +26,7 @@ export declare function setP2PNode(node: SoulprintP2PNode): void;
|
|
|
13
26
|
*
|
|
14
27
|
* PROTOCOL ENFORCEMENT:
|
|
15
28
|
* - Si el bot tiene DocumentVerified, su score total nunca puede caer por
|
|
16
|
-
* debajo de
|
|
29
|
+
* debajo de liveThresholds.VERIFIED_SCORE_FLOOR (52) - inamovible.
|
|
17
30
|
* - Anti-replay: la misma attestation (mismo issuer + timestamp + context)
|
|
18
31
|
* no se puede aplicar dos veces.
|
|
19
32
|
*
|
package/dist/validator.js
CHANGED
|
@@ -11,6 +11,7 @@ import { selectGossipPeers, routingStats } from "./crypto/peer-router.js";
|
|
|
11
11
|
import { NullifierConsensus, AttestationConsensus, StateSyncManager } from "./consensus/index.js";
|
|
12
12
|
import { BlockchainAnchor } from "./blockchain/blockchain-anchor.js";
|
|
13
13
|
import { SoulprintBlockchainClient, loadBlockchainConfig, } from "./blockchain/blockchain-client.js";
|
|
14
|
+
import { thresholdsClient, PROTOCOL_THRESHOLDS_ADDRESS, PROTOCOL_THRESHOLDS_CHAIN, } from "./blockchain/protocol-thresholds-client.js";
|
|
14
15
|
import { getCodeIntegrity, logCodeIntegrity, computeRuntimeHash } from "./code-integrity.js";
|
|
15
16
|
import { getMCPEntry, getVerifiedMCPEntries, getAllMCPEntries, getRegistryInfo, verifyMCPOnChain, revokeMCPOnChain, registerMCPOnChain, } from "./mcp-registry-client.js";
|
|
16
17
|
import { publishAttestationP2P, onAttestationReceived, getP2PStats, dialP2PPeer, } from "./p2p.js";
|
|
@@ -28,11 +29,52 @@ const MAX_BODY_BYTES = 64 * 1024;
|
|
|
28
29
|
const RATE_LIMIT_MS = PROTOCOL.RATE_LIMIT_WINDOW_MS;
|
|
29
30
|
const RATE_LIMIT_MAX = PROTOCOL.RATE_LIMIT_MAX;
|
|
30
31
|
const CLOCK_SKEW_MAX = PROTOCOL.CLOCK_SKEW_MAX_SECONDS;
|
|
31
|
-
const MIN_ATTESTER_SCORE = PROTOCOL.MIN_ATTESTER_SCORE; // 65 - inamovible
|
|
32
32
|
const ATT_MAX_AGE_SECONDS = PROTOCOL.ATT_MAX_AGE_SECONDS;
|
|
33
33
|
const GOSSIP_TIMEOUT_MS = PROTOCOL.GOSSIP_TIMEOUT_MS;
|
|
34
|
+
// ── Thresholds cargados desde blockchain al arrancar (fallback: local) ────────
|
|
35
|
+
// Usados en runtime — se pueden actualizar solo via superAdmin en el contrato.
|
|
36
|
+
// El validador recarga cada 10 minutos automáticamente.
|
|
37
|
+
let liveThresholds = {
|
|
38
|
+
SCORE_FLOOR: PROTOCOL.SCORE_FLOOR,
|
|
39
|
+
VERIFIED_SCORE_FLOOR: PROTOCOL.VERIFIED_SCORE_FLOOR,
|
|
40
|
+
MIN_ATTESTER_SCORE: PROTOCOL.MIN_ATTESTER_SCORE,
|
|
41
|
+
FACE_SIM_DOC_SELFIE: PROTOCOL.FACE_SIM_DOC_SELFIE,
|
|
42
|
+
FACE_SIM_SELFIE_SELFIE: PROTOCOL.FACE_SIM_SELFIE_SELFIE,
|
|
43
|
+
DEFAULT_REPUTATION: PROTOCOL.DEFAULT_REPUTATION,
|
|
44
|
+
IDENTITY_MAX: PROTOCOL.IDENTITY_MAX,
|
|
45
|
+
REPUTATION_MAX: PROTOCOL.REPUTATION_MAX,
|
|
46
|
+
source: "local_fallback",
|
|
47
|
+
};
|
|
48
|
+
export function getLiveThresholds() { return liveThresholds; }
|
|
49
|
+
async function refreshThresholds() {
|
|
50
|
+
try {
|
|
51
|
+
thresholdsClient.invalidate();
|
|
52
|
+
const t = await thresholdsClient.load();
|
|
53
|
+
liveThresholds = {
|
|
54
|
+
SCORE_FLOOR: t.SCORE_FLOOR,
|
|
55
|
+
VERIFIED_SCORE_FLOOR: t.VERIFIED_SCORE_FLOOR,
|
|
56
|
+
MIN_ATTESTER_SCORE: t.MIN_ATTESTER_SCORE,
|
|
57
|
+
FACE_SIM_DOC_SELFIE: t.FACE_SIM_DOC_SELFIE / 1000,
|
|
58
|
+
FACE_SIM_SELFIE_SELFIE: t.FACE_SIM_SELFIE_SELFIE / 1000,
|
|
59
|
+
DEFAULT_REPUTATION: t.DEFAULT_REPUTATION,
|
|
60
|
+
IDENTITY_MAX: t.IDENTITY_MAX,
|
|
61
|
+
REPUTATION_MAX: t.REPUTATION_MAX,
|
|
62
|
+
source: t.source,
|
|
63
|
+
};
|
|
64
|
+
console.log(`[thresholds] ✅ Cargados desde ${t.source === "blockchain" ? "blockchain" : "fallback local"}`);
|
|
65
|
+
if (t.source === "blockchain") {
|
|
66
|
+
console.log(`[thresholds] SCORE_FLOOR=${t.SCORE_FLOOR} VERIFIED_SCORE_FLOOR=${t.VERIFIED_SCORE_FLOOR} MIN_ATTESTER=${t.MIN_ATTESTER_SCORE}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch { /* usa los valores actuales */ }
|
|
70
|
+
}
|
|
34
71
|
// ── P2P Node (Phase 5) ────────────────────────────────────────────────────────
|
|
35
72
|
let p2pNode = null;
|
|
73
|
+
// ── PeerRegistry Client ───────────────────────────────────────────────────────
|
|
74
|
+
let peerRegistryClient = null;
|
|
75
|
+
export function setPeerRegistryClient(client) {
|
|
76
|
+
peerRegistryClient = client;
|
|
77
|
+
}
|
|
36
78
|
/**
|
|
37
79
|
* Inyecta el nodo libp2p al validador.
|
|
38
80
|
* Cuando se llama:
|
|
@@ -118,7 +160,7 @@ function getReputation(did) {
|
|
|
118
160
|
*
|
|
119
161
|
* PROTOCOL ENFORCEMENT:
|
|
120
162
|
* - Si el bot tiene DocumentVerified, su score total nunca puede caer por
|
|
121
|
-
* debajo de
|
|
163
|
+
* debajo de liveThresholds.VERIFIED_SCORE_FLOOR (52) - inamovible.
|
|
122
164
|
* - Anti-replay: la misma attestation (mismo issuer + timestamp + context)
|
|
123
165
|
* no se puede aplicar dos veces.
|
|
124
166
|
*
|
|
@@ -146,11 +188,11 @@ function applyAttestation(att) {
|
|
|
146
188
|
const hasDocument = existing?.hasDocumentVerified ?? false;
|
|
147
189
|
let finalRepScore = rep.score;
|
|
148
190
|
if (hasDocument) {
|
|
149
|
-
const minRepForFloor = Math.max(0,
|
|
191
|
+
const minRepForFloor = Math.max(0, liveThresholds.VERIFIED_SCORE_FLOOR - identityFromStore);
|
|
150
192
|
finalRepScore = Math.max(finalRepScore, minRepForFloor);
|
|
151
193
|
if (finalRepScore !== rep.score) {
|
|
152
194
|
console.log(`[floor] Reputation clamped for ${att.target_did.slice(0, 20)}...: ` +
|
|
153
|
-
`${rep.score} → ${finalRepScore} (VERIFIED_SCORE_FLOOR=${
|
|
195
|
+
`${rep.score} → ${finalRepScore} (VERIFIED_SCORE_FLOOR=${liveThresholds.VERIFIED_SCORE_FLOOR})`);
|
|
154
196
|
}
|
|
155
197
|
}
|
|
156
198
|
repStore[att.target_did] = {
|
|
@@ -293,7 +335,7 @@ function handleInfo(res, nodeKeypair) {
|
|
|
293
335
|
* Expone las constantes de protocolo inamovibles.
|
|
294
336
|
* Los clientes y otros nodos usan este endpoint para:
|
|
295
337
|
* 1. Verificar compatibilidad de versión antes de conectarse
|
|
296
|
-
* 2. Obtener los valores actuales de SCORE_FLOOR y MIN_ATTESTER_SCORE
|
|
338
|
+
* 2. Obtener los valores actuales de SCORE_FLOOR y liveThresholds.MIN_ATTESTER_SCORE
|
|
297
339
|
* 3. Validar que el nodo no ha sido modificado para bajar los thresholds
|
|
298
340
|
*/
|
|
299
341
|
function handleProtocol(res) {
|
|
@@ -304,9 +346,9 @@ function handleProtocol(res) {
|
|
|
304
346
|
// Si PROTOCOL fue modificado (aunque sea un valor), este hash cambia.
|
|
305
347
|
protocol_hash: PROTOCOL_HASH,
|
|
306
348
|
// ── Score limits ────────────────────────────────────────────────────────
|
|
307
|
-
score_floor:
|
|
308
|
-
verified_score_floor:
|
|
309
|
-
min_attester_score:
|
|
349
|
+
score_floor: liveThresholds.SCORE_FLOOR,
|
|
350
|
+
verified_score_floor: liveThresholds.VERIFIED_SCORE_FLOOR,
|
|
351
|
+
min_attester_score: liveThresholds.MIN_ATTESTER_SCORE,
|
|
310
352
|
identity_max: PROTOCOL.IDENTITY_MAX,
|
|
311
353
|
reputation_max: PROTOCOL.REPUTATION_MAX,
|
|
312
354
|
max_score: PROTOCOL.MAX_SCORE,
|
|
@@ -346,7 +388,7 @@ function handleGetReputation(res, did) {
|
|
|
346
388
|
* }
|
|
347
389
|
*
|
|
348
390
|
* Validaciones:
|
|
349
|
-
* 1. service_spt tiene score >= MIN_ATTESTER_SCORE (solo servicios verificados)
|
|
391
|
+
* 1. service_spt tiene score >= liveThresholds.MIN_ATTESTER_SCORE (solo servicios verificados)
|
|
350
392
|
* 2. service_spt.did == attestation.issuer_did (el emisor es quien dice ser)
|
|
351
393
|
* 3. Firma Ed25519 de la attestation es válida
|
|
352
394
|
* 4. timestamp no tiene más de ATT_MAX_AGE_SECONDS de antigüedad
|
|
@@ -422,10 +464,10 @@ async function handleAttest(req, res, ip) {
|
|
|
422
464
|
const serviceTok = decodeToken(service_spt);
|
|
423
465
|
if (!serviceTok)
|
|
424
466
|
return json(res, 401, { error: "Invalid or expired service_spt" });
|
|
425
|
-
if (serviceTok.score < MIN_ATTESTER_SCORE) {
|
|
467
|
+
if (serviceTok.score < liveThresholds.MIN_ATTESTER_SCORE) {
|
|
426
468
|
return json(res, 403, {
|
|
427
|
-
error: `Service score too low (${serviceTok.score} < ${MIN_ATTESTER_SCORE})`,
|
|
428
|
-
required: MIN_ATTESTER_SCORE,
|
|
469
|
+
error: `Service score too low (${serviceTok.score} < ${liveThresholds.MIN_ATTESTER_SCORE})`,
|
|
470
|
+
required: liveThresholds.MIN_ATTESTER_SCORE,
|
|
429
471
|
got: serviceTok.score,
|
|
430
472
|
});
|
|
431
473
|
}
|
|
@@ -747,7 +789,7 @@ async function handleTokenRenew(req, res, nodeKeypair) {
|
|
|
747
789
|
const currentRep = repEntry
|
|
748
790
|
? computeTotalScoreWithFloor(repEntry.identityScore ?? 0, repEntry.score ?? 0, repEntry.hasDocumentVerified ?? false)
|
|
749
791
|
: 0;
|
|
750
|
-
const scoreFloor =
|
|
792
|
+
const scoreFloor = liveThresholds.VERIFIED_SCORE_FLOOR ?? 52;
|
|
751
793
|
if (currentRep < scoreFloor) {
|
|
752
794
|
return json(res, 403, {
|
|
753
795
|
error: "Score por debajo del floor - renovación denegada",
|
|
@@ -794,6 +836,16 @@ export function startValidatorNode(port = PORT) {
|
|
|
794
836
|
loadPeers();
|
|
795
837
|
loadAudit();
|
|
796
838
|
const nodeKeypair = loadOrCreateNodeKeypair();
|
|
839
|
+
// Expose DID globally so server.ts can use it for peer registration
|
|
840
|
+
globalThis._nodeDid = nodeKeypair.did;
|
|
841
|
+
// ── Cargar thresholds desde blockchain al arrancar ────────────────────────
|
|
842
|
+
// No bloqueante — el nodo arranca con valores locales y los actualiza async
|
|
843
|
+
refreshThresholds().then(() => {
|
|
844
|
+
console.log(`[thresholds] 📡 Fuente: ${liveThresholds.source} | SCORE_FLOOR=${liveThresholds.SCORE_FLOOR}`);
|
|
845
|
+
console.log(`[thresholds] Contrato: ${PROTOCOL_THRESHOLDS_ADDRESS} (${PROTOCOL_THRESHOLDS_CHAIN})`);
|
|
846
|
+
});
|
|
847
|
+
// Refresco automático cada 10 minutos
|
|
848
|
+
setInterval(refreshThresholds, 10 * 60 * 1000);
|
|
797
849
|
// ── Módulos de consenso P2P (sin EVM, sin gas fees) ──────────────────────
|
|
798
850
|
const nullifierConsensus = new NullifierConsensus({
|
|
799
851
|
selfDid: nodeKeypair.did,
|
|
@@ -924,11 +976,57 @@ export function startValidatorNode(port = PORT) {
|
|
|
924
976
|
return handleInfo(res, nodeKeypair);
|
|
925
977
|
if (cleanUrl === "/protocol" && req.method === "GET")
|
|
926
978
|
return handleProtocol(res);
|
|
979
|
+
// GET /protocol/thresholds — thresholds live desde blockchain (superAdmin-mutable)
|
|
980
|
+
if (cleanUrl === "/protocol/thresholds" && req.method === "GET") {
|
|
981
|
+
return json(res, 200, {
|
|
982
|
+
source: liveThresholds.source,
|
|
983
|
+
contract: PROTOCOL_THRESHOLDS_ADDRESS,
|
|
984
|
+
chain: PROTOCOL_THRESHOLDS_CHAIN,
|
|
985
|
+
thresholds: {
|
|
986
|
+
SCORE_FLOOR: liveThresholds.SCORE_FLOOR,
|
|
987
|
+
VERIFIED_SCORE_FLOOR: liveThresholds.VERIFIED_SCORE_FLOOR,
|
|
988
|
+
MIN_ATTESTER_SCORE: liveThresholds.MIN_ATTESTER_SCORE,
|
|
989
|
+
FACE_SIM_DOC_SELFIE: liveThresholds.FACE_SIM_DOC_SELFIE,
|
|
990
|
+
FACE_SIM_SELFIE_SELFIE: liveThresholds.FACE_SIM_SELFIE_SELFIE,
|
|
991
|
+
DEFAULT_REPUTATION: liveThresholds.DEFAULT_REPUTATION,
|
|
992
|
+
IDENTITY_MAX: liveThresholds.IDENTITY_MAX,
|
|
993
|
+
REPUTATION_MAX: liveThresholds.REPUTATION_MAX,
|
|
994
|
+
},
|
|
995
|
+
last_loaded: new Date(Date.now()).toISOString(),
|
|
996
|
+
note: "Solo el superAdmin del contrato puede modificar estos valores on-chain",
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
// GET /network/peers — all peers from on-chain PeerRegistry
|
|
1000
|
+
if (cleanUrl === "/network/peers" && req.method === "GET") {
|
|
1001
|
+
try {
|
|
1002
|
+
const chainPeers = peerRegistryClient
|
|
1003
|
+
? await peerRegistryClient.getAllPeers()
|
|
1004
|
+
: [];
|
|
1005
|
+
return json(res, 200, {
|
|
1006
|
+
ok: true,
|
|
1007
|
+
peers: chainPeers,
|
|
1008
|
+
count: chainPeers.length,
|
|
1009
|
+
contract: peerRegistryClient?.contractAddress ?? null,
|
|
1010
|
+
timestamp: Date.now(),
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
catch (err) {
|
|
1014
|
+
return json(res, 500, { ok: false, error: err.message });
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
927
1017
|
// GET /network/stats — stats públicas para la landing page
|
|
928
1018
|
if (cleanUrl === "/network/stats" && req.method === "GET") {
|
|
929
1019
|
const p2pStats = p2pNode ? getP2PStats(p2pNode) : null;
|
|
930
1020
|
const httpPeers = peers.length;
|
|
931
1021
|
const libp2pPeers = p2pStats?.peers ?? 0;
|
|
1022
|
+
let registeredPeers = 0;
|
|
1023
|
+
try {
|
|
1024
|
+
if (peerRegistryClient) {
|
|
1025
|
+
const chainPeers = await peerRegistryClient.getAllPeers();
|
|
1026
|
+
registeredPeers = chainPeers.length;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
catch { /* non-fatal */ }
|
|
932
1030
|
return json(res, 200, {
|
|
933
1031
|
node_did: nodeKeypair.did.slice(0, 20) + "...",
|
|
934
1032
|
version: VERSION,
|
|
@@ -944,6 +1042,8 @@ export function startValidatorNode(port = PORT) {
|
|
|
944
1042
|
p2p_enabled: !!p2pNode,
|
|
945
1043
|
// total = max de ambas capas (HTTP gossip es el piso mínimo garantizado)
|
|
946
1044
|
total_peers: Math.max(httpPeers, libp2pPeers),
|
|
1045
|
+
// on-chain registered peers (PeerRegistry)
|
|
1046
|
+
registered_peers: registeredPeers,
|
|
947
1047
|
// estado general
|
|
948
1048
|
uptime_ms: Date.now() - (globalThis._startTime ?? Date.now()),
|
|
949
1049
|
timestamp: Date.now(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soulprint-network",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"dist",
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc && node scripts/compute-code-hash.mjs && cp -f src/blockchain/addresses.json dist/blockchain/addresses.json 2>/dev/null || true",
|
|
16
|
+
"start": "node dist/server.js",
|
|
17
|
+
"build:hash": "node scripts/compute-code-hash.mjs"
|
|
18
|
+
},
|
|
14
19
|
"publishConfig": {
|
|
15
20
|
"access": "public"
|
|
16
21
|
},
|
|
@@ -46,9 +51,9 @@
|
|
|
46
51
|
"nodemailer": "^8.0.1",
|
|
47
52
|
"otpauth": "^9.5.0",
|
|
48
53
|
"otplib": "^13.3.0",
|
|
49
|
-
"
|
|
50
|
-
"soulprint-
|
|
51
|
-
"
|
|
54
|
+
"soulprint-core": "workspace:*",
|
|
55
|
+
"soulprint-zkp": "workspace:*",
|
|
56
|
+
"uint8arrays": "5.1.0"
|
|
52
57
|
},
|
|
53
58
|
"devDependencies": {
|
|
54
59
|
"@types/node": "^20.0.0",
|
|
@@ -64,10 +69,5 @@
|
|
|
64
69
|
"import": "./dist/index.js",
|
|
65
70
|
"types": "./dist/index.d.ts"
|
|
66
71
|
}
|
|
67
|
-
},
|
|
68
|
-
"scripts": {
|
|
69
|
-
"build": "tsc && node scripts/compute-code-hash.mjs",
|
|
70
|
-
"start": "node dist/server.js",
|
|
71
|
-
"build:hash": "node scripts/compute-code-hash.mjs"
|
|
72
72
|
}
|
|
73
73
|
}
|