@evolu/web 1.0.1-preview.5 → 1.0.1-preview.7

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.
@@ -1,15 +1,13 @@
1
- import { createConsole, createEnglishMnemonic, createNanoIdLib, createRandom, createRandomBytes, createTime, } from "@evolu/common";
2
- import { createDbWorkerForPlatform, createWebSocketSync, } from "@evolu/common/evolu";
1
+ import { createConsole, createRandom, createRandomBytes, createTime, createWebSocket, } from "@evolu/common";
2
+ import { createDbWorkerForPlatform } from "@evolu/common/evolu";
3
3
  import { createWasmSqliteDriver } from "../WasmSqliteDriver.js";
4
4
  import { wrapWebWorkerSelf } from "../WebWorker.js";
5
5
  const dbWorker = createDbWorkerForPlatform({
6
- createSqliteDriver: createWasmSqliteDriver,
7
- createSync: createWebSocketSync,
8
6
  console: createConsole(),
9
- time: createTime(),
7
+ createSqliteDriver: createWasmSqliteDriver,
8
+ createWebSocket,
10
9
  random: createRandom(),
11
- nanoIdLib: createNanoIdLib(),
12
- createMnemonic: createEnglishMnemonic,
13
- createRandomBytes,
10
+ randomBytes: createRandomBytes(),
11
+ time: createTime(),
14
12
  });
15
13
  wrapWebWorkerSelf(dbWorker);
