@indigoai-us/hq-cloud 6.8.0 → 6.9.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.
@@ -141,7 +141,7 @@ describe("share", () => {
141
141
  expect(result.filesUploaded).toBe(1);
142
142
  expect(result.aborted).toBe(false);
143
143
  // Remote key must be company-relative, not hqRoot-relative
144
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "test.md");
144
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "test.md", undefined, expect.anything());
145
145
  });
146
146
  it("respects ignore rules", async () => {
147
147
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -181,7 +181,7 @@ describe("share", () => {
181
181
  hqRoot: tmpDir,
182
182
  });
183
183
  // Key is "knowledge/crawl.json", not "companies/acme/knowledge/crawl.json"
184
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), nested, "knowledge/crawl.json");
184
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), nested, "knowledge/crawl.json", undefined, expect.anything());
185
185
  });
186
186
  it("skips files outside the company folder with a warning", async () => {
187
187
  const warnSpy = vi.spyOn(console, "error").mockImplementation(() => { });
@@ -280,7 +280,7 @@ describe("share", () => {
280
280
  skipUnchanged: true,
281
281
  });
282
282
  expect(result.filesUploaded).toBe(1);
283
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "changed.md");
283
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "changed.md", undefined, expect.anything());
284
284
  });
285
285
  it("populates conflictPaths and emits a conflict event when both local and remote drifted from journal", async () => {
286
286
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -496,6 +496,142 @@ describe("share", () => {
496
496
  // Local file untouched under keep.
497
497
  expect(fs.readFileSync(testFile, "utf-8")).toBe("my-local-version");
498
498
  });
499
+ describe("conditional-write fence (If-Match / If-None-Match / 412)", () => {
500
+ const fence412 = () => Object.assign(new Error("Precondition failed"), {
501
+ name: "PreconditionFailed",
502
+ });
503
+ it("fences a PUT over an existing remote with If-Match on the observed etag", async () => {
504
+ const companyRoot = path.join(tmpDir, "companies", "acme");
505
+ fs.mkdirSync(companyRoot, { recursive: true });
506
+ const testFile = path.join(companyRoot, "fenced.md");
507
+ fs.writeFileSync(testFile, "local edit");
508
+ // Remote exists at the journal baseline; only local changed -> clean
509
+ // push, but the PUT must still assert the observed remote etag so a
510
+ // peer write landing between HEAD and PUT draws a 412 instead of
511
+ // being clobbered (HEAD->PUT TOCTOU).
512
+ const syncedAt = new Date(Date.now() - 60_000).toISOString();
513
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
514
+ lastModified: new Date(Date.parse(syncedAt) - 30_000),
515
+ etag: '"baseline-etag"',
516
+ size: 5,
517
+ });
518
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
519
+ fs.writeFileSync(journalPath, JSON.stringify({
520
+ version: "1",
521
+ lastSync: syncedAt,
522
+ files: {
523
+ "fenced.md": {
524
+ hash: "stale-local-hash",
525
+ size: 5,
526
+ syncedAt,
527
+ direction: "up",
528
+ remoteEtag: "baseline-etag",
529
+ },
530
+ },
531
+ }));
532
+ const result = await share({
533
+ paths: [testFile],
534
+ company: "acme",
535
+ vaultConfig: mockConfig,
536
+ hqRoot: tmpDir,
537
+ });
538
+ expect(result.filesUploaded).toBe(1);
539
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "fenced.md", undefined, { ifMatch: '"baseline-etag"' });
540
+ });
541
+ it("fences a brand-new key with If-None-Match:* (create-only)", async () => {
542
+ const companyRoot = path.join(tmpDir, "companies", "acme");
543
+ fs.mkdirSync(companyRoot, { recursive: true });
544
+ const testFile = path.join(companyRoot, "brand-new.md");
545
+ fs.writeFileSync(testFile, "fresh");
546
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
547
+ await share({
548
+ paths: [testFile],
549
+ company: "acme",
550
+ vaultConfig: mockConfig,
551
+ hqRoot: tmpDir,
552
+ });
553
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "brand-new.md", undefined, { ifNoneMatch: "*" });
554
+ });
555
+ it("412 under keep: conflict event + remote mirrored + ledger entry + NO journal stamp", async () => {
556
+ const companyRoot = path.join(tmpDir, "companies", "acme");
557
+ fs.mkdirSync(companyRoot, { recursive: true });
558
+ const testFile = path.join(companyRoot, "raced.md");
559
+ fs.writeFileSync(testFile, "my local bytes");
560
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
561
+ vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
562
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
563
+ fs.writeFileSync(dest, "the racing peer version");
564
+ return undefined;
565
+ });
566
+ const events = [];
567
+ const result = await share({
568
+ paths: [testFile],
569
+ company: "acme",
570
+ vaultConfig: mockConfig,
571
+ hqRoot: tmpDir,
572
+ onConflict: "keep",
573
+ onEvent: (e) => events.push(e),
574
+ });
575
+ // Surfaced as a push conflict, not an error.
576
+ expect(result.conflictPaths).toEqual(["raced.md"]);
577
+ expect(result.filesUploaded).toBe(0);
578
+ expect(result.aborted).toBe(false);
579
+ expect(events.some((e) => e.type === "conflict" && e.path === "raced.md")).toBe(true);
580
+ // The racing remote bytes were preserved next to the local copy.
581
+ const mirrors = fs
582
+ .readdirSync(companyRoot)
583
+ .filter((f) => f.includes(".conflict-"));
584
+ expect(mirrors).toHaveLength(1);
585
+ // Local copy untouched.
586
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("my local bytes");
587
+ // No journal stamp for the failed PUT — next pass re-evaluates fresh.
588
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
589
+ if (fs.existsSync(journalPath)) {
590
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
591
+ expect(journal.files?.["raced.md"]).toBeUndefined();
592
+ }
593
+ });
594
+ it("412 under overwrite: retries exactly once WITHOUT the fence (explicit consent)", async () => {
595
+ const companyRoot = path.join(tmpDir, "companies", "acme");
596
+ fs.mkdirSync(companyRoot, { recursive: true });
597
+ const testFile = path.join(companyRoot, "consented.md");
598
+ fs.writeFileSync(testFile, "clobber me in");
599
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
600
+ vi.mocked(uploadFile)
601
+ .mockRejectedValueOnce(fence412())
602
+ .mockResolvedValueOnce({ etag: '"retried-etag"' });
603
+ const result = await share({
604
+ paths: [testFile],
605
+ company: "acme",
606
+ vaultConfig: mockConfig,
607
+ hqRoot: tmpDir,
608
+ onConflict: "overwrite",
609
+ });
610
+ expect(result.filesUploaded).toBe(1);
611
+ const calls = vi.mocked(uploadFile).mock.calls;
612
+ expect(calls).toHaveLength(2);
613
+ // First attempt fenced; retry deliberately unfenced.
614
+ expect(calls[0][4]).toEqual({ ifNoneMatch: "*" });
615
+ expect(calls[1][4]).toBeUndefined();
616
+ });
617
+ it("412 under abort: halts the pass", async () => {
618
+ const companyRoot = path.join(tmpDir, "companies", "acme");
619
+ fs.mkdirSync(companyRoot, { recursive: true });
620
+ const testFile = path.join(companyRoot, "halting.md");
621
+ fs.writeFileSync(testFile, "stop here");
622
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
623
+ vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
624
+ const result = await share({
625
+ paths: [testFile],
626
+ company: "acme",
627
+ vaultConfig: mockConfig,
628
+ hqRoot: tmpDir,
629
+ onConflict: "abort",
630
+ });
631
+ expect(result.aborted).toBe(true);
632
+ expect(result.conflictPaths).toEqual(["halting.md"]);
633
+ });
634
+ });
499
635
  it("scoped push (plan-exceeds-grant): syncs the in-scope subset, skips out-of-scope paths, never aborts (feedback_ded09d56)", async () => {
500
636
  // Real case (look-optic): a FILE_ACL grant covered {knowledge,policies,
501
637
  // workers}/* + company.yaml, but the upload plan also contained
@@ -594,6 +730,39 @@ describe("share", () => {
594
730
  expect(errs[0].path).toBe("out-of-reach.md");
595
731
  expect(errs[0].message).toMatch(/outside granted ACL scope/i);
596
732
  });
733
+ it("a Forbidden HEAD (presigned-transport denial) skips the key — NEVER an unconditional PUT", async () => {
734
+ // Regression for the 2026-06-10..12 vault regression storm: the presigned
735
+ // transport used to map per-key denials and signed-GET 403s to `null`,
736
+ // which `processUploadItem` read as "no remote object" — skipping every
737
+ // conflict guard and blind-PUTting this machine's (possibly stale) bytes
738
+ // over a newer remote. The transport now throws `name: "Forbidden"`
739
+ // (SDK HeadObject parity); this locks the share side of that contract:
740
+ // a Forbidden HEAD must skip the key, emit a named error, and issue NO PUT.
741
+ const companyRoot = path.join(tmpDir, "companies", "acme");
742
+ fs.mkdirSync(companyRoot, { recursive: true });
743
+ const staleFile = path.join(companyRoot, "stale-local.md");
744
+ fs.writeFileSync(staleFile, "stale local bytes\n");
745
+ vi.mocked(headRemoteFile).mockImplementation(async () => {
746
+ const err = new Error("Access denied for stale-local.md: presigned HEAD returned 403");
747
+ err.name = "Forbidden";
748
+ throw err;
749
+ });
750
+ const events = [];
751
+ const result = await share({
752
+ paths: [staleFile],
753
+ company: "acme",
754
+ vaultConfig: mockConfig,
755
+ hqRoot: tmpDir,
756
+ onEvent: (e) => events.push(e),
757
+ });
758
+ // The one thing that must never happen: a PUT issued with unknown remote state.
759
+ expect(vi.mocked(uploadFile)).not.toHaveBeenCalled();
760
+ expect(result.filesUploaded).toBe(0);
761
+ expect(result.aborted).toBe(false);
762
+ const errs2 = events.filter((e) => e.type === "error");
763
+ expect(errs2).toHaveLength(1);
764
+ expect(errs2[0].path).toBe("stale-local.md");
765
+ });
597
766
  it("uploads (no conflict) when only the local side changed since last sync", async () => {
598
767
  // Regression for hq-cloud#<conflict-detection>: a local edit to a file
599
768
  // that exists on S3 used to trigger a push conflict because the
@@ -706,11 +875,12 @@ describe("share", () => {
706
875
  hqRoot: tmpDir,
707
876
  author: { userSub: "abc-123", email: "alice@example.com" },
708
877
  });
709
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "attribution.md", { userSub: "abc-123", email: "alice@example.com" });
878
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "attribution.md", { userSub: "abc-123", email: "alice@example.com" }, expect.anything());
710
879
  });
