selfstore 1.3.0 → 1.5.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 CHANGED
@@ -134,7 +134,7 @@ too, as subpaths of the same install:
134
134
  |---|---|---|
135
135
  | `selfstore` | the simple store + backup files | almost always: start here |
136
136
  | `selfstore/flows` | the user journeys (connect a destination, share panel, join an invitation) as headless, tested state machines | you are building the connect/share/join UI and want the ordering and failure rules - one popup per gesture, password proven before attach, merge by default - already right |
137
- | `selfstore/widgets` | the same journeys as drop-in web components (`<selfstore-connect>`, `<selfstore-share>`, `<selfstore-join>`) - framework-free, themable via CSS custom properties and `::part()`, reworded/localized via a `labels` map | you want the screens ready-made and your app's look on top |
137
+ | `selfstore/widgets` | the same journeys as drop-in web components (`<selfstore-connect>`, `<selfstore-gate>`, `<selfstore-share>`, `<selfstore-join>`) - framework-free, themable via CSS custom properties and `::part()`, reworded/localized via a `labels` map | you want the screens ready-made and your app's look on top |
138
138
  | `selfstore/advanced` | the pull-model store (`createLocalStore`), custom `BackupTarget`s, caches, headless status derivation, functional codec | your state lives in its own reactive model (Svelte runes, Redux), or you are writing a destination (S3, your KV...) |
139
139
  | `selfstore/groups` | passwordless group encryption: per-member identities, sealed envelopes, signed membership manifests | several PEOPLE share one encrypted store without a shared password ([PEERS.md](./PEERS.md)) |
140
140
  | `selfstore/sync` | the bare merge engine (HLC + per-collection strategies, no CRDT runtime) | you only want the algorithm inside your own persistence ([SYNC docs in SPEC.md](./SPEC.md)) |
