kaiwen-core-js 0.1.2 → 0.1.11

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 CHANGED
@@ -10,6 +10,8 @@ It is 100% interoperable with the Python `kaiwen-core` SDK, allowing seamless, c
10
10
  - **Cross-Platform Cryptography**: PBKDF2 (200k iterations) and HKDF derivations for master and app-isolated keys. Fully compatible with Python's `cryptography.hazmat` stack.
11
11
  - **AES-256-GCM**: Military-grade authenticated encryption.
12
12
  - **Logical Clock Conflict Resolution**: Decentralized, timestamp-agnostic conflict resolution across devices.
13
+ - **Automatic Background Synchronization**: Saving, updating, and deleting records automatically schedules a background push to the server.
14
+ - **Simplified WebSocket Channel**: One-line initialization `startSync` handles initial pull, starts socket listener, and updates UI on remote events.
13
15
  - **Chunked Asset Engine**: Securely encrypt and decrypt massive media files (videos, photos) in 1MB chunks without running out of RAM.
14
16
  - **Dependency Inversion**: Easily inject your own Storage Adapters (e.g., SQLite, AsyncStorage, IndexedDB).
15
17
  - **TypeScript First**: 100% strictly typed.
@@ -27,81 +29,112 @@ npm install kaiwen-core-js
27
29
 
28
30
  ## Usage
29
31
 
30
- ### 1. Initialization and Login
32
+ ### 1. Initialization
31
33
 
32
- Initialize the SDK by providing an App ID, the API URL of the Kaiwen Server, and your storage adapter.
34
+ Initialize the SDK by providing your App ID, the API URL of the Kaiwen Server, and your storage adapter.
33
35
 
34
36
  ```typescript
35
37
  import { KaiwenApp, MemoryStorage } from "kaiwen-core-js";
36
38
 
37
39
  // Initialize the Facade
38
- const storage = new MemoryStorage(); // Or implement IKWStorage for SQLite/IndexedDB
40
+ const storage = new MemoryStorage(); // Or implement IKWStorage for SQLite/AsyncStorage
39
41
  const app = new KaiwenApp(
40
42
  "core.kaiwen.notes", // App ID (Isolated Encryption Domain)
41
- "https://api.kaiwen.cloud", // Kaiwen API Server
43
+ "http://localhost:8000", // Kaiwen API Server
42
44
  "your_api_key_here", // Server API Key
43
45
  storage
44
46
  );
47
+ ```
48
+
49
+ ### 2. Zero-Knowledge Authentication Flow
50
+
51
+ #### A. Registration (Mnemonic Generation)
52
+ Registering a new account automatically generates a client-side **12-word recovery mnemonic**. The mnemonic is encrypted locally using a key derived from the user's password, and the encrypted bundle is sent to the server. The raw mnemonic is returned so the user can back it up.
45
53
 
46
- // Login (Derives master key and isolated app key using PBKDF2 + HKDF)
47
- await app.login("apple banana cherry date elderberry fig grape hazelnut ice juice kiwi lemon");
54
+ ```typescript
55
+ const registerRes = await app.register("username", "password");
56
+ console.log("Account created!");
57
+ console.log("Your 12-word recovery phrase is:", registerRes.mnemonic);
58
+ // IMPORTANT: Prompt the user to write this down securely. It cannot be recovered by the server!
48
59
  ```
49
60
 
50
- ### 2. Saving Encrypted Records
61
+ #### B. Login with Password
62
+ To log in on any device using just the password, the client requests the encrypted mnemonic from the server, decrypts it locally using the password-derived key, and derives the E2E Vault Key from the mnemonic.
63
+
64
+ ```typescript
65
+ await app.loginWithPassword("username", "password");
66
+ console.log("Logged in successfully! Vault is decrypted.");
67
+ ```
51
68
 
52
- Data is encrypted *before* it is saved to the local database queue.
69
+ #### C. Direct Mnemonic Login (Passwordless)
70
+ Users can log in directly using their 12-word recovery phrase. The client authenticates using a hash of the mnemonic (`mnemonic_hash`) and derives the vault keys directly.
53
71
 
