@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/README.md +78 -21
- package/dist/index.d.mts +301 -23
- package/dist/index.d.ts +301 -23
- package/dist/index.js +396 -66
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +387 -67
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
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
|
|
171
|
+
throw new ValidationError("name is required (must be a non-empty string)");
|
|
62
172
|
}
|
|
63
173
|
if (name.length > 64) {
|
|
64
|
-
throw new
|
|
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
|
|
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
|
|
199
|
+
throw new NetworkError(`Registration failed: ${res2.status} ${res2.statusText} (non-JSON response)`);
|
|
90
200
|
}
|
|
91
|
-
if (!res2.ok) throw
|
|
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
|
|
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
|
|
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
|
|
220
|
+
throw new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
|
|
111
221
|
}
|
|
112
222
|
if (!challengeRes.ok || !challengeData.nonce) {
|
|
113
|
-
throw
|
|
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
|
|
244
|
+
throw new NetworkError(`Ethereum registration failed: ${registerRes.status} (non-JSON response)`);
|
|
135
245
|
}
|
|
136
|
-
if (!registerRes.ok) throw
|
|
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
|
|
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
|
|
268
|
+
throw new NetworkError(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
|
|
159
269
|
}
|
|
160
270
|
if (!challengeRes.ok || !challengeData.nonce) {
|
|
161
|
-
throw
|
|
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
|
|
292
|
+
throw new NetworkError(`Operator registration failed: ${registerRes.status} (non-JSON response)`);
|
|
183
293
|
}
|
|
184
|
-
if (!registerRes.ok) throw
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
383
|
+
throw new NetworkError(`Registration failed: ${res.status} ${res.statusText} (non-JSON response)`);
|
|
272
384
|
}
|
|
273
|
-
if (!res.ok) throw
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
465
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
485
|
-
|
|
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
|
-
},
|
|
618
|
+
}, intervalSec * 1e3);
|
|
499
619
|
}
|
|
500
620
|
/**
|
|
501
|
-
* Stop the heartbeat
|
|
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("
|
|
627
|
+
this.log("Heartbeat stopped.");
|
|
509
628
|
}
|
|
510
629
|
}
|
|
511
630
|
// ── RE-SYNC ──
|
|
512
631
|
/**
|
|
513
|
-
*
|
|
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
|
|
700
|
+
throw new ValidationError("childName must be a non-empty string");
|
|
584
701
|
}
|
|
585
702
|
if (childName.length > 64) {
|
|
586
|
-
throw new
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|