@glance-apps/sync 1.5.1 → 1.5.2

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,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.5.2] - 2026-06-19
11
+
12
+ ### Fixed
13
+
14
+ - **Key verifier no longer fails with a cryptic `get row failed: 400`.** The
15
+ verifier's single-row GET (`GET /sync/{app}/__glance_keycheck`) is scoped by
16
+ `accountId`, which GLANCEvault requires as a query param on every row-scoped
17
+ endpoint. When the verifier ran with an `accountId` that was empty or
18
+ whitespace — e.g. fired during reconstruction before the account id was
19
+ populated — the request went out as `?accountId=` (or `?accountId=+++`), which
20
+ the server correctly rejects with `400 {"error":"accountId is required"}`,
21
+ aborting enable. The default `createVaultClient` now validates `accountId` on
22
+ every row-scoped call (`batch`, `list`, single-row GET, `DELETE`, `device`) and
23
+ throws a clear, typed, retryable `ACCOUNT_ID_REQUIRED` instead of putting a
24
+ malformed `accountId` on the wire — so the next cycle (once the id is loaded)
25
+ succeeds. `createDbSyncEngine` also rejects an empty/whitespace `accountId` at
26
+ construction (a whitespace-only id previously passed the plain falsy check).
27
+ The verifier path surfaces `ACCOUNT_ID_REQUIRED` (and `PASSPHRASE_REQUIRED`)
28
+ with their own code rather than mislabeling them `VERIFIER_UNSUPPORTED`. The
29
+ verifier GET correctly carries `?accountId=...` (like the working `list` call)
30
+ and, on a fresh account, 404 → writes the verifier via `batch` with
31
+ `accountId` + `insertOnly: true`.
32
+
33
+ ### Added
34
+
35
+ - `ACCOUNT_ID_REQUIRED` standardized on the `SyncErrorCode` type.
36
+
10
37
  ## [1.5.1] - 2026-06-19
11
38
 
