@indigoai-us/hq-cloud 6.15.41 → 6.15.43

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 (50) hide show
  1. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-company.js +48 -3
  3. package/dist/bin/sync-runner-company.js.map +1 -1
  4. package/dist/bin/sync-runner-company.test.js +135 -0
  5. package/dist/bin/sync-runner-company.test.js.map +1 -1
  6. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  7. package/dist/bin/sync-runner-watch-loop.js +22 -6
  8. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  9. package/dist/bin/sync-runner.d.ts +2 -0
  10. package/dist/bin/sync-runner.d.ts.map +1 -1
  11. package/dist/bin/sync-runner.js.map +1 -1
  12. package/dist/bin/sync-runner.test.js +93 -16
  13. package/dist/bin/sync-runner.test.js.map +1 -1
  14. package/dist/cli/share.d.ts +5 -17
  15. package/dist/cli/share.d.ts.map +1 -1
  16. package/dist/cli/share.js +16 -62
  17. package/dist/cli/share.js.map +1 -1
  18. package/dist/cli/share.test.js +57 -89
  19. package/dist/cli/share.test.js.map +1 -1
  20. package/dist/cli/sync.d.ts +16 -5
  21. package/dist/cli/sync.d.ts.map +1 -1
  22. package/dist/cli/sync.js +16 -63
  23. package/dist/cli/sync.js.map +1 -1
  24. package/dist/cli/sync.test.js +28 -54
  25. package/dist/cli/sync.test.js.map +1 -1
  26. package/dist/local-path-codec.test.js +20 -0
  27. package/dist/local-path-codec.test.js.map +1 -1
  28. package/dist/s3.d.ts +1 -1
  29. package/dist/s3.d.ts.map +1 -1
  30. package/dist/s3.js +7 -23
  31. package/dist/s3.js.map +1 -1
  32. package/dist/s3.test.js +13 -20
  33. package/dist/s3.test.js.map +1 -1
  34. package/dist/sync-core.d.ts +16 -0
  35. package/dist/sync-core.d.ts.map +1 -1
  36. package/dist/sync-core.js +38 -0
  37. package/dist/sync-core.js.map +1 -1
  38. package/dist/sync-progress.d.ts +18 -1
  39. package/dist/sync-progress.d.ts.map +1 -1
  40. package/dist/sync-progress.js +113 -7
  41. package/dist/sync-progress.js.map +1 -1
  42. package/dist/sync-progress.test.js +32 -2
  43. package/dist/sync-progress.test.js.map +1 -1
  44. package/dist/watcher.d.ts +26 -5
  45. package/dist/watcher.d.ts.map +1 -1
  46. package/dist/watcher.js +93 -13
  47. package/dist/watcher.js.map +1 -1
  48. package/dist/watcher.test.js +45 -1
  49. package/dist/watcher.test.js.map +1 -1
  50. package/package.json +1 -1
@@ -52,7 +52,7 @@ vi.mock("readline", () => ({
52
52
  })),
53
53
  }));
54
54
  import * as readline from "readline";
55
- import { share, isForbiddenCompanyVaultKey, ServerOwnedPushPathsError, UnreachablePushPathsError, currentUploadBytes, _testing as shareTesting } from "./share.js";
55
+ import { share, ServerOwnedPushPathsError, UnreachablePushPathsError, currentUploadBytes, _testing as shareTesting } from "./share.js";
56
56
  import { deleteRemoteFile, downloadFile, headRemoteFile, primeObjectTransport, primeUploads, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError, } from "../s3.js";
57
57
  import { VaultAuthError } from "../vault-client.js";
58
58
  import { hashFile, readJournal, writeJournal } from "../journal.js";
@@ -7419,7 +7419,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
7419
7419
  });
7420
7420
  });
7421
7421
  });
