hiloop-sdk 0.1.0 → 0.2.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 +11 -0
- package/dist/client.js +94 -5
- package/dist/crypto.d.ts +9 -1
- package/dist/crypto.js +27 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -27,7 +27,18 @@ export declare class HiloopClient {
|
|
|
27
27
|
private crypto;
|
|
28
28
|
private secretKeyB64;
|
|
29
29
|
private publicKeyB64;
|
|
30
|
+
private initialized;
|
|
31
|
+
private initPromise;
|
|
32
|
+
private options;
|
|
30
33
|
constructor(options: HiloopClientOptions);
|
|
34
|
+
/**
|
|
35
|
+
* Initialize encryption: derive keypair from API key, register public key,
|
|
36
|
+
* and fetch space encryption info. Called automatically on first API call.
|
|
37
|
+
*/
|
|
38
|
+
private ensureInitialized;
|
|
39
|
+
private doInit;
|
|
40
|
+
/** Raw HTTP request without triggering auto-init (used during init itself). */
|
|
41
|
+
private rawRequest;
|
|
31
42
|
private encryptField;
|
|
32
43
|
/**
|
|
33
44
|
* 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,99 @@ 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 ?? "
|
|
18
|
+
this.baseUrl = (options.baseUrl ?? "https://api.hi-loop.com").replace(/\/$/, "");
|
|
15
19
|
this.timeout = options.timeout ?? 30000;
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
async rawRequest(method, path, options) {
|
|
91
|
+
const url = `${this.baseUrl}/v1${path}`;
|
|
92
|
+
const headers = {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
"X-API-Key": this.apiKey,
|
|
95
|
+
};
|
|
96
|
+
const res = await fetch(url, {
|
|
97
|
+
method,
|
|
98
|
+
headers,
|
|
99
|
+
body: options?.body ? JSON.stringify(options.body) : undefined,
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const text = await res.text();
|
|
103
|
+
throw new HiloopError(res.status, text);
|
|
104
|
+
}
|
|
105
|
+
return await res.json();
|
|
19
106
|
}
|
|
20
107
|
encryptField(text) {
|
|
21
108
|
return this.crypto.encryptForApi(text);
|
|
@@ -59,6 +146,7 @@ export class HiloopClient {
|
|
|
59
146
|
return btoa(binary);
|
|
60
147
|
}
|
|
61
148
|
async request(method, path, options) {
|
|
149
|
+
await this.ensureInitialized();
|
|
62
150
|
let url = `${this.baseUrl}/v1${path}`;
|
|
63
151
|
if (options?.params !== undefined) {
|
|
64
152
|
const qs = new URLSearchParams(options.params).toString();
|
|
@@ -89,6 +177,7 @@ export class HiloopClient {
|
|
|
89
177
|
}
|
|
90
178
|
/** Fetch binary content (e.g. file download). Returns raw ArrayBuffer. */
|
|
91
179
|
async requestBinary(path) {
|
|
180
|
+
await this.ensureInitialized();
|
|
92
181
|
const url = `${this.baseUrl}/v1${path}`;
|
|
93
182
|
const controller = new AbortController();
|
|
94
183
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
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
|
|
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
|
|
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";
|