@haiai/haiai 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +127 -0
  2. package/bin/haiai.cjs +70 -0
  3. package/dist/cjs/a2a.js +352 -0
  4. package/dist/cjs/a2a.js.map +1 -0
  5. package/dist/cjs/agent.js +236 -0
  6. package/dist/cjs/agent.js.map +1 -0
  7. package/dist/cjs/client.js +2168 -0
  8. package/dist/cjs/client.js.map +1 -0
  9. package/dist/cjs/config.js +176 -0
  10. package/dist/cjs/config.js.map +1 -0
  11. package/dist/cjs/errors.js +102 -0
  12. package/dist/cjs/errors.js.map +1 -0
  13. package/dist/cjs/hash.js +52 -0
  14. package/dist/cjs/hash.js.map +1 -0
  15. package/dist/cjs/index.js +84 -0
  16. package/dist/cjs/index.js.map +1 -0
  17. package/dist/cjs/integrations.js +193 -0
  18. package/dist/cjs/integrations.js.map +1 -0
  19. package/dist/cjs/jacs.js +66 -0
  20. package/dist/cjs/jacs.js.map +1 -0
  21. package/dist/cjs/mime.js +100 -0
  22. package/dist/cjs/mime.js.map +1 -0
  23. package/dist/cjs/package.json +3 -0
  24. package/dist/cjs/signing.js +190 -0
  25. package/dist/cjs/signing.js.map +1 -0
  26. package/dist/cjs/sse.js +76 -0
  27. package/dist/cjs/sse.js.map +1 -0
  28. package/dist/cjs/types.js +6 -0
  29. package/dist/cjs/types.js.map +1 -0
  30. package/dist/cjs/verify.js +76 -0
  31. package/dist/cjs/verify.js.map +1 -0
  32. package/dist/cjs/ws.js +206 -0
  33. package/dist/cjs/ws.js.map +1 -0
  34. package/dist/esm/a2a.js +305 -0
  35. package/dist/esm/a2a.js.map +1 -0
  36. package/dist/esm/agent.js +231 -0
  37. package/dist/esm/agent.js.map +1 -0
  38. package/dist/esm/client.js +2131 -0
  39. package/dist/esm/client.js.map +1 -0
  40. package/dist/esm/config.js +171 -0
  41. package/dist/esm/config.js.map +1 -0
  42. package/dist/esm/errors.js +88 -0
  43. package/dist/esm/errors.js.map +1 -0
  44. package/dist/esm/hash.js +49 -0
  45. package/dist/esm/hash.js.map +1 -0
  46. package/dist/esm/index.js +27 -0
  47. package/dist/esm/index.js.map +1 -0
  48. package/dist/esm/integrations.js +147 -0
  49. package/dist/esm/integrations.js.map +1 -0
  50. package/dist/esm/jacs.js +61 -0
  51. package/dist/esm/jacs.js.map +1 -0
  52. package/dist/esm/mime.js +97 -0
  53. package/dist/esm/mime.js.map +1 -0
  54. package/dist/esm/signing.js +183 -0
  55. package/dist/esm/signing.js.map +1 -0
  56. package/dist/esm/sse.js +73 -0
  57. package/dist/esm/sse.js.map +1 -0
  58. package/dist/esm/types.js +5 -0
  59. package/dist/esm/types.js.map +1 -0
  60. package/dist/esm/verify.js +72 -0
  61. package/dist/esm/verify.js.map +1 -0
  62. package/dist/esm/ws.js +168 -0
  63. package/dist/esm/ws.js.map +1 -0
  64. package/dist/types/a2a.d.ts +52 -0
  65. package/dist/types/a2a.d.ts.map +1 -0
  66. package/dist/types/agent.d.ts +202 -0
  67. package/dist/types/agent.d.ts.map +1 -0
  68. package/dist/types/client.d.ts +486 -0
  69. package/dist/types/client.d.ts.map +1 -0
  70. package/dist/types/config.d.ts +31 -0
  71. package/dist/types/config.d.ts.map +1 -0
  72. package/dist/types/errors.d.ts +50 -0
  73. package/dist/types/errors.d.ts.map +1 -0
  74. package/dist/types/hash.d.ts +32 -0
  75. package/dist/types/hash.d.ts.map +1 -0
  76. package/dist/types/index.d.ts +22 -0
  77. package/dist/types/index.d.ts.map +1 -0
  78. package/dist/types/integrations.d.ts +25 -0
  79. package/dist/types/integrations.d.ts.map +1 -0
  80. package/dist/types/jacs.d.ts +26 -0
  81. package/dist/types/jacs.d.ts.map +1 -0
  82. package/dist/types/mime.d.ts +39 -0
  83. package/dist/types/mime.d.ts.map +1 -0
  84. package/dist/types/signing.d.ts +58 -0
  85. package/dist/types/signing.d.ts.map +1 -0
  86. package/dist/types/sse.d.ts +8 -0
  87. package/dist/types/sse.d.ts.map +1 -0
  88. package/dist/types/types.d.ts +652 -0
  89. package/dist/types/types.d.ts.map +1 -0
  90. package/dist/types/verify.d.ts +20 -0
  91. package/dist/types/verify.d.ts.map +1 -0
  92. package/dist/types/ws.d.ts +30 -0
  93. package/dist/types/ws.d.ts.map +1 -0
  94. package/examples/a2a_quickstart.ts +138 -0
  95. package/examples/hai_quickstart.ts +111 -0
  96. package/examples/mcp_quickstart.ts +53 -0
  97. package/npm/@haiai/cli-darwin-arm64/package.json +16 -0
  98. package/npm/@haiai/cli-darwin-x64/package.json +16 -0
  99. package/npm/@haiai/cli-linux-arm64/package.json +16 -0
  100. package/npm/@haiai/cli-linux-x64/package.json +16 -0
  101. package/npm/@haiai/cli-win32-x64/package.json +16 -0
  102. package/package.json +68 -0
  103. package/scripts/build-platform-packages.js +132 -0
  104. package/scripts/smoke-package.cjs +114 -0
  105. package/scripts/write-cjs-package.cjs +9 -0
  106. package/src/a2a.ts +463 -0
  107. package/src/agent.ts +302 -0
  108. package/src/client.ts +2504 -0
  109. package/src/config.ts +204 -0
  110. package/src/errors.ts +99 -0
  111. package/src/hash.ts +66 -0
  112. package/src/index.ts +163 -0
  113. package/src/integrations.ts +210 -0
  114. package/src/jacs.ts +86 -0
  115. package/src/mime.ts +131 -0
  116. package/src/signing.ts +233 -0
  117. package/src/sse.ts +86 -0
  118. package/src/types.ts +773 -0
  119. package/src/verify.ts +89 -0
  120. package/src/ws.ts +198 -0
  121. package/tests/_debug_jacs.cjs +29 -0
  122. package/tests/a2a-contract.test.ts +271 -0
  123. package/tests/a2a-fixtures.test.ts +73 -0
  124. package/tests/a2a.test.ts +379 -0
  125. package/tests/binary.test.ts +90 -0
  126. package/tests/client-api-methods.test.ts +176 -0
  127. package/tests/client-path-escaping.test.ts +80 -0
  128. package/tests/client-register.test.ts +61 -0
  129. package/tests/config.test.ts +281 -0
  130. package/tests/contract.test.ts +360 -0
  131. package/tests/cross-lang-contract.test.ts +67 -0
  132. package/tests/email-conformance.test.ts +289 -0
  133. package/tests/email-integration.test.ts +217 -0
  134. package/tests/email.test.ts +767 -0
  135. package/tests/errors.test.ts +167 -0
  136. package/tests/init-contract.test.ts +129 -0
  137. package/tests/integrations.test.ts +132 -0
  138. package/tests/jacs-passthrough.test.ts +125 -0
  139. package/tests/key-cache.test.ts +201 -0
  140. package/tests/key-integration.test.ts +119 -0
  141. package/tests/key-lookups.test.ts +187 -0
  142. package/tests/key-rotation.test.ts +362 -0
  143. package/tests/mime.test.ts +127 -0
  144. package/tests/security.test.ts +109 -0
  145. package/tests/setup.ts +60 -0
  146. package/tests/signing.test.ts +142 -0
  147. package/tests/sse.test.ts +125 -0
  148. package/tests/types.test.ts +294 -0
  149. package/tests/verify-link.test.ts +81 -0
  150. package/tests/ws.test.ts +213 -0
  151. package/tsconfig.cjs.json +11 -0
  152. package/tsconfig.json +22 -0
  153. package/vitest.config.ts +11 -0
