kaiwen-core-js 0.1.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 +108 -0
- package/dist/app.d.ts +20 -0
- package/dist/app.js +46 -0
- package/dist/core/exceptions.d.ts +9 -0
- package/dist/core/exceptions.js +24 -0
- package/dist/core/pipeline.d.ts +10 -0
- package/dist/core/pipeline.js +48 -0
- package/dist/crypto/engine.d.ts +10 -0
- package/dist/crypto/engine.js +80 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +34 -0
- package/dist/models/enums.d.ts +8 -0
- package/dist/models/enums.js +13 -0
- package/dist/models/migration.d.ts +7 -0
- package/dist/models/migration.js +22 -0
- package/dist/models/record.d.ts +12 -0
- package/dist/models/record.js +19 -0
- package/dist/models/tracer.d.ts +4 -0
- package/dist/models/tracer.js +9 -0
- package/dist/storage/assets.d.ts +17 -0
- package/dist/storage/assets.js +52 -0
- package/dist/storage/db.d.ts +12 -0
- package/dist/storage/db.js +19 -0
- package/dist/storage/vault.d.ts +7 -0
- package/dist/storage/vault.js +25 -0
- package/dist/sync/network.d.ts +8 -0
- package/dist/sync/network.js +33 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +12 -0
- package/dist/utils/tracer.d.ts +5 -0
- package/dist/utils/tracer.js +16 -0
- package/dist/utils/validator.d.ts +3 -0
- package/dist/utils/validator.js +11 -0
- package/package.json +21 -0
- package/src/app.ts +60 -0
- package/src/core/exceptions.ts +20 -0
- package/src/core/pipeline.ts +57 -0
- package/src/crypto/engine.ts +105 -0
- package/src/index.ts +14 -0
- package/src/models/enums.ts +9 -0
- package/src/models/migration.ts +18 -0
- package/src/models/record.ts +19 -0
- package/src/models/tracer.ts +6 -0
- package/src/storage/assets.ts +63 -0
- package/src/storage/db.ts +24 -0
- package/src/storage/vault.ts +23 -0
- package/src/sync/network.ts +28 -0
- package/src/utils/logger.ts +8 -0
- package/src/utils/tracer.ts +13 -0
- package/src/utils/validator.ts +7 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { KWRecord } from "../models/record";
|
|
2
|
+
export declare class KWNetwork {
|
|
3
|
+
private apiUrl;
|
|
4
|
+
private apiKey;
|
|
5
|
+
constructor(apiUrl: string, apiKey: string);
|
|
6
|
+
push(records: KWRecord[]): Promise<number>;
|
|
7
|
+
pull(appId: string, since: number): Promise<any[]>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KWNetwork = void 0;
|
|
4
|
+
const exceptions_1 = require("../core/exceptions");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
class KWNetwork {
|
|
7
|
+
constructor(apiUrl, apiKey) {
|
|
8
|
+
this.apiUrl = apiUrl;
|
|
9
|
+
this.apiKey = apiKey;
|
|
10
|
+
}
|
|
11
|
+
async push(records) {
|
|
12
|
+
logger_1.KWLogger.info("Network push initiated.");
|
|
13
|
+
const res = await fetch(`${this.apiUrl}/sync`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
|
|
16
|
+
body: JSON.stringify(records)
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok)
|
|
19
|
+
throw new exceptions_1.SyncFailedException("Network push failed");
|
|
20
|
+
const data = await res.json();
|
|
21
|
+
return data.synced || 0;
|
|
22
|
+
}
|
|
23
|
+
async pull(appId, since) {
|
|
24
|
+
logger_1.KWLogger.info(`Network pull initiated since ${since}.`);
|
|
25
|
+
const res = await fetch(`${this.apiUrl}/fetch?app_id=${appId}&since=${since}`, {
|
|
26
|
+
headers: { "x-api-key": this.apiKey }
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
throw new exceptions_1.SyncFailedException("Network pull failed");
|
|
30
|
+
return await res.json();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.KWNetwork = KWNetwork;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KWLogger = void 0;
|
|
4
|
+
class KWLogger {
|
|
5
|
+
static info(message) {
|
|
6
|
+
console.log(`[KWCore] INFO: ${message}`);
|
|
7
|
+
}
|
|
8
|
+
static error(message, error) {
|
|
9
|
+
console.error(`[KWCore] ERROR: ${message}`, error);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.KWLogger = KWLogger;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KWTracer = void 0;
|
|
4
|
+
const tracer_1 = require("../models/tracer");
|
|
5
|
+
const logger_1 = require("./logger");
|
|
6
|
+
class KWTracer {
|
|
7
|
+
static traceStart(operation) {
|
|
8
|
+
const ctx = new tracer_1.KWTracerContext();
|
|
9
|
+
logger_1.KWLogger.info(`[Trace ${ctx.traceId}] Started: ${operation}`);
|
|
10
|
+
return ctx;
|
|
11
|
+
}
|
|
12
|
+
static traceEnd(ctx, operation) {
|
|
13
|
+
logger_1.KWLogger.info(`[Trace ${ctx.traceId}] Ended: ${operation}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.KWTracer = KWTracer;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KWValidator = void 0;
|
|
4
|
+
class KWValidator {
|
|
5
|
+
static validatePayload(payload) {
|
|
6
|
+
if (payload === undefined || payload === null) {
|
|
7
|
+
throw new Error("Payload cannot be null or undefined");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.KWValidator = KWValidator;
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kaiwen-core-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kaiwen Cloud Ecosystem SDK for TypeScript & JavaScript environments.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"kaiwen",
|
|
12
|
+
"e2ee",
|
|
13
|
+
"crdt",
|
|
14
|
+
"offline-first"
|
|
15
|
+
],
|
|
16
|
+
"author": "Kerem Keskinoğlu",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { KWVault } from "./storage/vault";
|
|
2
|
+
import { KWCryptoEngine } from "./crypto/engine";
|
|
3
|
+
import { KWMigrationEngine } from "./models/migration";
|
|
4
|
+
import { KWAssetEngine } from "./storage/assets";
|
|
5
|
+
import { KWPipeline } from "./core/pipeline";
|
|
6
|
+
import { IKWStorage, MemoryStorage } from "./storage/db";
|
|
7
|
+
import { KWRecord } from "./models/record";
|
|
8
|
+
import { KWValidator } from "./utils/validator";
|
|
9
|
+
|
|
10
|
+
export class KaiwenApp {
|
|
11
|
+
public vault: KWVault;
|
|
12
|
+
public assetEngine: KWAssetEngine;
|
|
13
|
+
public pipeline: KWPipeline;
|
|
14
|
+
public migration: KWMigrationEngine;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private appId: string,
|
|
18
|
+
apiUrl: string,
|
|
19
|
+
apiKey: string,
|
|
20
|
+
private storage: IKWStorage = new MemoryStorage()
|
|
21
|
+
) {
|
|
22
|
+
this.vault = new KWVault();
|
|
23
|
+
this.assetEngine = new KWAssetEngine(this.vault);
|
|
24
|
+
this.pipeline = new KWPipeline(this.vault, storage, apiUrl, apiKey);
|
|
25
|
+
this.migration = new KWMigrationEngine(storage, 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async login(mnemonic: string): Promise<void> {
|
|
29
|
+
await this.vault.login(mnemonic, this.appId);
|
|
30
|
+
await this.migration.migrateAll();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async saveRecord(userId: string, payload: any): Promise<KWRecord> {
|
|
34
|
+
KWValidator.validatePayload(payload);
|
|
35
|
+
|
|
36
|
+
const encrypted = await KWCryptoEngine.encryptPayload(payload, this.vault.getKey());
|
|
37
|
+
const generateId = () => typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2);
|
|
38
|
+
|
|
39
|
+
const record = new KWRecord(
|
|
40
|
+
generateId(), this.appId, userId, encrypted, Date.now() / 1000, 0, 1, 1
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
await this.storage.saveRecord(record.id, record);
|
|
44
|
+
return record;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getDecryptedRecord(id: string): Promise<any> {
|
|
48
|
+
const record = await this.storage.getRecord(id);
|
|
49
|
+
if (!record) return null;
|
|
50
|
+
return await KWCryptoEngine.decryptPayload(record.payload, this.vault.getKey());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async pushSync(): Promise<number> {
|
|
54
|
+
return await this.pipeline.pushSync(this.appId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async pullSync(): Promise<number> {
|
|
58
|
+
return await this.pipeline.pullSync(this.appId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class KWException extends Error {
|
|
2
|
+
constructor(message: string) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "KWException";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class VaultLockedException extends KWException {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("Vault is locked. User not authenticated.");
|
|
11
|
+
this.name = "VaultLockedException";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SyncFailedException extends KWException {
|
|
16
|
+
constructor(message: string) {
|
|
17
|
+
super(`Sync failed: ${message}`);
|
|
18
|
+
this.name = "SyncFailedException";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { KWVault } from "../storage/vault";
|
|
2
|
+
import { KWNetwork } from "../sync/network";
|
|
3
|
+
import { IKWStorage } from "../storage/db";
|
|
4
|
+
import { KWRecord } from "../models/record";
|
|
5
|
+
import { KWLogger } from "../utils/logger";
|
|
6
|
+
|
|
7
|
+
export class KWPipeline {
|
|
8
|
+
private network: KWNetwork;
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private vault: KWVault,
|
|
12
|
+
private storage: IKWStorage,
|
|
13
|
+
apiUrl: string,
|
|
14
|
+
apiKey: string
|
|
15
|
+
) {
|
|
16
|
+
this.network = new KWNetwork(apiUrl, apiKey);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async pushSync(appId: string): Promise<number> {
|
|
20
|
+
KWLogger.info("Starting pushSync...");
|
|
21
|
+
const allRecords = await this.storage.getAllRecords();
|
|
22
|
+
const pending = allRecords.filter(r => r.is_synced === 0);
|
|
23
|
+
if (pending.length === 0) return 0;
|
|
24
|
+
|
|
25
|
+
const syncedCount = await this.network.push(pending);
|
|
26
|
+
|
|
27
|
+
for (const r of pending) {
|
|
28
|
+
r.is_synced = 1;
|
|
29
|
+
await this.storage.saveRecord(r.id, r);
|
|
30
|
+
}
|
|
31
|
+
return syncedCount;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async pullSync(appId: string): Promise<number> {
|
|
35
|
+
KWLogger.info("Starting pullSync...");
|
|
36
|
+
const allRecords = await this.storage.getAllRecords();
|
|
37
|
+
let latestUpdate = 0;
|
|
38
|
+
allRecords.forEach(r => {
|
|
39
|
+
if (r.updated_at > latestUpdate) latestUpdate = r.updated_at;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const incomingRecords = await this.network.pull(appId, latestUpdate);
|
|
43
|
+
|
|
44
|
+
let mergedCount = 0;
|
|
45
|
+
for (const remoteObj of incomingRecords) {
|
|
46
|
+
const remote = KWRecord.fromObject(remoteObj);
|
|
47
|
+
const local = await this.storage.getRecord(remote.id);
|
|
48
|
+
|
|
49
|
+
if (!local || remote.logical_clock > local.logical_clock) {
|
|
50
|
+
remote.is_synced = 1;
|
|
51
|
+
await this.storage.saveRecord(remote.id, remote);
|
|
52
|
+
mergedCount++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return mergedCount;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export class KWCryptoEngine {
|
|
2
|
+
private static getSubtleCrypto(): SubtleCrypto {
|
|
3
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
4
|
+
return crypto.subtle; // Modern Browsers & Deno
|
|
5
|
+
}
|
|
6
|
+
// Node.js fallback or RN polyfill
|
|
7
|
+
if (typeof globalThis !== "undefined" && (globalThis as any).crypto && (globalThis as any).crypto.subtle) {
|
|
8
|
+
return (globalThis as any).crypto.subtle;
|
|
9
|
+
}
|
|
10
|
+
throw new Error("Web Crypto API (crypto.subtle) is not available in this environment.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private static bufferToBase64(buffer: Uint8Array): string {
|
|
14
|
+
let binary = '';
|
|
15
|
+
const len = buffer.byteLength;
|
|
16
|
+
for (let i = 0; i < len; i++) {
|
|
17
|
+
binary += String.fromCharCode(buffer[i]);
|
|
18
|
+
}
|
|
19
|
+
return btoa(binary);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private static base64ToBuffer(base64: string): Uint8Array {
|
|
23
|
+
const binary = atob(base64);
|
|
24
|
+
const len = binary.length;
|
|
25
|
+
const bytes = new Uint8Array(len);
|
|
26
|
+
for (let i = 0; i < len; i++) {
|
|
27
|
+
bytes[i] = binary.charCodeAt(i);
|
|
28
|
+
}
|
|
29
|
+
return bytes;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static async deriveAppKeyFromMnemonic(mnemonic: string, appId: string): Promise<CryptoKey> {
|
|
33
|
+
const subtle = this.getSubtleCrypto();
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
|
|
36
|
+
// 1. PBKDF2 Master Key Derivation (matches Python)
|
|
37
|
+
const pbkdf2KeyMaterial = await subtle.importKey(
|
|
38
|
+
"raw", encoder.encode(mnemonic), { name: "PBKDF2" }, false, ["deriveBits"]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const pbkdf2Salt = encoder.encode("visuality_global_salt");
|
|
42
|
+
const masterKeyRaw = await subtle.deriveBits(
|
|
43
|
+
{ name: "PBKDF2", salt: pbkdf2Salt, iterations: 200000, hash: "SHA-256" },
|
|
44
|
+
pbkdf2KeyMaterial,
|
|
45
|
+
256
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// 2. HKDF App Key Derivation (matches Python)
|
|
49
|
+
const hkdfKeyMaterial = await subtle.importKey(
|
|
50
|
+
"raw", masterKeyRaw, { name: "HKDF" }, false, ["deriveKey"]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return await subtle.deriveKey(
|
|
54
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: encoder.encode(appId) },
|
|
55
|
+
hkdfKeyMaterial,
|
|
56
|
+
{ name: "AES-GCM", length: 256 },
|
|
57
|
+
false,
|
|
58
|
+
["encrypt", "decrypt"]
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static async encryptPayload(payload: any, appKey: CryptoKey): Promise<string> {
|
|
63
|
+
const encoder = new TextEncoder();
|
|
64
|
+
const data = encoder.encode(JSON.stringify(payload));
|
|
65
|
+
return this.encryptAssetChunk(data, appKey);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static async decryptPayload(encryptedPayloadBase64: string, appKey: CryptoKey): Promise<any> {
|
|
69
|
+
const decoder = new TextDecoder();
|
|
70
|
+
const decryptedBuffer = await this.decryptAssetChunk(encryptedPayloadBase64, appKey);
|
|
71
|
+
return JSON.parse(decoder.decode(decryptedBuffer));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Raw Uint8Array Encryption for Asset Engine
|
|
75
|
+
static async encryptAssetChunk(data: Uint8Array, appKey: CryptoKey): Promise<string> {
|
|
76
|
+
const subtle = this.getSubtleCrypto();
|
|
77
|
+
|
|
78
|
+
const iv = new Uint8Array(12);
|
|
79
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
80
|
+
crypto.getRandomValues(iv);
|
|
81
|
+
} else {
|
|
82
|
+
for (let i=0; i<12; i++) iv[i] = Math.floor(Math.random() * 256);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const encryptedBuffer = await subtle.encrypt({ name: "AES-GCM", iv: iv }, appKey, data as any);
|
|
86
|
+
|
|
87
|
+
const encryptedArray = new Uint8Array(encryptedBuffer);
|
|
88
|
+
const combined = new Uint8Array(iv.length + encryptedArray.length);
|
|
89
|
+
combined.set(iv, 0);
|
|
90
|
+
combined.set(encryptedArray, iv.length);
|
|
91
|
+
|
|
92
|
+
return this.bufferToBase64(combined);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static async decryptAssetChunk(encryptedBase64: string, appKey: CryptoKey): Promise<Uint8Array> {
|
|
96
|
+
const subtle = this.getSubtleCrypto();
|
|
97
|
+
const combined = this.base64ToBuffer(encryptedBase64);
|
|
98
|
+
|
|
99
|
+
const iv = combined.slice(0, 12);
|
|
100
|
+
const ciphertext = combined.slice(12);
|
|
101
|
+
|
|
102
|
+
const decryptedBuffer = await subtle.decrypt({ name: "AES-GCM", iv: iv }, appKey, ciphertext as any);
|
|
103
|
+
return new Uint8Array(decryptedBuffer);
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { KaiwenApp } from "./app";
|
|
2
|
+
export { IKWStorage, MemoryStorage } from "./storage/db";
|
|
3
|
+
export { KWRecord } from "./models/record";
|
|
4
|
+
export { KWVault } from "./storage/vault";
|
|
5
|
+
export { KWAssetEngine, KWAssetManifest } from "./storage/assets";
|
|
6
|
+
export { KWMigrationEngine } from "./models/migration";
|
|
7
|
+
export { KWCryptoEngine } from "./crypto/engine";
|
|
8
|
+
export { KWPipeline } from "./core/pipeline";
|
|
9
|
+
export { KWSyncStatus, KWRecordType } from "./models/enums";
|
|
10
|
+
export { KWException, VaultLockedException, SyncFailedException } from "./core/exceptions";
|
|
11
|
+
export { KWLogger } from "./utils/logger";
|
|
12
|
+
export { KWTracer } from "./utils/tracer";
|
|
13
|
+
export { KWValidator } from "./utils/validator";
|
|
14
|
+
export { KWNetwork } from "./sync/network";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { IKWStorage } from "../storage/db";
|
|
2
|
+
|
|
3
|
+
export class KWMigrationEngine {
|
|
4
|
+
constructor(private storage: IKWStorage, private currentSchemaVersion: number) {}
|
|
5
|
+
|
|
6
|
+
async migrateAll(): Promise<void> {
|
|
7
|
+
const records = await this.storage.getAllRecords();
|
|
8
|
+
for (const record of records) {
|
|
9
|
+
if (record.schema_version < this.currentSchemaVersion) {
|
|
10
|
+
record.schema_version = this.currentSchemaVersion;
|
|
11
|
+
record.updated_at = Date.now() / 1000;
|
|
12
|
+
record.logical_clock += 1;
|
|
13
|
+
record.is_synced = 0;
|
|
14
|
+
await this.storage.saveRecord(record.id, record);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class KWRecord {
|
|
2
|
+
constructor(
|
|
3
|
+
public id: string,
|
|
4
|
+
public app_id: string,
|
|
5
|
+
public user_id: string,
|
|
6
|
+
public payload: string,
|
|
7
|
+
public updated_at: number,
|
|
8
|
+
public is_synced: number = 0,
|
|
9
|
+
public schema_version: number = 1,
|
|
10
|
+
public logical_clock: number = 1
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
static fromObject(obj: any): KWRecord {
|
|
14
|
+
return new KWRecord(
|
|
15
|
+
obj.id, obj.app_id, obj.user_id, obj.payload,
|
|
16
|
+
obj.updated_at, obj.is_synced, obj.schema_version, obj.logical_clock
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { KWCryptoEngine } from "../crypto/engine";
|
|
2
|
+
import { KWVault } from "./vault";
|
|
3
|
+
|
|
4
|
+
export interface KWAssetManifest {
|
|
5
|
+
assetId: string;
|
|
6
|
+
totalChunks: number;
|
|
7
|
+
mimeType: string;
|
|
8
|
+
size: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class KWAssetEngine {
|
|
12
|
+
private readonly CHUNK_SIZE = 1024 * 1024; // 1MB
|
|
13
|
+
|
|
14
|
+
constructor(private vault: KWVault) {}
|
|
15
|
+
|
|
16
|
+
async encryptAsset(fileBuffer: Uint8Array, mimeType: string): Promise<{ manifest: KWAssetManifest, encryptedChunks: string[] }> {
|
|
17
|
+
if (!this.vault.isUnlocked()) throw new Error("Vault locked");
|
|
18
|
+
const key = this.vault.getKey();
|
|
19
|
+
const chunks: string[] = [];
|
|
20
|
+
|
|
21
|
+
const generateId = () => typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2);
|
|
22
|
+
const assetId = generateId();
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < fileBuffer.length; i += this.CHUNK_SIZE) {
|
|
25
|
+
const chunk = fileBuffer.slice(i, i + this.CHUNK_SIZE);
|
|
26
|
+
const encryptedChunk = await KWCryptoEngine.encryptAssetChunk(chunk, key);
|
|
27
|
+
chunks.push(encryptedChunk);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
manifest: {
|
|
32
|
+
assetId,
|
|
33
|
+
totalChunks: chunks.length,
|
|
34
|
+
mimeType,
|
|
35
|
+
size: fileBuffer.length
|
|
36
|
+
},
|
|
37
|
+
encryptedChunks: chunks
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async decryptAsset(encryptedChunks: string[], mimeType: string): Promise<Uint8Array> {
|
|
42
|
+
if (!this.vault.isUnlocked()) throw new Error("Vault locked");
|
|
43
|
+
const key = this.vault.getKey();
|
|
44
|
+
|
|
45
|
+
let totalLength = 0;
|
|
46
|
+
const decryptedChunks: Uint8Array[] = [];
|
|
47
|
+
|
|
48
|
+
for (const chunk64 of encryptedChunks) {
|
|
49
|
+
const decrypted = await KWCryptoEngine.decryptAssetChunk(chunk64, key);
|
|
50
|
+
decryptedChunks.push(decrypted);
|
|
51
|
+
totalLength += decrypted.length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = new Uint8Array(totalLength);
|
|
55
|
+
let offset = 0;
|
|
56
|
+
for (const chunk of decryptedChunks) {
|
|
57
|
+
result.set(chunk, offset);
|
|
58
|
+
offset += chunk.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { KWRecord } from "../models/record";
|
|
2
|
+
|
|
3
|
+
export interface IKWStorage {
|
|
4
|
+
saveRecord(id: string, data: KWRecord): Promise<boolean>;
|
|
5
|
+
getRecord(id: string): Promise<KWRecord | null>;
|
|
6
|
+
getAllRecords(): Promise<KWRecord[]>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class MemoryStorage implements IKWStorage {
|
|
10
|
+
private store: Map<string, KWRecord> = new Map();
|
|
11
|
+
|
|
12
|
+
async saveRecord(id: string, data: KWRecord): Promise<boolean> {
|
|
13
|
+
this.store.set(id, data);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getRecord(id: string): Promise<KWRecord | null> {
|
|
18
|
+
return this.store.get(id) || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getAllRecords(): Promise<KWRecord[]> {
|
|
22
|
+
return Array.from(this.store.values());
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { KWCryptoEngine } from "../crypto/engine";
|
|
2
|
+
import { VaultLockedException } from "../core/exceptions";
|
|
3
|
+
|
|
4
|
+
export class KWVault {
|
|
5
|
+
private appKey: CryptoKey | null = null;
|
|
6
|
+
|
|
7
|
+
async login(mnemonic: string, appId: string): Promise<void> {
|
|
8
|
+
this.appKey = await KWCryptoEngine.deriveAppKeyFromMnemonic(mnemonic, appId);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
isUnlocked(): boolean {
|
|
12
|
+
return this.appKey !== null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getKey(): CryptoKey {
|
|
16
|
+
if (!this.appKey) throw new VaultLockedException();
|
|
17
|
+
return this.appKey;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
lock(): void {
|
|
21
|
+
this.appKey = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { KWRecord } from "../models/record";
|
|
2
|
+
import { SyncFailedException } from "../core/exceptions";
|
|
3
|
+
import { KWLogger } from "../utils/logger";
|
|
4
|
+
|
|
5
|
+
export class KWNetwork {
|
|
6
|
+
constructor(private apiUrl: string, private apiKey: string) {}
|
|
7
|
+
|
|
8
|
+
async push(records: KWRecord[]): Promise<number> {
|
|
9
|
+
KWLogger.info("Network push initiated.");
|
|
10
|
+
const res = await fetch(`${this.apiUrl}/sync`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
|
|
13
|
+
body: JSON.stringify(records)
|
|
14
|
+
});
|
|
15
|
+
if (!res.ok) throw new SyncFailedException("Network push failed");
|
|
16
|
+
const data = await res.json();
|
|
17
|
+
return data.synced || 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async pull(appId: string, since: number): Promise<any[]> {
|
|
21
|
+
KWLogger.info(`Network pull initiated since ${since}.`);
|
|
22
|
+
const res = await fetch(`${this.apiUrl}/fetch?app_id=${appId}&since=${since}`, {
|
|
23
|
+
headers: { "x-api-key": this.apiKey }
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) throw new SyncFailedException("Network pull failed");
|
|
26
|
+
return await res.json();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { KWTracerContext } from "../models/tracer";
|
|
2
|
+
import { KWLogger } from "./logger";
|
|
3
|
+
|
|
4
|
+
export class KWTracer {
|
|
5
|
+
static traceStart(operation: string): KWTracerContext {
|
|
6
|
+
const ctx = new KWTracerContext();
|
|
7
|
+
KWLogger.info(`[Trace ${ctx.traceId}] Started: ${operation}`);
|
|
8
|
+
return ctx;
|
|
9
|
+
}
|
|
10
|
+
static traceEnd(ctx: KWTracerContext, operation: string) {
|
|
11
|
+
KWLogger.info(`[Trace ${ctx.traceId}] Ended: ${operation}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"moduleResolution": "node",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|