@indigoai-us/hq-cloud 5.4.5 → 5.4.6

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/dist/journal.js CHANGED
@@ -61,15 +61,29 @@ export function hashFile(filePath) {
61
61
  const content = fs.readFileSync(filePath);
62
62
  return crypto.createHash("sha256").update(content).digest("hex");
63
63
  }
64
- export function updateEntry(journal, relativePath, hash, size, direction) {
65
- journal.files[relativePath] = {
64
+ export function updateEntry(journal, relativePath, hash, size, direction, remoteEtag) {
65
+ const entry = {
66
66
  hash,
67
67
  size,
68
68
  syncedAt: new Date().toISOString(),
69
69
  direction,
70
70
  };
71
+ if (remoteEtag !== undefined && remoteEtag !== "") {
72
+ entry.remoteEtag = normalizeEtag(remoteEtag);
73
+ }
74
+ journal.files[relativePath] = entry;
71
75
  journal.lastSync = new Date().toISOString();
72
76
  }
77
+ /**
78
+ * S3 returns ETags wrapped in literal double-quotes (e.g. `"d41d8cd9..."`).
79
+ * Strip them so equality comparisons across HEAD / GET / PUT responses are
80
+ * stable regardless of which AWS SDK call surfaced the value.
81
+ */
82
+ export function normalizeEtag(etag) {
83
+ if (!etag)
84
+ return "";
85
+ return etag.replace(/^"|"$/g, "");
86
+ }
73
87
  export function getEntry(journal, relativePath) {
74
88
  return journal.files[relativePath];
75
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"journal.js","sourceRoot":"","sources":["../src/journal.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"journal.js","sourceRoot":"","sources":["../src/journal.ts"],"names":[],"mappings":"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"}
package/dist/s3.d.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  * is responsible for resolving the context via resolveEntityContext().
7
7
  */
8
8
  import type { EntityContext } from "./types.js";
9
- export declare function uploadFile(ctx: EntityContext, localPath: string, key: string): Promise<void>;
9
+ export declare function uploadFile(ctx: EntityContext, localPath: string, key: string): Promise<{
10
+ etag: string;
11
+ }>;
10
12
  export declare function downloadFile(ctx: EntityContext, key: string, localPath: string): Promise<void>;
11
13
  export interface RemoteFile {
12
14
  key: string;
package/dist/s3.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAYH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAkBhD,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAYf;AAED,wBAAsB,YAAY,CAChC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,eAAe,CACnC,GAAG,EAAE,aAAa,EAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,EAAE,CAAC,CA6BvB;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBpE"}
1
+ {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAYH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAkBhD,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAc3B;AAED,wBAAsB,YAAY,CAChC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,eAAe,CACnC,GAAG,EAAE,aAAa,EAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,EAAE,CAAC,CA6BvB;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBpE"}
package/dist/s3.js CHANGED
@@ -26,12 +26,13 @@ function buildClient(ctx) {
26
26
  export async function uploadFile(ctx, localPath, key) {
27
27
  const client = buildClient(ctx);
28
28
  const body = fs.readFileSync(localPath);
29
- await client.send(new PutObjectCommand({
29
+ const response = await client.send(new PutObjectCommand({
30
30
  Bucket: ctx.bucketName,
31
31
  Key: key,
32
32
  Body: body,
33
33
  ContentType: getMimeType(key),
34
34
  }));
35
+ return { etag: response.ETag || "" };
35
36
  }
36
37
  export async function downloadFile(ctx, key, localPath) {
37
38
  const client = buildClient(ctx);
package/dist/s3.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"s3.js","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"s3.js","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -23,6 +23,14 @@ export interface JournalEntry {
23
23
  size: number;
24
24
  syncedAt: string;
25
25
  direction: "up" | "down";
26
+ /**
27
+ * S3 ETag of the remote object as of last successful sync, normalized (no
28
+ * surrounding quotes). Optional for backwards compatibility: entries
29
+ * written before this field existed won't have it, in which case
30
+ * conflict detection falls back to comparing remote `lastModified`
31
+ * against `syncedAt`.
32
+ */
33
+ remoteEtag?: string;
26
34
  }
27
35
  export interface SyncJournal {
28
36
  version: "1";
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,GAAG,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,WAAW,EAAE,gBAAgB,CAAC;IAC9B,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,GAAG,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,WAAW,EAAE,gBAAgB,CAAC;IAC9B,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
package/dist/watcher.js CHANGED
@@ -91,8 +91,8 @@ export class SyncWatcher {
91
91
  const existing = journal.files[relativePath];
92
92
  if (existing && existing.hash === hash)
93
93
  continue;
94
- await uploadFile(this.ctx, change.absolutePath, relativePath);
95
- updateEntry(journal, relativePath, hash, stat.size, "up");
94
+ const { etag } = await uploadFile(this.ctx, change.absolutePath, relativePath);
95
+ updateEntry(journal, relativePath, hash, stat.size, "up", etag);
96
96
  }
97
97
  }
98
98
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"watcher.js","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"watcher.js","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cloud",
3
- "version": "5.4.5",
3
+ "version": "5.4.6",
4
4
  "description": "HQ by Indigo cloud sync engine — bidirectional S3 sync for mobile access",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,9 +9,11 @@ import * as os from "os";
9
9
  import { clearContextCache } from "../context.js";
10
10
  import type { VaultServiceConfig } from "../types.js";
11
11
 
12
- // Mock s3 module at the top level
12
+ // Mock s3 module at the top level. uploadFile resolves to a synthetic ETag
13
+ // so share() can record it on the journal entry — the real PutObject
14
+ // response shape is `{ ETag: '"<hex>"' }`.
13
15
  vi.mock("../s3.js", () => ({
14
- uploadFile: vi.fn().mockResolvedValue(undefined),
16
+ uploadFile: vi.fn().mockResolvedValue({ etag: '"upload-etag"' }),
15
17
  downloadFile: vi.fn().mockResolvedValue(undefined),
16
18
  listRemoteFiles: vi.fn().mockResolvedValue([]),
17
19
  deleteRemoteFile: vi.fn().mockResolvedValue(undefined),
@@ -77,6 +79,10 @@ describe("share", () => {
77
79
  afterEach(() => {
78
80
  vi.unstubAllGlobals();
79
81
  vi.clearAllMocks();
82
+ // clearAllMocks wipes the default ETag impl set in vi.mock(), so
83
+ // re-prime it for the next test.
84
+ vi.mocked(uploadFile).mockResolvedValue({ etag: '"upload-etag"' });
85
+ vi.mocked(headRemoteFile).mockResolvedValue(null);
80
86
  fs.rmSync(tmpDir, { recursive: true, force: true });
81
87
  fs.rmSync(stateDir, { recursive: true, force: true });
82
88
  delete process.env.HQ_STATE_DIR;
@@ -276,18 +282,18 @@ describe("share", () => {
276
282
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "changed.md");
277
283
  });
278
284
 
279
- it("populates conflictPaths and emits a conflict event when remote drifted from journal", async () => {
285
+ it("populates conflictPaths and emits a conflict event when both local and remote drifted from journal", async () => {
280
286
  const companyRoot = path.join(tmpDir, "companies", "acme");
281
287
  fs.mkdirSync(companyRoot, { recursive: true });
282
288
  const testFile = path.join(companyRoot, "drifted.md");
283
289
  fs.writeFileSync(testFile, "local edit");
284
290
 
285
- // Journal has a stale hash → local diverged. headRemoteFile returning
286
- // non-null tells share() the remote also exists; combined with the
287
- // hash mismatch this trips the conflict branch.
291
+ // Stale hash → local diverged. Remote ETag in head response differs
292
+ // from the one stored in the journal → remote also moved. Both sides
293
+ // changed since last sync = real conflict.
288
294
  vi.mocked(headRemoteFile).mockResolvedValueOnce({
289
295
  lastModified: new Date(),
290
- etag: '"remote-changed"',
296
+ etag: '"remote-new-etag"',
291
297
  size: 99,
292
298
  });
293
299
 
@@ -303,6 +309,7 @@ describe("share", () => {
303
309
  size: 10,
304
310
  syncedAt: new Date().toISOString(),
305
311
  direction: "up",
312
+ remoteEtag: "remote-old-etag",
306
313
  },
307
314
  },
308
315
  }),
@@ -332,6 +339,124 @@ describe("share", () => {
332
339
  });
333
340
  });
334
341
 
342
+ it("uploads (no conflict) when only the local side changed since last sync", async () => {
343
+ // Regression for hq-cloud#<conflict-detection>: a local edit to a file
344
+ // that exists on S3 used to trigger a push conflict because the
345
+ // detector compared `journalEntry.hash !== localHash` without checking
346
+ // the remote. Combined with `--on-conflict keep`, this silently dropped
347
+ // every edit to any pre-existing file.
348
+ const companyRoot = path.join(tmpDir, "companies", "acme");
349
+ fs.mkdirSync(companyRoot, { recursive: true });
350
+ const testFile = path.join(companyRoot, "edited.md");
351
+ fs.writeFileSync(testFile, "edited locally");
352
+
353
+ const syncedAt = new Date(Date.now() - 60_000).toISOString();
354
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
355
+ lastModified: new Date(Date.parse(syncedAt) - 30_000),
356
+ etag: '"unchanged-remote"',
357
+ size: 5,
358
+ });
359
+
360
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
361
+ fs.writeFileSync(
362
+ journalPath,
363
+ JSON.stringify({
364
+ version: "1",
365
+ lastSync: syncedAt,
366
+ files: {
367
+ "edited.md": {
368
+ hash: "stale-hash-for-old-content",
369
+ size: 5,
370
+ syncedAt,
371
+ direction: "down",
372
+ remoteEtag: "unchanged-remote",
373
+ },
374
+ },
375
+ }),
376
+ );
377
+
378
+ const events: unknown[] = [];
379
+ const result = await share({
380
+ paths: [testFile],
381
+ company: "acme",
382
+ vaultConfig: mockConfig,
383
+ hqRoot: tmpDir,
384
+ onConflict: "keep",
385
+ onEvent: (e) => events.push(e),
386
+ });
387
+
388
+ expect(result.conflictPaths).toEqual([]);
389
+ expect(result.filesUploaded).toBe(1);
390
+ expect(events.some((e): e is { type: string } =>
391
+ typeof e === "object" && e !== null && (e as { type?: string }).type === "conflict",
392
+ )).toBe(false);
393
+ });
394
+
395
+ it("falls back to lastModified vs syncedAt when journal entry has no remoteEtag (legacy)", async () => {
396
+ // Legacy entries from before the remoteEtag field existed should be
397
+ // treated as "remote unchanged" iff lastModified <= syncedAt.
398
+ const companyRoot = path.join(tmpDir, "companies", "acme");
399
+ fs.mkdirSync(companyRoot, { recursive: true });
400
+ const testFile = path.join(companyRoot, "legacy.md");
401
+ fs.writeFileSync(testFile, "edited locally");
402
+
403
+ const syncedAt = new Date().toISOString();
404
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
405
+ lastModified: new Date(Date.parse(syncedAt) - 5_000),
406
+ etag: '"some-etag"',
407
+ size: 5,
408
+ });
409
+
410
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
411
+ fs.writeFileSync(
412
+ journalPath,
413
+ JSON.stringify({
414
+ version: "1",
415
+ lastSync: syncedAt,
416
+ files: {
417
+ "legacy.md": {
418
+ hash: "stale-hash",
419
+ size: 5,
420
+ syncedAt,
421
+ direction: "down",
422
+ // no remoteEtag — pre-fix journal
423
+ },
424
+ },
425
+ }),
426
+ );
427
+
428
+ const result = await share({
429
+ paths: [testFile],
430
+ company: "acme",
431
+ vaultConfig: mockConfig,
432
+ hqRoot: tmpDir,
433
+ onConflict: "keep",
434
+ });
435
+
436
+ expect(result.conflictPaths).toEqual([]);
437
+ expect(result.filesUploaded).toBe(1);
438
+ });
439
+
440
+ it("records the upload's ETag on the journal entry", async () => {
441
+ const companyRoot = path.join(tmpDir, "companies", "acme");
442
+ fs.mkdirSync(companyRoot, { recursive: true });
443
+ const testFile = path.join(companyRoot, "fresh.md");
444
+ fs.writeFileSync(testFile, "new file");
445
+
446
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"new-upload-etag"' });
447
+
448
+ await share({
449
+ paths: [testFile],
450
+ company: "acme",
451
+ vaultConfig: mockConfig,
452
+ hqRoot: tmpDir,
453
+ });
454
+
455
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
456
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
457
+ expect(journal.files["fresh.md"].remoteEtag).toBe("new-upload-etag");
458
+ });
459
+
335
460
  it("skipUnchanged=false (default) uploads even when hash matches", async () => {
336
461
  const companyRoot = path.join(tmpDir, "companies", "acme");
337
462
  fs.mkdirSync(companyRoot, { recursive: true });
package/src/cli/share.ts CHANGED
@@ -10,7 +10,7 @@ import * as path from "path";
10
10
  import type { VaultServiceConfig } from "../types.js";
11
11
  import { resolveEntityContext, isExpiringSoon, refreshEntityContext } from "../context.js";
12
12
  import { uploadFile, headRemoteFile } from "../s3.js";
13
- import { readJournal, writeJournal, hashFile, updateEntry } from "../journal.js";
13
+ import { readJournal, writeJournal, hashFile, updateEntry, normalizeEtag } from "../journal.js";
14
14
  import { createIgnoreFilter, isWithinSizeLimit } from "../ignore.js";
15
15
  import { resolveConflict } from "./conflict.js";
16
16
  import type { ConflictStrategy } from "./conflict.js";
@@ -123,16 +123,24 @@ export async function share(options: ShareOptions): Promise<ShareResult> {
123
123
  ctx = await refreshEntityContext(companyRef, vaultConfig);
124
124
  }
125
125
 
126
- // Check for remote conflict — refuse to overwrite newer remote version
126
+ // Check for remote conflict — refuse to overwrite newer remote version.
127
+ //
128
+ // A real conflict requires BOTH sides to have moved since the last sync.
129
+ // The previous predicate only checked `journalEntry.hash !== localHash`,
130
+ // which mislabelled every local edit as a conflict and (combined with
131
+ // `--on-conflict keep`) silently dropped the user's edit. We now compare
132
+ // the current remote ETag against the one captured at last sync; when
133
+ // missing (legacy entries), we fall back to the same `lastModified >
134
+ // syncedAt` heuristic the pull side uses.
127
135
  const remoteMeta = await headRemoteFile(ctx, relativePath);
128
136
  if (remoteMeta) {
129
137
  const journalEntry = journal.files[relativePath];
138
+ const localChanged = !!journalEntry && journalEntry.hash !== localHash;
139
+ const remoteChanged = !!journalEntry && hasRemoteChanged(remoteMeta, journalEntry);
130
140
 
131
- // If remote has changed since our last sync, it's a conflict
132
- if (journalEntry && journalEntry.hash !== localHash) {
141
+ if (localChanged && remoteChanged) {
133
142
  conflictPaths.push(relativePath);
134
143
 
135
- // Local has changes — check if remote also changed
136
144
  const resolution = await resolveConflict(
137
145
  {
138
146
  path: relativePath,
@@ -171,10 +179,12 @@ export async function share(options: ShareOptions): Promise<ShareResult> {
171
179
  try {
172
180
  const stat = fs.statSync(absolutePath);
173
181
 
174
- await uploadFile(ctx, absolutePath, relativePath);
182
+ const { etag } = await uploadFile(ctx, absolutePath, relativePath);
175
183
 
176
- // Update journal with optional message
177
- updateEntry(journal, relativePath, localHash, stat.size, "up");
184
+ // Update journal with optional message; capture the post-upload ETag
185
+ // so the next sync can distinguish "remote moved since we last wrote"
186
+ // from "user edited locally" without conflating the two.
187
+ updateEntry(journal, relativePath, localHash, stat.size, "up", etag);
178
188
  if (message) {
179
189
  journal.files[relativePath] = {
180
190
  ...journal.files[relativePath],
@@ -318,3 +328,21 @@ function isWithin(parent: string, child: string): boolean {
318
328
  const rel = path.relative(parent, child);
319
329
  return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
320
330
  }
331
+
332
+ /**
333
+ * Returns true when the remote object appears to have moved since the
334
+ * journal entry's last-recorded sync. Prefers ETag equality; falls back to
335
+ * `lastModified > syncedAt` for legacy entries written before remoteEtag
336
+ * was tracked. Conservative on tie (`<=` skews "remote unchanged") so an
337
+ * S3-side mtime that exactly equals our syncedAt is not treated as drift.
338
+ */
339
+ function hasRemoteChanged(
340
+ remote: { lastModified: Date; etag: string },
341
+ entry: { syncedAt: string; remoteEtag?: string },
342
+ ): boolean {
343
+ if (entry.remoteEtag) {
344
+ return normalizeEtag(remote.etag) !== entry.remoteEtag;
345
+ }
346
+ const syncedAt = new Date(entry.syncedAt).getTime();
347
+ return remote.lastModified.getTime() > syncedAt;
348
+ }
@@ -330,4 +330,58 @@ describe("sync", () => {
330
330
  // File should be overwritten with mock content
331
331
  expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("mock file content");
332
332
  });
333
+
334
+ it("does NOT flag a pull conflict when only local changed since last sync", async () => {
335
+ // Regression: previously, any local edit to a file that also existed on
336
+ // S3 produced a pull conflict because the predicate only checked
337
+ // `journalEntry.hash !== localHash`. With `--on-conflict keep` this
338
+ // silently dropped local edits during the round-trip. With remoteEtag
339
+ // matching the journal, the remote is known unchanged and the pull
340
+ // phase should leave the local edit alone for the push phase to upload.
341
+ const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
342
+ fs.mkdirSync(companyDocs, { recursive: true });
343
+ fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local edit");
344
+
345
+ fs.writeFileSync(
346
+ journalPath,
347
+ JSON.stringify({
348
+ version: "1",
349
+ lastSync: new Date().toISOString(),
350
+ files: {
351
+ "docs/handoff.md": {
352
+ hash: "stale-hash-from-pre-edit",
353
+ size: 20,
354
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
355
+ direction: "down",
356
+ // Matches the listRemoteFiles mock's etag for handoff.md.
357
+ remoteEtag: "abc123",
358
+ },
359
+ },
360
+ }),
361
+ );
362
+
363
+ const result = await sync({
364
+ company: "acme",
365
+ onConflict: "keep",
366
+ vaultConfig: mockConfig,
367
+ hqRoot: tmpDir,
368
+ });
369
+
370
+ expect(result.conflicts).toBe(0);
371
+ expect(result.conflictPaths).toEqual([]);
372
+ // Local edit must be preserved (not clobbered by download)
373
+ expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("local edit");
374
+ });
375
+
376
+ it("records remoteEtag from listRemoteFiles on the journal entry after download", async () => {
377
+ await sync({
378
+ company: "acme",
379
+ vaultConfig: mockConfig,
380
+ hqRoot: tmpDir,
381
+ });
382
+
383
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
384
+ expect(journal.files["docs/handoff.md"].remoteEtag).toBe("abc123");
385
+ expect(journal.files["knowledge/readme.md"].remoteEtag).toBe("def456");
386
+ });
333
387
  });
package/src/cli/sync.ts CHANGED
@@ -10,7 +10,7 @@ import * as path from "path";
10
10
  import type { VaultServiceConfig } from "../types.js";
11
11
  import { resolveEntityContext, isExpiringSoon, refreshEntityContext } from "../context.js";
12
12
  import { downloadFile, listRemoteFiles } from "../s3.js";
13
- import { readJournal, writeJournal, hashFile, updateEntry, getEntry } from "../journal.js";
13
+ import { readJournal, writeJournal, hashFile, updateEntry, getEntry, normalizeEtag } from "../journal.js";
14
14
  import { createIgnoreFilter } from "../ignore.js";
15
15
  import { resolveConflict } from "./conflict.js";
16
16
  import type { ConflictStrategy, ConflictResolution } from "./conflict.js";
@@ -147,9 +147,14 @@ export async function sync(options: SyncOptions): Promise<SyncResult> {
147
147
 
148
148
  if (fs.existsSync(localPath)) {
149
149
  const localHash = hashFile(localPath);
150
+ const localChanged = !!journalEntry && journalEntry.hash !== localHash;
151
+ const remoteChanged = !!journalEntry && hasRemoteChanged(remoteFile, journalEntry);
150
152
 
151
- // If local file has changed since last sync, it's a conflict
152
- if (journalEntry && journalEntry.hash !== localHash) {
153
+ // A real conflict requires BOTH sides to have moved since the last
154
+ // sync. If only local changed, push will handle it; pulling here would
155
+ // clobber the local edit. If only remote changed, fall through to
156
+ // download. If neither moved, skip.
157
+ if (localChanged && remoteChanged) {
153
158
  conflicts++;
154
159
  conflictPaths.push(remoteFile.key);
155
160
 
@@ -187,17 +192,18 @@ export async function sync(options: SyncOptions): Promise<SyncResult> {
187
192
  continue;
188
193
  }
189
194
  // "overwrite" falls through to download
190
- } else if (journalEntry && journalEntry.hash === localHash) {
191
- // Local unchanged since last sync check if remote changed
192
- // by comparing etag/timestamp
193
- const lastSyncTime = new Date(journalEntry.syncedAt).getTime();
194
- const remoteModTime = remoteFile.lastModified.getTime();
195
- if (remoteModTime <= lastSyncTime) {
196
- // Remote hasn't changed either skip
197
- filesSkipped++;
198
- continue;
199
- }
195
+ } else if (journalEntry && localChanged && !remoteChanged) {
196
+ // Local-only edit: leave it for the push phase to upload. Pulling
197
+ // would silently overwrite the user's work.
198
+ filesSkipped++;
199
+ continue;
200
+ } else if (journalEntry && !localChanged && !remoteChanged) {
201
+ // Neither side movednothing to do.
202
+ filesSkipped++;
203
+ continue;
200
204
  }
205
+ // Otherwise (no journal entry, or remote-only changed) fall through
206
+ // to download.
201
207
  }
202
208
 
203
209
  // Download
@@ -206,7 +212,9 @@ export async function sync(options: SyncOptions): Promise<SyncResult> {
206
212
 
207
213
  const hash = hashFile(localPath);
208
214
  const stat = fs.statSync(localPath);
209
- updateEntry(journal, remoteFile.key, hash, stat.size, "down");
215
+ // Capture the listing's ETag so subsequent syncs can detect remote
216
+ // drift independently of mtime drift.
217
+ updateEntry(journal, remoteFile.key, hash, stat.size, "down", remoteFile.etag);
210
218
 
211
219
  // Attach message from journal entry if present
212
220
  const remoteJournalMessage = (journalEntry as { message?: string } | undefined)?.message;
@@ -262,6 +270,23 @@ function resolveActiveCompany(hqRoot: string): string | undefined {
262
270
  return undefined;
263
271
  }
264
272
 
273
+ /**
274
+ * Returns true when the remote object appears to have moved since the
275
+ * journal entry's last-recorded sync. Prefers ETag equality; falls back to
276
+ * `lastModified > syncedAt` for legacy entries written before remoteEtag
277
+ * was tracked. Conservative on tie (`<=` skews "remote unchanged").
278
+ */
279
+ function hasRemoteChanged(
280
+ remote: { lastModified: Date; etag: string },
281
+ entry: { syncedAt: string; remoteEtag?: string },
282
+ ): boolean {
283
+ if (entry.remoteEtag) {
284
+ return normalizeEtag(remote.etag) !== entry.remoteEtag;
285
+ }
286
+ const syncedAt = new Date(entry.syncedAt).getTime();
287
+ return remote.lastModified.getTime() > syncedAt;
288
+ }
289
+
265
290
  /**
266
291
  * Check if an error is an S3 access denied (expected for filtered guests).
267
292
  */
package/src/journal.ts CHANGED
@@ -80,16 +80,31 @@ export function updateEntry(
80
80
  hash: string,
81
81
  size: number,
82
82
  direction: "up" | "down",
83
+ remoteEtag?: string,
83
84
  ): void {
84
- journal.files[relativePath] = {
85
+ const entry: JournalEntry = {
85
86
  hash,
86
87
  size,
87
88
  syncedAt: new Date().toISOString(),
88
89
  direction,
89
90
  };
91
+ if (remoteEtag !== undefined && remoteEtag !== "") {
92
+ entry.remoteEtag = normalizeEtag(remoteEtag);
93
+ }
94
+ journal.files[relativePath] = entry;
90
95
  journal.lastSync = new Date().toISOString();
91
96
  }
92
97
 
98
+ /**
99
+ * S3 returns ETags wrapped in literal double-quotes (e.g. `"d41d8cd9..."`).
100
+ * Strip them so equality comparisons across HEAD / GET / PUT responses are
101
+ * stable regardless of which AWS SDK call surfaced the value.
102
+ */
103
+ export function normalizeEtag(etag: string): string {
104
+ if (!etag) return "";
105
+ return etag.replace(/^"|"$/g, "");
106
+ }
107
+
93
108
  export function getEntry(
94
109
  journal: SyncJournal,
95
110
  relativePath: string,
package/src/s3.ts CHANGED
@@ -38,11 +38,11 @@ export async function uploadFile(
38
38
  ctx: EntityContext,
39
39
  localPath: string,
40
40
  key: string,
41
- ): Promise<void> {
41
+ ): Promise<{ etag: string }> {
42
42
  const client = buildClient(ctx);
43
43
  const body = fs.readFileSync(localPath);
44
44
 
45
- await client.send(
45
+ const response = await client.send(
46
46
  new PutObjectCommand({
47
47
  Bucket: ctx.bucketName,
48
48
  Key: key,
@@ -50,6 +50,8 @@ export async function uploadFile(
50
50
  ContentType: getMimeType(key),
51
51
  }),
52
52
  );
53
+
54
+ return { etag: response.ETag || "" };
53
55
  }
54
56
 
55
57
  export async function downloadFile(
package/src/types.ts CHANGED
@@ -26,6 +26,14 @@ export interface JournalEntry {
26
26
  size: number;
27
27
  syncedAt: string;
28
28
  direction: "up" | "down";
29
+ /**
30
+ * S3 ETag of the remote object as of last successful sync, normalized (no
31
+ * surrounding quotes). Optional for backwards compatibility: entries
32
+ * written before this field existed won't have it, in which case
33
+ * conflict detection falls back to comparing remote `lastModified`
34
+ * against `syncedAt`.
35
+ */
36
+ remoteEtag?: string;
29
37
  }
30
38
 
31
39
  export interface SyncJournal {