@@ -0,0 +1,4 @@
1
+ import type { SecureStorage, RandomBytesDep, SymmetricCryptoDep } from "@evolu/common";
2
+ /** @experimental */
3
+ export declare const createWebAuthnStore: (deps: RandomBytesDep & SymmetricCryptoDep) => SecureStorage;
4
+ //# sourceMappingURL=LocalAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalAuth.d.ts","sourceRoot":"","sources":["../../../src/Evolu/LocalAuth.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAIV,aAAa,EACb,cAAc,EACd,kBAAkB,EACnB,MAAM,eAAe,CAAC;AAGvB,oBAAoB;AACpB,eAAO,MAAM,mBAAmB,GAC9B,MAAM,cAAc,GAAG,kBAAkB,KACxC,aA4GD,CAAC"}
@@ -0,0 +1,193 @@
1
+ import { set, get, del, keys, clear, createStore } from "idb-keyval";
2
+ import { createSlip21, utf8ToBytes, bytesToUtf8, base64UrlToUint8Array, uint8ArrayToBase64Url, EncryptionKey, Base64Url, } from "@evolu/common";
3
+ /** @experimental */
4
+ export const createWebAuthnStore = (deps) => ({
5
+ setItem: async (key, value, options) => {
6
+ if (options?.accessControl === "none") {
7
+ const metadata = createMetadata(false);
8
+ await set(key, { value, metadata }, getStore(options.service));
9
+ return { metadata };
10
+ }
11
+ const seed = generateSeed(deps)();
12
+ const authResult = JSON.parse(value);
13
+ const credential = await createCredential(deps)(options?.webAuthnUsername ?? "Evolu User", seed, options?.relyingPartyID, options?.relyingPartyName, options?.webAuthnUserVerification, options?.webAuthnAuthenticatorAttachment);
14
+ const encryptionKey = deriveEncryptionKey(seed);
15
+ const encryptedData = encryptAuthResult(deps)(authResult, encryptionKey);
16
+ const credentialId = uint8ArrayToBase64Url(new Uint8Array(credential.rawId));
17
+ const metadata = createMetadata();
18
+ await set(key, { credentialId, ...encryptedData, metadata }, getStore(options?.service));
19
+ return { metadata };
20
+ },
21
+ getItem: async (key, options) => {
22
+ if (options?.accessControl === "none") {
23
+ const data = await get(key, getStore(options.service));
24
+ return data
25
+ ? {
26
+ key,
27
+ value: data.value,
28
+ service: options.service ?? "default",
29
+ metadata: data.metadata,
30
+ }
31
+ : null;
32
+ }
33
+ const data = await get(key, getStore(options?.service));
34
+ if (!data) {
35
+ return null;
36
+ }
37
+ try {
38
+ const credential = await getCredential(deps)(data.credentialId, options?.relyingPartyID, options?.webAuthnUserVerification);
39
+ const credentialSeed = extractSeedFromCredential(credential);
40
+ const encryptionKey = deriveEncryptionKey(credentialSeed);
41
+ const authResultVal = decryptAuthResult(deps)(data, encryptionKey);
42
+ if (!authResultVal) {
43
+ return null;
44
+ }
45
+ return {
46
+ key,
47
+ service: options?.service ?? "default",
48
+ value: authResultVal,
49
+ metadata: data.metadata,
50
+ };
51
+ }
52
+ catch (_error) {
53
+ return null;
54
+ }
55
+ },
56
+ deleteItem: async (key, options) => {
57
+ await del(key, getStore(options?.service));
58
+ return true;
59
+ },
60
+ getAllItems: async (options) => {
61
+ const service = options?.service ?? "default";
62
+ const itemKeys = await keys(getStore(service));
63
+ const items = await Promise.all(itemKeys.map(async (key) => {
64
+ const data = await get(key, getStore(service));
65
+ return {
66
+ key,
67
+ service,
68
+ metadata: data?.metadata ?? createMetadata(),
69
+ ...(options?.includeValues && data?.value
70
+ ? { value: data.value }
71
+ : {}),
72
+ };
73
+ }));
74
+ return items;
75
+ },
76
+ clearService: async (options) => {
77
+ await clear(getStore(options?.service));
78
+ },
79
+ });
80
+ /**
81
+ * Create default metadata for backwards compatibility with items that don't
82
+ * have stored metadata.
83
+ */
84
+ const createMetadata = (isSecure = true) => {
85
+ return {
86
+ backend: "keychain",
87
+ accessControl: isSecure ? "biometryCurrentSet" : "none",
88
+ securityLevel: isSecure ? "biometry" : "software",
89
+ timestamp: Date.now(),
90
+ };
91
+ };
92
+ /** Get storage key for owner ID. (supports namespaces via prefix) */
93
+ const getStore = (prefix = "default") => {
94
+ return createStore(prefix, "evolu-auth");
95
+ };
96
+ const createCredential = (deps) => async (username, seed, relyingPartyID, relyingPartyName, userVerification, authenticatorAttachment) => {
97
+ const options = createCredentialCreationOptions(deps)(username, seed, relyingPartyID, relyingPartyName, userVerification, authenticatorAttachment);
98
+ const credential = (await navigator.credentials.create(options));
99
+ if (!credential) {
100
+ throw new Error("Failed to create WebAuthn credential");
101
+ }
102
+ return credential;
103
+ };
104
+ const getCredential = (deps) => async (credentialId, relyingPartyID, userVerification) => {
105
+ const options = createCredentialRequestOptions(deps)(credentialId, relyingPartyID, userVerification);
106
+ const credential = (await navigator.credentials.get(options));
107
+ if (!credential?.response) {
108
+ throw new Error("Failed to get WebAuthn credential");
109
+ }
110
+ return credential;
111
+ };
112
+ const extractSeedFromCredential = (credential) => {
113
+ const response = credential.response;
114
+ if (!response.userHandle) {
115
+ throw new Error("No userHandle in credential response");
116
+ }
117
+ return new Uint8Array(response.userHandle);
118
+ };
119
+ const createCredentialCreationOptions = (deps) => (username, seed, relyingPartyID, relyingPartyName, userVerification, authenticatorAttachment) => {
120
+ return {
121
+ publicKey: {
122
+ challenge: generateSeed(deps)(),
123
+ rp: {
124
+ id: relyingPartyID ?? document.location.hostname,
125
+ name: relyingPartyName ?? "Evolu",
126
+ },
127
+ user: {
128
+ id: seed,
129
+ name: username,
130
+ displayName: username,
131
+ },
132
+ pubKeyCredParams: [
133
+ { type: "public-key", alg: -8 }, // Ed25519
134
+ { type: "public-key", alg: -7 }, // ES256
135
+ { type: "public-key", alg: -257 }, // RS256
136
+ ],
137
+ attestation: "none",
138
+ authenticatorSelection: {
139
+ // - "platform": Uses the platform's built-in authenticator.
140
+ // - "cross-platform": Uses a device specific authenticator (yubikey, fido2, etc.)
141
+ authenticatorAttachment: authenticatorAttachment ?? "platform",
142
+ // - "discouraged": Only User Presence is needed.
143
+ // - "preferred": User Verification is preferred but not required. Falls back to User Presence.
144
+ // - "required": User Verification MUST occur (biometrics/PIN). Clients may silently downgrade to User Presence only.
145
+ userVerification: userVerification ?? "required",
146
+ // - "discouraged": Server-side credential is preferable, but will accept client-side discoverable credential.
147
+ // - "preferred": Relying Party strongly prefers client-side discoverable credential but will accept server-side credential.
148
+ // - "required": Client-side discoverable credential MUST be created, error if it can't be created.
149
+ residentKey: "required",
150
+ // Included for backwards compatibility. Deprecated in favor of residentKey (true = "required")
151
+ requireResidentKey: true,
152
+ },
153
+ },
154
+ };
155
+ };
156
+ const createCredentialRequestOptions = (deps) => (credentialId, relyingPartyID, userVerification) => {
157
+ return {
158
+ publicKey: {
159
+ challenge: generateSeed(deps)(),
160
+ rpId: relyingPartyID ?? document.location.hostname,
161
+ userVerification: userVerification ?? "preferred",
162
+ allowCredentials: [
163
+ {
164
+ type: "public-key",
165
+ id: base64UrlToUint8Array(Base64Url.orThrow(credentialId)),
166
+ },
167
+ ],
168
+ },
169
+ };
170
+ };
171
+ const deriveEncryptionKey = (seed) => {
172
+ const seed32 = seed.length === 32 ? seed : seed.slice(0, 32);
173
+ return EncryptionKey.orThrow(createSlip21(seed32, ["evolu", "auth"]));
174
+ };
175
+ const encryptAuthResult = (deps) => (authResult, encryptionKey) => {
176
+ const plaintext = utf8ToBytes(JSON.stringify(authResult));
177
+ const { nonce, ciphertext } = deps.symmetricCrypto.encrypt(plaintext, encryptionKey);
178
+ return {
179
+ nonce: uint8ArrayToBase64Url(nonce),
180
+ ciphertext: uint8ArrayToBase64Url(ciphertext),
181
+ };
182
+ };
183
+ const decryptAuthResult = (deps) => (encryptedData, encryptionKey) => {
184
+ const nonce = base64UrlToUint8Array(encryptedData.nonce);
185
+ const ciphertext = base64UrlToUint8Array(encryptedData.ciphertext);
186
+ const result = deps.symmetricCrypto.decrypt(ciphertext, encryptionKey, nonce);
187
+ if (!result.ok)
188
+ return null;
189
+ return bytesToUtf8(result.value);
190
+ };
191
+ const generateSeed = (deps) => () => {
192
+ return deps.randomBytes.create(32);
193
+ };
@@ -0,0 +1,3 @@
1
+ import { ReloadApp } from "@evolu/common/evolu";
2
+ export declare const reloadApp: ReloadApp;
3
+ //# sourceMappingURL=Platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Platform.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhD,eAAO,MAAM,SAAS,EAAE,SAMvB,CAAC"}
@@ -0,0 +1,6 @@
1
+ export const reloadApp = (url) => {
2
+ if (typeof document === "undefined") {
3
+ return;
4
+ }
5
+ location.replace(url);
6
+ };
@@ -1,3 +1,4 @@
1
1
  import { EvoluDeps } from "@evolu/common/evolu";
