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 +72 -39
- package/dist/app.d.ts +16 -2
- package/dist/app.js +232 -6
- package/dist/core/pipeline.d.ts +6 -4
- package/dist/core/pipeline.js +55 -17
- package/dist/crypto/engine.d.ts +8 -0
- package/dist/crypto/engine.js +77 -0
- package/dist/models/record.d.ts +2 -1
- package/dist/models/record.js +3 -2
- package/dist/storage/vault.d.ts +3 -0
- package/dist/storage/vault.js +11 -0
- package/dist/sync/network.d.ts +17 -1
- package/dist/sync/network.js +138 -5
- package/kaiwen-core-js-0.1.11.tgz +0 -0
- package/package.json +1 -1
- package/src/app.ts +265 -7
- package/src/core/pipeline.ts +75 -18
- package/src/crypto/engine.ts +110 -2
- package/src/models/record.ts +11 -2
- package/src/storage/vault.ts +13 -0
- package/src/sync/network.ts +147 -5
- package/kaiwen-core-js-0.1.1.tgz +0 -0
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
|
|
32
|
+
### 1. Initialization
|
|
31
33
|
|
|
32
|
-
Initialize the SDK by providing
|
|
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/
|
|
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
|
-
"
|
|
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
|
-
|
|
47
|
-
await app.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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.
|
|
106
|
+
### 4. Değişiklik Yapma (Otomatik Arkaplan Eşitleme)
|
|
74
107
|
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
123
|
+
### 5. Manuel Eşitleme (Opsiyonel)
|
|
92
124
|
|
|
93
|
-
|
|
125
|
+
If you need to force a manual sync, you can call push and pull directly:
|
|
94
126
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
201
|
+
return await this.pipeline.pushSync();
|
|
56
202
|
}
|
|
57
203
|
async pullSync() {
|
|
58
|
-
return await this.pipeline.pullSync(
|
|
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;
|
package/dist/core/pipeline.d.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
-
constructor(vault: KWVault, storage: IKWStorage, apiUrl: string, apiKey: string);
|
|
8
|
-
pushSync(
|
|
9
|
-
pullSync(
|
|
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
|
}
|
package/dist/core/pipeline.js
CHANGED
|
@@ -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(
|
|
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(
|
|
27
|
+
async pullSync() {
|
|
27
28
|
logger_1.KWLogger.info("Starting pullSync...");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
}
|
package/dist/crypto/engine.d.ts
CHANGED
|
@@ -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>;
|