7422
- describe("scope-invalid key hardening on push (doubled companies/ tree — incident 2026-07-11)", () => {
7422
+ describe("nested companies directories in a company vault", () => {
7423
7423
  let tmpDir;
7424
7424
  let stateDir;
7425
7425
  beforeEach(() => {
@@ -7448,18 +7448,25 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7448
7448
  fs.rmSync(stateDir, { recursive: true, force: true });
7449
7449
  delete process.env.HQ_STATE_DIR;
7450
7450
  });
7451
- it("refuses to upload doubled-tree companies/ keys in company mode — clear per-key warning, never an error", async () => {
7452
- // The incident writer: a stale doubled local tree
7453
- // companies/acme/companies/acme/… would push literal `companies/…` keys
7454
- // via the direct-S3 STS transport, poisoning the bucket with keys the
7455
- // presign download API rejects (INVALID_KEY_COMPANIES_SCOPED) for every
7456
- // other machine. The push planner must refuse them up front.
7451
+ it("plans every nested companies/ file for upload as vault-relative content", async () => {
7457
7452
  const companyRoot = path.join(tmpDir, "companies", "acme");
7458
- const doubled = path.join(companyRoot, "companies", "acme", "knowledge");
7459
- fs.mkdirSync(doubled, { recursive: true });
7460
- fs.writeFileSync(path.join(doubled, "poison.md"), "poison");
7461
- fs.mkdirSync(path.join(companyRoot, "docs"), { recursive: true });
7462
- fs.writeFileSync(path.join(companyRoot, "docs", "good.md"), "good");
7453
+ const canonicalSkill = path.join(companyRoot, "skills", "signals");
7454
+ const canonicalAudit = path.join(companyRoot, "audit", "chat-skills", "review");
7455
+ fs.mkdirSync(canonicalSkill, { recursive: true });
7456
+ fs.mkdirSync(canonicalAudit, { recursive: true });
7457
+ fs.writeFileSync(path.join(canonicalSkill, "SKILL.md"), "skill");
7458
+ fs.writeFileSync(path.join(canonicalAudit, "SKILL.md"), "audit");
7459
+ // This matches the live path shape: a literal `companies/acme/...` below
7460
+ // the acme vault root is content, not a scope selector. All 25 objects
7461
+ // must be planned for upload rather than summarized as invalid.
7462
+ for (let index = 0; index < 25; index++) {
7463
+ const nestedAudit = path.join(companyRoot, "companies", "acme", "audit", "chat-skills", `stale-${index.toString().padStart(2, "0")}`);
7464
+ fs.mkdirSync(nestedAudit, { recursive: true });
7465
+ fs.writeFileSync(path.join(nestedAudit, "SKILL.md"), "nested");
7466
+ }
7467
+ const midPath = path.join(companyRoot, "audit", "companies", "notes.md");
7468
+ fs.mkdirSync(path.dirname(midPath), { recursive: true });
7469
+ fs.writeFileSync(midPath, "ordinary segment");
7463
7470
  const events = [];
7464
7471
  const result = await share({
7465
7472
  paths: [companyRoot],
@@ -7469,15 +7476,16 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7469
7476
  onEvent: (e) => events.push(e),
7470
7477
  });
7471
7478
  expect(result.aborted).toBe(false);
7472
- expect(result.filesUploaded).toBe(1);
7479
+ expect(result.filesUploaded).toBe(28);
7473
7480
  const uploadedKeys = vi.mocked(uploadFile).mock.calls.map((c) => c[2]);
7474
- expect(uploadedKeys).toEqual(["docs/good.md"]);
7475
- // Per-key warning on the existing event channel, never type:"error"
7476
- // (an error event flips the runner pass to exit 2 — the flood class).
7477
- const skipEvents = events.filter((e) => e.type === "skip-invalid-scoped-key");
7478
- expect(skipEvents.map((e) => e.path)).toEqual([
7479
- "companies/acme/knowledge/poison.md",
7480
- ]);
7481
+ expect(uploadedKeys).toEqual(expect.arrayContaining([
7482
+ "skills/signals/SKILL.md",
7483
+ "audit/chat-skills/review/SKILL.md",
7484
+ "companies/acme/audit/chat-skills/stale-00/SKILL.md",
7485
+ "companies/acme/audit/chat-skills/stale-24/SKILL.md",
7486
+ "audit/companies/notes.md",
7487
+ ]));
7488
+ expect(events.filter((e) => e.type === "skip-invalid-scoped-key")).toEqual([]);
7481
7489
  expect(events.some((e) => e.type === "error")).toBe(false);
7482
7490
  });
7483
7491
  it("never plans an upload for macOS Finder Icon\\r files — silent skip, valid files still ship", async () => {
@@ -7545,11 +7553,9 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7545
7553
  ]);
7546
7554
  expect(events.some((e) => e.type === "error")).toBe(false);
7547
7555
  });
7548
- it("tombstones (journal drop, no remote HEAD/DELETE) a companies/ journal key in company mode instead of re-uploading", async () => {
7549
- // Delete-propagation on a poisoned journal entry: HEAD/DeleteObject on a
7550
- // companies/-prefixed key would be rejected by the presign transport's
7551
- // server validator, so the delete plan must route it straight to
7552
- // toTombstone — journal entry drained, no remote call, no re-upload.
7556
+ it("tombstones an absent nested companies/ key after normal remote verification", async () => {
7557
+ // A nested `companies/` directory is a normal company-vault path, so its
7558
+ // stale journal entry follows the ordinary HEAD-confirmed cleanup path.
7553
7559
  const companyRoot = path.join(tmpDir, "companies", "acme");
7554
7560
  fs.mkdirSync(companyRoot, { recursive: true });
7555
7561
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
@@ -7576,16 +7582,17 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7576
7582
  propagateDeletePolicy: "currency-gated",
7577
7583
  });
7578
7584
  expect(result.aborted).toBe(false);
7579
- // Tombstoned: journal entry dropped without any remote HEAD or DELETE.
7585
+ // The remote HEAD confirms this normal vault-relative key is absent, then
7586
+ // the journal entry is tombstoned without re-uploading it.
7580
7587
  expect(result.filesTombstoned).toBe(1);
7581
7588
  expect(result.filesDeleted).toBe(0);
7582
- expect(headRemoteFile).not.toHaveBeenCalled();
7589
+ expect(headRemoteFile).toHaveBeenCalledWith(expect.anything(), "companies/acme/knowledge/poison.md");
7583
7590
  expect(deleteRemoteFile).not.toHaveBeenCalled();
7584
7591
  expect(uploadFile).not.toHaveBeenCalled();
7585
7592
  const journal = journalAt(journalPath);
7586
7593
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
7587
7594
  });
7588
- it("defers an unreadable scope-invalid symlink tombstone without a fatal error", async () => {
7595
+ it("does not follow a nested symlink that points outside the company vault", async () => {
7589
7596
  const companyRoot = path.join(tmpDir, "companies", "acme");
7590
7597
  const linkKey = "companies/acme/knowledge/unreadable-link";
7591
7598
  const linkPath = path.join(companyRoot, ...linkKey.split("/"));
@@ -7607,44 +7614,28 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7607
7614
  },
7608
7615
  },
7609
7616
  }));
7610
- const realReadlinkSync = fs.readlinkSync;
7611
- let linkReadCount = 0;
7612
- const readlinkSpy = vi
7613
- .spyOn(fs, "readlinkSync")
7614
- .mockImplementation(((candidate) => {
7615
- if (candidate === linkPath && ++linkReadCount === 2) {
7616
- throw errnoError("EINVAL");
7617
- }
7618
- return realReadlinkSync(candidate);
7619
- }));
7620
7617
  const events = [];
7621
- try {
7622
- const result = await share({
7623
- paths: [companyRoot],
7624
- company: "acme",
7625
- vaultConfig: mockConfig,
7626
- hqRoot: tmpDir,
7627
- skipUnchanged: true,
7628
- propagateDeletes: true,
7629
- propagateDeletePolicy: "currency-gated",
7630
- onEvent: (event) => events.push(event),
7631
- });
7632
- expect(linkReadCount).toBe(2);
7633
- expect(result.filesTombstoned).toBe(0);
7634
- expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
7635
- expect(events).toContainEqual({
7636
- type: "not-shipped",
7637
- reason: "unreadable-link",
7638
- count: 1,
7639
- samplePaths: [linkKey],
7640
- });
7641
- expect(events.some((event) => event.type === "error")).toBe(false);
7642
- const journal = journalAt(journalPath);
7643
- expect(journal.files[linkKey]).toBeDefined();
7644
- }
7645
- finally {
7646
- readlinkSpy.mockRestore();
7647
- }
7618
+ const result = await share({
7619
+ paths: [companyRoot],
7620
+ company: "acme",
7621
+ vaultConfig: mockConfig,
7622
+ hqRoot: tmpDir,
7623
+ skipUnchanged: true,
7624
+ propagateDeletes: true,
7625
+ propagateDeletePolicy: "currency-gated",
7626
+ onEvent: (event) => events.push(event),
7627
+ });
7628
+ expect(result.filesTombstoned).toBe(0);
7629
+ expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
7630
+ expect(events).toContainEqual({
7631
+ type: "not-shipped",
7632
+ reason: "linked-subtree",
7633
+ count: 1,
7634
+ samplePaths: [linkKey],
7635
+ });
7636
+ expect(events.some((event) => event.type === "error")).toBe(false);
7637
+ const journal = journalAt(journalPath);
7638
+ expect(journal.files[linkKey]).toBeDefined();
7648
7639
  });
7649
7640
  it("personal-mode push still uploads companies/{slug}/ keys (local non-cloud companies are legitimate)", async () => {
7650
7641
  const localCo = path.join(tmpDir, "companies", "localco");
@@ -7666,29 +7657,6 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7666
7657
  expect(events.filter((e) => e.type === "skip-invalid-scoped-key")).toEqual([]);
7667
7658
  });
7668
7659
  });
