@mmmbuto/nexuscrew 0.8.57 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +164 -2
- package/README.md +1 -0
- package/frontend/dist/assets/index-0vuhL1YP.css +32 -0
- package/frontend/dist/assets/index-zjL6kZ7J.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/audio/adapters.js +50 -6
- package/lib/cli/commands.js +40 -2
- package/lib/cli/doctor.js +95 -12
- package/lib/cli/init.js +25 -3
- package/lib/cli/path.js +43 -10
- package/lib/cli/pidfile.js +23 -2
- package/lib/config.js +15 -0
- package/lib/fleet/builtin.js +161 -19
- package/lib/fleet/catalogs/opencode-go.json +328 -0
- package/lib/fleet/cell-exec.js +87 -9
- package/lib/fleet/cell-lease-server.js +719 -0
- package/lib/fleet/cell-lease.js +112 -0
- package/lib/fleet/definitions.js +101 -7
- package/lib/fleet/launch-broker.js +115 -3
- package/lib/fleet/lease-client.js +191 -0
- package/lib/fleet/lease-routes.js +92 -0
- package/lib/fleet/lease-verifier.js +230 -0
- package/lib/fleet/managed.js +444 -55
- package/lib/fleet/prompt-delivery.js +50 -2
- package/lib/fleet/provider.js +1 -1
- package/lib/fleet/runtime.js +53 -6
- package/lib/live-host/bridge.js +369 -0
- package/lib/live-host/routes.js +184 -0
- package/lib/live-host/store.js +96 -0
- package/lib/mcp/tools.js +51 -0
- package/lib/nodes/commands.js +9 -2
- package/lib/nodes/store.js +14 -0
- package/lib/nodes/tunnel.js +4 -1
- package/lib/proxy/federation.js +106 -9
- package/lib/proxy/node-proxy.js +33 -0
- package/lib/proxy/panel-auth.js +307 -0
- package/lib/proxy/panel-proxy.js +305 -0
- package/lib/server.js +127 -4
- package/package.json +1 -1
- package/skills/alibaba-token-media/SKILL.md +19 -0
- package/skills/crew/SKILL.md +15 -0
- package/skills/fill-forms/SKILL.md +23 -0
- package/skills/mail-assistant/SKILL.md +15 -0
- package/skills/memory/SKILL.md +15 -0
- package/skills/nexuscrew-agent/SKILL.md +18 -0
- package/skills/vl-msa/SKILL.md +15 -0
- package/frontend/dist/assets/index-CYi_lhCg.css +0 -32
- package/frontend/dist/assets/index-_c-1_3iR.js +0 -93
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// lib/fleet/lease-routes.js — route /api/lease, superficie child della fetta 2b
|
|
3
|
+
// (contratto rev1: B5 tre metodi distinti + D3 collegamento MCP↔leaseManager).
|
|
4
|
+
//
|
|
5
|
+
// Il bridge MCP di una cella (`nexuscrew mcp`) parla con l'HTTP API locale dietro
|
|
6
|
+
// Bearer (canale nativo del bridge): queste route sono quel collegamento.
|
|
7
|
+
// La CELLA e' derivata dalla sessione tmux dichiarata dal chiamante — lo stesso
|
|
8
|
+
// modello degli altri tool nc_*; il PROOF firmato dal verifier per-installazione
|
|
9
|
+
// e' l'authorizer di refresh/recovery (PREMESSA 2b: cambia il modello di
|
|
10
|
+
// autorizzazione, non il trasporto).
|
|
11
|
+
//
|
|
12
|
+
// Semantica degli status (tutti 200 salvo errori di protocollo):
|
|
13
|
+
// registered | live | pending | no-registration | expired | denied
|
|
14
|
+
// Il client MCP legge lo status e agisce; un 4xx/5xxx qui significa solo che la
|
|
15
|
+
// richiesta era malformata o il servizio non c'e' — non e' un esito di lease.
|
|
16
|
+
|
|
17
|
+
const express = require('express');
|
|
18
|
+
const { cellIdFromTmuxSession } = require('./definitions.js');
|
|
19
|
+
|
|
20
|
+
function leaseRoutes({ fleetP, readonly = () => false, log = () => {} }) {
|
|
21
|
+
const r = express.Router();
|
|
22
|
+
const smallJson = express.json({ limit: '8kb' });
|
|
23
|
+
|
|
24
|
+
const guard = (fn) => async (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const fleet = await fleetP;
|
|
27
|
+
if (!fleet || fleet.available !== true) return res.status(404).json({ error: 'fleet non disponibile' });
|
|
28
|
+
// D3: il collegamento vive sul provider — senza leaseManager (lease
|
|
29
|
+
// disattivato) e' 501, non 500: la capability manca, non e' un guasto.
|
|
30
|
+
if (!fleet.lease || typeof fleet.lease.childRegister !== 'function') {
|
|
31
|
+
return res.status(501).json({ error: 'lease non disponibile su questo nodo' });
|
|
32
|
+
}
|
|
33
|
+
return await fn(fleet.lease, req, res);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
res.status(500).json({ error: String((e && e.message) || e) });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// La sessione dichiarata determina la cella. Se non risolve in una cella
|
|
40
|
+
// valida la richiesta non ha soggetto: 400, senza cadere in un default.
|
|
41
|
+
const cellOf = (req) => cellIdFromTmuxSession(req.body && req.body.session);
|
|
42
|
+
const requireCell = (req, res) => {
|
|
43
|
+
const cell = cellOf(req);
|
|
44
|
+
if (!cell) {
|
|
45
|
+
res.status(400).json({ error: 'sessione non valida: impossibile derivare la cella' });
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return cell;
|
|
49
|
+
};
|
|
50
|
+
const requireProof = (req, res) => {
|
|
51
|
+
const proof = req.body && req.body.proof;
|
|
52
|
+
if (!proof || typeof proof !== 'object' || Array.isArray(proof)) {
|
|
53
|
+
res.status(400).json({ error: 'proof mancante o malformato' });
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return proof;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
r.post('/register', smallJson, guard((lease, req, res) => {
|
|
60
|
+
if (readonly()) return res.status(403).json({ error: 'READONLY: lease child bloccato' });
|
|
61
|
+
const cell = requireCell(req, res);
|
|
62
|
+
if (!cell) return undefined;
|
|
63
|
+
const out = lease.childRegister(cell);
|
|
64
|
+
log(`lease-route: register ${cell} -> ${out.status}`);
|
|
65
|
+
return res.json(out);
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
r.post('/refresh', smallJson, guard((lease, req, res) => {
|
|
69
|
+
if (readonly()) return res.status(403).json({ error: 'READONLY: lease child bloccato' });
|
|
70
|
+
const cell = requireCell(req, res);
|
|
71
|
+
if (!cell) return undefined;
|
|
72
|
+
const proof = requireProof(req, res);
|
|
73
|
+
if (!proof) return undefined;
|
|
74
|
+
const out = lease.childRefresh(cell, proof);
|
|
75
|
+
return res.json(out);
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
r.post('/recovery', smallJson, guard((lease, req, res) => {
|
|
79
|
+
if (readonly()) return res.status(403).json({ error: 'READONLY: lease child bloccato' });
|
|
80
|
+
const cell = requireCell(req, res);
|
|
81
|
+
if (!cell) return undefined;
|
|
82
|
+
const proof = requireProof(req, res);
|
|
83
|
+
if (!proof) return undefined;
|
|
84
|
+
const out = lease.childRecovery(cell, proof);
|
|
85
|
+
log(`lease-route: recovery ${cell} -> ${out.status}`);
|
|
86
|
+
return res.json(out);
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
return r;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { leaseRoutes };
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Verifier per-installazione e proof HMAC del lease Live (fetta 2b, contratto
|
|
4
|
+
// rev1: PREMESSA + B1/B4/B6/B7/B8 + C4/C5).
|
|
5
|
+
//
|
|
6
|
+
// Modello (PREMESSA): la 2a usava un segreto condiviso simmetrico — il
|
|
7
|
+
// supervisore presentava la capability cosi' com'e' e il server la confrontava.
|
|
8
|
+
// La 2b usa HMAC con verifier per-installazione: SOLO il server conosce il
|
|
9
|
+
// segreto, il supervisore/child presenta un proof firmato con claims ed expiry.
|
|
10
|
+
// Non e' la 2a con un giro in piu': e' un modello di autorizzazione diverso.
|
|
11
|
+
//
|
|
12
|
+
// - B7: la chiave verifier vive in un file DEDICATO separato 0o600, distinto
|
|
13
|
+
// dai token di liveness per-cella e dal segreto del bridge audio. «Un solo
|
|
14
|
+
// segreto» significa una sola chiave verifier, non un solo file segreto nel
|
|
15
|
+
// sistema.
|
|
16
|
+
// - C5: lo stato durevole contiene l'identificativo e l'impronta, MAI il
|
|
17
|
+
// segreto — forma gia' usata da vl-node (PendingEnrollment). Il keyId e'
|
|
18
|
+
// DERIVATO dall'impronta (sha256 della chiave): non esiste uno stato da
|
|
19
|
+
// tenere sincronizzato con la chiave, e il meta su disco e' diagnostica.
|
|
20
|
+
// - B4: la codifica canonica e' length-prefixed con proofKind come primo tag.
|
|
21
|
+
// La canonizzazione JSON e' fragile: due serializzatori onesti producono
|
|
22
|
+
// byte diversi. Il length-prefixing dichiara il confine di ogni campo, non
|
|
23
|
+
// lo deduce da un separatore.
|
|
24
|
+
// - B8: expiry = issuedAt + 60s, calcolabile all'emissione. «Ultimo-live+60»
|
|
25
|
+
// e' la proprieta' che si vuole, non la formula che si scrive: non e'
|
|
26
|
+
// calcolabile nel momento in cui il proof va firmato.
|
|
27
|
+
// - C4: fail-closed sulla verifica. La verifica prova TUTTE le chiavi vive
|
|
28
|
+
// (oggi una sola: la rotazione e' sospesa per scelta dichiarata, contratto
|
|
29
|
+
// C3; la forma e' gia' quella a due chiavi di C2).
|
|
30
|
+
// - C6: la verifica rende osservabile QUALE chiave ha firmato (keyId), cosi'
|
|
31
|
+
// il momento in cui una chiave subentra resta leggibile dopo il fatto.
|
|
32
|
+
//
|
|
33
|
+
// Disciplina del file segreto: stessa di lib/audio/bridge-auth.js e
|
|
34
|
+
// lib/auth/token.js — create esclusivo 'wx' + 0600, lettura no-follow (un
|
|
35
|
+
// symlink al posto della chiave e' un rifiuto, non un redirect).
|
|
36
|
+
|
|
37
|
+
const fs = require('node:fs');
|
|
38
|
+
const path = require('node:path');
|
|
39
|
+
const crypto = require('node:crypto');
|
|
40
|
+
|
|
41
|
+
// B8: vita di un proof emesso. Il refresh gira ogni 20s (REFRESH_MS): un proof
|
|
42
|
+
// da 60s lascia sempre al detentore >=2 presentazioni legittime di margine.
|
|
43
|
+
const PROOF_TTL_MS = 60_000;
|
|
44
|
+
|
|
45
|
+
// Tolleranza di clock sull'emissione: il proof e' emesso e presentato sulla
|
|
46
|
+
// stessa macchina dal server stesso, quindi serve solo un margine minimo.
|
|
47
|
+
const ISSUED_AT_SKEW_MS = 1_000;
|
|
48
|
+
|
|
49
|
+
const KEY_FILE = 'lease-verifier.key';
|
|
50
|
+
const META_FILE = 'lease-verifier.json';
|
|
51
|
+
const KEY_ID_LEN = 16;
|
|
52
|
+
const JTI_RE = /^[a-f0-9]{16,64}$/;
|
|
53
|
+
const SIG_RE = /^[a-f0-9]{64}$/;
|
|
54
|
+
|
|
55
|
+
// Campi firmati per proofKind, IN ORDINE, proofKind primo (B4). Per kind la
|
|
56
|
+
// lista e' fissa e tutti i campi sono obbligatori e non vuoti: campo mancante e
|
|
57
|
+
// campo vuoto non sono distinguibili nella canonica, quindi non esistono campi
|
|
58
|
+
// opzionali. B6: nel kind 'lease' l'identita' del lease e' 'leaseId' — non
|
|
59
|
+
// identityKey, che legherebbe il lease all'identita' della cella.
|
|
60
|
+
const KIND_FIELDS = Object.freeze({
|
|
61
|
+
// tupla del supervisore: autorizza il reconnect all'endpoint stabile.
|
|
62
|
+
lease: ['kind', 'cellId', 'launchEpoch', 'leaseId', 'generation', 'jti', 'issuedAt'],
|
|
63
|
+
// tupla del child: autorizza register/refresh/recovery (B5). incarnationId e'
|
|
64
|
+
// per-registration (B2), mai globale.
|
|
65
|
+
child: ['kind', 'cellId', 'incarnationId', 'jti', 'issuedAt'],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function fingerprintOf(secret) {
|
|
69
|
+
return crypto.createHash('sha256').update(String(secret), 'utf8').digest('hex');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- B4: codifica canonica ----------------------------------------------------
|
|
73
|
+
|
|
74
|
+
// Ogni campo: u32 big-endian della lunghezza in byte UTF-8, poi i byte. Il
|
|
75
|
+
// proofKind e' il primo tag: chiave di dominio della firma (un proof lease non
|
|
76
|
+
// e' riutilizzabile come proof child perche' il kind e' FIRMATO).
|
|
77
|
+
function canonicalProofFields(fields) {
|
|
78
|
+
const parts = [];
|
|
79
|
+
for (const f of fields) {
|
|
80
|
+
const b = Buffer.from(String(f), 'utf8');
|
|
81
|
+
const len = Buffer.alloc(4);
|
|
82
|
+
len.writeUInt32BE(b.length, 0);
|
|
83
|
+
parts.push(len, b);
|
|
84
|
+
}
|
|
85
|
+
return Buffer.concat(parts);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function claimsForKind(kind) {
|
|
89
|
+
const fields = KIND_FIELDS[kind];
|
|
90
|
+
if (!fields) throw new Error(`proof kind sconosciuto: ${kind}`);
|
|
91
|
+
return fields;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- B7/C5: chiave per-installazione ------------------------------------------
|
|
95
|
+
|
|
96
|
+
// Lettura no-follow (anti-symlink), stessa disciplina del bridge secret.
|
|
97
|
+
function readKeySafe(fsImpl, keyPath) {
|
|
98
|
+
const st = fsImpl.lstatSync(keyPath);
|
|
99
|
+
if (st.isSymbolicLink()) throw new Error(`rifiuto symlink per la chiave verifier: ${keyPath}`);
|
|
100
|
+
if (!st.isFile()) return null;
|
|
101
|
+
const s = fsImpl.readFileSync(keyPath, 'utf8').trim();
|
|
102
|
+
return s || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function loadOrCreateVerifier({ dir, fsImpl = fs, log = () => {}, now = Date.now } = {}) {
|
|
106
|
+
const keyPath = path.join(dir, KEY_FILE);
|
|
107
|
+
const metaPath = path.join(dir, META_FILE);
|
|
108
|
+
fsImpl.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
109
|
+
let secret = null;
|
|
110
|
+
try {
|
|
111
|
+
secret = readKeySafe(fsImpl, keyPath);
|
|
112
|
+
if (secret === null) fsImpl.unlinkSync(keyPath); // file vuoto: ricrea esclusivo
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (e.code !== 'ENOENT') throw e;
|
|
115
|
+
}
|
|
116
|
+
if (!secret) {
|
|
117
|
+
const fresh = crypto.randomBytes(32).toString('base64url');
|
|
118
|
+
try {
|
|
119
|
+
fsImpl.writeFileSync(keyPath, `${fresh}\n`, { flag: 'wx', mode: 0o600 });
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (e.code === 'EEXIST') {
|
|
122
|
+
const other = readKeySafe(fsImpl, keyPath); // race con un altro processo
|
|
123
|
+
if (other) secret = other;
|
|
124
|
+
}
|
|
125
|
+
if (!secret) throw e;
|
|
126
|
+
}
|
|
127
|
+
if (!secret) secret = fresh;
|
|
128
|
+
}
|
|
129
|
+
try { fsImpl.chmodSync(keyPath, 0o600); } catch (_) {}
|
|
130
|
+
// keyId derivato dall'impronta: la chiave porta con se' la propria identita'.
|
|
131
|
+
const keyId = fingerprintOf(secret).slice(0, KEY_ID_LEN);
|
|
132
|
+
// C5: il meta persiste id + impronta (MAI il segreto) per diagnostica e
|
|
133
|
+
// osservabilita' (C6). Best-effort: se e' assente o divergente si riscrive;
|
|
134
|
+
// un fallimento di scrittura non invalida la chiave.
|
|
135
|
+
try {
|
|
136
|
+
const meta = { version: 1, keyId, fingerprint: fingerprintOf(secret), createdAt: now() };
|
|
137
|
+
const existing = (() => { try { return JSON.parse(fsImpl.readFileSync(metaPath, 'utf8')); } catch (_) { return null; } })();
|
|
138
|
+
if (!existing || existing.keyId !== meta.keyId || existing.fingerprint !== meta.fingerprint) {
|
|
139
|
+
fsImpl.writeFileSync(metaPath, `${JSON.stringify(meta, null, 2)}\n`, { mode: 0o600 });
|
|
140
|
+
try { fsImpl.chmodSync(metaPath, 0o600); } catch (_) {}
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
log(`lease-verifier: meta non persistito: ${e && e.message}`);
|
|
144
|
+
}
|
|
145
|
+
return { keyId, secret, fingerprint: fingerprintOf(secret) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- firma / verifica ----------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
function signProof(verifier, claims, { now = Date.now, jti = null } = {}) {
|
|
151
|
+
if (!verifier || typeof verifier.secret !== 'string' || !verifier.secret) {
|
|
152
|
+
throw new Error('verifier mancante');
|
|
153
|
+
}
|
|
154
|
+
const kind = claims && claims.kind;
|
|
155
|
+
const fields = claimsForKind(kind);
|
|
156
|
+
const issuedAt = claims.issuedAt;
|
|
157
|
+
if (!Number.isSafeInteger(issuedAt)) throw new Error('issuedAt intero obbligatorio');
|
|
158
|
+
const values = {};
|
|
159
|
+
for (const f of fields) {
|
|
160
|
+
const v = f === 'issuedAt' ? String(issuedAt) : claims[f];
|
|
161
|
+
if (typeof v !== 'string' || !v.length) throw new Error(`campo firmato "${f}" mancante o vuoto per kind "${kind}"`);
|
|
162
|
+
values[f] = v;
|
|
163
|
+
}
|
|
164
|
+
const finalJti = jti || crypto.randomBytes(8).toString('hex');
|
|
165
|
+
if (!JTI_RE.test(finalJti)) throw new Error('jti malformato');
|
|
166
|
+
values.jti = finalJti;
|
|
167
|
+
const canonical = canonicalProofFields(fields.map((f) => values[f]));
|
|
168
|
+
const sig = crypto.createHmac('sha256', verifier.secret).update(canonical).digest('hex');
|
|
169
|
+
const proof = { ...values, expiresAt: issuedAt + PROOF_TTL_MS, proof: sig };
|
|
170
|
+
return proof;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isNonEmptyString(v) { return typeof v === 'string' && v.length > 0; }
|
|
174
|
+
|
|
175
|
+
// Fail-closed (C4): ogni difetto e' un motivo, non un'eccezione. `verifiers` e'
|
|
176
|
+
// la lista delle chiavi vive (oggi una; C2-ready per due). `expect` porta i
|
|
177
|
+
// claims che il chiamante gia' conosce: la firma prova il resto.
|
|
178
|
+
// `graceMs` (default 0) allarga la finestra di accettazione DOPO la scadenza:
|
|
179
|
+
// e' la finestra di recovery del child (B5) — un proof la cui firma e' valida e
|
|
180
|
+
// la cui scadenza e' recente NON e' una credenziale rubata riportata in vita, e'
|
|
181
|
+
// un detentore che ha saltato i refresh. Ogni altro check resta invariato.
|
|
182
|
+
function verifyProof(verifiers, candidate, { now = Date.now, expect = {}, graceMs = 0 } = {}) {
|
|
183
|
+
if (!Array.isArray(verifiers) || verifiers.length === 0) return { ok: false, reason: 'no-keys' };
|
|
184
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return { ok: false, reason: 'malformed' };
|
|
185
|
+
const kind = candidate.kind;
|
|
186
|
+
let fields;
|
|
187
|
+
try { fields = claimsForKind(kind); } catch (_) { return { ok: false, reason: 'malformed' }; }
|
|
188
|
+
for (const f of fields) {
|
|
189
|
+
if (!isNonEmptyString(candidate[f])) return { ok: false, reason: 'malformed' };
|
|
190
|
+
}
|
|
191
|
+
if (!JTI_RE.test(candidate.jti)) return { ok: false, reason: 'malformed' };
|
|
192
|
+
if (typeof candidate.proof !== 'string' || !SIG_RE.test(candidate.proof)) return { ok: false, reason: 'malformed' };
|
|
193
|
+
const issuedAt = Number(candidate.issuedAt);
|
|
194
|
+
if (!Number.isSafeInteger(issuedAt)) return { ok: false, reason: 'malformed' };
|
|
195
|
+
const expiresAt = Number(candidate.expiresAt);
|
|
196
|
+
// B8: expiresAt non e' un campo qualunque: deve essere ESATTAMENTE
|
|
197
|
+
// issuedAt + PROOF_TTL_MS. Manometterlo non estende la vita del proof.
|
|
198
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt !== issuedAt + PROOF_TTL_MS) {
|
|
199
|
+
return { ok: false, reason: 'malformed' };
|
|
200
|
+
}
|
|
201
|
+
const t = now();
|
|
202
|
+
const graceMsNum = Number.isSafeInteger(graceMs) && graceMs >= 0 ? graceMs : 0;
|
|
203
|
+
if (t >= expiresAt + graceMsNum) return { ok: false, reason: 'expired' };
|
|
204
|
+
// Emissione nel futuro oltre la tolleranza: non e' un proof che questo
|
|
205
|
+
// processo ha potuto emettere onestamente.
|
|
206
|
+
if (issuedAt > t + ISSUED_AT_SKEW_MS) return { ok: false, reason: 'expired' };
|
|
207
|
+
// Claims attesi dal chiamante (scope della presentazione).
|
|
208
|
+
if (expect.kind !== undefined && kind !== expect.kind) return { ok: false, reason: 'kind' };
|
|
209
|
+
if (expect.cellId !== undefined && candidate.cellId !== expect.cellId) return { ok: false, reason: 'cellId' };
|
|
210
|
+
if (expect.launchEpoch !== undefined && candidate.launchEpoch !== expect.launchEpoch) return { ok: false, reason: 'launchEpoch' };
|
|
211
|
+
if (expect.leaseId !== undefined && candidate.leaseId !== expect.leaseId) return { ok: false, reason: 'leaseId' };
|
|
212
|
+
if (expect.incarnationId !== undefined && candidate.incarnationId !== expect.incarnationId) return { ok: false, reason: 'incarnationId' };
|
|
213
|
+
// Firma contro OGNI chiave viva: la prima che passa vince (C2/C6).
|
|
214
|
+
const canonical = canonicalProofFields(fields.map((f) => candidate[f]));
|
|
215
|
+
for (const v of verifiers) {
|
|
216
|
+
if (!v || typeof v.secret !== 'string') continue;
|
|
217
|
+
const expected = crypto.createHmac('sha256', v.secret).update(canonical).digest('hex');
|
|
218
|
+
const a = Buffer.from(expected, 'hex');
|
|
219
|
+
const b = Buffer.from(candidate.proof, 'hex');
|
|
220
|
+
if (a.length === b.length && crypto.timingSafeEqual(a, b)) {
|
|
221
|
+
return { ok: true, claims: { ...candidate }, keyId: v.keyId };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { ok: false, reason: 'bad-proof' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
PROOF_TTL_MS, KIND_FIELDS,
|
|
229
|
+
canonicalProofFields, loadOrCreateVerifier, fingerprintOf, signProof, verifyProof,
|
|
230
|
+
};
|