diff --git a/examples/react-electron/vite.config.ts b/examples/react-electron/vite.config.ts index 5a8dde8a1..36d480bbd 100644 --- a/examples/react-electron/vite.config.ts +++ b/examples/react-electron/vite.config.ts @@ -24,13 +24,13 @@ export default defineConfig({ path.resolve(__dirname, "../../packages/web/dist"), path.resolve( __dirname, - "../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm", + "../../node_modules/@evolu/sqlite-wasm/sqlite-wasm/jswasm", ), ], }, }, optimizeDeps: { - exclude: ["@sqlite.org/sqlite-wasm", "kysely", "@evolu/react-web"], + exclude: ["@evolu/sqlite-wasm", "kysely", "@evolu/react-web"], }, assetsInclude: ["**/*.wasm"], }); diff --git a/examples/react-expo/app.json b/examples/react-expo/app.json index f280e0bb1..3d2b5ca0f 100644 --- a/examples/react-expo/app.json +++ b/examples/react-expo/app.json @@ -35,7 +35,11 @@ "backgroundColor": "#ffffff" } ], - "expo-sqlite" + [ + "expo-sqlite", { + "useSqlCipher": true + } + ] ], "experiments": { "typedRoutes": true, diff --git a/examples/react-expo/metro.config.js b/examples/react-expo/metro.config.js index 6853b1530..fc7882b45 100644 --- a/examples/react-expo/metro.config.js +++ b/examples/react-expo/metro.config.js @@ -4,4 +4,16 @@ const { getDefaultConfig } = require("expo/metro-config"); /** @type {import("expo/metro-config").MetroConfig} */ const config = getDefaultConfig(__dirname); +// Add wasm asset support +config.resolver.assetExts.push('wasm'); + +// Add COEP and COOP headers to support SharedArrayBuffer +config.server.enhanceMiddleware = (middleware) => { + return (req, res, next) => { + res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless'); + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + middleware(req, res, next); + }; +}; + module.exports = config; diff --git a/examples/react-vite-pwa/src/components/EvoluMinimalExample.tsx b/examples/react-vite-pwa/src/components/EvoluMinimalExample.tsx index cd5100453..08bef3c6f 100644 --- a/examples/react-vite-pwa/src/components/EvoluMinimalExample.tsx +++ b/examples/react-vite-pwa/src/components/EvoluMinimalExample.tsx @@ -39,9 +39,9 @@ const evolu = Evolu.createEvolu(evoluReactWebDeps)(Schema, { name: Evolu.SimpleName.orThrow( `${service}-${authResult?.owner?.id ?? "guest"}`, ), - externalAppOwner: authResult?.owner, reloadUrl: "/", - + encryptionKey: authResult?.owner?.encryptionKey, + externalAppOwner: authResult?.owner, ...(process.env.NODE_ENV === "development" && { transports: [{ type: "WebSocket", url: "ws://localhost:4000" }], }), diff --git a/examples/react-vite-pwa/vite.config.ts b/examples/react-vite-pwa/vite.config.ts index b10bf8021..68aed230c 100644 --- a/examples/react-vite-pwa/vite.config.ts +++ b/examples/react-vite-pwa/vite.config.ts @@ -7,7 +7,7 @@ import { VitePWA } from "vite-plugin-pwa"; export default defineConfig({ cacheDir: ".vite", optimizeDeps: { - exclude: ["@sqlite.org/sqlite-wasm", "kysely", "@evolu/react-web"], + exclude: ["@evolu/sqlite-wasm", "kysely", "@evolu/react-web"], }, plugins: [ tailwindcss(), @@ -41,5 +41,15 @@ export default defineConfig({ type: "module", }, }), + { + name: "configure-response-headers", + configureServer: (server) => { + server.middlewares.use((_req, res, next) => { + res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + next(); + }); + }, + }, ], }); diff --git a/examples/svelte-vite-pwa/vite.config.ts b/examples/svelte-vite-pwa/vite.config.ts index 67f90ebd5..507f90322 100644 --- a/examples/svelte-vite-pwa/vite.config.ts +++ b/examples/svelte-vite-pwa/vite.config.ts @@ -17,6 +17,6 @@ export default defineConfig({ ], optimizeDeps: { // A workaround for Vite bug: https://github.com/vitejs/vite/issues/13314#issuecomment-1560745780 - exclude: ["@sqlite.org/sqlite-wasm", "kysely", "@evolu/react-web"], + exclude: ["@evolu/sqlite-wasm", "kysely", "@evolu/react-web"], }, }); diff --git a/packages/common/src/Evolu/Db.ts b/packages/common/src/Evolu/Db.ts index cd0bf84b1..176776e0f 100644 --- a/packages/common/src/Evolu/Db.ts +++ b/packages/common/src/Evolu/Db.ts @@ -7,6 +7,7 @@ import { import { ConsoleConfig, ConsoleDep } from "../Console.js"; import { createSymmetricCrypto, + EncryptionKey, RandomBytesDep, SymmetricCryptoDecryptError, } from "../Crypto.js"; @@ -174,6 +175,7 @@ export interface DbConfig extends ConsoleConfig, TimestampConfig { * The default value is: `false`. */ readonly inMemory?: boolean; + readonly encryptionKey?: EncryptionKey; } export const defaultDbConfig: DbConfig = { @@ -339,6 +341,7 @@ const createDbWorkerDeps = initMessage.config.name, { memory: initMessage.config.inMemory ?? false, + encryptionKey: initMessage.config.encryptionKey ?? undefined, }, ); if (!sqliteResult.ok) { diff --git a/packages/common/src/Sqlite.ts b/packages/common/src/Sqlite.ts index c5121fbd6..c6cf3929f 100644 --- a/packages/common/src/Sqlite.ts +++ b/packages/common/src/Sqlite.ts @@ -1,5 +1,6 @@ import { Brand } from "./Brand.js"; import { ConsoleDep } from "./Console.js"; +import { EncryptionKey } from "./Crypto.js"; import { createTransferableError, TransferableError } from "./Error.js"; import { err, ok, Result, tryAsync, trySync } from "./Result.js"; import { Null, Number, SimpleName, String, Uint8Array, union } from "./Type.js"; @@ -25,6 +26,7 @@ export interface CreateSqliteDriverDep { export interface SqliteDriverOptions { memory?: boolean; + encryptionKey?: EncryptionKey | undefined; } /** diff --git a/packages/react-native/src/providers/ExpoSqliteDriver.ts b/packages/react-native/src/providers/ExpoSqliteDriver.ts index ae1e4c67b..133c3f06b 100644 --- a/packages/react-native/src/providers/ExpoSqliteDriver.ts +++ b/packages/react-native/src/providers/ExpoSqliteDriver.ts @@ -3,6 +3,7 @@ import { CreateSqliteDriver, SqliteDriver, SqliteRow, + bytesToHex, } from "@evolu/common"; import { openDatabaseSync, SQLiteStatement } from "expo-sqlite"; @@ -11,6 +12,13 @@ export const createExpoSqliteDriver: CreateSqliteDriver = (name, options) => { const db = openDatabaseSync( options?.memory ? ":memory:" : `evolu1-${name}.db`, ); + if (options?.encryptionKey) { + db.execSync(` + PRAGMA cipher = 'sqlcipher'; + PRAGMA legacy = 4; + PRAGMA key = "x'${bytesToHex(options.encryptionKey)}'"; + `); + } let isDisposed = false; const cache = createPreparedStatementsCache( diff --git a/packages/react-native/src/providers/OpSqliteDriver.ts b/packages/react-native/src/providers/OpSqliteDriver.ts index bd800bc86..b30fd2416 100644 --- a/packages/react-native/src/providers/OpSqliteDriver.ts +++ b/packages/react-native/src/providers/OpSqliteDriver.ts @@ -5,6 +5,7 @@ import { CreateSqliteDriver, SqliteDriver, SqliteRow, + bytesToHex, } from "@evolu/common"; import { open, PreparedStatement } from "@op-engineering/op-sqlite"; @@ -14,7 +15,12 @@ export const createOpSqliteDriver: CreateSqliteDriver = (name, options) => { const db = open( options?.memory ? { name: `inMemoryDb`, location: ":memory:" } - : { name: `evolu1-${name}.db` }, + : { + name: `evolu1-${name}.db`, + ...(options?.encryptionKey && { + encryptionKey: `x'${bytesToHex(options.encryptionKey)}'`, + }), + } ); let isDisposed = false; diff --git a/packages/web/package.json b/packages/web/package.json index 036beb240..6c5954efc 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -36,8 +36,8 @@ "format": "prettier --write \"src/*.{ts,tsx,md}\"" }, "dependencies": { - "@sqlite.org/sqlite-wasm": "3.50.4-build1", - "idb-keyval": "^6.2.2" + "idb-keyval": "^6.2.2", + "@evolu/sqlite-wasm": "2.2.4" }, "devDependencies": { "@evolu/common": "workspace:*", diff --git a/packages/web/src/WasmSqliteDriver.ts b/packages/web/src/WasmSqliteDriver.ts index 0dfea26f2..dfc0946df 100644 --- a/packages/web/src/WasmSqliteDriver.ts +++ b/packages/web/src/WasmSqliteDriver.ts @@ -4,8 +4,12 @@ import { CreateSqliteDriver, SqliteDriver, SqliteRow, + bytesToHex, } from "@evolu/common"; -import sqlite3InitModule, { PreparedStatement } from "@sqlite.org/sqlite-wasm"; +import sqlite3InitModule, { + PreparedStatement, + Database, +} from "@evolu/sqlite-wasm"; // TODO: Do we still need that? // https://github.com/sqlite/sqlite-wasm/issues/62 @@ -22,12 +26,29 @@ export const createWasmSqliteDriver: CreateSqliteDriver = async ( options, ) => { const sqlite3 = await sqlite3Promise; + // This is used to make OPFS default vfs for multipleciphers + // @ts-expect-error Missing types (update @evolu/sqlite-wasm types) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + sqlite3.capi.sqlite3mc_vfs_create("opfs", 1); + + let db: Database; + if (options?.memory) { + db = new sqlite3.oo1.DB(":memory:"); + } else if (options?.encryptionKey) { + const pool = await sqlite3.installOpfsSAHPoolVfs({ directory: `.${name}` }); + db = new pool.OpfsSAHPoolDb( + "file:evolu1.db?vfs=multipleciphers-opfs-sahpool", + ); + db.exec(` + PRAGMA cipher = 'sqlcipher'; + PRAGMA legacy = 4; + PRAGMA key = "x'${bytesToHex(options.encryptionKey)}'"; + `); + } else { + const pool = await sqlite3.installOpfsSAHPoolVfs({ name }); + db = new pool.OpfsSAHPoolDb("file:evolu1.db"); + } - const db = options?.memory - ? new sqlite3.oo1.DB(":memory:") - : new (await sqlite3.installOpfsSAHPoolVfs({ name })).OpfsSAHPoolDb( - "/evolu1.db", - ); let isDisposed = false; const cache = createPreparedStatementsCache( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 366e0f434..c0eb7a720 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -842,9 +842,9 @@ importers: packages/web: dependencies: - '@sqlite.org/sqlite-wasm': - specifier: 3.50.4-build1 - version: 3.50.4-build1 + '@evolu/sqlite-wasm': + specifier: 2.2.4 + version: 2.2.4 idb-keyval: specifier: ^6.2.2 version: 6.2.2 @@ -2265,8 +2265,12 @@ packages: resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@expo/cli@54.0.14': - resolution: {integrity: sha512-M7QW/GHx1FJg+CGgChGKerYXmCGWDskJ8S6w+8m49IBZ41CMDeWRH5snQkFoGCttF8WnzhGiX+nu69AFnEuDHQ==} + '@evolu/sqlite-wasm@2.2.4': + resolution: {integrity: sha512-/JOYGFN93QspD2C8HVxVgBUlFWqJ1IpaVuIhEB53u4+ZvE+D3LjpNHDYiwZgf0n7VaH2U85OY5eV3wUrWc3scg==} + hasBin: true + + '@expo/cli@54.0.13': + resolution: {integrity: sha512-wUJVTByZzDN0q8UjXDlu6WD2BWoTJCKVVBGUBNmvViDX4FhnESwefmtXPoO54QUUKs6vY89WZryHllGArGfLLw==} hasBin: true peerDependencies: expo: '*' @@ -3986,10 +3990,6 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@sqlite.org/sqlite-wasm@3.50.4-build1': - resolution: {integrity: sha512-Qig2Wso7gPkU1PtXwFzndh+CTRzrIFxVGqv6eCetjU7YqxlHItj+GvQYwYTppCRgAPawtRN/4AJcEgB9xDHGug==} - hasBin: true - '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -11464,7 +11464,9 @@ snapshots: '@eslint/core': 0.16.0 levn: 0.4.1 - '@expo/cli@54.0.14(expo-router@6.0.14)(expo@54.0.21)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))': + '@evolu/sqlite-wasm@2.2.4': {} + + '@expo/cli@54.0.13(expo-router@6.0.13)(expo@54.0.20)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))': dependencies: '@0no-co/graphql.web': 1.2.0 '@expo/code-signing-certificates': 0.0.5 @@ -13245,8 +13247,6 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@sqlite.org/sqlite-wasm@3.50.4-build1': {} - '@standard-schema/spec@1.0.0': {} '@surma/rollup-plugin-off-main-thread@2.2.3':