@provenonce/sdk 0.11.0 → 0.14.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/dist/index.d.mts +98 -58
- package/dist/index.d.ts +98 -58
- package/dist/index.js +246 -220
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +245 -219
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/beat-sdk.ts
|
|
2
|
-
import { createHash,
|
|
2
|
+
import { createHash, verify, createPublicKey } from "crypto";
|
|
3
3
|
|
|
4
4
|
// src/errors.ts
|
|
5
5
|
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
@@ -109,22 +109,53 @@ function computeBeat(prevHash, beatIndex, difficulty, nonce, anchorHash) {
|
|
|
109
109
|
}
|
|
110
110
|
return { index: beatIndex, hash: current, prev: prevHash, timestamp, nonce, anchor_hash: anchorHash };
|
|
111
111
|
}
|
|
112
|
-
var
|
|
113
|
-
function
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
113
|
+
function base58DecodeToBuffer(str) {
|
|
114
|
+
const map = {};
|
|
115
|
+
for (let i = 0; i < BASE58_ALPHABET.length; i++) map[BASE58_ALPHABET[i]] = i;
|
|
116
|
+
let bytes = [0];
|
|
117
|
+
for (let i = 0; i < str.length; i++) {
|
|
118
|
+
const val = map[str[i]];
|
|
119
|
+
if (val === void 0) throw new Error("invalid base58 character");
|
|
120
|
+
let carry = val;
|
|
121
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
122
|
+
const x = bytes[j] * 58 + carry;
|
|
123
|
+
bytes[j] = x & 255;
|
|
124
|
+
carry = x >> 8;
|
|
125
|
+
}
|
|
126
|
+
while (carry > 0) {
|
|
127
|
+
bytes.push(carry & 255);
|
|
128
|
+
carry >>= 8;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (let i = 0; i < str.length && str[i] === "1"; i++) bytes.push(0);
|
|
132
|
+
return Buffer.from(bytes.reverse());
|
|
133
|
+
}
|
|
134
|
+
function u64be(n) {
|
|
135
|
+
const buf = Buffer.alloc(8);
|
|
136
|
+
buf.writeUInt32BE(Math.floor(n / 4294967296), 0);
|
|
137
|
+
buf.writeUInt32BE(n >>> 0, 4);
|
|
138
|
+
return buf;
|
|
121
139
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
var ANCHOR_DOMAIN_PREFIX = "PROVENONCE_BEATS_V1";
|
|
141
|
+
function verifyAnchorHash(anchor) {
|
|
142
|
+
if (!anchor || !anchor.hash || !anchor.prev_hash) return false;
|
|
143
|
+
if (anchor.solana_entropy) {
|
|
144
|
+
const prefix = Buffer.from(ANCHOR_DOMAIN_PREFIX, "utf8");
|
|
145
|
+
const prev = Buffer.from(anchor.prev_hash, "hex");
|
|
146
|
+
const idx = u64be(anchor.beat_index);
|
|
147
|
+
const entropy = base58DecodeToBuffer(anchor.solana_entropy);
|
|
148
|
+
const preimage = Buffer.concat([prefix, prev, idx, entropy]);
|
|
149
|
+
const computed = createHash("sha256").update(preimage).digest("hex");
|
|
150
|
+
return computed === anchor.hash;
|
|
151
|
+
}
|
|
152
|
+
const nonce = `anchor:${anchor.utc}:${anchor.epoch}`;
|
|
153
|
+
const seed = `${anchor.prev_hash}:${anchor.beat_index}:${nonce}`;
|
|
154
|
+
let current = createHash("sha256").update(seed).digest("hex");
|
|
155
|
+
for (let i = 0; i < anchor.difficulty; i++) {
|
|
156
|
+
current = createHash("sha256").update(current).digest("hex");
|
|
157
|
+
}
|
|
158
|
+
return current === anchor.hash;
|
|
128
159
|
}
|
|
129
160
|
async function register(name, options) {
|
|
130
161
|
if (!name || typeof name !== "string" || name.trim().length === 0) {
|
|
@@ -211,8 +242,6 @@ async function register(name, options) {
|
|
|
211
242
|
}
|
|
212
243
|
if (!registerRes.ok) throw mapApiError(registerRes.status, data2, "/api/v1/register");
|
|
213
244
|
data2.wallet = {
|
|
214
|
-
public_key: "",
|
|
215
|
-
secret_key: "",
|
|
216
245
|
address: data2.wallet?.address || options.walletAddress,
|
|
217
246
|
chain: "ethereum"
|
|
218
247
|
};
|
|
@@ -260,77 +289,6 @@ async function register(name, options) {
|
|
|
260
289
|
if (!registerRes.ok) throw mapApiError(registerRes.status, data2, "/api/v1/register");
|
|
261
290
|
const addr = data2.wallet?.address || data2.wallet?.solana_address || options.operatorWalletAddress;
|
|
262
291
|
data2.wallet = {
|
|
263
|
-
public_key: "",
|
|
264
|
-
secret_key: "",
|
|
265
|
-
solana_address: addr,
|
|
266
|
-
address: addr,
|
|
267
|
-
chain: "solana"
|
|
268
|
-
};
|
|
269
|
-
return data2;
|
|
270
|
-
}
|
|
271
|
-
if (options?.walletModel === "self-custody" || options?.walletSecretKey) {
|
|
272
|
-
let walletKeys;
|
|
273
|
-
if (options?.walletSecretKey) {
|
|
274
|
-
const privRaw = Buffer.from(options.walletSecretKey, "hex");
|
|
275
|
-
const privKeyDer = Buffer.concat([ED25519_PKCS8_PREFIX, privRaw]);
|
|
276
|
-
const keyObject = createPrivateKey({ key: privKeyDer, format: "der", type: "pkcs8" });
|
|
277
|
-
const pubRaw = keyObject.export({ type: "spki", format: "der" }).subarray(12);
|
|
278
|
-
walletKeys = {
|
|
279
|
-
publicKey: Buffer.from(pubRaw).toString("hex"),
|
|
280
|
-
secretKey: options.walletSecretKey
|
|
281
|
-
};
|
|
282
|
-
} else {
|
|
283
|
-
walletKeys = generateWalletKeypair();
|
|
284
|
-
}
|
|
285
|
-
const challengeRes = await fetch(`${url}/api/v1/register`, {
|
|
286
|
-
method: "POST",
|
|
287
|
-
headers,
|
|
288
|
-
body: JSON.stringify({ name, action: "challenge" })
|
|
289
|
-
});
|
|
290
|
-
let challengeData;
|
|
291
|
-
try {
|
|
292
|
-
challengeData = await challengeRes.json();
|
|
293
|
-
} catch {
|
|
294
|
-
const err = new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
|
|
295
|
-
err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
|
|
296
|
-
throw err;
|
|
297
|
-
}
|
|
298
|
-
if (!challengeRes.ok || !challengeData.nonce) {
|
|
299
|
-
const err = mapApiError(challengeRes.status, challengeData, "/api/v1/register");
|
|
300
|
-
err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
|
|
301
|
-
throw err;
|
|
302
|
-
}
|
|
303
|
-
const nonce = challengeData.nonce;
|
|
304
|
-
const message = `provenonce-register:${nonce}:${walletKeys.publicKey}:${name}`;
|
|
305
|
-
const walletSignature = signMessage(walletKeys.secretKey, message);
|
|
306
|
-
const registerRes = await fetch(`${url}/api/v1/register`, {
|
|
307
|
-
method: "POST",
|
|
308
|
-
headers,
|
|
309
|
-
body: JSON.stringify({
|
|
310
|
-
name,
|
|
311
|
-
wallet_public_key: walletKeys.publicKey,
|
|
312
|
-
wallet_signature: walletSignature,
|
|
313
|
-
wallet_nonce: nonce,
|
|
314
|
-
...options?.metadata && { metadata: options.metadata }
|
|
315
|
-
})
|
|
316
|
-
});
|
|
317
|
-
let data2;
|
|
318
|
-
try {
|
|
319
|
-
data2 = await registerRes.json();
|
|
320
|
-
} catch {
|
|
321
|
-
const err = new NetworkError(`Registration failed: ${registerRes.status} (non-JSON response)`);
|
|
322
|
-
err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
|
|
323
|
-
throw err;
|
|
324
|
-
}
|
|
325
|
-
if (!registerRes.ok) {
|
|
326
|
-
const err = mapApiError(registerRes.status, data2, "/api/v1/register");
|
|
327
|
-
err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
|
|
328
|
-
throw err;
|
|
329
|
-
}
|
|
330
|
-
const addr = data2.wallet?.address || data2.wallet?.solana_address || "";
|
|
331
|
-
data2.wallet = {
|
|
332
|
-
public_key: walletKeys.publicKey,
|
|
333
|
-
secret_key: walletKeys.secretKey,
|
|
334
292
|
solana_address: addr,
|
|
335
293
|
address: addr,
|
|
336
294
|
chain: "solana"
|
|
@@ -398,6 +356,8 @@ var BeatAgent = class {
|
|
|
398
356
|
onStatusChange: () => {
|
|
399
357
|
},
|
|
400
358
|
verbose: false,
|
|
359
|
+
verifyAnchors: true,
|
|
360
|
+
beatsUrl: "https://beats.provenonce.dev",
|
|
401
361
|
...config
|
|
402
362
|
};
|
|
403
363
|
}
|
|
@@ -439,27 +399,7 @@ var BeatAgent = class {
|
|
|
439
399
|
return { ok: false, error: err.message };
|
|
440
400
|
}
|
|
441
401
|
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* @deprecated Phase 2: VDF computation retired (D-68). Payment is the liveness mechanism.
|
|
445
|
-
* Use heartbeat() instead. This method will be removed in the next major version.
|
|
446
|
-
*
|
|
447
|
-
* Compute N beats locally (VDF hash chain).
|
|
448
|
-
*/
|
|
449
|
-
pulse(count) {
|
|
450
|
-
console.warn("[Provenonce SDK] pulse() is deprecated. Use heartbeat() instead (Phase 2).");
|
|
451
|
-
if (this.status === "frozen") {
|
|
452
|
-
throw new FrozenError("Cannot pulse: agent is frozen. Use resync() to re-establish provenance.");
|
|
453
|
-
}
|
|
454
|
-
if (this.status !== "active") {
|
|
455
|
-
throw new StateError(`Cannot pulse: agent is ${this.status}.`, this.status);
|
|
456
|
-
}
|
|
457
|
-
if (count !== void 0 && (!Number.isInteger(count) || count < 1 || count > 1e4)) {
|
|
458
|
-
throw new ValidationError("pulse count must be an integer between 1 and 10000");
|
|
459
|
-
}
|
|
460
|
-
return this.computeBeats(count);
|
|
461
|
-
}
|
|
462
|
-
/** Internal beat computation — no status check. Used by both pulse() and resync(). */
|
|
402
|
+
/** Internal beat computation — no status check. Used by resync(). */
|
|
463
403
|
computeBeats(count, onProgress) {
|
|
464
404
|
const n = count || this.config.beatsPerPulse;
|
|
465
405
|
if (!this.latestBeat) {
|
|
@@ -489,63 +429,6 @@ var BeatAgent = class {
|
|
|
489
429
|
this.log(`Pulse: ${n} beats in ${elapsed}ms (${(elapsed / n).toFixed(1)}ms/beat, D=${this.difficulty})`);
|
|
490
430
|
return newBeats;
|
|
491
431
|
}
|
|
492
|
-
// ── CHECK-IN ──
|
|
493
|
-
/**
|
|
494
|
-
* @deprecated Phase 2: VDF check-in retired (D-68). Use heartbeat() instead.
|
|
495
|
-
* This method will be removed in the next major version.
|
|
496
|
-
*/
|
|
497
|
-
async checkin() {
|
|
498
|
-
console.warn("[Provenonce SDK] checkin() is deprecated. Use heartbeat() instead (Phase 2).");
|
|
499
|
-
if (!this.latestBeat || this.latestBeat.index <= this.lastCheckinBeat) {
|
|
500
|
-
this.log("No new beats since last check-in. Call pulse() first.");
|
|
501
|
-
return { ok: true, total_beats: this.totalBeats };
|
|
502
|
-
}
|
|
503
|
-
try {
|
|
504
|
-
const fromBeat = this.lastCheckinBeat;
|
|
505
|
-
const toBeat = this.latestBeat.index;
|
|
506
|
-
const spotChecks = [];
|
|
507
|
-
const toBeatEntry = this.chain.find((b) => b.index === toBeat);
|
|
508
|
-
if (toBeatEntry) {
|
|
509
|
-
spotChecks.push({ index: toBeatEntry.index, hash: toBeatEntry.hash, prev: toBeatEntry.prev, nonce: toBeatEntry.nonce });
|
|
510
|
-
}
|
|
511
|
-
const available = this.chain.filter((b) => b.index > this.lastCheckinBeat && b.index !== toBeat);
|
|
512
|
-
const sampleCount = Math.min(4, available.length);
|
|
513
|
-
for (let i = 0; i < sampleCount; i++) {
|
|
514
|
-
const idx = Math.floor(Math.random() * available.length);
|
|
515
|
-
const beat = available[idx];
|
|
516
|
-
spotChecks.push({ index: beat.index, hash: beat.hash, prev: beat.prev, nonce: beat.nonce });
|
|
517
|
-
available.splice(idx, 1);
|
|
518
|
-
}
|
|
519
|
-
const fromHash = this.chain.find((b) => b.index === fromBeat)?.hash || this.genesisHash;
|
|
520
|
-
const toHash = this.latestBeat.hash;
|
|
521
|
-
const res = await this.api("POST", "/api/v1/agent/checkin", {
|
|
522
|
-
proof: {
|
|
523
|
-
from_beat: fromBeat,
|
|
524
|
-
to_beat: toBeat,
|
|
525
|
-
from_hash: fromHash,
|
|
526
|
-
to_hash: toHash,
|
|
527
|
-
beats_computed: toBeat - fromBeat,
|
|
528
|
-
global_anchor: this.globalBeat,
|
|
529
|
-
anchor_hash: this.globalAnchorHash || void 0,
|
|
530
|
-
spot_checks: spotChecks
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
if (res.ok) {
|
|
534
|
-
this.lastCheckinBeat = toBeat;
|
|
535
|
-
this.totalBeats = res.total_beats;
|
|
536
|
-
this.config.onCheckin(res);
|
|
537
|
-
this.log(`Check-in accepted: ${res.beats_accepted} beats, total=${res.total_beats}, global=${res.global_beat}`);
|
|
538
|
-
if (res.status === "warning_overdue") {
|
|
539
|
-
this.config.onStatusChange("warning", { beats_behind: res.beats_behind });
|
|
540
|
-
this.log(`\u26A0 WARNING: ${res.beats_behind} anchors behind. Check in more frequently.`);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
return { ok: res.ok, total_beats: res.total_beats };
|
|
544
|
-
} catch (err) {
|
|
545
|
-
this.config.onError(err, "checkin");
|
|
546
|
-
return { ok: false, error: err.message };
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
432
|
// ── AUTONOMOUS HEARTBEAT ──
|
|
550
433
|
/**
|
|
551
434
|
* Start the autonomous heartbeat loop.
|
|
@@ -595,60 +478,66 @@ var BeatAgent = class {
|
|
|
595
478
|
}
|
|
596
479
|
// ── RE-SYNC ──
|
|
597
480
|
/**
|
|
598
|
-
*
|
|
599
|
-
*
|
|
481
|
+
* Re-Sync Challenge (D-67 reversal): reactivate a frozen agent by proving CPU work.
|
|
482
|
+
*
|
|
483
|
+
* When BEATS_REQUIRED=true on the server: requires a signed Beats work-proof
|
|
484
|
+
* receipt. This method computes the proof automatically using computeWorkProof().
|
|
485
|
+
*
|
|
486
|
+
* When BEATS_REQUIRED=false (devnet): no receipt needed — agent is reactivated freely.
|
|
487
|
+
*
|
|
488
|
+
* Gap formula: min(gap_anchors * 100, 10_000) beats required (matches Beats constants).
|
|
600
489
|
*/
|
|
601
490
|
async resync() {
|
|
602
|
-
console.warn("[Provenonce SDK] resync() is deprecated (D-67). Use heartbeat() to resume (Phase 2).");
|
|
603
491
|
try {
|
|
604
|
-
this.log("
|
|
605
|
-
const challenge = await this.api("POST", "/api/v1/agent/resync", {
|
|
606
|
-
action: "challenge"
|
|
607
|
-
});
|
|
608
|
-
if (!challenge.challenge) {
|
|
609
|
-
return { ok: false, error: "Failed to get challenge" };
|
|
610
|
-
}
|
|
611
|
-
const required = challenge.challenge.required_beats;
|
|
612
|
-
this.difficulty = challenge.challenge.difficulty;
|
|
613
|
-
this.log(`Re-sync challenge: compute ${required} beats at D=${this.difficulty}`);
|
|
492
|
+
this.log("Attempting resync...");
|
|
614
493
|
await this.syncGlobal();
|
|
615
|
-
const
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
spot_checks: (() => {
|
|
635
|
-
const toBeatEntry = this.chain.find((b) => b.index === this.latestBeat.index);
|
|
636
|
-
const available = this.chain.filter((b) => b.index !== this.latestBeat.index && b.index > startBeat);
|
|
637
|
-
const step = Math.max(1, Math.ceil(available.length / 5));
|
|
638
|
-
const others = available.filter((_, i) => i % step === 0).slice(0, 4);
|
|
639
|
-
const checks = toBeatEntry ? [toBeatEntry, ...others] : others;
|
|
640
|
-
return checks.map((b) => ({ index: b.index, hash: b.hash, prev: b.prev, nonce: b.nonce }));
|
|
641
|
-
})()
|
|
642
|
-
}
|
|
643
|
-
});
|
|
644
|
-
if (proof.ok) {
|
|
494
|
+
const controller = new AbortController();
|
|
495
|
+
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
496
|
+
let probeRes;
|
|
497
|
+
let probeData;
|
|
498
|
+
try {
|
|
499
|
+
probeRes = await fetch(`${this.config.registryUrl}/api/v1/agent/resync`, {
|
|
500
|
+
method: "POST",
|
|
501
|
+
headers: {
|
|
502
|
+
"Content-Type": "application/json",
|
|
503
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
504
|
+
},
|
|
505
|
+
body: JSON.stringify({}),
|
|
506
|
+
signal: controller.signal
|
|
507
|
+
});
|
|
508
|
+
probeData = await probeRes.json();
|
|
509
|
+
} finally {
|
|
510
|
+
clearTimeout(timeout);
|
|
511
|
+
}
|
|
512
|
+
if (probeData.ok && probeData.status === "active") {
|
|
645
513
|
this.status = "active";
|
|
646
|
-
this.totalBeats = proof.total_beats;
|
|
647
|
-
this.lastCheckinBeat = this.latestBeat.index;
|
|
648
514
|
this.config.onStatusChange("active", { resynced: true });
|
|
649
|
-
this.log("\u2713 Re-synced. Agent is alive again in Beat time.");
|
|
515
|
+
this.log("\u2713 Re-synced (free). Agent is alive again in Beat time.");
|
|
516
|
+
return { ok: true, beats_required: 0 };
|
|
517
|
+
}
|
|
518
|
+
if (probeRes.status === 402 || probeData.code === "RECEIPT_REQUIRED") {
|
|
519
|
+
const requiredBeats = probeData.required_beats ?? 1e3;
|
|
520
|
+
this.log(`Re-sync challenge: compute ${requiredBeats} beats at D=${this.difficulty}`);
|
|
521
|
+
const proofResult = await this.computeWorkProof({
|
|
522
|
+
beatsNeeded: requiredBeats,
|
|
523
|
+
anchorHash: this.globalAnchorHash,
|
|
524
|
+
anchorIndex: this.globalBeat
|
|
525
|
+
});
|
|
526
|
+
if (!proofResult.ok || !proofResult.receipt) {
|
|
527
|
+
return { ok: false, error: proofResult.error || "Failed to compute work proof for resync", beats_required: requiredBeats };
|
|
528
|
+
}
|
|
529
|
+
this.log(`Work proof computed: ${proofResult.beats_computed} beats in ${proofResult.elapsed_ms}ms`);
|
|
530
|
+
const result = await this.api("POST", "/api/v1/agent/resync", {
|
|
531
|
+
beats_receipt: proofResult.receipt
|
|
532
|
+
});
|
|
533
|
+
if (result.ok && result.status === "active") {
|
|
534
|
+
this.status = "active";
|
|
535
|
+
this.config.onStatusChange("active", { resynced: true });
|
|
536
|
+
this.log("\u2713 Re-synced with work proof. Agent is alive again in Beat time.");
|
|
537
|
+
}
|
|
538
|
+
return { ok: !!result.ok, beats_required: requiredBeats };
|
|
650
539
|
}
|
|
651
|
-
return { ok:
|
|
540
|
+
return { ok: false, error: probeData.error || `Resync failed (status ${probeRes.status})` };
|
|
652
541
|
} catch (err) {
|
|
653
542
|
this.config.onError(err, "resync");
|
|
654
543
|
return { ok: false, error: err.message };
|
|
@@ -657,9 +546,13 @@ var BeatAgent = class {
|
|
|
657
546
|
// ── SPAWN ──
|
|
658
547
|
/**
|
|
659
548
|
* Request to spawn a child agent.
|
|
660
|
-
* Requires sufficient accumulated beats (Temporal Gestation).
|
|
549
|
+
* Requires sufficient accumulated beats (Temporal Gestation), OR a valid Beats work-proof receipt.
|
|
550
|
+
*
|
|
551
|
+
* @param childName Optional name for the child agent
|
|
552
|
+
* @param childHash Pre-registered child hash (Step 2 finalization)
|
|
553
|
+
* @param beatsReceipt Signed work-proof receipt from computeWorkProof() (receipt-based spawn)
|
|
661
554
|
*/
|
|
662
|
-
async requestSpawn(childName, childHash) {
|
|
555
|
+
async requestSpawn(childName, childHash, beatsReceipt) {
|
|
663
556
|
try {
|
|
664
557
|
if (childName !== void 0) {
|
|
665
558
|
if (typeof childName !== "string" || childName.trim().length === 0) {
|
|
@@ -671,12 +564,15 @@ var BeatAgent = class {
|
|
|
671
564
|
}
|
|
672
565
|
const res = await this.api("POST", "/api/v1/agent/spawn", {
|
|
673
566
|
child_name: childName,
|
|
674
|
-
child_hash: childHash
|
|
567
|
+
child_hash: childHash,
|
|
568
|
+
...beatsReceipt && { beats_receipt: beatsReceipt }
|
|
675
569
|
});
|
|
676
570
|
if (res.eligible === false) {
|
|
677
571
|
this.log(`Gestation incomplete: ${res.progress_pct}% (need ${res.deficit} more beats)`);
|
|
678
572
|
} else if (res.ok) {
|
|
679
573
|
this.log(`Child spawned: ${res.child_hash?.slice(0, 16)}...`);
|
|
574
|
+
} else if (res.spawn_authorization) {
|
|
575
|
+
this.log(`Spawn authorized${res.receipt_based ? " (receipt-based)" : ""}`);
|
|
680
576
|
}
|
|
681
577
|
return res;
|
|
682
578
|
} catch (err) {
|
|
@@ -684,6 +580,132 @@ var BeatAgent = class {
|
|
|
684
580
|
throw err;
|
|
685
581
|
}
|
|
686
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Compute a Beats work-proof for spawn or resync authorization.
|
|
585
|
+
*
|
|
586
|
+
* Computes `beatsNeeded` sequential SHA-256 beats at `difficulty`, weaving in
|
|
587
|
+
* the given anchor hash, then submits to the Beats service and returns a signed receipt.
|
|
588
|
+
*
|
|
589
|
+
* @param opts.beatsNeeded Minimum beats required (from spawn/resync response.required_beats)
|
|
590
|
+
* @param opts.anchorHash Current global anchor hash (from syncGlobal or getAnchor)
|
|
591
|
+
* @param opts.anchorIndex Current global anchor index
|
|
592
|
+
* @param opts.difficulty Beat difficulty (default: agent's current difficulty)
|
|
593
|
+
*/
|
|
594
|
+
async computeWorkProof(opts) {
|
|
595
|
+
const { beatsNeeded, anchorHash, anchorIndex } = opts;
|
|
596
|
+
const difficulty = opts.difficulty ?? this.difficulty;
|
|
597
|
+
if (!Number.isInteger(beatsNeeded) || beatsNeeded < 0) {
|
|
598
|
+
return { ok: false, error: "beatsNeeded must be a non-negative integer" };
|
|
599
|
+
}
|
|
600
|
+
const t0 = Date.now();
|
|
601
|
+
const genesisHash = createHash("sha256").update(`provenonce:work-proof-genesis:${this.config.apiKey.slice(0, 16)}:${Date.now()}`).digest("hex");
|
|
602
|
+
const beats = Math.max(beatsNeeded, 1);
|
|
603
|
+
let prevHash = genesisHash;
|
|
604
|
+
const spotCheckCount = Math.min(5, Math.max(1, Math.floor(beats / 100)));
|
|
605
|
+
const spotInterval = Math.max(1, Math.floor(beats / (spotCheckCount + 1)));
|
|
606
|
+
const spotChecks = [];
|
|
607
|
+
for (let i = 1; i <= beats; i++) {
|
|
608
|
+
const beat = computeBeat(prevHash, i, difficulty, void 0, anchorHash);
|
|
609
|
+
prevHash = beat.hash;
|
|
610
|
+
if (i % spotInterval === 0 && spotChecks.length < spotCheckCount) {
|
|
611
|
+
spotChecks.push({ index: beat.index, hash: beat.hash, prev: beat.prev });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const toHash = prevHash;
|
|
615
|
+
const elapsed_ms = Date.now() - t0;
|
|
616
|
+
this.log(`Work proof computed locally: ${beats} beats in ${elapsed_ms}ms`);
|
|
617
|
+
const beatsUrl = this.config.beatsUrl || "https://beats.provenonce.dev";
|
|
618
|
+
const controller = new AbortController();
|
|
619
|
+
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
620
|
+
try {
|
|
621
|
+
const res = await fetch(`${beatsUrl}/api/v1/beat/work-proof`, {
|
|
622
|
+
method: "POST",
|
|
623
|
+
headers: { "Content-Type": "application/json" },
|
|
624
|
+
body: JSON.stringify({
|
|
625
|
+
work_proof: {
|
|
626
|
+
from_hash: genesisHash,
|
|
627
|
+
to_hash: toHash,
|
|
628
|
+
beats_computed: beats,
|
|
629
|
+
difficulty,
|
|
630
|
+
anchor_index: anchorIndex,
|
|
631
|
+
anchor_hash: anchorHash,
|
|
632
|
+
spot_checks: spotChecks
|
|
633
|
+
}
|
|
634
|
+
}),
|
|
635
|
+
signal: controller.signal
|
|
636
|
+
});
|
|
637
|
+
let data;
|
|
638
|
+
try {
|
|
639
|
+
data = await res.json();
|
|
640
|
+
} catch {
|
|
641
|
+
return { ok: false, error: `Beats service returned non-JSON (status ${res.status})` };
|
|
642
|
+
}
|
|
643
|
+
if (!data.valid || !data.receipt) {
|
|
644
|
+
return { ok: false, error: data.reason || data.error || "Work proof rejected by Beats service" };
|
|
645
|
+
}
|
|
646
|
+
this.log(`Work proof receipt issued by Beats: ${data.receipt.beats_verified} beats verified`);
|
|
647
|
+
return { ok: true, receipt: data.receipt, beats_computed: beats, elapsed_ms };
|
|
648
|
+
} catch (err) {
|
|
649
|
+
if (err.name === "AbortError") {
|
|
650
|
+
return { ok: false, error: "Work proof submission timed out" };
|
|
651
|
+
}
|
|
652
|
+
return { ok: false, error: err.message };
|
|
653
|
+
} finally {
|
|
654
|
+
clearTimeout(timeout);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Compute a Beats work-proof and use it to request spawn authorization.
|
|
659
|
+
*
|
|
660
|
+
* Probes the spawn endpoint to determine required beats, computes the proof,
|
|
661
|
+
* and returns the spawn_authorization token. The caller still needs to:
|
|
662
|
+
* 1. Register the child via POST /api/v1/register with spawn_authorization
|
|
663
|
+
* 2. Finalize via POST /api/v1/agent/spawn with child_hash
|
|
664
|
+
*
|
|
665
|
+
* @param opts.childName Optional name for the child agent
|
|
666
|
+
* @param opts.beatsNeeded Override the required beats (default: auto-probed)
|
|
667
|
+
*/
|
|
668
|
+
async requestSpawnWithBeatsProof(opts) {
|
|
669
|
+
try {
|
|
670
|
+
await this.syncGlobal();
|
|
671
|
+
let requiredBeats = opts?.beatsNeeded;
|
|
672
|
+
if (requiredBeats === void 0) {
|
|
673
|
+
const controller = new AbortController();
|
|
674
|
+
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
675
|
+
try {
|
|
676
|
+
const probeRes = await fetch(`${this.config.registryUrl}/api/v1/agent/spawn`, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers: {
|
|
679
|
+
"Content-Type": "application/json",
|
|
680
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
681
|
+
},
|
|
682
|
+
body: JSON.stringify({ child_name: opts?.childName }),
|
|
683
|
+
signal: controller.signal
|
|
684
|
+
});
|
|
685
|
+
const probeData = await probeRes.json();
|
|
686
|
+
if (probeData.spawn_authorization && probeData.eligible) {
|
|
687
|
+
return probeData;
|
|
688
|
+
}
|
|
689
|
+
requiredBeats = probeData.required_beats ?? 1e3;
|
|
690
|
+
} finally {
|
|
691
|
+
clearTimeout(timeout);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const proofResult = await this.computeWorkProof({
|
|
695
|
+
beatsNeeded: requiredBeats,
|
|
696
|
+
anchorHash: this.globalAnchorHash,
|
|
697
|
+
anchorIndex: this.globalBeat
|
|
698
|
+
});
|
|
699
|
+
if (!proofResult.ok || !proofResult.receipt) {
|
|
700
|
+
return { ok: false, eligible: false, error: proofResult.error || "Failed to compute work proof" };
|
|
701
|
+
}
|
|
702
|
+
this.log(`Submitting spawn with work proof (${proofResult.beats_computed} beats)`);
|
|
703
|
+
return this.requestSpawn(opts?.childName, void 0, proofResult.receipt);
|
|
704
|
+
} catch (err) {
|
|
705
|
+
this.config.onError(err, "requestSpawnWithBeatsProof");
|
|
706
|
+
throw err;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
687
709
|
/**
|
|
688
710
|
* Purchase a SIGIL (cryptographic identity) for this agent.
|
|
689
711
|
* SIGILs gate heartbeating, lineage proofs, and offline verification.
|
|
@@ -923,6 +945,10 @@ var BeatAgent = class {
|
|
|
923
945
|
clearTimeout(timeout);
|
|
924
946
|
const data = await res.json();
|
|
925
947
|
if (data.anchor) {
|
|
948
|
+
if (this.config.verifyAnchors && !verifyAnchorHash(data.anchor)) {
|
|
949
|
+
this.log("\u26A0 Anchor hash verification FAILED \u2014 rejecting untrusted anchor");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
926
952
|
this.globalBeat = data.anchor.beat_index;
|
|
927
953
|
this.globalAnchorHash = data.anchor.hash || "";
|
|
928
954
|
if (data.anchor.difficulty) this.difficulty = data.anchor.difficulty;
|
|
@@ -1020,7 +1046,7 @@ export {
|
|
|
1020
1046
|
ValidationError,
|
|
1021
1047
|
computeBeat,
|
|
1022
1048
|
computeBeatsLite,
|
|
1023
|
-
|
|
1024
|
-
|
|
1049
|
+
register,
|
|
1050
|
+
verifyAnchorHash
|
|
1025
1051
|
};
|
|
1026
1052
|
//# sourceMappingURL=index.mjs.map
|