2
+ export declare const localAuth: import("@evolu/common").LocalAuth;
2
3
  export declare const evoluWebDeps: EvoluDeps;
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/Evolu/index.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,SAAS,EACV,MAAM,qBAAqB,CAAC;AAa7B,eAAO,MAAM,YAAY,EAAE,SAM1B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/Evolu/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,SAAS,EACV,MAAM,qBAAqB,CAAC;AAiB7B,eAAO,MAAM,SAAS,mCAGpB,CAAC;AAEH,eAAO,MAAM,YAAY,EAAE,SAM1B,CAAC"}
@@ -1,13 +1,20 @@
1
- import { createConsole, createNanoIdLib, createTime } from "@evolu/common";
1
+ import { createConsole, createLocalAuth, createRandomBytes, createSymmetricCrypto, createTime, } from "@evolu/common";
2
2
  import { createSharedWebWorker } from "../SharedWebWorker.js";
3
- import { createAppState } from "./AppState.js";
3
+ import { createWebAuthnStore } from "./LocalAuth.js";
4
+ import { reloadApp } from "./Platform.js";
5
+ const randomBytes = createRandomBytes();
6
+ const symmetricCrypto = createSymmetricCrypto({ randomBytes });
4
7
  const createDbWorker = (name) => createSharedWebWorker(name, () => new Worker(new URL("Db.worker.js", import.meta.url), {
5
8
  type: "module",
6
9
  }));
10
+ export const localAuth = createLocalAuth({
11
+ randomBytes,
12
+ secureStorage: createWebAuthnStore({ randomBytes, symmetricCrypto }),
13
+ });
7
14
  export const evoluWebDeps = {
8
- time: createTime(),
9
15
  console: createConsole(),
10
- nanoIdLib: createNanoIdLib(),
11
- createAppState,
12
16
  createDbWorker,
17
+ randomBytes: createRandomBytes(),
18
+ reloadApp,
19
+ time: createTime(),
13
20
  };
