hiloop-sdk 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +3 -0
- package/dist/client.js +17 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/ws.d.ts +63 -0
- package/dist/ws.js +188 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare class HiloopError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
export interface HiloopClientOptions {
|
|
9
9
|
apiKey: string;
|
|
10
|
+
/** Agent name. Auto-creates the agent if it doesn't exist (space API keys only). */
|
|
11
|
+
agentName?: string;
|
|
10
12
|
baseUrl?: string;
|
|
11
13
|
/** Agent's X25519 secret key (base64). Required for E2E encryption. */
|
|
12
14
|
secretKey?: string;
|
|
@@ -38,6 +40,7 @@ export declare class HiloopClient {
|
|
|
38
40
|
private ensureInitialized;
|
|
39
41
|
private doInit;
|
|
40
42
|
/** Raw HTTP request without triggering auto-init (used during init itself). */
|
|
43
|
+
private buildHeaders;
|
|
41
44
|
private rawRequest;
|
|
42
45
|
private encryptField;
|
|
43
46
|
/**
|
package/dist/client.js
CHANGED
|
@@ -87,15 +87,20 @@ export class HiloopClient {
|
|
|
87
87
|
this.initialized = true;
|
|
88
88
|
}
|
|
89
89
|
/** Raw HTTP request without triggering auto-init (used during init itself). */
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
const headers = {
|
|
90
|
+
buildHeaders() {
|
|
91
|
+
const h = {
|
|
93
92
|
"Content-Type": "application/json",
|
|
94
93
|
"X-API-Key": this.apiKey,
|
|
95
94
|
};
|
|
95
|
+
if (this.options.agentName)
|
|
96
|
+
h["X-Agent"] = this.options.agentName;
|
|
97
|
+
return h;
|
|
98
|
+
}
|
|
99
|
+
async rawRequest(method, path, options) {
|
|
100
|
+
const url = `${this.baseUrl}/v1${path}`;
|
|
96
101
|
const res = await fetch(url, {
|
|
97
102
|
method,
|
|
98
|
-
headers,
|
|
103
|
+
headers: this.buildHeaders(),
|
|
99
104
|
body: options?.body ? JSON.stringify(options.body) : undefined,
|
|
100
105
|
});
|
|
101
106
|
if (!res.ok) {
|
|
@@ -115,6 +120,7 @@ export class HiloopClient {
|
|
|
115
120
|
* with the same (scope, recipientKeyId) are silently dropped.
|
|
116
121
|
*/
|
|
117
122
|
async encryptFields(fields) {
|
|
123
|
+
await this.ensureInitialized();
|
|
118
124
|
const encrypted = {};
|
|
119
125
|
const entries = Object.entries(fields).filter(([, v]) => v !== undefined);
|
|
120
126
|
if (entries.length === 0)
|
|
@@ -158,10 +164,7 @@ export class HiloopClient {
|
|
|
158
164
|
try {
|
|
159
165
|
const res = await fetch(url, {
|
|
160
166
|
method,
|
|
161
|
-
headers:
|
|
162
|
-
"X-API-Key": this.apiKey,
|
|
163
|
-
"Content-Type": "application/json",
|
|
164
|
-
},
|
|
167
|
+
headers: this.buildHeaders(),
|
|
165
168
|
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
166
169
|
signal: controller.signal,
|
|
167
170
|
});
|
|
@@ -184,7 +187,7 @@ export class HiloopClient {
|
|
|
184
187
|
try {
|
|
185
188
|
const res = await fetch(url, {
|
|
186
189
|
method: "GET",
|
|
187
|
-
headers:
|
|
190
|
+
headers: this.buildHeaders(),
|
|
188
191
|
signal: controller.signal,
|
|
189
192
|
});
|
|
190
193
|
if (!res.ok) {
|
|
@@ -358,6 +361,7 @@ export class HiloopClient {
|
|
|
358
361
|
}
|
|
359
362
|
/** Push content blocks to an interaction. */
|
|
360
363
|
async pushContentBlocks(interactionId, blocks) {
|
|
364
|
+
await this.ensureInitialized();
|
|
361
365
|
const encryptedBlocks = await Promise.all(blocks.map(async (block) => {
|
|
362
366
|
const plaintext = JSON.stringify(block.data);
|
|
363
367
|
const wrapped = await this.crypto.encryptWithWrapping(plaintext);
|
|
@@ -488,6 +492,7 @@ export class HiloopClient {
|
|
|
488
492
|
}
|
|
489
493
|
// -- Conversation Sessions -------------------------------------------------
|
|
490
494
|
async createConvSession(opts) {
|
|
495
|
+
await this.ensureInitialized();
|
|
491
496
|
const body = {
|
|
492
497
|
sessionType: opts.sessionType ?? "direct",
|
|
493
498
|
isPublic: opts.isPublic ?? false,
|
|
@@ -547,6 +552,7 @@ export class HiloopClient {
|
|
|
547
552
|
* using the session message key (`{agentPub}:{AES-GCM ciphertext}` format).
|
|
548
553
|
*/
|
|
549
554
|
async sendConvSessionMessage(sessionId, content) {
|
|
555
|
+
await this.ensureInitialized();
|
|
550
556
|
const encryptedContent = await this.crypto.encryptSessionMessage(content);
|
|
551
557
|
const data = await this.request("POST", `/agent/sessions/${sessionId}/messages`, { body: { encryptedContent } });
|
|
552
558
|
return parseConvSessionMessage(data);
|
|
@@ -1039,6 +1045,7 @@ export class HiloopClient {
|
|
|
1039
1045
|
// -- Channels ---------------------------------------------------------------
|
|
1040
1046
|
/** Create a new agent-to-agent channel. */
|
|
1041
1047
|
async createChannel(opts) {
|
|
1048
|
+
await this.ensureInitialized();
|
|
1042
1049
|
const body = {};
|
|
1043
1050
|
if (opts.name !== undefined) {
|
|
1044
1051
|
const w = await this.crypto.encryptWithWrapping(opts.name);
|
|
@@ -1095,6 +1102,7 @@ export class HiloopClient {
|
|
|
1095
1102
|
}
|
|
1096
1103
|
/** Send a message in a channel. */
|
|
1097
1104
|
async sendChannelMessage(channelId, content) {
|
|
1105
|
+
await this.ensureInitialized();
|
|
1098
1106
|
const wrapped = await this.crypto.encryptWithWrapping(content);
|
|
1099
1107
|
const body = {
|
|
1100
1108
|
encryptedContent: wrapped.ciphertext,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { HiloopClient, HiloopError } from "./client.js";
|
|
2
2
|
export type { HiloopClientOptions } from "./client.js";
|
|
3
|
+
export { HiloopWsClient } from "./ws.js";
|
|
4
|
+
export type { HiloopWsClientOptions, WsEventType, WsEventHandler } from "./ws.js";
|
|
3
5
|
export { generateKeyPair, deriveKeyPairFromApiKey, computeFingerprint, encrypt, decrypt, encryptAes, decryptAes, deriveWrappingKey, CryptoContext } from "./crypto.js";
|
|
4
6
|
export type { KeyPair, ContentWrappingResult } from "./crypto.js";
|
|
5
7
|
export type { Interaction, InteractionType, InteractionStatus, Priority, Message, PaginatedResult, CreateInteractionOptions, ConvSession, ConvSessionType, ConvSessionStatus, ConvSessionRole, ConvSessionParticipant, ConvSessionMessage, GuestToken, CreateConvSessionOptions, Channel, ChannelStatus, ChannelMessage, ChannelParticipant, CreateChannelOptions, FileChangeStatus, CommandInvocationStatus, AgentCommand, CommandInvocation, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { HiloopClient, HiloopError } from "./client.js";
|
|
2
|
+
export { HiloopWsClient } from "./ws.js";
|
|
2
3
|
export { generateKeyPair, deriveKeyPairFromApiKey, computeFingerprint, encrypt, decrypt, encryptAes, decryptAes, deriveWrappingKey, CryptoContext } from "./crypto.js";
|
|
3
4
|
export { parseInteraction, parseMessage, parseConvSession, parseConvSessionMessage, parseGuestToken, parseChannel, parseChannelMessage, parseChannelParticipant, } from "./types.js";
|
package/dist/ws.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HiloopWsClient — real-time WebSocket client for agents.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const ws = new HiloopWsClient({
|
|
6
|
+
* apiKey: "hlp_space_...",
|
|
7
|
+
* agentName: "my-bot",
|
|
8
|
+
* });
|
|
9
|
+
* ws.on("session.message.new", (msg) => console.log(msg.content));
|
|
10
|
+
* await ws.connect();
|
|
11
|
+
* ws.subscribe(`session:${sessionId}`);
|
|
12
|
+
* ws.sendSessionMessage(sessionId, "Hello!");
|
|
13
|
+
*/
|
|
14
|
+
export interface HiloopWsClientOptions {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
/** Agent name (required for space API keys, auto-creates if needed). */
|
|
17
|
+
agentName?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
/** Auto-reconnect on disconnect (default: true). */
|
|
20
|
+
autoReconnect?: boolean;
|
|
21
|
+
/** Max reconnect delay in ms (default: 30000). */
|
|
22
|
+
maxReconnectDelay?: number;
|
|
23
|
+
}
|
|
24
|
+
export type WsEventType = "connected" | "session.message.new" | "session.message.sent" | "interaction.created" | "interaction.sent" | "interaction.updated" | "interaction.message.new" | "typing.start" | "typing.stop" | "session.closed" | "session.member.joined" | "session.member.left" | "subscribed" | "unsubscribed" | "error" | "pong";
|
|
25
|
+
export type WsEventHandler = (data: any) => void;
|
|
26
|
+
export declare class HiloopWsClient {
|
|
27
|
+
private ws;
|
|
28
|
+
private options;
|
|
29
|
+
private handlers;
|
|
30
|
+
private anyHandlers;
|
|
31
|
+
private reconnectAttempts;
|
|
32
|
+
private reconnectTimer;
|
|
33
|
+
private closed;
|
|
34
|
+
private crypto;
|
|
35
|
+
private initPromise;
|
|
36
|
+
constructor(options: HiloopWsClientOptions);
|
|
37
|
+
/** Initialize encryption context (derives keypair from API key, fetches space key). */
|
|
38
|
+
private ensureCrypto;
|
|
39
|
+
private doInitCrypto;
|
|
40
|
+
/** Connect to the agent WebSocket. Returns when connected. */
|
|
41
|
+
connect(): Promise<void>;
|
|
42
|
+
/** Disconnect and stop reconnecting. */
|
|
43
|
+
disconnect(): void;
|
|
44
|
+
/** Subscribe to a topic (e.g., `session:<id>`, `agent:<id>`, `space:<id>`). */
|
|
45
|
+
subscribe(topic: string): void;
|
|
46
|
+
/** Unsubscribe from a topic. */
|
|
47
|
+
unsubscribe(topic: string): void;
|
|
48
|
+
/** Send a session message (auto-encrypts). */
|
|
49
|
+
sendSessionMessage(sessionId: string, content: string): Promise<void>;
|
|
50
|
+
/** Send a typing indicator. */
|
|
51
|
+
sendTyping(sessionId: string, isTyping: boolean): void;
|
|
52
|
+
/** Listen for a specific event type. */
|
|
53
|
+
on(eventType: WsEventType | string, handler: WsEventHandler): () => void;
|
|
54
|
+
/** Listen for all events. */
|
|
55
|
+
onAny(handler: WsEventHandler): () => void;
|
|
56
|
+
/** Remove all listeners for an event type. */
|
|
57
|
+
off(eventType: string): void;
|
|
58
|
+
/** Auto-decrypt a session message's encryptedContent field. */
|
|
59
|
+
decryptSessionMessage(encryptedContent: string): Promise<string | null>;
|
|
60
|
+
private send;
|
|
61
|
+
private emit;
|
|
62
|
+
private scheduleReconnect;
|
|
63
|
+
}
|
package/dist/ws.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HiloopWsClient — real-time WebSocket client for agents.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const ws = new HiloopWsClient({
|
|
6
|
+
* apiKey: "hlp_space_...",
|
|
7
|
+
* agentName: "my-bot",
|
|
8
|
+
* });
|
|
9
|
+
* ws.on("session.message.new", (msg) => console.log(msg.content));
|
|
10
|
+
* await ws.connect();
|
|
11
|
+
* ws.subscribe(`session:${sessionId}`);
|
|
12
|
+
* ws.sendSessionMessage(sessionId, "Hello!");
|
|
13
|
+
*/
|
|
14
|
+
import { CryptoContext, deriveKeyPairFromApiKey, computeFingerprint } from "./crypto.js";
|
|
15
|
+
export class HiloopWsClient {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.ws = null;
|
|
18
|
+
this.handlers = new Map();
|
|
19
|
+
this.anyHandlers = new Set();
|
|
20
|
+
this.reconnectAttempts = 0;
|
|
21
|
+
this.reconnectTimer = null;
|
|
22
|
+
this.closed = false;
|
|
23
|
+
this.crypto = null;
|
|
24
|
+
this.initPromise = null;
|
|
25
|
+
this.options = options;
|
|
26
|
+
}
|
|
27
|
+
/** Initialize encryption context (derives keypair from API key, fetches space key). */
|
|
28
|
+
async ensureCrypto() {
|
|
29
|
+
if (this.crypto)
|
|
30
|
+
return;
|
|
31
|
+
if (this.initPromise)
|
|
32
|
+
return this.initPromise;
|
|
33
|
+
this.initPromise = this.doInitCrypto();
|
|
34
|
+
await this.initPromise;
|
|
35
|
+
}
|
|
36
|
+
async doInitCrypto() {
|
|
37
|
+
const kp = await deriveKeyPairFromApiKey(this.options.apiKey);
|
|
38
|
+
const baseUrl = (this.options.baseUrl ?? "https://api.hi-loop.com").replace(/\/$/, "");
|
|
39
|
+
const headers = {
|
|
40
|
+
"X-API-Key": this.options.apiKey,
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
};
|
|
43
|
+
if (this.options.agentName)
|
|
44
|
+
headers["X-Agent"] = this.options.agentName;
|
|
45
|
+
// Register public key
|
|
46
|
+
const fingerprint = await computeFingerprint(kp.publicKey);
|
|
47
|
+
try {
|
|
48
|
+
await fetch(`${baseUrl}/v1/agent/keys/me`, { method: "PUT", headers, body: JSON.stringify({ publicKey: kp.publicKey, fingerprint }) });
|
|
49
|
+
}
|
|
50
|
+
catch { /* non-fatal */ }
|
|
51
|
+
// Fetch space encryption info
|
|
52
|
+
let spacePublicKey = "";
|
|
53
|
+
let spaceKeyId = "";
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(`${baseUrl}/v1/agent/encryption-info`, { headers });
|
|
56
|
+
if (res.ok) {
|
|
57
|
+
const info = await res.json();
|
|
58
|
+
spacePublicKey = info.spacePublicKey;
|
|
59
|
+
spaceKeyId = info.spaceKeyId;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { /* non-fatal */ }
|
|
63
|
+
this.crypto = new CryptoContext(kp.secretKey, kp.publicKey, spacePublicKey, spaceKeyId);
|
|
64
|
+
}
|
|
65
|
+
/** Connect to the agent WebSocket. Returns when connected. */
|
|
66
|
+
async connect() {
|
|
67
|
+
await this.ensureCrypto();
|
|
68
|
+
this.closed = false;
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const baseUrl = (this.options.baseUrl ?? "https://api.hi-loop.com").replace(/\/$/, "");
|
|
71
|
+
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
72
|
+
const params = new URLSearchParams({ key: this.options.apiKey });
|
|
73
|
+
if (this.options.agentName)
|
|
74
|
+
params.set("agent", this.options.agentName);
|
|
75
|
+
this.ws = new WebSocket(`${wsUrl}/v1/agent/ws?${params}`);
|
|
76
|
+
this.ws.onopen = () => {
|
|
77
|
+
this.reconnectAttempts = 0;
|
|
78
|
+
};
|
|
79
|
+
this.ws.onmessage = (event) => {
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(event.data);
|
|
82
|
+
if (data.type === "connected") {
|
|
83
|
+
resolve();
|
|
84
|
+
}
|
|
85
|
+
this.emit(data.type, data);
|
|
86
|
+
}
|
|
87
|
+
catch { /* ignore malformed */ }
|
|
88
|
+
};
|
|
89
|
+
this.ws.onclose = () => {
|
|
90
|
+
if (!this.closed && (this.options.autoReconnect ?? true)) {
|
|
91
|
+
this.scheduleReconnect();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
this.ws.onerror = (err) => {
|
|
95
|
+
if (this.reconnectAttempts === 0) {
|
|
96
|
+
reject(err);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Disconnect and stop reconnecting. */
|
|
102
|
+
disconnect() {
|
|
103
|
+
this.closed = true;
|
|
104
|
+
if (this.reconnectTimer) {
|
|
105
|
+
clearTimeout(this.reconnectTimer);
|
|
106
|
+
this.reconnectTimer = null;
|
|
107
|
+
}
|
|
108
|
+
if (this.ws) {
|
|
109
|
+
this.ws.close();
|
|
110
|
+
this.ws = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Subscribe to a topic (e.g., `session:<id>`, `agent:<id>`, `space:<id>`). */
|
|
114
|
+
subscribe(topic) {
|
|
115
|
+
this.send({ type: "subscribe", topic });
|
|
116
|
+
}
|
|
117
|
+
/** Unsubscribe from a topic. */
|
|
118
|
+
unsubscribe(topic) {
|
|
119
|
+
this.send({ type: "unsubscribe", topic });
|
|
120
|
+
}
|
|
121
|
+
/** Send a session message (auto-encrypts). */
|
|
122
|
+
async sendSessionMessage(sessionId, content) {
|
|
123
|
+
await this.ensureCrypto();
|
|
124
|
+
const encryptedContent = await this.crypto.encryptSessionMessage(content);
|
|
125
|
+
this.send({ type: "session.send", sessionId, encryptedContent });
|
|
126
|
+
}
|
|
127
|
+
/** Send a typing indicator. */
|
|
128
|
+
sendTyping(sessionId, isTyping) {
|
|
129
|
+
this.send({ type: "typing", sessionId, isTyping });
|
|
130
|
+
}
|
|
131
|
+
/** Listen for a specific event type. */
|
|
132
|
+
on(eventType, handler) {
|
|
133
|
+
let handlers = this.handlers.get(eventType);
|
|
134
|
+
if (!handlers) {
|
|
135
|
+
handlers = new Set();
|
|
136
|
+
this.handlers.set(eventType, handlers);
|
|
137
|
+
}
|
|
138
|
+
handlers.add(handler);
|
|
139
|
+
return () => { handlers.delete(handler); };
|
|
140
|
+
}
|
|
141
|
+
/** Listen for all events. */
|
|
142
|
+
onAny(handler) {
|
|
143
|
+
this.anyHandlers.add(handler);
|
|
144
|
+
return () => { this.anyHandlers.delete(handler); };
|
|
145
|
+
}
|
|
146
|
+
/** Remove all listeners for an event type. */
|
|
147
|
+
off(eventType) {
|
|
148
|
+
this.handlers.delete(eventType);
|
|
149
|
+
}
|
|
150
|
+
/** Auto-decrypt a session message's encryptedContent field. */
|
|
151
|
+
async decryptSessionMessage(encryptedContent) {
|
|
152
|
+
await this.ensureCrypto();
|
|
153
|
+
return this.crypto.decryptSessionMessage(encryptedContent);
|
|
154
|
+
}
|
|
155
|
+
// -- Private ----------------------------------------------------------------
|
|
156
|
+
send(data) {
|
|
157
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
158
|
+
this.ws.send(JSON.stringify(data));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
emit(eventType, data) {
|
|
162
|
+
const handlers = this.handlers.get(eventType);
|
|
163
|
+
if (handlers) {
|
|
164
|
+
for (const h of handlers) {
|
|
165
|
+
try {
|
|
166
|
+
h(data);
|
|
167
|
+
}
|
|
168
|
+
catch { /* don't break event loop */ }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const h of this.anyHandlers) {
|
|
172
|
+
try {
|
|
173
|
+
h(data);
|
|
174
|
+
}
|
|
175
|
+
catch { /* don't break event loop */ }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
scheduleReconnect() {
|
|
179
|
+
const maxDelay = this.options.maxReconnectDelay ?? 30000;
|
|
180
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), maxDelay);
|
|
181
|
+
this.reconnectAttempts++;
|
|
182
|
+
this.reconnectTimer = setTimeout(() => {
|
|
183
|
+
this.connect().catch(() => {
|
|
184
|
+
// reconnect will retry via onclose
|
|
185
|
+
});
|
|
186
|
+
}, delay);
|
|
187
|
+
}
|
|
188
|
+
}
|