@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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/beat-sdk.ts
2
- import { createHash, generateKeyPairSync, sign, verify, createPrivateKey, createPublicKey } from "crypto";
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 ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
113
- function generateWalletKeypair() {
114
- const { publicKey, privateKey } = generateKeyPairSync("ed25519");
115
- const pubRaw = publicKey.export({ type: "spki", format: "der" }).subarray(12);
116
- const privRaw = privateKey.export({ type: "pkcs8", format: "der" }).subarray(16);
117
- return {
118
- publicKey: Buffer.from(pubRaw).toString("hex"),
119
- secretKey: Buffer.from(privRaw).toString("hex")
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
- function signMessage(secretKeyHex, message) {
123
- const privRaw = Buffer.from(secretKeyHex, "hex");
124
- const privKeyDer = Buffer.concat([ED25519_PKCS8_PREFIX, privRaw]);
125
- const keyObject = createPrivateKey({ key: privKeyDer, format: "der", type: "pkcs8" });
126
- const sig = sign(null, Buffer.from(message), keyObject);
127
- return Buffer.from(sig).toString("hex");
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
- // ── PULSE (COMPUTE BEATS) ──
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
- * @deprecated Phase 2: Resync retired (D-67). Dormancy resume is free just call heartbeat().
599
- * This method will be removed in the next major version.
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("Requesting re-sync challenge...");
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 startHash = challenge.challenge.start_from_hash;
616
- const startBeat = challenge.challenge.start_from_beat;
617
- this.latestBeat = { index: startBeat, hash: startHash, prev: "", timestamp: Date.now() };
618
- this.chain = [this.latestBeat];
619
- const t0 = Date.now();
620
- this.computeBeats(required);
621
- const elapsed = Date.now() - t0;
622
- this.log(`Re-sync beats computed in ${elapsed}ms`);
623
- const proof = await this.api("POST", "/api/v1/agent/resync", {
624
- action: "prove",
625
- challenge_nonce: challenge.challenge.nonce,
626
- proof: {
627
- from_beat: startBeat,
628
- to_beat: this.latestBeat.index,
629
- from_hash: startHash,
630
- to_hash: this.latestBeat.hash,
631
- beats_computed: required,
632
- global_anchor: challenge.challenge.sync_to_global,
633
- anchor_hash: this.globalAnchorHash || void 0,
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: proof.ok, beats_required: required };
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
- generateWalletKeypair,
1024
- register
1049
+ register,
1050
+ verifyAnchorHash
1025
1051
  };
1026
1052
  //# sourceMappingURL=index.mjs.map