@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.mjs CHANGED
@@ -1,5 +1,105 @@
1
1
  // src/beat-sdk.ts
2
- import { createHash } from "crypto";
2
+ import { createHash, generateKeyPairSync, sign, verify, createPrivateKey, createPublicKey } from "crypto";
3
+
4
+ // src/errors.ts
5
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
6
+ ErrorCode2["VALIDATION"] = "VALIDATION";
7
+ ErrorCode2["AUTH_INVALID"] = "AUTH_INVALID";
8
+ ErrorCode2["AUTH_MISSING"] = "AUTH_MISSING";
9
+ ErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
10
+ ErrorCode2["AGENT_FROZEN"] = "AGENT_FROZEN";
11
+ ErrorCode2["AGENT_NOT_INITIALIZED"] = "AGENT_NOT_INITIALIZED";
12
+ ErrorCode2["AGENT_WRONG_STATE"] = "AGENT_WRONG_STATE";
13
+ ErrorCode2["NOT_FOUND"] = "NOT_FOUND";
14
+ ErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
15
+ ErrorCode2["TIMEOUT"] = "TIMEOUT";
16
+ ErrorCode2["SERVER_ERROR"] = "SERVER_ERROR";
17
+ return ErrorCode2;
18
+ })(ErrorCode || {});
19
+ var ProvenonceError = class extends Error {
20
+ constructor(message, code, statusCode, details) {
21
+ super(message);
22
+ this.name = "ProvenonceError";
23
+ this.code = code;
24
+ this.statusCode = statusCode;
25
+ this.details = details;
26
+ Object.setPrototypeOf(this, new.target.prototype);
27
+ }
28
+ };
29
+ var ValidationError = class extends ProvenonceError {
30
+ constructor(message, details) {
31
+ super(message, "VALIDATION" /* VALIDATION */, void 0, details);
32
+ this.name = "ValidationError";
33
+ }
34
+ };
35
+ var AuthError = class extends ProvenonceError {
36
+ constructor(message, code = "AUTH_INVALID" /* AUTH_INVALID */, statusCode) {
37
+ super(message, code, statusCode);
38
+ this.name = "AuthError";
39
+ }
40
+ };
41
+ var RateLimitError = class extends ProvenonceError {
42
+ constructor(message, statusCode = 429, retryAfterMs) {
43
+ super(message, "RATE_LIMITED" /* RATE_LIMITED */, statusCode);
44
+ this.name = "RateLimitError";
45
+ this.retryAfterMs = retryAfterMs;
46
+ }
47
+ };
48
+ var FrozenError = class extends ProvenonceError {
49
+ constructor(message = "Agent is frozen. Use resync() to re-establish provenance.") {
50
+ super(message, "AGENT_FROZEN" /* AGENT_FROZEN */);
51
+ this.name = "FrozenError";
52
+ }
53
+ };
54
+ var StateError = class extends ProvenonceError {
55
+ constructor(message, currentState, code = "AGENT_WRONG_STATE" /* AGENT_WRONG_STATE */) {
56
+ super(message, code);
57
+ this.name = "StateError";
58
+ this.currentState = currentState;
59
+ }
60
+ };
61
+ var NotFoundError = class extends ProvenonceError {
62
+ constructor(message, statusCode = 404) {
63
+ super(message, "NOT_FOUND" /* NOT_FOUND */, statusCode);
64
+ this.name = "NotFoundError";
65
+ }
66
+ };
67
+ var NetworkError = class extends ProvenonceError {
68
+ constructor(message, code = "NETWORK_ERROR" /* NETWORK_ERROR */) {
69
+ super(message, code);
70
+ this.name = "NetworkError";
71
+ }
72
+ };
73
+ var ServerError = class extends ProvenonceError {
74
+ constructor(message, statusCode = 500) {
75
+ super(message, "SERVER_ERROR" /* SERVER_ERROR */, statusCode);
76
+ this.name = "ServerError";
77
+ }
78
+ };
79
+ function mapApiError(statusCode, body, path) {
80
+ const msg = typeof body.error === "string" ? body.error : `API error ${statusCode}`;
81
+ if (statusCode === 401 || statusCode === 403) {
82
+ const code = statusCode === 401 ? "AUTH_MISSING" /* AUTH_MISSING */ : "AUTH_INVALID" /* AUTH_INVALID */;
83
+ return new AuthError(msg, code, statusCode);
84
+ }
85
+ if (statusCode === 429) {
86
+ const retryAfter = typeof body.retry_after_ms === "number" ? body.retry_after_ms : void 0;
87
+ return new RateLimitError(msg, statusCode, retryAfter);
88
+ }
89
+ if (statusCode === 404) {
90
+ return new NotFoundError(msg, statusCode);
91
+ }
92
+ if (statusCode >= 500) {
93
+ return new ServerError(msg, statusCode);
94
+ }
95
+ const lowerMsg = msg.toLowerCase();
96
+ if (lowerMsg.includes("frozen")) {
97
+ return new FrozenError(msg);
98
+ }
99
+ return new ProvenonceError(msg, "SERVER_ERROR" /* SERVER_ERROR */, statusCode);
100
+ }
101
+
102
+ // src/beat-sdk.ts
3
103
  function computeBeat(prevHash, beatIndex, difficulty, nonce, anchorHash) {
4
104
  const timestamp = Date.now();
5
105
  const seed = anchorHash ? `${prevHash}:${beatIndex}:${nonce || ""}:${anchorHash}` : `${prevHash}:${beatIndex}:${nonce || ""}`;
@@ -9,26 +109,240 @@ function computeBeat(prevHash, beatIndex, difficulty, nonce, anchorHash) {
9
109
  }
10
110
  return { index: beatIndex, hash: current, prev: prevHash, timestamp, nonce, anchor_hash: anchorHash };
11
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
+ };
121
+ }
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");
128
+ }
12
129
  async function register(name, options) {
13
- const url = options?.registryUrl || "https://provenonce.vercel.app";
14
- const body = { name };
15
- const headers = { "Content-Type": "application/json" };
16
- if (options?.parentHash) {
17
- body.parent = options.parentHash;
130
+ if (!name || typeof name !== "string" || name.trim().length === 0) {
131
+ throw new ValidationError("name is required (must be a non-empty string)");
132
+ }
133
+ if (name.length > 64) {
134
+ throw new ValidationError("name must be 64 characters or fewer");
18
135
  }
19
- if (options?.parentApiKey) {
20
- headers["Authorization"] = `Bearer ${options.parentApiKey}`;
136
+ const url = options?.registryUrl || "https://provenonce.io";
137
+ try {
138
+ new URL(url);
139
+ } catch {
140
+ throw new ValidationError("registryUrl is not a valid URL");
21
141
  }
142
+ const headers = { "Content-Type": "application/json" };
22
143
  if (options?.registrationSecret) {
23
144
  headers["x-registration-secret"] = options.registrationSecret;
24
145
  }
146
+ if (options?.parentHash) {
147
+ if (options.parentApiKey) {
148
+ headers["Authorization"] = `Bearer ${options.parentApiKey}`;
149
+ }
150
+ const res2 = await fetch(`${url}/api/v1/register`, {
151
+ method: "POST",
152
+ headers,
153
+ body: JSON.stringify({ name, parent: options.parentHash, ...options.metadata && { metadata: options.metadata } })
154
+ });
155
+ let data2;
156
+ try {
157
+ data2 = await res2.json();
158
+ } catch {
159
+ throw new NetworkError(`Registration failed: ${res2.status} ${res2.statusText} (non-JSON response)`);
160
+ }
161
+ if (!res2.ok) throw mapApiError(res2.status, data2, "/api/v1/register");
162
+ return data2;
163
+ }
164
+ if (options?.walletChain === "ethereum") {
165
+ if (!options.walletAddress || !options.walletSignFn) {
166
+ throw new ValidationError("Ethereum registration requires walletAddress and walletSignFn");
167
+ }
168
+ if (!/^0x[0-9a-fA-F]{40}$/.test(options.walletAddress)) {
169
+ throw new ValidationError("walletAddress must be a valid Ethereum address (0x + 40 hex chars)");
170
+ }
171
+ const challengeRes = await fetch(`${url}/api/v1/register`, {
172
+ method: "POST",
173
+ headers,
174
+ body: JSON.stringify({ name, action: "challenge", wallet_chain: "ethereum" })
175
+ });
176
+ let challengeData;
177
+ try {
178
+ challengeData = await challengeRes.json();
179
+ } catch {
180
+ throw new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
181
+ }
182
+ if (!challengeRes.ok || !challengeData.nonce) {
183
+ throw mapApiError(challengeRes.status, challengeData, "/api/v1/register");
184
+ }
185
+ const nonce = challengeData.nonce;
186
+ const message = `provenonce-register-ethereum:${nonce}:${options.walletAddress}:${name}`;
187
+ const walletSignature = await options.walletSignFn(message);
188
+ const registerRes = await fetch(`${url}/api/v1/register`, {
189
+ method: "POST",
190
+ headers,
191
+ body: JSON.stringify({
192
+ name,
193
+ wallet_chain: "ethereum",
194
+ wallet_address: options.walletAddress,
195
+ wallet_signature: walletSignature,
196
+ wallet_nonce: nonce,
197
+ ...options.metadata && { metadata: options.metadata }
198
+ })
199
+ });
200
+ let data2;
201
+ try {
202
+ data2 = await registerRes.json();
203
+ } catch {
204
+ throw new NetworkError(`Ethereum registration failed: ${registerRes.status} (non-JSON response)`);
205
+ }
206
+ if (!registerRes.ok) throw mapApiError(registerRes.status, data2, "/api/v1/register");
207
+ data2.wallet = {
208
+ public_key: "",
209
+ secret_key: "",
210
+ address: data2.wallet?.address || options.walletAddress,
211
+ chain: "ethereum"
212
+ };
213
+ return data2;
214
+ }
215
+ if (options?.walletModel === "operator") {
216
+ if (!options.operatorWalletAddress || !options.operatorSignFn) {
217
+ throw new ValidationError("Operator registration requires operatorWalletAddress and operatorSignFn");
218
+ }
219
+ const challengeRes = await fetch(`${url}/api/v1/register`, {
220
+ method: "POST",
221
+ headers,
222
+ body: JSON.stringify({ name, action: "challenge", wallet_model: "operator" })
223
+ });
224
+ let challengeData;
225
+ try {
226
+ challengeData = await challengeRes.json();
227
+ } catch {
228
+ throw new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
229
+ }
230
+ if (!challengeRes.ok || !challengeData.nonce) {
231
+ throw mapApiError(challengeRes.status, challengeData, "/api/v1/register");
232
+ }
233
+ const nonce = challengeData.nonce;
234
+ const message = `provenonce-register-operator:${nonce}:${options.operatorWalletAddress}:${name}`;
235
+ const walletSignature = await options.operatorSignFn(message);
236
+ const registerRes = await fetch(`${url}/api/v1/register`, {
237
+ method: "POST",
238
+ headers,
239
+ body: JSON.stringify({
240
+ name,
241
+ wallet_model: "operator",
242
+ operator_wallet_address: options.operatorWalletAddress,
243
+ wallet_signature: walletSignature,
244
+ wallet_nonce: nonce,
245
+ ...options.metadata && { metadata: options.metadata }
246
+ })
247
+ });
248
+ let data2;
249
+ try {
250
+ data2 = await registerRes.json();
251
+ } catch {
252
+ throw new NetworkError(`Operator registration failed: ${registerRes.status} (non-JSON response)`);
253
+ }
254
+ if (!registerRes.ok) throw mapApiError(registerRes.status, data2, "/api/v1/register");
255
+ const addr = data2.wallet?.address || data2.wallet?.solana_address || options.operatorWalletAddress;
256
+ data2.wallet = {
257
+ public_key: "",
258
+ secret_key: "",
259
+ solana_address: addr,
260
+ address: addr,
261
+ chain: "solana"
262
+ };
263
+ return data2;
264
+ }
265
+ if (options?.walletModel === "self-custody" || options?.walletSecretKey) {
266
+ let walletKeys;
267
+ if (options?.walletSecretKey) {
268
+ const privRaw = Buffer.from(options.walletSecretKey, "hex");
269
+ const privKeyDer = Buffer.concat([ED25519_PKCS8_PREFIX, privRaw]);
270
+ const keyObject = createPrivateKey({ key: privKeyDer, format: "der", type: "pkcs8" });
271
+ const pubRaw = keyObject.export({ type: "spki", format: "der" }).subarray(12);
272
+ walletKeys = {
273
+ publicKey: Buffer.from(pubRaw).toString("hex"),
274
+ secretKey: options.walletSecretKey
275
+ };
276
+ } else {
277
+ walletKeys = generateWalletKeypair();
278
+ }
279
+ const challengeRes = await fetch(`${url}/api/v1/register`, {
280
+ method: "POST",
281
+ headers,
282
+ body: JSON.stringify({ name, action: "challenge" })
283
+ });
284
+ let challengeData;
285
+ try {
286
+ challengeData = await challengeRes.json();
287
+ } catch {
288
+ const err = new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
289
+ err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
290
+ throw err;
291
+ }
292
+ if (!challengeRes.ok || !challengeData.nonce) {
293
+ const err = mapApiError(challengeRes.status, challengeData, "/api/v1/register");
294
+ err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
295
+ throw err;
296
+ }
297
+ const nonce = challengeData.nonce;
298
+ const message = `provenonce-register:${nonce}:${walletKeys.publicKey}:${name}`;
299
+ const walletSignature = signMessage(walletKeys.secretKey, message);
300
+ const registerRes = await fetch(`${url}/api/v1/register`, {
301
+ method: "POST",
302
+ headers,
303
+ body: JSON.stringify({
304
+ name,
305
+ wallet_public_key: walletKeys.publicKey,
306
+ wallet_signature: walletSignature,
307
+ wallet_nonce: nonce,
308
+ ...options?.metadata && { metadata: options.metadata }
309
+ })
310
+ });
311
+ let data2;
312
+ try {
313
+ data2 = await registerRes.json();
314
+ } catch {
315
+ const err = new NetworkError(`Registration failed: ${registerRes.status} (non-JSON response)`);
316
+ err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
317
+ throw err;
318
+ }
319
+ if (!registerRes.ok) {
320
+ const err = mapApiError(registerRes.status, data2, "/api/v1/register");
321
+ err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
322
+ throw err;
323
+ }
324
+ const addr = data2.wallet?.address || data2.wallet?.solana_address || "";
325
+ data2.wallet = {
326
+ public_key: walletKeys.publicKey,
327
+ secret_key: walletKeys.secretKey,
328
+ solana_address: addr,
329
+ address: addr,
330
+ chain: "solana"
331
+ };
332
+ return data2;
333
+ }
25
334
  const res = await fetch(`${url}/api/v1/register`, {
26
335
  method: "POST",
27
336
  headers,
28
- body: JSON.stringify(body)
337
+ body: JSON.stringify({ name, ...options?.metadata && { metadata: options.metadata } })
29
338
  });
30
- const data = await res.json();
31
- if (!res.ok) throw new Error(data.error || "Registration failed");
339
+ let data;
340
+ try {
341
+ data = await res.json();
342
+ } catch {
343
+ throw new NetworkError(`Registration failed: ${res.status} ${res.statusText} (non-JSON response)`);
344
+ }
345
+ if (!res.ok) throw mapApiError(res.status, data, "/api/v1/register");
32
346
  return data;
33
347
  }
34
348
  var BeatAgent = class {
@@ -43,13 +357,36 @@ var BeatAgent = class {
43
357
  this.heartbeatInterval = null;
44
358
  this.globalBeat = 0;
45
359
  this.globalAnchorHash = "";
360
+ // ── PHASE 2: SIGIL + HEARTBEAT + PROOF ──
361
+ /** Cached lineage proof from the most recent heartbeat or SIGIL purchase */
362
+ this.cachedProof = null;
363
+ if (!config.apiKey || typeof config.apiKey !== "string") {
364
+ throw new ValidationError("BeatAgentConfig.apiKey is required (must be a non-empty string)");
365
+ }
366
+ if (!config.registryUrl || typeof config.registryUrl !== "string") {
367
+ throw new ValidationError("BeatAgentConfig.registryUrl is required (must be a non-empty string)");
368
+ }
369
+ try {
370
+ new URL(config.registryUrl);
371
+ } catch {
372
+ throw new ValidationError("BeatAgentConfig.registryUrl is not a valid URL");
373
+ }
374
+ if (config.beatsPerPulse !== void 0 && (!Number.isInteger(config.beatsPerPulse) || config.beatsPerPulse < 1 || config.beatsPerPulse > 1e4)) {
375
+ throw new ValidationError("BeatAgentConfig.beatsPerPulse must be an integer between 1 and 10000");
376
+ }
377
+ if (config.checkinIntervalSec !== void 0 && (!Number.isFinite(config.checkinIntervalSec) || config.checkinIntervalSec < 10 || config.checkinIntervalSec > 86400)) {
378
+ throw new ValidationError("BeatAgentConfig.checkinIntervalSec must be between 10 and 86400");
379
+ }
46
380
  this.config = {
47
381
  beatsPerPulse: 10,
48
382
  checkinIntervalSec: 300,
383
+ heartbeatIntervalSec: 300,
49
384
  onPulse: () => {
50
385
  },
51
386
  onCheckin: () => {
52
387
  },
388
+ onHeartbeat: () => {
389
+ },
53
390
  onError: () => {
54
391
  },
55
392
  onStatusChange: () => {
@@ -98,26 +435,42 @@ var BeatAgent = class {
98
435
  }
99
436
  // ── PULSE (COMPUTE BEATS) ──
100
437
  /**
438
+ * @deprecated Phase 2: VDF computation retired (D-68). Payment is the liveness mechanism.
439
+ * Use heartbeat() instead. This method will be removed in the next major version.
440
+ *
101
441
  * Compute N beats locally (VDF hash chain).
102
- * This is the "heartbeat" — proof that the agent has lived
103
- * through a specific window of computational time.
104
442
  */
105
443
  pulse(count) {
106
- const n = count || this.config.beatsPerPulse;
107
- if (!this.latestBeat) {
108
- throw new Error("Beat chain not initialized. Call init() first.");
444
+ console.warn("[Provenonce SDK] pulse() is deprecated. Use heartbeat() instead (Phase 2).");
445
+ if (this.status === "frozen") {
446
+ throw new FrozenError("Cannot pulse: agent is frozen. Use resync() to re-establish provenance.");
109
447
  }
110
448
  if (this.status !== "active") {
111
- throw new Error(`Cannot pulse in status '${this.status}'. Use resync() if frozen.`);
449
+ throw new StateError(`Cannot pulse: agent is ${this.status}.`, this.status);
450
+ }
451
+ if (count !== void 0 && (!Number.isInteger(count) || count < 1 || count > 1e4)) {
452
+ throw new ValidationError("pulse count must be an integer between 1 and 10000");
453
+ }
454
+ return this.computeBeats(count);
455
+ }
456
+ /** Internal beat computation — no status check. Used by both pulse() and resync(). */
457
+ computeBeats(count, onProgress) {
458
+ const n = count || this.config.beatsPerPulse;
459
+ if (!this.latestBeat) {
460
+ throw new StateError("Beat chain not initialized. Call init() first.", "uninitialized", "AGENT_NOT_INITIALIZED" /* AGENT_NOT_INITIALIZED */);
112
461
  }
113
462
  const newBeats = [];
114
463
  let prevHash = this.latestBeat.hash;
115
464
  let startIndex = this.latestBeat.index + 1;
116
465
  const t0 = Date.now();
466
+ const progressInterval = Math.max(1, Math.floor(n / 10));
117
467
  for (let i = 0; i < n; i++) {
118
468
  const beat = computeBeat(prevHash, startIndex + i, this.difficulty, void 0, this.globalAnchorHash || void 0);
119
469
  newBeats.push(beat);
120
470
  prevHash = beat.hash;
471
+ if (onProgress && (i + 1) % progressInterval === 0) {
472
+ onProgress(i + 1, n);
473
+ }
121
474
  }
122
475
  const elapsed = Date.now() - t0;
123
476
  this.chain.push(...newBeats);
@@ -132,27 +485,31 @@ var BeatAgent = class {
132
485
  }
133
486
  // ── CHECK-IN ──
134
487
  /**
135
- * Submit a Beat proof to the registry.
136
- *
137
- * "To remain on the Whitelist, an agent must periodically
138
- * submit a proof of its Local Beats to the Registry."
488
+ * @deprecated Phase 2: VDF check-in retired (D-68). Use heartbeat() instead.
489
+ * This method will be removed in the next major version.
139
490
  */
140
491
  async checkin() {
141
- if (!this.latestBeat || this.totalBeats === this.lastCheckinBeat) {
492
+ console.warn("[Provenonce SDK] checkin() is deprecated. Use heartbeat() instead (Phase 2).");
493
+ if (!this.latestBeat || this.latestBeat.index <= this.lastCheckinBeat) {
494
+ this.log("No new beats since last check-in. Call pulse() first.");
142
495
  return { ok: true, total_beats: this.totalBeats };
143
496
  }
144
497
  try {
498
+ const fromBeat = this.lastCheckinBeat;
499
+ const toBeat = this.latestBeat.index;
145
500
  const spotChecks = [];
146
- const available = this.chain.filter((b) => b.index > this.lastCheckinBeat);
147
- const sampleCount = Math.min(5, available.length);
501
+ const toBeatEntry = this.chain.find((b) => b.index === toBeat);
502
+ if (toBeatEntry) {
503
+ spotChecks.push({ index: toBeatEntry.index, hash: toBeatEntry.hash, prev: toBeatEntry.prev, nonce: toBeatEntry.nonce });
504
+ }
505
+ const available = this.chain.filter((b) => b.index > this.lastCheckinBeat && b.index !== toBeat);
506
+ const sampleCount = Math.min(4, available.length);
148
507
  for (let i = 0; i < sampleCount; i++) {
149
508
  const idx = Math.floor(Math.random() * available.length);
150
509
  const beat = available[idx];
151
510
  spotChecks.push({ index: beat.index, hash: beat.hash, prev: beat.prev, nonce: beat.nonce });
152
511
  available.splice(idx, 1);
153
512
  }
154
- const fromBeat = this.lastCheckinBeat;
155
- const toBeat = this.latestBeat.index;
156
513
  const fromHash = this.chain.find((b) => b.index === fromBeat)?.hash || this.genesisHash;
157
514
  const toHash = this.latestBeat.hash;
158
515
  const res = await this.api("POST", "/api/v1/agent/checkin", {
@@ -186,52 +543,57 @@ var BeatAgent = class {
186
543
  // ── AUTONOMOUS HEARTBEAT ──
187
544
  /**
188
545
  * Start the autonomous heartbeat loop.
189
- * Computes beats continuously and checks in periodically.
190
- * This is "keeping the agent alive" in Beat time.
546
+ * Phase 2: Sends paid heartbeats at regular intervals.
547
+ *
548
+ * @param paymentTxFn - Optional function that returns a payment tx for each heartbeat.
549
+ * If not provided, uses 'devnet-skip' (devnet only).
191
550
  */
192
- startHeartbeat() {
551
+ startHeartbeat(paymentTxFn) {
193
552
  if (this.heartbeatInterval) {
194
553
  this.log("Heartbeat already running.");
195
554
  return;
196
555
  }
197
- if (this.status !== "active") {
198
- throw new Error(`Cannot start heartbeat in status '${this.status}'.`);
556
+ if (this.status !== "active" && this.status !== "uninitialized") {
557
+ throw new StateError(`Cannot start heartbeat in status '${this.status}'.`, this.status);
199
558
  }
200
- this.log("\u2661 Starting heartbeat...");
559
+ const intervalSec = this.config.heartbeatIntervalSec || this.config.checkinIntervalSec || 300;
560
+ this.log(`Starting heartbeat (interval: ${intervalSec}s)...`);
561
+ let consecutiveErrors = 0;
562
+ let skipCount = 0;
201
563
  this.heartbeatInterval = setInterval(async () => {
564
+ if (skipCount > 0) {
565
+ skipCount--;
566
+ return;
567
+ }
202
568
  try {
203
- this.pulse();
204
- const beatsSinceCheckin = this.latestBeat.index - this.lastCheckinBeat;
205
- const shouldCheckin = beatsSinceCheckin >= this.config.beatsPerPulse * 5;
206
- if (shouldCheckin) {
207
- await this.checkin();
208
- await this.syncGlobal();
209
- }
569
+ const paymentTx = paymentTxFn ? await paymentTxFn() : "devnet-skip";
570
+ await this.heartbeat(paymentTx);
571
+ consecutiveErrors = 0;
210
572
  } catch (err) {
573
+ consecutiveErrors++;
211
574
  this.config.onError(err, "heartbeat");
575
+ skipCount = Math.min(32, Math.pow(2, consecutiveErrors - 1));
576
+ this.log(`Heartbeat error #${consecutiveErrors}, backing off ${skipCount} ticks`);
212
577
  }
213
- }, this.config.checkinIntervalSec * 1e3 / 10);
578
+ }, intervalSec * 1e3);
214
579
  }
215
580
  /**
216
- * Stop the heartbeat. Agent's time "freezes."
217
- * Must call resync() when waking up.
581
+ * Stop the heartbeat loop.
218
582
  */
219
583
  stopHeartbeat() {
220
584
  if (this.heartbeatInterval) {
221
585
  clearInterval(this.heartbeatInterval);
222
586
  this.heartbeatInterval = null;
223
- this.log("\u2661 Heartbeat stopped. Time frozen.");
587
+ this.log("Heartbeat stopped.");
224
588
  }
225
589
  }
226
590
  // ── RE-SYNC ──
227
591
  /**
228
- * Re-sync after being offline/frozen.
229
- *
230
- * "When an agent powers down, its time 'freezes.' Upon waking,
231
- * it must perform a Re-Sync Challenge with the Registry to
232
- * fill the 'Temporal Gap' and re-establish its provenance."
592
+ * @deprecated Phase 2: Resync retired (D-67). Dormancy resume is free — just call heartbeat().
593
+ * This method will be removed in the next major version.
233
594
  */
234
595
  async resync() {
596
+ console.warn("[Provenonce SDK] resync() is deprecated (D-67). Use heartbeat() to resume (Phase 2).");
235
597
  try {
236
598
  this.log("Requesting re-sync challenge...");
237
599
  const challenge = await this.api("POST", "/api/v1/agent/resync", {
@@ -243,12 +605,13 @@ var BeatAgent = class {
243
605
  const required = challenge.challenge.required_beats;
244
606
  this.difficulty = challenge.challenge.difficulty;
245
607
  this.log(`Re-sync challenge: compute ${required} beats at D=${this.difficulty}`);
608
+ await this.syncGlobal();
246
609
  const startHash = challenge.challenge.start_from_hash;
247
610
  const startBeat = challenge.challenge.start_from_beat;
248
611
  this.latestBeat = { index: startBeat, hash: startHash, prev: "", timestamp: Date.now() };
249
612
  this.chain = [this.latestBeat];
250
613
  const t0 = Date.now();
251
- this.pulse(required);
614
+ this.computeBeats(required);
252
615
  const elapsed = Date.now() - t0;
253
616
  this.log(`Re-sync beats computed in ${elapsed}ms`);
254
617
  const proof = await this.api("POST", "/api/v1/agent/resync", {
@@ -261,7 +624,15 @@ var BeatAgent = class {
261
624
  to_hash: this.latestBeat.hash,
262
625
  beats_computed: required,
263
626
  global_anchor: challenge.challenge.sync_to_global,
264
- 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 }))
627
+ anchor_hash: this.globalAnchorHash || void 0,
628
+ spot_checks: (() => {
629
+ const toBeatEntry = this.chain.find((b) => b.index === this.latestBeat.index);
630
+ const available = this.chain.filter((b) => b.index !== this.latestBeat.index && b.index > startBeat);
631
+ const step = Math.max(1, Math.ceil(available.length / 5));
632
+ const others = available.filter((_, i) => i % step === 0).slice(0, 4);
633
+ const checks = toBeatEntry ? [toBeatEntry, ...others] : others;
634
+ return checks.map((b) => ({ index: b.index, hash: b.hash, prev: b.prev, nonce: b.nonce }));
635
+ })()
265
636
  }
266
637
  });
267
638
  if (proof.ok) {
@@ -284,6 +655,14 @@ var BeatAgent = class {
284
655
  */
285
656
  async requestSpawn(childName, childHash) {
286
657
  try {
658
+ if (childName !== void 0) {
659
+ if (typeof childName !== "string" || childName.trim().length === 0) {
660
+ throw new ValidationError("childName must be a non-empty string");
661
+ }
662
+ if (childName.length > 64) {
663
+ throw new ValidationError("childName must be 64 characters or fewer");
664
+ }
665
+ }
287
666
  const res = await this.api("POST", "/api/v1/agent/spawn", {
288
667
  child_name: childName,
289
668
  child_hash: childHash
@@ -299,6 +678,147 @@ var BeatAgent = class {
299
678
  throw err;
300
679
  }
301
680
  }
681
+ /**
682
+ * Purchase a SIGIL (cryptographic identity) for this agent.
683
+ * SIGILs gate heartbeating, lineage proofs, and offline verification.
684
+ * One-time purchase — cannot be re-purchased.
685
+ *
686
+ * @param identityClass - 'narrow_task' (0.05 SOL), 'autonomous' (0.15 SOL), or 'orchestrator' (0.35 SOL)
687
+ * @param paymentTx - Solana transaction signature proving payment. Use 'devnet-skip' on devnet.
688
+ */
689
+ async purchaseSigil(identityClass, paymentTx) {
690
+ if (!identityClass || !["narrow_task", "autonomous", "orchestrator"].includes(identityClass)) {
691
+ throw new ValidationError("identityClass must be narrow_task, autonomous, or orchestrator");
692
+ }
693
+ if (!paymentTx || typeof paymentTx !== "string") {
694
+ throw new ValidationError('paymentTx is required (Solana transaction signature or "devnet-skip")');
695
+ }
696
+ try {
697
+ const res = await this.api("POST", "/api/v1/sigil", {
698
+ identity_class: identityClass,
699
+ payment_tx: paymentTx
700
+ });
701
+ if (res.lineage_proof) {
702
+ this.cachedProof = res.lineage_proof;
703
+ }
704
+ this.log(`SIGIL purchased: ${identityClass}`);
705
+ this.config.onStatusChange("sigil_issued", { identity_class: identityClass });
706
+ return {
707
+ ok: true,
708
+ sigil: res.sigil,
709
+ lineage_proof: res.lineage_proof,
710
+ fee: res.fee
711
+ };
712
+ } catch (err) {
713
+ this.config.onError(err, "purchaseSigil");
714
+ return { ok: false, error: err.message };
715
+ }
716
+ }
717
+ /**
718
+ * Send a paid heartbeat to the registry.
719
+ * Requires a SIGIL. Returns a signed lineage proof.
720
+ * This is the Phase 2 replacement for pulse() + checkin().
721
+ *
722
+ * @param paymentTx - Solana transaction signature. Omit or 'devnet-skip' on devnet.
723
+ * @param globalAnchor - Optional: the global anchor index to reference.
724
+ */
725
+ async heartbeat(paymentTx, globalAnchor) {
726
+ try {
727
+ const res = await this.api("POST", "/api/v1/agent/heartbeat", {
728
+ payment_tx: paymentTx || "devnet-skip",
729
+ global_anchor: globalAnchor
730
+ });
731
+ if (res.lineage_proof) {
732
+ this.cachedProof = res.lineage_proof;
733
+ }
734
+ if (res.ok) {
735
+ this.status = "active";
736
+ const onHb = this.config.onHeartbeat || this.config.onCheckin;
737
+ if (onHb) onHb(res);
738
+ this.log(`Heartbeat accepted: epoch=${res.billing_epoch}, count=${res.heartbeat_count_epoch}`);
739
+ }
740
+ return {
741
+ ok: res.ok,
742
+ lineage_proof: res.lineage_proof,
743
+ heartbeat_count_epoch: res.heartbeat_count_epoch,
744
+ billing_epoch: res.billing_epoch,
745
+ current_beat: res.current_beat,
746
+ fee: res.fee
747
+ };
748
+ } catch (err) {
749
+ this.config.onError(err, "heartbeat");
750
+ return { ok: false, error: err.message };
751
+ }
752
+ }
753
+ /**
754
+ * Reissue a lineage proof. "Reprint, not a renewal."
755
+ * Does NOT create a new lineage event.
756
+ *
757
+ * @param paymentTx - Solana transaction signature. Omit or 'devnet-skip' on devnet.
758
+ */
759
+ async reissueProof(paymentTx) {
760
+ try {
761
+ const res = await this.api("POST", "/api/v1/agent/reissue-proof", {
762
+ payment_tx: paymentTx || "devnet-skip"
763
+ });
764
+ if (res.lineage_proof) {
765
+ this.cachedProof = res.lineage_proof;
766
+ }
767
+ return { ok: true, lineage_proof: res.lineage_proof };
768
+ } catch (err) {
769
+ this.config.onError(err, "reissueProof");
770
+ return { ok: false, error: err.message };
771
+ }
772
+ }
773
+ /**
774
+ * Get the latest cached lineage proof (no network call).
775
+ * Returns null if no proof has been obtained yet.
776
+ */
777
+ getLatestProof() {
778
+ return this.cachedProof;
779
+ }
780
+ /**
781
+ * Get the agent's passport (alias for getLatestProof).
782
+ * The passport is the agent's portable, offline-verifiable credential.
783
+ * Returns null if no passport has been issued yet (requires SIGIL + heartbeat).
784
+ */
785
+ getPassport() {
786
+ return this.cachedProof;
787
+ }
788
+ /**
789
+ * Verify a lineage proof locally using the authority public key.
790
+ * Offline verification — no API call, no SOL cost.
791
+ *
792
+ * @param proof - The LineageProof to verify
793
+ * @param authorityPubKeyHex - 32-byte hex-encoded Ed25519 public key from /.well-known/provenonce-authority.json
794
+ */
795
+ static verifyProofLocally(proof, authorityPubKeyHex) {
796
+ try {
797
+ if (Date.now() > proof.valid_until) {
798
+ return false;
799
+ }
800
+ const canonical = JSON.stringify({
801
+ agent_hash: proof.agent_hash,
802
+ agent_public_key: proof.agent_public_key,
803
+ identity_class: proof.identity_class,
804
+ registered_at_beat: proof.registered_at_beat,
805
+ sigil_issued_at_beat: proof.sigil_issued_at_beat,
806
+ last_heartbeat_beat: proof.last_heartbeat_beat,
807
+ lineage_chain_hash: proof.lineage_chain_hash,
808
+ issued_at: proof.issued_at,
809
+ valid_until: proof.valid_until
810
+ });
811
+ const pubBytes = Buffer.from(authorityPubKeyHex, "hex");
812
+ if (pubBytes.length !== 32) return false;
813
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
814
+ const pubKeyDer = Buffer.concat([ED25519_SPKI_PREFIX, pubBytes]);
815
+ const keyObject = createPublicKey({ key: pubKeyDer, format: "der", type: "spki" });
816
+ const sigBuffer = Buffer.from(proof.provenonce_signature, "hex");
817
+ return verify(null, Buffer.from(canonical), keyObject, sigBuffer);
818
+ } catch {
819
+ return false;
820
+ }
821
+ }
302
822
  // ── STATUS ──
303
823
  /**
304
824
  * Get this agent's full beat status from the registry.
@@ -328,7 +848,10 @@ var BeatAgent = class {
328
848
  // ── INTERNALS ──
329
849
  async syncGlobal() {
330
850
  try {
331
- const res = await fetch(`${this.config.registryUrl}/api/v1/beat/anchor`);
851
+ const controller = new AbortController();
852
+ const timeout = setTimeout(() => controller.abort(), 15e3);
853
+ const res = await fetch(`${this.config.registryUrl}/api/v1/beat/anchor`, { signal: controller.signal });
854
+ clearTimeout(timeout);
332
855
  const data = await res.json();
333
856
  if (data.anchor) {
334
857
  this.globalBeat = data.anchor.beat_index;
@@ -346,23 +869,51 @@ var BeatAgent = class {
346
869
  this.totalBeats = res.total_beats;
347
870
  this.genesisHash = res.genesis_hash;
348
871
  this.status = res.status;
872
+ this.difficulty = res.difficulty || this.difficulty;
873
+ this.lastCheckinBeat = res.last_checkin_beat || 0;
874
+ if (!this.latestBeat && this.genesisHash) {
875
+ this.latestBeat = {
876
+ index: res.latest_beat || this.totalBeats,
877
+ hash: res.latest_hash || this.genesisHash,
878
+ prev: "0".repeat(64),
879
+ timestamp: Date.now()
880
+ };
881
+ this.chain = [this.latestBeat];
882
+ }
349
883
  }
350
884
  return res;
351
885
  }
352
886
  async api(method, path, body) {
353
- const res = await fetch(`${this.config.registryUrl}${path}`, {
354
- method,
355
- headers: {
356
- "Content-Type": "application/json",
357
- "Authorization": `Bearer ${this.config.apiKey}`
358
- },
359
- body: body ? JSON.stringify(body) : void 0
360
- });
361
- const data = await res.json();
362
- if (!res.ok && !data.ok && !data.already_initialized && !data.eligible) {
363
- throw new Error(data.error || `API ${res.status}: ${res.statusText}`);
887
+ const controller = new AbortController();
888
+ const timeout = setTimeout(() => controller.abort(), 3e4);
889
+ try {
890
+ const res = await fetch(`${this.config.registryUrl}${path}`, {
891
+ method,
892
+ headers: {
893
+ "Content-Type": "application/json",
894
+ "Authorization": `Bearer ${this.config.apiKey}`
895
+ },
896
+ body: body ? JSON.stringify(body) : void 0,
897
+ signal: controller.signal
898
+ });
899
+ let data;
900
+ try {
901
+ data = await res.json();
902
+ } catch {
903
+ throw new NetworkError(`API error: ${res.status} non-JSON response from ${path}`);
904
+ }
905
+ if (!res.ok && !data.ok && !data.already_initialized && !data.eligible) {
906
+ throw mapApiError(res.status, data, path);
907
+ }
908
+ return data;
909
+ } catch (err) {
910
+ if (err.name === "AbortError") {
911
+ throw new NetworkError(`Request timeout: ${method} ${path}`, "TIMEOUT" /* TIMEOUT */);
912
+ }
913
+ throw err;
914
+ } finally {
915
+ clearTimeout(timeout);
364
916
  }
365
- return data;
366
917
  }
367
918
  log(msg) {
368
919
  if (this.config.verbose) {
@@ -371,6 +922,12 @@ var BeatAgent = class {
371
922
  }
372
923
  };
373
924
  function computeBeatsLite(startHash, startIndex, count, difficulty = 1e3, anchorHash) {
925
+ if (!startHash || typeof startHash !== "string") {
926
+ throw new ValidationError("computeBeatsLite: startHash must be a non-empty string");
927
+ }
928
+ if (!Number.isInteger(count) || count < 1) {
929
+ throw new ValidationError("computeBeatsLite: count must be a positive integer");
930
+ }
374
931
  const t0 = Date.now();
375
932
  let prev = startHash;
376
933
  let lastBeat = null;
@@ -381,9 +938,20 @@ function computeBeatsLite(startHash, startIndex, count, difficulty = 1e3, anchor
381
938
  return { lastBeat, elapsed: Date.now() - t0 };
382
939
  }
383
940
  export {
941
+ AuthError,
384
942
  BeatAgent,
943
+ ErrorCode,
944
+ FrozenError,
945
+ NetworkError,
946
+ NotFoundError,
947
+ ProvenonceError,
948
+ RateLimitError,
949
+ ServerError,
950
+ StateError,
951
+ ValidationError,
385
952
  computeBeat,
386
953
  computeBeatsLite,
954
+ generateWalletKeypair,
387
955
  register
388
956
  };
389
957
  //# sourceMappingURL=index.mjs.map