@interop/was-react 0.8.2 → 0.9.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 +55 -44
- package/dist/auth/chapi.d.ts.map +1 -1
- package/dist/auth/chapi.js +16 -5
- package/dist/auth/chapi.js.map +1 -1
- package/dist/auth/loginFlow.d.ts.map +1 -1
- package/dist/auth/loginFlow.js +55 -49
- package/dist/auth/loginFlow.js.map +1 -1
- package/dist/auth/verifyResponse.d.ts.map +1 -1
- package/dist/auth/verifyResponse.js +27 -10
- package/dist/auth/verifyResponse.js.map +1 -1
- package/dist/auth/walletRequestTypes.d.ts +9 -90
- package/dist/auth/walletRequestTypes.d.ts.map +1 -1
- package/dist/config.d.ts +9 -9
- package/dist/config.js +0 -0
- package/dist/dev/provisionDevGrants.d.ts.map +1 -1
- package/dist/dev/provisionDevGrants.js +18 -19
- package/dist/dev/provisionDevGrants.js.map +1 -1
- package/dist/grants.d.ts +19 -9
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +17 -2
- package/dist/grants.js.map +1 -1
- package/dist/identity/agents.d.ts +1 -1
- package/dist/identity/agents.d.ts.map +1 -1
- package/dist/identity/agents.js +10 -25
- package/dist/identity/agents.js.map +1 -1
- package/dist/identity/appSession.d.ts +27 -0
- package/dist/identity/appSession.d.ts.map +1 -1
- package/dist/identity/appSession.js +51 -13
- package/dist/identity/appSession.js.map +1 -1
- package/dist/identity/seedCredential.d.ts +7 -0
- package/dist/identity/seedCredential.d.ts.map +1 -1
- package/dist/identity/seedCredential.js +11 -14
- package/dist/identity/seedCredential.js.map +1 -1
- package/dist/identity/seedStore.d.ts +19 -0
- package/dist/identity/seedStore.d.ts.map +1 -1
- package/dist/identity/seedStore.js +40 -13
- package/dist/identity/seedStore.js.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/jsonLd.d.ts +19 -0
- package/dist/jsonLd.d.ts.map +1 -0
- package/dist/jsonLd.js +24 -0
- package/dist/jsonLd.js.map +1 -0
- package/dist/mui/AdoptDialog.d.ts.map +1 -1
- package/dist/mui/AdoptDialog.js +7 -3
- package/dist/mui/AdoptDialog.js.map +1 -1
- package/dist/mui/ClearDataDialog.d.ts.map +1 -1
- package/dist/mui/ClearDataDialog.js +12 -11
- package/dist/mui/ClearDataDialog.js.map +1 -1
- package/dist/mui/ConfirmDialog.d.ts +35 -0
- package/dist/mui/ConfirmDialog.d.ts.map +1 -0
- package/dist/mui/ConfirmDialog.js +18 -0
- package/dist/mui/ConfirmDialog.js.map +1 -0
- package/dist/mui/LogoutDialog.d.ts.map +1 -1
- package/dist/mui/LogoutDialog.js +7 -3
- package/dist/mui/LogoutDialog.js.map +1 -1
- package/dist/react/documentApp.d.ts +29 -0
- package/dist/react/documentApp.d.ts.map +1 -1
- package/dist/react/documentApp.js +9 -38
- package/dist/react/documentApp.js.map +1 -1
- package/dist/react/hooks.d.ts +13 -7
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/hooks.js +17 -11
- package/dist/react/hooks.js.map +1 -1
- package/dist/session/authStore.d.ts +9 -9
- package/dist/session/authStore.d.ts.map +1 -1
- package/dist/session/authStore.js +203 -112
- package/dist/session/authStore.js.map +1 -1
- package/dist/storage/adopt.d.ts +5 -1
- package/dist/storage/adopt.d.ts.map +1 -1
- package/dist/storage/adopt.js +8 -24
- package/dist/storage/adopt.js.map +1 -1
- package/dist/storage/entityStore.d.ts.map +1 -1
- package/dist/storage/entityStore.js +27 -20
- package/dist/storage/entityStore.js.map +1 -1
- package/dist/storage/localStore.d.ts +12 -6
- package/dist/storage/localStore.d.ts.map +1 -1
- package/dist/storage/localStore.js +146 -77
- package/dist/storage/localStore.js.map +1 -1
- package/dist/storage/rehydrate.d.ts.map +1 -1
- package/dist/storage/rehydrate.js +20 -4
- package/dist/storage/rehydrate.js.map +1 -1
- package/dist/storage/sharedCollectionReader.d.ts +1 -1
- package/dist/storage/sharedCollectionReader.d.ts.map +1 -1
- package/dist/storage/sharedCollectionReader.js +79 -64
- package/dist/storage/sharedCollectionReader.js.map +1 -1
- package/dist/storage/storageManager.d.ts +3 -1
- package/dist/storage/storageManager.d.ts.map +1 -1
- package/dist/storage/storageManager.js +20 -6
- package/dist/storage/storageManager.js.map +1 -1
- package/dist/storage/syncController.d.ts +1 -16
- package/dist/storage/syncController.d.ts.map +1 -1
- package/dist/storage/syncController.js +48 -37
- package/dist/storage/syncController.js.map +1 -1
- package/dist/storage/syncStatusStore.d.ts +8 -6
- package/dist/storage/syncStatusStore.d.ts.map +1 -1
- package/dist/storage/syncStatusStore.js +6 -6
- package/dist/storage/syncStatusStore.js.map +1 -1
- package/dist/storage/wasRemoteStore.d.ts +34 -2
- package/dist/storage/wasRemoteStore.d.ts.map +1 -1
- package/dist/storage/wasRemoteStore.js +45 -7
- package/dist/storage/wasRemoteStore.js.map +1 -1
- package/dist/storage/wasSync.d.ts.map +1 -1
- package/dist/storage/wasSync.js +47 -38
- package/dist/storage/wasSync.js.map +1 -1
- package/dist/sync/changesQuery.d.ts.map +1 -1
- package/dist/sync/changesQuery.js +2 -9
- package/dist/sync/changesQuery.js.map +1 -1
- package/dist/sync/docCipher.d.ts +23 -10
- package/dist/sync/docCipher.d.ts.map +1 -1
- package/dist/sync/docCipher.js +17 -11
- package/dist/sync/docCipher.js.map +1 -1
- package/dist/sync/feedMasterPort.d.ts +6 -0
- package/dist/sync/feedMasterPort.d.ts.map +1 -1
- package/dist/sync/feedMasterPort.js +82 -37
- package/dist/sync/feedMasterPort.js.map +1 -1
- package/dist/sync/index.d.ts +6 -5
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +6 -3
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/lww.d.ts +27 -18
- package/dist/sync/lww.d.ts.map +1 -1
- package/dist/sync/lww.js +35 -8
- package/dist/sync/lww.js.map +1 -1
- package/dist/sync/lwwConflictHandler.d.ts +15 -14
- package/dist/sync/lwwConflictHandler.d.ts.map +1 -1
- package/dist/sync/lwwConflictHandler.js +37 -26
- package/dist/sync/lwwConflictHandler.js.map +1 -1
- package/dist/sync/pushWrites.d.ts +9 -9
- package/dist/sync/pushWrites.d.ts.map +1 -1
- package/dist/sync/pushWrites.js +102 -68
- package/dist/sync/pushWrites.js.map +1 -1
- package/dist/sync/syncedDocSchema.d.ts.map +1 -1
- package/dist/sync/syncedDocSchema.js +3 -0
- package/dist/sync/syncedDocSchema.js.map +1 -1
- package/dist/sync/types.d.ts +115 -15
- package/dist/sync/types.d.ts.map +1 -1
- package/dist/sync/types.js +36 -18
- package/dist/sync/types.js.map +1 -1
- package/dist/sync/wasSyncPort.d.ts +0 -11
- package/dist/sync/wasSyncPort.d.ts.map +1 -1
- package/dist/sync/wasSyncPort.js +20 -36
- package/dist/sync/wasSyncPort.js.map +1 -1
- package/package.json +6 -6
- package/dist/identity/initAppSession.d.ts +0 -27
- package/dist/identity/initAppSession.d.ts.map +0 -1
- package/dist/identity/initAppSession.js +0 -30
- package/dist/identity/initAppSession.js.map +0 -1
|
@@ -33,26 +33,22 @@
|
|
|
33
33
|
* vanilla zustand store the hooks consume through context.
|
|
34
34
|
*/
|
|
35
35
|
import { createStore } from 'zustand/vanilla';
|
|
36
|
+
import { acquireDescriptors } from '@interop/wallet-core/descriptors';
|
|
36
37
|
import { DEFAULT_DB_NAME, DEFAULT_EXPIRY_WARNING_MS, DEFAULT_EXPIRY_WATCH_MS, DEFAULT_ONBOARDING } from '../config.js';
|
|
37
38
|
import { createDocumentLoader } from '../identity/documentLoader.js';
|
|
38
|
-
import {
|
|
39
|
-
import { createSeedStore } from '../identity/seedStore.js';
|
|
40
|
-
import { clearAppSession, persistAppSession, restoreAppSession, isNearExpiry, earliestExpiry } from '../identity/appSession.js';
|
|
39
|
+
import { deriveIdentity } from '../identity/agents.js';
|
|
40
|
+
import { createDescriptorCache, createSeedStore } from '../identity/seedStore.js';
|
|
41
|
+
import { clearAppSession, peekAppSession, persistAppSession, restoreAppSession, isNearExpiry, earliestExpiry, NO_EXPIRY_MS } from '../identity/appSession.js';
|
|
41
42
|
import { loginWithWallet, requestGrants, LoginCancelledError } from '../auth/loginFlow.js';
|
|
42
43
|
import { parseGrants } from '../grants.js';
|
|
43
44
|
import { LocalStore, dbNameForController } from '../storage/localStore.js';
|
|
44
|
-
import { clearLocalStore, clearRemoteStore, hasStore, requireStore, setLocalStore, setRemoteStore } from '../storage/storageManager.js';
|
|
45
|
+
import { clearLocalStore, clearRemoteStore, getClientId, hasStore, requireStore, setLocalStore, setRemoteStore } from '../storage/storageManager.js';
|
|
45
46
|
import { cancelScheduledRehydrates, clearAllEntityStores, hydrateAll, patchFromChange } from '../storage/rehydrate.js';
|
|
46
47
|
import { mergeAdopted } from '../storage/adopt.js';
|
|
47
48
|
import { startWasSync } from '../storage/wasSync.js';
|
|
48
49
|
import { errorMessage } from '../sync/index.js';
|
|
49
|
-
import {
|
|
50
|
+
import { SyncController } from '../storage/syncController.js';
|
|
50
51
|
import { useSyncStatusStore } from '../storage/syncStatusStore.js';
|
|
51
|
-
/**
|
|
52
|
-
* A nominal far-future expiry (ms) for grants minted without one (dev/test).
|
|
53
|
-
* Well past the near-expiry warning, so the watch never fires against them.
|
|
54
|
-
*/
|
|
55
|
-
const FAR_FUTURE_EXPIRY_MS = 100 * 365 * 24 * 60 * 60 * 1000;
|
|
56
52
|
/**
|
|
57
53
|
* Builds the session auth store bound to an app's config and store registry.
|
|
58
54
|
* Call once (the React provider does) and share the returned store through
|
|
@@ -71,7 +67,20 @@ const FAR_FUTURE_EXPIRY_MS = 100 * 365 * 24 * 60 * 60 * 1000;
|
|
|
71
67
|
export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
72
68
|
const dbName = config.dbName ?? DEFAULT_DB_NAME;
|
|
73
69
|
const onboarding = config.onboarding ?? DEFAULT_ONBOARDING;
|
|
70
|
+
// Resolve the per-install LWW client id ONCE, honoring the configured
|
|
71
|
+
// localStorage prefix, so every stamp -- the app's own writes and the
|
|
72
|
+
// adoption repair -- carries the same attribution identity.
|
|
73
|
+
const clientId = getClientId({
|
|
74
|
+
...(config.storageKeyPrefix !== undefined && {
|
|
75
|
+
storageKeyPrefix: config.storageKeyPrefix
|
|
76
|
+
})
|
|
77
|
+
});
|
|
74
78
|
const sessionStore = seedStore ?? createSeedStore({ dbName: `${dbName}-session` });
|
|
79
|
+
// The offline encryption-descriptor cache, presented as the seam
|
|
80
|
+
// `@interop/wallet-core/descriptors` acquires through. Reads serve the
|
|
81
|
+
// connected replica's epoch-aware open (cache-only, no network); writes land
|
|
82
|
+
// the descriptors the sync bootstrap fetched.
|
|
83
|
+
const descriptorCache = createDescriptorCache({ store: sessionStore });
|
|
75
84
|
// The anonymous-seed persistence for `local` mode: only a raw 32-byte seed,
|
|
76
85
|
// no session record, in its own IndexedDB so it never collides with the
|
|
77
86
|
// wallet session or a connected replica.
|
|
@@ -108,6 +117,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
108
117
|
*/
|
|
109
118
|
const watchMs = config.expiry?.watchMs ?? DEFAULT_EXPIRY_WATCH_MS;
|
|
110
119
|
let expiryTimer;
|
|
120
|
+
let expiryInitialCheck;
|
|
111
121
|
// The per-session controller: single-use, stopped on logout and replaced on a
|
|
112
122
|
// reconnect (started once per grant set).
|
|
113
123
|
let controller = null;
|
|
@@ -145,11 +155,22 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
145
155
|
clearInterval(expiryTimer);
|
|
146
156
|
expiryTimer = undefined;
|
|
147
157
|
}
|
|
158
|
+
if (expiryInitialCheck) {
|
|
159
|
+
clearTimeout(expiryInitialCheck);
|
|
160
|
+
expiryInitialCheck = undefined;
|
|
161
|
+
}
|
|
148
162
|
}
|
|
149
163
|
/**
|
|
150
164
|
* Watches the session's earliest grant expiry and raises the reconnect banner
|
|
151
|
-
* once it is within `warningMs` (or already past).
|
|
152
|
-
* a
|
|
165
|
+
* once it is within `warningMs` (or already past). The first check is
|
|
166
|
+
* DEFERRED a macrotask rather than run synchronously: every caller awaits
|
|
167
|
+
* `persistAndStartSync` (which arms this watch) BEFORE writing
|
|
168
|
+
* `status: 'connected'`, so a synchronous check
|
|
169
|
+
* would always be swallowed by `notifyAccessExpired`'s connected gate and
|
|
170
|
+
* then actively cleared -- leaving a restored near-expiry session without
|
|
171
|
+
* its proactive banner until the first `watchMs` tick. Deferring runs the
|
|
172
|
+
* check after the caller's continuation has set the status. Then checks on a
|
|
173
|
+
* coarse interval; re-armed with the fresh expiry after a reconnect.
|
|
153
174
|
*/
|
|
154
175
|
function armExpiryWatch(expires) {
|
|
155
176
|
disarmExpiryWatch();
|
|
@@ -158,7 +179,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
158
179
|
store.getState().notifyAccessExpired();
|
|
159
180
|
}
|
|
160
181
|
};
|
|
161
|
-
check
|
|
182
|
+
expiryInitialCheck = setTimeout(check, 0);
|
|
162
183
|
expiryTimer = setInterval(check, watchMs);
|
|
163
184
|
}
|
|
164
185
|
/**
|
|
@@ -169,7 +190,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
169
190
|
* `stopController` clears it.
|
|
170
191
|
*/
|
|
171
192
|
async function beginSync({ parsed, zcapClient, keyAgreementKey, keyResolver }) {
|
|
172
|
-
controller =
|
|
193
|
+
controller = new SyncController({
|
|
173
194
|
collections: config.collections,
|
|
174
195
|
...(config.sync && { sync: config.sync })
|
|
175
196
|
});
|
|
@@ -185,9 +206,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
185
206
|
}),
|
|
186
207
|
identityKeys: { keyAgreementKey, keyResolver },
|
|
187
208
|
onAuthError: () => store.getState().notifyAccessExpired(),
|
|
188
|
-
onDescriptorsFetched: descriptors => void
|
|
189
|
-
.saveDescriptors(descriptors)
|
|
190
|
-
.catch(err => console.warn('Failed to cache encryption descriptors:', err))
|
|
209
|
+
onDescriptorsFetched: descriptors => void cacheDescriptors(descriptors).catch(err => console.warn('Failed to cache encryption descriptors:', err))
|
|
191
210
|
});
|
|
192
211
|
setRemoteStore(remoteStore);
|
|
193
212
|
store.setState({ sharedCollections });
|
|
@@ -225,7 +244,20 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
225
244
|
keyAgreementKey: identity.keyAgreementKey,
|
|
226
245
|
keyResolver: identity.keyResolver
|
|
227
246
|
});
|
|
228
|
-
|
|
247
|
+
// A failed bootstrap must not resolve silently: surface it on the session
|
|
248
|
+
// `error` (the sync rollup shows the per-collection error statuses the
|
|
249
|
+
// controller leaves behind). The session stays usable -- local-first --
|
|
250
|
+
// but the UI no longer reports "Local only" over a wallet-connected
|
|
251
|
+
// session that simply failed to start replicating.
|
|
252
|
+
void pendingSync.catch(err => {
|
|
253
|
+
console.warn('WAS sync failed to start:', err);
|
|
254
|
+
// Deferred a macrotask: an immediate bootstrap failure would otherwise
|
|
255
|
+
// race the caller's own `set({ ..., error: null })` (which lands right
|
|
256
|
+
// after `persistAndStartSync` resolves) and be wiped by it.
|
|
257
|
+
setTimeout(() => store.setState({
|
|
258
|
+
error: `Sync failed to start: ${errorMessage(err)}`
|
|
259
|
+
}), 0);
|
|
260
|
+
});
|
|
229
261
|
armExpiryWatch(expires);
|
|
230
262
|
}
|
|
231
263
|
/**
|
|
@@ -286,7 +318,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
286
318
|
});
|
|
287
319
|
setLocalStore(local);
|
|
288
320
|
if (adopt) {
|
|
289
|
-
await mergeAdopted({ store: local, entities: adopt.entities });
|
|
321
|
+
await mergeAdopted({ store: local, entities: adopt.entities, clientId });
|
|
290
322
|
}
|
|
291
323
|
await hydrateAll(registry);
|
|
292
324
|
}
|
|
@@ -315,7 +347,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
315
347
|
*/
|
|
316
348
|
async function openLocal() {
|
|
317
349
|
const { seed, created } = await loadOrCreateAnonSeed();
|
|
318
|
-
const identity = await
|
|
350
|
+
const identity = await deriveIdentity({ seed });
|
|
319
351
|
await openAndHydrate({ identity });
|
|
320
352
|
if (created && config.seedLocal) {
|
|
321
353
|
await config.seedLocal();
|
|
@@ -324,7 +356,6 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
324
356
|
status: 'local',
|
|
325
357
|
controllerDid: identity.controllerDid,
|
|
326
358
|
expires: null,
|
|
327
|
-
accessExpired: false,
|
|
328
359
|
error: null
|
|
329
360
|
});
|
|
330
361
|
}
|
|
@@ -334,25 +365,42 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
334
365
|
* anonymous local replica re-opened (so the app never dead-ends), then the
|
|
335
366
|
* error is surfaced and rethrown for the caller to finalize.
|
|
336
367
|
*
|
|
337
|
-
* @param session {
|
|
368
|
+
* @param session {object} the session to open: its seed, identity, parsed
|
|
369
|
+
* grants and expiry, plus the payloads to adopt (merge logins only)
|
|
338
370
|
* @returns {Promise<void>}
|
|
339
371
|
*/
|
|
340
372
|
/**
|
|
341
|
-
* Loads the
|
|
342
|
-
* or `undefined` when none
|
|
343
|
-
*
|
|
373
|
+
* Loads the cached encryption descriptors for the registered collections
|
|
374
|
+
* (keyed by WAS collection id), or `undefined` when none are cached.
|
|
375
|
+
*
|
|
376
|
+
* Acquired CACHE-ONLY -- `acquireDescriptors` with no `source` -- because
|
|
377
|
+
* this runs before any remote store exists and must not touch the network:
|
|
378
|
+
* the whole point is that an offline hot restore opens epoch-aware. The
|
|
379
|
+
* connected session refreshes them from the server once sync bootstraps.
|
|
380
|
+
* Best-effort: a read failure falls back to opening single-key ciphers.
|
|
344
381
|
*/
|
|
345
382
|
async function loadCachedDescriptors() {
|
|
346
383
|
try {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
384
|
+
const descriptors = await acquireDescriptors({
|
|
385
|
+
cache: descriptorCache,
|
|
386
|
+
collectionIds: config.collections.map(collection => collection.id)
|
|
387
|
+
});
|
|
388
|
+
return Object.keys(descriptors).length > 0 ? descriptors : undefined;
|
|
351
389
|
}
|
|
352
390
|
catch (err) {
|
|
353
391
|
console.warn('Failed to load cached encryption descriptors:', err);
|
|
392
|
+
return undefined;
|
|
354
393
|
}
|
|
355
|
-
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Writes the descriptors the sync bootstrap fetched into the offline cache,
|
|
397
|
+
* through the same seam {@link loadCachedDescriptors} reads.
|
|
398
|
+
*
|
|
399
|
+
* @param descriptors {Record<string, CollectionEncryption>}
|
|
400
|
+
* @returns {Promise<void>}
|
|
401
|
+
*/
|
|
402
|
+
async function cacheDescriptors(descriptors) {
|
|
403
|
+
await Promise.all(Object.entries(descriptors).map(([collectionId, descriptor]) => descriptorCache.writeDescriptor({ collectionId, descriptor })));
|
|
356
404
|
}
|
|
357
405
|
async function activateConnected(session) {
|
|
358
406
|
try {
|
|
@@ -384,20 +432,29 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
384
432
|
/**
|
|
385
433
|
* Tears down the live replica + sync + entity stores WITHOUT touching either
|
|
386
434
|
* persisted seed (the anonymous seed and the session record survive). Closes
|
|
387
|
-
* the database;
|
|
435
|
+
* the database by default; `deleteDb` deletes it instead (the wipe paths, via
|
|
436
|
+
* {@link resetToFreshLocal}).
|
|
388
437
|
*
|
|
438
|
+
* @param [options] {object}
|
|
439
|
+
* @param [options.deleteDb] {boolean} delete the database rather than
|
|
440
|
+
* closing it
|
|
389
441
|
* @returns {Promise<void>}
|
|
390
442
|
*/
|
|
391
|
-
async function deactivateStore() {
|
|
443
|
+
async function deactivateStore({ deleteDb = false } = {}) {
|
|
392
444
|
disarmExpiryWatch();
|
|
393
445
|
cancelScheduledRehydrates();
|
|
394
446
|
await stopController();
|
|
395
447
|
if (hasStore()) {
|
|
396
448
|
try {
|
|
397
|
-
|
|
449
|
+
if (deleteDb) {
|
|
450
|
+
await requireStore().remove();
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
await requireStore().close();
|
|
454
|
+
}
|
|
398
455
|
}
|
|
399
456
|
catch (err) {
|
|
400
|
-
console.warn('Error
|
|
457
|
+
console.warn('Error tearing down the local store:', err);
|
|
401
458
|
}
|
|
402
459
|
clearLocalStore();
|
|
403
460
|
}
|
|
@@ -417,25 +474,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
417
474
|
* @returns {Promise<void>}
|
|
418
475
|
*/
|
|
419
476
|
async function resetToFreshLocal({ deleteDb, discardAnonSeed = false }) {
|
|
420
|
-
|
|
421
|
-
cancelScheduledRehydrates();
|
|
422
|
-
await stopController();
|
|
423
|
-
if (hasStore()) {
|
|
424
|
-
try {
|
|
425
|
-
if (deleteDb) {
|
|
426
|
-
await requireStore().remove();
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
await requireStore().close();
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
catch (err) {
|
|
433
|
-
console.warn('Error tearing down the local store:', err);
|
|
434
|
-
}
|
|
435
|
-
clearLocalStore();
|
|
436
|
-
}
|
|
437
|
-
clearAllEntityStores(registry);
|
|
438
|
-
useSyncStatusStore.getState().reset();
|
|
477
|
+
await deactivateStore({ deleteDb });
|
|
439
478
|
if (discardAnonSeed) {
|
|
440
479
|
await anonStore.clearSeedStore();
|
|
441
480
|
}
|
|
@@ -457,10 +496,19 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
457
496
|
* are OFF the serialized boot/destroy lifecycle chain). Re-deriving from the
|
|
458
497
|
* seed keeps the collect independent of the holder's lifecycle.
|
|
459
498
|
*
|
|
499
|
+
* Decides off the caller's PRE-FLOW snapshot of `{ status, controllerDid }`,
|
|
500
|
+
* never the live state: a provider unmount (a route change or StrictMode
|
|
501
|
+
* remount) firing `destroy` while the CHAPI popup is open resets the live
|
|
502
|
+
* status to `boot`, and reading it here would silently collect nothing --
|
|
503
|
+
* an `adopt: 'merge'` login would then resolve successfully while the
|
|
504
|
+
* user's pre-login data never reaches their Web Space.
|
|
505
|
+
*
|
|
506
|
+
* @param snapshot {object} the caller's state snapshot from before its
|
|
507
|
+
* first await
|
|
460
508
|
* @returns {Promise<AdoptSource | null>}
|
|
461
509
|
*/
|
|
462
|
-
async function collectAdoptable() {
|
|
463
|
-
const { status, controllerDid } =
|
|
510
|
+
async function collectAdoptable(snapshot) {
|
|
511
|
+
const { status, controllerDid } = snapshot;
|
|
464
512
|
if (status !== 'local' || controllerDid === null) {
|
|
465
513
|
return null;
|
|
466
514
|
}
|
|
@@ -468,7 +516,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
468
516
|
if (!seed) {
|
|
469
517
|
return null;
|
|
470
518
|
}
|
|
471
|
-
const { keyAgreementKey, keyResolver } = await
|
|
519
|
+
const { keyAgreementKey, keyResolver } = await deriveIdentity({ seed });
|
|
472
520
|
const anonLocal = await LocalStore.init({
|
|
473
521
|
keyAgreementKey,
|
|
474
522
|
keyResolver,
|
|
@@ -477,10 +525,16 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
477
525
|
...(storage && { storage })
|
|
478
526
|
});
|
|
479
527
|
try {
|
|
528
|
+
// Read every collection at once: the replica is detached for the whole
|
|
529
|
+
// read, so the transition is as short as the slowest collection rather
|
|
530
|
+
// than the sum of them all.
|
|
531
|
+
const listed = await Promise.all(config.collections.map(async ({ key }) => ({
|
|
532
|
+
key,
|
|
533
|
+
payloads: await anonLocal.listEntities(key)
|
|
534
|
+
})));
|
|
480
535
|
const entities = {};
|
|
481
536
|
let total = 0;
|
|
482
|
-
for (const { key } of
|
|
483
|
-
const payloads = await anonLocal.listEntities(key);
|
|
537
|
+
for (const { key, payloads } of listed) {
|
|
484
538
|
if (payloads.length > 0) {
|
|
485
539
|
entities[key] = payloads;
|
|
486
540
|
total += payloads.length;
|
|
@@ -507,15 +561,17 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
507
561
|
* has been adopted yet; `local` survives intact) and rethrows.
|
|
508
562
|
*
|
|
509
563
|
* @param adopt {'merge' | 'leave'}
|
|
564
|
+
* @param snapshot {object} the caller's pre-flow `{ status, controllerDid }`
|
|
565
|
+
* snapshot (see {@link collectAdoptable})
|
|
510
566
|
* @returns {Promise<AdoptSource | null>}
|
|
511
567
|
*/
|
|
512
|
-
async function detachAndCollect(adopt) {
|
|
568
|
+
async function detachAndCollect(adopt, snapshot) {
|
|
513
569
|
await deactivateStore();
|
|
514
570
|
if (adopt !== 'merge') {
|
|
515
571
|
return null;
|
|
516
572
|
}
|
|
517
573
|
try {
|
|
518
|
-
return await collectAdoptable();
|
|
574
|
+
return await collectAdoptable(snapshot);
|
|
519
575
|
}
|
|
520
576
|
catch (err) {
|
|
521
577
|
await openLocal();
|
|
@@ -526,18 +582,30 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
526
582
|
* Deletes the adopted anonymous replica -- its persisted seed and its
|
|
527
583
|
* per-controller database -- so the data lives on only in the connected
|
|
528
584
|
* replica and a later logout lands in a genuinely fresh `local`. Called only
|
|
529
|
-
* after the connected activation has
|
|
530
|
-
* the anonymous replica intact for the fallback to re-open.
|
|
585
|
+
* after the connected activation has landed (status written); any earlier
|
|
586
|
+
* failure leaves the anonymous replica intact for the fallback to re-open.
|
|
587
|
+
*
|
|
588
|
+
* Best-effort: a deletion failure (an anonymous Dexie database still open in
|
|
589
|
+
* another tab, an IndexedDB quota error) is logged and swallowed. The
|
|
590
|
+
* connected session is already live and syncing at this point, so failing
|
|
591
|
+
* the login over its cleanup would report `local` over a genuinely connected
|
|
592
|
+
* session; at worst a stale anonymous replica lingers and its (already
|
|
593
|
+
* merged) payloads are re-collected by a later login's idempotent LWW merge.
|
|
531
594
|
*
|
|
532
595
|
* @param controllerDid {string} the anonymous controller DID
|
|
533
596
|
* @returns {Promise<void>}
|
|
534
597
|
*/
|
|
535
598
|
async function discardAnonReplica(controllerDid) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
599
|
+
try {
|
|
600
|
+
await anonStore.clearSeedStore();
|
|
601
|
+
await LocalStore.removeDatabase({
|
|
602
|
+
dbName: dbNameForController({ dbName, controllerDid }),
|
|
603
|
+
...(storage && { storage })
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
console.warn('Failed to delete the adopted anonymous replica:', err);
|
|
608
|
+
}
|
|
541
609
|
}
|
|
542
610
|
// Declared last so `prefer-const` is satisfied; the lifecycle closures above
|
|
543
611
|
// only dereference `store` at call time, by which point it is assigned.
|
|
@@ -552,7 +620,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
552
620
|
await openLocal();
|
|
553
621
|
return;
|
|
554
622
|
}
|
|
555
|
-
const identity = await
|
|
623
|
+
const identity = await deriveIdentity({ seed: restored.seed });
|
|
556
624
|
if (identity.controllerDid !== restored.controllerDid) {
|
|
557
625
|
// A corrupt record; treat as logged out and fall to local.
|
|
558
626
|
await clearAppSession({ store: sessionStore });
|
|
@@ -576,8 +644,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
576
644
|
status: uncovered.length > 0 ? 'reconnect' : 'connected',
|
|
577
645
|
controllerDid: identity.controllerDid,
|
|
578
646
|
expires: restored.expires,
|
|
579
|
-
error: null
|
|
580
|
-
accessExpired: uncovered.length > 0
|
|
647
|
+
error: null
|
|
581
648
|
});
|
|
582
649
|
}
|
|
583
650
|
catch (err) {
|
|
@@ -608,12 +675,10 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
608
675
|
// fully-open session, never an in-flight one.
|
|
609
676
|
set({
|
|
610
677
|
status: 'boot',
|
|
611
|
-
authenticating: false,
|
|
612
678
|
phase: null,
|
|
613
679
|
error: null,
|
|
614
680
|
controllerDid: null,
|
|
615
681
|
expires: null,
|
|
616
|
-
accessExpired: false,
|
|
617
682
|
reconnecting: false,
|
|
618
683
|
sharedCollections: {}
|
|
619
684
|
});
|
|
@@ -621,20 +686,27 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
621
686
|
return {
|
|
622
687
|
status: 'boot',
|
|
623
688
|
onboarding,
|
|
624
|
-
|
|
689
|
+
clientId,
|
|
625
690
|
phase: null,
|
|
626
691
|
error: null,
|
|
627
692
|
controllerDid: null,
|
|
628
693
|
expires: null,
|
|
629
|
-
accessExpired: false,
|
|
630
694
|
reconnecting: false,
|
|
631
695
|
sharedCollections: {},
|
|
632
696
|
boot: () => serializeLifecycle(bootImpl),
|
|
633
697
|
login: async ({ adopt = 'merge' } = {}) => {
|
|
634
|
-
if (get().
|
|
698
|
+
if (get().phase !== null || get().status === 'connected') {
|
|
635
699
|
return null;
|
|
636
700
|
}
|
|
637
|
-
|
|
701
|
+
// Snapshot the pre-login state BEFORE the popup await: a provider
|
|
702
|
+
// unmount (`destroy`) landing while CHAPI is open resets the live
|
|
703
|
+
// status to `boot`, and the adoption collect must still see the
|
|
704
|
+
// `local` session this login left.
|
|
705
|
+
const preLogin = {
|
|
706
|
+
status: get().status,
|
|
707
|
+
controllerDid: get().controllerDid
|
|
708
|
+
};
|
|
709
|
+
set({ error: null, phase: 'connecting' });
|
|
638
710
|
try {
|
|
639
711
|
const outcome = await loginWithWallet({
|
|
640
712
|
config: loginConfig,
|
|
@@ -645,7 +717,7 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
645
717
|
// The anonymous seed and database are deleted only after the
|
|
646
718
|
// activation lands, so a failure below still falls back to an intact
|
|
647
719
|
// `local`.
|
|
648
|
-
const source = await detachAndCollect(adopt);
|
|
720
|
+
const source = await detachAndCollect(adopt, preLogin);
|
|
649
721
|
await activateConnected({
|
|
650
722
|
seed: outcome.seed,
|
|
651
723
|
identity: outcome.identity,
|
|
@@ -654,18 +726,19 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
654
726
|
expires: outcome.expires,
|
|
655
727
|
...(source && { adopt: source })
|
|
656
728
|
});
|
|
657
|
-
if (source) {
|
|
658
|
-
await discardAnonReplica(source.controllerDid);
|
|
659
|
-
}
|
|
660
729
|
set({
|
|
661
730
|
status: 'connected',
|
|
662
|
-
authenticating: false,
|
|
663
731
|
controllerDid: outcome.identity.controllerDid,
|
|
664
732
|
expires: outcome.expires,
|
|
665
733
|
phase: null,
|
|
666
|
-
error: null
|
|
667
|
-
accessExpired: false
|
|
734
|
+
error: null
|
|
668
735
|
});
|
|
736
|
+
// Adopted-replica cleanup runs AFTER the connected status lands (and
|
|
737
|
+
// is best-effort): a cleanup failure must not leave the session
|
|
738
|
+
// reporting `local` over a live, syncing connected session.
|
|
739
|
+
if (source) {
|
|
740
|
+
await discardAnonReplica(source.controllerDid);
|
|
741
|
+
}
|
|
669
742
|
return { firstRun: outcome.firstRun };
|
|
670
743
|
}
|
|
671
744
|
catch (err) {
|
|
@@ -673,15 +746,13 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
673
746
|
// scary error, and resolve with `null` so the caller can distinguish it
|
|
674
747
|
// from a connected outcome. `local` stays intact.
|
|
675
748
|
if (err instanceof LoginCancelledError) {
|
|
676
|
-
set({
|
|
749
|
+
set({ phase: null, error: null });
|
|
677
750
|
return null;
|
|
678
751
|
}
|
|
679
752
|
// A genuine failure: record the message so the UI state still reflects
|
|
680
753
|
// it, then rethrow so the caller's promise rejects.
|
|
681
|
-
const message =
|
|
682
|
-
|
|
683
|
-
: 'Login failed.';
|
|
684
|
-
set({ authenticating: false, phase: null, error: message });
|
|
754
|
+
const message = `Login failed: ${errorMessage(err)}`;
|
|
755
|
+
set({ phase: null, error: message });
|
|
685
756
|
throw err;
|
|
686
757
|
}
|
|
687
758
|
},
|
|
@@ -703,11 +774,15 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
703
774
|
if (get().status === 'connected') {
|
|
704
775
|
return;
|
|
705
776
|
}
|
|
706
|
-
const
|
|
777
|
+
const preConnect = {
|
|
778
|
+
status: get().status,
|
|
779
|
+
controllerDid: get().controllerDid
|
|
780
|
+
};
|
|
781
|
+
const identity = await deriveIdentity({ seed });
|
|
707
782
|
const parsed = parseGrants(grants);
|
|
708
783
|
const expires = earliestExpiry(grants) ??
|
|
709
|
-
new Date(Date.now() +
|
|
710
|
-
const source = await detachAndCollect(adopt);
|
|
784
|
+
new Date(Date.now() + NO_EXPIRY_MS).toISOString();
|
|
785
|
+
const source = await detachAndCollect(adopt, preConnect);
|
|
711
786
|
await activateConnected({
|
|
712
787
|
seed,
|
|
713
788
|
identity,
|
|
@@ -716,16 +791,16 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
716
791
|
expires,
|
|
717
792
|
...(source && { adopt: source })
|
|
718
793
|
});
|
|
719
|
-
if (source) {
|
|
720
|
-
await discardAnonReplica(source.controllerDid);
|
|
721
|
-
}
|
|
722
794
|
set({
|
|
723
795
|
status: 'connected',
|
|
724
796
|
controllerDid: identity.controllerDid,
|
|
725
797
|
expires,
|
|
726
|
-
error: null
|
|
727
|
-
accessExpired: false
|
|
798
|
+
error: null
|
|
728
799
|
});
|
|
800
|
+
// After the status lands, and best-effort (see `discardAnonReplica`).
|
|
801
|
+
if (source) {
|
|
802
|
+
await discardAnonReplica(source.controllerDid);
|
|
803
|
+
}
|
|
729
804
|
});
|
|
730
805
|
},
|
|
731
806
|
reconnect: async () => {
|
|
@@ -735,17 +810,37 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
735
810
|
}
|
|
736
811
|
set({ reconnecting: true, error: null });
|
|
737
812
|
try {
|
|
738
|
-
// The seed survives grant expiry; only the grants need renewing
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
|
|
813
|
+
// The seed survives grant expiry; only the grants need renewing, and
|
|
814
|
+
// the record carries the storage location the continuity check below
|
|
815
|
+
// compares against. Peeked (not `restoreAppSession`, which WIPES an
|
|
816
|
+
// expired record -- seed included -- exactly in the case reconnect
|
|
817
|
+
// exists for), and read before the popup so the comparison baseline
|
|
818
|
+
// cannot be overwritten mid-flow. A missing seed means the session is
|
|
819
|
+
// unrecoverable in place.
|
|
820
|
+
const { seed, record } = await peekAppSession({ store: sessionStore });
|
|
743
821
|
if (!seed) {
|
|
744
822
|
await get().logout();
|
|
745
823
|
return;
|
|
746
824
|
}
|
|
747
|
-
const identity = await
|
|
825
|
+
const identity = await deriveIdentity({ seed });
|
|
748
826
|
const checked = await requestGrants({ identity, config: loginConfig });
|
|
827
|
+
// Continuity check: `parseGrants` only asserts the returned set is
|
|
828
|
+
// INTERNALLY consistent. Without this comparison, a reconnect
|
|
829
|
+
// returning internally-consistent grants for a DIFFERENT server or
|
|
830
|
+
// space would silently re-point the whole encrypted replica (whose
|
|
831
|
+
// database name derives from the controller DID alone) at a new
|
|
832
|
+
// storage location the user never chose. Refuse instead; switching
|
|
833
|
+
// storage is a logout + fresh login, an explicit user action.
|
|
834
|
+
if (record !== null &&
|
|
835
|
+
(checked.parsed.serverUrl !== record.serverUrl ||
|
|
836
|
+
checked.parsed.spaceId !== record.spaceId)) {
|
|
837
|
+
throw new Error(`The renewed grants point at a different storage location ` +
|
|
838
|
+
`("${checked.parsed.serverUrl}", space ` +
|
|
839
|
+
`"${checked.parsed.spaceId}") than this session's ` +
|
|
840
|
+
`("${record.serverUrl}", space "${record.spaceId}"). ` +
|
|
841
|
+
`Refusing to re-point the session; log out and log in again ` +
|
|
842
|
+
`to switch storage.`);
|
|
843
|
+
}
|
|
749
844
|
await stopController();
|
|
750
845
|
await persistAndStartSync({
|
|
751
846
|
seed,
|
|
@@ -756,14 +851,12 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
756
851
|
});
|
|
757
852
|
set({
|
|
758
853
|
status: 'connected',
|
|
759
|
-
accessExpired: false,
|
|
760
854
|
expires: checked.expires,
|
|
761
855
|
reconnecting: false
|
|
762
856
|
});
|
|
763
857
|
}
|
|
764
858
|
catch (err) {
|
|
765
|
-
|
|
766
|
-
set({ reconnecting: false, error: message });
|
|
859
|
+
set({ reconnecting: false, error: errorMessage(err) });
|
|
767
860
|
}
|
|
768
861
|
},
|
|
769
862
|
logout: async ({ wipe = false } = {}) => {
|
|
@@ -783,11 +876,10 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
783
876
|
return false;
|
|
784
877
|
}
|
|
785
878
|
try {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
879
|
+
// Counted in parallel: this runs on a login-button click, so the
|
|
880
|
+
// probe costs one round of counts rather than one per collection.
|
|
881
|
+
const counts = await Promise.all(config.collections.map(({ key }) => requireStore().countEntities(key)));
|
|
882
|
+
return counts.some(count => count > 0);
|
|
791
883
|
}
|
|
792
884
|
catch {
|
|
793
885
|
// A defensive guard: this probe runs off the serialized boot/destroy
|
|
@@ -797,11 +889,10 @@ export function createAuthStore({ config, registry, seedStore, storage }) {
|
|
|
797
889
|
// collects (through a fresh handle) whatever exists.
|
|
798
890
|
return false;
|
|
799
891
|
}
|
|
800
|
-
return false;
|
|
801
892
|
},
|
|
802
893
|
notifyAccessExpired: () => {
|
|
803
894
|
if (get().status === 'connected') {
|
|
804
|
-
set({ status: 'reconnect'
|
|
895
|
+
set({ status: 'reconnect' });
|
|
805
896
|
}
|
|
806
897
|
},
|
|
807
898
|
destroy: () => serializeLifecycle(destroyImpl)
|