@novaqore/ai 0.3.1 → 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/README.md +42 -0
- package/index.js +57 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -239,6 +239,48 @@ Call `stop()` at any time to abort the stream (e.g. wire it to a stop button in
|
|
|
239
239
|
| `tools` | array | - | Tool definitions |
|
|
240
240
|
| `tool_choice` | string | - | Tool selection mode |
|
|
241
241
|
|
|
242
|
+
### Encrypted Chat History (localStorage)
|
|
243
|
+
|
|
244
|
+
Enable `localStorage` to automatically save encrypted chat history in the browser. Messages are stored as encrypted blobs — zero extra crypto work, the SDK just keeps what's already encrypted.
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
const nq = new NovaQoreAI({
|
|
248
|
+
uid: process.env.NOVAQORE_UID,
|
|
249
|
+
quantumKey: process.env.NOVAQORE_QUANTUM_KEY,
|
|
250
|
+
keyId: process.env.NOVAQORE_KEY_ID,
|
|
251
|
+
localStorage: true,
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Every `chat()` call automatically saves the encrypted request and response. Retrieve and decrypt on demand:
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
const history = nq.getHistory();
|
|
259
|
+
|
|
260
|
+
for (const entry of history) {
|
|
261
|
+
console.log("User:", entry.messages[entry.messages.length - 1].content);
|
|
262
|
+
console.log("Assistant:", entry.response.content);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Clear all stored history:
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
nq.clearHistory();
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Use a custom storage key to separate conversations:
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
275
|
+
const nq = new NovaQoreAI({
|
|
276
|
+
uid, quantumKey, keyId,
|
|
277
|
+
localStorage: true,
|
|
278
|
+
storageKey: "my-conversation-123",
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Data at rest stays encrypted in the browser — decryption only happens when you call `getHistory()`.
|
|
283
|
+
|
|
242
284
|
### Health check
|
|
243
285
|
|
|
244
286
|
```javascript
|
package/index.js
CHANGED
|
@@ -13,6 +13,8 @@ class NovaQoreAI {
|
|
|
13
13
|
#authToken;
|
|
14
14
|
#getAuthToken;
|
|
15
15
|
#abort;
|
|
16
|
+
#useLocalStorage;
|
|
17
|
+
#storageKey;
|
|
16
18
|
|
|
17
19
|
constructor(config) {
|
|
18
20
|
if (config === undefined) {
|
|
@@ -50,6 +52,11 @@ class NovaQoreAI {
|
|
|
50
52
|
this.#keyId = config.keyId;
|
|
51
53
|
this.#authToken = config.authToken || config.bearerToken || null;
|
|
52
54
|
this.#getAuthToken = config.getAuthToken || null;
|
|
55
|
+
this.#useLocalStorage = config.localStorage || false;
|
|
56
|
+
this.#storageKey = config.storageKey || `novaqore-ai-history-${this.#uid}`;
|
|
57
|
+
if (this.#useLocalStorage && typeof localStorage === "undefined") {
|
|
58
|
+
throw new Error("localStorage is not available in this environment");
|
|
59
|
+
}
|
|
53
60
|
this.version = version;
|
|
54
61
|
this.description = "NovaQore AI - Quantum-encrypted LLM client by NovaQore";
|
|
55
62
|
this.methods = ["chat", "health"];
|
|
@@ -59,6 +66,42 @@ class NovaQoreAI {
|
|
|
59
66
|
this.#authToken = token;
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
#saveToStorage(entry) {
|
|
70
|
+
if (!this.#useLocalStorage) return;
|
|
71
|
+
const raw = localStorage.getItem(this.#storageKey);
|
|
72
|
+
const history = raw ? JSON.parse(raw) : [];
|
|
73
|
+
history.push(entry);
|
|
74
|
+
localStorage.setItem(this.#storageKey, JSON.stringify(history));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getHistory() {
|
|
78
|
+
if (!this.#useLocalStorage) return [];
|
|
79
|
+
const raw = localStorage.getItem(this.#storageKey);
|
|
80
|
+
if (!raw) return [];
|
|
81
|
+
const history = JSON.parse(raw);
|
|
82
|
+
return history.map(entry => {
|
|
83
|
+
const sharedSecret = Buffer.from(entry.sharedSecret, "base64");
|
|
84
|
+
const request = this.#decryptResponse(entry.request, sharedSecret);
|
|
85
|
+
let response = null;
|
|
86
|
+
if (entry.chunks) {
|
|
87
|
+
const parts = entry.chunks.map(c => {
|
|
88
|
+
const chunk = this.#decryptResponse(c, sharedSecret);
|
|
89
|
+
return chunk.choices[0]?.delta?.content || "";
|
|
90
|
+
});
|
|
91
|
+
response = { role: "assistant", content: parts.join("") };
|
|
92
|
+
} else if (entry.response) {
|
|
93
|
+
const decrypted = this.#decryptResponse(entry.response, sharedSecret);
|
|
94
|
+
response = decrypted.choices[0].message;
|
|
95
|
+
}
|
|
96
|
+
return { messages: request.messages, response };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clearHistory() {
|
|
101
|
+
if (!this.#useLocalStorage) return;
|
|
102
|
+
localStorage.removeItem(this.#storageKey);
|
|
103
|
+
}
|
|
104
|
+
|
|
62
105
|
async #encryptPayload(payload) {
|
|
63
106
|
const quantumKey = new Uint8Array(Buffer.from(this.#quantumKey, "base64"));
|
|
64
107
|
|
|
@@ -140,12 +183,14 @@ class NovaQoreAI {
|
|
|
140
183
|
}
|
|
141
184
|
|
|
142
185
|
if (options.stream) {
|
|
143
|
-
const
|
|
186
|
+
const entry = { sharedSecret: sharedSecret.toString("base64"), request: encrypted, chunks: [] };
|
|
187
|
+
const stream = this.#readStream(res, sharedSecret, entry);
|
|
144
188
|
const stop = () => this.#abort.abort();
|
|
145
189
|
return { stream, stop };
|
|
146
190
|
}
|
|
147
191
|
|
|
148
192
|
const { encrypted: encryptedResponse } = await res.json();
|
|
193
|
+
this.#saveToStorage({ sharedSecret: sharedSecret.toString("base64"), request: encrypted, response: encryptedResponse });
|
|
149
194
|
const result = this.#decryptResponse(encryptedResponse, sharedSecret);
|
|
150
195
|
return { result, stop: () => {} };
|
|
151
196
|
} catch (err) {
|
|
@@ -155,7 +200,7 @@ class NovaQoreAI {
|
|
|
155
200
|
}
|
|
156
201
|
}
|
|
157
202
|
|
|
158
|
-
async *#readStream(res, sharedSecret) {
|
|
203
|
+
async *#readStream(res, sharedSecret, entry) {
|
|
159
204
|
const decoder = new TextDecoder();
|
|
160
205
|
const reader = res.body.getReader();
|
|
161
206
|
let buffer = "";
|
|
@@ -175,9 +220,13 @@ class NovaQoreAI {
|
|
|
175
220
|
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
176
221
|
const data = trimmed.slice(6);
|
|
177
222
|
|
|
178
|
-
if (data === "[DONE]")
|
|
223
|
+
if (data === "[DONE]") {
|
|
224
|
+
this.#saveToStorage(entry);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
179
227
|
|
|
180
228
|
const { encrypted } = JSON.parse(data);
|
|
229
|
+
entry.chunks.push(encrypted);
|
|
181
230
|
const chunk = this.#decryptResponse(encrypted, sharedSecret);
|
|
182
231
|
|
|
183
232
|
if (chunk.seq !== expectedSeq) {
|
|
@@ -188,8 +237,12 @@ class NovaQoreAI {
|
|
|
188
237
|
yield chunk;
|
|
189
238
|
}
|
|
190
239
|
}
|
|
240
|
+
this.#saveToStorage(entry);
|
|
191
241
|
} catch (err) {
|
|
192
|
-
if (err.name === "AbortError")
|
|
242
|
+
if (err.name === "AbortError") {
|
|
243
|
+
this.#saveToStorage(entry);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
193
246
|
throw err;
|
|
194
247
|
}
|
|
195
248
|
}
|
package/package.json
CHANGED