release(v4.3.0): browser persistence via @shade/storage-indexeddb

Ship an official IndexedDB-backed StorageProvider so browser-based Shade
consumers persist identity, prekeys, sessions, retired identities,
peer-verification state and stream-resume rows across tab refresh and
browser restart. Closes the gap that forced browser apps onto
storage:"memory" (regenerated identity each load, orphaned device
records server-side).

- New package @shade/storage-indexeddb (4.3.0): full StorageProvider
  conformance, schema v1, idb-backed; bumpPeerIdentityVersion is wrapped
  in a single readwrite IDB transaction (atomic, vs SQLite's
  read-then-upsert race).
- @shade/sdk resolveStorage() accepts { type: 'indexeddb', dbName? } via
  dynamic import (lazy, optional dep — same pattern as
  @shade/storage-postgres). Named StorageSpec type now reused by
  ResolvedConfig.
- Tests: 16 new tests in shade-storage-indexeddb (StorageProvider
  surface + peer-verifications + full E2EE conversation surviving a
  simulated tab reload). Run on fake-indexeddb.
- Lockstep version bump 4.2.1 → 4.3.0 across all 25 packages.
- Publish scripts updated to include the new package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 17:35:02 +02:00
parent b77b7e771c
commit f5f42fe557
37 changed files with 1167 additions and 54 deletions

View File

@@ -44,7 +44,7 @@ import {
backupFromString,
} from './backup.js';
import { computeFingerprint, deserializeIdentityKeyPair } from '@shade/core';
import type { ResolvedConfig } from './config.js';
import type { ResolvedConfig, StorageSpec } from './config.js';
import {
ShadeControlChannel,
ShadeTransferAuthenticator,
@@ -1484,9 +1484,7 @@ class HttpEnvelopeTransport implements ControlEnvelopeTransport {
// ─── Helpers ─────────────────────────────────────────────────
async function resolveStorage(
spec: string | StorageProvider | { type: 'postgres'; url: string },
): Promise<StorageProvider> {
async function resolveStorage(spec: StorageSpec): Promise<StorageProvider> {
if (typeof spec === 'object' && 'getIdentityKeyPair' in spec) {
return spec;
}
@@ -1512,6 +1510,18 @@ async function resolveStorage(
return mod.PostgresStorage.create(spec.url);
}
if (typeof spec === 'object' && spec.type === 'indexeddb') {
// Dynamic import keeps @shade/storage-indexeddb optional — Node-only
// consumers don't need to install a browser-only adapter.
const moduleId = '@shade/storage-indexeddb';
const mod = (await import(moduleId)) as {
IndexedDBStorage: { create(opts: { dbName?: string }): Promise<StorageProvider> };
};
const opts: { dbName?: string } = {};
if (spec.dbName !== undefined) opts.dbName = spec.dbName;
return mod.IndexedDBStorage.create(opts);
}
throw new Error(`Unsupported storage spec: ${JSON.stringify(spec)}`);
}