hiloop-sdk 0.1.0 → 0.3.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;
@@ -27,7 +29,19 @@ export declare class HiloopClient {
27
29
  private crypto;
28
30
  private secretKeyB64;
29
31
  private publicKeyB64;
32
+ private initialized;
33
+ private initPromise;
34
+ private options;
30
35
  constructor(options: HiloopClientOptions);
36
+ /**
37
+ * Initialize encryption: derive keypair from API key, register public key,
38
+ * and fetch space encryption info. Called automatically on first API call.
39
+ */
40
+ private ensureInitialized;
41
+ private doInit;
42
+ /** Raw HTTP request without triggering auto-init (used during init itself). */
43
+ private buildHeaders;
44
+ private rawRequest;
31
45
  private encryptField;
32
46
  /**
33
47
  * Encrypt multiple fields with a SINGLE shared content key (AES-256-GCM).
package/dist/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Hiloop SDK client for agents to interact with the Hiloop platform. */
2
- import { CryptoContext, decryptAes, deriveWrappingKey, encryptAes, generateKeyPair } from "./crypto.js";
2
+ import { CryptoContext, computeFingerprint, decryptAes, deriveKeyPairFromApiKey, deriveWrappingKey, encryptAes, generateKeyPair } from "./crypto.js";
3
3
  import { parseChannel, parseChannelMessage, parseChannelParticipant, parseConvSession, parseConvSessionMessage, parseGuestToken, parseInteraction, parseMessage, } from "./types.js";
4
4
  export class HiloopError extends Error {
5
5
  constructor(statusCode, message) {
@@ -10,12 +10,104 @@ export class HiloopError extends Error {
10
10
  }
11
11
  export class HiloopClient {
12
12
  constructor(options) {
13
+ this.secretKeyB64 = "";
14
+ this.publicKeyB64 = "";
15
+ this.initialized = false;
16
+ this.initPromise = null;
13
17
  this.apiKey = options.apiKey;
14
- this.baseUrl = (options.baseUrl ?? "http://localhost:8000").replace(/\/$/, "");
18
+ this.baseUrl = (options.baseUrl ?? "https://api.hi-loop.com").replace(/\/$/, "");
15
19
  this.timeout = options.timeout ?? 30000;
16
- this.secretKeyB64 = options.secretKey ?? "";
17
- this.publicKeyB64 = options.publicKey ?? "";
18
- this.crypto = new CryptoContext(options.secretKey ?? "", options.publicKey ?? "", options.spacePublicKey ?? "", options.spaceKeyId ?? "", options.serverKeyId);
20
+ this.options = options;
21
+ // If all crypto keys provided, initialize synchronously (advanced usage)
22
+ if (options.secretKey && options.publicKey && options.spacePublicKey && options.spaceKeyId) {
23
+ this.secretKeyB64 = options.secretKey;
24
+ this.publicKeyB64 = options.publicKey;
25
+ this.crypto = new CryptoContext(options.secretKey, options.publicKey, options.spacePublicKey, options.spaceKeyId, options.serverKeyId);
26
+ this.initialized = true;
27
+ }
28
+ else {
29
+ // Placeholder — real init happens lazily on first API call
30
+ this.crypto = new CryptoContext("", "", "", "");
31
+ }
32
+ }
33
+ /**
34
+ * Initialize encryption: derive keypair from API key, register public key,
35
+ * and fetch space encryption info. Called automatically on first API call.
36
+ */
37
+ async ensureInitialized() {
38
+ if (this.initialized)
39
+ return;
40
+ if (this.initPromise)
41
+ return this.initPromise;
42
+ this.initPromise = this.doInit();
43
+ await this.initPromise;
44
+ }
45
+ async doInit() {
46
+ // 1. Derive keypair from API key (or use provided keys)
47
+ let secretKey = this.options.secretKey;
48
+ let publicKey = this.options.publicKey;
49
+ if (!secretKey || !publicKey) {
50
+ const kp = await deriveKeyPairFromApiKey(this.apiKey);
51
+ secretKey = kp.secretKey;
52
+ publicKey = kp.publicKey;
53
+ }
54
+ this.secretKeyB64 = secretKey;
55
+ this.publicKeyB64 = publicKey;
56
+ // 2. Register agent public key (idempotent)
57
+ const fingerprint = await computeFingerprint(publicKey);
58
+ try {
59
+ const existing = await this.rawRequest("GET", "/agent/keys/me");
60
+ if (existing.fingerprint !== fingerprint) {
61
+ await this.rawRequest("PUT", "/agent/keys/me", { body: { publicKey, fingerprint } });
62
+ }
63
+ }
64
+ catch {
65
+ try {
66
+ await this.rawRequest("PUT", "/agent/keys/me", { body: { publicKey, fingerprint } });
67
+ }
68
+ catch {
69
+ // Non-fatal — key registration may fail in dev environments
70
+ }
71
+ }
72
+ // 3. Fetch space encryption info
73
+ let spacePublicKey = this.options.spacePublicKey ?? "";
74
+ let spaceKeyId = this.options.spaceKeyId ?? "";
75
+ if (!spacePublicKey || !spaceKeyId) {
76
+ try {
77
+ const info = await this.rawRequest("GET", "/agent/encryption-info");
78
+ spacePublicKey = info.spacePublicKey;
79
+ spaceKeyId = info.spaceKeyId;
80
+ }
81
+ catch {
82
+ // Space key not available — crypto operations will fail but non-crypto calls work
83
+ }
84
+ }
85
+ // 4. Create CryptoContext
86
+ this.crypto = new CryptoContext(secretKey, publicKey, spacePublicKey, spaceKeyId, this.options.serverKeyId);
87
+ this.initialized = true;
88
+ }
89
+ /** Raw HTTP request without triggering auto-init (used during init itself). */
90
+ buildHeaders() {
91
+ const h = {
92
+ "Content-Type": "application/json",
93
+ "X-API-Key": this.apiKey,
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}`;
101
+ const res = await fetch(url, {
102
+ method,
103
+ headers: this.buildHeaders(),
104
+ body: options?.body ? JSON.stringify(options.body) : undefined,
105
+ });
106
+ if (!res.ok) {
107
+ const text = await res.text();
108
+ throw new HiloopError(res.status, text);
109
+ }
110
+ return await res.json();
19
111
  }
20
112
  encryptField(text) {
21
113
  return this.crypto.encryptForApi(text);
@@ -28,6 +120,7 @@ export class HiloopClient {
28
120
  * with the same (scope, recipientKeyId) are silently dropped.
29
121
  */
30
122
  async encryptFields(fields) {
123
+ await this.ensureInitialized();
31
124
  const encrypted = {};
32
125
  const entries = Object.entries(fields).filter(([, v]) => v !== undefined);
33
126
  if (entries.length === 0)
@@ -59,6 +152,7 @@ export class HiloopClient {
59
152
  return btoa(binary);
60
153
  }
61
154
  async request(method, path, options) {
155
+ await this.ensureInitialized();
62
156
  let url = `${this.baseUrl}/v1${path}`;
63
157
  if (options?.params !== undefined) {
64
158
  const qs = new URLSearchParams(options.params).toString();
@@ -70,10 +164,7 @@ export class HiloopClient {
70
164
  try {
71
165
  const res = await fetch(url, {
72
166
  method,
73
- headers: {
74
- "X-API-Key": this.apiKey,
75
- "Content-Type": "application/json",
76
- },
167
+ headers: this.buildHeaders(),
77
168
  body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
78
169
  signal: controller.signal,
79
170
  });
@@ -89,13 +180,14 @@ export class HiloopClient {
89
180
  }
90
181
  /** Fetch binary content (e.g. file download). Returns raw ArrayBuffer. */
91
182
  async requestBinary(path) {
183
+ await this.ensureInitialized();
92
184
  const url = `${this.baseUrl}/v1${path}`;
93
185
  const controller = new AbortController();
94
186
  const timer = setTimeout(() => controller.abort(), this.timeout);
95
187
  try {
96
188
  const res = await fetch(url, {
97
189
  method: "GET",
98
- headers: { "X-API-Key": this.apiKey },
190
+ headers: this.buildHeaders(),
99
191
  signal: controller.signal,
100
192
  });
101
193
  if (!res.ok) {
@@ -269,6 +361,7 @@ export class HiloopClient {
269
361
  }
270
362
  /** Push content blocks to an interaction. */
271
363
  async pushContentBlocks(interactionId, blocks) {
364
+ await this.ensureInitialized();
272
365
  const encryptedBlocks = await Promise.all(blocks.map(async (block) => {
273
366
  const plaintext = JSON.stringify(block.data);
274
367
  const wrapped = await this.crypto.encryptWithWrapping(plaintext);
@@ -399,6 +492,7 @@ export class HiloopClient {
399
492
  }
400
493
  // -- Conversation Sessions -------------------------------------------------
401
494
  async createConvSession(opts) {
495
+ await this.ensureInitialized();
402
496
  const body = {
403
497
  sessionType: opts.sessionType ?? "direct",
404
498
  isPublic: opts.isPublic ?? false,
@@ -458,6 +552,7 @@ export class HiloopClient {
458
552
  * using the session message key (`{agentPub}:{AES-GCM ciphertext}` format).
459
553
  */
460
554
  async sendConvSessionMessage(sessionId, content) {
555
+ await this.ensureInitialized();
461
556
  const encryptedContent = await this.crypto.encryptSessionMessage(content);
462
557
  const data = await this.request("POST", `/agent/sessions/${sessionId}/messages`, { body: { encryptedContent } });
463
558
  return parseConvSessionMessage(data);
@@ -950,6 +1045,7 @@ export class HiloopClient {
950
1045
  // -- Channels ---------------------------------------------------------------
951
1046
  /** Create a new agent-to-agent channel. */
952
1047
  async createChannel(opts) {
1048
+ await this.ensureInitialized();
953
1049
  const body = {};
954
1050
  if (opts.name !== undefined) {
955
1051
  const w = await this.crypto.encryptWithWrapping(opts.name);
@@ -1006,6 +1102,7 @@ export class HiloopClient {
1006
1102
  }
1007
1103
  /** Send a message in a channel. */
1008
1104
  async sendChannelMessage(channelId, content) {
1105
+ await this.ensureInitialized();
1009
1106
  const wrapped = await this.crypto.encryptWithWrapping(content);
1010
1107
  const body = {
1011
1108
  encryptedContent: wrapped.ciphertext,
package/dist/crypto.d.ts CHANGED
@@ -3,8 +3,16 @@ export interface KeyPair {
3
3
  publicKey: string;
4
4
  secretKey: string;
5
5
  }
6
- /** Generate an X25519 keypair. */
6
+ /** Generate a random X25519 keypair. */
7
7
  export declare function generateKeyPair(): KeyPair;
8
+ /**
9
+ * Derive a deterministic X25519 keypair from an API key.
10
+ * Uses HKDF-SHA256 with context "hiloop-agent-encrypt-v1" — same derivation
11
+ * as the MCP server, so the same API key produces the same keypair everywhere.
12
+ */
13
+ export declare function deriveKeyPairFromApiKey(apiKey: string): Promise<KeyPair>;
14
+ /** Compute SHA-256 fingerprint of a base64 public key string. */
15
+ export declare function computeFingerprint(publicKeyBase64: string): Promise<string>;
8
16
  /** Encrypt plaintext for a recipient using NaCl box. Returns base64 ciphertext. */
9
17
  export declare function encrypt(plaintext: string, senderSecretKeyB64: string, recipientPublicKeyB64: string): string;
10
18
  /** Decrypt base64 ciphertext. Returns plaintext. */
package/dist/crypto.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** E2E encryption for the Hiloop SDK using tweetnacl (X25519 + XSalsa20-Poly1305). */
2
2
  import nacl from "tweetnacl";
3
3
  import util from "tweetnacl-util";
4
- /** Generate an X25519 keypair. */
4
+ /** Generate a random X25519 keypair. */
5
5
  export function generateKeyPair() {
6
6
  const kp = nacl.box.keyPair();
7
7
  return {
@@ -9,6 +9,32 @@ export function generateKeyPair() {
9
9
  secretKey: util.encodeBase64(kp.secretKey),
10
10
  };
11
11
  }
12
+ /**
13
+ * Derive a deterministic X25519 keypair from an API key.
14
+ * Uses HKDF-SHA256 with context "hiloop-agent-encrypt-v1" — same derivation
15
+ * as the MCP server, so the same API key produces the same keypair everywhere.
16
+ */
17
+ export async function deriveKeyPairFromApiKey(apiKey) {
18
+ const keyMaterial = await crypto.subtle.importKey("raw", new TextEncoder().encode(apiKey), "HKDF", false, ["deriveBits"]);
19
+ const secretKeyBits = await crypto.subtle.deriveBits({
20
+ name: "HKDF",
21
+ hash: "SHA-256",
22
+ salt: new Uint8Array(32),
23
+ info: new TextEncoder().encode("hiloop-agent-encrypt-v1"),
24
+ }, keyMaterial, 256);
25
+ const secretKey = new Uint8Array(secretKeyBits);
26
+ const kp = nacl.box.keyPair.fromSecretKey(secretKey);
27
+ return {
28
+ publicKey: util.encodeBase64(kp.publicKey),
29
+ secretKey: util.encodeBase64(kp.secretKey),
30
+ };
31
+ }
32
+ /** Compute SHA-256 fingerprint of a base64 public key string. */
33
+ export async function computeFingerprint(publicKeyBase64) {
34
+ const data = new TextEncoder().encode(publicKeyBase64);
35
+ const hash = await crypto.subtle.digest("SHA-256", data);
36
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
37
+ }
12
38
  /** Encrypt plaintext for a recipient using NaCl box. Returns base64 ciphertext. */
13
39
  export function encrypt(plaintext, senderSecretKeyB64, recipientPublicKeyB64) {
14
40
  const sk = util.decodeBase64(senderSecretKeyB64);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { HiloopClient, HiloopError } from "./client.js";
2
2
  export type { HiloopClientOptions } from "./client.js";
3
- export { generateKeyPair, encrypt, decrypt, encryptAes, decryptAes, deriveWrappingKey, CryptoContext } from "./crypto.js";
3
+ export { generateKeyPair, deriveKeyPairFromApiKey, computeFingerprint, encrypt, decrypt, encryptAes, decryptAes, deriveWrappingKey, CryptoContext } from "./crypto.js";
4
4
  export type { KeyPair, ContentWrappingResult } from "./crypto.js";
5
5
  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";
6
6
  export { parseInteraction, parseMessage, parseConvSession, parseConvSessionMessage, parseGuestToken, parseChannel, parseChannelMessage, parseChannelParticipant, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { HiloopClient, HiloopError } from "./client.js";
2
- export { generateKeyPair, encrypt, decrypt, encryptAes, decryptAes, deriveWrappingKey, CryptoContext } from "./crypto.js";
2
+ export { generateKeyPair, deriveKeyPairFromApiKey, computeFingerprint, encrypt, decrypt, encryptAes, decryptAes, deriveWrappingKey, CryptoContext } from "./crypto.js";
3
3
  export { parseInteraction, parseMessage, parseConvSession, parseConvSessionMessage, parseGuestToken, parseChannel, parseChannelMessage, parseChannelParticipant, } from "./types.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hiloop-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.3.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",