@@ -0,0 +1,360 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { HaiClient } from '../src/client.js';
7
+ import { generateTestKeypair as generateKeypair } from './setup.js';
8
+ import type { EmailMessage, EmailStatus, KeyRegistryResponse, EmailVerificationResult } from '../src/types.js';
9
+
10
+ interface EndpointContract {
11
+ method: string;
12
+ path: string;
13
+ auth_required: boolean;
14
+ }
15
+
16
+ interface ContractFixture {
17
+ base_url: string;
18
+ hello: EndpointContract;
19
+ check_username: EndpointContract;
20
+ submit_response: EndpointContract;
21
+ }
22
+
23
+ function loadContractFixture(): ContractFixture {
24
+ const here = dirname(fileURLToPath(import.meta.url));
25
+ const fixturePath = resolve(here, '../../fixtures/contract_endpoints.json');
26
+ return JSON.parse(readFileSync(fixturePath, 'utf-8')) as ContractFixture;
27
+ }
28
+
29
+ async function makeClient(baseUrl: string): Promise<HaiClient> {
30
+ const keypair = generateKeypair();
31
+ return HaiClient.fromCredentials('test-agent-001', keypair.privateKeyPem, { url: baseUrl, privateKeyPassphrase: 'keygen-password' });
32
+ }
33
+
34
+ describe('mock API contract (node)', () => {
35
+ afterEach(() => {
36
+ vi.unstubAllGlobals();
37
+ vi.restoreAllMocks();
38
+ });
39
+
40
+ it('hello uses the shared method/path/auth contract', async () => {
41
+ const contract = loadContractFixture();
42
+ const client = await makeClient(contract.base_url);
43
+
44
+ const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
45
+ expect(String(url)).toBe(`${contract.base_url}${contract.hello.path}`);
46
+ expect(init?.method).toBe(contract.hello.method);
47
+ const headers = (init?.headers ?? {}) as Record<string, string>;
48
+ if (contract.hello.auth_required) {
49
+ expect(headers.Authorization).toMatch(/^JACS /);
50
+ } else {
51
+ expect(headers.Authorization).toBeUndefined();
52
+ }
53
+ return new Response(JSON.stringify({
54
+ timestamp: '2026-01-01T00:00:00Z',
55
+ client_ip: '127.0.0.1',
56
+ hai_public_key_fingerprint: 'fp',
57
+ message: 'ok',
58
+ hello_id: 'h1',
59
+ }), {
60
+ status: 200,
61
+ headers: { 'Content-Type': 'application/json' },
62
+ });
63
+ });
64
+ vi.stubGlobal('fetch', fetchMock);
65
+
66
+ await client.hello();
67
+ });
68
+
69
+ it('checkUsername uses the shared method/path/auth contract', async () => {
70
+ const contract = loadContractFixture();
71
+ const client = await makeClient(contract.base_url);
72
+
73
+ const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
74
+ const parsed = new URL(String(url));
75
+ expect(parsed.origin + parsed.pathname).toBe(`${contract.base_url}${contract.check_username.path}`);
76
+ expect(parsed.searchParams.get('username')).toBe('alice');
77
+ expect(init?.method).toBe(contract.check_username.method);
78
+ const headers = (init?.headers ?? {}) as Record<string, string>;
79
+ if (contract.check_username.auth_required) {
80
+ expect(headers.Authorization).toMatch(/^JACS /);
81
+ } else {
82
+ expect(headers.Authorization).toBeUndefined();
83
+ }
84
+
85
+ return new Response(JSON.stringify({
86
+ available: true,
87
+ username: 'alice',
88
+ }), {
89
+ status: 200,
90
+ headers: { 'Content-Type': 'application/json' },
91
+ });
92
+ });
93
+ vi.stubGlobal('fetch', fetchMock);
94
+
95
+ await client.checkUsername('alice');
96
+ });
97
+
98
+ it('submitResponse uses the shared method/path/auth contract', async () => {
99
+ const contract = loadContractFixture();
100
+ const client = await makeClient(contract.base_url);
101
+ const jobId = 'job-123';
102
+ const expectedPath = contract.submit_response.path.replace('{job_id}', jobId);
103
+
104
+ const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
105
+ expect(String(url)).toBe(`${contract.base_url}${expectedPath}`);
106
+ expect(init?.method).toBe(contract.submit_response.method);
107
+ const headers = (init?.headers ?? {}) as Record<string, string>;
108
+ if (contract.submit_response.auth_required) {
109
+ expect(headers.Authorization).toMatch(/^JACS /);
110
+ } else {
111
+ expect(headers.Authorization).toBeUndefined();
112
+ }
113
+ return new Response(JSON.stringify({
114
+ success: true,
115
+ job_id: jobId,
116
+ message: 'ok',
117
+ }), {
118
+ status: 200,
119
+ headers: { 'Content-Type': 'application/json' },
120
+ });
121
+ });
122
+ vi.stubGlobal('fetch', fetchMock);
123
+
124
+ await client.submitResponse(jobId, 'response body');
125
+ });
126
+ });
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Email contract JSON helpers
130
+ // ---------------------------------------------------------------------------
131
+
132
+ function loadEmailContract(filename: string): Record<string, unknown> {
133
+ const here = dirname(fileURLToPath(import.meta.url));
134
+ const contractPath = resolve(here, '../../contract', filename);
135
+ return JSON.parse(readFileSync(contractPath, 'utf-8')) as Record<string, unknown>;
136
+ }
137
+
138
+ /**
139
+ * Replicates the private `HaiClient.parseEmailMessage` field mapping so we
140
+ * can validate contract JSON deserialization without needing a live client.
141
+ */
142
+ function parseEmailMessage(m: Record<string, unknown>): EmailMessage {
143
+ return {
144
+ id: (m.id as string) || '',
145
+ direction: (m.direction as string) || '',
146
+ fromAddress: (m.from_address as string) || '',
147
+ toAddress: (m.to_address as string) || '',
148
+ subject: (m.subject as string) || '',
149
+ bodyText: (m.body_text as string) || '',
150
+ messageId: (m.message_id as string) || '',
151
+ inReplyTo: (m.in_reply_to as string | null) ?? null,
152
+ isRead: (m.is_read as boolean) ?? false,
153
+ deliveryStatus: (m.delivery_status as string) || '',
154
+ createdAt: (m.created_at as string) || '',
155
+ readAt: (m.read_at as string | null) ?? null,
156
+ jacsVerified: (m.jacs_verified as boolean) ?? false,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Replicates the `getEmailStatus` response mapping from HaiClient.
162
+ */
163
+ function parseEmailStatus(data: Record<string, unknown>): EmailStatus {
164
+ return {
165
+ email: (data.email as string) || '',
166
+ status: (data.status as string) || '',
167
+ tier: (data.tier as string) || '',
168
+ billingTier: (data.billing_tier as string) || '',
169
+ messagesSent24h: (data.messages_sent_24h as number) || 0,
170
+ dailyLimit: (data.daily_limit as number) || 0,
171
+ dailyUsed: (data.daily_used as number) || 0,
172
+ resetsAt: (data.resets_at as string) || '',
173
+ messagesSentTotal: (data.messages_sent_total as number) || 0,
174
+ externalEnabled: (data.external_enabled as boolean) ?? false,
175
+ externalSendsToday: (data.external_sends_today as number) ?? 0,
176
+ lastTierChange: (data.last_tier_change as string | null) ?? null,
177
+ };
178
+ }
179
+
180
+ function parseKeyRegistryResponse(data: Record<string, unknown>): KeyRegistryResponse {
181
+ return {
182
+ email: (data.email as string) || '',
183
+ jacsId: (data.jacs_id as string) || '',
184
+ publicKey: (data.public_key as string) || '',
185
+ algorithm: (data.algorithm as string) || '',
186
+ reputationTier: (data.reputation_tier as string) || '',
187
+ registeredAt: (data.registered_at as string) || '',
188
+ };
189
+ }
190
+
191
+ function parseEmailVerificationResult(data: Record<string, unknown>): EmailVerificationResult {
192
+ return {
193
+ valid: (data.valid as boolean) ?? false,
194
+ jacsId: (data.jacs_id as string) || '',
195
+ reputationTier: (data.reputation_tier as string) || '',
196
+ error: (data.error as string | null) ?? null,
197
+ };
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Email contract deserialization tests
202
+ // ---------------------------------------------------------------------------
203
+
204
+ describe('contract: deserialize email message', () => {
205
+ it('maps all snake_case API fields to camelCase SDK fields', () => {
206
+ const raw = loadEmailContract('email_message.json');
207
+ const msg = parseEmailMessage(raw);
208
+
209
+ expect(msg.id).toBe('550e8400-e29b-41d4-a716-446655440000');
210
+ expect(msg.direction).toBe('inbound');
211
+ expect(msg.fromAddress).toBe('sender@hai.ai');
212
+ expect(msg.toAddress).toBe('recipient@hai.ai');
213
+ expect(msg.subject).toBe('Test Subject');
214
+ expect(msg.bodyText).toBe('Hello, this is a test email body.');
215
+ expect(msg.messageId).toBe('<550e8400@hai.ai>');
216
+ expect(msg.inReplyTo).toBeNull();
217
+ expect(msg.isRead).toBe(false);
218
+ expect(msg.deliveryStatus).toBe('delivered');
219
+ expect(msg.createdAt).toBe('2026-02-24T12:00:00Z');
220
+ expect(msg.readAt).toBeNull();
221
+ expect(msg.jacsVerified).toBe(true);
222
+ });
223
+ });
224
+
225
+ describe('contract: deserialize list messages response', () => {
226
+ it('parses messages array, total, and unread from envelope', () => {
227
+ const raw = loadEmailContract('list_messages_response.json');
228
+ const messagesRaw = raw.messages as Array<Record<string, unknown>>;
229
+ const messages = messagesRaw.map((m) => parseEmailMessage(m));
230
+
231
+ expect(messages).toHaveLength(1);
232
+ expect(raw.total).toBe(1);
233
+ expect(raw.unread).toBe(1);
234
+
235
+ // Verify the nested message was deserialized correctly
236
+ expect(messages[0].id).toBe('550e8400-e29b-41d4-a716-446655440000');
237
+ expect(messages[0].fromAddress).toBe('sender@hai.ai');
238
+ expect(messages[0].jacsVerified).toBe(true);
239
+ });
240
+ });
241
+
242
+ describe('contract: deserialize email status', () => {
243
+ it('maps all snake_case API fields to camelCase EmailStatus', () => {
244
+ const raw = loadEmailContract('email_status_response.json');
245
+ const status = parseEmailStatus(raw);
246
+
247
+ expect(status.email).toBe('testbot@hai.ai');
248
+ expect(status.status).toBe('active');
249
+ expect(status.tier).toBe('new');
250
+ expect(status.billingTier).toBe('free');
251
+ expect(status.messagesSent24h).toBe(5);
252
+ expect(status.dailyLimit).toBe(10);
253
+ expect(status.dailyUsed).toBe(5);
254
+ expect(status.resetsAt).toBe('2026-02-25T00:00:00Z');
255
+ expect(status.messagesSentTotal).toBe(42);
256
+ expect(status.externalEnabled).toBe(false);
257
+ expect(status.externalSendsToday).toBe(0);
258
+ expect(status.lastTierChange).toBeNull();
259
+ });
260
+ });
261
+
262
+ describe('contract: deserialize key registry response', () => {
263
+ it('maps all snake_case API fields to camelCase KeyRegistryResponse', () => {
264
+ const raw = loadEmailContract('key_registry_response.json');
265
+ const resp = parseKeyRegistryResponse(raw);
266
+
267
+ expect(resp.email).toBe('testbot@hai.ai');
268
+ expect(resp.jacsId).toBe('test-agent-jacs-id');
269
+ expect(resp.publicKey).toBe('MCowBQYDK2VwAyEAExampleBase64PublicKeyData1234567890ABCDEF');
270
+ expect(resp.algorithm).toBe('ed25519');
271
+ expect(resp.reputationTier).toBe('new');
272
+ expect(resp.registeredAt).toBe('2026-01-15T00:00:00Z');
273
+ });
274
+ });
275
+
276
+ describe('contract: deserialize verification result', () => {
277
+ it('maps all fields correctly', () => {
278
+ const raw = loadEmailContract('verification_result.json');
279
+ const result = parseEmailVerificationResult(raw);
280
+
281
+ expect(result.valid).toBe(true);
282
+ expect(result.jacsId).toBe('test-agent-jacs-id');
283
+ expect(result.reputationTier).toBe('established');
284
+ expect(result.error).toBeNull();
285
+ });
286
+ });
287
+
288
+ describe('contract: content hash computation', () => {
289
+ it('computes the same sha256 hash as the contract fixture', () => {
290
+ const fixture = loadEmailContract('content_hash_example.json');
291
+ const subject = fixture.subject as string;
292
+ const body = fixture.body as string;
293
+ const expectedHash = fixture.expected_hash as string;
294
+
295
+ const computed = 'sha256:' + createHash('sha256')
296
+ .update(subject + '\n' + body, 'utf8')
297
+ .digest('hex');
298
+
299
+ expect(computed).toBe(expectedHash);
300
+ });
301
+ });
302
+
303
+ describe('contract: sign input format', () => {
304
+ it('produces the correct sign_input from content hash and timestamp', () => {
305
+ const fixture = loadEmailContract('content_hash_example.json');
306
+ const expectedHash = fixture.expected_hash as string;
307
+ const fromEmail = fixture.from_email as string;
308
+ const timestamp = fixture.timestamp as number;
309
+ const expectedSignInput = fixture.sign_input_example as string;
310
+
311
+ const signInput = `${expectedHash}:${fromEmail}:${timestamp}`;
312
+
313
+ expect(signInput).toBe(expectedSignInput);
314
+ });
315
+ });
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Versioned key lookup contract fixture
319
+ // ---------------------------------------------------------------------------
320
+
321
+ function loadFixture(filename: string): Record<string, unknown> {
322
+ const here = dirname(fileURLToPath(import.meta.url));
323
+ const fixturePath = resolve(here, '../../contract', filename);
324
+ return JSON.parse(readFileSync(fixturePath, 'utf-8')) as Record<string, unknown>;
325
+ }
326
+
327
+ function parsePublicKeyInfo(data: Record<string, unknown>): PublicKeyInfo {
328
+ return {
329
+ jacsId: (data.jacs_id as string) || '',
330
+ version: (data.version as string) || '',
331
+ publicKey: (data.public_key as string) || '',
332
+ publicKeyRawB64: (data.public_key_raw_b64 as string) || '',
333
+ algorithm: (data.algorithm as string) || '',
334
+ publicKeyHash: (data.public_key_hash as string) || '',
335
+ status: (data.status as string) || '',
336
+ dnsVerified: (data.dns_verified as boolean) ?? false,
337
+ createdAt: (data.created_at as string) || '',
338
+ };
339
+ }
340
+
341
+ describe('contract: deserialize versioned key lookup response', () => {
342
+ it('maps all snake_case API fields to camelCase PublicKeyInfo', () => {
343
+ const fixture = loadFixture('key_lookup_versioned_response.json');
344
+ const resp = fixture.response as Record<string, unknown>;
345
+ const info = parsePublicKeyInfo(resp);
346
+
347
+ expect(info.jacsId).toBe('fixture-agent-00000000-0000-0000-0000-000000000001');
348
+ expect(info.version).toBe('fixture-version-00000000-0000-0000-0000-000000000001');
349
+ expect(info.publicKey).toMatch(/^-----BEGIN PUBLIC KEY-----/);
350
+ expect(info.publicKey).toMatch(/-----END PUBLIC KEY-----$/);
351
+ expect(info.algorithm).toBe('ed25519');
352
+ expect(info.publicKeyHash).toMatch(/^sha256:[a-f0-9]{64}$/);
353
+ expect(info.status).toBe('active');
354
+ expect(info.dnsVerified).toBe(true);
355
+ expect(info.createdAt).toBe('2026-01-01T00:00:00Z');
356
+ expect(info.publicKeyRawB64).toBeTruthy();
357
+ // Verify base64 PEM field is present
358
+ expect(resp.public_key_b64).toBeTruthy();
359
+ });
360
+ });
@@ -0,0 +1,67 @@
1
+ import { 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 { canonicalJson } from '../src/signing.js';
7
+
8
+ interface CrossLangFixture {
9
+ auth_header: {
10
+ scheme: string;
11
+ parts: string[];
12
+ signed_message_template: string;
13
+ example: {
14
+ jacs_id: string;
15
+ timestamp: number;
16
+ stub_signature_base64: string;
17
+ expected_header: string;
18
+ };
19
+ };
20
+ canonical_json_cases: Array<{
21
+ name: string;
22
+ input: unknown;
23
+ expected: string;
24
+ }>;
25
+ }
26
+
27
+ function loadFixture(): CrossLangFixture {
28
+ const here = dirname(fileURLToPath(import.meta.url));
29
+ const fixturePath = resolve(here, '../../fixtures/cross_lang_test.json');
30
+ return JSON.parse(readFileSync(fixturePath, 'utf-8')) as CrossLangFixture;
31
+ }
32
+
33
+ describe('cross-language wrapper contract (node)', () => {
34
+ it('matches the shared canonical JSON cases', () => {
35
+ const fixture = loadFixture();
36
+ for (const testCase of fixture.canonical_json_cases) {
37
+ expect(canonicalJson(testCase.input), testCase.name).toBe(testCase.expected);
38
+ }
39
+ });
40
+
41
+ it('matches the shared auth header example', () => {
42
+ const fixture = loadFixture();
43
+ const client = Object.create(HaiClient.prototype) as HaiClient & {
44
+ agent: { signStringSync: (message: string) => string };
45
+ config: { jacsId: string; jacsAgentName: string };
46
+ };
47
+ const signStringSync = vi.fn(() => fixture.auth_header.example.stub_signature_base64);
48
+
49
+ client.agent = { signStringSync };
50
+ client.config = {
51
+ jacsId: fixture.auth_header.example.jacs_id,
52
+ jacsAgentName: fixture.auth_header.example.jacs_id,
53
+ };
54
+
55
+ vi.useFakeTimers();
56
+ vi.setSystemTime(fixture.auth_header.example.timestamp * 1000);
57
+
58
+ expect(client.buildAuthHeader()).toBe(fixture.auth_header.example.expected_header);
59
+ expect(signStringSync).toHaveBeenCalledWith(
60
+ fixture.auth_header.signed_message_template
61
+ .replace('{jacs_id}', fixture.auth_header.example.jacs_id)
62
+ .replace('{timestamp}', String(fixture.auth_header.example.timestamp)),
63
+ );
64
+
65
+ vi.useRealTimers();
66
+ });
67
+ });