@provenonce/sdk 0.8.0 → 0.10.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.js CHANGED
@@ -20,7 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AuthError: () => AuthError,
23
24
  BeatAgent: () => BeatAgent,
25
+ ErrorCode: () => ErrorCode,
26
+ FrozenError: () => FrozenError,
27
+ NetworkError: () => NetworkError,
28
+ NotFoundError: () => NotFoundError,
29
+ ProvenonceError: () => ProvenonceError,
30
+ RateLimitError: () => RateLimitError,
31
+ ServerError: () => ServerError,
32
+ StateError: () => StateError,
33
+ ValidationError: () => ValidationError,
24
34
  computeBeat: () => computeBeat,
25
35
  computeBeatsLite: () => computeBeatsLite,
26
36
  generateWalletKeypair: () => generateWalletKeypair,
@@ -30,6 +40,106 @@ module.exports = __toCommonJS(index_exports);
30
40
 
31
41
  // src/beat-sdk.ts
32
42
  var import_crypto = require("crypto");
43
+
44
+ // src/errors.ts
45
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
46
+ ErrorCode2["VALIDATION"] = "VALIDATION";
47
+ ErrorCode2["AUTH_INVALID"] = "AUTH_INVALID";
48
+ ErrorCode2["AUTH_MISSING"] = "AUTH_MISSING";
49
+ ErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
50
+ ErrorCode2["AGENT_FROZEN"] = "AGENT_FROZEN";
51
+ ErrorCode2["AGENT_NOT_INITIALIZED"] = "AGENT_NOT_INITIALIZED";
52
+ ErrorCode2["AGENT_WRONG_STATE"] = "AGENT_WRONG_STATE";
53
+ ErrorCode2["NOT_FOUND"] = "NOT_FOUND";
54
+ ErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
55
+ ErrorCode2["TIMEOUT"] = "TIMEOUT";
56
+ ErrorCode2["SERVER_ERROR"] = "SERVER_ERROR";
57
+ return ErrorCode2;
58
+ })(ErrorCode || {});
59
+ var ProvenonceError = class extends Error {
60
+ constructor(message, code, statusCode, details) {
61
+ super(message);
62
+ this.name = "ProvenonceError";
63
+ this.code = code;
64
+ this.statusCode = statusCode;
65
+ this.details = details;
66
+ Object.setPrototypeOf(this, new.target.prototype);
67
+ }
68
+ };
69
+ var ValidationError = class extends ProvenonceError {
70
+ constructor(message, details) {
71
+ super(message, "VALIDATION" /* VALIDATION */, void 0, details);
72
+ this.name = "ValidationError";
73
+ }
74
+ };
75
+ var AuthError = class extends ProvenonceError {
76
+ constructor(message, code = "AUTH_INVALID" /* AUTH_INVALID */, statusCode) {
77
+ super(message, code, statusCode);
78
+ this.name = "AuthError";
79
+ }
80
+ };
81
+ var RateLimitError = class extends ProvenonceError {
82
+ constructor(message, statusCode = 429, retryAfterMs) {
83
+ super(message, "RATE_LIMITED" /* RATE_LIMITED */, statusCode);
84
+ this.name = "RateLimitError";
85
+ this.retryAfterMs = retryAfterMs;
86
+ }
87
+ };
88
+ var FrozenError = class extends ProvenonceError {
89
+ constructor(message = "Agent is frozen. Use resync() to re-establish provenance.") {
90
+ super(message, "AGENT_FROZEN" /* AGENT_FROZEN */);
91
+ this.name = "FrozenError";
92
+ }
93
+ };
94
+ var StateError = class extends ProvenonceError {
95
+ constructor(message, currentState, code = "AGENT_WRONG_STATE" /* AGENT_WRONG_STATE */) {
96
+ super(message, code);
97
+ this.name = "StateError";
98
+ this.currentState = currentState;
99
+ }
100
+ };
101
+ var NotFoundError = class extends ProvenonceError {
102
+ constructor(message, statusCode = 404) {
103
+ super(message, "NOT_FOUND" /* NOT_FOUND */, statusCode);
104
+ this.name = "NotFoundError";
105
+ }
106
+ };
107
+ var NetworkError = class extends ProvenonceError {
108
+ constructor(message, code = "NETWORK_ERROR" /* NETWORK_ERROR */) {
109
+ super(message, code);
110
+ this.name = "NetworkError";
111
+ }
112
+ };
113
+ var ServerError = class extends ProvenonceError {
114
+ constructor(message, statusCode = 500) {
115
+ super(message, "SERVER_ERROR" /* SERVER_ERROR */, statusCode);
116
+ this.name = "ServerError";
117
+ }
118
+ };
119
+ function mapApiError(statusCode, body, path) {
120
+ const msg = typeof body.error === "string" ? body.error : `API error ${statusCode}`;
121
+ if (statusCode === 401 || statusCode === 403) {
122
+ const code = statusCode === 401 ? "AUTH_MISSING" /* AUTH_MISSING */ : "AUTH_INVALID" /* AUTH_INVALID */;
123
+ return new AuthError(msg, code, statusCode);
124
+ }
125
+ if (statusCode === 429) {
126
+ const retryAfter = typeof body.retry_after_ms === "number" ? body.retry_after_ms : void 0;
127
+ return new RateLimitError(msg, statusCode, retryAfter);
128
+ }
129
+ if (statusCode === 404) {
130
+ return new NotFoundError(msg, statusCode);
131
+ }
132
+ if (statusCode >= 500) {
133
+ return new ServerError(msg, statusCode);
134
+ }
135
+ const lowerMsg = msg.toLowerCase();
136
+ if (lowerMsg.includes("frozen")) {
137
+ return new FrozenError(msg);
138
+ }
139
+ return new ProvenonceError(msg, "SERVER_ERROR" /* SERVER_ERROR */, statusCode);
140
+ }
141
+
142
+ // src/beat-sdk.ts
33
143
  function computeBeat(prevHash, beatIndex, difficulty, nonce, anchorHash) {
34
144
  const timestamp = Date.now();
35
145
  const seed = anchorHash ? `${prevHash}:${beatIndex}:${nonce || ""}:${anchorHash}` : `${prevHash}:${beatIndex}:${nonce || ""}`;
@@ -58,16 +168,16 @@ function signMessage(secretKeyHex, message) {
58
168
  }
59
169
  async function register(name, options) {
60
170
  if (!name || typeof name !== "string" || name.trim().length === 0) {
61
- throw new Error("name is required (must be a non-empty string)");
171
+ throw new ValidationError("name is required (must be a non-empty string)");
62
172
  }
63
173
  if (name.length > 64) {
64
- throw new Error("name must be 64 characters or fewer");
174
+ throw new ValidationError("name must be 64 characters or fewer");
65
175
  }
66
176
  const url = options?.registryUrl || "https://provenonce.io";
67
177
  try {
68
178
  new URL(url);
69
179
  } catch {
70
- throw new Error("registryUrl is not a valid URL");
180
+ throw new ValidationError("registryUrl is not a valid URL");
71
181
  }
72
182
  const headers = { "Content-Type": "application/json" };
73
183
  if (options?.registrationSecret) {
@@ -86,17 +196,17 @@ async function register(name, options) {
86
196
  try {
87
197
  data2 = await res2.json();
88
198
  } catch {
89
- throw new Error(`Registration failed: ${res2.status} ${res2.statusText} (non-JSON response)`);
199
+ throw new NetworkError(`Registration failed: ${res2.status} ${res2.statusText} (non-JSON response)`);
90
200
  }
91
- if (!res2.ok) throw new Error(data2.error || "Registration failed");
201
+ if (!res2.ok) throw mapApiError(res2.status, data2, "/api/v1/register");
92
202
  return data2;
93
203
  }
94
204
  if (options?.walletChain === "ethereum") {
95
205
  if (!options.walletAddress || !options.walletSignFn) {
96
- throw new Error("Ethereum registration requires walletAddress and walletSignFn");
206
+ throw new ValidationError("Ethereum registration requires walletAddress and walletSignFn");
97
207
  }
98
208
  if (!/^0x[0-9a-fA-F]{40}$/.test(options.walletAddress)) {
99
- throw new Error("walletAddress must be a valid Ethereum address (0x + 40 hex chars)");
209
+ throw new ValidationError("walletAddress must be a valid Ethereum address (0x + 40 hex chars)");
100
210
  }
101
211
  const challengeRes = await fetch(`${url}/api/v1/register`, {
102
212
  method: "POST",
@@ -107,10 +217,10 @@ async function register(name, options) {
107
217
  try {
108
218
  challengeData = await challengeRes.json();
109
219
  } catch {
110
- throw new Error(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
220
+ throw new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
111
221
  }
112
222
  if (!challengeRes.ok || !challengeData.nonce) {
113
- throw new Error(challengeData.error || "Failed to get registration challenge");
223
+ throw mapApiError(challengeRes.status, challengeData, "/api/v1/register");
114
224
  }
115
225
  const nonce = challengeData.nonce;
116
226
  const message = `provenonce-register-ethereum:${nonce}:${options.walletAddress}:${name}`;
@@ -131,9 +241,9 @@ async function register(name, options) {
131
241
  try {
132
242
  data2 = await registerRes.json();
133
243
  } catch {
134
- throw new Error(`Ethereum registration failed: ${registerRes.status} (non-JSON response)`);
244
+ throw new NetworkError(`Ethereum registration failed: ${registerRes.status} (non-JSON response)`);
135
245
  }
136
- if (!registerRes.ok) throw new Error(data2.error || "Registration failed");
246
+ if (!registerRes.ok) throw mapApiError(registerRes.status, data2, "/api/v1/register");
137
247
  data2.wallet = {
138
248
  public_key: "",
139
249
  secret_key: "",
@@ -144,7 +254,7 @@ async function register(name, options) {
144
254
  }
145
255
  if (options?.walletModel === "operator") {
146
256
  if (!options.operatorWalletAddress || !options.operatorSignFn) {
147
- throw new Error("Operator registration requires operatorWalletAddress and operatorSignFn");
257
+ throw new ValidationError("Operator registration requires operatorWalletAddress and operatorSignFn");
148
258
  }
149
259
  const challengeRes = await fetch(`${url}/api/v1/register`, {
150
260
  method: "POST",
@@ -155,10 +265,10 @@ async function register(name, options) {
155
265
  try {
156
266
  challengeData = await challengeRes.json();
157
267
  } catch {
158
- throw new Error(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
268
+ throw new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
159
269
  }
160
270
  if (!challengeRes.ok || !challengeData.nonce) {
161
- throw new Error(challengeData.error || "Failed to get registration challenge");
271
+ throw mapApiError(challengeRes.status, challengeData, "/api/v1/register");
162
272
  }
163
273
  const nonce = challengeData.nonce;
164
274
  const message = `provenonce-register-operator:${nonce}:${options.operatorWalletAddress}:${name}`;
@@ -179,9 +289,9 @@ async function register(name, options) {
179
289
  try {
180
290
  data2 = await registerRes.json();
181
291
  } catch {
182
- throw new Error(`Operator registration failed: ${registerRes.status} (non-JSON response)`);
292
+ throw new NetworkError(`Operator registration failed: ${registerRes.status} (non-JSON response)`);
183
293
  }
184
- if (!registerRes.ok) throw new Error(data2.error || "Registration failed");
294
+ if (!registerRes.ok) throw mapApiError(registerRes.status, data2, "/api/v1/register");
185
295
  const addr = data2.wallet?.address || data2.wallet?.solana_address || options.operatorWalletAddress;
186
296
  data2.wallet = {
187
297
  public_key: "",
@@ -215,10 +325,12 @@ async function register(name, options) {
215
325
  try {
216
326
  challengeData = await challengeRes.json();
217
327
  } catch {
218
- throw new Error(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
328
+ const err = new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
329
+ err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
330
+ throw err;
219
331
  }
220
332
  if (!challengeRes.ok || !challengeData.nonce) {
221
- const err = new Error(challengeData.error || "Failed to get registration challenge");
333
+ const err = mapApiError(challengeRes.status, challengeData, "/api/v1/register");
222
334
  err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
223
335
  throw err;
224
336
  }
@@ -240,12 +352,12 @@ async function register(name, options) {
240
352
  try {
241
353
  data2 = await registerRes.json();
242
354
  } catch {
243
- const err = new Error(`Registration failed: ${registerRes.status} (non-JSON response)`);
355
+ const err = new NetworkError(`Registration failed: ${registerRes.status} (non-JSON response)`);
244
356
  err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
245
357
  throw err;
246
358
  }
247
359
  if (!registerRes.ok) {
248
- const err = new Error(data2.error || "Registration failed");
360
+ const err = mapApiError(registerRes.status, data2, "/api/v1/register");
249
361
  err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
250
362
  throw err;
251
363
  }
@@ -268,9 +380,9 @@ async function register(name, options) {
268
380
  try {
269
381
  data = await res.json();
270
382
  } catch {
271
- throw new Error(`Registration failed: ${res.status} ${res.statusText} (non-JSON response)`);
383
+ throw new NetworkError(`Registration failed: ${res.status} ${res.statusText} (non-JSON response)`);
272
384
  }
273
- if (!res.ok) throw new Error(data.error || "Registration failed");
385
+ if (!res.ok) throw mapApiError(res.status, data, "/api/v1/register");
274
386
  return data;
275
387
  }
276
388
  var BeatAgent = class {
@@ -285,30 +397,36 @@ var BeatAgent = class {
285
397
  this.heartbeatInterval = null;
286
398
  this.globalBeat = 0;
287
399
  this.globalAnchorHash = "";
400
+ // ── PHASE 2: SIGIL + HEARTBEAT + PROOF ──
401
+ /** Cached lineage proof from the most recent heartbeat or SIGIL purchase */
402
+ this.cachedProof = null;
288
403
  if (!config.apiKey || typeof config.apiKey !== "string") {
289
- throw new Error("BeatAgentConfig.apiKey is required (must be a non-empty string)");
404
+ throw new ValidationError("BeatAgentConfig.apiKey is required (must be a non-empty string)");
290
405
  }
291
406
  if (!config.registryUrl || typeof config.registryUrl !== "string") {
292
- throw new Error("BeatAgentConfig.registryUrl is required (must be a non-empty string)");
407
+ throw new ValidationError("BeatAgentConfig.registryUrl is required (must be a non-empty string)");
293
408
  }
294
409
  try {
295
410
  new URL(config.registryUrl);
296
411
  } catch {
297
- throw new Error("BeatAgentConfig.registryUrl is not a valid URL");
412
+ throw new ValidationError("BeatAgentConfig.registryUrl is not a valid URL");
298
413
  }
299
414
  if (config.beatsPerPulse !== void 0 && (!Number.isInteger(config.beatsPerPulse) || config.beatsPerPulse < 1 || config.beatsPerPulse > 1e4)) {
300
- throw new Error("BeatAgentConfig.beatsPerPulse must be an integer between 1 and 10000");
415
+ throw new ValidationError("BeatAgentConfig.beatsPerPulse must be an integer between 1 and 10000");
301
416
  }
302
417
  if (config.checkinIntervalSec !== void 0 && (!Number.isFinite(config.checkinIntervalSec) || config.checkinIntervalSec < 10 || config.checkinIntervalSec > 86400)) {
303
- throw new Error("BeatAgentConfig.checkinIntervalSec must be between 10 and 86400");
418
+ throw new ValidationError("BeatAgentConfig.checkinIntervalSec must be between 10 and 86400");
304
419
  }
305
420
  this.config = {
306
421
  beatsPerPulse: 10,
307
422
  checkinIntervalSec: 300,
423
+ heartbeatIntervalSec: 300,
308
424
  onPulse: () => {
309
425
  },
310
426
  onCheckin: () => {
311
427
  },
428
+ onHeartbeat: () => {
429
+ },
312
430
  onError: () => {
313
431
  },
314
432
  onStatusChange: () => {
@@ -357,16 +475,21 @@ var BeatAgent = class {
357
475
  }
358
476
  // ── PULSE (COMPUTE BEATS) ──
359
477
  /**
478
+ * @deprecated Phase 2: VDF computation retired (D-68). Payment is the liveness mechanism.
479
+ * Use heartbeat() instead. This method will be removed in the next major version.
480
+ *
360
481
  * Compute N beats locally (VDF hash chain).
361
- * This is the "heartbeat" — proof that the agent has lived
362
- * through a specific window of computational time.
363
482
  */
364
483
  pulse(count) {
484
+ console.warn("[Provenonce SDK] pulse() is deprecated. Use heartbeat() instead (Phase 2).");
485
+ if (this.status === "frozen") {
486
+ throw new FrozenError("Cannot pulse: agent is frozen. Use resync() to re-establish provenance.");
487
+ }
365
488
  if (this.status !== "active") {
366
- throw new Error(`Cannot pulse: agent is ${this.status}. Use resync() if frozen.`);
489
+ throw new StateError(`Cannot pulse: agent is ${this.status}.`, this.status);
367
490
  }
368
491
  if (count !== void 0 && (!Number.isInteger(count) || count < 1 || count > 1e4)) {
369
- throw new Error("pulse count must be an integer between 1 and 10000");
492
+ throw new ValidationError("pulse count must be an integer between 1 and 10000");
370
493
  }
371
494
  return this.computeBeats(count);
372
495
  }
@@ -374,7 +497,7 @@ var BeatAgent = class {
374
497
  computeBeats(count, onProgress) {
375
498
  const n = count || this.config.beatsPerPulse;
376
499
  if (!this.latestBeat) {
377
- throw new Error("Beat chain not initialized. Call init() first.");
500
+ throw new StateError("Beat chain not initialized. Call init() first.", "uninitialized", "AGENT_NOT_INITIALIZED" /* AGENT_NOT_INITIALIZED */);
378
501
  }
379
502
  const newBeats = [];
380
503
  let prevHash = this.latestBeat.hash;
@@ -402,12 +525,11 @@ var BeatAgent = class {
402
525
  }
403
526
  // ── CHECK-IN ──
404
527
  /**
405
- * Submit a Beat proof to the registry.
406
- *
407
- * "To remain on the Whitelist, an agent must periodically
408
- * submit a proof of its Local Beats to the Registry."
528
+ * @deprecated Phase 2: VDF check-in retired (D-68). Use heartbeat() instead.
529
+ * This method will be removed in the next major version.
409
530
  */
410
531
  async checkin() {
532
+ console.warn("[Provenonce SDK] checkin() is deprecated. Use heartbeat() instead (Phase 2).");
411
533
  if (!this.latestBeat || this.latestBeat.index <= this.lastCheckinBeat) {
412
534
  this.log("No new beats since last check-in. Call pulse() first.");
413
535
  return { ok: true, total_beats: this.totalBeats };
@@ -461,18 +583,21 @@ var BeatAgent = class {
461
583
  // ── AUTONOMOUS HEARTBEAT ──
462
584
  /**
463
585
  * Start the autonomous heartbeat loop.
464
- * Computes beats continuously and checks in periodically.
465
- * This is "keeping the agent alive" in Beat time.
586
+ * Phase 2: Sends paid heartbeats at regular intervals.
587
+ *
588
+ * @param paymentTxFn - Optional function that returns a payment tx for each heartbeat.
589
+ * If not provided, uses 'devnet-skip' (devnet only).
466
590
  */
467
- startHeartbeat() {
591
+ startHeartbeat(paymentTxFn) {
468
592
  if (this.heartbeatInterval) {
469
593
  this.log("Heartbeat already running.");
470
594
  return;
471
595
  }
472
- if (this.status !== "active") {
473
- throw new Error(`Cannot start heartbeat in status '${this.status}'.`);
596
+ if (this.status !== "active" && this.status !== "uninitialized") {
597
+ throw new StateError(`Cannot start heartbeat in status '${this.status}'.`, this.status);
474
598
  }
475
- this.log("\u2661 Starting heartbeat...");
599
+ const intervalSec = this.config.heartbeatIntervalSec || this.config.checkinIntervalSec || 300;
600
+ this.log(`Starting heartbeat (interval: ${intervalSec}s)...`);
476
601
  let consecutiveErrors = 0;
477
602
  let skipCount = 0;
478
603
  this.heartbeatInterval = setInterval(async () => {
@@ -481,13 +606,8 @@ var BeatAgent = class {
481
606
  return;
482
607
  }
483
608
  try {
484
- this.pulse();
485
- const beatsSinceCheckin = this.latestBeat.index - this.lastCheckinBeat;
486
- const shouldCheckin = beatsSinceCheckin >= this.config.beatsPerPulse * 5;
487
- if (shouldCheckin) {
488
- await this.checkin();
489
- await this.syncGlobal();
490
- }
609
+ const paymentTx = paymentTxFn ? await paymentTxFn() : "devnet-skip";
610
+ await this.heartbeat(paymentTx);
491
611
  consecutiveErrors = 0;
492
612
  } catch (err) {
493
613
  consecutiveErrors++;
@@ -495,28 +615,25 @@ var BeatAgent = class {
495
615
  skipCount = Math.min(32, Math.pow(2, consecutiveErrors - 1));
496
616
  this.log(`Heartbeat error #${consecutiveErrors}, backing off ${skipCount} ticks`);
497
617
  }
498
- }, this.config.checkinIntervalSec * 1e3 / 10);
618
+ }, intervalSec * 1e3);
499
619
  }
500
620
  /**
501
- * Stop the heartbeat. Agent's time "freezes."
502
- * Must call resync() when waking up.
621
+ * Stop the heartbeat loop.
503
622
  */
504
623
  stopHeartbeat() {
505
624
  if (this.heartbeatInterval) {
506
625
  clearInterval(this.heartbeatInterval);
507
626
  this.heartbeatInterval = null;
508
- this.log("\u2661 Heartbeat stopped. Time frozen.");
627
+ this.log("Heartbeat stopped.");
509
628
  }
510
629
  }
511
630
  // ── RE-SYNC ──
512
631
  /**
513
- * Re-sync after being offline/frozen.
514
- *
515
- * "When an agent powers down, its time 'freezes.' Upon waking,
516
- * it must perform a Re-Sync Challenge with the Registry to
517
- * fill the 'Temporal Gap' and re-establish its provenance."
632
+ * @deprecated Phase 2: Resync retired (D-67). Dormancy resume is free — just call heartbeat().
633
+ * This method will be removed in the next major version.
518
634
  */
519
635
  async resync() {
636
+ console.warn("[Provenonce SDK] resync() is deprecated (D-67). Use heartbeat() to resume (Phase 2).");
520
637
  try {
521
638
  this.log("Requesting re-sync challenge...");
522
639
  const challenge = await this.api("POST", "/api/v1/agent/resync", {
@@ -580,10 +697,10 @@ var BeatAgent = class {
580
697
  try {
581
698
  if (childName !== void 0) {
582
699
  if (typeof childName !== "string" || childName.trim().length === 0) {
583
- throw new Error("childName must be a non-empty string");
700
+ throw new ValidationError("childName must be a non-empty string");
584
701
  }
585
702
  if (childName.length > 64) {
586
- throw new Error("childName must be 64 characters or fewer");
703
+ throw new ValidationError("childName must be 64 characters or fewer");
587
704
  }
588
705
  }
589
706
  const res = await this.api("POST", "/api/v1/agent/spawn", {
@@ -601,6 +718,210 @@ var BeatAgent = class {
601
718
  throw err;
602
719
  }
603
720
  }
721
+ /**
722
+ * Purchase a SIGIL (cryptographic identity) for this agent.
723
+ * SIGILs gate heartbeating, lineage proofs, and offline verification.
724
+ * One-time purchase — cannot be re-purchased.
725
+ *
726
+ * @param options - SIGIL purchase options (identity_class, principal, tier, name, payment_tx, + optional metadata)
727
+ *
728
+ * Legacy signature (deprecated):
729
+ * @param identityClass - 'narrow_task' | 'autonomous' | 'orchestrator'
730
+ * @param paymentTx - Solana transaction signature or 'devnet-skip'
731
+ */
732
+ async purchaseSigil(optionsOrClass, paymentTx) {
733
+ let body;
734
+ if (typeof optionsOrClass === "string") {
735
+ if (!optionsOrClass || !["narrow_task", "autonomous", "orchestrator"].includes(optionsOrClass)) {
736
+ throw new ValidationError("identityClass must be narrow_task, autonomous, or orchestrator");
737
+ }
738
+ if (!paymentTx || typeof paymentTx !== "string") {
739
+ throw new ValidationError('paymentTx is required (Solana transaction signature or "devnet-skip")');
740
+ }
741
+ body = {
742
+ identity_class: optionsOrClass,
743
+ payment_tx: paymentTx
744
+ // Legacy calls without principal/tier — server will require these now
745
+ // Callers must migrate to the options object form
746
+ };
747
+ } else {
748
+ const opts = optionsOrClass;
749
+ if (!opts.identity_class || !["narrow_task", "autonomous", "orchestrator"].includes(opts.identity_class)) {
750
+ throw new ValidationError("identity_class must be narrow_task, autonomous, or orchestrator");
751
+ }
752
+ if (!opts.principal || typeof opts.principal !== "string") {
753
+ throw new ValidationError("principal is required");
754
+ }
755
+ if (!opts.tier || !["sov", "org", "ind", "eph", "sbx"].includes(opts.tier)) {
756
+ throw new ValidationError("tier must be one of: sov, org, ind, eph, sbx");
757
+ }
758
+ if (!opts.payment_tx || typeof opts.payment_tx !== "string") {
759
+ throw new ValidationError("payment_tx is required");
760
+ }
761
+ body = { ...opts };
762
+ }
763
+ try {
764
+ const res = await this.api("POST", "/api/v1/sigil", body);
765
+ if (res.lineage_proof) {
766
+ this.cachedProof = res.lineage_proof;
767
+ }
768
+ const sigilStr = res.sigil?.sigil || res.sigil?.identity_class || "";
769
+ this.log(`SIGIL purchased: ${sigilStr}`);
770
+ this.config.onStatusChange("sigil_issued", { sigil: sigilStr });
771
+ return {
772
+ ok: true,
773
+ sigil: res.sigil,
774
+ lineage_proof: res.lineage_proof,
775
+ fee: res.fee
776
+ };
777
+ } catch (err) {
778
+ this.config.onError(err, "purchaseSigil");
779
+ return { ok: false, error: err.message };
780
+ }
781
+ }
782
+ /**
783
+ * Update mutable SIGIL metadata fields.
784
+ * Requires a SIGIL. Cannot modify immutable fields.
785
+ *
786
+ * @param fields - Subset of mutable SIGIL fields to update
787
+ */
788
+ async updateMetadata(fields) {
789
+ if (!fields || Object.keys(fields).length === 0) {
790
+ throw new ValidationError("At least one metadata field is required");
791
+ }
792
+ try {
793
+ const res = await this.api("PATCH", "/api/v1/agent/metadata", fields);
794
+ this.log(`Metadata updated: ${res.updated_fields?.join(", ") || "unknown"}`);
795
+ return {
796
+ ok: true,
797
+ sigil: res.sigil,
798
+ generation: res.generation,
799
+ updated_fields: res.updated_fields
800
+ };
801
+ } catch (err) {
802
+ this.config.onError(err, "updateMetadata");
803
+ return { ok: false, error: err.message };
804
+ }
805
+ }
806
+ /**
807
+ * Send a paid heartbeat to the registry.
808
+ * Requires a SIGIL. Returns a signed lineage proof.
809
+ * This is the Phase 2 replacement for pulse() + checkin().
810
+ *
811
+ * @param paymentTx - Solana transaction signature. Omit or 'devnet-skip' on devnet.
812
+ * @param globalAnchor - Optional: the global anchor index to reference.
813
+ */
814
+ async heartbeat(paymentTx, globalAnchor) {
815
+ try {
816
+ const res = await this.api("POST", "/api/v1/agent/heartbeat", {
817
+ payment_tx: paymentTx || "devnet-skip",
818
+ global_anchor: globalAnchor
819
+ });
820
+ if (res.lineage_proof) {
821
+ this.cachedProof = res.lineage_proof;
822
+ }
823
+ if (res.ok) {
824
+ this.status = "active";
825
+ const onHb = this.config.onHeartbeat || this.config.onCheckin;
826
+ if (onHb) onHb(res);
827
+ this.log(`Heartbeat accepted: epoch=${res.billing_epoch}, count=${res.heartbeat_count_epoch}`);
828
+ }
829
+ return {
830
+ ok: res.ok,
831
+ lineage_proof: res.lineage_proof,
832
+ heartbeat_count_epoch: res.heartbeat_count_epoch,
833
+ billing_epoch: res.billing_epoch,
834
+ current_beat: res.current_beat,
835
+ fee: res.fee
836
+ };
837
+ } catch (err) {
838
+ this.config.onError(err, "heartbeat");
839
+ return { ok: false, error: err.message };
840
+ }
841
+ }
842
+ /**
843
+ * Reissue a lineage proof. "Reprint, not a renewal."
844
+ * Does NOT create a new lineage event.
845
+ *
846
+ * @param paymentTx - Solana transaction signature. Omit or 'devnet-skip' on devnet.
847
+ */
848
+ async reissueProof(paymentTx) {
849
+ try {
850
+ const res = await this.api("POST", "/api/v1/agent/reissue-proof", {
851
+ payment_tx: paymentTx || "devnet-skip"
852
+ });
853
+ if (res.lineage_proof) {
854
+ this.cachedProof = res.lineage_proof;
855
+ }
856
+ return { ok: true, lineage_proof: res.lineage_proof };
857
+ } catch (err) {
858
+ this.config.onError(err, "reissueProof");
859
+ return { ok: false, error: err.message };
860
+ }
861
+ }
862
+ /**
863
+ * Get the latest cached lineage proof (no network call).
864
+ * Returns null if no proof has been obtained yet.
865
+ */
866
+ getLatestProof() {
867
+ return this.cachedProof;
868
+ }
869
+ /**
870
+ * Get the agent's passport (alias for getLatestProof).
871
+ * The passport is the agent's portable, offline-verifiable credential.
872
+ * Returns null if no passport has been issued yet (requires SIGIL + heartbeat).
873
+ */
874
+ getPassport() {
875
+ return this.cachedProof;
876
+ }
877
+ /**
878
+ * Verify a lineage proof locally using the authority public key.
879
+ * Offline verification — no API call, no SOL cost.
880
+ *
881
+ * Returns a VerificationResult object. The object is truthy when valid,
882
+ * so `if (BeatAgent.verifyProofLocally(proof, key))` still works.
883
+ *
884
+ * @param proof - The LineageProof to verify
885
+ * @param authorityPubKeyHex - 32-byte hex-encoded Ed25519 public key from /.well-known/provenonce-authority.json
886
+ * @param currentBeat - Optional current global beat index (for beatsSinceHeartbeat calculation)
887
+ */
888
+ static verifyProofLocally(proof, authorityPubKeyHex, currentBeat) {
889
+ const now = Date.now();
890
+ const expired = now > proof.valid_until;
891
+ let signatureValid = false;
892
+ try {
893
+ const canonical = JSON.stringify({
894
+ agent_hash: proof.agent_hash,
895
+ agent_public_key: proof.agent_public_key,
896
+ identity_class: proof.identity_class,
897
+ registered_at_beat: proof.registered_at_beat,
898
+ sigil_issued_at_beat: proof.sigil_issued_at_beat,
899
+ last_heartbeat_beat: proof.last_heartbeat_beat,
900
+ lineage_chain_hash: proof.lineage_chain_hash,
901
+ issued_at: proof.issued_at,
902
+ valid_until: proof.valid_until
903
+ });
904
+ const pubBytes = Buffer.from(authorityPubKeyHex, "hex");
905
+ if (pubBytes.length === 32) {
906
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
907
+ const pubKeyDer = Buffer.concat([ED25519_SPKI_PREFIX, pubBytes]);
908
+ const keyObject = (0, import_crypto.createPublicKey)({ key: pubKeyDer, format: "der", type: "spki" });
909
+ const sigBuffer = Buffer.from(proof.provenonce_signature, "hex");
910
+ signatureValid = (0, import_crypto.verify)(null, Buffer.from(canonical), keyObject, sigBuffer);
911
+ }
912
+ } catch {
913
+ signatureValid = false;
914
+ }
915
+ const valid = signatureValid && !expired;
916
+ const beatsSinceHeartbeat = currentBeat != null ? currentBeat - proof.last_heartbeat_beat : null;
917
+ let warning;
918
+ if (expired) {
919
+ warning = "Proof has expired. Reissue with reissueProof() or send a heartbeat.";
920
+ } else if (beatsSinceHeartbeat != null && beatsSinceHeartbeat > 60) {
921
+ warning = `Agent is ${beatsSinceHeartbeat} beats behind. Heartbeat may be stale.`;
922
+ }
923
+ return { valid, signatureValid, expired, lastHeartbeatBeat: proof.last_heartbeat_beat, beatsSinceHeartbeat, warning };
924
+ }
604
925
  // ── STATUS ──
605
926
  /**
606
927
  * Get this agent's full beat status from the registry.
@@ -682,16 +1003,15 @@ var BeatAgent = class {
682
1003
  try {
683
1004
  data = await res.json();
684
1005
  } catch {
685
- throw new Error(`API error: ${res.status} non-JSON response from ${path}`);
1006
+ throw new NetworkError(`API error: ${res.status} non-JSON response from ${path}`);
686
1007
  }
687
1008
  if (!res.ok && !data.ok && !data.already_initialized && !data.eligible) {
688
- const serverMsg = typeof data.error === "string" ? data.error : `API error ${res.status}`;
689
- throw new Error(serverMsg);
1009
+ throw mapApiError(res.status, data, path);
690
1010
  }
691
1011
  return data;
692
1012
  } catch (err) {
693
1013
  if (err.name === "AbortError") {
694
- throw new Error(`Request timeout: ${method} ${path}`);
1014
+ throw new NetworkError(`Request timeout: ${method} ${path}`, "TIMEOUT" /* TIMEOUT */);
695
1015
  }
696
1016
  throw err;
697
1017
  } finally {
@@ -706,10 +1026,10 @@ var BeatAgent = class {
706
1026
  };
707
1027
  function computeBeatsLite(startHash, startIndex, count, difficulty = 1e3, anchorHash) {
708
1028
  if (!startHash || typeof startHash !== "string") {
709
- throw new Error("computeBeatsLite: startHash must be a non-empty string");
1029
+ throw new ValidationError("computeBeatsLite: startHash must be a non-empty string");
710
1030
  }
711
1031
  if (!Number.isInteger(count) || count < 1) {
712
- throw new Error("computeBeatsLite: count must be a positive integer");
1032
+ throw new ValidationError("computeBeatsLite: count must be a positive integer");
713
1033
  }
714
1034
  const t0 = Date.now();
715
1035
  let prev = startHash;
@@ -722,7 +1042,17 @@ function computeBeatsLite(startHash, startIndex, count, difficulty = 1e3, anchor
722
1042
  }
723
1043
  // Annotate the CommonJS export names for ESM import in node:
724
1044
  0 && (module.exports = {
1045
+ AuthError,
725
1046
  BeatAgent,
1047
+ ErrorCode,
1048
+ FrozenError,
1049
+ NetworkError,
1050
+ NotFoundError,
1051
+ ProvenonceError,
1052
+ RateLimitError,
1053
+ ServerError,
1054
+ StateError,
1055
+ ValidationError,
726
1056
  computeBeat,
727
1057
  computeBeatsLite,
728
1058
  generateWalletKeypair,