12
39
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glance-apps/sync",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/dbEngine.js CHANGED
@@ -92,7 +92,13 @@ export const createDbSyncEngine = (config) => {
92
92
  if (!storageKeyPrefix) throw new Error('createDbSyncEngine: storageKeyPrefix is required');
93
93
  if (!config.vaultClient && !vaultUrl) throw new Error('createDbSyncEngine: vaultUrl is required');
94
94
  if (!config.vaultClient && !vaultToken) throw new Error('createDbSyncEngine: vaultToken is required');
95
- if (!accountId) throw new Error('createDbSyncEngine: accountId is required');
95
+ if (typeof accountId !== 'string' || accountId.trim() === '') {
96
+ // Reject empty/whitespace too: a whitespace-only id passes a plain falsy
97
+ // check but serialises to `?accountId=+++`, which the server trims to empty
98
+ // and rejects with a cryptic 400 on every row-scoped call (incl. the key
99
+ // verifier's single-row GET).
100
+ throw new Error('createDbSyncEngine: accountId is required');
101
+ }
96
102
  if (typeof getLocalEntity !== 'function') throw new Error('createDbSyncEngine: getLocalEntity is required');
97
103
  if (typeof applyRemoteEntity !== 'function') throw new Error('createDbSyncEngine: applyRemoteEntity is required');
98
104
  if (typeof applyRemoteDelete !== 'function') throw new Error('createDbSyncEngine: applyRemoteDelete is required');
@@ -242,7 +248,14 @@ export const createDbSyncEngine = (config) => {
242
248
  // skipped would still push poison rows. Pausing sync with a clear reason is
243
249
  // the correct degradation. (config.allowUnverified is an explicit operator
244
250
  // escape hatch, off by default, that downgrades this to a logged warning.)
251
+ // Client-readiness failures (no passphrase yet, or accountId not populated)
252
+ // are NOT a server problem: re-throw them as-is so they surface with their own
253
+ // typed, retryable code instead of being mislabeled "update your server".
254
+ const isClientNotReady = (err) =>
255
+ err && (err.code === 'ACCOUNT_ID_REQUIRED' || err.code === 'PASSPHRASE_REQUIRED');
256
+
245
257
  const verifierUnsupported = (cause) => {
258
+ if (isClientNotReady(cause)) throw cause;
246
259
  if (allowUnverified) {
247
260
  // eslint-disable-next-line no-console
248
261
  console.warn(`[${appId}] db sync proceeding UNVERIFIED: the server cannot host the key verifier (${KEYCHECK_ENTITY_ID}).`, cause);
@@ -73,12 +73,27 @@ export function createVaultClient({ vaultUrl, vaultToken, fetchImpl } = {}) {
73
73
  return res.json();
74
74
  };
75
75
 
76
+ // accountId scopes every row-scoped endpoint (batch, list, single-row GET,
77
+ // DELETE, device). The server has no default and rejects a missing/empty value
78
+ // with a cryptic 400 {"error":"accountId is required"}. Validate it here so a
79
+ // caller that fires before the account id is populated (e.g. the key verifier
80
+ // running during reconstruction) fails with a clear, typed, retryable error
81
+ // and we never put a malformed `?accountId=` on the wire.
82
+ const requireAccountId = (accountId, context) => {
83
+ if (typeof accountId !== 'string' || accountId.trim() === '') {
84
+ const err = new Error(`${context}: accountId is required but was missing or empty.`);
85
+ err.code = 'ACCOUNT_ID_REQUIRED';
86
+ throw err;
87
+ }
88
+ };
89
+
76
90
  return {
77
91
  /**
78
92
  * Upserts a batch of rows. Each row is { entityId, envelope, createdAt }.
79
93
  * @returns {Promise<{ written: number, maxSeq: number }>}
80
94
  */
81
95
  async batch(app, { accountId, rows }) {
96
+ requireAccountId(accountId, 'batch upsert');
82
97
  const res = await request('POST', `/sync/${encodeURIComponent(app)}/batch`, {
83
98
  body: { accountId, rows },
84
99
  });
@@ -90,6 +105,7 @@ export function createVaultClient({ vaultUrl, vaultToken, fetchImpl } = {}) {
90
105
  * @returns {Promise<{ rows: Array, hasMore: boolean }>}
91
106
  */
92
107
  async list(app, { accountId, since }) {
108
+ requireAccountId(accountId, 'list');
93
109
  const res = await request('GET', `/sync/${encodeURIComponent(app)}/list`, {
94
110
  query: { accountId, since: String(since) },
95
111
  });
@@ -100,6 +116,7 @@ export function createVaultClient({ vaultUrl, vaultToken, fetchImpl } = {}) {
100
116
  * Fetches a single row by entityId. Returns null on 404.
101
117
  */
102
118
  async getRow(app, entityId, accountId) {
119
+ requireAccountId(accountId, 'get row');
103
120
  const res = await request('GET', `/sync/${encodeURIComponent(app)}/${encodeURIComponent(entityId)}`, {
104
121
  query: { accountId },
105
122
  });
@@ -112,6 +129,7 @@ export function createVaultClient({ vaultUrl, vaultToken, fetchImpl } = {}) {
112
129
  * the new seq for the tombstone).
113
130
  */
114
131
  async deleteRow(app, entityId, accountId) {
132
+ requireAccountId(accountId, 'delete row');
115
133
  const res = await request('DELETE', `/sync/${encodeURIComponent(app)}/${encodeURIComponent(entityId)}`, {
116
134
  query: { accountId },
117
135
  });
@@ -125,6 +143,7 @@ export function createVaultClient({ vaultUrl, vaultToken, fetchImpl } = {}) {
125
143
  * @returns {Promise<{ updated: boolean }>}
126
144
  */
127
145
  async device(app, { accountId, deviceId, lastSeenSeq }) {
146
+ requireAccountId(accountId, 'device cursor');
128
147
  const res = await request('POST', `/sync/${encodeURIComponent(app)}/device`, {
129
148
  body: { accountId, deviceId, lastSeenSeq },
130
149
  });
package/types/index.d.ts CHANGED
@@ -24,7 +24,10 @@ export type SyncErrorCode =
24
24
  | 'ROW_DECRYPT_FAILED'
25
25
  // DB transport: the server can't host the key verifier (doesn't support the
26
26
  // reserved __glance_keycheck id or the single-row endpoint).
27
- | 'VERIFIER_UNSUPPORTED';
27
+ | 'VERIFIER_UNSUPPORTED'
28
+ // DB transport: a row-scoped call (incl. the key verifier) was made before the
29
+ // accountId was populated — retryable once the account id is available.
30
+ | 'ACCOUNT_ID_REQUIRED';
28
31
 
29
32
  export type SyncStatus = 'idle' | 'uploading' | 'downloading' | 'success' | 'error';
30
33