@@ -306,6 +306,12 @@ const persistence = {
306
306
  full-profile copy cannot read it. One unlock per session - `cacheLock` on the
307
307
  simple facade, or `indexedDbCache(name, { lock: true })` at the store layer.
308
308
  Still bounded by the origin (unlocked, code in the page decrypts).
309
+ A forgotten secret is unrecoverable by design, so give your users a way
310
+ out rather than a dead-end prompt: `indexedDbCache(name, { lock: true })
311
+ .clear()` needs no key and abandons the sealed cache, after which they can
312
+ start again from a backup file or from scratch. Throwing from the
313
+ `cacheLock` callback aborts the boot and hands control back to you, which
314
+ is where that offer belongs.
309
315
  - Opt-in hardening for sensitive deployments: `requireEncryption` refuses to
310
316
  write or export a plaintext backup, and `passwordPolicy` refuses a password
311
317
  weaker than a length / character-class rule - both enforced at the store, so
@@ -1,10 +1,10 @@
1
- import { B as BackupTarget } from './store-2RnED1Vq.js';
2
- export { A as AuthExpiredError, a as BuiltinTargetKind, E as EncodeOptions, H as Header, K as KdfParams, L as LocalStore, b as LocalStoreOptions, c as LocalStoreState, M as MirrorState, d as Mode, P as PasswordCheck, e as PasswordPolicy, f as PasswordRequirement, g as PeerSource, h as PeerState, R as RESERVED_COLLECTION_PREFIX, i as RESERVED_STORE_MODES, j as RecipientStanza, k as ReplicaState, S as SelfstoreError, l as SelfstoreErrorCode, m as Severity, n as Snapshot, o as SnapshotFile, p as StatusAction, q as StatusDescriptor, r as StatusInput, s as StorageState, t as StoreError, u as StoreGroupConfig, v as SyncJournalEntry, w as SyncSource, T as TargetKind, x as checkPasswordPolicy, y as createLocalStore, z as deriveStatus, C as errorLabelKey, D as isAuthExpired, F as isReservedStoreMode, G as isSelfstoreError } from './store-2RnED1Vq.js';
3
- import { K as KV } from './cache-De5_sALA.js';
4
- export { C as CachedFile, L as LocalCache, e as LockableCache, i as indexedDbCache, f as isLockableCache, m as memoryCache } from './cache-De5_sALA.js';
5
- export { e as exportSnapshot, g as gisDriveAuth, i as importSnapshot, a as inspect, b as isEncrypted, p as pickFromDisk, s as saveToDisk } from './local-DsXG7QW7.js';
6
- import { c as connect$1, f as fromSession$1, p as preview, a as adopt, b as findOrCreateOwnFile, l as listBackups, d as createBackup, e as deleteBackup, r as renameBackup, g as connect$2, h as fromSession$2, i as connect$3, j as fromSession$3, k as peer } from './drive-DNiDXBDe.js';
7
- export { D as DriveAuth, m as DriveBackupInfo, n as DriveOptions, S as S3Config, o as S3ConnectOptions, W as WebdavConfig, q as WebdavConnectOptions, s as WebdavPeerOptions } from './drive-DNiDXBDe.js';
1
+ import { B as BackupTarget } from './store-C20_6JBo.js';
2
+ export { A as AuthExpiredError, a as BuiltinTargetKind, E as EncodeOptions, H as Header, K as KdfParams, L as LocalStore, b as LocalStoreOptions, c as LocalStoreState, M as MirrorState, d as Mode, P as PasswordCheck, e as PasswordPolicy, f as PasswordRequirement, g as PeerSource, h as PeerState, R as RESERVED_COLLECTION_PREFIX, i as RESERVED_STORE_MODES, j as RecipientStanza, k as ReplicaState, S as SelfstoreError, l as SelfstoreErrorCode, m as Severity, n as Snapshot, o as SnapshotFile, p as StatusAction, q as StatusDescriptor, r as StatusInput, s as StorageState, t as StoreError, u as StoreGroupConfig, v as SyncJournalEntry, w as SyncSource, T as TargetKind, x as checkPasswordPolicy, y as createLocalStore, z as deriveStatus, C as errorLabelKey, D as isAuthExpired, F as isReservedStoreMode, G as isSelfstoreError } from './store-C20_6JBo.js';
3
+ import { K as KV } from './cache-ilsK3Tki.js';
4
+ export { C as CachedFile, L as LocalCache, e as LockableCache, i as indexedDbCache, f as isLockableCache, m as memoryCache } from './cache-ilsK3Tki.js';
5
+ export { e as exportSnapshot, g as gisDriveAuth, i as importSnapshot, a as inspect, b as isEncrypted, p as pickFromDisk, s as saveToDisk } from './local-6OALceUy.js';
6
+ import { c as connect$1, f as fromSession$1, p as preview, a as adopt, b as findOrCreateOwnFile, l as listBackups, d as createBackup, e as deleteBackup, r as renameBackup, g as connect$2, h as fromSession$2, i as connect$3, j as fromSession$3, k as peer } from './drive-BhiTdk-Q.js';
7
+ export { D as DriveAuth, m as DriveBackupInfo, n as DriveOptions, S as S3Config, o as S3ConnectOptions, W as WebdavConfig, q as WebdavConnectOptions, s as WebdavPeerOptions } from './drive-BhiTdk-Q.js';
8
8
  import './sync.js';
9
9
 
10
10
  /**
package/dist/advanced.js CHANGED
@@ -1,4 +1,4 @@
1
- export { RESERVED_STORE_MODES, createLocalStore, deriveStatus, gisDriveAuth, indexedDbCache, isLockableCache, isReservedStoreMode, memoryCache } from './chunk-XI3MGK4M.js';
1
+ export { RESERVED_STORE_MODES, createLocalStore, deriveStatus, gisDriveAuth, indexedDbCache, isLockableCache, isReservedStoreMode, memoryCache } from './chunk-FL66ACWR.js';
2
2
  export { pickFromDisk, saveToDisk } from './chunk-UWVAKBU4.js';
3
3
  import './chunk-SKWHKM25.js';
4
4
  import { FILE_ID_KEY, renameBackup, deleteBackup, createBackup, listBackups, findOrCreateOwnFile, adopt, preview, fromSession, connect, isSupported, fromSession2, connect2, fromSession4, connect4, peer, fromSession3, connect3 } from './chunk-TEIAFKFS.js';
package/dist/backups.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { K as KV } from './cache-De5_sALA.js';
2
- import { B as BackupTarget, L as LocalStore } from './store-2RnED1Vq.js';
1
+ import { K as KV } from './cache-ilsK3Tki.js';
2
+ import { B as BackupTarget, L as LocalStore } from './store-C20_6JBo.js';
3
3
  import { F as FlowStore } from './machine-ALMkT3mk.js';
4
4
  import './sync.js';
5
5
 
@@ -107,6 +107,17 @@ interface LockableCache extends LocalCache {
107
107
  * does not open existing data - the cache stays locked. The first secret on
108
108
  * an empty cache sets the lock. */
109
109
  unlock(secret: string | CryptoKey): Promise<boolean>;
110
+ /**
111
+ * Change the secret that seals this cache, in place: re-derive the key (a
112
+ * fresh salt for a password) and rewrite every sealed record under it.
113
+ *
114
+ * Without this, "change my password" has no honest implementation for a
115
+ * locked cache - the app has to wipe and rebuild, which also throws away
116
+ * the durable destination's session, and a half-finished wipe leaves data
117
+ * no key can open. Requires the cache to be unlocked: what cannot be read
118
+ * cannot be re-sealed.
119
+ */
120
+ reseal(secret: string | CryptoKey): Promise<void>;
110
121
  /** Drop the in-memory key: reads and writes lock until unlock() is called. */
111
122
  lockNow(): void;
112
123
  /** Whether no seal key is currently held. */
@@ -1886,6 +1886,40 @@ function indexedDbCache(name, opts) {
1886
1886
  lockNow() {
1887
1887
  sealKey = null;
1888
1888
  },
1889
+ async reseal(secret) {
1890
+ const { keyFromPassword, freshCacheKdf } = await import('./cache-lock-NXIR6JRB.js');
1891
+ const previous = sealKey;
1892
+ if (previous === null) {
1893
+ throw new SelfstoreError(
1894
+ "PASSWORD_REQUIRED",
1895
+ "reseal(): the local cache is locked - unlock() it first."
1896
+ );
1897
+ }
1898
+ const d = await db();
1899
+ const kdf = typeof secret === "string" ? freshCacheKdf() : null;
1900
+ const next = kdf ? await keyFromPassword(secret, kdf) : secret;
1901
+ const rawCollections = await d.get("kv", COLLECTIONS_KEY);
1902
+ const collections = isEnvelope(rawCollections) ? await seal(next, await unseal(previous, rawCollections)) : null;
1903
+ const files = [];
1904
+ for (const fileKey of await d.getAllKeys("files")) {
1905
+ const v = await d.get("files", fileKey);
1906
+ if (!v) continue;
1907
+ const bytes = await unseal(previous, v);
1908
+ files.push({
1909
+ id: fileKey,
1910
+ rec: { name: v.name, mime: v.mime, ...await seal(next, bytes) }
1911
+ });
1912
+ }
1913
+ const tx = d.transaction(["kv", "files"], "readwrite");
1914
+ const store = tx.objectStore("kv");
1915
+ if (kdf) store.put(kdf, CACHE_KDF);
1916
+ else store.delete(CACHE_KDF);
1917
+ if (collections) store.put(collections, COLLECTIONS_KEY);
1918
+ const fileStore = tx.objectStore("files");
1919
+ for (const { id, rec } of files) fileStore.put(rec, id);
1920
+ await tx.done;
1921
+ sealKey = next;
1922
+ },
1889
1923
  async unlock(secret) {
1890
1924
  const { keyFromPassword, freshCacheKdf, opens } = await import('./cache-lock-NXIR6JRB.js');
1891
1925
  const d = await db();
@@ -1994,5 +2028,5 @@ function gisDriveAuth(opts) {
1994
2028
  }
1995
2029
 
1996
2030
  export { RESERVED_STORE_MODES, createLocalStore, deriveStatus, gisDriveAuth, indexedDbCache, isLockableCache, isReservedStoreMode, memoryCache };
1997
- //# sourceMappingURL=chunk-XI3MGK4M.js.map
1998
- //# sourceMappingURL=chunk-XI3MGK4M.js.map
2031
+ //# sourceMappingURL=chunk-FL66ACWR.js.map
2032
+ //# sourceMappingURL=chunk-FL66ACWR.js.map