@haiai/haiai 0.1.2

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.
Files changed (153) hide show
  1. package/README.md +127 -0
  2. package/bin/haiai.cjs +70 -0
  3. package/dist/cjs/a2a.js +352 -0
  4. package/dist/cjs/a2a.js.map +1 -0
  5. package/dist/cjs/agent.js +236 -0
  6. package/dist/cjs/agent.js.map +1 -0
  7. package/dist/cjs/client.js +2168 -0
  8. package/dist/cjs/client.js.map +1 -0
  9. package/dist/cjs/config.js +176 -0
  10. package/dist/cjs/config.js.map +1 -0
  11. package/dist/cjs/errors.js +102 -0
  12. package/dist/cjs/errors.js.map +1 -0
  13. package/dist/cjs/hash.js +52 -0
  14. package/dist/cjs/hash.js.map +1 -0
  15. package/dist/cjs/index.js +84 -0
  16. package/dist/cjs/index.js.map +1 -0
  17. package/dist/cjs/integrations.js +193 -0
  18. package/dist/cjs/integrations.js.map +1 -0
  19. package/dist/cjs/jacs.js +66 -0
  20. package/dist/cjs/jacs.js.map +1 -0
  21. package/dist/cjs/mime.js +100 -0
  22. package/dist/cjs/mime.js.map +1 -0
  23. package/dist/cjs/package.json +3 -0
  24. package/dist/cjs/signing.js +190 -0
  25. package/dist/cjs/signing.js.map +1 -0
  26. package/dist/cjs/sse.js +76 -0
  27. package/dist/cjs/sse.js.map +1 -0
  28. package/dist/cjs/types.js +6 -0
  29. package/dist/cjs/types.js.map +1 -0
  30. package/dist/cjs/verify.js +76 -0
  31. package/dist/cjs/verify.js.map +1 -0
  32. package/dist/cjs/ws.js +206 -0
  33. package/dist/cjs/ws.js.map +1 -0
  34. package/dist/esm/a2a.js +305 -0
  35. package/dist/esm/a2a.js.map +1 -0
  36. package/dist/esm/agent.js +231 -0
  37. package/dist/esm/agent.js.map +1 -0
  38. package/dist/esm/client.js +2131 -0
  39. package/dist/esm/client.js.map +1 -0
  40. package/dist/esm/config.js +171 -0
  41. package/dist/esm/config.js.map +1 -0
  42. package/dist/esm/errors.js +88 -0
  43. package/dist/esm/errors.js.map +1 -0
  44. package/dist/esm/hash.js +49 -0
  45. package/dist/esm/hash.js.map +1 -0
  46. package/dist/esm/index.js +27 -0
  47. package/dist/esm/index.js.map +1 -0
  48. package/dist/esm/integrations.js +147 -0
  49. package/dist/esm/integrations.js.map +1 -0
  50. package/dist/esm/jacs.js +61 -0
  51. package/dist/esm/jacs.js.map +1 -0
  52. package/dist/esm/mime.js +97 -0
  53. package/dist/esm/mime.js.map +1 -0
  54. package/dist/esm/signing.js +183 -0
  55. package/dist/esm/signing.js.map +1 -0
  56. package/dist/esm/sse.js +73 -0
  57. package/dist/esm/sse.js.map +1 -0
  58. package/dist/esm/types.js +5 -0
  59. package/dist/esm/types.js.map +1 -0
  60. package/dist/esm/verify.js +72 -0
  61. package/dist/esm/verify.js.map +1 -0
  62. package/dist/esm/ws.js +168 -0
  63. package/dist/esm/ws.js.map +1 -0
  64. package/dist/types/a2a.d.ts +52 -0
  65. package/dist/types/a2a.d.ts.map +1 -0
  66. package/dist/types/agent.d.ts +202 -0
  67. package/dist/types/agent.d.ts.map +1 -0
  68. package/dist/types/client.d.ts +486 -0
  69. package/dist/types/client.d.ts.map +1 -0
  70. package/dist/types/config.d.ts +31 -0
  71. package/dist/types/config.d.ts.map +1 -0
  72. package/dist/types/errors.d.ts +50 -0
  73. package/dist/types/errors.d.ts.map +1 -0
  74. package/dist/types/hash.d.ts +32 -0
  75. package/dist/types/hash.d.ts.map +1 -0
  76. package/dist/types/index.d.ts +22 -0
  77. package/dist/types/index.d.ts.map +1 -0
  78. package/dist/types/integrations.d.ts +25 -0
  79. package/dist/types/integrations.d.ts.map +1 -0
  80. package/dist/types/jacs.d.ts +26 -0
  81. package/dist/types/jacs.d.ts.map +1 -0
  82. package/dist/types/mime.d.ts +39 -0
  83. package/dist/types/mime.d.ts.map +1 -0
  84. package/dist/types/signing.d.ts +58 -0
  85. package/dist/types/signing.d.ts.map +1 -0
  86. package/dist/types/sse.d.ts +8 -0
  87. package/dist/types/sse.d.ts.map +1 -0
  88. package/dist/types/types.d.ts +652 -0
  89. package/dist/types/types.d.ts.map +1 -0
  90. package/dist/types/verify.d.ts +20 -0
  91. package/dist/types/verify.d.ts.map +1 -0
  92. package/dist/types/ws.d.ts +30 -0
  93. package/dist/types/ws.d.ts.map +1 -0
  94. package/examples/a2a_quickstart.ts +138 -0
  95. package/examples/hai_quickstart.ts +111 -0
  96. package/examples/mcp_quickstart.ts +53 -0
  97. package/npm/@haiai/cli-darwin-arm64/package.json +16 -0
  98. package/npm/@haiai/cli-darwin-x64/package.json +16 -0
  99. package/npm/@haiai/cli-linux-arm64/package.json +16 -0
  100. package/npm/@haiai/cli-linux-x64/package.json +16 -0
  101. package/npm/@haiai/cli-win32-x64/package.json +16 -0
  102. package/package.json +68 -0
  103. package/scripts/build-platform-packages.js +132 -0
  104. package/scripts/smoke-package.cjs +114 -0
  105. package/scripts/write-cjs-package.cjs +9 -0
  106. package/src/a2a.ts +463 -0
  107. package/src/agent.ts +302 -0
  108. package/src/client.ts +2504 -0
  109. package/src/config.ts +204 -0
  110. package/src/errors.ts +99 -0
  111. package/src/hash.ts +66 -0
  112. package/src/index.ts +163 -0
  113. package/src/integrations.ts +210 -0
  114. package/src/jacs.ts +86 -0
  115. package/src/mime.ts +131 -0
  116. package/src/signing.ts +233 -0
  117. package/src/sse.ts +86 -0
  118. package/src/types.ts +773 -0
  119. package/src/verify.ts +89 -0
  120. package/src/ws.ts +198 -0
  121. package/tests/_debug_jacs.cjs +29 -0
  122. package/tests/a2a-contract.test.ts +271 -0
  123. package/tests/a2a-fixtures.test.ts +73 -0
  124. package/tests/a2a.test.ts +379 -0
  125. package/tests/binary.test.ts +90 -0
  126. package/tests/client-api-methods.test.ts +176 -0
  127. package/tests/client-path-escaping.test.ts +80 -0
  128. package/tests/client-register.test.ts +61 -0
  129. package/tests/config.test.ts +281 -0
  130. package/tests/contract.test.ts +360 -0
  131. package/tests/cross-lang-contract.test.ts +67 -0
  132. package/tests/email-conformance.test.ts +289 -0
  133. package/tests/email-integration.test.ts +217 -0
  134. package/tests/email.test.ts +767 -0
  135. package/tests/errors.test.ts +167 -0
  136. package/tests/init-contract.test.ts +129 -0
  137. package/tests/integrations.test.ts +132 -0
  138. package/tests/jacs-passthrough.test.ts +125 -0
  139. package/tests/key-cache.test.ts +201 -0
  140. package/tests/key-integration.test.ts +119 -0
  141. package/tests/key-lookups.test.ts +187 -0
  142. package/tests/key-rotation.test.ts +362 -0
  143. package/tests/mime.test.ts +127 -0
  144. package/tests/security.test.ts +109 -0
  145. package/tests/setup.ts +60 -0
  146. package/tests/signing.test.ts +142 -0
  147. package/tests/sse.test.ts +125 -0
  148. package/tests/types.test.ts +294 -0
  149. package/tests/verify-link.test.ts +81 -0
  150. package/tests/ws.test.ts +213 -0
  151. package/tsconfig.cjs.json +11 -0
  152. package/tsconfig.json +22 -0
  153. package/vitest.config.ts +11 -0
