@glance-apps/sync 1.2.0 → 1.3.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 +22 -0
- package/package.json +1 -1
- package/src/dbCrypto.js +2 -2
- package/src/dbEngine.js +3 -3
- package/src/merge.js +20 -0
- package/src/vaultClient.js +1 -1
- package/types/index.d.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.0] - 2026-06-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Multi-user roster sync in `mergeSyncData`.** A `users` array is now merged
|
|
15
|
+
across devices using last-write-wins per user, keyed by `syncId` (falling back
|
|
16
|
+
to `id`) and resolved by `updatedAt`. A roster signature (id, `updatedAt`,
|
|
17
|
+
`deleted` flag, and `name`) drives the `localChanged` / `remoteChanged` dirty
|
|
18
|
+
flags so roster edits propagate. The per-device `multiUserEnabled` toggle is
|
|
19
|
+
intentionally not merged.
|
|
20
|
+
|
|
21
|
+
## [1.2.1] - 2026-06-14
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **DB sync transport: row field renamed `ciphertext` to `envelope`** to match the
|
|
26
|
+
GLANCEvault batch and list endpoints. The encrypted rows exchanged with the
|
|
27
|
+
vault are now `{ entityId, envelope, createdAt }`; the field rename is applied
|
|
28
|
+
across the push row, the pull read, the vault client and engine types, the
|
|
29
|
+
tests, and the docs. The base64 encoding is unchanged: the value is still
|
|
30
|
+
`base64(IV || AES-GCM output)` produced by `encryptEntity`.
|
|
31
|
+
|
|
10
32
|
## [1.2.0] - 2026-06-13
|
|
11
33
|
|
|
12
34
|
### Added
|
package/package.json
CHANGED
package/src/dbCrypto.js
CHANGED
|
@@ -257,7 +257,7 @@ export async function clearDbRootKey({ cryptoDBName, nativeGetSyncKey = null, na
|
|
|
257
257
|
|
|
258
258
|
/**
|
|
259
259
|
* Encrypts one entity into a base64 ciphertext string for the row's
|
|
260
|
-
* `
|
|
260
|
+
* `envelope` field. The output encodes [ 12-byte IV ][ AES-GCM ciphertext ].
|
|
261
261
|
*
|
|
262
262
|
* @param {*} entity - plain JS value to encrypt (serialised with JSON)
|
|
263
263
|
* @param {string} entityId - stable entity UUID, used as HKDF context
|
|
@@ -280,7 +280,7 @@ export async function encryptEntity(entity, entityId, rootKey = _rootKey) {
|
|
|
280
280
|
/**
|
|
281
281
|
* Decrypts a base64 ciphertext produced by encryptEntity back into the entity.
|
|
282
282
|
*
|
|
283
|
-
* @param {string} ciphertextB64 - the row's `
|
|
283
|
+
* @param {string} ciphertextB64 - the row's `envelope` field
|
|
284
284
|
* @param {string} entityId - stable entity UUID, used as HKDF context
|
|
285
285
|
* @param {CryptoKey} [rootKey] - explicit root key (defaults to the session key)
|
|
286
286
|
* @returns {Promise<*>} the decrypted entity
|
package/src/dbEngine.js
CHANGED
|
@@ -202,8 +202,8 @@ export const createDbSyncEngine = (config) => {
|
|
|
202
202
|
if (entity == null) {
|
|
203
203
|
deletes.push(entityId);
|
|
204
204
|
} else {
|
|
205
|
-
const
|
|
206
|
-
upserts.push({ entityId,
|
|
205
|
+
const envelope = await encryptEntity(entity, entityId);
|
|
206
|
+
upserts.push({ entityId, envelope, createdAt: Date.now() });
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -256,7 +256,7 @@ export const createDbSyncEngine = (config) => {
|
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
await ensureRootKey();
|
|
259
|
-
const remoteEntity = await decryptEntity(R.
|
|
259
|
+
const remoteEntity = await decryptEntity(R.envelope, R.entityId);
|
|
260
260
|
const local = await getLocalEntity(R.entityId);
|
|
261
261
|
|
|
262
262
|
if (local == null) {
|
package/src/merge.js
CHANGED
|
@@ -832,6 +832,25 @@ export const mergeSyncData = (localData, remoteData, retentionDays = 90) => {
|
|
|
832
832
|
const prunedDeletedGoalIds = pruneTombstones(allDeletedGoalIds, tombstoneCutoff);
|
|
833
833
|
const prunedDeletedProjectIds = pruneTombstones(allDeletedProjectIds, tombstoneCutoff);
|
|
834
834
|
|
|
835
|
+
// ── Multi-user roster: last-write-wins per user by updatedAt (keyed by syncId).
|
|
836
|
+
// NOTE: multiUserEnabled is intentionally NOT merged — it's a per-device toggle.
|
|
837
|
+
const localUsers = Array.isArray(localData.users) ? localData.users : [];
|
|
838
|
+
const remoteUsers = Array.isArray(remoteData.users) ? remoteData.users : [];
|
|
839
|
+
const usersById = new Map(localUsers.map(u => [u.syncId ?? u.id, u]));
|
|
840
|
+
for (const u of remoteUsers) {
|
|
841
|
+
const k = u.syncId ?? u.id;
|
|
842
|
+
const prev = usersById.get(k);
|
|
843
|
+
if (!prev || (u.updatedAt || '') > (prev.updatedAt || '')) usersById.set(k, u);
|
|
844
|
+
}
|
|
845
|
+
const mergedUsers = [...usersById.values()];
|
|
846
|
+
|
|
847
|
+
const rosterSig = (arr) => arr
|
|
848
|
+
.map(u => `${u.syncId ?? u.id}:${u.updatedAt || ''}:${u.deleted ? 1 : 0}:${u.name || ''}`)
|
|
849
|
+
.sort().join('|');
|
|
850
|
+
const mergedSig = rosterSig(mergedUsers);
|
|
851
|
+
if (mergedSig !== rosterSig(localUsers)) localChanged = true;
|
|
852
|
+
if (mergedSig !== rosterSig(remoteUsers)) remoteChanged = true;
|
|
853
|
+
|
|
835
854
|
return {
|
|
836
855
|
data: {
|
|
837
856
|
tasks: finalTasks,
|
|
@@ -877,6 +896,7 @@ export const mergeSyncData = (localData, remoteData, retentionDays = 90) => {
|
|
|
877
896
|
minimizedSections: localData.minimizedSections, // UI pref — keep local
|
|
878
897
|
use24HourClock: localData.use24HourClock, // device pref — keep local
|
|
879
898
|
tombstonePrunedBefore: mergedTombstonePrunedBefore,
|
|
899
|
+
users: mergedUsers,
|
|
880
900
|
},
|
|
881
901
|
localChanged,
|
|
882
902
|
remoteChanged
|
package/src/vaultClient.js
CHANGED
|
@@ -75,7 +75,7 @@ export function createVaultClient({ vaultUrl, vaultToken, fetchImpl } = {}) {
|
|
|
75
75
|
|
|
76
76
|
return {
|
|
77
77
|
/**
|
|
78
|
-
* Upserts a batch of rows. Each row is { entityId,
|
|
78
|
+
* Upserts a batch of rows. Each row is { entityId, envelope, createdAt }.
|
|
79
79
|
* @returns {Promise<{ written: number, maxSeq: number }>}
|
|
80
80
|
*/
|
|
81
81
|
async batch(app, { accountId, rows }) {
|
package/types/index.d.ts
CHANGED
|
@@ -366,10 +366,10 @@ export function hasDbRootKey(): boolean;
|
|
|
366
366
|
export function encryptEntity(entity: unknown, entityId: string, rootKey?: CryptoKey): Promise<string>;
|
|
367
367
|
export function decryptEntity<T = unknown>(ciphertext: string, entityId: string, rootKey?: CryptoKey): Promise<T>;
|
|
368
368
|
|
|
369
|
-
// Row exchanged with GLANCEvault.
|
|
369
|
+
// Row exchanged with GLANCEvault. envelope is base64(IV || AES-GCM output).
|
|
370
370
|
export interface VaultRow {
|
|
371
371
|
entityId: string;
|
|
372
|
-
|
|
372
|
+
envelope: string;
|
|
373
373
|
createdAt: number;
|
|
374
374
|
}
|
|
375
375
|
|
|
@@ -377,7 +377,7 @@ export interface VaultPulledRow extends Partial<VaultRow> {
|
|
|
377
377
|
entityId: string;
|
|
378
378
|
seq: number;
|
|
379
379
|
deleted?: boolean;
|
|
380
|
-
|
|
380
|
+
envelope?: string;
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
export interface VaultClient {
|