@indigoai-us/hq-cloud 5.48.4 → 6.0.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.
Files changed (55) hide show
  1. package/dist/cli/index.d.ts +2 -2
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +2 -2
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/cli/reindex.d.ts +23 -0
  6. package/dist/cli/reindex.d.ts.map +1 -0
  7. package/dist/cli/reindex.js +45 -0
  8. package/dist/cli/reindex.js.map +1 -0
  9. package/dist/cli/reindex.test.d.ts +11 -0
  10. package/dist/cli/reindex.test.d.ts.map +1 -0
  11. package/dist/cli/{master-sync.test.js → reindex.test.js} +15 -15
  12. package/dist/cli/reindex.test.js.map +1 -0
  13. package/dist/cli/rescue.d.ts.map +1 -1
  14. package/dist/cli/rescue.js +16 -1
  15. package/dist/cli/rescue.js.map +1 -1
  16. package/dist/cli/rescue.reindex.test.d.ts +2 -0
  17. package/dist/cli/rescue.reindex.test.d.ts.map +1 -0
  18. package/dist/cli/rescue.reindex.test.js +41 -0
  19. package/dist/cli/rescue.reindex.test.js.map +1 -0
  20. package/dist/cli/share.d.ts.map +1 -1
  21. package/dist/cli/share.js +21 -1
  22. package/dist/cli/share.js.map +1 -1
  23. package/dist/cli/share.test.js +108 -2
  24. package/dist/cli/share.test.js.map +1 -1
  25. package/dist/cli/sync.d.ts +9 -0
  26. package/dist/cli/sync.d.ts.map +1 -1
  27. package/dist/cli/sync.js +17 -0
  28. package/dist/cli/sync.js.map +1 -1
  29. package/dist/cli/sync.test.js +20 -0
  30. package/dist/cli/sync.test.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -2
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -1
  36. package/scripts/{master-sync.sh → reindex.sh} +10 -10
  37. package/scripts/replace-rescue.sh +20 -20
  38. package/src/cli/index.ts +3 -3
  39. package/src/cli/{master-sync.test.ts → reindex.test.ts} +14 -14
  40. package/src/cli/reindex.ts +57 -0
  41. package/src/cli/rescue.reindex.test.ts +46 -0
  42. package/src/cli/rescue.ts +15 -1
  43. package/src/cli/share.test.ts +138 -2
  44. package/src/cli/share.ts +23 -1
  45. package/src/cli/sync.test.ts +23 -0
  46. package/src/cli/sync.ts +27 -0
  47. package/src/index.ts +3 -3
  48. package/dist/cli/master-sync.d.ts +0 -22
  49. package/dist/cli/master-sync.d.ts.map +0 -1
  50. package/dist/cli/master-sync.js +0 -44
  51. package/dist/cli/master-sync.js.map +0 -1
  52. package/dist/cli/master-sync.test.d.ts +0 -11
  53. package/dist/cli/master-sync.test.d.ts.map +0 -1
  54. package/dist/cli/master-sync.test.js.map +0 -1
  55. package/src/cli/master-sync.ts +0 -56
@@ -898,6 +898,112 @@ describe("share", () => {
898
898
  const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
899
899
  expect(journal.files["dangling-link.md"]).toBeDefined();
900
900
  });
