mirror of
https://github.com/zadam/trilium.git
synced 2026-01-12 09:34:26 +01:00
chore(client/lightweight): fix errors in SQL provider & implement crypto provider
This commit is contained in:
parent
ccf4df8e86
commit
405db7cedb
153
apps/client/src/lightweight/crypto_provider.ts
Normal file
153
apps/client/src/lightweight/crypto_provider.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import type { CryptoProvider } from "@triliumnext/core";
|
||||
|
||||
interface Cipher {
|
||||
update(data: Uint8Array): Uint8Array;
|
||||
final(): Uint8Array;
|
||||
}
|
||||
|
||||
const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
/**
|
||||
* Crypto provider for browser environments using the Web Crypto API.
|
||||
*/
|
||||
export default class BrowserCryptoProvider implements CryptoProvider {
|
||||
|
||||
createHash(algorithm: "sha1" | "sha512", content: string | Uint8Array): Uint8Array {
|
||||
// Web Crypto API is async, but the interface expects sync.
|
||||
// We'll use a synchronous fallback or throw if not available.
|
||||
// For now, we'll implement a simple synchronous hash using SubtleCrypto
|
||||
// Note: This is a limitation - we may need to make the interface async
|
||||
throw new Error(
|
||||
"Synchronous hash not available in browser. " +
|
||||
"Use createHashAsync() instead or refactor to support async hashing."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of createHash using Web Crypto API.
|
||||
*/
|
||||
async createHashAsync(algorithm: "sha1" | "sha512", content: string | Uint8Array): Promise<Uint8Array> {
|
||||
const webAlgorithm = algorithm === "sha1" ? "SHA-1" : "SHA-512";
|
||||
const data = typeof content === "string"
|
||||
? new TextEncoder().encode(content)
|
||||
: new Uint8Array(content);
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest(webAlgorithm, data);
|
||||
return new Uint8Array(hashBuffer);
|
||||
}
|
||||
|
||||
createCipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): Cipher {
|
||||
// Web Crypto API doesn't support streaming cipher like Node.js
|
||||
// We need to implement a wrapper that collects data and encrypts on final()
|
||||
return new WebCryptoCipher(algorithm, key, iv, "encrypt");
|
||||
}
|
||||
|
||||
createDecipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): Cipher {
|
||||
return new WebCryptoCipher(algorithm, key, iv, "decrypt");
|
||||
}
|
||||
|
||||
randomBytes(size: number): Uint8Array {
|
||||
const bytes = new Uint8Array(size);
|
||||
crypto.getRandomValues(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
randomString(length: number): string {
|
||||
const bytes = this.randomBytes(length);
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += CHARS[bytes[i] % CHARS.length];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A cipher implementation that wraps Web Crypto API.
|
||||
* Note: This buffers all data until final() is called, which differs from
|
||||
* Node.js's streaming cipher behavior.
|
||||
*/
|
||||
class WebCryptoCipher implements Cipher {
|
||||
private chunks: Uint8Array[] = [];
|
||||
private algorithm: string;
|
||||
private key: Uint8Array;
|
||||
private iv: Uint8Array;
|
||||
private mode: "encrypt" | "decrypt";
|
||||
private finalized = false;
|
||||
|
||||
constructor(
|
||||
algorithm: "aes-128-cbc",
|
||||
key: Uint8Array,
|
||||
iv: Uint8Array,
|
||||
mode: "encrypt" | "decrypt"
|
||||
) {
|
||||
this.algorithm = algorithm;
|
||||
this.key = key;
|
||||
this.iv = iv;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
update(data: Uint8Array): Uint8Array {
|
||||
if (this.finalized) {
|
||||
throw new Error("Cipher has already been finalized");
|
||||
}
|
||||
// Buffer the data - Web Crypto doesn't support streaming
|
||||
this.chunks.push(data);
|
||||
// Return empty array since we process everything in final()
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
final(): Uint8Array {
|
||||
if (this.finalized) {
|
||||
throw new Error("Cipher has already been finalized");
|
||||
}
|
||||
this.finalized = true;
|
||||
|
||||
// Web Crypto API is async, but we need sync behavior
|
||||
// This is a fundamental limitation that requires architectural changes
|
||||
// For now, throw an error directing users to use async methods
|
||||
throw new Error(
|
||||
"Synchronous cipher finalization not available in browser. " +
|
||||
"The Web Crypto API is async-only. Use finalizeAsync() instead."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version that actually performs the encryption/decryption.
|
||||
*/
|
||||
async finalizeAsync(): Promise<Uint8Array> {
|
||||
if (this.finalized) {
|
||||
throw new Error("Cipher has already been finalized");
|
||||
}
|
||||
this.finalized = true;
|
||||
|
||||
// Concatenate all chunks
|
||||
const totalLength = this.chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const data = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of this.chunks) {
|
||||
data.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
// Copy key and iv to ensure they're plain ArrayBuffer-backed
|
||||
const keyBuffer = new Uint8Array(this.key);
|
||||
const ivBuffer = new Uint8Array(this.iv);
|
||||
|
||||
// Import the key
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyBuffer,
|
||||
{ name: "AES-CBC" },
|
||||
false,
|
||||
[this.mode]
|
||||
);
|
||||
|
||||
// Perform encryption/decryption
|
||||
const result = this.mode === "encrypt"
|
||||
? await crypto.subtle.encrypt({ name: "AES-CBC", iv: ivBuffer }, cryptoKey, data)
|
||||
: await crypto.subtle.decrypt({ name: "AES-CBC", iv: ivBuffer }, cryptoKey, data);
|
||||
|
||||
return new Uint8Array(result);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import type { DatabaseProvider, RunResult, Statement, Transaction } from "@triliumnext/core";
|
||||
import type sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
||||
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
||||
import type { BindableValue } from "@sqlite.org/sqlite-wasm";
|
||||
|
||||
// Type definitions for SQLite WASM (the library doesn't export these directly)
|
||||
@ -116,18 +116,81 @@ class WasmStatement implements Statement {
|
||||
*
|
||||
* This provider wraps the official @sqlite.org/sqlite-wasm package to provide
|
||||
* a DatabaseProvider implementation compatible with trilium-core.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const provider = new BrowserSqlProvider();
|
||||
* await provider.initWasm(); // Initialize SQLite WASM module
|
||||
* provider.loadFromMemory(); // Open an in-memory database
|
||||
* // or
|
||||
* provider.loadFromBuffer(existingDbBuffer); // Load from existing data
|
||||
* ```
|
||||
*/
|
||||
export default class BrowserSqlProvider implements DatabaseProvider {
|
||||
private db?: Sqlite3Database;
|
||||
private sqlite3?: Sqlite3Module;
|
||||
private _inTransaction = false;
|
||||
private initPromise?: Promise<void>;
|
||||
private initError?: Error;
|
||||
|
||||
/**
|
||||
* Initialize the provider with an already-initialized SQLite WASM module.
|
||||
* This must be called before using any database operations.
|
||||
* Get the SQLite WASM module version info.
|
||||
* Returns undefined if the module hasn't been initialized yet.
|
||||
*/
|
||||
initialize(sqlite3: Sqlite3Module): void {
|
||||
this.sqlite3 = sqlite3;
|
||||
get version(): { libVersion: string; sourceId: string } | undefined {
|
||||
return this.sqlite3?.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the SQLite WASM module.
|
||||
* This must be called before using any database operations.
|
||||
* Safe to call multiple times - subsequent calls return the same promise.
|
||||
*
|
||||
* @returns A promise that resolves when the module is initialized
|
||||
* @throws Error if initialization fails
|
||||
*/
|
||||
async initWasm(): Promise<void> {
|
||||
// Return existing promise if already initializing/initialized
|
||||
if (this.initPromise) {
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
// Fail fast if we already tried and failed
|
||||
if (this.initError) {
|
||||
throw this.initError;
|
||||
}
|
||||
|
||||
this.initPromise = this.doInitWasm();
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
private async doInitWasm(): Promise<void> {
|
||||
try {
|
||||
console.log("[BrowserSqlProvider] Initializing SQLite WASM...");
|
||||
const startTime = performance.now();
|
||||
|
||||
this.sqlite3 = await sqlite3InitModule({
|
||||
print: console.log,
|
||||
printErr: console.error,
|
||||
});
|
||||
|
||||
const initTime = performance.now() - startTime;
|
||||
console.log(
|
||||
`[BrowserSqlProvider] SQLite WASM initialized in ${initTime.toFixed(2)}ms:`,
|
||||
this.sqlite3.version.libVersion
|
||||
);
|
||||
} catch (e) {
|
||||
this.initError = e instanceof Error ? e : new Error(String(e));
|
||||
console.error("[BrowserSqlProvider] SQLite WASM initialization failed:", this.initError);
|
||||
throw this.initError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the SQLite WASM module has been initialized.
|
||||
*/
|
||||
get isInitialized(): boolean {
|
||||
return this.sqlite3 !== undefined;
|
||||
}
|
||||
|
||||
loadFromFile(_path: string, _isReadOnly: boolean): void {
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
// This will eventually import your core server and DB provider.
|
||||
// import { createCoreServer } from "@trilium/core"; (bundled)
|
||||
|
||||
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
||||
import BrowserExecutionContext from './lightweight/cls_provider';
|
||||
import BrowserSqlProvider from './lightweight/sql_provider';
|
||||
import BrowserCryptoProvider from './lightweight/crypto_provider';
|
||||
|
||||
// Global error handlers - MUST be set up before any async imports
|
||||
self.onerror = (message, source, lineno, colno, error) => {
|
||||
@ -45,6 +45,54 @@ self.onunhandledrejection = (event) => {
|
||||
|
||||
console.log("[Worker] Error handlers installed");
|
||||
|
||||
// Shared SQL provider instance
|
||||
const sqlProvider = new BrowserSqlProvider();
|
||||
let sqlInitPromise: Promise<void> | null = null;
|
||||
let sqlInitError: string | null = null;
|
||||
|
||||
// Initialize SQLite WASM via the provider
|
||||
async function initSQLite(): Promise<void> {
|
||||
if (sqlProvider.isInitialized && sqlProvider.isOpen()) {
|
||||
return; // Already initialized and database open
|
||||
}
|
||||
if (sqlInitError) {
|
||||
throw new Error(sqlInitError); // Failed before, don't retry
|
||||
}
|
||||
if (sqlInitPromise) {
|
||||
return sqlInitPromise; // Already initializing
|
||||
}
|
||||
|
||||
sqlInitPromise = (async () => {
|
||||
try {
|
||||
// Initialize the WASM module
|
||||
await sqlProvider.initWasm();
|
||||
|
||||
// Open an in-memory database
|
||||
sqlProvider.loadFromMemory();
|
||||
console.log("[Worker] Database opened via provider");
|
||||
|
||||
// Create a simple test table
|
||||
sqlProvider.exec(`
|
||||
CREATE TABLE IF NOT EXISTS options (
|
||||
name TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
INSERT INTO options (name, value) VALUES
|
||||
('theme', 'dark'),
|
||||
('layoutOrientation', 'vertical'),
|
||||
('headingStyle', 'default');
|
||||
`);
|
||||
console.log("[Worker] Test table created and populated");
|
||||
} catch (error) {
|
||||
sqlInitError = String(error);
|
||||
console.error("[Worker] SQLite initialization failed:", error);
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
return sqlInitPromise;
|
||||
}
|
||||
|
||||
// Deferred import for @triliumnext/core to catch initialization errors
|
||||
let coreModule: typeof import("@triliumnext/core") | null = null;
|
||||
let coreInitError: Error | null = null;
|
||||
@ -54,12 +102,16 @@ async function loadCoreModule() {
|
||||
if (coreInitError) throw coreInitError;
|
||||
|
||||
try {
|
||||
// Ensure SQLite is initialized before loading core
|
||||
await initSQLite();
|
||||
|
||||
console.log("[Worker] Loading @triliumnext/core...");
|
||||
coreModule = await import("@triliumnext/core");
|
||||
coreModule.initializeCore({
|
||||
executionContext: new BrowserExecutionContext(),
|
||||
crypto: new BrowserCryptoProvider(),
|
||||
dbConfig: {
|
||||
provider: new BrowserSqlProvider(),
|
||||
provider: sqlProvider,
|
||||
isReadOnly: false,
|
||||
onTransactionCommit: () => {
|
||||
// No-op for now
|
||||
@ -80,13 +132,7 @@ async function loadCoreModule() {
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
// SQLite WASM instance
|
||||
let sqlite3: any = null;
|
||||
let db: any = null;
|
||||
let sqliteInitPromise: Promise<void> | null = null;
|
||||
let sqliteInitError: string | null = null;
|
||||
|
||||
function jsonResponse(obj, status = 200, extraHeaders = {}) {
|
||||
function jsonResponse(obj: unknown, status = 200, extraHeaders = {}) {
|
||||
const body = encoder.encode(JSON.stringify(obj)).buffer;
|
||||
return {
|
||||
status,
|
||||
@ -95,7 +141,7 @@ function jsonResponse(obj, status = 200, extraHeaders = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function textResponse(text, status = 200, extraHeaders = {}) {
|
||||
function textResponse(text: string, status = 200, extraHeaders = {}) {
|
||||
const body = encoder.encode(text).buffer;
|
||||
return {
|
||||
status,
|
||||
@ -104,62 +150,15 @@ function textResponse(text, status = 200, extraHeaders = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize SQLite WASM
|
||||
async function initSQLite() {
|
||||
if (sqlite3) return; // Already initialized
|
||||
if (sqliteInitError) return; // Failed before, don't retry
|
||||
if (sqliteInitPromise) return sqliteInitPromise; // Already initializing
|
||||
|
||||
sqliteInitPromise = (async () => {
|
||||
try {
|
||||
console.log("[Worker] Initializing SQLite WASM...");
|
||||
const startTime = performance.now();
|
||||
|
||||
// Just call the init module without custom locateFile
|
||||
// The module will use import.meta.url to find sqlite3.wasm
|
||||
sqlite3 = await sqlite3InitModule({
|
||||
print: console.log,
|
||||
printErr: console.error,
|
||||
});
|
||||
|
||||
const initTime = performance.now() - startTime;
|
||||
console.log(`[Worker] SQLite WASM initialized in ${initTime.toFixed(2)}ms:`, sqlite3.version);
|
||||
|
||||
// Open a database in memory for now
|
||||
db = new sqlite3.oo1.DB(':memory:', 'c');
|
||||
console.log("[Worker] Database opened");
|
||||
|
||||
// Create a simple test table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS options (
|
||||
name TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
INSERT INTO options (name, value) VALUES
|
||||
('theme', 'dark'),
|
||||
('layoutOrientation', 'vertical'),
|
||||
('headingStyle', 'default');
|
||||
`);
|
||||
console.log("[Worker] Test table created and populated");
|
||||
} catch (error) {
|
||||
sqliteInitError = String(error);
|
||||
console.error("[Worker] SQLite initialization failed:", error);
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
return sqliteInitPromise;
|
||||
}
|
||||
|
||||
// Example: your /bootstrap handler placeholder
|
||||
async function handleBootstrap() {
|
||||
console.log("[Worker] Bootstrap request received");
|
||||
|
||||
// Try to initialize SQLite with timeout
|
||||
let dbInfo: any = { dbStatus: 'not initialized' };
|
||||
let dbInfo: Record<string, unknown> = { dbStatus: 'not initialized' };
|
||||
|
||||
if (sqliteInitError) {
|
||||
dbInfo = { dbStatus: 'failed', error: sqliteInitError };
|
||||
if (sqlInitError) {
|
||||
dbInfo = { dbStatus: 'failed', error: sqlInitError };
|
||||
} else {
|
||||
try {
|
||||
// Don't wait too long for SQLite initialization
|
||||
@ -169,17 +168,16 @@ async function handleBootstrap() {
|
||||
]);
|
||||
|
||||
// Query the database if initialized
|
||||
if (db) {
|
||||
const stmt = db.prepare('SELECT * FROM options');
|
||||
if (sqlProvider.isOpen()) {
|
||||
const stmt = sqlProvider.prepare('SELECT * FROM options');
|
||||
const rows = stmt.all() as Array<{ name: string; value: string }>;
|
||||
const options: Record<string, string> = {};
|
||||
while (stmt.step()) {
|
||||
const row = stmt.get({});
|
||||
for (const row of rows) {
|
||||
options[row.name] = row.value;
|
||||
}
|
||||
stmt.finalize();
|
||||
|
||||
dbInfo = {
|
||||
sqliteVersion: sqlite3.version.libVersion,
|
||||
sqliteVersion: sqlProvider.version?.libVersion,
|
||||
optionsFromDB: options,
|
||||
dbStatus: 'connected'
|
||||
};
|
||||
@ -212,8 +210,13 @@ async function handleBootstrap() {
|
||||
});
|
||||
}
|
||||
|
||||
interface LocalRequest {
|
||||
method: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
// Main dispatch
|
||||
async function dispatch(request) {
|
||||
async function dispatch(request: LocalRequest) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
console.log("[Worker] Dispatch:", url.pathname);
|
||||
@ -271,18 +274,18 @@ self.onmessage = async (event) => {
|
||||
const response = await dispatch(request);
|
||||
console.log("[Worker] Dispatch completed, sending response:", id);
|
||||
|
||||
// Transfer body back (if any)
|
||||
self.postMessage({
|
||||
// Transfer body back (if any) - use options object for proper typing
|
||||
(self as unknown as Worker).postMessage({
|
||||
type: "LOCAL_RESPONSE",
|
||||
id,
|
||||
response
|
||||
}, response.body ? [response.body] : []);
|
||||
}, { transfer: response.body ? [response.body] : [] });
|
||||
} catch (e) {
|
||||
console.error("[Worker] Dispatch error:", e);
|
||||
self.postMessage({
|
||||
(self as unknown as Worker).postMessage({
|
||||
type: "LOCAL_RESPONSE",
|
||||
id,
|
||||
error: String(e?.message || e)
|
||||
error: String((e as Error)?.message || e)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user