711
880
  it("omits author arg when not provided (back-compat)", async () => {
712
- // share() must remain a 3-arg call to uploadFile when no author is
713
- // configured older test stubs and external integrations rely on it.
881
+ // With no author configured the author slot is undefined (the
882
+ // conditional-write fence appended a 5th arg in 6.9.0, so the legacy
883
+ // 3-arg shape grew to 5; author-less calls pass undefined explicitly).
714
884
  const companyRoot = path.join(tmpDir, "companies", "acme");
715
885
  fs.mkdirSync(companyRoot, { recursive: true });
716
886
  const testFile = path.join(companyRoot, "no-author.md");
@@ -721,7 +891,7 @@ describe("share", () => {
721
891
  vaultConfig: mockConfig,
722
892
  hqRoot: tmpDir,
723
893
  });
724
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "no-author.md");
894
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "no-author.md", undefined, expect.anything());
725
895
  });
726
896
  it("skipUnchanged=false (default) uploads even when hash matches", async () => {
727
897
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -863,7 +1033,7 @@ describe("share", () => {
863
1033
  expect(fetchMock).not.toHaveBeenCalled();
864
1034
  // The S3 upload sees the pre-vended credentials, not freshly-vended ones.
865
1035
  // (uploadFile is mocked, so we just verify it was called with our ctx.)
866
- expect(uploadFile).toHaveBeenCalledWith(ctx, testFile, "from-appbar.md");
1036
+ expect(uploadFile).toHaveBeenCalledWith(ctx, testFile, "from-appbar.md", undefined, expect.anything());
867
1037
  });
868
1038
  it("falls back to entityContext.slug when company is not specified", async () => {
869
1039
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -879,7 +1049,7 @@ describe("share", () => {
879
1049
  expect(result.filesUploaded).toBe(1);
880
1050
  // Confirms the relative-path scoping landed under acme even without an
881
1051
  // explicit company arg.
882
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "no-company-arg.md");
1052
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "no-company-arg.md", undefined, expect.anything());
883
1053
  });
884
1054
  it("does NOT auto-refresh when entityContext is expiring soon (no vending source)", async () => {
885
1055
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -905,7 +1075,7 @@ describe("share", () => {
905
1075
  expect(result.filesUploaded).toBe(1);
906
1076
  expect(fetchMock).not.toHaveBeenCalled();
907
1077
  // The original (still-valid-for-30s) credentials must have been used as-is.
908
- expect(uploadFile).toHaveBeenCalledWith(expiringCtx, testFile, "race.md");
1078
+ expect(uploadFile).toHaveBeenCalledWith(expiringCtx, testFile, "race.md", undefined, expect.anything());
909
1079
  });
910
1080
  it("throws when both vaultConfig and entityContext are provided (ambiguous)", async () => {
911
1081
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1779,7 +1949,7 @@ describe("share", () => {
1779
1949
  skipUnchanged: true,
1780
1950
  });
1781
1951
  expect(uploadFile).toHaveBeenCalledTimes(1);
1782
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), expect.stringContaining("real.md"), "skills/real.md");
1952
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), expect.stringContaining("real.md"), "skills/real.md", undefined, expect.anything());
1783
1953
  // Spy was called once total — the conflict mirror never reaches uploadFile.
1784
1954
  // (Asserted by count above; the explicit non-call below is defensive.)
1785
1955
  const calls = vi.mocked(uploadFile).mock.calls;
@@ -2184,7 +2354,7 @@ describe("share", () => {
2184
2354
  journalSlug: "personal",
2185
2355
  });
2186
2356
  expect(result.filesUploaded).toBe(1);
2187
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), outsideFile, "stray.md");
2357
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), outsideFile, "stray.md", undefined, expect.anything());
2188
2358
  });
