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 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
- async rawRequest(method, path, options) {
91
- const url = `${this.baseUrl}/v1${path}`;
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: { "X-API-Key": this.apiKey },
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hiloop-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript SDK for Hiloop — zero-trust human-AI agent interaction platform",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",