54
72
  ```typescript
55
- // Save an end-to-end encrypted note
56
- const record = await app.saveRecord("user_123", {
57
- title: "My Secret Plan",
58
- content: "This is fully encrypted using AES-256-GCM."
59
- });
73
+ await app.loginWithMnemonic("username", "amateur upper armed actor alert unit...");
74
+ console.log("Logged in directly via mnemonic!");
75
+ ```
60
76
 
61
- console.log("Locally saved record ID:", record.id);
77
+ #### D. Password Recovery (Forgotten Password)
78
+ If a user forgets their password, they can reset it using their 12-word recovery mnemonic. The client sends a cryptographically signed proof (the mnemonic hash) to reset the password verifier on the server, re-encrypts the mnemonic under the new password key, and uploads it.
79
+
80
+ ```typescript
81
+ await app.recoverPassword("username", "amateur upper armed actor alert...", "new_password_999");
82
+ console.log("Password reset successfully! Vault unlocked with new password.");
62
83
  ```
63
84
 
64
- ### 3. Pushing to the Cloud
85
+ #### E. Password Rotation (Key Rotation)
86
+ Changing the password re-encrypts the recovery mnemonic with the new password-derived key on the server. If the user was a legacy password-only user, the SDK automatically upgrades their account to E2EE mnemonic recovery and re-encrypts all local database records seamlessly.
65
87
 
66
- Once records are saved locally, push the pending queue to the Kaiwen Server.
88
+ ```typescript
89
+ await app.rotatePassword("username", "old_password_123", "new_password_456");
90
+ console.log("Password rotated and E2EE keys synchronized!");
91
+ ```
92
+
93
+ ### 3. Giriş ve Otomatik Canlı Eşitleme (startSync)
94
+
95
+ Once logged in, call `startSync` to initialize real-time synchronization. It will pull the latest updates, connect the WebSocket channel, and trigger your callback whenever remote updates arrive.
67
96
 
68
97
  ```typescript
69
- const pushedCount = await app.pushSync();
70
- console.log(`Successfully synced ${pushedCount} records to the cloud.`);
98
+ // Start real-time synchronization on startup or login
99
+ await app.startSync(() => {
100
+ // Callback triggered when remote changes are received and synced locally
101
+ console.log("Database updated! Reloading UI...");
102
+ loadNotes();
103
+ });
71
104
  ```
72
105
 
73
- ### 4. Pulling from the Cloud
106
+ ### 4. Değişiklik Yapma (Otomatik Arkaplan Eşitleme)
74
107
 
75
- Pull changes made by other devices (e.g., a Python desktop app or an iOS device). The SDK will automatically handle decryption and logical clock conflict resolution.
108
+ Creating, editing, or deleting a record automatically triggers a background push sync to the cloud. You do **not** need to call `pushSync()` manually.
76
109
 
77
110
  ```typescript
78
- const pulledCount = await app.pullSync();
79
- console.log(`Fetched ${pulledCount} new or updated records.`);
80
-
81
- // Decrypt and read the latest state
82
- const allRecords = await storage.getAllRecords();
83
- for (const r of allRecords) {
84
- const decryptedPayload = await app.getDecryptedRecord(r.id);
85
- console.log("Decrypted Data:", decryptedPayload);
86
- }
111
+ // A. Yeni Kayıt Ekleme
112
+ await app.saveRecord("user_123", { title: "New Note", content: "Secret data" }, "notes");
113
+
114
+ // B. Kayıt Düzenleme (Edit)
115
+ await app.updateRecord("note_id_123", { title: "Updated Note", content: "Modified secret data" });
116
+
117
+ // C. Kayıt Silme (Delete)
118
+ await app.deleteRecord("note_id_123");
87
119
  ```
88
120
 
89
- ---
121
+ Whenever you perform any of these write actions, the library saves it locally and starts a background thread/promise to synchronize with the server, which broadcasts the change to all other active client instances instantly.
90
122
 
91
- ## The Kaiwen Ecosystem Architecture
123
+ ### 5. Manuel Eşitleme (Opsiyonel)
92
124
 
