@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.
- package/README.md +127 -0
- package/bin/haiai.cjs +70 -0
- package/dist/cjs/a2a.js +352 -0
- package/dist/cjs/a2a.js.map +1 -0
- package/dist/cjs/agent.js +236 -0
- package/dist/cjs/agent.js.map +1 -0
- package/dist/cjs/client.js +2168 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/config.js +176 -0
- package/dist/cjs/config.js.map +1 -0
- package/dist/cjs/errors.js +102 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/hash.js +52 -0
- package/dist/cjs/hash.js.map +1 -0
- package/dist/cjs/index.js +84 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/integrations.js +193 -0
- package/dist/cjs/integrations.js.map +1 -0
- package/dist/cjs/jacs.js +66 -0
- package/dist/cjs/jacs.js.map +1 -0
- package/dist/cjs/mime.js +100 -0
- package/dist/cjs/mime.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/signing.js +190 -0
- package/dist/cjs/signing.js.map +1 -0
- package/dist/cjs/sse.js +76 -0
- package/dist/cjs/sse.js.map +1 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/verify.js +76 -0
- package/dist/cjs/verify.js.map +1 -0
- package/dist/cjs/ws.js +206 -0
- package/dist/cjs/ws.js.map +1 -0
- package/dist/esm/a2a.js +305 -0
- package/dist/esm/a2a.js.map +1 -0
- package/dist/esm/agent.js +231 -0
- package/dist/esm/agent.js.map +1 -0
- package/dist/esm/client.js +2131 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/config.js +171 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/errors.js +88 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/hash.js +49 -0
- package/dist/esm/hash.js.map +1 -0
- package/dist/esm/index.js +27 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/integrations.js +147 -0
- package/dist/esm/integrations.js.map +1 -0
- package/dist/esm/jacs.js +61 -0
- package/dist/esm/jacs.js.map +1 -0
- package/dist/esm/mime.js +97 -0
- package/dist/esm/mime.js.map +1 -0
- package/dist/esm/signing.js +183 -0
- package/dist/esm/signing.js.map +1 -0
- package/dist/esm/sse.js +73 -0
- package/dist/esm/sse.js.map +1 -0
- package/dist/esm/types.js +5 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/verify.js +72 -0
- package/dist/esm/verify.js.map +1 -0
- package/dist/esm/ws.js +168 -0
- package/dist/esm/ws.js.map +1 -0
- package/dist/types/a2a.d.ts +52 -0
- package/dist/types/a2a.d.ts.map +1 -0
- package/dist/types/agent.d.ts +202 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/client.d.ts +486 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/config.d.ts +31 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/errors.d.ts +50 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/hash.d.ts +32 -0
- package/dist/types/hash.d.ts.map +1 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/integrations.d.ts +25 -0
- package/dist/types/integrations.d.ts.map +1 -0
- package/dist/types/jacs.d.ts +26 -0
- package/dist/types/jacs.d.ts.map +1 -0
- package/dist/types/mime.d.ts +39 -0
- package/dist/types/mime.d.ts.map +1 -0
- package/dist/types/signing.d.ts +58 -0
- package/dist/types/signing.d.ts.map +1 -0
- package/dist/types/sse.d.ts +8 -0
- package/dist/types/sse.d.ts.map +1 -0
- package/dist/types/types.d.ts +652 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/verify.d.ts +20 -0
- package/dist/types/verify.d.ts.map +1 -0
- package/dist/types/ws.d.ts +30 -0
- package/dist/types/ws.d.ts.map +1 -0
- package/examples/a2a_quickstart.ts +138 -0
- package/examples/hai_quickstart.ts +111 -0
- package/examples/mcp_quickstart.ts +53 -0
- package/npm/@haiai/cli-darwin-arm64/package.json +16 -0
- package/npm/@haiai/cli-darwin-x64/package.json +16 -0
- package/npm/@haiai/cli-linux-arm64/package.json +16 -0
- package/npm/@haiai/cli-linux-x64/package.json +16 -0
- package/npm/@haiai/cli-win32-x64/package.json +16 -0
- package/package.json +68 -0
- package/scripts/build-platform-packages.js +132 -0
- package/scripts/smoke-package.cjs +114 -0
- package/scripts/write-cjs-package.cjs +9 -0
- package/src/a2a.ts +463 -0
- package/src/agent.ts +302 -0
- package/src/client.ts +2504 -0
- package/src/config.ts +204 -0
- package/src/errors.ts +99 -0
- package/src/hash.ts +66 -0
- package/src/index.ts +163 -0
- package/src/integrations.ts +210 -0
- package/src/jacs.ts +86 -0
- package/src/mime.ts +131 -0
- package/src/signing.ts +233 -0
- package/src/sse.ts +86 -0
- package/src/types.ts +773 -0
- package/src/verify.ts +89 -0
- package/src/ws.ts +198 -0
- package/tests/_debug_jacs.cjs +29 -0
- package/tests/a2a-contract.test.ts +271 -0
- package/tests/a2a-fixtures.test.ts +73 -0
- package/tests/a2a.test.ts +379 -0
- package/tests/binary.test.ts +90 -0
- package/tests/client-api-methods.test.ts +176 -0
- package/tests/client-path-escaping.test.ts +80 -0
- package/tests/client-register.test.ts +61 -0
- package/tests/config.test.ts +281 -0
- package/tests/contract.test.ts +360 -0
- package/tests/cross-lang-contract.test.ts +67 -0
- package/tests/email-conformance.test.ts +289 -0
- package/tests/email-integration.test.ts +217 -0
- package/tests/email.test.ts +767 -0
- package/tests/errors.test.ts +167 -0
- package/tests/init-contract.test.ts +129 -0
- package/tests/integrations.test.ts +132 -0
- package/tests/jacs-passthrough.test.ts +125 -0
- package/tests/key-cache.test.ts +201 -0
- package/tests/key-integration.test.ts +119 -0
- package/tests/key-lookups.test.ts +187 -0
- package/tests/key-rotation.test.ts +362 -0
- package/tests/mime.test.ts +127 -0
- package/tests/security.test.ts +109 -0
- package/tests/setup.ts +60 -0
- package/tests/signing.test.ts +142 -0
- package/tests/sse.test.ts +125 -0
- package/tests/types.test.ts +294 -0
- package/tests/verify-link.test.ts +81 -0
- package/tests/ws.test.ts +213 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.json +22 -0
- 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
|