7669
- // ── Pure-function unit coverage: isForbiddenCompanyVaultKey ───────────────────
7670
- //
7671
- // Contract: in a COMPANY vault (personalMode=false) any key under `companies/`
7672
- // is a doubly-scoped corrupt object and must be refused; in a PERSONAL vault
7673
- // (personalMode=true) `companies/` keys are legitimate and handled by the
7674
- // dedicated personalMode branch, so this guard must NOT fire there.
7675
- describe("isForbiddenCompanyVaultKey (company-vault double-scope contract)", () => {
7676
- it.each([
7677
- // Company vault: companies/* keys are corrupt → forbidden.
7678
- ["companies/frogbear/drafts/reports/signals-2026-06-15.html", false, true],
7679
- ["companies/acme/knowledge/readme.md", false, true],
7680
- ["companies/manifest.yaml", false, true],
7681
- // Company vault: bucket-relative keys are legitimate → allowed.
7682
- ["docs/handoff.md", false, false],
7683
- ["knowledge/readme.md", false, false],
7684
- // Personal vault: companies/* keys are NOT forbidden here (handled by the
7685
- // personalMode branch, which carries its own nuanced gating).
7686
- ["companies/frogbear/drafts/reports/signals-2026-06-15.html", true, false],
7687
- ["docs/handoff.md", true, false],
7688
- ])("key=%s personalMode=%s → forbidden=%s", (key, personalMode, expected) => {
7689
- expect(isForbiddenCompanyVaultKey(key, personalMode)).toBe(expected);
7690
- });
7691
- });
7692
7660
  // ── delete-refusal rendering ───────────────────────────────────────────────
7693
7661
  //
7694
7662
  // Every refusal cause shares ONE event type (`delete-refused-stale-etag`),