@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.
Files changed (3) hide show
  1. package/README.md +42 -0
  2. package/index.js +57 -4
  3. 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 stream = this.#readStream(res, sharedSecret);
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]") return;
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") return;
242
+ if (err.name === "AbortError") {
243
+ this.#saveToStorage(entry);
244
+ return;
245
+ }
193
246
  throw err;
194
247
  }
195
248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novaqore/ai",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "The world's first private, quantum-encrypted LLM clients. CRYSTALS-Kyber + AES-256-GCM end-to-end encryption. Zero dependencies.",
5
5
  "main": "index.js",
6
6
  "files": [