@glance-apps/sync 1.0.3 → 1.1.1

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/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.1] - 2026-06-03
11
+
12
+ ### Fixed
13
+
14
+ - **WebDAV — 405 on MKCOL treated as directory-already-exists**: `mkcolWithParents`
15
+ now handles HTTP 405 (Method Not Allowed) the same as 409/404, triggering the
16
+ parent-directory creation fallback. Some WebDAV servers (e.g. certain nginx
17
+ configurations) return 405 rather than 409 when MKCOL is called on a path that
18
+ already exists as a collection.
19
+
20
+ ## [1.1.0] - 2026-05-25
21
+
22
+ ### Added
23
+
24
+ - `deriveKeyForSalt(salt: Uint8Array): Promise<CryptoKey>` — derives a fresh
25
+ non-extractable AES-256-GCM key from the cached passphrase and a caller-supplied
26
+ salt (PBKDF2-SHA-256, 310 000 iterations). Intended as the `deriveKey` callback
27
+ for `@glance-apps/intents` per-envelope key derivation (Phase 2.6): the intents
28
+ emitter generates a random salt per envelope, embeds it in the envelope, and
29
+ calls this to obtain the encryption key; the poller extracts the salt from the
30
+ envelope and calls this again to obtain the decryption key — enabling cross-app
31
+ decryption without sharing a per-app session key.
32
+
33
+ Throws with `err.code === 'PASSPHRASE_REQUIRED'` when no passphrase is held in
34
+ the current session. Note that this guard is on `_sessionPassphrase`, **not** on
35
+ `hasEncryptionReady()`: after `initSessionKey()` restores a key from device
36
+ storage the session key is ready but the passphrase is not. Callers that want to
37
+ gate on passphrase availability should check `getSyncPassphrase() !== null`.
38
+
39
+ `getSessionKey()` (added in 1.0.2) is **not removed**; it remains exported for
40
+ any consumer that wants the cached per-app `CryptoKey` directly.
41
+
10
42
  ## [1.0.3] - 2026-05-23
11
43
 
12
44
  ### Fixed
@@ -75,7 +107,9 @@ entire sync infrastructure can be shared across the GLANCE app family.
75
107
  - TypeScript declarations in `types/index.d.ts`.
76
108
  - WebDAV CORS proxy handler in `api/webdav-proxy.js`.
77
109
 
78
- [Unreleased]: https://github.com/glance-apps/glance-sync/compare/v1.0.3...HEAD
110
+ [Unreleased]: https://github.com/glance-apps/glance-sync/compare/v1.1.1...HEAD
111
+ [1.1.1]: https://github.com/glance-apps/glance-sync/compare/v1.1.0...v1.1.1
112
+ [1.1.0]: https://github.com/glance-apps/glance-sync/compare/v1.0.3...v1.1.0
79
113
  [1.0.3]: https://github.com/glance-apps/glance-sync/compare/v1.0.2...v1.0.3
80
114
  [1.0.2]: https://github.com/glance-apps/glance-sync/compare/v1.0.1...v1.0.2
81
115
  [1.0.1]: https://github.com/glance-apps/glance-sync/compare/v1.0.0...v1.0.1
