@provenonce/sdk 0.6.0 → 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/dist/index.js CHANGED
@@ -20,15 +20,126 @@ 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,
36
+ generateWalletKeypair: () => generateWalletKeypair,
26
37
  register: () => register
27
38
  });
28
39
  module.exports = __toCommonJS(index_exports);
29
40
 
30
41
  // src/beat-sdk.ts
31
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
32
143
  function computeBeat(prevHash, beatIndex, difficulty, nonce, anchorHash) {
33
144
  const timestamp = Date.now();
34
145
  const seed = anchorHash ? `${prevHash}:${beatIndex}:${nonce || ""}:${anchorHash}` : `${prevHash}:${beatIndex}:${nonce || ""}`;
@@ -38,26 +149,240 @@ function computeBeat(prevHash, beatIndex, difficulty, nonce, anchorHash) {
38
149
  }
39
150
  return { index: beatIndex, hash: current, prev: prevHash, timestamp, nonce, anchor_hash: anchorHash };
40
151
  }
152
+ var ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
153
+ function generateWalletKeypair() {
154
+ const { publicKey, privateKey } = (0, import_crypto.generateKeyPairSync)("ed25519");
155
+ const pubRaw = publicKey.export({ type: "spki", format: "der" }).subarray(12);
156
+ const privRaw = privateKey.export({ type: "pkcs8", format: "der" }).subarray(16);
157
+ return {
158
+ publicKey: Buffer.from(pubRaw).toString("hex"),
159
+ secretKey: Buffer.from(privRaw).toString("hex")
160
+ };
161
+ }
162
+ function signMessage(secretKeyHex, message) {
163
+ const privRaw = Buffer.from(secretKeyHex, "hex");
164
+ const privKeyDer = Buffer.concat([ED25519_PKCS8_PREFIX, privRaw]);
165
+ const keyObject = (0, import_crypto.createPrivateKey)({ key: privKeyDer, format: "der", type: "pkcs8" });
166
+ const sig = (0, import_crypto.sign)(null, Buffer.from(message), keyObject);
167
+ return Buffer.from(sig).toString("hex");
168
+ }
41
169
  async function register(name, options) {
42
- const url = options?.registryUrl || "https://provenonce.vercel.app";
43
- const body = { name };
44
- const headers = { "Content-Type": "application/json" };
45
- if (options?.parentHash) {
46
- body.parent = options.parentHash;
170
+ if (!name || typeof name !== "string" || name.trim().length === 0) {
171
+ throw new ValidationError("name is required (must be a non-empty string)");
47
172
  }
48
- if (options?.parentApiKey) {
49
- headers["Authorization"] = `Bearer ${options.parentApiKey}`;
173
+ if (name.length > 64) {
174
+ throw new ValidationError("name must be 64 characters or fewer");
50
175
  }
176
+ const url = options?.registryUrl || "https://provenonce.io";
177
+ try {
178
+ new URL(url);
179
+ } catch {
180
+ throw new ValidationError("registryUrl is not a valid URL");
181
+ }
182
+ const headers = { "Content-Type": "application/json" };
51
183
  if (options?.registrationSecret) {
52
184
  headers["x-registration-secret"] = options.registrationSecret;
53
185
  }
186
+ if (options?.parentHash) {
187
+ if (options.parentApiKey) {
188
+ headers["Authorization"] = `Bearer ${options.parentApiKey}`;
189
+ }
190
+ const res2 = await fetch(`${url}/api/v1/register`, {
191
+ method: "POST",
192
+ headers,
193
+ body: JSON.stringify({ name, parent: options.parentHash, ...options.metadata && { metadata: options.metadata } })
194
+ });
195
+ let data2;
196
+ try {
197
+ data2 = await res2.json();
198
+ } catch {
199
+ throw new NetworkError(`Registration failed: ${res2.status} ${res2.statusText} (non-JSON response)`);
200
+ }
201
+ if (!res2.ok) throw mapApiError(res2.status, data2, "/api/v1/register");
202
+ return data2;
203
+ }
204
+ if (options?.walletChain === "ethereum") {
205
+ if (!options.walletAddress || !options.walletSignFn) {
206
+ throw new ValidationError("Ethereum registration requires walletAddress and walletSignFn");
207
+ }
208
+ if (!/^0x[0-9a-fA-F]{40}$/.test(options.walletAddress)) {
209
+ throw new ValidationError("walletAddress must be a valid Ethereum address (0x + 40 hex chars)");
210
+ }
211
+ const challengeRes = await fetch(`${url}/api/v1/register`, {
212
+ method: "POST",
213
+ headers,
214
+ body: JSON.stringify({ name, action: "challenge", wallet_chain: "ethereum" })
215
+ });
216
+ let challengeData;
217
+ try {
218
+ challengeData = await challengeRes.json();
219
+ } catch {
220
+ throw new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
221
+ }
222
+ if (!challengeRes.ok || !challengeData.nonce) {
223
+ throw mapApiError(challengeRes.status, challengeData, "/api/v1/register");
224
+ }
225
+ const nonce = challengeData.nonce;
226
+ const message = `provenonce-register-ethereum:${nonce}:${options.walletAddress}:${name}`;
227
+ const walletSignature = await options.walletSignFn(message);
228
+ const registerRes = await fetch(`${url}/api/v1/register`, {
229
+ method: "POST",
230
+ headers,
231
+ body: JSON.stringify({
232
+ name,
233
+ wallet_chain: "ethereum",
234
+ wallet_address: options.walletAddress,
235
+ wallet_signature: walletSignature,
236
+ wallet_nonce: nonce,
237
+ ...options.metadata && { metadata: options.metadata }
238
+ })
239
+ });
240
+ let data2;
241
+ try {
242
+ data2 = await registerRes.json();
243
+ } catch {
244
+ throw new NetworkError(`Ethereum registration failed: ${registerRes.status} (non-JSON response)`);
245
+ }
246
+ if (!registerRes.ok) throw mapApiError(registerRes.status, data2, "/api/v1/register");
247
+ data2.wallet = {
248
+ public_key: "",
249
+ secret_key: "",
250
+ address: data2.wallet?.address || options.walletAddress,
251
+ chain: "ethereum"
252
+ };
253
+ return data2;
254
+ }
255
+ if (options?.walletModel === "operator") {
256
+ if (!options.operatorWalletAddress || !options.operatorSignFn) {
257
+ throw new ValidationError("Operator registration requires operatorWalletAddress and operatorSignFn");
258
+ }
259
+ const challengeRes = await fetch(`${url}/api/v1/register`, {
260
+ method: "POST",
261
+ headers,
262
+ body: JSON.stringify({ name, action: "challenge", wallet_model: "operator" })
263
+ });
264
+ let challengeData;
265
+ try {
266
+ challengeData = await challengeRes.json();
267
+ } catch {
268
+ throw new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
269
+ }
270
+ if (!challengeRes.ok || !challengeData.nonce) {
271
+ throw mapApiError(challengeRes.status, challengeData, "/api/v1/register");
272
+ }
273
+ const nonce = challengeData.nonce;
274
+ const message = `provenonce-register-operator:${nonce}:${options.operatorWalletAddress}:${name}`;
275
+ const walletSignature = await options.operatorSignFn(message);
276
+ const registerRes = await fetch(`${url}/api/v1/register`, {
277
+ method: "POST",
278
+ headers,
279
+ body: JSON.stringify({
280
+ name,
281
+ wallet_model: "operator",
282
+ operator_wallet_address: options.operatorWalletAddress,
283
+ wallet_signature: walletSignature,
284
+ wallet_nonce: nonce,
285
+ ...options.metadata && { metadata: options.metadata }
286
+ })
287
+ });
288
+ let data2;
289
+ try {
290
+ data2 = await registerRes.json();
291
+ } catch {
292
+ throw new NetworkError(`Operator registration failed: ${registerRes.status} (non-JSON response)`);
293
+ }
294
+ if (!registerRes.ok) throw mapApiError(registerRes.status, data2, "/api/v1/register");
295
+ const addr = data2.wallet?.address || data2.wallet?.solana_address || options.operatorWalletAddress;
296
+ data2.wallet = {
297
+ public_key: "",
298
+ secret_key: "",
299
+ solana_address: addr,
300
+ address: addr,
301
+ chain: "solana"
302
+ };
303
+ return data2;
304
+ }
305
+ if (options?.walletModel === "self-custody" || options?.walletSecretKey) {
306
+ let walletKeys;
307
+ if (options?.walletSecretKey) {
308
+ const privRaw = Buffer.from(options.walletSecretKey, "hex");
309
+ const privKeyDer = Buffer.concat([ED25519_PKCS8_PREFIX, privRaw]);
310
+ const keyObject = (0, import_crypto.createPrivateKey)({ key: privKeyDer, format: "der", type: "pkcs8" });
311
+ const pubRaw = keyObject.export({ type: "spki", format: "der" }).subarray(12);
312
+ walletKeys = {
313
+ publicKey: Buffer.from(pubRaw).toString("hex"),
314
+ secretKey: options.walletSecretKey
315
+ };
316
+ } else {
317
+ walletKeys = generateWalletKeypair();
318
+ }
319
+ const challengeRes = await fetch(`${url}/api/v1/register`, {
320
+ method: "POST",
321
+ headers,
322
+ body: JSON.stringify({ name, action: "challenge" })
323
+ });
324
+ let challengeData;
325
+ try {
326
+ challengeData = await challengeRes.json();
327
+ } catch {
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;
331
+ }
332
+ if (!challengeRes.ok || !challengeData.nonce) {
333
+ const err = mapApiError(challengeRes.status, challengeData, "/api/v1/register");
334
+ err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
335
+ throw err;
336
+ }
337
+ const nonce = challengeData.nonce;
338
+ const message = `provenonce-register:${nonce}:${walletKeys.publicKey}:${name}`;
339
+ const walletSignature = signMessage(walletKeys.secretKey, message);
340
+ const registerRes = await fetch(`${url}/api/v1/register`, {
341
+ method: "POST",
342
+ headers,
343
+ body: JSON.stringify({
344
+ name,
345
+ wallet_public_key: walletKeys.publicKey,
346
+ wallet_signature: walletSignature,
347
+ wallet_nonce: nonce,
348
+ ...options?.metadata && { metadata: options.metadata }
349
+ })
350
+ });
351
+ let data2;
352
+ try {
353
+ data2 = await registerRes.json();
354
+ } catch {
355
+ const err = new NetworkError(`Registration failed: ${registerRes.status} (non-JSON response)`);
356
+ err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
357
+ throw err;
358
+ }
359
+ if (!registerRes.ok) {
360
+ const err = mapApiError(registerRes.status, data2, "/api/v1/register");
361
+ err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
362
+ throw err;
363
+ }
364
+ const addr = data2.wallet?.address || data2.wallet?.solana_address || "";
365
+ data2.wallet = {
366
+ public_key: walletKeys.publicKey,
367
+ secret_key: walletKeys.secretKey,
368
+ solana_address: addr,
369
+ address: addr,
370
+ chain: "solana"
371
+ };
372
+ return data2;
373
+ }
54
374
  const res = await fetch(`${url}/api/v1/register`, {
55
375
  method: "POST",
56
376
  headers,
57
- body: JSON.stringify(body)
377
+ body: JSON.stringify({ name, ...options?.metadata && { metadata: options.metadata } })
58
378
  });
59
- const data = await res.json();
60
- if (!res.ok) throw new Error(data.error || "Registration failed");
379
+ let data;
380
+ try {
381
+ data = await res.json();
382
+ } catch {
383
+ throw new NetworkError(`Registration failed: ${res.status} ${res.statusText} (non-JSON response)`);
384
+ }
385
+ if (!res.ok) throw mapApiError(res.status, data, "/api/v1/register");
61
386
  return data;
62
387
  }
63
388
  var BeatAgent = class {
@@ -72,13 +397,36 @@ var BeatAgent = class {
72
397
  this.heartbeatInterval = null;
73
398
  this.globalBeat = 0;
74
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;
403
+ if (!config.apiKey || typeof config.apiKey !== "string") {
404
+ throw new ValidationError("BeatAgentConfig.apiKey is required (must be a non-empty string)");
405
+ }
406
+ if (!config.registryUrl || typeof config.registryUrl !== "string") {
407
+ throw new ValidationError("BeatAgentConfig.registryUrl is required (must be a non-empty string)");
408
+ }
409
+ try {
410
+ new URL(config.registryUrl);
411
+ } catch {
412
+ throw new ValidationError("BeatAgentConfig.registryUrl is not a valid URL");
413
+ }
414
+ if (config.beatsPerPulse !== void 0 && (!Number.isInteger(config.beatsPerPulse) || config.beatsPerPulse < 1 || config.beatsPerPulse > 1e4)) {
415
+ throw new ValidationError("BeatAgentConfig.beatsPerPulse must be an integer between 1 and 10000");
416
+ }
417
+ if (config.checkinIntervalSec !== void 0 && (!Number.isFinite(config.checkinIntervalSec) || config.checkinIntervalSec < 10 || config.checkinIntervalSec > 86400)) {
418
+ throw new ValidationError("BeatAgentConfig.checkinIntervalSec must be between 10 and 86400");
419
+ }
75
420
  this.config = {
76
421
  beatsPerPulse: 10,
77
422
  checkinIntervalSec: 300,
423
+ heartbeatIntervalSec: 300,
78
424
  onPulse: () => {
79
425
  },
80
426
  onCheckin: () => {
81
427
  },
428
+ onHeartbeat: () => {
429
+ },
82
430
  onError: () => {
83
431
  },
84
432
  onStatusChange: () => {
@@ -127,26 +475,42 @@ var BeatAgent = class {
127
475
  }
128
476
  // ── PULSE (COMPUTE BEATS) ──
129
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
+ *
130
481
  * Compute N beats locally (VDF hash chain).
131
- * This is the "heartbeat" — proof that the agent has lived
132
- * through a specific window of computational time.
133
482
  */
134
483
  pulse(count) {
135
- const n = count || this.config.beatsPerPulse;
136
- if (!this.latestBeat) {
137
- throw new Error("Beat chain not initialized. Call init() first.");
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.");
138
487
  }
139
488
  if (this.status !== "active") {
140
- throw new Error(`Cannot pulse in status '${this.status}'. Use resync() if frozen.`);
489
+ throw new StateError(`Cannot pulse: agent is ${this.status}.`, this.status);
490
+ }
491
+ if (count !== void 0 && (!Number.isInteger(count) || count < 1 || count > 1e4)) {
492
+ throw new ValidationError("pulse count must be an integer between 1 and 10000");
493
+ }
494
+ return this.computeBeats(count);
495
+ }
496
+ /** Internal beat computation — no status check. Used by both pulse() and resync(). */
497
+ computeBeats(count, onProgress) {
498
+ const n = count || this.config.beatsPerPulse;
499
+ if (!this.latestBeat) {
500
+ throw new StateError("Beat chain not initialized. Call init() first.", "uninitialized", "AGENT_NOT_INITIALIZED" /* AGENT_NOT_INITIALIZED */);
141
501
  }
142
502
  const newBeats = [];
143
503
  let prevHash = this.latestBeat.hash;
144
504
  let startIndex = this.latestBeat.index + 1;
145
505
  const t0 = Date.now();
506
+ const progressInterval = Math.max(1, Math.floor(n / 10));
146
507
  for (let i = 0; i < n; i++) {
147
508
  const beat = computeBeat(prevHash, startIndex + i, this.difficulty, void 0, this.globalAnchorHash || void 0);
148
509
  newBeats.push(beat);
149
510
  prevHash = beat.hash;
511
+ if (onProgress && (i + 1) % progressInterval === 0) {
512
+ onProgress(i + 1, n);
513
+ }
150
514
  }
151
515
  const elapsed = Date.now() - t0;
152
516
  this.chain.push(...newBeats);
@@ -161,27 +525,31 @@ var BeatAgent = class {
161
525
  }
162
526
  // ── CHECK-IN ──
163
527
  /**
164
- * Submit a Beat proof to the registry.
165
- *
166
- * "To remain on the Whitelist, an agent must periodically
167
- * 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.
168
530
  */
169
531
  async checkin() {
170
- if (!this.latestBeat || this.totalBeats === this.lastCheckinBeat) {
532
+ console.warn("[Provenonce SDK] checkin() is deprecated. Use heartbeat() instead (Phase 2).");
533
+ if (!this.latestBeat || this.latestBeat.index <= this.lastCheckinBeat) {
534
+ this.log("No new beats since last check-in. Call pulse() first.");
171
535
  return { ok: true, total_beats: this.totalBeats };
172
536
  }
173
537
  try {
538
+ const fromBeat = this.lastCheckinBeat;
539
+ const toBeat = this.latestBeat.index;
174
540
  const spotChecks = [];
175
- const available = this.chain.filter((b) => b.index > this.lastCheckinBeat);
176
- const sampleCount = Math.min(5, available.length);
541
+ const toBeatEntry = this.chain.find((b) => b.index === toBeat);
542
+ if (toBeatEntry) {
543
+ spotChecks.push({ index: toBeatEntry.index, hash: toBeatEntry.hash, prev: toBeatEntry.prev, nonce: toBeatEntry.nonce });
544
+ }
545
+ const available = this.chain.filter((b) => b.index > this.lastCheckinBeat && b.index !== toBeat);
546
+ const sampleCount = Math.min(4, available.length);
177
547
  for (let i = 0; i < sampleCount; i++) {
178
548
  const idx = Math.floor(Math.random() * available.length);
179
549
  const beat = available[idx];
180
550
  spotChecks.push({ index: beat.index, hash: beat.hash, prev: beat.prev, nonce: beat.nonce });
181
551
  available.splice(idx, 1);
182
552
  }
183
- const fromBeat = this.lastCheckinBeat;
184
- const toBeat = this.latestBeat.index;
185
553
  const fromHash = this.chain.find((b) => b.index === fromBeat)?.hash || this.genesisHash;
186
554
  const toHash = this.latestBeat.hash;
187
555
  const res = await this.api("POST", "/api/v1/agent/checkin", {
@@ -215,52 +583,57 @@ var BeatAgent = class {
215
583
  // ── AUTONOMOUS HEARTBEAT ──
216
584
  /**
217
585
  * Start the autonomous heartbeat loop.
218
- * Computes beats continuously and checks in periodically.
219
- * 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).
220
590
  */
221
- startHeartbeat() {
591
+ startHeartbeat(paymentTxFn) {
222
592
  if (this.heartbeatInterval) {
223
593
  this.log("Heartbeat already running.");
224
594
  return;
225
595
  }
226
- if (this.status !== "active") {
227
- 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);
228
598
  }
229
- this.log("\u2661 Starting heartbeat...");
599
+ const intervalSec = this.config.heartbeatIntervalSec || this.config.checkinIntervalSec || 300;
600
+ this.log(`Starting heartbeat (interval: ${intervalSec}s)...`);
601
+ let consecutiveErrors = 0;
602
+ let skipCount = 0;
230
603
  this.heartbeatInterval = setInterval(async () => {
604
+ if (skipCount > 0) {
605
+ skipCount--;
606
+ return;
607
+ }
231
608
  try {
232
- this.pulse();
233
- const beatsSinceCheckin = this.latestBeat.index - this.lastCheckinBeat;
234
- const shouldCheckin = beatsSinceCheckin >= this.config.beatsPerPulse * 5;
235
- if (shouldCheckin) {
236
- await this.checkin();
237
- await this.syncGlobal();
238
- }
609
+ const paymentTx = paymentTxFn ? await paymentTxFn() : "devnet-skip";
610
+ await this.heartbeat(paymentTx);
611
+ consecutiveErrors = 0;
239
612
  } catch (err) {
613
+ consecutiveErrors++;
240
614
  this.config.onError(err, "heartbeat");
615
+ skipCount = Math.min(32, Math.pow(2, consecutiveErrors - 1));
616
+ this.log(`Heartbeat error #${consecutiveErrors}, backing off ${skipCount} ticks`);
241
617
  }
242
- }, this.config.checkinIntervalSec * 1e3 / 10);
618
+ }, intervalSec * 1e3);
243
619
  }
244
620
  /**
245
- * Stop the heartbeat. Agent's time "freezes."
246
- * Must call resync() when waking up.
621
+ * Stop the heartbeat loop.
247
622
  */
248
623
  stopHeartbeat() {
249
624
  if (this.heartbeatInterval) {
250
625
  clearInterval(this.heartbeatInterval);
251
626
  this.heartbeatInterval = null;
252
- this.log("\u2661 Heartbeat stopped. Time frozen.");
627
+ this.log("Heartbeat stopped.");
253
628
  }
254
629
  }
255
630
  // ── RE-SYNC ──
256
631
  /**
257
- * Re-sync after being offline/frozen.
258
- *
259
- * "When an agent powers down, its time 'freezes.' Upon waking,
260
- * it must perform a Re-Sync Challenge with the Registry to
261
- * 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.
262
634
  */
263
635
  async resync() {
636
+ console.warn("[Provenonce SDK] resync() is deprecated (D-67). Use heartbeat() to resume (Phase 2).");
264
637
  try {
265
638
  this.log("Requesting re-sync challenge...");
266
639
  const challenge = await this.api("POST", "/api/v1/agent/resync", {
@@ -272,12 +645,13 @@ var BeatAgent = class {
272
645
  const required = challenge.challenge.required_beats;
273
646
  this.difficulty = challenge.challenge.difficulty;
274
647
  this.log(`Re-sync challenge: compute ${required} beats at D=${this.difficulty}`);
648
+ await this.syncGlobal();
275
649
  const startHash = challenge.challenge.start_from_hash;
276
650
  const startBeat = challenge.challenge.start_from_beat;
277
651
  this.latestBeat = { index: startBeat, hash: startHash, prev: "", timestamp: Date.now() };
278
652
  this.chain = [this.latestBeat];
279
653
  const t0 = Date.now();
280
- this.pulse(required);
654
+ this.computeBeats(required);
281
655
  const elapsed = Date.now() - t0;
282
656
  this.log(`Re-sync beats computed in ${elapsed}ms`);
283
657
  const proof = await this.api("POST", "/api/v1/agent/resync", {
@@ -290,7 +664,15 @@ var BeatAgent = class {
290
664
  to_hash: this.latestBeat.hash,
291
665
  beats_computed: required,
292
666
  global_anchor: challenge.challenge.sync_to_global,
293
- spot_checks: this.chain.filter((_, i) => i % Math.ceil(required / 5) === 0).slice(0, 5).map((b) => ({ index: b.index, hash: b.hash, prev: b.prev, nonce: b.nonce }))
667
+ anchor_hash: this.globalAnchorHash || void 0,
668
+ spot_checks: (() => {
669
+ const toBeatEntry = this.chain.find((b) => b.index === this.latestBeat.index);
670
+ const available = this.chain.filter((b) => b.index !== this.latestBeat.index && b.index > startBeat);
671
+ const step = Math.max(1, Math.ceil(available.length / 5));
672
+ const others = available.filter((_, i) => i % step === 0).slice(0, 4);
673
+ const checks = toBeatEntry ? [toBeatEntry, ...others] : others;
674
+ return checks.map((b) => ({ index: b.index, hash: b.hash, prev: b.prev, nonce: b.nonce }));
675
+ })()
294
676
  }
295
677
  });
296
678
  if (proof.ok) {
@@ -313,6 +695,14 @@ var BeatAgent = class {
313
695
  */
314
696
  async requestSpawn(childName, childHash) {
315
697
  try {
698
+ if (childName !== void 0) {
699
+ if (typeof childName !== "string" || childName.trim().length === 0) {
700
+ throw new ValidationError("childName must be a non-empty string");
701
+ }
702
+ if (childName.length > 64) {
703
+ throw new ValidationError("childName must be 64 characters or fewer");
704
+ }
705
+ }
316
706
  const res = await this.api("POST", "/api/v1/agent/spawn", {
317
707
  child_name: childName,
318
708
  child_hash: childHash
@@ -328,6 +718,147 @@ var BeatAgent = class {
328
718
  throw err;
329
719
  }
330
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 identityClass - 'narrow_task' (0.05 SOL), 'autonomous' (0.15 SOL), or 'orchestrator' (0.35 SOL)
727
+ * @param paymentTx - Solana transaction signature proving payment. Use 'devnet-skip' on devnet.
728
+ */
729
+ async purchaseSigil(identityClass, paymentTx) {
730
+ if (!identityClass || !["narrow_task", "autonomous", "orchestrator"].includes(identityClass)) {
731
+ throw new ValidationError("identityClass must be narrow_task, autonomous, or orchestrator");
732
+ }
733
+ if (!paymentTx || typeof paymentTx !== "string") {
734
+ throw new ValidationError('paymentTx is required (Solana transaction signature or "devnet-skip")');
735
+ }
736
+ try {
737
+ const res = await this.api("POST", "/api/v1/sigil", {
738
+ identity_class: identityClass,
739
+ payment_tx: paymentTx
740
+ });
741
+ if (res.lineage_proof) {
742
+ this.cachedProof = res.lineage_proof;
743
+ }
744
+ this.log(`SIGIL purchased: ${identityClass}`);
745
+ this.config.onStatusChange("sigil_issued", { identity_class: identityClass });
746
+ return {
747
+ ok: true,
748
+ sigil: res.sigil,
749
+ lineage_proof: res.lineage_proof,
750
+ fee: res.fee
751
+ };
752
+ } catch (err) {
753
+ this.config.onError(err, "purchaseSigil");
754
+ return { ok: false, error: err.message };
755
+ }
756
+ }
757
+ /**
758
+ * Send a paid heartbeat to the registry.
759
+ * Requires a SIGIL. Returns a signed lineage proof.
760
+ * This is the Phase 2 replacement for pulse() + checkin().
761
+ *
762
+ * @param paymentTx - Solana transaction signature. Omit or 'devnet-skip' on devnet.
763
+ * @param globalAnchor - Optional: the global anchor index to reference.
764
+ */
765
+ async heartbeat(paymentTx, globalAnchor) {
766
+ try {
767
+ const res = await this.api("POST", "/api/v1/agent/heartbeat", {
768
+ payment_tx: paymentTx || "devnet-skip",
769
+ global_anchor: globalAnchor
770
+ });
771
+ if (res.lineage_proof) {
772
+ this.cachedProof = res.lineage_proof;
773
+ }
774
+ if (res.ok) {
775
+ this.status = "active";
776
+ const onHb = this.config.onHeartbeat || this.config.onCheckin;
777
+ if (onHb) onHb(res);
778
+ this.log(`Heartbeat accepted: epoch=${res.billing_epoch}, count=${res.heartbeat_count_epoch}`);
779
+ }
780
+ return {
781
+ ok: res.ok,
782
+ lineage_proof: res.lineage_proof,
783
+ heartbeat_count_epoch: res.heartbeat_count_epoch,
784
+ billing_epoch: res.billing_epoch,
785
+ current_beat: res.current_beat,
786
+ fee: res.fee
787
+ };
788
+ } catch (err) {
789
+ this.config.onError(err, "heartbeat");
790
+ return { ok: false, error: err.message };
791
+ }
792
+ }
793
+ /**
794
+ * Reissue a lineage proof. "Reprint, not a renewal."
795
+ * Does NOT create a new lineage event.
796
+ *
797
+ * @param paymentTx - Solana transaction signature. Omit or 'devnet-skip' on devnet.
798
+ */
799
+ async reissueProof(paymentTx) {
800
+ try {
801
+ const res = await this.api("POST", "/api/v1/agent/reissue-proof", {
802
+ payment_tx: paymentTx || "devnet-skip"
803
+ });
804
+ if (res.lineage_proof) {
805
+ this.cachedProof = res.lineage_proof;
806
+ }
807
+ return { ok: true, lineage_proof: res.lineage_proof };
808
+ } catch (err) {
809
+ this.config.onError(err, "reissueProof");
810
+ return { ok: false, error: err.message };
811
+ }
812
+ }
813
+ /**
814
+ * Get the latest cached lineage proof (no network call).
815
+ * Returns null if no proof has been obtained yet.
816
+ */
817
+ getLatestProof() {
818
+ return this.cachedProof;
819
+ }
820
+ /**
821
+ * Get the agent's passport (alias for getLatestProof).
822
+ * The passport is the agent's portable, offline-verifiable credential.
823
+ * Returns null if no passport has been issued yet (requires SIGIL + heartbeat).
824
+ */
825
+ getPassport() {
826
+ return this.cachedProof;
827
+ }
828
+ /**
829
+ * Verify a lineage proof locally using the authority public key.
830
+ * Offline verification — no API call, no SOL cost.
831
+ *
832
+ * @param proof - The LineageProof to verify
833
+ * @param authorityPubKeyHex - 32-byte hex-encoded Ed25519 public key from /.well-known/provenonce-authority.json
834
+ */
835
+ static verifyProofLocally(proof, authorityPubKeyHex) {
836
+ try {
837
+ if (Date.now() > proof.valid_until) {
838
+ return false;
839
+ }
840
+ const canonical = JSON.stringify({
841
+ agent_hash: proof.agent_hash,
842
+ agent_public_key: proof.agent_public_key,
843
+ identity_class: proof.identity_class,
844
+ registered_at_beat: proof.registered_at_beat,
845
+ sigil_issued_at_beat: proof.sigil_issued_at_beat,
846
+ last_heartbeat_beat: proof.last_heartbeat_beat,
847
+ lineage_chain_hash: proof.lineage_chain_hash,
848
+ issued_at: proof.issued_at,
849
+ valid_until: proof.valid_until
850
+ });
851
+ const pubBytes = Buffer.from(authorityPubKeyHex, "hex");
852
+ if (pubBytes.length !== 32) return false;
853
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
854
+ const pubKeyDer = Buffer.concat([ED25519_SPKI_PREFIX, pubBytes]);
855
+ const keyObject = (0, import_crypto.createPublicKey)({ key: pubKeyDer, format: "der", type: "spki" });
856
+ const sigBuffer = Buffer.from(proof.provenonce_signature, "hex");
857
+ return (0, import_crypto.verify)(null, Buffer.from(canonical), keyObject, sigBuffer);
858
+ } catch {
859
+ return false;
860
+ }
861
+ }
331
862
  // ── STATUS ──
332
863
  /**
333
864
  * Get this agent's full beat status from the registry.
@@ -357,7 +888,10 @@ var BeatAgent = class {
357
888
  // ── INTERNALS ──
358
889
  async syncGlobal() {
359
890
  try {
360
- const res = await fetch(`${this.config.registryUrl}/api/v1/beat/anchor`);
891
+ const controller = new AbortController();
892
+ const timeout = setTimeout(() => controller.abort(), 15e3);
893
+ const res = await fetch(`${this.config.registryUrl}/api/v1/beat/anchor`, { signal: controller.signal });
894
+ clearTimeout(timeout);
361
895
  const data = await res.json();
362
896
  if (data.anchor) {
363
897
  this.globalBeat = data.anchor.beat_index;
@@ -375,23 +909,51 @@ var BeatAgent = class {
375
909
  this.totalBeats = res.total_beats;
376
910
  this.genesisHash = res.genesis_hash;
377
911
  this.status = res.status;
912
+ this.difficulty = res.difficulty || this.difficulty;
913
+ this.lastCheckinBeat = res.last_checkin_beat || 0;
914
+ if (!this.latestBeat && this.genesisHash) {
915
+ this.latestBeat = {
916
+ index: res.latest_beat || this.totalBeats,
917
+ hash: res.latest_hash || this.genesisHash,
918
+ prev: "0".repeat(64),
919
+ timestamp: Date.now()
920
+ };
921
+ this.chain = [this.latestBeat];
922
+ }
378
923
  }
379
924
  return res;
380
925
  }
381
926
  async api(method, path, body) {
382
- const res = await fetch(`${this.config.registryUrl}${path}`, {
383
- method,
384
- headers: {
385
- "Content-Type": "application/json",
386
- "Authorization": `Bearer ${this.config.apiKey}`
387
- },
388
- body: body ? JSON.stringify(body) : void 0
389
- });
390
- const data = await res.json();
391
- if (!res.ok && !data.ok && !data.already_initialized && !data.eligible) {
392
- throw new Error(data.error || `API ${res.status}: ${res.statusText}`);
927
+ const controller = new AbortController();
928
+ const timeout = setTimeout(() => controller.abort(), 3e4);
929
+ try {
930
+ const res = await fetch(`${this.config.registryUrl}${path}`, {
931
+ method,
932
+ headers: {
933
+ "Content-Type": "application/json",
934
+ "Authorization": `Bearer ${this.config.apiKey}`
935
+ },
936
+ body: body ? JSON.stringify(body) : void 0,
937
+ signal: controller.signal
938
+ });
939
+ let data;
940
+ try {
941
+ data = await res.json();
942
+ } catch {
943
+ throw new NetworkError(`API error: ${res.status} non-JSON response from ${path}`);
944
+ }
945
+ if (!res.ok && !data.ok && !data.already_initialized && !data.eligible) {
946
+ throw mapApiError(res.status, data, path);
947
+ }
948
+ return data;
949
+ } catch (err) {
950
+ if (err.name === "AbortError") {
951
+ throw new NetworkError(`Request timeout: ${method} ${path}`, "TIMEOUT" /* TIMEOUT */);
952
+ }
953
+ throw err;
954
+ } finally {
955
+ clearTimeout(timeout);
393
956
  }
394
- return data;
395
957
  }
396
958
  log(msg) {
397
959
  if (this.config.verbose) {
@@ -400,6 +962,12 @@ var BeatAgent = class {
400
962
  }
401
963
  };
402
964
  function computeBeatsLite(startHash, startIndex, count, difficulty = 1e3, anchorHash) {
965
+ if (!startHash || typeof startHash !== "string") {
966
+ throw new ValidationError("computeBeatsLite: startHash must be a non-empty string");
967
+ }
968
+ if (!Number.isInteger(count) || count < 1) {
969
+ throw new ValidationError("computeBeatsLite: count must be a positive integer");
970
+ }
403
971
  const t0 = Date.now();
404
972
  let prev = startHash;
405
973
  let lastBeat = null;
@@ -411,9 +979,20 @@ function computeBeatsLite(startHash, startIndex, count, difficulty = 1e3, anchor
411
979
  }
412
980
  // Annotate the CommonJS export names for ESM import in node:
413
981
  0 && (module.exports = {
982
+ AuthError,
414
983
  BeatAgent,
984
+ ErrorCode,
985
+ FrozenError,
986
+ NetworkError,
987
+ NotFoundError,
988
+ ProvenonceError,
989
+ RateLimitError,
990
+ ServerError,
991
+ StateError,
992
+ ValidationError,
415
993
  computeBeat,
416
994
  computeBeatsLite,
995
+ generateWalletKeypair,
417
996
  register
418
997
  });
419
998
  //# sourceMappingURL=index.js.map