@@ -1 +1 @@
1
- {"version":3,"file":"WasmSqliteDriver.d.ts","sourceRoot":"","sources":["../../src/WasmSqliteDriver.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,kBAAkB,EAGnB,MAAM,eAAe,CAAC;AAavB,eAAO,MAAM,sBAAsB,EAAE,kBAsEpC,CAAC"}
1
+ {"version":3,"file":"WasmSqliteDriver.d.ts","sourceRoot":"","sources":["../../src/WasmSqliteDriver.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,kBAAkB,EAInB,MAAM,eAAe,CAAC;AAgBvB,eAAO,MAAM,sBAAsB,EAAE,kBAuFpC,CAAC"}
@@ -1,5 +1,5 @@
1
- import { constVoid, createPreparedStatementsCache, } from "@evolu/common";
2
- import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
1
+ import { constVoid, createPreparedStatementsCache, bytesToHex, } from "@evolu/common";
2
+ import sqlite3InitModule from "@evolu/sqlite-wasm";
3
3
  // TODO: Do we still need that?
4
4
  // https://github.com/sqlite/sqlite-wasm/issues/62
5
5
  // @ts-expect-error Missing types.
@@ -10,9 +10,27 @@ globalThis.sqlite3ApiConfig = {
10
10
  const sqlite3Promise = sqlite3InitModule();
11
11
  export const createWasmSqliteDriver = async (name, options) => {
12
12
  const sqlite3 = await sqlite3Promise;
13
- const db = options?.memory
14
- ? new sqlite3.oo1.DB(":memory:")
15
- : new (await sqlite3.installOpfsSAHPoolVfs({ name })).OpfsSAHPoolDb("/evolu1.db");
13
+ // This is used to make OPFS default vfs for multipleciphers
14
+ // @ts-expect-error Missing types (update @evolu/sqlite-wasm types)
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
16
+ sqlite3.capi.sqlite3mc_vfs_create("opfs", 1);
17
+ let db;
18
+ if (options?.memory) {
19
+ db = new sqlite3.oo1.DB(":memory:");
20
+ }
21
+ else if (options?.encryptionKey) {
22
+ const pool = await sqlite3.installOpfsSAHPoolVfs({ directory: `.${name}` });
23
+ db = new pool.OpfsSAHPoolDb("file:evolu1.db?vfs=multipleciphers-opfs-sahpool");
24
+ db.exec(`
25
+ PRAGMA cipher = 'sqlcipher';
26
+ PRAGMA legacy = 4;
27
+ PRAGMA key = "x'${bytesToHex(options.encryptionKey)}'";
28
+ `);
29
+ }
30
+ else {
31
+ const pool = await sqlite3.installOpfsSAHPoolVfs({ name });
32
+ db = new pool.OpfsSAHPoolDb("file:evolu1.db");
33
+ }
16
34
  let isDisposed = false;
17
35
  const cache = createPreparedStatementsCache((sql) => db.prepare(sql), (statement) => {
18
36
  statement.finalize();
@@ -4,7 +4,7 @@
4
4
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
5
5
  import { expect, test, vi, beforeEach, afterEach } from "vitest";
6
6
  import { createSharedWebWorker } from "../src/SharedWebWorker.js";
7
- import { getOrThrow, SimpleName, wait } from "@evolu/common";
7
+ import { SimpleName, wait } from "@evolu/common";
8
8
  // Mock BroadcastChannel
9
9
  class MockBroadcastChannel {
10
10
  name;
@@ -27,12 +27,12 @@ class MockWorker {
27
27
  }
28
28
  beforeEach(() => {
29
29
  // Create a spy for BroadcastChannel constructor
30
- global.BroadcastChannel = vi
31
- .fn()
32
- .mockImplementation((name) => new MockBroadcastChannel(name));
33
- global.document = {}; // Simulate browser environment
34
- // Mock navigator.locks properly - remove the global.navigator assignment
35
- Object.defineProperty(global.navigator, "locks", {
30
+ globalThis.BroadcastChannel = vi.fn().mockImplementation(function (name) {
31
+ return new MockBroadcastChannel(name);
32
+ });
33
+ globalThis.document = {}; // Simulate browser environment
34
+ // Mock navigator.locks properly - remove the globalThis.navigator assignment
35
+ Object.defineProperty(globalThis.navigator, "locks", {
36
36
  value: mockLocks,
37
37
  writable: true,
38
38
  configurable: true,
@@ -44,11 +44,11 @@ afterEach(() => {
44
44
  });
45
45
  test("createSharedWebWorker creates BroadcastChannel and requests lock", () => {
46
46
  const mockCreateWorker = vi.fn(() => new MockWorker());
47
- const sharedWorker = createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
47
+ const sharedWorker = createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
48
48
  // Should create BroadcastChannel with namespaced name
49
- expect(global.BroadcastChannel).toHaveBeenCalledWith("evolu-sharedwebworker-test-worker");
49
+ expect(globalThis.BroadcastChannel).toHaveBeenCalledWith("evolu-sharedwebworker-test-worker");
50
50
  // Should request owner-ready immediately
51
- const channelInstance = vi.mocked(global.BroadcastChannel).mock.results[0]
51
+ const channelInstance = vi.mocked(globalThis.BroadcastChannel).mock.results[0]
52
52
  .value;
53
53
  expect(channelInstance.postMessage).toHaveBeenCalledWith({
54
54
  type: "request-owner-ready",
@@ -63,22 +63,22 @@ test("createSharedWebWorker creates BroadcastChannel and requests lock", () => {
63
63
  });
64
64
  test("createSharedWebWorker returns no-op on server", () => {
65
65
  // Simulate server environment
66
- delete global.document;
66
+ delete globalThis.document;
67
67
  const mockCreateWorker = vi.fn();
68
- const sharedWorker = createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
68
+ const sharedWorker = createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
69
69
  // Should not create BroadcastChannel or request locks
70
- expect(global.BroadcastChannel).not.toHaveBeenCalled();
70
+ expect(globalThis.BroadcastChannel).not.toHaveBeenCalled();
71
71
  expect(mockLocks.request).not.toHaveBeenCalled();
72
72
  // Should return no-op worker
73
73
  expect(sharedWorker.postMessage).toBeDefined();
74
74
  expect(sharedWorker.onMessage).toBeDefined();
75
75
  // Restore document for other tests
76
- global.document = {};
76
+ globalThis.document = {};
77
77
  });
78
78
  test("createSharedWebWorker queues messages when owner not ready", () => {
79
79
  const mockCreateWorker = vi.fn(() => new MockWorker());
80
- const sharedWorker = createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
81
- const channelInstance = vi.mocked(global.BroadcastChannel).mock.results[0]
80
+ const sharedWorker = createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
81
+ const channelInstance = vi.mocked(globalThis.BroadcastChannel).mock.results[0]
82
82
  .value;
83
83
  // Send message before owner is ready
84
84
  sharedWorker.postMessage({ type: "test-message" });
@@ -97,8 +97,8 @@ test("createSharedWebWorker queues messages when owner not ready", () => {
97
97
  });
98
98
  test("createSharedWebWorker forwards messages when owner ready", () => {
99
99
  const mockCreateWorker = vi.fn(() => new MockWorker());
100
- const sharedWorker = createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
101
- const channelInstance = vi.mocked(global.BroadcastChannel).mock.results[0]
100
+ const sharedWorker = createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
101
+ const channelInstance = vi.mocked(globalThis.BroadcastChannel).mock.results[0]
102
102
  .value;
103
103
  // Simulate owner-ready message
104
104
  const ownerReadyEvent = new MessageEvent("message", {
@@ -116,9 +116,9 @@ test("createSharedWebWorker forwards messages when owner ready", () => {
116
116
  test("createSharedWebWorker handles onMessage callback", () => {
117
117
  const mockCreateWorker = vi.fn(() => new MockWorker());
118
118
  const onMessageCallback = vi.fn();
119
- const sharedWorker = createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
119
+ const sharedWorker = createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
120
120
  sharedWorker.onMessage(onMessageCallback);
121
- const channelInstance = vi.mocked(global.BroadcastChannel).mock.results[0]
121
+ const channelInstance = vi.mocked(globalThis.BroadcastChannel).mock.results[0]
122
122
  .value;
123
123
  // Simulate message from worker
124
124
  const workerMessage = new MessageEvent("message", {
@@ -143,13 +143,15 @@ test("createSharedWebWorker handles multiple tabs - first tab becomes owner", as
143
143
  });
144
144
  const mockCreateWorker = vi.fn(() => new MockWorker());
145
145
  // Create first tab (will become owner)
146
- createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
146
+ createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
147
147
  // Create second tab
148
- createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
148
+ createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
149
149
  // Both tabs should create BroadcastChannels
150
- expect(global.BroadcastChannel).toHaveBeenCalledTimes(2);
151
- const tab1Channel = vi.mocked(global.BroadcastChannel).mock.results[0].value;
152
- const tab2Channel = vi.mocked(global.BroadcastChannel).mock.results[1].value;
150
+ expect(globalThis.BroadcastChannel).toHaveBeenCalledTimes(2);
151
+ const tab1Channel = vi.mocked(globalThis.BroadcastChannel).mock.results[0]
152
+ .value;
153
+ const tab2Channel = vi.mocked(globalThis.BroadcastChannel).mock.results[1]
154
+ .value;
153
155
  // Both tabs should request owner-ready
154
156
  expect(tab1Channel.postMessage).toHaveBeenCalledWith({
155
157
  type: "request-owner-ready",
@@ -158,7 +160,7 @@ test("createSharedWebWorker handles multiple tabs - first tab becomes owner", as
158
160
  type: "request-owner-ready",
159
161
  });
160
162
  // Wait for lock acquisition
161
- await wait(10);
163
+ await wait("10ms")();
162
164
  // Only first tab should create worker (it became owner)
163
165
  expect(mockCreateWorker).toHaveBeenCalledTimes(1);
164
166
  // First tab should announce ownership
@@ -167,10 +169,12 @@ test("createSharedWebWorker handles multiple tabs - first tab becomes owner", as
167
169
  test("createSharedWebWorker handles cross-tab message forwarding", () => {
168
170
  const mockCreateWorker = vi.fn(() => new MockWorker());
169
171
  // Create two tabs
170
- const tab1 = createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
171
- const tab2 = createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
172
- const tab1Channel = vi.mocked(global.BroadcastChannel).mock.results[0].value;
173
- const tab2Channel = vi.mocked(global.BroadcastChannel).mock.results[1].value;
172
+ const tab1 = createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
173
+ const tab2 = createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
174
+ const tab1Channel = vi.mocked(globalThis.BroadcastChannel).mock.results[0]
175
+ .value;
176
+ const tab2Channel = vi.mocked(globalThis.BroadcastChannel).mock.results[1]
177
+ .value;
174
178
  // Simulate tab1 receiving owner-ready (tab1 becomes aware of owner)
175
179
  const ownerReadyEvent = new MessageEvent("message", {
176
180
  data: { type: "owner-ready" },
@@ -196,13 +200,15 @@ test("createSharedWebWorker handles worker responses across tabs", () => {
196
200
  const tab1Callback = vi.fn();
197
201
  const tab2Callback = vi.fn();
198
202
  // Create two tabs
199
- const tab1 = createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
200
- const tab2 = createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
203
+ const tab1 = createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
204
+ const tab2 = createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
201
205
  // Set up message callbacks
202
206
  tab1.onMessage(tab1Callback);
203
207
  tab2.onMessage(tab2Callback);
204
- const tab1Channel = vi.mocked(global.BroadcastChannel).mock.results[0].value;
205
- const tab2Channel = vi.mocked(global.BroadcastChannel).mock.results[1].value;
208
+ const tab1Channel = vi.mocked(globalThis.BroadcastChannel).mock.results[0]
209
+ .value;
210
+ const tab2Channel = vi.mocked(globalThis.BroadcastChannel).mock.results[1]
211
+ .value;
206
212
  // Simulate worker response broadcast
207
213
  const workerResponse = new MessageEvent("message", {
208
214
  data: { type: "from-worker", message: { result: "shared-result" } },
@@ -218,7 +224,7 @@ test("createSharedWebWorker multi-tab scenario", () => {
218
224
  const mockCreateWorker = vi.fn(() => new MockWorker());
219
225
  const channelInstances = [];
220
226
  // Track all BroadcastChannel instances
221
- global.BroadcastChannel = vi.fn().mockImplementation((name) => {
227
+ globalThis.BroadcastChannel = vi.fn().mockImplementation(function (name) {
222
228
  const instance = new MockBroadcastChannel(name);
223
229
  channelInstances.push(instance);
224
230
  // Override postMessage to broadcast to all instances with same name
@@ -239,8 +245,8 @@ test("createSharedWebWorker multi-tab scenario", () => {
239
245
  return Promise.resolve();
240
246
  });
241
247
  // Create two tabs
242
- createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
243
- createSharedWebWorker(getOrThrow(SimpleName.from("test-worker")), mockCreateWorker);
248
+ createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
249
+ createSharedWebWorker(SimpleName.orThrow("test-worker"), mockCreateWorker);
244
250
  // Only first tab should create worker (owner)
245
251
  expect(mockCreateWorker).toHaveBeenCalledTimes(1);
246
252
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/web",
3
- "version": "1.0.1-preview.5",
3
+ "version": "1.0.1-preview.7",
4
4
  "description": "Evolu for web",
5
5
  "keywords": [
6
6
  "evolu",
@@ -28,18 +28,20 @@
28
28
  "README.md"
29
29
  ],
30
30
  "dependencies": {
31
- "@sqlite.org/sqlite-wasm": "3.50.4-build1"
31
+ "@evolu/sqlite-wasm": "2.2.4",
32
+ "idb-keyval": "^6.2.2"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/web-locks-api": "^0.0.5",
36
+ "shx": "^0.4.0",
35
37
  "typescript": "^5.9.2",
36
38
  "user-agent-data-types": "^0.4.2",
37
- "vitest": "^3.2.3",
38
- "@evolu/common": "6.0.1-preview.18",
39
+ "vitest": "^4.0.4",
40
+ "@evolu/common": "6.0.1-preview.23",
39
41
  "@evolu/tsconfig": "0.0.2"
40
42
  },
41
43
  "peerDependencies": {
42
- "@evolu/common": "^6.0.1-preview.18"
44
+ "@evolu/common": "^6.0.1-preview.23"
43
45
  },
44
46
  "publishConfig": {
45
47
  "access": "public"
@@ -1,27 +1,21 @@
1
1
  import {
2
2
  createConsole,
3
- createEnglishMnemonic,
4
- createNanoIdLib,
5
3
  createRandom,
6
4
  createRandomBytes,
7
5
  createTime,
6
+ createWebSocket,
8
7
  } from "@evolu/common";
9
- import {
10
- createDbWorkerForPlatform,
11
- createWebSocketSync,
12
- } from "@evolu/common/evolu";
8
+ import { createDbWorkerForPlatform } from "@evolu/common/evolu";
13
9
  import { createWasmSqliteDriver } from "../WasmSqliteDriver.js";
14
10
  import { wrapWebWorkerSelf } from "../WebWorker.js";
15
11
 
16
12
  const dbWorker = createDbWorkerForPlatform({
17
- createSqliteDriver: createWasmSqliteDriver,
18
- createSync: createWebSocketSync,
19
13
  console: createConsole(),
20
- time: createTime(),
14
+ createSqliteDriver: createWasmSqliteDriver,
15
+ createWebSocket,
21
16
  random: createRandom(),
22
- nanoIdLib: createNanoIdLib(),
23
- createMnemonic: createEnglishMnemonic,
24
- createRandomBytes,
17
+ randomBytes: createRandomBytes(),
18
+ time: createTime(),
25
19
  });
26
20
 
27
21
  wrapWebWorkerSelf(dbWorker);
@@ -0,0 +1,329 @@
1
+ import { set, get, del, keys, clear, createStore } from "idb-keyval";
2
+
3
+ import {
4
+ createSlip21,
5
+ utf8ToBytes,
6
+ bytesToUtf8,
7
+ base64UrlToUint8Array,
8
+ uint8ArrayToBase64Url,
9
+ EncryptionKey,
10
+ Base64Url,
11
+ } from "@evolu/common";
12
+
13
+ import type {
14
+ AuthResult,
15
+ Entropy32,
16
+ SensitiveInfoItem,
17
+ SecureStorage,
18
+ RandomBytesDep,
19
+ SymmetricCryptoDep,
20
+ } from "@evolu/common";
21
+ import type { UseStore } from "idb-keyval";
22
+
23
+ /** @experimental */
24
+ export const createWebAuthnStore = (
25
+ deps: RandomBytesDep & SymmetricCryptoDep,
26
+ ): SecureStorage => ({
27
+ setItem: async (key, value, options) => {
28
+ if (options?.accessControl === "none") {
29
+ const metadata = createMetadata(false);
30
+ await set(key, { value, metadata }, getStore(options.service));
31
+ return { metadata };
32
+ }
33
+ const seed = generateSeed(deps)();
34
+ const authResult = JSON.parse(value) as AuthResult;
35
+ const credential = await createCredential(deps)(
36
+ options?.webAuthnUsername ?? "Evolu User",
37
+ seed,
38
+ options?.relyingPartyID,
39
+ options?.relyingPartyName,
40
+ options?.webAuthnUserVerification,
41
+ options?.webAuthnAuthenticatorAttachment,
42
+ );
43
+ const encryptionKey = deriveEncryptionKey(seed);
44
+ const encryptedData = encryptAuthResult(deps)(authResult, encryptionKey);
45
+ const credentialId = uint8ArrayToBase64Url(
46
+ new Uint8Array(credential.rawId),
47
+ );
48
+ const metadata = createMetadata();
49
+ await set(
50
+ key,
51
+ { credentialId, ...encryptedData, metadata },
52
+ getStore(options?.service),
53
+ );
54
+ return { metadata };
55
+ },
56
+
57
+ getItem: async (key, options) => {
58
+ if (options?.accessControl === "none") {
59
+ const data = await get<{
60
+ readonly value: string;
61
+ readonly metadata: SensitiveInfoItem["metadata"];
62
+ }>(key, getStore(options.service));
63
+ return data
64
+ ? {
65
+ key,
66
+ value: data.value,
67
+ service: options.service ?? "default",
68
+ metadata: data.metadata,
69
+ }
70
+ : null;
71
+ }
72
+ const data = await get<{
73
+ readonly nonce: Base64Url;
74
+ readonly ciphertext: Base64Url;
75
+ readonly credentialId: string;
76
+ readonly metadata: SensitiveInfoItem["metadata"];
77
+ }>(key, getStore(options?.service));
78
+ if (!data) {
79
+ return null;
80
+ }
81
+ try {
82
+ const credential = await getCredential(deps)(
83
+ data.credentialId,
84
+ options?.relyingPartyID,
85
+ options?.webAuthnUserVerification,
86
+ );
87
+ const credentialSeed = extractSeedFromCredential(credential);
88
+ const encryptionKey = deriveEncryptionKey(credentialSeed);
89
+ const authResultVal = decryptAuthResult(deps)(data, encryptionKey);
90
+ if (!authResultVal) {
91
+ return null;
92
+ }
93
+ return {
94
+ key,
95
+ service: options?.service ?? "default",
96
+ value: authResultVal,
97
+ metadata: data.metadata,
98
+ };
99
+ } catch (_error) {
100
+ return null;
101
+ }
102
+ },
103
+
104
+ deleteItem: async (key, options) => {
105
+ await del(key, getStore(options?.service));
106
+ return true;
107
+ },
108
+
109
+ getAllItems: async (options) => {
110
+ const service = options?.service ?? "default";
111
+ const itemKeys = await keys<string>(getStore(service));
112
+ const items = await Promise.all(
113
+ itemKeys.map(async (key) => {
114
+ const data = await get<{
115
+ readonly metadata?: SensitiveInfoItem["metadata"];
116
+ readonly value?: string;
117
+ }>(key, getStore(service));
118
+ return {
119
+ key,
120
+ service,
121
+ metadata: data?.metadata ?? createMetadata(),
122
+ ...(options?.includeValues && data?.value
123
+ ? { value: data.value }
124
+ : {}),
125
+ };
126
+ }),
127
+ );
128
+ return items;
129
+ },
130
+
131
+ clearService: async (options) => {
132
+ await clear(getStore(options?.service));
133
+ },
134
+ });
135
+
136
+ /**
137
+ * Create default metadata for backwards compatibility with items that don't
138
+ * have stored metadata.
139
+ */
140
+ const createMetadata = (isSecure = true): SensitiveInfoItem["metadata"] => {
141
+ return {
142
+ backend: "keychain",
143
+ accessControl: isSecure ? "biometryCurrentSet" : "none",
144
+ securityLevel: isSecure ? "biometry" : "software",
145
+ timestamp: Date.now(),
146
+ };
147
+ };
148
+
149
+ /** Get storage key for owner ID. (supports namespaces via prefix) */
150
+ const getStore = (prefix = "default"): UseStore => {
151
+ return createStore(prefix, "evolu-auth");
152
+ };
153
+
154
+ const createCredential =
155
+ (deps: RandomBytesDep) =>
156
+ async (
157
+ username: string,
158
+ seed: Uint8Array,
159
+ relyingPartyID?: string,
160
+ relyingPartyName?: string,
161
+ userVerification?: UserVerificationRequirement,
162
+ authenticatorAttachment?: AuthenticatorAttachment,
163
+ ): Promise<PublicKeyCredential> => {
164
+ const options = createCredentialCreationOptions(deps)(
165
+ username,
166
+ seed,
167
+ relyingPartyID,
168
+ relyingPartyName,
169
+ userVerification,
170
+ authenticatorAttachment,
171
+ );
172
+ const credential = (await navigator.credentials.create(
173
+ options,
174
+ )) as PublicKeyCredential | null;
175
+ if (!credential) {
176
+ throw new Error("Failed to create WebAuthn credential");
177
+ }
178
+ return credential;
179
+ };
180
+
181
+ const getCredential =
182
+ (deps: RandomBytesDep) =>
183
+ async (
184
+ credentialId: string,
185
+ relyingPartyID?: string,
186
+ userVerification?: UserVerificationRequirement,
187
+ ): Promise<PublicKeyCredential> => {
188
+ const options = createCredentialRequestOptions(deps)(
189
+ credentialId,
190
+ relyingPartyID,
191
+ userVerification,
192
+ );
193
+ const credential = (await navigator.credentials.get(
194
+ options,
195
+ )) as PublicKeyCredential | null;
196
+ if (!credential?.response) {
197
+ throw new Error("Failed to get WebAuthn credential");
198
+ }
199
+ return credential;
200
+ };
201
+
202
+ const extractSeedFromCredential = (
203
+ credential: PublicKeyCredential,
204
+ ): Uint8Array => {
205
+ const response = credential.response as AuthenticatorAssertionResponse;
206
+ if (!response.userHandle) {
207
+ throw new Error("No userHandle in credential response");
208
+ }
209
+ return new Uint8Array(response.userHandle);
210
+ };
211
+
212
+ const createCredentialCreationOptions =
213
+ (deps: RandomBytesDep) =>
214
+ (
215
+ username: string,
216
+ seed: Uint8Array,
217
+ relyingPartyID?: string,
218
+ relyingPartyName?: string,
219
+ userVerification?: UserVerificationRequirement,
220
+ authenticatorAttachment?: AuthenticatorAttachment,
221
+ ): CredentialCreationOptions => {
222
+ return {
223
+ publicKey: {
224
+ challenge: generateSeed(deps)() as BufferSource,
225
+ rp: {
226
+ id: relyingPartyID ?? document.location.hostname,
227
+ name: relyingPartyName ?? "Evolu",
228
+ },
229
+ user: {
230
+ id: seed as BufferSource,
231
+ name: username,
232
+ displayName: username,
233
+ },
234
+ pubKeyCredParams: [
235
+ { type: "public-key", alg: -8 }, // Ed25519
236
+ { type: "public-key", alg: -7 }, // ES256
237
+ { type: "public-key", alg: -257 }, // RS256
238
+ ],
239
+ attestation: "none",
240
+ authenticatorSelection: {
241
+ // - "platform": Uses the platform's built-in authenticator.
242
+ // - "cross-platform": Uses a device specific authenticator (yubikey, fido2, etc.)
243
+ authenticatorAttachment: authenticatorAttachment ?? "platform",
244
+ // - "discouraged": Only User Presence is needed.
245
+ // - "preferred": User Verification is preferred but not required. Falls back to User Presence.
246
+ // - "required": User Verification MUST occur (biometrics/PIN). Clients may silently downgrade to User Presence only.
247
+ userVerification: userVerification ?? "required",
248
+ // - "discouraged": Server-side credential is preferable, but will accept client-side discoverable credential.
249
+ // - "preferred": Relying Party strongly prefers client-side discoverable credential but will accept server-side credential.
250
+ // - "required": Client-side discoverable credential MUST be created, error if it can't be created.
251
+ residentKey: "required",
252
+ // Included for backwards compatibility. Deprecated in favor of residentKey (true = "required")
253
+ requireResidentKey: true,
254
+ },
255
+ },
256
+ };
257
+ };
258
+
259
+ const createCredentialRequestOptions =
260
+ (deps: RandomBytesDep) =>
261
+ (
262
+ credentialId: string,
263
+ relyingPartyID?: string,
264
+ userVerification?: UserVerificationRequirement,
265
+ ): CredentialRequestOptions => {
266
+ return {
267
+ publicKey: {
268
+ challenge: generateSeed(deps)() as BufferSource,
269
+ rpId: relyingPartyID ?? document.location.hostname,
270
+ userVerification: userVerification ?? "preferred",
271
+ allowCredentials: [
272
+ {
273
+ type: "public-key",
274
+ id: base64UrlToUint8Array(
275
+ Base64Url.orThrow(credentialId),
276
+ ) as BufferSource,
277
+ },
278
+ ],
279
+ },
280
+ };
281
+ };
282
+
283
+ const deriveEncryptionKey = (seed: Uint8Array): EncryptionKey => {
284
+ const seed32 = seed.length === 32 ? seed : seed.slice(0, 32);
285
+ return EncryptionKey.orThrow(
286
+ createSlip21(seed32 as Entropy32, ["evolu", "auth"]),
287
+ );
288
+ };
289
+
290
+ const encryptAuthResult =
291
+ (deps: SymmetricCryptoDep) =>
292
+ (
293
+ authResult: AuthResult,
294
+ encryptionKey: EncryptionKey,
295
+ ): {
296
+ nonce: Base64Url;
297
+ ciphertext: Base64Url;
298
+ } => {
299
+ const plaintext = utf8ToBytes(JSON.stringify(authResult));
300
+ const { nonce, ciphertext } = deps.symmetricCrypto.encrypt(
301
+ plaintext,
302
+ encryptionKey,
303
+ );
304
+ return {
305
+ nonce: uint8ArrayToBase64Url(nonce),
306
+ ciphertext: uint8ArrayToBase64Url(ciphertext),
307
+ };
308
+ };
309
+
310
+ const decryptAuthResult =
311
+ (deps: SymmetricCryptoDep) =>
312
+ (
313
+ encryptedData: { nonce: Base64Url; ciphertext: Base64Url },
314
+ encryptionKey: EncryptionKey,
315
+ ): string | null => {
316
+ const nonce = base64UrlToUint8Array(encryptedData.nonce);
317
+ const ciphertext = base64UrlToUint8Array(encryptedData.ciphertext);
318
+ const result = deps.symmetricCrypto.decrypt(
319
+ ciphertext,
320
+ encryptionKey,
321
+ nonce,
322
+ );
323
+ if (!result.ok) return null;
324
+ return bytesToUtf8(result.value);
325
+ };
326
+
327
+ const generateSeed = (deps: RandomBytesDep) => () => {
328
+ return deps.randomBytes.create(32);
329
+ };
@@ -0,0 +1,9 @@
1
+ import { ReloadApp } from "@evolu/common/evolu";
2
+
3
+ export const reloadApp: ReloadApp = (url: string) => {
4
+ if (typeof document === "undefined") {
5
+ return;
6
+ }
7
+
8
+ location.replace(url);
9
+ };
@@ -1,4 +1,10 @@
1
- import { createConsole, createNanoIdLib, createTime } from "@evolu/common";
1
+ import {
2
+ createConsole,
3
+ createLocalAuth,
4
+ createRandomBytes,
5
+ createSymmetricCrypto,
6
+ createTime,
7
+ } from "@evolu/common";
2
8
  import {
3
9
  CreateDbWorker,
4
10
  DbWorkerInput,
@@ -6,7 +12,11 @@ import {
6
12
  EvoluDeps,
7
13
  } from "@evolu/common/evolu";
8
14
  import { createSharedWebWorker } from "../SharedWebWorker.js";
9
- import { createAppState } from "./AppState.js";
15
+ import { createWebAuthnStore } from "./LocalAuth.js";
16
+ import { reloadApp } from "./Platform.js";
17
+
18
+ const randomBytes = createRandomBytes();
19
+ const symmetricCrypto = createSymmetricCrypto({ randomBytes });
10
20
 
11
21
  const createDbWorker: CreateDbWorker = (name) =>
12
22
  createSharedWebWorker<DbWorkerInput, DbWorkerOutput>(
@@ -17,10 +27,15 @@ const createDbWorker: CreateDbWorker = (name) =>
17
27
  }),
18
28
  );
19
29
 
30
+ export const localAuth = createLocalAuth({
31
+ randomBytes,
32
+ secureStorage: createWebAuthnStore({ randomBytes, symmetricCrypto }),
33
+ });
34
+
20
35
  export const evoluWebDeps: EvoluDeps = {
21
- time: createTime(),
22
36
  console: createConsole(),
23
- nanoIdLib: createNanoIdLib(),
24
- createAppState,
25
37
  createDbWorker,
38
+ randomBytes: createRandomBytes(),
39
+ reloadApp,
40
+ time: createTime(),
26
41
  };
@@ -4,8 +4,12 @@ import {
4
4
  CreateSqliteDriver,
5
5
  SqliteDriver,
6
6
  SqliteRow,
7
+ bytesToHex,
7
8
  } from "@evolu/common";
8
- import sqlite3InitModule, { PreparedStatement } from "@sqlite.org/sqlite-wasm";
9
+ import sqlite3InitModule, {
10
+ PreparedStatement,
11
+ Database,
12
+ } from "@evolu/sqlite-wasm";
9
13
 
10
14
  // TODO: Do we still need that?
11
15
  // https://github.com/sqlite/sqlite-wasm/issues/62
@@ -22,12 +26,29 @@ export const createWasmSqliteDriver: CreateSqliteDriver = async (
22
26
  options,
23
27
  ) => {
24
28
  const sqlite3 = await sqlite3Promise;
29
+ // This is used to make OPFS default vfs for multipleciphers
30
+ // @ts-expect-error Missing types (update @evolu/sqlite-wasm types)
31
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
32
+ sqlite3.capi.sqlite3mc_vfs_create("opfs", 1);
33
+
34
+ let db: Database;
35
+ if (options?.memory) {
36
+ db = new sqlite3.oo1.DB(":memory:");
37
+ } else if (options?.encryptionKey) {
38
+ const pool = await sqlite3.installOpfsSAHPoolVfs({ directory: `.${name}` });
39
+ db = new pool.OpfsSAHPoolDb(
40
+ "file:evolu1.db?vfs=multipleciphers-opfs-sahpool",
41
+ );
42
+ db.exec(`
43
+ PRAGMA cipher = 'sqlcipher';
44
+ PRAGMA legacy = 4;
45
+ PRAGMA key = "x'${bytesToHex(options.encryptionKey)}'";
46
+ `);
47
+ } else {
48
+ const pool = await sqlite3.installOpfsSAHPoolVfs({ name });
49
+ db = new pool.OpfsSAHPoolDb("file:evolu1.db");
50
+ }
25
51
 
26
- const db = options?.memory
27
- ? new sqlite3.oo1.DB(":memory:")
28
- : new (await sqlite3.installOpfsSAHPoolVfs({ name })).OpfsSAHPoolDb(
29
- "/evolu1.db",
30
- );
31
52
  let isDisposed = false;
32
53
 
33
54
  const cache = createPreparedStatementsCache<PreparedStatement>(
@@ -1,3 +0,0 @@
1
- import { CreateAppState } from "@evolu/common/evolu";
2
- export declare const createAppState: CreateAppState;
3
- //# sourceMappingURL=AppState.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AppState.d.ts","sourceRoot":"","sources":["../../../src/Evolu/AppState.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE/D,eAAO,MAAM,cAAc,EAAE,cAe5B,CAAC"}
@@ -1,15 +0,0 @@
1
- import { constVoid } from "@evolu/common";
2
- export const createAppState = (config) => {
3
- if (typeof document === "undefined") {
4
- const appState = {
5
- reset: constVoid,
6
- };
7
- return appState;
8
- }
9
- const appState = {
10
- reset: () => {
11
- location.replace(config.reloadUrl);
12
- },
13
- };
14
- return appState;
15
- };
@@ -1,19 +0,0 @@
1
- import { constVoid } from "@evolu/common";
2
- import { AppState, CreateAppState } from "@evolu/common/evolu";
3
-
4
- export const createAppState: CreateAppState = (config) => {
5
- if (typeof document === "undefined") {
6
- const appState: AppState = {
7
- reset: constVoid,
8
- };
9
- return appState;
10
- }
11
-
12
- const appState: AppState = {
13
- reset: () => {
14
- location.replace(config.reloadUrl);
15
- },
16
- };
17
-
18
- return appState;
19
- };