@glance-apps/sync 1.0.2 → 1.1.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/CHANGELOG.md CHANGED
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.0] - 2026-05-25
11
+
12
+ ### Added
13
+
14
+ - `deriveKeyForSalt(salt: Uint8Array): Promise<CryptoKey>` — derives a fresh
15
+ non-extractable AES-256-GCM key from the cached passphrase and a caller-supplied
16
+ salt (PBKDF2-SHA-256, 310 000 iterations). Intended as the `deriveKey` callback
17
+ for `@glance-apps/intents` per-envelope key derivation (Phase 2.6): the intents
18
+ emitter generates a random salt per envelope, embeds it in the envelope, and
19
+ calls this to obtain the encryption key; the poller extracts the salt from the
20
+ envelope and calls this again to obtain the decryption key — enabling cross-app
21
+ decryption without sharing a per-app session key.
22
+
23
+ Throws with `err.code === 'PASSPHRASE_REQUIRED'` when no passphrase is held in
24
+ the current session. Note that this guard is on `_sessionPassphrase`, **not** on
25
+ `hasEncryptionReady()`: after `initSessionKey()` restores a key from device
26
+ storage the session key is ready but the passphrase is not. Callers that want to
27
+ gate on passphrase availability should check `getSyncPassphrase() !== null`.
28
+
29
+ `getSessionKey()` (added in 1.0.2) is **not removed**; it remains exported for
30
+ any consumer that wants the cached per-app `CryptoKey` directly.
31
+
32
+ ## [1.0.3] - 2026-05-23
33
+
34
+ ### Fixed
35
+
36
+ - **Generic WebDAV — `appFolderName` not used in URLs**: `getFileUrl` and
37
+ `getDirUrl` now insert `appFolderName` between the server root and the sync
38
+ filename, matching the behaviour of the Nextcloud and Koofr providers.
39
+ `webdavUrl` is now expected to be a server root (e.g. `https://dav.example.com`)
40
+ rather than a full folder path. Callers with an existing `webdavUrl` that
41
+ already includes the folder path should migrate it into `syncFolder`/`appFolderName`
42
+ and strip the path back to the origin.
43
+ - **Generic WebDAV — Apache 403 on missing parent directories**: `upload` now
44
+ calls `mkcolWithParents` when PUT returns 403 in addition to 404/409. Apache
45
+ `mod_dav` returns 403 (not 409) when both the target directory and its parent
46
+ are absent. The `if (res.status === 403) throw FORBIDDEN` guard that follows
47
+ still catches genuine auth failures after the retry.
48
+
10
49
  ## [1.0.2] - 2026-05-22
11
50
 
12
51
  ### Added
@@ -58,5 +97,8 @@ entire sync infrastructure can be shared across the GLANCE app family.
58
97
  - TypeScript declarations in `types/index.d.ts`.
59
98
  - WebDAV CORS proxy handler in `api/webdav-proxy.js`.
60
99
 
61
- [Unreleased]: https://github.com/glance-apps/glance-sync/compare/v1.0.0...HEAD
100
+ [Unreleased]: https://github.com/glance-apps/glance-sync/compare/v1.0.3...HEAD
101
+ [1.0.3]: https://github.com/glance-apps/glance-sync/compare/v1.0.2...v1.0.3
102
+ [1.0.2]: https://github.com/glance-apps/glance-sync/compare/v1.0.1...v1.0.2
103
+ [1.0.1]: https://github.com/glance-apps/glance-sync/compare/v1.0.0...v1.0.1
62
104
  [1.0.0]: https://github.com/glance-apps/glance-sync/releases/tag/v1.0.0
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
 
@@ -169,7 +170,7 @@ TypeScript declarations for all of the above are in `types/index.d.ts`.
169
170
 
170
171
  ## Versioning
171
172
 
172
- This package is at **v1.0.0** and follows [Semantic Versioning](https://semver.org/). Breaking changes to the public API or the sync envelope schema will result in a major version bump.
173
+ This package follows [Semantic Versioning](https://semver.org/). Breaking changes to the public API or the sync envelope schema will result in a major version bump.
173
174
 
174
175
  ## License
175
176
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glance-apps/sync",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
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
@@ -261,9 +261,9 @@ export function createProviders(engineConfig) {
261
261
  webdav: {
262
262
  name: 'Generic WebDAV',
263
263
  getFileUrl: (config) =>
264
- `${config.webdavUrl.replace(/\/+$/, '')}/${syncFilename}`,
264
+ `${config.webdavUrl.replace(/\/+$/, '')}/${appFolderName}/${syncFilename}`,
265
265
  getDirUrl: (config) =>
266
- `${config.webdavUrl.replace(/\/+$/, '')}/`,
266
+ `${config.webdavUrl.replace(/\/+$/, '')}/${appFolderName}/`,
267
267
  getAuthHeaders: (config) => ({
268
268
  'X-WebDAV-Auth': 'Basic ' + toBase64(config.username + ':' + config.appPassword)
269
269
  }),
@@ -277,7 +277,7 @@ export function createProviders(engineConfig) {
277
277
  if (etag) extraHeaders['If-Match'] = etag;
278
278
  const doUpload = () => wf('PUT', fileUrl, authHeaders, body, extraHeaders);
279
279
  let res = await doUpload();
280
- if (res.status === 404 || res.status === 409) {
280
+ if (res.status === 404 || res.status === 409 || res.status === 403) {
281
281
  await mkcol(dirUrl, authHeaders);
282
282
  res = await doUpload();
283
283
  }
@@ -307,11 +307,11 @@ export function createProviders(engineConfig) {
307
307
  return { success: false, error: `Unexpected response: ${res.status}${res.statusText ? ' ' + res.statusText : ''}` };
308
308
  },
309
309
  configFields: [
310
- { key: 'webdavUrl', label: 'WebDAV URL', type: 'url', placeholder: 'https://dav.example.com/dayGLANCE/' },
310
+ { key: 'webdavUrl', label: 'WebDAV URL', type: 'url', placeholder: 'https://dav.example.com' },
311
311
  { key: 'username', label: 'Username', type: 'text', placeholder: 'your-username' },
312
312
  { key: 'appPassword', label: 'Password / App Password', type: 'password', placeholder: 'your-password' }
313
313
  ],
314
- helpText: 'Enter the full URL of the WebDAV folder where dayGLANCE should store its sync file. pCloud, Seafile, and most self-hosted WebDAV providers are supported.'
314
+ helpText: 'Enter the server root URL (e.g. https://dav.example.com). The sync folder is configured separately via the Sync Folder setting. pCloud, Seafile, and most self-hosted WebDAV providers are supported.'
315
315
  }
316
316
  };
317
317
  }
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;