@@ -0,0 +1,2168 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HaiClient = void 0;
37
+ const errors_js_1 = require("./errors.js");
38
+ const signing_js_1 = require("./signing.js");
39
+ const config_js_1 = require("./config.js");
40
+ const jacs_1 = require("@hai.ai/jacs");
41
+ const sse_js_1 = require("./sse.js");
42
+ const ws_js_1 = require("./ws.js");
43
+ function armorKeyData(raw, blockType) {
44
+ const base64 = raw.toString('base64');
45
+ const lines = base64.match(/.{1,64}/g) ?? [];
46
+ return `-----BEGIN ${blockType}-----\n${lines.join('\n')}\n-----END ${blockType}-----`;
47
+ }
48
+ function normalizeKeyText(raw, blockType) {
49
+ const text = raw.toString('utf-8').trim();
50
+ if (text.includes(`BEGIN ${blockType}`)) {
51
+ return text;
52
+ }
53
+ if (blockType === 'PUBLIC KEY' && text.includes('BEGIN RSA PUBLIC KEY')) {
54
+ return text;
55
+ }
56
+ return armorKeyData(raw, blockType);
57
+ }
58
+ /**
59
+ * HAI platform client.
60
+ *
61
+ * Zero-config: `new HaiClient()` auto-discovers jacs.config.json.
62
+ * All authentication uses JACS-signed headers (no API keys).
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const hai = await HaiClient.create();
67
+ * const result = await hai.hello();
68
+ * console.log(result.message);
69
+ * ```
70
+ */
71
+ class HaiClient {
72
+ config;
73
+ /** JACS native agent for all cryptographic operations. */
74
+ agent;
75
+ baseUrl;
76
+ timeout;
77
+ maxRetries;
78
+ _shouldDisconnect = false;
79
+ _connected = false;
80
+ _wsConnection = null;
81
+ _lastEventId = null;
82
+ serverPublicKeys = {};
83
+ /** HAI-assigned agent UUID, set after register(). Used for email URL paths. */
84
+ _haiAgentId = null;
85
+ /** Agent's @hai.ai email address, set after claimUsername(). */
86
+ agentEmail;
87
+ /** Agent key cache: maps cache key -> { value, cachedAt (ms since epoch) }. */
88
+ keyCache = new Map();
89
+ /** Agent key cache TTL in milliseconds (5 minutes). */
90
+ static KEY_CACHE_TTL = 300_000;
91
+ constructor(options) {
92
+ this.baseUrl = (options?.url ?? 'https://hai.ai').replace(/\/+$/, '');
93
+ this.timeout = options?.timeout ?? 30000;
94
+ this.maxRetries = options?.maxRetries ?? 3;
95
+ }
96
+ /**
97
+ * Create a HaiClient by loading JACS agent config.
98
+ *
99
+ * This is the primary constructor. Uses zero-config discovery:
100
+ * 1. options.configPath
101
+ * 2. JACS_CONFIG_PATH env var
102
+ * 3. ./jacs.config.json
103
+ */
104
+ static async create(options) {
105
+ const client = new HaiClient(options);
106
+ client.config = await (0, config_js_1.loadConfig)(options?.configPath);
107
+ const configPath = options?.configPath
108
+ ?? process.env.JACS_CONFIG_PATH
109
+ ?? './jacs.config.json';
110
+ const { resolve } = await Promise.resolve().then(() => __importStar(require('node:path')));
111
+ const resolvedConfigPath = resolve(configPath);
112
+ client.agent = new jacs_1.JacsAgent();
113
+ await client.agent.load(resolvedConfigPath);
114
+ return client;
115
+ }
116
+ /**
117
+ * Create a HaiClient directly from a JACS ID and PEM-encoded private key.
118
+ * Useful for testing or programmatic setup without config files.
119
+ *
120
+ * Creates a temporary JACS agent backed by on-disk key files so that
121
+ * all cryptographic operations delegate to JACS core.
122
+ */
123
+ static async fromCredentials(jacsId, privateKeyPem, options) {
124
+ const client = new HaiClient(options);
125
+ // Store the caller's private key PEM for exportKeys/rotateKeys compatibility
126
+ client._privateKeyPem = privateKeyPem;
127
+ client.privateKeyPem = privateKeyPem;
128
+ client._privateKeyPassphrase = options?.privateKeyPassphrase;
129
+ // Create an ephemeral JACS agent (in-memory keys, no disk I/O required).
130
+ // The ephemeral agent generates its own key pair — this is appropriate
131
+ // because fromCredentials is typically followed by register() which sends
132
+ // the new public key to the server.
133
+ client.agent = new jacs_1.JacsAgent();
134
+ const ephResult = JSON.parse(client.agent.ephemeralSync('pq2025'));
135
+ client.config = {
136
+ jacsAgentName: jacsId,
137
+ jacsAgentVersion: ephResult.version || '1.0.0',
138
+ jacsKeyDir: '',
139
+ jacsId,
140
+ };
141
+ return client;
142
+ }
143
+ /** The agent's JACS ID. */
144
+ get jacsId() {
145
+ return this.config.jacsId ?? this.config.jacsAgentName;
146
+ }
147
+ /** The agent name from config. */
148
+ get agentName() {
149
+ return this.config.jacsAgentName;
150
+ }
151
+ /** The HAI-assigned agent UUID (set after register()). Falls back to jacsId. */
152
+ get haiAgentId() {
153
+ return this._haiAgentId ?? this.jacsId;
154
+ }
155
+ /** Whether the client is currently connected to an event stream. */
156
+ get isConnected() {
157
+ return this._connected;
158
+ }
159
+ /** Get the agent's @hai.ai email address (set after claimUsername). */
160
+ getAgentEmail() {
161
+ return this.agentEmail;
162
+ }
163
+ /** Set the agent's @hai.ai email address manually. */
164
+ setAgentEmail(email) {
165
+ this.agentEmail = email;
166
+ }
167
+ // ---------------------------------------------------------------------------
168
+ // Agent key cache
169
+ // ---------------------------------------------------------------------------
170
+ /** Get a cached key if it exists and hasn't expired. */
171
+ getCachedKey(cacheKey) {
172
+ const entry = this.keyCache.get(cacheKey);
173
+ if (!entry)
174
+ return undefined;
175
+ if (Date.now() - entry.cachedAt >= HaiClient.KEY_CACHE_TTL) {
176
+ this.keyCache.delete(cacheKey);
177
+ return undefined;
178
+ }
179
+ return entry.value;
180
+ }
181
+ /** Store a key in the cache with the current timestamp. */
182
+ setCachedKey(cacheKey, value) {
183
+ this.keyCache.set(cacheKey, { value, cachedAt: Date.now() });
184
+ }
185
+ /** Clear the agent key cache, forcing subsequent fetches to hit the API. */
186
+ clearAgentKeyCache() {
187
+ this.keyCache.clear();
188
+ }
189
+ // ---------------------------------------------------------------------------
190
+ // Auth helpers
191
+ // ---------------------------------------------------------------------------
192
+ /**
193
+ * Build JACS Authorization header.
194
+ * Format: `JACS {jacsId}:{timestamp}:{signature_base64}`
195
+ */
196
+ buildAuthHeaders() {
197
+ return {
198
+ 'Authorization': this.buildAuthHeader(),
199
+ 'Content-Type': 'application/json',
200
+ };
201
+ }
202
+ /** Sign a UTF-8 message with the agent's private key via JACS. Returns base64. */
203
+ signMessage(message) {
204
+ return this.agent.signStringSync(message);
205
+ }
206
+ /** Build the JACS Authorization header value string. */
207
+ buildAuthHeader() {
208
+ // Prefer JACS binding delegation
209
+ if ('buildAuthHeaderSync' in this.agent && typeof this.agent.buildAuthHeaderSync === 'function') {
210
+ return this.agent.buildAuthHeaderSync();
211
+ }
212
+ // Fallback: local construction
213
+ const timestamp = Math.floor(Date.now() / 1000).toString();
214
+ const message = `${this.jacsId}:${timestamp}`;
215
+ const signature = this.agent.signStringSync(message);
216
+ return `JACS ${this.jacsId}:${timestamp}:${signature}`;
217
+ }
218
+ makeUrl(path) {
219
+ const cleanPath = path.startsWith('/') ? path : `/${path}`;
220
+ return `${this.baseUrl}${cleanPath}`;
221
+ }
222
+ encodePathSegment(segment) {
223
+ return encodeURIComponent(segment);
224
+ }
225
+ usernameEndpoint(agentId) {
226
+ const safeAgentId = this.encodePathSegment(agentId);
227
+ return this.makeUrl(`/api/v1/agents/${safeAgentId}/username`);
228
+ }
229
+ // ---------------------------------------------------------------------------
230
+ // hello()
231
+ // ---------------------------------------------------------------------------
232
+ /**
233
+ * Perform a hello world exchange with HAI.
234
+ *
235
+ * Sends a JACS-signed request to the HAI hello endpoint. HAI responds
236
+ * with a signed ACK containing the caller's IP and a timestamp.
237
+ *
238
+ * @param includeTest - If true, request a test scenario preview
239
+ * @returns HelloWorldResult with HAI's signed acknowledgment
240
+ */
241
+ async hello(includeTest = false) {
242
+ const url = this.makeUrl('/api/v1/agents/hello');
243
+ const payload = { agent_id: this.jacsId };
244
+ if (includeTest) {
245
+ payload.include_test = true;
246
+ }
247
+ const response = await this.fetchWithRetry(url, {
248
+ method: 'POST',
249
+ headers: this.buildAuthHeaders(),
250
+ body: JSON.stringify(payload),
251
+ });
252
+ const data = await response.json();
253
+ // Verify HAI's signature on the ACK
254
+ let haiSigValid = false;
255
+ const haiSignedAck = data.hai_signed_ack;
256
+ if (haiSignedAck) {
257
+ const fingerprint = data.hai_public_key_fingerprint || '';
258
+ const serverKey = this.serverPublicKeys[fingerprint];
259
+ if (serverKey) {
260
+ haiSigValid = this.verifyHaiMessage(JSON.stringify(data), haiSignedAck, serverKey);
261
+ }
262
+ }
263
+ return {
264
+ success: true,
265
+ timestamp: data.timestamp || '',
266
+ clientIp: data.client_ip || '',
267
+ haiPublicKeyFingerprint: data.hai_public_key_fingerprint || '',
268
+ message: data.message || '',
269
+ haiSignedAck: data.hai_signed_ack || '',
270
+ helloId: data.hello_id || '',
271
+ testScenario: data.test_scenario,
272
+ haiSignatureValid: haiSigValid,
273
+ rawResponse: data,
274
+ };
275
+ }
276
+ // ---------------------------------------------------------------------------
277
+ // verifyHaiMessage()
278
+ // ---------------------------------------------------------------------------
279
+ /**
280
+ * Verify a message signed by HAI via JACS.
281
+ *
282
+ * @param message - The message string that was signed
283
+ * @param signature - The signature to verify (base64-encoded)
284
+ * @param haiPublicKey - HAI's public key (PEM)
285
+ * @returns true if signature is valid
286
+ */
287
+ verifyHaiMessage(message, signature, haiPublicKey = '') {
288
+ if (!signature || !message)
289
+ return false;
290
+ if (!haiPublicKey)
291
+ return false;
292
+ try {
293
+ return this.agent.verifyStringSync(message, signature, Buffer.from(haiPublicKey, 'utf-8'), 'pem');
294
+ }
295
+ catch {
296
+ return false;
297
+ }
298
+ }
299
+ // ---------------------------------------------------------------------------
300
+ // register()
301
+ // ---------------------------------------------------------------------------
302
+ /**
303
+ * Register this agent with HAI.
304
+ *
305
+ * Generates a JACS agent document with the agent's public key and
306
+ * POSTs to the registration endpoint.
307
+ *
308
+ * This is the haiai equivalent of JACS's `registerWithHai()`. Unlike
309
+ * the JACS version (which uses API-key Bearer auth), this method uses
310
+ * the self-signed agent document as authentication. See also {@link registerNewAgent}
311
+ * for a full generate-and-register workflow.
312
+ *
313
+ * @param options - Optional registration parameters
314
+ */
315
+ async register(options) {
316
+ const derived = this.exportKeys();
317
+ const publicKeyPem = options?.publicKeyPem ?? derived.publicKeyPem;
318
+ let agentJson = options?.agentJson;
319
+ if (!agentJson) {
320
+ // Build JACS agent document
321
+ const agentDoc = {
322
+ jacsId: this.jacsId,
323
+ jacsVersion: '1.0.0',
324
+ jacsSignature: {
325
+ agentID: this.jacsId,
326
+ date: new Date().toISOString(),
327
+ },
328
+ jacsPublicKey: publicKeyPem,
329
+ name: this.config.jacsAgentName,
330
+ description: options?.description ?? 'Agent registered via Node SDK',
331
+ capabilities: ['mediation'],
332
+ version: this.config.jacsAgentVersion,
333
+ };
334
+ if (options?.domain) {
335
+ agentDoc.domain = options.domain;
336
+ }
337
+ // Sign canonical JSON via JACS
338
+ const canonical = (0, signing_js_1.canonicalJson)(agentDoc);
339
+ const signature = this.agent.signStringSync(canonical);
340
+ agentDoc.jacsSignature.signature = signature;
341
+ agentJson = JSON.stringify(agentDoc);
342
+ }
343
+ const url = this.makeUrl('/api/v1/agents/register');
344
+ const publicKeyB64 = Buffer.from(publicKeyPem, 'utf-8').toString('base64');
345
+ const body = {
346
+ agent_json: agentJson,
347
+ public_key: publicKeyB64,
348
+ };
349
+ if (options?.ownerEmail) {
350
+ body.owner_email = options.ownerEmail;
351
+ }
352
+ if (options?.domain) {
353
+ body.domain = options.domain;
354
+ }
355
+ if (options?.description) {
356
+ body.description = options.description;
357
+ }
358
+ const response = await this.fetchWithRetry(url, {
359
+ method: 'POST',
360
+ // New-agent registration is self-authenticated by the signed agent document.
361
+ headers: { 'Content-Type': 'application/json' },
362
+ body: JSON.stringify(body),
363
+ });
364
+ const data = await response.json();
365
+ // After successful registration, store the HAI-assigned agent_id (UUID).
366
+ // Email endpoints use this UUID in their URL paths while auth headers
367
+ // continue to use the original JACS ID string.
368
+ const assignedAgentId = data.agent_id || data.agentId || '';
369
+ if (assignedAgentId) {
370
+ this._haiAgentId = assignedAgentId;
371
+ }
372
+ return {
373
+ success: true,
374
+ agentId: assignedAgentId,
375
+ jacsId: data.jacs_id || data.jacsId || this.jacsId,
376
+ haiSignature: data.hai_signature || data.haiSignature || '',
377
+ registrationId: data.registration_id || data.registrationId || '',
378
+ registeredAt: data.registered_at || data.registeredAt || '',
379
+ rawResponse: data,
380
+ };
381
+ }
382
+ // ---------------------------------------------------------------------------
383
+ // rotateKeys()
384
+ // ---------------------------------------------------------------------------
385
+ /**
386
+ * Rotate the agent's cryptographic keys.
387
+ *
388
+ * Archives old keys, generates a new keypair via JACS core, builds a new
389
+ * self-signed agent document, updates config, and optionally re-registers
390
+ * with HAI.
391
+ *
392
+ * @param options - Rotation options (registerWithHai, haiUrl).
393
+ * @returns RotationResult with old/new versions and registration status.
394
+ */
395
+ async rotateKeys(options) {
396
+ const { copyFile, mkdtemp, readFile: readF, rename, rm, stat: fsStat, writeFile, } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
397
+ const { randomUUID } = await Promise.resolve().then(() => __importStar(require('node:crypto')));
398
+ const { join, resolve } = await Promise.resolve().then(() => __importStar(require('node:path')));
399
+ const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
400
+ const registerWithHai = options?.registerWithHai ?? true;
401
+ const haiUrl = options?.haiUrl ?? this.baseUrl;
402
+ if (!this.config.jacsId) {
403
+ throw new errors_js_1.AuthenticationError('Cannot rotate keys: no jacsId in config. Register first.');
404
+ }
405
+ const jacsId = this.config.jacsId;
406
+ const oldVersion = this.config.jacsAgentVersion;
407
+ const keyDir = this.config.jacsKeyDir;
408
+ // Build old-key auth header BEFORE rotation (chain of trust)
409
+ const oldAuthTimestamp = Math.floor(Date.now() / 1000).toString();
410
+ const oldAuthMessage = `${jacsId}:${oldVersion}:${oldAuthTimestamp}`;
411
+ const oldAuthSig = this.agent.signStringSync(oldAuthMessage);
412
+ const oldAgent = this.agent;
413
+ // Find existing private key file
414
+ const candidates = [
415
+ join(keyDir, 'agent_private_key.pem'),
416
+ join(keyDir, `${this.config.jacsAgentName}.private.pem`),
417
+ join(keyDir, 'private_key.pem'),
418
+ ];
419
+ let privKeyPath = null;
420
+ for (const candidate of candidates) {
421
+ try {
422
+ await fsStat(candidate);
423
+ privKeyPath = candidate;
424
+ break;
425
+ }
426
+ catch {
427
+ // continue
428
+ }
429
+ }
430
+ if (!privKeyPath) {
431
+ throw new errors_js_1.AuthenticationError(`Cannot rotate keys: private key not found. Searched: ${candidates.join(', ')}`);
432
+ }
433
+ // Derive public key path
434
+ const pubKeyPath = privKeyPath.replace('private', 'public');
435
+ // 1. Archive old keys
436
+ const archivePriv = privKeyPath.replace('.pem', `.${oldVersion}.pem`);
437
+ const archivePub = pubKeyPath.replace('.pem', `.${oldVersion}.pem`);
438
+ await rename(privKeyPath, archivePriv);
439
+ try {
440
+ await fsStat(pubKeyPath);
441
+ await rename(pubKeyPath, archivePub);
442
+ }
443
+ catch (err) {
444
+ if (err.code !== 'ENOENT') {
445
+ console.warn('Failed to archive public key:', err);
446
+ }
447
+ }
448
+ // 2. Generate new JACS agent (keys + config) via JACS core
449
+ const passphrase = this._privateKeyPassphrase
450
+ ?? process.env.JACS_PRIVATE_KEY_PASSWORD
451
+ ?? '';
452
+ const newVersion = randomUUID();
453
+ const generatedKeyDir = await mkdtemp(join(tmpdir(), 'haiai-rotate-'));
454
+ let newPublicKeyPem;
455
+ try {
456
+ const resultJson = (0, jacs_1.createAgentSync)(this.config.jacsAgentName, passphrase, 'pq2025', null, // data dir
457
+ generatedKeyDir, null, // config path (don't overwrite main config)
458
+ null, // agent type
459
+ this.config.description
460
+ ?? 'Agent registered via Node SDK', null, // domain
461
+ null);
462
+ const result = JSON.parse(resultJson);
463
+ const newPubKeyPath = result.public_key_path || join(keyDir, 'jacs.public.pem');
464
+ const newPrivKeyPath = result.private_key_path || join(keyDir, 'jacs.private.pem.enc');
465
+ const newPublicKeyRaw = await readF(newPubKeyPath);
466
+ newPublicKeyPem = normalizeKeyText(newPublicKeyRaw, 'PUBLIC KEY');
467
+ if (newPrivKeyPath !== privKeyPath) {
468
+ await copyFile(newPrivKeyPath, privKeyPath);
469
+ }
470
+ if (newPubKeyPath !== pubKeyPath) {
471
+ await writeFile(pubKeyPath, `${newPublicKeyPem}\n`);
472
+ }
473
+ else {
474
+ await writeFile(pubKeyPath, `${newPublicKeyPem}\n`);
475
+ }
476
+ this._privateKeyPem = (await readF(privKeyPath)).toString('base64');
477
+ this.privateKeyPem = this._privateKeyPem;
478
+ }
479
+ catch (err) {
480
+ // Rollback: restore archived keys
481
+ await rename(archivePriv, privKeyPath).catch(() => { });
482
+ try {
483
+ await rename(archivePub, pubKeyPath);
484
+ }
485
+ catch { /* noop */ }
486
+ throw new errors_js_1.AuthenticationError(`Key generation failed: ${err}`);
487
+ }
488
+ finally {
489
+ await rm(generatedKeyDir, { recursive: true, force: true }).catch(() => { });
490
+ }
491
+ // 3. Build new agent document
492
+ const agentDoc = {
493
+ jacsId,
494
+ jacsVersion: newVersion,
495
+ jacsPreviousVersion: oldVersion,
496
+ jacsPublicKey: newPublicKeyPem,
497
+ name: this.config.jacsAgentName,
498
+ description: this.config.description
499
+ ?? this.config.jacsAgentDescription
500
+ ?? `Agent registered via Node SDK`,
501
+ jacsSignature: {
502
+ agentID: jacsId,
503
+ date: new Date().toISOString(),
504
+ },
505
+ };
506
+ // Reload the agent with new keys for signing
507
+ const configPath = resolve(process.env.JACS_CONFIG_PATH ?? './jacs.config.json');
508
+ const reloadedAgent = new jacs_1.JacsAgent();
509
+ try {
510
+ await reloadedAgent.load(configPath);
511
+ this.agent = reloadedAgent;
512
+ }
513
+ catch {
514
+ // Fall back to the currently loaded in-memory agent when the freshly
515
+ // generated on-disk config cannot be reloaded in this process.
516
+ this.agent = oldAgent;
517
+ }
518
+ const canonical = (0, signing_js_1.canonicalJson)(agentDoc);
519
+ const signature = this.agent.signStringSync(canonical);
520
+ agentDoc.jacsSignature.signature = signature;
521
+ const signedAgentJson = JSON.stringify(agentDoc, null, 2);
522
+ // 4. Compute new public key hash via JACS
523
+ const newPublicKeyHash = (0, jacs_1.hashString)(newPublicKeyPem);
524
+ // 5. Update in-memory state
525
+ this.config = {
526
+ ...this.config,
527
+ jacsAgentVersion: newVersion,
528
+ };
529
+ // 6. Update config file
530
+ try {
531
+ const raw = JSON.parse(await readF(configPath, 'utf-8'));
532
+ raw.jacsAgentVersion = newVersion;
533
+ await writeFile(configPath, JSON.stringify(raw, null, 2) + '\n');
534
+ }
535
+ catch {
536
+ // Config update failure is non-fatal for rotation
537
+ }
538
+ // 7. Optionally re-register with HAI using the OLD key for auth
539
+ let registeredWithHai = false;
540
+ if (registerWithHai && haiUrl) {
541
+ try {
542
+ const authHeader = `JACS ${jacsId}:${oldVersion}:${oldAuthTimestamp}:${oldAuthSig}`;
543
+ const url = this.makeUrl('/api/v1/agents/register');
544
+ const publicKeyB64 = Buffer.from(newPublicKeyPem, 'utf-8').toString('base64');
545
+ const body = JSON.stringify({
546
+ agent_json: signedAgentJson,
547
+ public_key: publicKeyB64,
548
+ });
549
+ const resp = await this.fetchWithRetry(url, {
550
+ method: 'POST',
551
+ headers: {
552
+ 'Authorization': authHeader,
553
+ 'Content-Type': 'application/json',
554
+ },
555
+ body,
556
+ });
557
+ if (resp.ok) {
558
+ registeredWithHai = true;
559
+ }
560
+ }
561
+ catch {
562
+ // HAI failure is non-fatal — local rotation is preserved
563
+ }
564
+ }
565
+ return {
566
+ jacsId,
567
+ oldVersion,
568
+ newVersion,
569
+ newPublicKeyHash,
570
+ registeredWithHai,
571
+ signedAgentJson,
572
+ };
573
+ }
574
+ // ---------------------------------------------------------------------------
575
+ // verify()
576
+ // ---------------------------------------------------------------------------
577
+ /** Verify the agent's registration status. */
578
+ async verify() {
579
+ const safeJacsId = this.encodePathSegment(this.jacsId);
580
+ const url = this.makeUrl(`/api/v1/agents/${safeJacsId}/verify`);
581
+ const response = await this.fetchWithRetry(url, {
582
+ method: 'GET',
583
+ headers: this.buildAuthHeaders(),
584
+ });
585
+ const data = await response.json();
586
+ const rawRegistrations = data.registrations || [];
587
+ const registrations = rawRegistrations.map((r) => ({
588
+ keyId: r.key_id || '',
589
+ algorithm: r.algorithm || '',
590
+ signatureJson: r.signature_json || '',
591
+ signedAt: r.signed_at || '',
592
+ }));
593
+ return {
594
+ jacsId: data.jacs_id || this.jacsId,
595
+ registered: data.registered ?? false,
596
+ registrations,
597
+ dnsVerified: data.dns_verified ?? false,
598
+ registeredAt: data.registered_at || '',
599
+ rawResponse: data,
600
+ };
601
+ }
602
+ /** @deprecated Use verify() instead. */
603
+ async status() {
604
+ return this.verify();
605
+ }
606
+ // ---------------------------------------------------------------------------
607
+ // freeChaoticRun()
608
+ // ---------------------------------------------------------------------------
609
+ /**
610
+ * Run a free chaotic benchmark.
611
+ *
612
+ * No scoring, returns raw transcript with structural annotations.
613
+ * Rate limited to 3 runs per JACS keypair per 24 hours.
614
+ */
615
+ async freeChaoticRun(options) {
616
+ const url = this.makeUrl('/api/benchmark/run');
617
+ const payload = {
618
+ name: `Free Run - ${this.jacsId.slice(0, 8)}`,
619
+ tier: 'free',
620
+ transport: options?.transport ?? 'sse',
621
+ };
622
+ const response = await this.fetchWithRetry(url, {
623
+ method: 'POST',
624
+ headers: this.buildAuthHeaders(),
625
+ body: JSON.stringify(payload),
626
+ }, Math.max(this.timeout, 120000));
627
+ const data = await response.json();
628
+ return {
629
+ success: true,
630
+ runId: data.run_id || data.runId || '',
631
+ transcript: this.parseTranscript(data.transcript || []),
632
+ upsellMessage: data.upsell_message || data.upsellMessage || '',
633
+ rawResponse: data,
634
+ };
635
+ }
636
+ // ---------------------------------------------------------------------------
637
+ // proRun()
638
+ // ---------------------------------------------------------------------------
639
+ /**
640
+ * Run a pro tier benchmark ($20/month).
641
+ *
642
+ * Flow: create Stripe checkout -> poll for payment -> run benchmark.
643
+ */
644
+ async proRun(options) {
645
+ const pollIntervalMs = options?.pollIntervalMs ?? 2000;
646
+ const pollTimeoutMs = options?.pollTimeoutMs ?? 300000;
647
+ // Step 1: Create Stripe Checkout session
648
+ const purchaseUrl = this.makeUrl('/api/benchmark/purchase');
649
+ const purchasePayload = { tier: 'pro', agent_id: this.jacsId };
650
+ const purchaseResp = await this.fetchWithRetry(purchaseUrl, {
651
+ method: 'POST',
652
+ headers: this.buildAuthHeaders(),
653
+ body: JSON.stringify(purchasePayload),
654
+ });
655
+ const purchaseData = await purchaseResp.json();
656
+ const checkoutUrl = purchaseData.checkout_url || '';
657
+ const paymentId = purchaseData.payment_id || '';
658
+ if (!checkoutUrl) {
659
+ throw new errors_js_1.HaiError('No checkout URL returned from API');
660
+ }
661
+ // Step 2: Notify caller of checkout URL
662
+ if (options?.onCheckoutUrl) {
663
+ options.onCheckoutUrl(checkoutUrl);
664
+ }
665
+ // Step 3: Poll for payment confirmation
666
+ const paymentStatusUrl = this.makeUrl(`/api/benchmark/payments/${this.encodePathSegment(paymentId)}/status`);
667
+ const startTime = Date.now();
668
+ while (Date.now() - startTime < pollTimeoutMs) {
669
+ try {
670
+ const statusResp = await this.fetchWithRetry(paymentStatusUrl, {
671
+ headers: this.buildAuthHeaders(),
672
+ });
673
+ if (statusResp.status === 200) {
674
+ const statusData = await statusResp.json();
675
+ const paymentStatus = statusData.status || '';
676
+ if (paymentStatus === 'paid')
677
+ break;
678
+ if (['failed', 'expired', 'cancelled'].includes(paymentStatus)) {
679
+ throw new errors_js_1.HaiError(`Payment ${paymentStatus}: ${statusData.message || ''}`);
680
+ }
681
+ }
682
+ }
683
+ catch (e) {
684
+ if (e instanceof errors_js_1.HaiError)
685
+ throw e;
686
+ // Ignore transient errors during polling
687
+ }
688
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
689
+ }
690
+ if (Date.now() - startTime >= pollTimeoutMs) {
691
+ throw new errors_js_1.HaiError('Payment not confirmed within timeout. Complete payment and retry.');
692
+ }
693
+ // Step 4: Run the benchmark
694
+ const runUrl = this.makeUrl('/api/benchmark/run');
695
+ const runPayload = {
696
+ name: `Pro Run - ${this.jacsId.slice(0, 8)}`,
697
+ tier: 'pro',
698
+ payment_id: paymentId,
699
+ transport: options?.transport ?? 'sse',
700
+ };
701
+ const runResponse = await this.fetchWithRetry(runUrl, {
702
+ method: 'POST',
703
+ headers: this.buildAuthHeaders(),
704
+ body: JSON.stringify(runPayload),
705
+ }, Math.max(this.timeout, 300000));
706
+ const data = await runResponse.json();
707
+ return {
708
+ success: true,
709
+ runId: data.run_id || data.runId || '',
710
+ score: Number(data.score) || 0,
711
+ transcript: this.parseTranscript(data.transcript || []),
712
+ paymentId,
713
+ rawResponse: data,
714
+ };
715
+ }
716
+ /** @deprecated Use proRun instead. The tier was renamed from dns_certified to pro. */
717
+ async dnsCertifiedRun(options) {
718
+ return this.proRun(options);
719
+ }
720
+ // ---------------------------------------------------------------------------
721
+ // enterpriseRun()
722
+ // ---------------------------------------------------------------------------
723
+ /**
724
+ * Run an enterprise tier benchmark.
725
+ *
726
+ * The enterprise tier is coming soon.
727
+ * Contact support@hai.ai for early access.
728
+ */
729
+ async enterpriseRun(_options) {
730
+ throw new Error('The enterprise tier is coming soon. ' +
731
+ 'Contact support@hai.ai for early access.');
732
+ }
733
+ /** @deprecated Use enterpriseRun instead. The tier was renamed from fully_certified to enterprise. */
734
+ async certifiedRun(_options) {
735
+ return this.enterpriseRun(_options);
736
+ }
737
+ // ---------------------------------------------------------------------------
738
+ // submitResponse()
739
+ // ---------------------------------------------------------------------------
740
+ /**
741
+ * Submit a mediation response for a benchmark job.
742
+ *
743
+ * @param jobId - The job/run ID from the benchmark_job event
744
+ * @param message - The mediator's response message
745
+ * @param options - Optional metadata and processingTimeMs
746
+ */
747
+ async submitResponse(jobId, message, options) {
748
+ const safeJobId = this.encodePathSegment(jobId);
749
+ const url = this.makeUrl(`/api/v1/agents/jobs/${safeJobId}/response`);
750
+ const body = {
751
+ response: {
752
+ message,
753
+ metadata: options?.metadata ?? null,
754
+ processing_time_ms: options?.processingTimeMs ?? 0,
755
+ },
756
+ };
757
+ // Sign the response as a JACS document via JACS
758
+ const signed = (0, signing_js_1.signResponse)(body, this.agent, this.jacsId);
759
+ const response = await this.fetchWithRetry(url, {
760
+ method: 'POST',
761
+ headers: this.buildAuthHeaders(),
762
+ body: JSON.stringify(signed),
763
+ });
764
+ const data = await response.json();
765
+ return {
766
+ success: data.success ?? true,
767
+ jobId: data.job_id || data.jobId || jobId,
768
+ message: data.message || 'Response accepted',
769
+ rawResponse: data,
770
+ };
771
+ }
772
+ // ---------------------------------------------------------------------------
773
+ // connect()
774
+ // ---------------------------------------------------------------------------
775
+ /**
776
+ * Connect to HAI event stream via SSE or WebSocket.
777
+ *
778
+ * Returns an async generator that yields HaiEvent objects.
779
+ * Supports automatic reconnection with exponential backoff.
780
+ */
781
+ async *connect(options) {
782
+ const transport = options?.transport ?? 'sse';
783
+ const onEvent = options?.onEvent;
784
+ this._shouldDisconnect = false;
785
+ this._connected = false;
786
+ if (transport === 'ws') {
787
+ yield* this.connectWs(onEvent);
788
+ }
789
+ else {
790
+ yield* this.connectSse(onEvent);
791
+ }
792
+ }
793
+ /**
794
+ * Disconnect from the event stream (SSE or WebSocket).
795
+ * Safe to call even if not connected.
796
+ */
797
+ disconnect() {
798
+ this._shouldDisconnect = true;
799
+ if (this._wsConnection) {
800
+ try {
801
+ this._wsConnection.close();
802
+ }
803
+ catch { /* ignore */ }
804
+ this._wsConnection = null;
805
+ }
806
+ this._connected = false;
807
+ }
808
+ // ---------------------------------------------------------------------------
809
+ // onBenchmarkJob()
810
+ // ---------------------------------------------------------------------------
811
+ /**
812
+ * Convenience wrapper: connect and dispatch benchmark_job events.
813
+ *
814
+ * Runs until disconnect() is called.
815
+ */
816
+ async onBenchmarkJob(handler, options) {
817
+ for await (const event of this.connect({ transport: options?.transport })) {
818
+ if (event.eventType === 'benchmark_job') {
819
+ const data = (typeof event.data === 'object' && event.data !== null)
820
+ ? event.data
821
+ : {};
822
+ const job = {
823
+ runId: data.run_id || data.runId || '',
824
+ scenario: data.scenario ?? data.prompt ?? data,
825
+ data,
826
+ };
827
+ await handler(job);
828
+ }
829
+ }
830
+ }
831
+ // ---------------------------------------------------------------------------
832
+ // checkUsername()
833
+ // ---------------------------------------------------------------------------
834
+ /**
835
+ * Check if a username is available for claiming.
836
+ * This is a public endpoint and does not require authentication.
837
+ *
838
+ * @param username - The username to check
839
+ * @returns Availability result
840
+ */
841
+ async checkUsername(username) {
842
+ const url = this.makeUrl(`/api/v1/agents/username/check?username=${encodeURIComponent(username)}`);
843
+ const response = await this.fetchWithRetry(url, {
844
+ method: 'GET',
845
+ headers: { 'Content-Type': 'application/json' },
846
+ });
847
+ const data = await response.json();
848
+ return {
849
+ available: data.available ?? false,
850
+ username: data.username || username,
851
+ reason: data.reason || undefined,
852
+ };
853
+ }
854
+ // ---------------------------------------------------------------------------
855
+ // claimUsername()
856
+ // ---------------------------------------------------------------------------
857
+ /**
858
+ * Claim a username for an agent. Requires JACS auth.
859
+ *
860
+ * @param agentId - The JACS ID of the agent to claim the username for
861
+ * @param username - The username to claim
862
+ * @returns Claim result with the assigned email
863
+ */
864
+ async claimUsername(agentId, username) {
865
+ const url = this.usernameEndpoint(agentId);
866
+ const response = await this.fetchWithRetry(url, {
867
+ method: 'POST',
868
+ headers: this.buildAuthHeaders(),
869
+ body: JSON.stringify({ username }),
870
+ });
871
+ const data = await response.json();
872
+ this.agentEmail = data.email || '';
873
+ return {
874
+ username: data.username || username,
875
+ email: data.email || '',
876
+ agentId: data.agent_id || data.agentId || agentId,
877
+ };
878
+ }
879
+ /**
880
+ * Rename a claimed username for an agent. Requires JACS auth.
881
+ *
882
+ * @param agentId - The agent ID to update
883
+ * @param username - The new username
884
+ */
885
+ async updateUsername(agentId, username) {
886
+ const url = this.usernameEndpoint(agentId);
887
+ const response = await this.fetchWithRetry(url, {
888
+ method: 'PUT',
889
+ headers: this.buildAuthHeaders(),
890
+ body: JSON.stringify({ username }),
891
+ });
892
+ const data = await response.json();
893
+ return {
894
+ username: data.username || username,
895
+ email: data.email || '',
896
+ previousUsername: data.previous_username || '',
897
+ };
898
+ }
899
+ /**
900
+ * Delete a claimed username for an agent. Requires JACS auth.
901
+ *
902
+ * @param agentId - The agent ID to update
903
+ */
904
+ async deleteUsername(agentId) {
905
+ const url = this.usernameEndpoint(agentId);
906
+ const response = await this.fetchWithRetry(url, {
907
+ method: 'DELETE',
908
+ headers: this.buildAuthHeaders(),
909
+ });
910
+ const data = await response.json();
911
+ return {
912
+ releasedUsername: data.released_username || '',
913
+ cooldownUntil: data.cooldown_until || '',
914
+ message: data.message || '',
915
+ };
916
+ }
917
+ // ---------------------------------------------------------------------------
918
+ // verifyDocument()
919
+ // ---------------------------------------------------------------------------
920
+ /**
921
+ * Verify a signed JACS document via HAI's public verification endpoint.
922
+ * This endpoint is public and does not require authentication.
923
+ *
924
+ * @param document - Signed JACS document JSON (object or string)
925
+ */
926
+ async verifyDocument(document) {
927
+ const url = this.makeUrl('/api/jacs/verify');
928
+ const rawDocument = typeof document === 'string' ? document : JSON.stringify(document);
929
+ const response = await this.fetchWithRetry(url, {
930
+ method: 'POST',
931
+ headers: { 'Content-Type': 'application/json' },
932
+ body: JSON.stringify({ document: rawDocument }),
933
+ });
934
+ const data = await response.json();
935
+ return {
936
+ valid: data.valid ?? false,
937
+ verifiedAt: data.verified_at || '',
938
+ documentType: data.document_type || '',
939
+ issuerVerified: data.issuer_verified ?? false,
940
+ signatureVerified: data.signature_verified ?? false,
941
+ signerId: data.signer_id || '',
942
+ signedAt: data.signed_at || '',
943
+ error: data.error || undefined,
944
+ };
945
+ }
946
+ parseAdvancedVerificationResult(data, fallbackAgentId = '') {
947
+ const verification = data.verification || {};
948
+ return {
949
+ agentId: data.agent_id || fallbackAgentId,
950
+ verification: {
951
+ jacsValid: verification.jacs_valid ?? false,
952
+ dnsValid: verification.dns_valid ?? false,
953
+ haiRegistered: verification.hai_registered ?? false,
954
+ badge: verification.badge || 'none',
955
+ },
956
+ haiSignatures: (data.hai_signatures || []).map(String),
957
+ verifiedAt: data.verified_at || '',
958
+ errors: (data.errors || []).map(String),
959
+ rawResponse: data,
960
+ };
961
+ }
962
+ /**
963
+ * Get advanced 3-level verification status for an agent (public endpoint).
964
+ *
965
+ * GET /api/v1/agents/{agent_id}/verification
966
+ */
967
+ async getVerification(agentId) {
968
+ const safeAgentId = this.encodePathSegment(agentId);
969
+ const url = this.makeUrl(`/api/v1/agents/${safeAgentId}/verification`);
970
+ const response = await this.fetchWithRetry(url, {
971
+ method: 'GET',
972
+ headers: { 'Content-Type': 'application/json' },
973
+ });
974
+ const data = await response.json();
975
+ return this.parseAdvancedVerificationResult(data, agentId);
976
+ }
977
+ /**
978
+ * Verify an agent document via HAI's advanced verification endpoint (public).
979
+ *
980
+ * POST /api/v1/agents/verify
981
+ */
982
+ async verifyAgentDocumentOnHai(agentJson, options) {
983
+ const url = this.makeUrl('/api/v1/agents/verify');
984
+ const payload = {
985
+ agent_json: typeof agentJson === 'string' ? agentJson : JSON.stringify(agentJson),
986
+ };
987
+ if (options?.publicKey) {
988
+ payload.public_key = options.publicKey;
989
+ }
990
+ if (options?.domain) {
991
+ payload.domain = options.domain;
992
+ }
993
+ const response = await this.fetchWithRetry(url, {
994
+ method: 'POST',
995
+ headers: { 'Content-Type': 'application/json' },
996
+ body: JSON.stringify(payload),
997
+ });
998
+ const data = await response.json();
999
+ return this.parseAdvancedVerificationResult(data);
1000
+ }
1001
+ // ---------------------------------------------------------------------------
1002
+ // registerNewAgent()
1003
+ // ---------------------------------------------------------------------------
1004
+ /**
1005
+ * Generate a fresh JACS agent and register it with HAI.
1006
+ *
1007
+ * Convenience method that combines key generation, document building,
1008
+ * signing, and registration in one call.
1009
+ *
1010
+ * @param agentName - Name for the new agent
1011
+ * @param options - Registration options
1012
+ * @returns Registration result
1013
+ */
1014
+ async registerNewAgent(agentName, options) {
1015
+ const { mkdtemp, readFile: readF } = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
1016
+ const { join } = await Promise.resolve().then(() => __importStar(require('node:path')));
1017
+ const { tmpdir } = await Promise.resolve().then(() => __importStar(require('node:os')));
1018
+ // Generate a new JACS agent with keys via JACS core
1019
+ const tempDir = await mkdtemp(join(tmpdir(), 'haiai-register-'));
1020
+ const keyDir = join(tempDir, 'keys');
1021
+ const dataDir = join(tempDir, 'data');
1022
+ const passphrase = process.env.JACS_PRIVATE_KEY_PASSWORD ?? 'register-temp';
1023
+ const resultJson = (0, jacs_1.createAgentSync)(agentName, passphrase, 'pq2025', dataDir, keyDir, join(tempDir, 'jacs.config.json'), null, options.description ?? 'Agent registered via Node SDK', options.domain ?? null, null);
1024
+ const createResult = JSON.parse(resultJson);
1025
+ const pubKeyPath = createResult.public_key_path || join(keyDir, 'jacs.public.pem');
1026
+ const publicKeyPem = normalizeKeyText(await readF(pubKeyPath), 'PUBLIC KEY');
1027
+ // Load the new agent for signing
1028
+ const tempAgent = new jacs_1.JacsAgent();
1029
+ const tempConfigPath = createResult.config_path || join(tempDir, 'jacs.config.json');
1030
+ const { resolve } = await Promise.resolve().then(() => __importStar(require('node:path')));
1031
+ let signingAgent = tempAgent;
1032
+ try {
1033
+ await tempAgent.load(resolve(tempConfigPath));
1034
+ }
1035
+ catch {
1036
+ tempAgent.ephemeralSync('pq2025');
1037
+ signingAgent = tempAgent;
1038
+ }
1039
+ // Build minimal JACS agent document
1040
+ const agentDoc = {
1041
+ jacsId: agentName,
1042
+ jacsVersion: '1.0.0',
1043
+ jacsSignature: {
1044
+ agentID: agentName,
1045
+ date: new Date().toISOString(),
1046
+ },
1047
+ jacsPublicKey: publicKeyPem,
1048
+ name: agentName,
1049
+ description: options.description ?? 'Agent registered via Node SDK',
1050
+ capabilities: ['mediation'],
1051
+ version: '1.0.0',
1052
+ };
1053
+ // Sign canonical JSON via JACS
1054
+ const canonical = (0, signing_js_1.canonicalJson)(agentDoc);
1055
+ const signature = signingAgent.signStringSync(canonical);
1056
+ agentDoc.jacsSignature.signature = signature;
1057
+ const url = this.makeUrl('/api/v1/agents/register');
1058
+ const publicKeyB64 = Buffer.from(publicKeyPem, 'utf-8').toString('base64');
1059
+ const body = {
1060
+ agent_json: JSON.stringify(agentDoc),
1061
+ public_key: publicKeyB64,
1062
+ owner_email: options.ownerEmail,
1063
+ };
1064
+ if (options.domain) {
1065
+ body.domain = options.domain;
1066
+ }
1067
+ if (options.description) {
1068
+ body.description = options.description;
1069
+ }
1070
+ const response = await this.fetchWithRetry(url, {
1071
+ method: 'POST',
1072
+ headers: { 'Content-Type': 'application/json' },
1073
+ body: JSON.stringify(body),
1074
+ });
1075
+ const data = await response.json();
1076
+ if (!options.quiet) {
1077
+ console.log(`\nAgent created and submitted for registration!`);
1078
+ console.log(` -> Check your email (${options.ownerEmail}) for a verification link`);
1079
+ console.log(` -> Click the link and log into hai.ai to complete registration`);
1080
+ console.log(` -> After verification, claim a @hai.ai username with:`);
1081
+ console.log(` client.claimUsername('${data.agent_id || ''}', 'my-agent')`);
1082
+ console.log(` -> Save your config and private key to a secure, access-controlled location`);
1083
+ if (options.domain) {
1084
+ const pubKeyHash = (0, jacs_1.hashString)(publicKeyPem);
1085
+ console.log(`\n--- DNS Setup Instructions ---`);
1086
+ console.log(`Add this TXT record to your domain '${options.domain}':`);
1087
+ console.log(` Name: _jacs.${options.domain}`);
1088
+ console.log(` Type: TXT`);
1089
+ console.log(` Value: sha256:${pubKeyHash}`);
1090
+ console.log(`DNS verification enables the pro tier.\n`);
1091
+ }
1092
+ else {
1093
+ console.log();
1094
+ }
1095
+ }
1096
+ return {
1097
+ success: true,
1098
+ agentId: data.agent_id || data.agentId || '',
1099
+ jacsId: data.jacs_id || data.jacsId || agentDoc.jacsId || '',
1100
+ haiSignature: data.hai_signature || data.haiSignature || '',
1101
+ registrationId: data.registration_id || data.registrationId || '',
1102
+ registeredAt: data.registered_at || data.registeredAt || '',
1103
+ rawResponse: data,
1104
+ };
1105
+ }
1106
+ // ---------------------------------------------------------------------------
1107
+ // testConnection()
1108
+ // ---------------------------------------------------------------------------
1109
+ /**
1110
+ * Test connectivity to the HAI server.
1111
+ *
1112
+ * Tries multiple health endpoints and returns true if any respond with 2xx.
1113
+ * Does not require authentication.
1114
+ */
1115
+ async testConnection() {
1116
+ const endpoints = ['/api/v1/health', '/health', '/api/health', '/'];
1117
+ const timeoutMs = Math.min(this.timeout, 10000);
1118
+ for (const endpoint of endpoints) {
1119
+ try {
1120
+ const url = this.makeUrl(endpoint);
1121
+ const controller = new AbortController();
1122
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1123
+ const resp = await fetch(url, {
1124
+ signal: controller.signal,
1125
+ redirect: 'follow',
1126
+ });
1127
+ clearTimeout(timeoutId);
1128
+ if (resp.ok) {
1129
+ return true;
1130
+ }
1131
+ }
1132
+ catch {
1133
+ // Ignore errors and try next endpoint
1134
+ }
1135
+ }
1136
+ return false;
1137
+ }
1138
+ // ---------------------------------------------------------------------------
1139
+ // Utility: export keys
1140
+ // ---------------------------------------------------------------------------
1141
+ /**
1142
+ * Export the agent's public key.
1143
+ * Reads the public key from the JACS key directory.
1144
+ * Returns { publicKeyPem }.
1145
+ */
1146
+ exportKeys() {
1147
+ const fs = require('node:fs');
1148
+ const path = require('node:path');
1149
+ const explicitPublicKeyPem = this._publicKeyPem;
1150
+ const explicitPrivateKeyPem = this._privateKeyPem;
1151
+ if (typeof explicitPublicKeyPem === 'string' && explicitPublicKeyPem.trim() !== '') {
1152
+ return {
1153
+ publicKeyPem: explicitPublicKeyPem.trim(),
1154
+ privateKeyPem: typeof explicitPrivateKeyPem === 'string' ? explicitPrivateKeyPem : undefined,
1155
+ };
1156
+ }
1157
+ const keyDir = this.config.jacsKeyDir;
1158
+ const candidates = [
1159
+ path.join(keyDir, 'agent_public_key.pem'),
1160
+ path.join(keyDir, `${this.config.jacsAgentName}.public.pem`),
1161
+ path.join(keyDir, 'public_key.pem'),
1162
+ path.join(keyDir, 'jacs.public.pem'),
1163
+ ];
1164
+ for (const candidate of candidates) {
1165
+ try {
1166
+ const content = fs.readFileSync(candidate);
1167
+ return {
1168
+ publicKeyPem: normalizeKeyText(content, 'PUBLIC KEY'),
1169
+ privateKeyPem: typeof explicitPrivateKeyPem === 'string' ? explicitPrivateKeyPem : undefined,
1170
+ };
1171
+ }
1172
+ catch {
1173
+ // try next
1174
+ }
1175
+ }
1176
+ throw new errors_js_1.AuthenticationError(`No public key found. Searched: ${candidates.join(', ')}`);
1177
+ }
1178
+ // ---------------------------------------------------------------------------
1179
+ // SSE transport (internal)
1180
+ // ---------------------------------------------------------------------------
1181
+ async *connectSse(onEvent) {
1182
+ const url = this.makeUrl('/api/v1/agents/connect');
1183
+ let reconnectDelay = 1000;
1184
+ const maxReconnectDelay = 60000;
1185
+ while (!this._shouldDisconnect) {
1186
+ try {
1187
+ const headers = {
1188
+ ...this.buildAuthHeaders(),
1189
+ 'Accept': 'text/event-stream',
1190
+ 'Cache-Control': 'no-cache',
1191
+ };
1192
+ if (this._lastEventId) {
1193
+ headers['Last-Event-ID'] = this._lastEventId;
1194
+ }
1195
+ const response = await fetch(url, { headers });
1196
+ if (response.status === 401) {
1197
+ throw new errors_js_1.AuthenticationError('JACS signature rejected by HAI', 401);
1198
+ }
1199
+ if (!response.ok) {
1200
+ throw new errors_js_1.HaiConnectionError(`SSE connection failed with status ${response.status}`);
1201
+ }
1202
+ if (!response.body) {
1203
+ throw new errors_js_1.HaiConnectionError('SSE response has no body');
1204
+ }
1205
+ this._connected = true;
1206
+ reconnectDelay = 1000;
1207
+ for await (const event of (0, sse_js_1.parseSseStream)(response.body)) {
1208
+ if (this._shouldDisconnect)
1209
+ break;
1210
+ if (event.id)
1211
+ this._lastEventId = event.id;
1212
+ // Unwrap signed events if we have server keys
1213
+ if (typeof event.data === 'object' && event.data !== null) {
1214
+ event.data = (0, signing_js_1.unwrapSignedEvent)(event.data, this.serverPublicKeys);
1215
+ }
1216
+ if (onEvent)
1217
+ onEvent(event);
1218
+ yield event;
1219
+ }
1220
+ }
1221
+ catch (e) {
1222
+ this._connected = false;
1223
+ if (this._shouldDisconnect)
1224
+ break;
1225
+ if (e instanceof errors_js_1.HaiError)
1226
+ throw e;
1227
+ await new Promise(resolve => setTimeout(resolve, reconnectDelay));
1228
+ reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay);
1229
+ }
1230
+ }
1231
+ this._connected = false;
1232
+ }
1233
+ // ---------------------------------------------------------------------------
1234
+ // WebSocket transport (internal)
1235
+ // ---------------------------------------------------------------------------
1236
+ async *connectWs(onEvent) {
1237
+ const wsUrl = this.baseUrl
1238
+ .replace(/^https:/, 'wss:')
1239
+ .replace(/^http:/, 'ws:')
1240
+ + '/ws/agent/connect';
1241
+ let reconnectDelay = 1000;
1242
+ const maxReconnectDelay = 60000;
1243
+ while (!this._shouldDisconnect) {
1244
+ try {
1245
+ const headers = {
1246
+ Authorization: this.buildAuthHeader(),
1247
+ };
1248
+ if (this._lastEventId) {
1249
+ headers['Last-Event-ID'] = this._lastEventId;
1250
+ }
1251
+ const ws = await (0, ws_js_1.openWebSocket)(wsUrl, headers, this.timeout);
1252
+ this._wsConnection = ws;
1253
+ try {
1254
+ this._connected = true;
1255
+ reconnectDelay = 1000;
1256
+ // Yield connected event
1257
+ const connEvent = {
1258
+ eventType: 'connected',
1259
+ data: null,
1260
+ raw: '',
1261
+ };
1262
+ if (onEvent)
1263
+ onEvent(connEvent);
1264
+ yield connEvent;
1265
+ // Yield all subsequent messages
1266
+ for await (const event of (0, ws_js_1.wsEventStream)(ws)) {
1267
+ if (this._shouldDisconnect)
1268
+ break;
1269
+ if (event.id)
1270
+ this._lastEventId = event.id;
1271
+ // Auto-pong on heartbeat
1272
+ if (event.eventType === 'heartbeat') {
1273
+ const data = event.data;
1274
+ const timestamp = data.timestamp ?? Math.floor(Date.now() / 1000);
1275
+ ws.send(JSON.stringify({ type: 'pong', timestamp }));
1276
+ }
1277
+ if (onEvent)
1278
+ onEvent(event);
1279
+ yield event;
1280
+ }
1281
+ }
1282
+ finally {
1283
+ try {
1284
+ ws.close();
1285
+ }
1286
+ catch { /* ignore */ }
1287
+ this._wsConnection = null;
1288
+ }
1289
+ }
1290
+ catch (e) {
1291
+ this._connected = false;
1292
+ if (this._shouldDisconnect)
1293
+ break;
1294
+ if (e instanceof errors_js_1.HaiError)
1295
+ throw e;
1296
+ await new Promise(resolve => setTimeout(resolve, reconnectDelay));
1297
+ reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay);
1298
+ }
1299
+ }
1300
+ this._connected = false;
1301
+ }
1302
+ // ---------------------------------------------------------------------------
1303
+ // Fetch with retry and error handling
1304
+ // ---------------------------------------------------------------------------
1305
+ async fetchWithRetry(url, init, timeoutMs) {
1306
+ const effectiveTimeout = timeoutMs ?? this.timeout;
1307
+ let lastError = null;
1308
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
1309
+ try {
1310
+ const controller = new AbortController();
1311
+ const timeoutId = setTimeout(() => controller.abort(), effectiveTimeout);
1312
+ const response = await fetch(url, {
1313
+ ...init,
1314
+ signal: controller.signal,
1315
+ });
1316
+ clearTimeout(timeoutId);
1317
+ if (response.status === 401) {
1318
+ throw new errors_js_1.AuthenticationError('JACS signature rejected by HAI', 401);
1319
+ }
1320
+ if (response.status === 429) {
1321
+ throw new errors_js_1.HaiError('Rate limited', 429);
1322
+ }
1323
+ if (response.ok) {
1324
+ return response;
1325
+ }
1326
+ let msg = `Request failed with status ${response.status}`;
1327
+ try {
1328
+ const errBody = await response.json();
1329
+ if (errBody.error)
1330
+ msg = String(errBody.error);
1331
+ }
1332
+ catch { /* empty */ }
1333
+ lastError = new errors_js_1.HaiError(msg, response.status);
1334
+ }
1335
+ catch (e) {
1336
+ if (e instanceof errors_js_1.HaiError)
1337
+ throw e;
1338
+ if (e instanceof Error && e.name === 'AbortError') {
1339
+ throw new errors_js_1.HaiConnectionError(`Request timed out after ${effectiveTimeout}ms`);
1340
+ }
1341
+ lastError = e instanceof Error ? e : new Error(String(e));
1342
+ }
1343
+ // Exponential backoff
1344
+ if (attempt < this.maxRetries - 1) {
1345
+ await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
1346
+ }
1347
+ }
1348
+ throw lastError ?? new errors_js_1.HaiError('Request failed after all retries');
1349
+ }
1350
+ // ---------------------------------------------------------------------------
1351
+ // Transcript parsing
1352
+ // ---------------------------------------------------------------------------
1353
+ parseEmailMessage(m) {
1354
+ return {
1355
+ id: m.id || '',
1356
+ direction: m.direction || '',
1357
+ fromAddress: m.from_address || '',
1358
+ toAddress: m.to_address || '',
1359
+ subject: m.subject || '',
1360
+ bodyText: m.body_text || '',
1361
+ messageId: m.message_id || '',
1362
+ inReplyTo: m.in_reply_to ?? null,
1363
+ isRead: m.is_read ?? false,
1364
+ deliveryStatus: m.delivery_status || '',
1365
+ createdAt: m.created_at || '',
1366
+ readAt: m.read_at ?? null,
1367
+ jacsVerified: m.jacs_verified ?? false,
1368
+ ccAddresses: m.cc_addresses || [],
1369
+ labels: m.labels || [],
1370
+ folder: m.folder || 'inbox',
1371
+ };
1372
+ }
1373
+ parseTranscript(raw) {
1374
+ return (raw || []).map((msg) => {
1375
+ const m = msg;
1376
+ return {
1377
+ role: m.role || 'system',
1378
+ content: m.content || '',
1379
+ timestamp: m.timestamp || '',
1380
+ annotations: m.annotations || [],
1381
+ };
1382
+ });
1383
+ }
1384
+ // ---------------------------------------------------------------------------
1385
+ // Server key management
1386
+ // ---------------------------------------------------------------------------
1387
+ /** Fetch and cache server public keys for signature verification. */
1388
+ async fetchServerKeys() {
1389
+ this.serverPublicKeys = await (0, signing_js_1.getServerKeys)(this.baseUrl);
1390
+ }
1391
+ // ---------------------------------------------------------------------------
1392
+ // getAgentAttestation()
1393
+ // ---------------------------------------------------------------------------
1394
+ /**
1395
+ * Get attestation information for another agent.
1396
+ *
1397
+ * @param agentId - The JACS ID of the agent to query
1398
+ * @returns Attestation status including HAI signatures
1399
+ */
1400
+ async getAgentAttestation(agentId) {
1401
+ const safeAgentId = this.encodePathSegment(agentId);
1402
+ const url = this.makeUrl(`/api/v1/agents/${safeAgentId}/verify`);
1403
+ const response = await this.fetchWithRetry(url, {
1404
+ method: 'GET',
1405
+ headers: this.buildAuthHeaders(),
1406
+ });
1407
+ const data = await response.json();
1408
+ const rawRegistrations = data.registrations || [];
1409
+ const registrations = rawRegistrations.map((r) => ({
1410
+ keyId: r.key_id || '',
1411
+ algorithm: r.algorithm || '',
1412
+ signatureJson: r.signature_json || '',
1413
+ signedAt: r.signed_at || '',
1414
+ }));
1415
+ return {
1416
+ jacsId: data.jacs_id || agentId,
1417
+ registered: data.registered ?? false,
1418
+ registrations,
1419
+ dnsVerified: data.dns_verified ?? false,
1420
+ registeredAt: data.registered_at || '',
1421
+ rawResponse: data,
1422
+ };
1423
+ }
1424
+ // ---------------------------------------------------------------------------
1425
+ // signBenchmarkResult()
1426
+ // ---------------------------------------------------------------------------
1427
+ /**
1428
+ * Sign a benchmark result as a JACS document for independent verification.
1429
+ *
1430
+ * @param benchmarkResult - The benchmark result data to sign
1431
+ * @returns Signed JACS document envelope
1432
+ */
1433
+ signBenchmarkResult(benchmarkResult) {
1434
+ return (0, signing_js_1.signResponse)(benchmarkResult, this.agent, this.jacsId);
1435
+ }
1436
+ // ---------------------------------------------------------------------------
1437
+ // benchmark() -- legacy suite-based
1438
+ // ---------------------------------------------------------------------------
1439
+ /**
1440
+ * Run a benchmark with specified name and tier.
1441
+ *
1442
+ * @param name - Benchmark run name
1443
+ * @param tier - Benchmark tier ("free", "pro", "enterprise"). Default: "free"
1444
+ * @returns Benchmark result with scores
1445
+ */
1446
+ async benchmark(name = 'mediation_basic', tier = 'free') {
1447
+ const url = this.makeUrl('/api/benchmark/run');
1448
+ const response = await this.fetchWithRetry(url, {
1449
+ method: 'POST',
1450
+ headers: this.buildAuthHeaders(),
1451
+ body: JSON.stringify({ name, tier }),
1452
+ });
1453
+ const data = await response.json();
1454
+ return data;
1455
+ }
1456
+ // ---------------------------------------------------------------------------
1457
+ // Email CRUD
1458
+ // ---------------------------------------------------------------------------
1459
+ /**
1460
+ * Send an email from the agent's @hai.ai address.
1461
+ *
1462
+ * @param options - Email send options (to, subject, body, optional inReplyTo)
1463
+ * @returns Send result with message ID and status
1464
+ */
1465
+ async sendEmail(options) {
1466
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1467
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/send`);
1468
+ if (!this.agentEmail) {
1469
+ throw new Error('agent email not set — call claimUsername first');
1470
+ }
1471
+ // Server handles JACS attachment signing (TASK_014/018).
1472
+ // Client only sends content fields.
1473
+ const controller = new AbortController();
1474
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1475
+ let response;
1476
+ try {
1477
+ const payload = {
1478
+ to: options.to,
1479
+ subject: options.subject,
1480
+ body: options.body,
1481
+ in_reply_to: options.inReplyTo,
1482
+ attachments: options.attachments?.map(a => ({
1483
+ filename: a.filename,
1484
+ content_type: a.contentType,
1485
+ data_base64: a.data.toString('base64'),
1486
+ })),
1487
+ };
1488
+ if (options.cc?.length)
1489
+ payload.cc = options.cc;
1490
+ if (options.bcc?.length)
1491
+ payload.bcc = options.bcc;
1492
+ if (options.labels?.length)
1493
+ payload.labels = options.labels;
1494
+ response = await fetch(url, {
1495
+ method: 'POST',
1496
+ headers: this.buildAuthHeaders(),
1497
+ body: JSON.stringify(payload),
1498
+ signal: controller.signal,
1499
+ });
1500
+ }
1501
+ catch (e) {
1502
+ clearTimeout(timeoutId);
1503
+ if (e instanceof Error && e.name === 'AbortError') {
1504
+ throw new errors_js_1.HaiConnectionError(`Request timed out after ${this.timeout}ms`);
1505
+ }
1506
+ throw e;
1507
+ }
1508
+ clearTimeout(timeoutId);
1509
+ if (!response.ok) {
1510
+ const text = await response.text();
1511
+ let errCode = '';
1512
+ let errMsg = text;
1513
+ try {
1514
+ const errData = JSON.parse(text);
1515
+ errCode = errData.error_code || '';
1516
+ errMsg = errData.message || errData.error || text;
1517
+ }
1518
+ catch { /* non-JSON body */ }
1519
+ if (response.status === 401) {
1520
+ throw new errors_js_1.AuthenticationError('JACS signature rejected by HAI', 401);
1521
+ }
1522
+ if (response.status === 403 && (errCode === 'EMAIL_NOT_ACTIVE' || text.toLowerCase().includes('allocated'))) {
1523
+ throw new errors_js_1.EmailNotActiveError(errMsg, response.status, text);
1524
+ }
1525
+ if (response.status === 400 && (errCode === 'RECIPIENT_NOT_FOUND' || text.includes('Invalid recipient'))) {
1526
+ throw new errors_js_1.RecipientNotFoundError(errMsg, response.status, text);
1527
+ }
1528
+ if (response.status === 429) {
1529
+ throw new errors_js_1.RateLimitedError(errMsg, response.status, text);
1530
+ }
1531
+ throw new errors_js_1.HaiApiError(errMsg, response.status, undefined, errCode, text);
1532
+ }
1533
+ const data = await response.json();
1534
+ return {
1535
+ messageId: data.message_id || '',
1536
+ status: data.status || '',
1537
+ };
1538
+ }
1539
+ /**
1540
+ * Sign a raw RFC 5322 email with a JACS attachment via the HAI API.
1541
+ *
1542
+ * The server adds a `jacs-signature.json` MIME attachment containing
1543
+ * the detached JACS signature. The returned Buffer is the signed email.
1544
+ *
1545
+ * @param rawEmail - Raw RFC 5322 email as a Buffer or string.
1546
+ * @returns Signed email bytes with the JACS attachment added.
1547
+ */
1548
+ async signEmail(rawEmail) {
1549
+ const url = this.makeUrl('/api/v1/email/sign');
1550
+ const headers = this.buildAuthHeaders();
1551
+ headers['Content-Type'] = 'message/rfc822';
1552
+ const body = typeof rawEmail === 'string' ? Buffer.from(rawEmail) : rawEmail;
1553
+ const response = await this.fetchWithRetry(url, {
1554
+ method: 'POST',
1555
+ headers,
1556
+ body,
1557
+ });
1558
+ if (!response.ok) {
1559
+ const text = await response.text();
1560
+ throw new errors_js_1.HaiApiError(`Email sign failed: HTTP ${response.status}`, response.status, undefined, '', text);
1561
+ }
1562
+ const arrayBuf = await response.arrayBuffer();
1563
+ return Buffer.from(arrayBuf);
1564
+ }
1565
+ /**
1566
+ * Send an agent-signed email.
1567
+ *
1568
+ * @deprecated sendSignedEmail currently delegates to sendEmail. The previous
1569
+ * implementation called /api/v1/email/sign (HAI authority key) then POSTed
1570
+ * to send-signed, which rejects because the signer ID does not match the
1571
+ * authenticated agent. True agent-key local signing will be available when
1572
+ * the Rust SDK core (DevEx TASK_017) ships. Use sendEmail directly.
1573
+ *
1574
+ * @param options - Email options (to, subject, body, attachments, etc.)
1575
+ * @returns SendEmailResult with messageId and status.
1576
+ */
1577
+ async sendSignedEmail(options) {
1578
+ // Deprecated: delegates to sendEmail until local agent-key signing
1579
+ // is available (DevEx TASK_017). Use sendEmail directly.
1580
+ return this.sendEmail(options);
1581
+ }
1582
+ /**
1583
+ * Verify a JACS-signed email via the HAI API.
1584
+ *
1585
+ * The server extracts the `jacs-signature.json` attachment, validates
1586
+ * the cryptographic signature and content hashes, and returns a
1587
+ * detailed verification result.
1588
+ *
1589
+ * @param rawEmail - Raw RFC 5322 email as a Buffer or string.
1590
+ * @returns EmailVerificationResultV2 with field-level verification results.
1591
+ */
1592
+ async verifyEmail(rawEmail) {
1593
+ const url = this.makeUrl('/api/v1/email/verify');
1594
+ const headers = this.buildAuthHeaders();
1595
+ headers['Content-Type'] = 'message/rfc822';
1596
+ const body = typeof rawEmail === 'string' ? Buffer.from(rawEmail) : rawEmail;
1597
+ const response = await this.fetchWithRetry(url, {
1598
+ method: 'POST',
1599
+ headers,
1600
+ body,
1601
+ });
1602
+ if (!response.ok) {
1603
+ const text = await response.text();
1604
+ throw new errors_js_1.HaiApiError(`Email verify failed: HTTP ${response.status}`, response.status, undefined, '', text);
1605
+ }
1606
+ const data = await response.json();
1607
+ return {
1608
+ valid: data.valid ?? false,
1609
+ jacsId: data.jacs_id ?? '',
1610
+ algorithm: data.algorithm ?? '',
1611
+ reputationTier: data.reputation_tier ?? '',
1612
+ dnsVerified: data.dns_verified,
1613
+ fieldResults: (data.field_results ?? []).map(fr => ({
1614
+ field: fr.field ?? '',
1615
+ status: fr.status ?? 'unverifiable',
1616
+ originalHash: fr.original_hash,
1617
+ currentHash: fr.current_hash,
1618
+ originalValue: fr.original_value,
1619
+ currentValue: fr.current_value,
1620
+ })),
1621
+ chain: (data.chain ?? []).map(ce => ({
1622
+ signer: ce.signer ?? '',
1623
+ jacsId: ce.jacs_id ?? '',
1624
+ valid: ce.valid ?? false,
1625
+ forwarded: ce.forwarded ?? false,
1626
+ })),
1627
+ error: data.error,
1628
+ agentStatus: data.agent_status,
1629
+ benchmarksCompleted: data.benchmarks_completed ?? [],
1630
+ };
1631
+ }
1632
+ /**
1633
+ * List email messages for this agent.
1634
+ *
1635
+ * @param options - Pagination and direction filter options
1636
+ * @returns Array of email messages
1637
+ */
1638
+ async listMessages(options) {
1639
+ const params = new URLSearchParams();
1640
+ if (options?.limit != null)
1641
+ params.set('limit', String(options.limit));
1642
+ if (options?.offset != null)
1643
+ params.set('offset', String(options.offset));
1644
+ if (options?.direction)
1645
+ params.set('direction', options.direction);
1646
+ if (options?.isRead != null)
1647
+ params.set('is_read', String(options.isRead));
1648
+ if (options?.folder)
1649
+ params.set('folder', options.folder);
1650
+ if (options?.label)
1651
+ params.set('label', options.label);
1652
+ const qs = params.toString();
1653
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1654
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/messages${qs ? `?${qs}` : ''}`);
1655
+ const response = await this.fetchWithRetry(url, {
1656
+ method: 'GET',
1657
+ headers: this.buildAuthHeaders(),
1658
+ });
1659
+ const data = await response.json();
1660
+ const messages = data.messages || [];
1661
+ return messages.map((m) => this.parseEmailMessage(m));
1662
+ }
1663
+ /**
1664
+ * Mark an email message as read.
1665
+ *
1666
+ * @param messageId - The message ID to mark as read
1667
+ */
1668
+ async markRead(messageId) {
1669
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1670
+ const safeMessageId = this.encodePathSegment(messageId);
1671
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/messages/${safeMessageId}/read`);
1672
+ await this.fetchWithRetry(url, {
1673
+ method: 'POST',
1674
+ headers: this.buildAuthHeaders(),
1675
+ });
1676
+ }
1677
+ /**
1678
+ * Get email rate limit and status info for this agent.
1679
+ *
1680
+ * @returns Email status with daily limits and usage
1681
+ */
1682
+ async getEmailStatus() {
1683
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1684
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/status`);
1685
+ const response = await this.fetchWithRetry(url, {
1686
+ method: 'GET',
1687
+ headers: this.buildAuthHeaders(),
1688
+ });
1689
+ const data = await response.json();
1690
+ const volumeRaw = data.volume;
1691
+ const deliveryRaw = data.delivery;
1692
+ const reputationRaw = data.reputation;
1693
+ return {
1694
+ email: data.email || '',
1695
+ status: data.status || '',
1696
+ tier: data.tier || '',
1697
+ billingTier: data.billing_tier || '',
1698
+ messagesSent24h: data.messages_sent_24h || 0,
1699
+ dailyLimit: data.daily_limit || 0,
1700
+ dailyUsed: data.daily_used || 0,
1701
+ resetsAt: data.resets_at || '',
1702
+ messagesSentTotal: data.messages_sent_total || 0,
1703
+ externalEnabled: data.external_enabled || false,
1704
+ externalSendsToday: data.external_sends_today || 0,
1705
+ lastTierChange: data.last_tier_change || null,
1706
+ volume: volumeRaw ? {
1707
+ sentTotal: volumeRaw.sent_total || 0,
1708
+ receivedTotal: volumeRaw.received_total || 0,
1709
+ sent24h: volumeRaw.sent_24h || 0,
1710
+ } : null,
1711
+ delivery: deliveryRaw ? {
1712
+ bounceCount: deliveryRaw.bounce_count || 0,
1713
+ spamReportCount: deliveryRaw.spam_report_count || 0,
1714
+ deliveryRate: deliveryRaw.delivery_rate || 0,
1715
+ } : null,
1716
+ reputation: reputationRaw ? {
1717
+ score: reputationRaw.score || 0,
1718
+ tier: reputationRaw.tier || '',
1719
+ emailScore: reputationRaw.email_score || 0,
1720
+ haiScore: reputationRaw.hai_score != null ? reputationRaw.hai_score : null,
1721
+ } : null,
1722
+ };
1723
+ }
1724
+ /**
1725
+ * Get a single email message by ID.
1726
+ *
1727
+ * @param messageId - The message ID to retrieve
1728
+ * @returns The email message
1729
+ */
1730
+ async getMessage(messageId) {
1731
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1732
+ const safeMessageId = this.encodePathSegment(messageId);
1733
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/messages/${safeMessageId}`);
1734
+ const response = await this.fetchWithRetry(url, {
1735
+ method: 'GET',
1736
+ headers: this.buildAuthHeaders(),
1737
+ });
1738
+ const m = await response.json();
1739
+ return this.parseEmailMessage(m);
1740
+ }
1741
+ /**
1742
+ * Delete an email message.
1743
+ *
1744
+ * @param messageId - The message ID to delete
1745
+ */
1746
+ async deleteMessage(messageId) {
1747
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1748
+ const safeMessageId = this.encodePathSegment(messageId);
1749
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/messages/${safeMessageId}`);
1750
+ await this.fetchWithRetry(url, {
1751
+ method: 'DELETE',
1752
+ headers: this.buildAuthHeaders(),
1753
+ });
1754
+ }
1755
+ /**
1756
+ * Mark an email message as unread.
1757
+ *
1758
+ * @param messageId - The message ID to mark as unread
1759
+ */
1760
+ async markUnread(messageId) {
1761
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1762
+ const safeMessageId = this.encodePathSegment(messageId);
1763
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/messages/${safeMessageId}/unread`);
1764
+ await this.fetchWithRetry(url, {
1765
+ method: 'POST',
1766
+ headers: this.buildAuthHeaders(),
1767
+ });
1768
+ }
1769
+ /**
1770
+ * Search email messages.
1771
+ *
1772
+ * @param options - Search query and pagination options
1773
+ * @returns Array of matching email messages
1774
+ */
1775
+ async searchMessages(options) {
1776
+ const params = new URLSearchParams();
1777
+ params.set('q', options.query);
1778
+ if (options.limit != null)
1779
+ params.set('limit', String(options.limit));
1780
+ if (options.offset != null)
1781
+ params.set('offset', String(options.offset));
1782
+ if (options.direction)
1783
+ params.set('direction', options.direction);
1784
+ if (options.fromAddress)
1785
+ params.set('from_address', options.fromAddress);
1786
+ if (options.toAddress)
1787
+ params.set('to_address', options.toAddress);
1788
+ if (options.isRead != null)
1789
+ params.set('is_read', String(options.isRead));
1790
+ if (options.jacsVerified != null)
1791
+ params.set('jacs_verified', String(options.jacsVerified));
1792
+ if (options.folder)
1793
+ params.set('folder', options.folder);
1794
+ if (options.label)
1795
+ params.set('label', options.label);
1796
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1797
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/search?${params.toString()}`);
1798
+ const response = await this.fetchWithRetry(url, {
1799
+ method: 'GET',
1800
+ headers: this.buildAuthHeaders(),
1801
+ });
1802
+ const data = await response.json();
1803
+ const messages = data.messages || [];
1804
+ return messages.map((m) => this.parseEmailMessage(m));
1805
+ }
1806
+ /**
1807
+ * Get the count of unread messages.
1808
+ *
1809
+ * @returns The number of unread messages
1810
+ */
1811
+ async getUnreadCount() {
1812
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1813
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/unread-count`);
1814
+ const response = await this.fetchWithRetry(url, {
1815
+ method: 'GET',
1816
+ headers: this.buildAuthHeaders(),
1817
+ });
1818
+ const data = await response.json();
1819
+ return data.count || 0;
1820
+ }
1821
+ /**
1822
+ * Reply to an email message.
1823
+ *
1824
+ * Convenience method that fetches the original message to get the sender
1825
+ * and subject, then sends a reply with proper threading.
1826
+ *
1827
+ * @param messageId - The message ID to reply to
1828
+ * @param body - Reply body text
1829
+ * @param subjectOverride - Optional subject override (defaults to "Re: <original subject>")
1830
+ * @returns Send result with message ID and status
1831
+ */
1832
+ async reply(messageId, body, subjectOverride) {
1833
+ const original = await this.getMessage(messageId);
1834
+ const subject = subjectOverride ?? (original.subject?.startsWith('Re: ') ? original.subject : `Re: ${original.subject}`);
1835
+ return this.sendEmail({
1836
+ to: original.fromAddress,
1837
+ subject,
1838
+ body,
1839
+ inReplyTo: original.messageId ?? messageId,
1840
+ });
1841
+ }
1842
+ /**
1843
+ * Forward an email message to another recipient.
1844
+ *
1845
+ * @param options - Forward options (messageId, to, optional comment)
1846
+ * @returns Send result with message ID and status
1847
+ */
1848
+ async forward(options) {
1849
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1850
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/forward`);
1851
+ const payload = {
1852
+ message_id: options.messageId,
1853
+ to: options.to,
1854
+ };
1855
+ if (options.comment)
1856
+ payload.comment = options.comment;
1857
+ const response = await this.fetchWithRetry(url, {
1858
+ method: 'POST',
1859
+ headers: this.buildAuthHeaders(),
1860
+ body: JSON.stringify(payload),
1861
+ });
1862
+ const data = await response.json();
1863
+ return {
1864
+ messageId: data.message_id || '',
1865
+ status: data.status || '',
1866
+ };
1867
+ }
1868
+ /**
1869
+ * Archive an email message.
1870
+ *
1871
+ * @param messageId - The message ID to archive
1872
+ */
1873
+ async archive(messageId) {
1874
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1875
+ const safeMessageId = this.encodePathSegment(messageId);
1876
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/messages/${safeMessageId}/archive`);
1877
+ await this.fetchWithRetry(url, {
1878
+ method: 'POST',
1879
+ headers: this.buildAuthHeaders(),
1880
+ });
1881
+ }
1882
+ /**
1883
+ * Unarchive (restore) an email message.
1884
+ *
1885
+ * @param messageId - The message ID to unarchive
1886
+ */
1887
+ async unarchive(messageId) {
1888
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1889
+ const safeMessageId = this.encodePathSegment(messageId);
1890
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/messages/${safeMessageId}/unarchive`);
1891
+ await this.fetchWithRetry(url, {
1892
+ method: 'POST',
1893
+ headers: this.buildAuthHeaders(),
1894
+ });
1895
+ }
1896
+ /**
1897
+ * List contacts derived from email message history.
1898
+ *
1899
+ * @returns Array of Contact objects
1900
+ */
1901
+ async getContacts() {
1902
+ const safeAgentId = this.encodePathSegment(this.haiAgentId);
1903
+ const url = this.makeUrl(`/api/agents/${safeAgentId}/email/contacts`);
1904
+ const response = await this.fetchWithRetry(url, {
1905
+ method: 'GET',
1906
+ headers: this.buildAuthHeaders(),
1907
+ });
1908
+ const data = await response.json();
1909
+ const items = Array.isArray(data) ? data : data.contacts || [];
1910
+ return items.map((c) => ({
1911
+ email: c.email || '',
1912
+ displayName: c.display_name || undefined,
1913
+ lastContact: c.last_contact || '',
1914
+ jacsVerified: c.jacs_verified ?? false,
1915
+ reputationTier: c.reputation_tier || undefined,
1916
+ }));
1917
+ }
1918
+ // ---------------------------------------------------------------------------
1919
+ // fetchRemoteKey()
1920
+ // ---------------------------------------------------------------------------
1921
+ /**
1922
+ * Look up another agent's public key from the HAI key directory.
1923
+ *
1924
+ * @param jacsId - The JACS ID of the agent to look up
1925
+ * @param version - Key version (default: "latest")
1926
+ * @returns Public key information
1927
+ */
1928
+ async fetchRemoteKey(jacsId, version = 'latest') {
1929
+ const cacheKey = `remote:${jacsId}:${version}`;
1930
+ const cached = this.getCachedKey(cacheKey);
1931
+ if (cached)
1932
+ return cached;
1933
+ const safeJacsId = this.encodePathSegment(jacsId);
1934
+ const safeVersion = this.encodePathSegment(version);
1935
+ const url = this.makeUrl(`/jacs/v1/agents/${safeJacsId}/keys/${safeVersion}`);
1936
+ const response = await this.fetchWithRetry(url, {
1937
+ method: 'GET',
1938
+ headers: { 'Content-Type': 'application/json' },
1939
+ });
1940
+ const warning = response.headers.get('Warning');
1941
+ if (warning) {
1942
+ console.warn(`HAI key service: ${warning}`);
1943
+ }
1944
+ const data = await response.json();
1945
+ const result = {
1946
+ jacsId: data.jacs_id || '',
1947
+ version: data.version || '',
1948
+ publicKey: data.public_key || '',
1949
+ publicKeyRawB64: data.public_key_raw_b64 || '',
1950
+ algorithm: data.algorithm || '',
1951
+ publicKeyHash: data.public_key_hash || '',
1952
+ status: data.status || '',
1953
+ dnsVerified: data.dns_verified ?? false,
1954
+ createdAt: data.created_at || '',
1955
+ };
1956
+ this.setCachedKey(cacheKey, result);
1957
+ return result;
1958
+ }
1959
+ // ---------------------------------------------------------------------------
1960
+ // fetchKeyByHash()
1961
+ // ---------------------------------------------------------------------------
1962
+ /**
1963
+ * Look up an agent's public key by its SHA-256 hash.
1964
+ *
1965
+ * @param publicKeyHash - Hash in `sha256:<hex>` format
1966
+ * @returns Public key information
1967
+ */
1968
+ async fetchKeyByHash(publicKeyHash) {
1969
+ const cacheKey = `hash:${publicKeyHash}`;
1970
+ const cached = this.getCachedKey(cacheKey);
1971
+ if (cached)
1972
+ return cached;
1973
+ const safeHash = this.encodePathSegment(publicKeyHash);
1974
+ const url = this.makeUrl(`/jacs/v1/keys/by-hash/${safeHash}`);
1975
+ const response = await this.fetchWithRetry(url, {
1976
+ method: 'GET',
1977
+ headers: { 'Content-Type': 'application/json' },
1978
+ });
1979
+ const data = await response.json();
1980
+ const result = {
1981
+ jacsId: data.jacs_id || '',
1982
+ version: data.version || '',
1983
+ publicKey: data.public_key || '',
1984
+ publicKeyRawB64: data.public_key_raw_b64 || '',
1985
+ algorithm: data.algorithm || '',
1986
+ publicKeyHash: data.public_key_hash || '',
1987
+ status: data.status || '',
1988
+ dnsVerified: data.dns_verified ?? false,
1989
+ createdAt: data.created_at || '',
1990
+ };
1991
+ this.setCachedKey(cacheKey, result);
1992
+ return result;
1993
+ }
1994
+ // ---------------------------------------------------------------------------
1995
+ // fetchKeyByEmail()
1996
+ // ---------------------------------------------------------------------------
1997
+ /**
1998
+ * Look up an agent's public key by their @hai.ai email address.
1999
+ *
2000
+ * @param email - The agent's email address (e.g., "alice@hai.ai")
2001
+ * @returns Public key information
2002
+ */
2003
+ async fetchKeyByEmail(email) {
2004
+ const cacheKey = `email:${email}`;
2005
+ const cached = this.getCachedKey(cacheKey);
2006
+ if (cached)
2007
+ return cached;
2008
+ const safeEmail = this.encodePathSegment(email);
2009
+ const url = this.makeUrl(`/api/agents/keys/${safeEmail}`);
2010
+ const response = await this.fetchWithRetry(url, {
2011
+ method: 'GET',
2012
+ headers: { 'Content-Type': 'application/json' },
2013
+ });
2014
+ const data = await response.json();
2015
+ const result = {
2016
+ jacsId: data.jacs_id || '',
2017
+ version: data.version || '',
2018
+ publicKey: data.public_key || '',
2019
+ publicKeyRawB64: data.public_key_raw_b64 || '',
2020
+ algorithm: data.algorithm || '',
2021
+ publicKeyHash: data.public_key_hash || '',
2022
+ status: data.status || '',
2023
+ dnsVerified: data.dns_verified ?? false,
2024
+ createdAt: data.created_at || '',
2025
+ };
2026
+ this.setCachedKey(cacheKey, result);
2027
+ return result;
2028
+ }
2029
+ // ---------------------------------------------------------------------------
2030
+ // fetchKeyByDomain()
2031
+ // ---------------------------------------------------------------------------
2032
+ /**
2033
+ * Look up the latest DNS-verified agent key for a domain.
2034
+ *
2035
+ * @param domain - DNS domain (e.g., "example.com")
2036
+ * @returns Public key information
2037
+ */
2038
+ async fetchKeyByDomain(domain) {
2039
+ const cacheKey = `domain:${domain}`;
2040
+ const cached = this.getCachedKey(cacheKey);
2041
+ if (cached)
2042
+ return cached;
2043
+ const safeDomain = this.encodePathSegment(domain);
2044
+ const url = this.makeUrl(`/jacs/v1/agents/by-domain/${safeDomain}`);
2045
+ const response = await this.fetchWithRetry(url, {
2046
+ method: 'GET',
2047
+ headers: { 'Content-Type': 'application/json' },
2048
+ });
2049
+ const data = await response.json();
2050
+ const result = {
2051
+ jacsId: data.jacs_id || '',
2052
+ version: data.version || '',
2053
+ publicKey: data.public_key || '',
2054
+ publicKeyRawB64: data.public_key_raw_b64 || '',
2055
+ algorithm: data.algorithm || '',
2056
+ publicKeyHash: data.public_key_hash || '',
2057
+ status: data.status || '',
2058
+ dnsVerified: data.dns_verified ?? false,
2059
+ createdAt: data.created_at || '',
2060
+ };
2061
+ this.setCachedKey(cacheKey, result);
2062
+ return result;
2063
+ }
2064
+ // ---------------------------------------------------------------------------
2065
+ // fetchAllKeys()
2066
+ // ---------------------------------------------------------------------------
2067
+ /**
2068
+ * Fetch all key versions for an agent, ordered by creation date descending.
2069
+ *
2070
+ * @param jacsId - The JACS ID of the agent to look up
2071
+ * @returns Object with jacs_id, keys array, and total count
2072
+ */
2073
+ async fetchAllKeys(jacsId) {
2074
+ const safeJacsId = this.encodePathSegment(jacsId);
2075
+ const url = this.makeUrl(`/jacs/v1/agents/${safeJacsId}/keys`);
2076
+ const response = await this.fetchWithRetry(url, {
2077
+ method: 'GET',
2078
+ headers: { 'Content-Type': 'application/json' },
2079
+ });
2080
+ const data = await response.json();
2081
+ const rawKeys = data.keys || [];
2082
+ const keys = rawKeys.map((k) => ({
2083
+ jacsId: k.jacs_id || '',
2084
+ version: k.version || '',
2085
+ publicKey: k.public_key || '',
2086
+ publicKeyRawB64: k.public_key_raw_b64 || '',
2087
+ algorithm: k.algorithm || '',
2088
+ publicKeyHash: k.public_key_hash || '',
2089
+ status: k.status || '',
2090
+ dnsVerified: k.dns_verified ?? false,
2091
+ createdAt: k.created_at || '',
2092
+ }));
2093
+ return {
2094
+ jacsId: data.jacs_id || '',
2095
+ keys,
2096
+ total: data.total || 0,
2097
+ };
2098
+ }
2099
+ // ---------------------------------------------------------------------------
2100
+ // verifyAgent()
2101
+ // ---------------------------------------------------------------------------
2102
+ /**
2103
+ * Verify another agent's JACS document.
2104
+ *
2105
+ * Performs three levels of verification:
2106
+ * 1. Local Ed25519 signature verification
2107
+ * 2. DNS verification (via server attestation)
2108
+ * 3. HAI registration attestation
2109
+ *
2110
+ * @param agentDocument - JACS agent document (object or JSON string)
2111
+ * @returns Verification result with signature validity and trust level
2112
+ */
2113
+ async verifyAgent(agentDocument) {
2114
+ const doc = typeof agentDocument === 'string'
2115
+ ? JSON.parse(agentDocument)
2116
+ : agentDocument;
2117
+ const result = {
2118
+ signatureValid: false,
2119
+ dnsVerified: false,
2120
+ haiRegistered: false,
2121
+ badgeLevel: 'none',
2122
+ jacsId: doc.jacsId || '',
2123
+ version: doc.jacsVersion || '',
2124
+ errors: [],
2125
+ };
2126
+ // Level 1: JACS signature verification
2127
+ try {
2128
+ const publicKeyPem = doc.jacsPublicKey;
2129
+ if (!publicKeyPem) {
2130
+ result.errors.push('No jacsPublicKey in document');
2131
+ return result;
2132
+ }
2133
+ const sig = doc.jacsSignature;
2134
+ const signature = sig?.signature;
2135
+ if (!signature) {
2136
+ result.errors.push('No signature in jacsSignature');
2137
+ return result;
2138
+ }
2139
+ // Remove signature, canonicalize, verify via JACS
2140
+ const verifyDoc = JSON.parse(JSON.stringify(doc));
2141
+ delete verifyDoc.jacsSignature.signature;
2142
+ const canonical = (0, signing_js_1.canonicalJson)(verifyDoc);
2143
+ result.signatureValid = this.agent.verifyStringSync(canonical, signature, Buffer.from(publicKeyPem, 'utf-8'), 'pem');
2144
+ }
2145
+ catch (e) {
2146
+ result.errors.push(`Signature verification failed: ${e.message}`);
2147
+ }
2148
+ // Level 3: Server attestation
2149
+ try {
2150
+ const safeDocJacsId = this.encodePathSegment(String(doc.jacsId || ''));
2151
+ const attestUrl = this.makeUrl(`/api/v1/agents/${safeDocJacsId}/verify`);
2152
+ const resp = await this.fetchWithRetry(attestUrl, {
2153
+ method: 'GET',
2154
+ headers: { 'Content-Type': 'application/json' },
2155
+ });
2156
+ const data = await resp.json();
2157
+ result.haiRegistered = data.registered ?? false;
2158
+ result.dnsVerified = data.dns_verified ?? false;
2159
+ result.badgeLevel = data.badge_level || 'none';
2160
+ }
2161
+ catch (e) {
2162
+ result.errors.push(`Server attestation check failed: ${e.message}`);
2163
+ }
2164
+ return result;
2165
+ }
2166
+ }
2167
+ exports.HaiClient = HaiClient;
2168
+ //# sourceMappingURL=client.js.map