package/README.md CHANGED
@@ -147,7 +147,8 @@ Pass `encryptionEnabled: true` in the sync config object stored via `engine.setC
147
147
  | `isEncryptedEnvelope(value)` | Type guard — returns `true` if the value looks like an `EncryptedEnvelope`. |
148
148
  | `setSyncPassphrase(p)` / `getSyncPassphrase()` | Store/retrieve the passphrase in session memory (not persisted). |
149
149
  | `hasEncryptionReady()` | Returns `true` if the session key is loaded. |
150
- | `getSessionKey()` | Returns the cached non-extractable `CryptoKey`, or `null` if no key is loaded — intended for sibling packages (e.g., `@glance-apps/intents`) that need to reuse the same key. |
150
+ | `getSessionKey()` | Returns the cached non-extractable `CryptoKey`, or `null` if no key is loaded. |
151
+ | `deriveKeyForSalt(salt)` | Derives a fresh non-extractable AES-256-GCM key from the cached passphrase and a caller-supplied 16-byte salt (PBKDF2-SHA-256, 310 000 iterations). Intended as the `deriveKey` callback for `@glance-apps/intents` per-envelope encryption: pass `sync.deriveKeyForSalt` to `buildEncryptedEnvelope` / `parseEncryptedEnvelope` so each envelope is independently rekeyed. Throws with `err.code === 'PASSPHRASE_REQUIRED'` when no passphrase is in session — gate on `getSyncPassphrase() !== null` before using it. |
151
152
 
152
153
  ### Transport & providers
153
154
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glance-apps/sync",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/crypto.js CHANGED
@@ -19,6 +19,45 @@ export function getSyncPassphrase() { return _sessionPassphrase; }
19
19
  export function hasEncryptionReady() { return _sessionKey !== null; }
20
20
  export function getSessionKey() { return _sessionKey; }
21
21
 
22
+ /**
23
+ * Derives a fresh AES-256-GCM key from the cached passphrase and the supplied
24
+ * salt. Intended for per-envelope key derivation in `@glance-apps/intents`:
25
+ * callers generate a new random salt per envelope, embed it in the envelope,
26
+ * and call this to get the encryption key — enabling cross-app decryption with
27
+ * the same passphrase but a fresh salt on every write.
28
+ *
29
+ * Uses identical PBKDF2 parameters as the rest of this module (SHA-256,
30
+ * 310 000 iterations, AES-256-GCM). The returned key is non-extractable because
31
+ * it is ephemeral and never stored.
32
+ *
33
+ * Throws with `err.code === 'PASSPHRASE_REQUIRED'` when no passphrase is
34
+ * held in the current session (e.g. after `initSessionKey()` restores from
35
+ * device storage without the user re-entering their passphrase). Note: this
36
+ * is a different condition from `hasEncryptionReady()` — callers that want to
37
+ * gate on passphrase availability should check `getSyncPassphrase() !== null`.
38
+ *
39
+ * @param {Uint8Array} salt - 16-byte salt to derive the key from
40
+ * @returns {Promise<CryptoKey>}
41
+ */
42
+ export async function deriveKeyForSalt(salt) {
43
+ if (!_sessionPassphrase) {
44
+ const err = new Error('Encryption passphrase not available. Please enter your sync passphrase.');
45
+ err.code = 'PASSPHRASE_REQUIRED';
46
+ throw err;
47
+ }
48
+ const enc = new TextEncoder();
49
+ const keyMaterial = await crypto.subtle.importKey(
50
+ 'raw', enc.encode(_sessionPassphrase), 'PBKDF2', false, ['deriveKey']
51
+ );
52
+ return crypto.subtle.deriveKey(
53
+ { name: 'PBKDF2', salt, iterations: 310_000, hash: 'SHA-256' },
54
+ keyMaterial,
55
+ { name: 'AES-GCM', length: 256 },
56
+ false,
57
+ ['encrypt', 'decrypt']
58
+ );
59
+ }
60
+
22
61
  // ---------------------------------------------------------------------------
23
62
  // IndexedDB key store
24
63
  // ---------------------------------------------------------------------------
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  clearEncryptionKey,
18
18
  hasEncryptionReady,
19
19
  getSessionKey,
20
+ deriveKeyForSalt,
20
21
  isEncryptedEnvelope,
21
22
  initSessionKey,
22
23
  } from './crypto.js';
package/src/providers.js CHANGED
@@ -102,7 +102,7 @@ async function parseDownloadResponse(res, engineConfig) {
102
102
 
103
103
  async function mkcolWithParents(dirUrl, authHeaders, wf) {
104
104
  const res = await wf('MKCOL', dirUrl, authHeaders);
105
- if (res.status === 409 || res.status === 404) {
105
+ if (res.status === 409 || res.status === 404 || res.status === 405) {
106
106
  // Derive parent by stripping the last path segment via URL() to avoid
107
107
  // producing malformed strings on root-level inputs.
108
108
  let parent;
package/types/index.d.ts CHANGED
@@ -156,6 +156,17 @@ export function setSyncPassphrase(passphrase: string | null): void;
156
156
  export function getSyncPassphrase(): string | null;
157
157
  export function hasEncryptionReady(): boolean;
158
158
  export function getSessionKey(): CryptoKey | null;
159
+ /**
160
+ * Derives a fresh non-extractable AES-256-GCM key from the cached passphrase
161
+ * and the supplied salt (PBKDF2-SHA-256, 310 000 iterations). Intended for
162
+ * per-envelope key derivation in `@glance-apps/intents`: pass this function as
163
+ * the `deriveKey` callback to `buildEncryptedEnvelope` / `parseEncryptedEnvelope`.
164
+ *
165
+ * Throws with `err.code === 'PASSPHRASE_REQUIRED'` when no passphrase is held
166
+ * in the current session. Gate on `getSyncPassphrase() !== null` (not
167
+ * `hasEncryptionReady()`) before passing this as a callback.
168
+ */
169
+ export function deriveKeyForSalt(salt: Uint8Array): Promise<CryptoKey>;
159
170
  export function encryptData<T>(data: T, config?: CryptoConfig): Promise<EncryptedEnvelope>;
160
171
  export function decryptData<T = unknown>(envelope: EncryptedEnvelope, config?: CryptoConfig): Promise<T>;
161
172
  export function isEncryptedEnvelope(value: unknown): value is EncryptedEnvelope;