2189
2359
  it("uploads 0-byte files (e.g. .gitkeep placeholders)", async () => {
2190
2360
  // Regression for the bug where `projects/.gitkeep` (0 bytes) was
@@ -2205,7 +2375,7 @@ describe("share", () => {
2205
2375
  journalSlug: "personal",
2206
2376
  });
2207
2377
  expect(result.filesUploaded).toBe(1);
2208
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), gitkeep, "projects/.gitkeep");
2378
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), gitkeep, "projects/.gitkeep", undefined, expect.anything());
2209
2379
  });
2210
2380
  it("personalMode=true + skipUnchanged honors the personal-journal hash", async () => {
2211
2381
  fs.mkdirSync(path.join(tmpDir, "knowledge"), { recursive: true });
@@ -2268,7 +2438,7 @@ describe("share", () => {
2268
2438
  hqRoot: tmpDir,
2269
2439
  });
2270
2440
  expect(result.filesUploaded).toBe(1);
2271
- expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), "target.md", "link.md");
2441
+ expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), "target.md", "link.md", undefined, expect.anything());
2272
2442
  // The link itself must NOT be uploaded as a regular file. Pre-fix,
2273
2443
  // fs.statSync(link) followed the link and uploadFile got called with
2274
2444
  // the link's path → cloud stored a copy of target.md's bytes under