93
- This library implements the precise directory mapping and modularity of the Python `kaiwen-core` SDK:
125
+ If you need to force a manual sync, you can call push and pull directly:
94
126
 
95
- - `/core`: Exceptions and pipeline flows.
96
- - `/crypto`: Cryptographic engine (WebCrypto API).
97
- - `/models`: Database schemas, enums, and migration engines.
98
- - `/storage`: Vault (Key Management), Interfaces, and Chunked Asset Engine.
99
- - `/sync`: Network dispatchers.
100
- - `/utils`: Logging and telemetry.
127
+ ```typescript
128
+ const pushed = await app.pushSync();
129
+ const pulled = await app.pullSync();
130
+ console.log(`Pushed: ${pushed}, Pulled: ${pulled}`);
131
+ ```
132
+
133
+ ---
101
134
 
102
135
  ## Security Notice
103
136
 
104
- - **Never hardcode mnemonics.**
137
+ - **Never learn the user's raw mnemonic on the server.** The server only stores the E2E-encrypted mnemonic and the mnemonic hash for validation.
105
138
  - **Ensure TLS (HTTPS)** is used when communicating with the `kaiwen_server` to protect metadata (even though payloads are E2E encrypted).
106
139
 
107
140
  ---
package/dist/app.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { KWVault } from "./storage/vault";
2
2
  import { KWMigrationEngine } from "./models/migration";
3
- import { KWAssetEngine } from "./storage/assets";
3
+ import { KWAssetEngine, KWAssetManifest } from "./storage/assets";
4
4
  import { KWPipeline } from "./core/pipeline";
5
5
  import { IKWStorage } from "./storage/db";
6
6
  import { KWRecord } from "./models/record";
@@ -11,11 +11,25 @@ export declare class KaiwenApp {
11
11
  assetEngine: KWAssetEngine;
12
12
  pipeline: KWPipeline;
13
13
  migration: KWMigrationEngine;
14
+ private ws;
14
15
  constructor(appId: string, apiUrl: string, apiKey: string, storage?: IKWStorage);
16
+ register(username: string, password: string): Promise<any>;
15
17
  login(mnemonic: string): Promise<void>;
16
- saveRecord(userId: string, payload: any): Promise<KWRecord>;
18
+ loginWithMnemonic(username: string, mnemonic: string): Promise<void>;
19
+ loginWithPassword(username: string, password: string): Promise<void>;
20
+ recoverPassword(username: string, mnemonic: string, newPassword: string): Promise<any>;
21
+ logout(): Promise<void>;
22
+ saveRecord(userId: string, payload: any, category?: string): Promise<KWRecord>;
17
23
  getDecryptedRecord(id: string): Promise<any>;
24
+ getDecryptedRecordsByCategory(category: string): Promise<any[]>;
18
25
  deleteRecord(id: string): Promise<boolean>;
26
+ updateRecord(id: string, payload: any): Promise<boolean>;
27
+ rotatePassword(username: string, oldPassword: string, newPassword: string): Promise<void>;
19
28
  pushSync(): Promise<number>;
20
29
  pullSync(): Promise<number>;
30
+ uploadAsset(fileBuffer: Uint8Array, mimeType: string, userId: string): Promise<KWAssetManifest>;
31
+ downloadAsset(assetId: string, mimeType: string): Promise<Uint8Array>;
32
+ startSync(onSyncUpdate?: () => void): Promise<void>;
33
+ startSyncListener(onSyncUpdate?: () => void): void;
34
+ stopSyncListener(): void;
21
35
  }
package/dist/app.js CHANGED
@@ -9,25 +9,79 @@ const pipeline_1 = require("./core/pipeline");
9
9
  const db_1 = require("./storage/db");
10
10
  const record_1 = require("./models/record");
11
11
  const validator_1 = require("./utils/validator");
