@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,289 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { HaiClient } from '../src/client.js';
|
|
6
|
+
import { generateTestKeypair as generateKeypair } from './setup.js';
|
|
7
|
+
import {
|
|
8
|
+
EmailNotActiveError,
|
|
9
|
+
RecipientNotFoundError,
|
|
10
|
+
RateLimitedError,
|
|
11
|
+
} from '../src/errors.js';
|
|
12
|
+
import type {
|
|
13
|
+
EmailVerificationResultV2,
|
|
14
|
+
FieldStatus,
|
|
15
|
+
} from '../src/types.js';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Fixture loading
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
interface ConformanceFixture {
|
|
22
|
+
verification_result_v2_schema: {
|
|
23
|
+
required_fields: Record<string, string>;
|
|
24
|
+
field_status_values: string[];
|
|
25
|
+
};
|
|
26
|
+
api_contracts: {
|
|
27
|
+
sign_email: { method: string; path: string; request_content_type: string };
|
|
28
|
+
verify_email: { method: string; path: string; request_content_type: string };
|
|
29
|
+
send_email: { excluded_fields: string[] };
|
|
30
|
+
};
|
|
31
|
+
mock_verify_response: { json: Record<string, unknown> };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadConformanceFixture(): ConformanceFixture {
|
|
35
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const fixturePath = resolve(here, '../../fixtures/email_conformance.json');
|
|
37
|
+
return JSON.parse(readFileSync(fixturePath, 'utf-8')) as ConformanceFixture;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function makeClient(baseUrl: string): Promise<HaiClient> {
|
|
41
|
+
const keypair = generateKeypair();
|
|
42
|
+
const client = await HaiClient.fromCredentials('test-agent-001', keypair.privateKeyPem, { url: baseUrl, privateKeyPassphrase: 'keygen-password' });
|
|
43
|
+
client.agentEmail = 'test@hai.ai';
|
|
44
|
+
return client;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// EmailVerificationResultV2 structural conformance
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
describe('email conformance: mock verify response deserialization', () => {
|
|
52
|
+
const fixture = loadConformanceFixture();
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
vi.restoreAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('deserializes mock response into EmailVerificationResultV2 via verifyEmail', async () => {
|
|
59
|
+
const mockJson = fixture.mock_verify_response.json;
|
|
60
|
+
|
|
61
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
62
|
+
ok: true,
|
|
63
|
+
status: 200,
|
|
64
|
+
json: () => Promise.resolve(mockJson),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const client = await makeClient('https://mock.hai.ai');
|
|
68
|
+
const result: EmailVerificationResultV2 = await client.verifyEmail('raw email content');
|
|
69
|
+
|
|
70
|
+
expect(result.valid).toBe(true);
|
|
71
|
+
expect(result.jacsId).toBe('conformance-test-agent-001');
|
|
72
|
+
expect(result.algorithm).toBe('ed25519');
|
|
73
|
+
expect(result.reputationTier).toBe('established');
|
|
74
|
+
expect(result.dnsVerified).toBe(true);
|
|
75
|
+
expect(result.error).toBeNull();
|
|
76
|
+
|
|
77
|
+
// field_results
|
|
78
|
+
expect(result.fieldResults).toHaveLength(4);
|
|
79
|
+
expect(result.fieldResults[0].field).toBe('subject');
|
|
80
|
+
expect(result.fieldResults[0].status).toBe('pass');
|
|
81
|
+
expect(result.fieldResults[3].field).toBe('date');
|
|
82
|
+
expect(result.fieldResults[3].status).toBe('modified');
|
|
83
|
+
|
|
84
|
+
// chain
|
|
85
|
+
expect(result.chain).toHaveLength(1);
|
|
86
|
+
expect(result.chain[0].signer).toBe('agent@hai.ai');
|
|
87
|
+
expect(result.chain[0].jacsId).toBe('conformance-test-agent-001');
|
|
88
|
+
expect(result.chain[0].valid).toBe(true);
|
|
89
|
+
expect(result.chain[0].forwarded).toBe(false);
|
|
90
|
+
|
|
91
|
+
// agent_status and benchmarks_completed (TASK_012)
|
|
92
|
+
expect(result.agentStatus).toBe('active');
|
|
93
|
+
expect(result.benchmarksCompleted).toEqual(['free_chaotic']);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Content hash golden vector conformance (TASK_013)
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
describe('email conformance: content hash golden vectors', () => {
|
|
102
|
+
it('all golden vectors produce the expected content hash', async () => {
|
|
103
|
+
const { computeContentHash } = await import('../src/hash.js');
|
|
104
|
+
const fixture = loadConformanceFixture() as ConformanceFixture & {
|
|
105
|
+
content_hash_golden: {
|
|
106
|
+
vectors: Array<{
|
|
107
|
+
name: string;
|
|
108
|
+
subject: string;
|
|
109
|
+
body: string;
|
|
110
|
+
attachments: Array<{ filename: string; content_type: string; data_utf8: string }>;
|
|
111
|
+
expected_hash: string;
|
|
112
|
+
}>;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
for (const vector of fixture.content_hash_golden.vectors) {
|
|
117
|
+
const result = computeContentHash(vector.subject, vector.body, vector.attachments);
|
|
118
|
+
expect(result).toBe(vector.expected_hash);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// MIME round-trip conformance (TASK_014)
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
describe('email conformance: MIME round-trip content hash', () => {
|
|
128
|
+
it('produces expected content hash from round-trip input', async () => {
|
|
129
|
+
const { computeContentHash } = await import('../src/hash.js');
|
|
130
|
+
const fixture = loadConformanceFixture() as ConformanceFixture & {
|
|
131
|
+
mime_round_trip: {
|
|
132
|
+
input: {
|
|
133
|
+
subject: string;
|
|
134
|
+
body: string;
|
|
135
|
+
attachments: Array<{ filename: string; content_type: string; data_utf8: string }>;
|
|
136
|
+
};
|
|
137
|
+
expected_content_hash: string;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const { input, expected_content_hash: expectedHash } = fixture.mime_round_trip;
|
|
142
|
+
const result = computeContentHash(input.subject, input.body, input.attachments);
|
|
143
|
+
expect(result).toBe(expectedHash);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// FieldStatus enum conformance
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe('email conformance: FieldStatus values', () => {
|
|
152
|
+
const fixture = loadConformanceFixture();
|
|
153
|
+
|
|
154
|
+
it('all fixture field_status_values are valid FieldStatus literals', () => {
|
|
155
|
+
const validStatuses: FieldStatus[] = ['pass', 'modified', 'fail', 'unverifiable'];
|
|
156
|
+
for (const val of fixture.verification_result_v2_schema.field_status_values) {
|
|
157
|
+
expect(validStatuses).toContain(val);
|
|
158
|
+
}
|
|
159
|
+
expect(fixture.verification_result_v2_schema.field_status_values).toHaveLength(validStatuses.length);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// API contract conformance: SignEmail
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
describe('email conformance: signEmail API contract', () => {
|
|
168
|
+
const fixture = loadConformanceFixture();
|
|
169
|
+
|
|
170
|
+
afterEach(() => {
|
|
171
|
+
vi.restoreAllMocks();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('sends POST to correct path with message/rfc822 content-type', async () => {
|
|
175
|
+
let gotMethod = '';
|
|
176
|
+
let gotPath = '';
|
|
177
|
+
let gotContentType = '';
|
|
178
|
+
|
|
179
|
+
vi.stubGlobal('fetch', vi.fn().mockImplementation((url: string, init?: RequestInit) => {
|
|
180
|
+
const u = new URL(url);
|
|
181
|
+
gotMethod = init?.method ?? 'GET';
|
|
182
|
+
gotPath = u.pathname;
|
|
183
|
+
gotContentType = (init?.headers as Record<string, string>)?.['Content-Type'] ?? '';
|
|
184
|
+
return Promise.resolve({
|
|
185
|
+
ok: true,
|
|
186
|
+
status: 200,
|
|
187
|
+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
|
|
188
|
+
});
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
const client = await makeClient('https://mock.hai.ai');
|
|
192
|
+
await client.signEmail('raw email');
|
|
193
|
+
|
|
194
|
+
expect(gotMethod).toBe(fixture.api_contracts.sign_email.method);
|
|
195
|
+
expect(gotPath).toBe(fixture.api_contracts.sign_email.path);
|
|
196
|
+
expect(gotContentType).toBe(fixture.api_contracts.sign_email.request_content_type);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// API contract conformance: VerifyEmail
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
describe('email conformance: verifyEmail API contract', () => {
|
|
205
|
+
const fixture = loadConformanceFixture();
|
|
206
|
+
|
|
207
|
+
afterEach(() => {
|
|
208
|
+
vi.restoreAllMocks();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('sends POST to correct path with message/rfc822 content-type', async () => {
|
|
212
|
+
let gotMethod = '';
|
|
213
|
+
let gotPath = '';
|
|
214
|
+
let gotContentType = '';
|
|
215
|
+
|
|
216
|
+
vi.stubGlobal('fetch', vi.fn().mockImplementation((url: string, init?: RequestInit) => {
|
|
217
|
+
const u = new URL(url);
|
|
218
|
+
gotMethod = init?.method ?? 'GET';
|
|
219
|
+
gotPath = u.pathname;
|
|
220
|
+
gotContentType = (init?.headers as Record<string, string>)?.['Content-Type'] ?? '';
|
|
221
|
+
return Promise.resolve({
|
|
222
|
+
ok: true,
|
|
223
|
+
status: 200,
|
|
224
|
+
json: () => Promise.resolve(fixture.mock_verify_response.json),
|
|
225
|
+
});
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
const client = await makeClient('https://mock.hai.ai');
|
|
229
|
+
await client.verifyEmail('raw email');
|
|
230
|
+
|
|
231
|
+
expect(gotMethod).toBe(fixture.api_contracts.verify_email.method);
|
|
232
|
+
expect(gotPath).toBe(fixture.api_contracts.verify_email.path);
|
|
233
|
+
expect(gotContentType).toBe(fixture.api_contracts.verify_email.request_content_type);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// API contract conformance: SendEmail excluded fields
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
describe('email conformance: sendEmail excluded fields', () => {
|
|
242
|
+
const fixture = loadConformanceFixture();
|
|
243
|
+
|
|
244
|
+
afterEach(() => {
|
|
245
|
+
vi.restoreAllMocks();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('does not send client-side signing fields', async () => {
|
|
249
|
+
let gotBody: Record<string, unknown> = {};
|
|
250
|
+
|
|
251
|
+
vi.stubGlobal('fetch', vi.fn().mockImplementation((_url: string, init?: RequestInit) => {
|
|
252
|
+
if (init?.body) {
|
|
253
|
+
gotBody = JSON.parse(init.body as string) as Record<string, unknown>;
|
|
254
|
+
}
|
|
255
|
+
return Promise.resolve({
|
|
256
|
+
ok: true,
|
|
257
|
+
status: 200,
|
|
258
|
+
json: () => Promise.resolve({ message_id: 'msg-conf', status: 'sent' }),
|
|
259
|
+
});
|
|
260
|
+
}));
|
|
261
|
+
|
|
262
|
+
const client = await makeClient('https://mock.hai.ai');
|
|
263
|
+
await client.sendEmail({ to: 'bob@hai.ai', subject: 'Test', body: 'Body' });
|
|
264
|
+
|
|
265
|
+
for (const excluded of fixture.api_contracts.send_email.excluded_fields) {
|
|
266
|
+
expect(gotBody).not.toHaveProperty(excluded);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Error type conformance
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
describe('email conformance: error types', () => {
|
|
276
|
+
it('all email error sentinel classes exist', () => {
|
|
277
|
+
expect(EmailNotActiveError).toBeDefined();
|
|
278
|
+
expect(RecipientNotFoundError).toBeDefined();
|
|
279
|
+
expect(RateLimitedError).toBeDefined();
|
|
280
|
+
|
|
281
|
+
// Verify they are constructable
|
|
282
|
+
const e1 = new EmailNotActiveError('test');
|
|
283
|
+
const e2 = new RecipientNotFoundError('test');
|
|
284
|
+
const e3 = new RateLimitedError('test');
|
|
285
|
+
expect(e1.message).toBe('test');
|
|
286
|
+
expect(e2.message).toBe('test');
|
|
287
|
+
expect(e3.message).toBe('test');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live integration tests for HAI email CRUD operations.
|
|
3
|
+
*
|
|
4
|
+
* Gated behind HAI_LIVE_TEST=1. Requires a running HAI API at
|
|
5
|
+
* HAI_URL (defaults to http://localhost:3000) backed by Stalwart.
|
|
6
|
+
*
|
|
7
|
+
* Run:
|
|
8
|
+
* HAI_LIVE_TEST=1 HAI_URL=http://localhost:3000 npx vitest run tests/email-integration.test.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
12
|
+
import { HaiClient } from '../src/index.js';
|
|
13
|
+
import { generateTestKeypair as generateKeypair } from './setup.js';
|
|
14
|
+
import type { SendEmailResult, EmailMessage, EmailStatus } from '../src/types.js';
|
|
15
|
+
|
|
16
|
+
const LIVE = process.env.HAI_LIVE_TEST === '1';
|
|
17
|
+
const API_URL = process.env.HAI_URL || 'http://localhost:3000';
|
|
18
|
+
|
|
19
|
+
/** Small helper to wait for async email delivery to settle. */
|
|
20
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
21
|
+
|
|
22
|
+
describe.skipIf(!LIVE)('Email integration (live API)', () => {
|
|
23
|
+
let client: HaiClient;
|
|
24
|
+
const agentName = `node-integ-${Date.now()}`;
|
|
25
|
+
let sentMessageId: string;
|
|
26
|
+
let replyMessageId: string;
|
|
27
|
+
const subject = `node-integ-test-${Date.now()}`;
|
|
28
|
+
const body = 'Hello from Node integration test!';
|
|
29
|
+
|
|
30
|
+
// -------------------------------------------------------------------------
|
|
31
|
+
// Setup: register agent + claim username to provision @hai.ai email
|
|
32
|
+
// -------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
// 1. Generate fresh Ed25519 keypair.
|
|
36
|
+
const keypair = generateKeypair();
|
|
37
|
+
|
|
38
|
+
// 2. Build client from credentials (no jacs.config.json needed).
|
|
39
|
+
client = await HaiClient.fromCredentials(agentName, keypair.privateKeyPem, {
|
|
40
|
+
url: API_URL,
|
|
41
|
+
privateKeyPassphrase: 'keygen-password',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 3. Register the agent with the local API (bootstrap registration).
|
|
45
|
+
const ownerEmail = process.env.HAI_OWNER_EMAIL || 'jonathan@hai.io';
|
|
46
|
+
const result = await client.register({
|
|
47
|
+
description: 'Node SDK email integration test agent',
|
|
48
|
+
ownerEmail,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.success).toBe(true);
|
|
52
|
+
expect(result.jacsId).toBeTruthy();
|
|
53
|
+
expect(result.agentId).toBeTruthy();
|
|
54
|
+
console.log(`Registered agent: jacsId=${result.jacsId}, agentId=${result.agentId}`);
|
|
55
|
+
|
|
56
|
+
// 4. Claim a username to provision the @hai.ai email address.
|
|
57
|
+
const claim = await client.claimUsername(client.haiAgentId, agentName);
|
|
58
|
+
expect(claim.email).toContain('@hai.ai');
|
|
59
|
+
console.log(`Claimed username: ${claim.username}, email=${claim.email}`);
|
|
60
|
+
}, 30_000);
|
|
61
|
+
|
|
62
|
+
// -------------------------------------------------------------------------
|
|
63
|
+
// 1. Send email -> assert message_id
|
|
64
|
+
// -------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
it('should send an email and return a message_id', async () => {
|
|
67
|
+
const result: SendEmailResult = await client.sendEmail({
|
|
68
|
+
to: `${agentName}@hai.ai`,
|
|
69
|
+
subject,
|
|
70
|
+
body,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
sentMessageId = result.messageId;
|
|
74
|
+
expect(sentMessageId).toBeTruthy();
|
|
75
|
+
expect(typeof sentMessageId).toBe('string');
|
|
76
|
+
expect(result.status).toBeTruthy();
|
|
77
|
+
console.log(`Sent email: messageId=${sentMessageId}, status=${result.status}`);
|
|
78
|
+
|
|
79
|
+
// Allow time for async delivery before subsequent tests.
|
|
80
|
+
await sleep(2000);
|
|
81
|
+
}, 15_000);
|
|
82
|
+
|
|
83
|
+
// -------------------------------------------------------------------------
|
|
84
|
+
// 2. List messages -> assert sent message appears
|
|
85
|
+
// -------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
it('should list messages including the sent message', async () => {
|
|
88
|
+
const messages: EmailMessage[] = await client.listMessages({ limit: 50 });
|
|
89
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
90
|
+
|
|
91
|
+
// The sent message should appear in the list (either as outbound or
|
|
92
|
+
// delivered back to self as inbound).
|
|
93
|
+
const found = messages.some(
|
|
94
|
+
(m) => m.id === sentMessageId || m.subject === subject,
|
|
95
|
+
);
|
|
96
|
+
expect(found).toBe(true);
|
|
97
|
+
console.log(`Listed ${messages.length} messages; sent message found=${found}`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// -------------------------------------------------------------------------
|
|
101
|
+
// 3. Get message -> assert subject/body match
|
|
102
|
+
// -------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
it('should get the message by id with matching subject and body', async () => {
|
|
105
|
+
const msg: EmailMessage = await client.getMessage(sentMessageId);
|
|
106
|
+
expect(msg.id).toBe(sentMessageId);
|
|
107
|
+
expect(msg.subject).toBe(subject);
|
|
108
|
+
expect(msg.bodyText).toContain(body);
|
|
109
|
+
expect(msg.direction).toBeTruthy();
|
|
110
|
+
expect(msg.createdAt).toBeTruthy();
|
|
111
|
+
console.log(`Got message: id=${msg.id}, subject=${msg.subject}`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
// 4. Mark read / unread -> no error
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
it('should mark a message as read without error', async () => {
|
|
119
|
+
await client.markRead(sentMessageId);
|
|
120
|
+
|
|
121
|
+
// Verify the read state persisted.
|
|
122
|
+
const msg = await client.getMessage(sentMessageId);
|
|
123
|
+
expect(msg.isRead).toBe(true);
|
|
124
|
+
console.log('Marked read; isRead=' + msg.isRead);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should mark a message as unread without error', async () => {
|
|
128
|
+
await client.markUnread(sentMessageId);
|
|
129
|
+
|
|
130
|
+
// Verify the unread state persisted.
|
|
131
|
+
const msg = await client.getMessage(sentMessageId);
|
|
132
|
+
expect(msg.isRead).toBe(false);
|
|
133
|
+
console.log('Marked unread; isRead=' + msg.isRead);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// -------------------------------------------------------------------------
|
|
137
|
+
// 5. Search -> assert message found
|
|
138
|
+
// -------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
it('should search messages and find the sent message', async () => {
|
|
141
|
+
const results: EmailMessage[] = await client.searchMessages({ query: subject });
|
|
142
|
+
expect(results.length).toBeGreaterThan(0);
|
|
143
|
+
|
|
144
|
+
const found = results.some(
|
|
145
|
+
(m) => m.id === sentMessageId || m.subject === subject,
|
|
146
|
+
);
|
|
147
|
+
expect(found).toBe(true);
|
|
148
|
+
console.log(`Search found ${results.length} results; target found=${found}`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// -------------------------------------------------------------------------
|
|
152
|
+
// 6. Unread count -> assert returns number
|
|
153
|
+
// -------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
it('should return unread count as a number', async () => {
|
|
156
|
+
const count: number = await client.getUnreadCount();
|
|
157
|
+
expect(typeof count).toBe('number');
|
|
158
|
+
expect(count).toBeGreaterThanOrEqual(0);
|
|
159
|
+
console.log(`Unread count: ${count}`);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// -------------------------------------------------------------------------
|
|
163
|
+
// 7. Email status -> assert returns status
|
|
164
|
+
// -------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
it('should return email status with expected fields', async () => {
|
|
167
|
+
const status: EmailStatus = await client.getEmailStatus();
|
|
168
|
+
expect(status.email).toBeTruthy();
|
|
169
|
+
expect(status.email).toContain('@hai.ai');
|
|
170
|
+
expect(status.status).toBeTruthy();
|
|
171
|
+
expect(typeof status.dailyLimit).toBe('number');
|
|
172
|
+
expect(typeof status.dailyUsed).toBe('number');
|
|
173
|
+
expect(typeof status.messagesSentTotal).toBe('number');
|
|
174
|
+
console.log(
|
|
175
|
+
`Email status: email=${status.email}, status=${status.status}, tier=${status.tier}`,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// -------------------------------------------------------------------------
|
|
180
|
+
// 8. Reply -> assert reply message_id
|
|
181
|
+
// -------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
it('should reply to the sent message and return a reply message_id', async () => {
|
|
184
|
+
// reply() expects the internal message ID (used in URL path), not the
|
|
185
|
+
// RFC Message-ID. It fetches the original message, derives the sender
|
|
186
|
+
// and subject, then calls sendEmail with inReplyTo threading.
|
|
187
|
+
const result: SendEmailResult = await client.reply(
|
|
188
|
+
sentMessageId,
|
|
189
|
+
'Reply from Node integration test!',
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
replyMessageId = result.messageId;
|
|
193
|
+
expect(replyMessageId).toBeTruthy();
|
|
194
|
+
expect(typeof replyMessageId).toBe('string');
|
|
195
|
+
expect(result.status).toBeTruthy();
|
|
196
|
+
console.log(`Reply sent: messageId=${replyMessageId}, status=${result.status}`);
|
|
197
|
+
}, 15_000);
|
|
198
|
+
|
|
199
|
+
// -------------------------------------------------------------------------
|
|
200
|
+
// 9. Delete -> assert no error
|
|
201
|
+
// -------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
it('should delete the sent message without error', async () => {
|
|
204
|
+
// deleteMessage resolves void on success; any throw means failure.
|
|
205
|
+
await client.deleteMessage(sentMessageId);
|
|
206
|
+
console.log(`Deleted message: ${sentMessageId}`);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// -------------------------------------------------------------------------
|
|
210
|
+
// 10. Get deleted -> assert error/404
|
|
211
|
+
// -------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
it('should throw when getting a deleted message', async () => {
|
|
214
|
+
await expect(client.getMessage(sentMessageId)).rejects.toThrow();
|
|
215
|
+
console.log('Confirmed deleted message returns error');
|
|
216
|
+
});
|
|
217
|
+
});
|