@indigoai-us/hq-cloud 6.11.19 → 6.12.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.
Files changed (62) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  3. package/dist/bin/sync-runner-company.js +9 -11
  4. package/dist/bin/sync-runner-company.js.map +1 -1
  5. package/dist/bin/sync-runner.d.ts.map +1 -1
  6. package/dist/bin/sync-runner.js.map +1 -1
  7. package/dist/bin/sync-runner.test.js +35 -64
  8. package/dist/bin/sync-runner.test.js.map +1 -1
  9. package/dist/cli/rescue-core.js +1 -1
  10. package/dist/cli/rescue-core.js.map +1 -1
  11. package/dist/cli/share.js +25 -19
  12. package/dist/cli/share.js.map +1 -1
  13. package/dist/cli/share.test.js +63 -9
  14. package/dist/cli/share.test.js.map +1 -1
  15. package/dist/cli/sync.d.ts +11 -25
  16. package/dist/cli/sync.d.ts.map +1 -1
  17. package/dist/cli/sync.js +16 -17
  18. package/dist/cli/sync.js.map +1 -1
  19. package/dist/cli/sync.test.js +40 -42
  20. package/dist/cli/sync.test.js.map +1 -1
  21. package/dist/company-resolver.test.js.map +1 -1
  22. package/dist/lib/conflict-file.js +1 -1
  23. package/dist/lib/conflict-file.js.map +1 -1
  24. package/dist/personal-vault.d.ts +23 -24
  25. package/dist/personal-vault.d.ts.map +1 -1
  26. package/dist/personal-vault.js +38 -30
  27. package/dist/personal-vault.js.map +1 -1
  28. package/dist/personal-vault.test.d.ts +9 -6
  29. package/dist/personal-vault.test.d.ts.map +1 -1
  30. package/dist/personal-vault.test.js +90 -79
  31. package/dist/personal-vault.test.js.map +1 -1
  32. package/dist/prefix-coalesce.d.ts.map +1 -1
  33. package/dist/prefix-coalesce.js +0 -3
  34. package/dist/prefix-coalesce.js.map +1 -1
  35. package/dist/s3.test.js +0 -1
  36. package/dist/s3.test.js.map +1 -1
  37. package/dist/sync/push-receiver.test.js.map +1 -1
  38. package/dist/vault-client.d.ts +1 -1
  39. package/dist/vault-client.d.ts.map +1 -1
  40. package/dist/vault-client.js +1 -1
  41. package/dist/vault-client.js.map +1 -1
  42. package/dist/vault-client.test.js +15 -0
  43. package/dist/vault-client.test.js.map +1 -1
  44. package/eslint.config.js +67 -0
  45. package/package.json +5 -1
  46. package/src/bin/sync-runner-company.ts +9 -12
  47. package/src/bin/sync-runner.test.ts +35 -74
  48. package/src/bin/sync-runner.ts +0 -1
  49. package/src/cli/rescue-core.ts +1 -1
  50. package/src/cli/share.test.ts +79 -11
  51. package/src/cli/share.ts +24 -20
  52. package/src/cli/sync.test.ts +40 -42
  53. package/src/cli/sync.ts +25 -42
  54. package/src/company-resolver.test.ts +1 -1
  55. package/src/lib/conflict-file.ts +1 -1
  56. package/src/personal-vault.test.ts +91 -83
  57. package/src/personal-vault.ts +45 -41
  58. package/src/prefix-coalesce.ts +0 -4
  59. package/src/s3.test.ts +0 -2
  60. package/src/sync/push-receiver.test.ts +1 -1
  61. package/src/vault-client.test.ts +20 -0
  62. package/src/vault-client.ts +2 -2
@@ -831,7 +831,7 @@ describe("share", () => {
831
831
  expect(putKeys).not.toContain("settings/.gitkeep");
832
832
  expect(putKeys).not.toContain("projects/.gitkeep");
833
833
  });