12
+ const logger_1 = require("./utils/logger");
12
13
  class KaiwenApp {
13
14
  constructor(appId, apiUrl, apiKey, storage = new db_1.MemoryStorage()) {
14
15
  this.appId = appId;
15
16
  this.storage = storage;
17
+ this.ws = null;
16
18
  this.vault = new vault_1.KWVault();
17
19
  this.assetEngine = new assets_1.KWAssetEngine(this.vault);
18
- this.pipeline = new pipeline_1.KWPipeline(this.vault, storage, apiUrl, apiKey);
20
+ this.pipeline = new pipeline_1.KWPipeline(appId, this.vault, storage, apiUrl, apiKey);
19
21
  this.migration = new migration_1.KWMigrationEngine(storage, 1);
20
22
  }
23
+ async register(username, password) {
24
+ const mnemonic = engine_1.KWCryptoEngine.generateMnemonic();
25
+ const passwordKey = await engine_1.KWCryptoEngine.deriveMasterKeyFromPassword(password, username);
26
+ const encryptedMnemonic = await engine_1.KWCryptoEngine.encryptMnemonic(mnemonic, passwordKey);
27
+ const mnemonicHash = await engine_1.KWCryptoEngine.sha256(mnemonic);
28
+ const res = await this.pipeline.network.register(username, password, encryptedMnemonic, mnemonicHash);
29
+ res.mnemonic = mnemonic;
30
+ return res;
31
+ }
21
32
  async login(mnemonic) {
22
33
  await this.vault.login(mnemonic, this.appId);
23
34
  await this.migration.migrateAll();
24
35
  }
25
- async saveRecord(userId, payload) {
36
+ async loginWithMnemonic(username, mnemonic) {
37
+ const mnemonicHash = await engine_1.KWCryptoEngine.sha256(mnemonic);
38
+ await this.pipeline.network.loginMnemonic(username, mnemonicHash, this.appId);
39
+ await this.vault.login(mnemonic, this.appId);
40
+ await this.migration.migrateAll();
41
+ }
42
+ async loginWithPassword(username, password) {
43
+ const authData = await this.pipeline.network.login(username, password, this.appId);
44
+ const encryptedMnemonic = authData.encrypted_mnemonic;
45
+ if (encryptedMnemonic) {
46
+ const passwordKey = await engine_1.KWCryptoEngine.deriveMasterKeyFromPassword(password, username);
47
+ const mnemonic = await engine_1.KWCryptoEngine.decryptMnemonic(encryptedMnemonic, passwordKey);
48
+ await this.vault.login(mnemonic, this.appId);
49
+ }
50
+ else {
51
+ await this.vault.loginWithPassword(password, username, this.appId);
52
+ }
53
+ await this.migration.migrateAll();
54
+ }
55
+ async recoverPassword(username, mnemonic, newPassword) {
56
+ const mnemonicHash = await engine_1.KWCryptoEngine.sha256(mnemonic);
57
+ const passwordKey = await engine_1.KWCryptoEngine.deriveMasterKeyFromPassword(newPassword, username);
58
+ const encryptedMnemonic = await engine_1.KWCryptoEngine.encryptMnemonic(mnemonic, passwordKey);
59
+ const res = await this.pipeline.network.recoverPassword(username, mnemonicHash, newPassword, encryptedMnemonic, this.appId);
60
+ await this.vault.login(mnemonic, this.appId);
61
+ await this.migration.migrateAll();
62
+ return res;
63
+ }
64
+ async logout() {
65
+ try {
66
+ await this.pipeline.network.logout();
67
+ }
68
+ catch (err) {
69
+ logger_1.KWLogger.info("Network logout failed, clearing token locally: " + err);
70
+ this.pipeline.network.setSessionToken(null);
71
+ }
72
+ finally {
73
+ this.vault.lock();
74
+ this.stopSyncListener();
75
+ }
76
+ }
77
+ async saveRecord(userId, payload, category = "default") {
26
78
  validator_1.KWValidator.validatePayload(payload);
27
79
  const encrypted = await engine_1.KWCryptoEngine.encryptPayload(payload, this.vault.getKey());
28
80
  const generateId = () => typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2);
29
- const record = new record_1.KWRecord(generateId(), this.appId, userId, encrypted, Date.now() / 1000, 0, 1, 1);
81
+ const record = new record_1.KWRecord(generateId(), this.appId, userId, category, encrypted, Date.now() / 1000, 0, 0, 1, 1);
30
82
  await this.storage.saveRecord(record.id, record);
83
+ // Auto-trigger background pushSync
84
+ this.pushSync().catch(err => logger_1.KWLogger.error("Auto-push sync failed in saveRecord: " + err));
31
85
  return record;
32
86
  }
33
87
  async getDecryptedRecord(id) {
@@ -40,6 +94,21 @@ class KaiwenApp {
40
94
  return null;
41
95
  return await engine_1.KWCryptoEngine.decryptPayload(record.payload, this.vault.getKey());
42
96
  }
97
+ async getDecryptedRecordsByCategory(category) {
98
+ const all = await this.storage.getAllRecords();
99
+ const filtered = all.filter(r => r.category === category && r.is_deleted !== 1);
100
+ const decrypted = [];
101
+ for (const r of filtered) {
102
+ try {
103
+ const data = await engine_1.KWCryptoEngine.decryptPayload(r.payload, this.vault.getKey());
104
+ decrypted.push({ id: r.id, ...data });
105
+ }
106
+ catch (err) {
107
+ logger_1.KWLogger.error(`Decryption failed for record ${r.id}: ${err}`);
108
+ }
109
+ }
110
+ return decrypted;
111
+ }
43
112
  async deleteRecord(id) {
44
113
  const record = await this.storage.getRecord(id);
45
114
  if (!record)
@@ -49,13 +118,170 @@ class KaiwenApp {
49
118
  record.updated_at = Date.now() / 1000;
50
119
  record.logical_clock += 1;
51
120
  record.is_synced = 0;
52
- return await this.storage.saveRecord(id, record);
121
+ const saved = await this.storage.saveRecord(id, record);
122
+ if (saved) {
123
+ this.pushSync().catch(err => logger_1.KWLogger.error("Auto-push sync failed in deleteRecord: " + err));
124
+ }
125
+ return saved;
126
+ }
127
+ async updateRecord(id, payload) {
128
+ const record = await this.storage.getRecord(id);
129
+ if (!record)
130
+ return false;
131
+ validator_1.KWValidator.validatePayload(payload);
132
+ const encrypted = await engine_1.KWCryptoEngine.encryptPayload(payload, this.vault.getKey());
133
+ record.payload = encrypted;
134
+ record.updated_at = Date.now() / 1000;
135
+ record.logical_clock += 1;
136
+ record.is_synced = 0;
137
+ const saved = await this.storage.saveRecord(id, record);
138
+ if (saved) {
139
+ this.pushSync().catch(err => logger_1.KWLogger.error("Auto-push sync failed in updateRecord: " + err));
140
+ }
141
+ return saved;
142
+ }
143
+ async rotatePassword(username, oldPassword, newPassword) {
144
+ logger_1.KWLogger.info("Initiating password rotation (Key Rotation)...");
145
+ if (!this.vault.isUnlocked()) {
146
+ throw new Error("Vault must be unlocked to perform password rotation.");
147
+ }
148
+ const oldKey = this.vault.getKey();
149
+ let mnemonic = this.vault.getMnemonic();
150
+ let wasLegacy = false;
151
+ if (!mnemonic) {
152
+ wasLegacy = true;
153
+ mnemonic = engine_1.KWCryptoEngine.generateMnemonic();
154
+ }
155
+ const newPasswordKey = await engine_1.KWCryptoEngine.deriveMasterKeyFromPassword(newPassword, username);
156
+ const newEncryptedMnemonic = await engine_1.KWCryptoEngine.encryptMnemonic(mnemonic, newPasswordKey);
157
+ // 1. Update password verifier on the server
158
+ await this.pipeline.network.changePassword(username, oldPassword, newPassword, newEncryptedMnemonic);
159
+ // 2. Derive new encryption key locally
160
+ const tempVault = new vault_1.KWVault();
161
+ await tempVault.login(mnemonic, this.appId);
162
+ const newKey = tempVault.getKey();
163
+ // 3. If the user was legacy, re-encrypt all local records
164
+ if (wasLegacy) {
165
+ logger_1.KWLogger.info("Upgrading legacy user to E2EE recovery: re-encrypting local records...");
166
+ const allRecords = await this.storage.getAllRecords();
167
+ for (const r of allRecords) {
168
+ if (r.category === "sync_metadata" || r.id.startsWith("__sync_metadata_")) {
169
+ continue;
170
+ }
171
+ try {
172
+ let decryptedPayload = null;
173
+ if (r.payload) {
174
+ decryptedPayload = await engine_1.KWCryptoEngine.decryptPayload(r.payload, oldKey);
175
+ }
176
+ if (decryptedPayload) {
177
+ const newEncrypted = await engine_1.KWCryptoEngine.encryptPayload(decryptedPayload, newKey);
178
+ r.payload = newEncrypted;
179
+ }
180
+ r.logical_clock += 1;
181
+ r.updated_at = Date.now() / 1000;
182
+ r.is_synced = 0;
183
+ await this.storage.saveRecord(r.id, r);
184
+ }
185
+ catch (err) {
186
+ logger_1.KWLogger.error(`Failed to rotate key for record ${r.id}: ${err}`);
187
+ }
188
+ }
189
+ }
190
+ // 4. Update current vault and session token
191
+ await this.vault.login(mnemonic, this.appId);
192
+ await this.pipeline.network.login(username, newPassword, this.appId);
193
+ // 5. Force push the re-encrypted records if they were upgraded
194
+ if (wasLegacy) {
195
+ logger_1.KWLogger.info("Pushing re-encrypted records to the cloud...");
196
+ await this.pushSync();
197
+ }
198
+ logger_1.KWLogger.info("Password rotation completed successfully!");
53
199
  }
54
200
  async pushSync() {
55
- return await this.pipeline.pushSync(this.appId);
201
+ return await this.pipeline.pushSync();
56
202
  }
57
203
  async pullSync() {
58
- return await this.pipeline.pullSync(this.appId);
204
+ return await this.pipeline.pullSync();
205
+ }
206
+ async uploadAsset(fileBuffer, mimeType, userId) {
207
+ const { manifest, encryptedChunks } = await this.assetEngine.encryptAsset(fileBuffer, mimeType);
208
+ for (let i = 0; i < encryptedChunks.length; i++) {
209
+ await this.pipeline.network.uploadAssetChunk({
210
+ asset_id: manifest.assetId,
211
+ chunk_index: i,
212
+ total_chunks: encryptedChunks.length,
213
+ payload: encryptedChunks[i]
214
+ });
215
+ }
216
+ await this.saveRecord(userId, manifest, "assets");
217
+ return manifest;
218
+ }
219
+ async downloadAsset(assetId, mimeType) {
220
+ const chunks = await this.pipeline.network.downloadAsset(assetId);
221
+ chunks.sort((a, b) => a.chunk_index - b.chunk_index);
222
+ const encryptedChunks = chunks.map(c => c.payload);
223
+ return await this.assetEngine.decryptAsset(encryptedChunks, mimeType);
224
+ }
225
+ async startSync(onSyncUpdate) {
226
+ logger_1.KWLogger.info("Initializing sync: performing initial pullSync...");
227
+ try {
228
+ await this.pullSync();
229
+ }
230
+ catch (e) {
231
+ logger_1.KWLogger.error("Initial pullSync failed during startSync: " + e);
232
+ }
233
+ this.startSyncListener(onSyncUpdate);
234
+ if (onSyncUpdate) {
235
+ onSyncUpdate();
236
+ }
237
+ }
238
+ startSyncListener(onSyncUpdate) {
239
+ if (this.ws) {
240
+ this.ws.close();
241
+ }
242
+ let wsUrl = this.pipeline.network["apiUrl"].replace(/^http/, "ws");
243
+ const token = this.pipeline.network["sessionToken"] || "";
244
+ wsUrl = `${wsUrl}/ws/${this.appId}?token=${token}`;
245
+ const WS = typeof WebSocket !== "undefined" ? WebSocket : globalThis.WebSocket;
246
+ if (!WS) {
247
+ logger_1.KWLogger.info("WebSocket is not supported in this environment.");
248
+ return;
249
+ }
250
+ this.ws = new WS(wsUrl);
251
+ this.ws.onmessage = async (event) => {
252
+ try {
253
+ const data = JSON.parse(event.data);
254
+ if (data.event === "SYNC_UPDATED") {
255
+ logger_1.KWLogger.info("Real-time sync event received. Pulling updates...");
256
+ await this.pullSync();
257
+ if (onSyncUpdate) {
258
+ onSyncUpdate();
259
+ }
260
+ }
261
+ else if (data.event === "SESSION_EXPIRED") {
262
+ logger_1.KWLogger.info("Session expired (password changed on another device). Logging out...");
263
+ await this.logout();
264
+ if (onSyncUpdate) {
265
+ onSyncUpdate();
266
+ }
267
+ }
268
+ }
269
+ catch (err) {
270
+ logger_1.KWLogger.error("Failed to process sync websocket message: " + err);
271
+ }
272
+ };
273
+ this.ws.onerror = (err) => {
274
+ logger_1.KWLogger.error("Sync WebSocket error: " + err);
275
+ };
276
+ this.ws.onclose = () => {
277
+ logger_1.KWLogger.info("Sync WebSocket closed.");
278
+ };
279
+ }
280
+ stopSyncListener() {
281
+ if (this.ws) {
282
+ this.ws.close();
283
+ this.ws = null;
284
+ }
59
285
  }
60
286
  }
61
287
  exports.KaiwenApp = KaiwenApp;
@@ -1,10 +1,12 @@
1
1
  import { KWVault } from "../storage/vault";
2
+ import { KWNetwork } from "../sync/network";
2
3
  import { IKWStorage } from "../storage/db";
3
4
  export declare class KWPipeline {
5
+ private appId;
4
6
  private vault;
5
7
  private storage;
6
- private network;
7
- constructor(vault: KWVault, storage: IKWStorage, apiUrl: string, apiKey: string);
8
- pushSync(appId: string): Promise<number>;
9
- pullSync(appId: string): Promise<number>;
8
+ network: KWNetwork;
9
+ constructor(appId: string, vault: KWVault, storage: IKWStorage, apiUrl: string, apiKey: string);
10
+ pushSync(): Promise<number>;
11
+ pullSync(): Promise<number>;
10
12
  }
@@ -5,12 +5,13 @@ const network_1 = require("../sync/network");
5
5
  const record_1 = require("../models/record");
6
6
  const logger_1 = require("../utils/logger");
7
7
  class KWPipeline {
8
- constructor(vault, storage, apiUrl, apiKey) {
8
+ constructor(appId, vault, storage, apiUrl, apiKey) {
9
+ this.appId = appId;
9
10
  this.vault = vault;
10
11
  this.storage = storage;
11
12
  this.network = new network_1.KWNetwork(apiUrl, apiKey);
12
13
  }
13
- async pushSync(appId) {
14
+ async pushSync() {
14
15
  logger_1.KWLogger.info("Starting pushSync...");
15
16
  const allRecords = await this.storage.getAllRecords();
16
17
  const pending = allRecords.filter(r => r.is_synced === 0);
@@ -23,25 +24,62 @@ class KWPipeline {
23
24
  }
24
25
  return syncedCount;
25
26
  }
26
- async pullSync(appId) {
27
+ async pullSync() {
27
28
  logger_1.KWLogger.info("Starting pullSync...");
28
- const allRecords = await this.storage.getAllRecords();
29
- let latestUpdate = 0;
30
- allRecords.forEach(r => {
31
- if (r.updated_at > latestUpdate)
32
- latestUpdate = r.updated_at;
33
- });
34
- const incomingRecords = await this.network.pull(appId, latestUpdate);
29
+ let lastPullTime = 0;
30
+ const metadata = await this.storage.getRecord(`__sync_metadata_${this.appId}`);
31
+ if (metadata) {
32
+ try {
33
+ const data = JSON.parse(metadata.payload);
34
+ lastPullTime = data.lastPullTime || 0;
35
+ }
36
+ catch (e) {
37
+ // ignore parsing errors
38
+ }
39
+ }
35
40
  let mergedCount = 0;
36
- for (const remoteObj of incomingRecords) {
37
- const remote = record_1.KWRecord.fromObject(remoteObj);
38
- const local = await this.storage.getRecord(remote.id);
39
- if (!local || remote.logical_clock > local.logical_clock) {
40
- remote.is_synced = 1;
41
- await this.storage.saveRecord(remote.id, remote);
42
- mergedCount++;
41
+ let maxRemoteUpdatedAt = lastPullTime;
42
+ const limit = 50;
43
+ let hasMore = true;
44
+ let currentSince = lastPullTime;
45
+ while (hasMore) {
46
+ const querySince = (currentSince === lastPullTime)
47
+ ? Math.max(0, lastPullTime - 1)
48
+ : currentSince;
49
+ const incomingRecords = await this.network.pull(this.appId, querySince, limit);
50
+ if (incomingRecords.length === 0) {
51
+ hasMore = false;
52
+ break;
53
+ }
54
+ let batchMaxRemoteUpdatedAt = currentSince;
55
+ for (const remoteObj of incomingRecords) {
56
+ const remote = record_1.KWRecord.fromObject(remoteObj);
57
+ const local = await this.storage.getRecord(remote.id);
58
+ if (remote.updated_at > batchMaxRemoteUpdatedAt) {
59
+ batchMaxRemoteUpdatedAt = remote.updated_at;
60
+ }
61
+ if (!local || remote.logical_clock > local.logical_clock) {
62
+ remote.is_synced = 1;
63
+ await this.storage.saveRecord(remote.id, remote);
64
+ mergedCount++;
65
+ }
66
+ }
67
+ if (incomingRecords.length < limit) {
68
+ hasMore = false;
69
+ }
70
+ if (batchMaxRemoteUpdatedAt <= currentSince) {
71
+ hasMore = false;
72
+ }
73
+ else {
74
+ currentSince = batchMaxRemoteUpdatedAt;
75
+ }
76
+ if (batchMaxRemoteUpdatedAt > maxRemoteUpdatedAt) {
77
+ maxRemoteUpdatedAt = batchMaxRemoteUpdatedAt;
43
78
  }
44
79
  }
80
+ // Save metadata record
81
+ const metaRecord = new record_1.KWRecord(`__sync_metadata_${this.appId}`, this.appId, "system", "sync_metadata", JSON.stringify({ lastPullTime: maxRemoteUpdatedAt }), Date.now() / 1000, 1, 0, 1, 1);
82
+ await this.storage.saveRecord(metaRecord.id, metaRecord);
45
83
  return mergedCount;
46
84
  }
47
85
  }
@@ -3,6 +3,14 @@ export declare class KWCryptoEngine {
3
3
  private static bufferToBase64;
4
4
  private static base64ToBuffer;
5
5
  static deriveAppKeyFromMnemonic(mnemonic: string, appId: string): Promise<CryptoKey>;
6
+ private static readonly WORDLIST;
7
+ static generateMnemonic(): string;
8
+ private static importAesKey;
9
+ static encryptMnemonic(mnemonic: string, masterKeyRaw: Uint8Array): Promise<string>;
10
+ static decryptMnemonic(encryptedMnemonic: string, masterKeyRaw: Uint8Array): Promise<string>;
11
+ static sha256(text: string): Promise<string>;
12
+ static deriveMasterKeyFromPassword(password: string, username: string): Promise<Uint8Array>;
13
+ static deriveAppKeyFromMasterKey(masterKeyRaw: Uint8Array, appId: string): Promise<CryptoKey>;
6
14
  static encryptPayload(payload: any, appKey: CryptoKey): Promise<string>;
7
15
  static decryptPayload(encryptedPayloadBase64: string, appKey: CryptoKey): Promise<any>;
8
16
  static encryptAssetChunk(data: Uint8Array, appKey: CryptoKey): Promise<string>;