@glance-apps/sync 1.5.0 → 1.5.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 +26 -0
- package/package.json +1 -1
- package/src/dbEngine.js +49 -11
- package/types/index.d.ts +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.5.1] - 2026-06-19
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Key verification fails clearly when the server can't host the verifier.**
|
|
15
|
+
1.5.0's `verifyAccountKey` did `GET /sync/{app}/__glance_keycheck`; against a
|
|
16
|
+
server that doesn't yet support the reserved id or the single-row GET, that
|
|
17
|
+
threw a raw `get row failed: 400` and aborted the whole sync with a meaningless
|
|
18
|
+
message. `verifyAccountKey` now distinguishes three outcomes: a
|
|
19
|
+
present-but-undecryptable verifier still throws `KEY_MISMATCH`; an absent
|
|
20
|
+
verifier (404) is still written; but any *other* error from the single-row GET
|
|
21
|
+
or the verifier write (HTTP 400/405/500, or a 404 on the route itself) now
|
|
22
|
+
throws a typed `VERIFIER_UNSUPPORTED` with an actionable "update your server"
|
|
23
|
+
message. The engine never silently proceeds unverified (the verifier gates push
|
|
24
|
+
and quarantine does not, so a wrong-key device with verification skipped would
|
|
25
|
+
still push poison rows), so sync pauses with a clear reason. Surfaced via
|
|
26
|
+
`config.onError(message, code)` like `KEY_MISMATCH`.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `config.allowUnverified` (default `false`): an opt-in operator escape hatch
|
|
31
|
+
that downgrades `VERIFIER_UNSUPPORTED` to a logged warning and proceeds, for
|
|
32
|
+
operators who knowingly accept the risk of syncing against a server that can't
|
|
33
|
+
host the verifier.
|
|
34
|
+
- `VERIFIER_UNSUPPORTED` standardized on the `SyncErrorCode` type.
|
|
35
|
+
|
|
10
36
|
## [1.5.0] - 2026-06-19
|
|
11
37
|
|
|
12
38
|
### Added
|
package/package.json
CHANGED
package/src/dbEngine.js
CHANGED
|
@@ -61,8 +61,9 @@ const ts = (v) => {
|
|
|
61
61
|
*
|
|
62
62
|
* Event callbacks (optional):
|
|
63
63
|
* @param {Function} [config.onStatusChange]
|
|
64
|
-
* @param {Function} [config.onError] - (message, code, isHardStop) called with code 'KEY_MISMATCH' on a wrong key
|
|
64
|
+
* @param {Function} [config.onError] - (message, code, isHardStop) called with code 'KEY_MISMATCH' on a wrong key, or 'VERIFIER_UNSUPPORTED' when the server can't host the verifier
|
|
65
65
|
* @param {Function} [config.onRowsSkipped] - (count, entityIds) called when a cycle skipped undecryptable rows
|
|
66
|
+
* @param {boolean} [config.allowUnverified=false] - operator escape hatch: downgrade VERIFIER_UNSUPPORTED to a logged warning and proceed (unsafe; off by default)
|
|
66
67
|
*/
|
|
67
68
|
export const createDbSyncEngine = (config) => {
|
|
68
69
|
const {
|
|
@@ -85,6 +86,7 @@ export const createDbSyncEngine = (config) => {
|
|
|
85
86
|
onStatusChange,
|
|
86
87
|
onError,
|
|
87
88
|
onRowsSkipped,
|
|
89
|
+
allowUnverified = false,
|
|
88
90
|
} = config;
|
|
89
91
|
|
|
90
92
|
if (!storageKeyPrefix) throw new Error('createDbSyncEngine: storageKeyPrefix is required');
|
|
@@ -233,15 +235,44 @@ export const createDbSyncEngine = (config) => {
|
|
|
233
235
|
// The signal is decryptability: AES-GCM tag validation means a wrong key
|
|
234
236
|
// simply fails, so the verifier payload's contents need not match. Runs once
|
|
235
237
|
// per session (cached via `verified`).
|
|
238
|
+
// A server that can't host the verifier (doesn't support the reserved id or
|
|
239
|
+
// the single-row GET endpoint) must fail with a CLEAR reason, not a raw
|
|
240
|
+
// "get row failed: 400". We never silently proceed unverified: the verifier
|
|
241
|
+
// gates push and quarantine does NOT, so a wrong-key device with verification
|
|
242
|
+
// skipped would still push poison rows. Pausing sync with a clear reason is
|
|
243
|
+
// the correct degradation. (config.allowUnverified is an explicit operator
|
|
244
|
+
// escape hatch, off by default, that downgrades this to a logged warning.)
|
|
245
|
+
const verifierUnsupported = (cause) => {
|
|
246
|
+
if (allowUnverified) {
|
|
247
|
+
// eslint-disable-next-line no-console
|
|
248
|
+
console.warn(`[${appId}] db sync proceeding UNVERIFIED: the server cannot host the key verifier (${KEYCHECK_ENTITY_ID}).`, cause);
|
|
249
|
+
verified = true;
|
|
250
|
+
return; // caller returns from verifyAccountKey and sync proceeds
|
|
251
|
+
}
|
|
252
|
+
const err = new Error('Your GLANCEvault server needs to be updated to support key verification (the __glance_keycheck row).');
|
|
253
|
+
err.code = 'VERIFIER_UNSUPPORTED';
|
|
254
|
+
if (cause) err.cause = cause;
|
|
255
|
+
throw err;
|
|
256
|
+
};
|
|
257
|
+
|
|
236
258
|
const verifyAccountKey = async () => {
|
|
237
259
|
if (verified) return;
|
|
238
260
|
// Verification needs a single-row fetch. Clients that don't implement getRow
|
|
239
261
|
// (e.g. minimal test stubs) cannot be verified; don't block sync on them.
|
|
240
262
|
if (typeof vault.getRow !== 'function') { verified = true; return; }
|
|
241
263
|
|
|
242
|
-
|
|
264
|
+
// Outcome 3: any error from the GET (HTTP 400/405/500, or a 404 on the route
|
|
265
|
+
// itself) means the server can't host the verifier — fail clearly.
|
|
266
|
+
let row;
|
|
267
|
+
try {
|
|
268
|
+
row = await vault.getRow(app, KEYCHECK_ENTITY_ID, accountId);
|
|
269
|
+
} catch (cause) {
|
|
270
|
+
verifierUnsupported(cause);
|
|
271
|
+
return; // only reached when downgraded via allowUnverified
|
|
272
|
+
}
|
|
273
|
+
|
|
243
274
|
if (row && !row.deleted && row.envelope) {
|
|
244
|
-
//
|
|
275
|
+
// Outcome 1: existing account — the verifier must decrypt under our key.
|
|
245
276
|
try {
|
|
246
277
|
await decryptEntity(row.envelope, KEYCHECK_ENTITY_ID);
|
|
247
278
|
verified = true;
|
|
@@ -251,15 +282,22 @@ export const createDbSyncEngine = (config) => {
|
|
|
251
282
|
throw err;
|
|
252
283
|
}
|
|
253
284
|
} else {
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
// verifiers are mutually decryptable
|
|
285
|
+
// Outcome 2: row absent (404 -> getRow returned null) — new account.
|
|
286
|
+
// Establish the verifier insert-only / first-write-wins so concurrent
|
|
287
|
+
// establishers don't clobber; since the salt is already first-write-wins,
|
|
288
|
+
// they derive the same key and their verifiers are mutually decryptable.
|
|
258
289
|
const envelope = await encryptEntity(KEYCHECK_PAYLOAD, KEYCHECK_ENTITY_ID);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
290
|
+
try {
|
|
291
|
+
await vault.batch(app, {
|
|
292
|
+
accountId,
|
|
293
|
+
rows: [{ entityId: KEYCHECK_ENTITY_ID, envelope, createdAt: Date.now(), insertOnly: true }],
|
|
294
|
+
});
|
|
295
|
+
} catch (cause) {
|
|
296
|
+
// The write itself rejecting the reserved id (400/405/…) is the same
|
|
297
|
+
// "server can't host the verifier" condition.
|
|
298
|
+
verifierUnsupported(cause);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
263
301
|
verified = true;
|
|
264
302
|
}
|
|
265
303
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -21,7 +21,10 @@ export type SyncErrorCode =
|
|
|
21
21
|
| 'KEY_MISMATCH'
|
|
22
22
|
// DB transport: a single row failed to decrypt (surfaced as a count via
|
|
23
23
|
// onRowsSkipped, never thrown).
|
|
24
|
-
| 'ROW_DECRYPT_FAILED'
|
|
24
|
+
| 'ROW_DECRYPT_FAILED'
|
|
25
|
+
// DB transport: the server can't host the key verifier (doesn't support the
|
|
26
|
+
// reserved __glance_keycheck id or the single-row endpoint).
|
|
27
|
+
| 'VERIFIER_UNSUPPORTED';
|
|
25
28
|
|
|
26
29
|
export type SyncStatus = 'idle' | 'uploading' | 'downloading' | 'success' | 'error';
|
|
27
30
|
|
|
@@ -438,10 +441,16 @@ export interface DbSyncEngineConfig {
|
|
|
438
441
|
|
|
439
442
|
// Event callbacks
|
|
440
443
|
onStatusChange?: (status: SyncStatus, hints?: { from?: SyncStatus }) => void;
|
|
441
|
-
// Called with code === 'KEY_MISMATCH' when the derived key doesn't match the
|
|
444
|
+
// Called with code === 'KEY_MISMATCH' when the derived key doesn't match the
|
|
445
|
+
// account, or 'VERIFIER_UNSUPPORTED' when the server can't host the verifier.
|
|
442
446
|
onError?: (message: string | null, code: SyncErrorCode | null, isHardStop: boolean) => void;
|
|
443
447
|
// Called once per cycle that skipped > 0 undecryptable rows (per-row quarantine).
|
|
444
448
|
onRowsSkipped?: (count: number, entityIds: string[]) => void;
|
|
449
|
+
|
|
450
|
+
// Operator escape hatch: downgrade VERIFIER_UNSUPPORTED to a logged warning
|
|
451
|
+
// and proceed without verification (unsafe — a wrong-key device could push
|
|
452
|
+
// poison rows). Off by default so the safe behavior is the default.
|
|
453
|
+
allowUnverified?: boolean;
|
|
445
454
|
}
|
|
446
455
|
|
|
447
456
|
/** Result of a DB sync cycle / pull: how many rows applied vs. quarantined. */
|