901
+ it("personal-vault override: `owned-only` is coerced to `currency-gated` so a direction:'down' " +
902
+ "entry whose local file is gone DOES tombstone in the vault (the May-27 .drift-* leak class)", async () => {
903
+ // The personal vault is a single-human-many-machines target. The
904
+ // owned-only "I won't tombstone peer content" rule has no peer to
905
+ // protect against, so it traps legitimate local deletes as permanent
906
+ // vault litter — exactly what the May-27 `personal/.obsidian/*.drift-*`
907
+ // pair did on this EC2 (journal direction "down", local rm, push
908
+ // silently swallowed the delete). Coercing to currency-gated keeps the
909
+ // only safety intent that still applies (HEAD-verify etag matches the
910
+ // journal before tombstone) without the multi-user premise.
911
+ const stateDirPersonal = fs.mkdtempSync(path.join(os.tmpdir(), "hq-state-prs-"));
912
+ process.env.HQ_STATE_DIR = stateDirPersonal;
913
+ try {
914
+ // Personal-vault syncRoot is `hqRoot` itself in personalMode, so the
915
+ // missing file's absolute path resolves to <hqRoot>/<key>.
916
+ // No on-disk file = local missing. Journal records a prior pull.
917
+ const journalPath = path.join(stateDirPersonal, "sync-journal.__hq_personal_vault__.json");
918
+ fs.writeFileSync(journalPath, JSON.stringify({
919
+ version: "1",
920
+ lastSync: new Date().toISOString(),
921
+ files: {
922
+ "personal/scripts/run-project.sh.drift-1779863862-42519": {
923
+ hash: "personal-vault-drift-hash",
924
+ size: 1054,
925
+ syncedAt: new Date(Date.now() - 86400000).toISOString(),
926
+ direction: "down",
927
+ remoteEtag: "personal-vault-drift-etag",
928
+ },
929
+ },
930
+ }));
931
+ // Currency-gated HEADs the candidate; return an etag matching the
932
+ // journal so the entry resolves as "remote still current → safe to
933
+ // tombstone."
934
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
935
+ lastModified: new Date(),
936
+ etag: '"personal-vault-drift-etag"',
937
+ size: 1054,
938
+ });
939
+ const personalCtx = makeEntityContext({
940
+ uid: "prs_01HASSAANTESTUSER",
941
+ slug: "__hq_personal_vault__",
942
+ bucketName: "hq-vault-prs-01HASSAANTESTUSER",
943
+ });
944
+ const result = await share({
945
+ paths: [tmpDir],
946
+ entityContext: personalCtx,
947
+ hqRoot: tmpDir,
948
+ personalMode: true,
949
+ skipUnchanged: true,
950
+ propagateDeletes: true,
951
+ // Caller pinned owned-only. The override MUST take precedence on a
952
+ // personal-vault entity — the whole point of the fix is that owned-
953
+ // only's "don't tombstone peer-uploaded content" rule has no peer
954
+ // to protect against here, so we coerce to currency-gated.
955
+ propagateDeletePolicy: "owned-only",
956
+ });
957
+ expect(result.filesDeleted).toBe(1);
958
+ expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/scripts/run-project.sh.drift-1779863862-42519");
959
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
960
+ expect(journal.files["personal/scripts/run-project.sh.drift-1779863862-42519"]).toBeUndefined();
961
+ }
962
+ finally {
963
+ fs.rmSync(stateDirPersonal, { recursive: true, force: true });
964
+ delete process.env.HQ_STATE_DIR;
965
+ }
966
+ });
967
+ it("personal-vault override does NOT apply to company vaults: `owned-only` on cmp_ entity still " +
968
+ "refuses to tombstone a direction:'down' entry (multi-user curation intact)", async () => {
969
+ // Sibling guard for the previous test: the override is scoped strictly
970
+ // to `prs_` entities. Company vaults preserve owned-only's multi-user
971
+ // semantics — a behind machine pulling Alice's upload, then rm'ing it
972
+ // locally, must NOT tombstone Alice's file. Without this guard the
973
+ // override could silently widen its blast radius the next time someone
974
+ // refactors share()'s entity-typing.
975
+ const companyRoot = path.join(tmpDir, "companies", "acme");
976
+ fs.mkdirSync(companyRoot, { recursive: true });
977
+ // No file on disk under companies/acme/peer-uploaded.md → local missing.
978
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
979
+ fs.writeFileSync(journalPath, JSON.stringify({
980
+ version: "1",
981
+ lastSync: new Date().toISOString(),
982
+ files: {
983
+ "peer-uploaded.md": {
984
+ hash: "alice-hash",
985
+ size: 100,
986
+ syncedAt: new Date(Date.now() - 86400000).toISOString(),
987
+ direction: "down",
988
+ remoteEtag: "alice-etag",
989
+ },
990
+ },
991
+ }));
992
+ const result = await share({
993
+ paths: [companyRoot],
994
+ company: "acme",
995
+ vaultConfig: mockConfig,
996
+ hqRoot: tmpDir,
997
+ skipUnchanged: true,
998
+ propagateDeletes: true,
999
+ // Company-vault entity (cmp_) → owned-only stays in effect.
1000
+ propagateDeletePolicy: "owned-only",
1001
+ });
1002
+ expect(result.filesDeleted).toBe(0);
1003
+ expect(deleteRemoteFile).not.toHaveBeenCalled();
1004
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1005
+ expect(journal.files["peer-uploaded.md"]).toBeDefined();
1006
+ });
901
1007
  it("propagateDeletes: deletes journal-tracked files whose local copy is gone", async () => {
902
1008
  const companyRoot = path.join(tmpDir, "companies", "acme");
903
1009
  fs.mkdirSync(companyRoot, { recursive: true });
@@ -1899,7 +2005,7 @@ describe("share", () => {
1899
2005
  // target's bytes under the link's key while a nested symlink was
1900
2006
  // silently dropped from every push. The link topology never survived a
1901
2007
  // round trip — fresh-machine pulls landed in a state where overlay
1902
- // symlinks just didn't exist until master-sync.sh recreated them
2008
+ // symlinks just didn't exist until reindex.sh recreated them
1903
2009
  // locally. The fix detects symlinks via lstat / Dirent.isSymbolicLink
1904
2010
  // and routes them to a new uploadSymlink primitive that PUTs a
1905
2011
  // zero-byte object with x-amz-meta-hq-symlink-target carrying the
@@ -1933,7 +2039,7 @@ describe("share", () => {
1933
2039
  const realPolicy = path.join(policiesDir, "real.md");
1934
2040
  fs.writeFileSync(realPolicy, "real content");
1935
2041
  const linkPolicy = path.join(policiesDir, "link.md");
1936
- // Mirrors the master-sync.sh overlay shape: relative target pointing
2042
+ // Mirrors the reindex.sh overlay shape: relative target pointing
1937
2043
  // to a sibling in the same dir.
1938
2044
  fs.symlinkSync("real.md", linkPolicy);
1939
2045
  const result = await share({