@@ -2296,8 +2466,8 @@ describe("share", () => {
2296
2466
  // Pre-fix, walkDir's Dirent.isFile() returned false for the symlink and
2297
2467
  // it was silently dropped — only the real file was uploaded.
2298
2468
  expect(result.filesUploaded).toBe(2);
2299
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), realPolicy, "policies/real.md");
2300
- expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), "real.md", "policies/link.md");
2469
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), realPolicy, "policies/real.md", undefined, expect.anything());
2470
+ expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), "real.md", "policies/link.md", undefined, expect.anything());
2301
2471
  });
2302
2472
  it("accepts a symlink inside the company folder even when its target lives outside", async () => {
2303
2473
  // Codex P2 follow-up: pre-fix, isWithin canonicalized the link
@@ -2322,7 +2492,7 @@ describe("share", () => {
2322
2492
  });
2323
2493
  warnSpy.mockRestore();
2324
2494
  expect(result.filesUploaded).toBe(1);
2325
- expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), externalTarget, "knowledge");
2495
+ expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), externalTarget, "knowledge", undefined, expect.anything());
2326
2496
  // No "outside company folder" warning for this case.
2327
2497
  expect(warnSpy).not.toHaveBeenCalledWith(expect.stringMatching(/outside company folder/i));
2328
2498
  });
@@ -2375,7 +2545,7 @@ describe("share", () => {
2375
2545
  // symlink's journal hash differs from the regular-file hash.
2376
2546
  expect(result.filesUploaded).toBe(1);
2377
2547
  expect(result.filesSkipped).toBe(0);
2378
- expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), target, "case-key.md");
2548
+ expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), target, "case-key.md", undefined, expect.anything());
2379
2549
  });
2380
2550
  it("includes a directory symlink whose only matching allowlist pattern is dir-only", async () => {
2381
2551
  // Codex round-6 P1 follow-up: pre-fix, walkDir called the filter
@@ -2409,7 +2579,7 @@ describe("share", () => {
2409
2579
  });
2410
2580
  // The symlink record uploaded; the sibling regular file did NOT.
2411
2581
  expect(result.filesUploaded).toBe(1);
2412
- expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), externalTarget, "knowledge");
2582
+ expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), externalTarget, "knowledge", undefined, expect.anything());
2413
2583
  expect(uploadFile).not.toHaveBeenCalledWith(expect.anything(), expect.stringContaining("stray.md"), expect.anything());
2414
2584
  });
2415
2585
  it("does not recurse into directory symlinks (record-only)", async () => {
@@ -2432,7 +2602,7 @@ describe("share", () => {
2432
2602
  });
2433
2603
  // The link record itself uploads; the target's contents do NOT.
2434
2604
  expect(result.filesUploaded).toBe(1);
2435
- expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), realDir, "linked-dir");
2605
+ expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), realDir, "linked-dir", undefined, expect.anything());
2436
2606
  // Crucially, no upload of the dir's contents.
2437
2607
  const calls = vi.mocked(uploadFile).mock.calls;
2438
2608
  expect(calls.find((c) => c[2].includes("secret.md"))).toBeUndefined();