834
- it("scoped push defense-in-depth: a 403 on HEAD skips that key (named error) instead of aborting the company", async () => {
834
+ it("scoped push defense-in-depth: a 403 on HEAD skips that key as scope-excluded without an error event", async () => {
835
835
  // Belt-and-suspenders: even if a key slips past the prefix filter (a grant
836
836
  // that changed mid-run, a pin outside the grant, prefix-coalesce
837
837
  // imprecision), the server's correct 403 on the HEAD must NOT abort the
@@ -866,20 +866,72 @@ describe("share", () => {
866
866
  expect(result.filesUploaded).toBe(1);
867
867
  const putKeys = vi.mocked(uploadFile).mock.calls.map((c) => c[2]);
868
868
  expect(putKeys).toEqual(["in-scope.md"]);
869
- // The blocked key surfaced as a path-named, scope-clear error event.
869
+ expect(result.filesExcludedByScope).toBe(1);
870
+ const scopeEv = events.find((e) => e.type === "scope-excluded");
871
+ expect(scopeEv).toBeDefined();
872
+ expect(scopeEv.count).toBe(1);
873
+ expect(scopeEv.samplePaths).toEqual(["out-of-reach.md"]);
874
+ expect(events.some((e) => e.type === "error")).toBe(false);
875
+ });
876
+ it("scoped push defense-in-depth: a 403 on PUT skips that key as scope-excluded without an error event", async () => {
877
+ const companyRoot = path.join(tmpDir, "companies", "acme");
878
+ fs.mkdirSync(companyRoot, { recursive: true });
879
+ const blockedFile = path.join(companyRoot, "put-denied.md");
880
+ fs.writeFileSync(blockedFile, "denied\n");
881
+ vi.mocked(uploadFile).mockImplementation(async (_ctx, _abs, key) => {
882
+ if (key === "put-denied.md") {
883
+ const err = new Error("access denied");
884
+ err.name = "AccessDenied";
885
+ throw err;
886
+ }
887
+ return { etag: '"upload-etag"' };
888
+ });
889
+ const events = [];
890
+ const result = await share({
891
+ paths: [blockedFile],
892
+ company: "acme",
893
+ vaultConfig: mockConfig,
894
+ hqRoot: tmpDir,
895
+ onEvent: (e) => events.push(e),
896
+ });
897
+ expect(result.aborted).toBe(false);
898
+ expect(result.filesUploaded).toBe(0);
899
+ expect(result.filesExcludedByScope).toBe(1);
900
+ const scopeEv = events.find((e) => e.type === "scope-excluded");
901
+ expect(scopeEv).toBeDefined();
902
+ expect(scopeEv.count).toBe(1);
903
+ expect(scopeEv.samplePaths).toEqual(["put-denied.md"]);
904
+ expect(events.some((e) => e.type === "error")).toBe(false);
905
+ });
906
+ it("scoped push defense-in-depth: a non-access-denied PUT failure still emits an error event", async () => {
907
+ const companyRoot = path.join(tmpDir, "companies", "acme");
908
+ fs.mkdirSync(companyRoot, { recursive: true });
909
+ const failedFile = path.join(companyRoot, "put-failed.md");
910
+ fs.writeFileSync(failedFile, "failed\n");
911
+ vi.mocked(uploadFile).mockRejectedValueOnce(new Error("S3 down"));
912
+ const events = [];
913
+ const result = await share({
914
+ paths: [failedFile],
915
+ company: "acme",
916
+ vaultConfig: mockConfig,
917
+ hqRoot: tmpDir,
918
+ onEvent: (e) => events.push(e),
919
+ });
920
+ expect(result.filesUploaded).toBe(0);
921
+ expect(result.filesExcludedByScope).toBe(0);
870
922
  const errs = events.filter((e) => e.type === "error");
871
923
  expect(errs).toHaveLength(1);
872
- expect(errs[0].path).toBe("out-of-reach.md");
873
- expect(errs[0].message).toMatch(/outside granted ACL scope/i);
924
+ expect(errs[0].path).toBe("put-failed.md");
925
+ expect(errs[0].message).toBe("S3 down");
874
926
  });
875
- it("a Forbidden HEAD (presigned-transport denial) skips the key — NEVER an unconditional PUT", async () => {
927
+ it("a Forbidden HEAD (presigned-transport denial) skips the key as scope-excluded — NEVER an unconditional PUT", async () => {
876
928
  // Regression for the 2026-06-10..12 vault regression storm: the presigned
877
929
  // transport used to map per-key denials and signed-GET 403s to `null`,
878
930
  // which `processUploadItem` read as "no remote object" — skipping every
879
931
  // conflict guard and blind-PUTting this machine's (possibly stale) bytes
880
932
  // over a newer remote. The transport now throws `name: "Forbidden"`
881
933
  // (SDK HeadObject parity); this locks the share side of that contract:
882
- // a Forbidden HEAD must skip the key, emit a named error, and issue NO PUT.
934
+ // a Forbidden HEAD must skip the key as scope-excluded and issue NO PUT.
883
935
  const companyRoot = path.join(tmpDir, "companies", "acme");
884
936
  fs.mkdirSync(companyRoot, { recursive: true });
885
937
  const staleFile = path.join(companyRoot, "stale-local.md");
@@ -901,9 +953,11 @@ describe("share", () => {
901
953
  expect(vi.mocked(uploadFile)).not.toHaveBeenCalled();
902
954
  expect(result.filesUploaded).toBe(0);
903
955
  expect(result.aborted).toBe(false);
904
- const errs2 = events.filter((e) => e.type === "error");
905
- expect(errs2).toHaveLength(1);
906
- expect(errs2[0].path).toBe("stale-local.md");
956
+ expect(result.filesExcludedByScope).toBe(1);
957
+ const scopeEv = events.find((e) => e.type === "scope-excluded");
958
+ expect(scopeEv).toBeDefined();
959
+ expect(scopeEv.samplePaths).toEqual(["stale-local.md"]);
960
+ expect(events.some((e) => e.type === "error")).toBe(false);
907
961
  });
908
962
  it("uploads (no conflict) when only the local side changed since last sync", async () => {
909
963
  // Regression for hq-cloud#<conflict-detection>: a local edit to a file