@indigoai-us/hq-cloud 6.15.42 → 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 (47) hide show
  1. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-company.js +48 -12
  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.d.ts +0 -5
  7. package/dist/bin/sync-runner.d.ts.map +1 -1
  8. package/dist/bin/sync-runner.js.map +1 -1
  9. package/dist/bin/sync-runner.test.js +7 -2
  10. package/dist/bin/sync-runner.test.js.map +1 -1
  11. package/dist/cli/share.d.ts +5 -17
  12. package/dist/cli/share.d.ts.map +1 -1
  13. package/dist/cli/share.js +16 -77
  14. package/dist/cli/share.js.map +1 -1
  15. package/dist/cli/share.test.js +45 -99
  16. package/dist/cli/share.test.js.map +1 -1
  17. package/dist/cli/sync.d.ts +16 -18
  18. package/dist/cli/sync.d.ts.map +1 -1
  19. package/dist/cli/sync.js +16 -72
  20. package/dist/cli/sync.js.map +1 -1
  21. package/dist/cli/sync.test.js +28 -54
  22. package/dist/cli/sync.test.js.map +1 -1
  23. package/dist/local-path-codec.test.js +20 -0
  24. package/dist/local-path-codec.test.js.map +1 -1
  25. package/dist/s3.d.ts +1 -1
  26. package/dist/s3.d.ts.map +1 -1
  27. package/dist/s3.js +7 -23
  28. package/dist/s3.js.map +1 -1
  29. package/dist/s3.test.js +13 -20
  30. package/dist/s3.test.js.map +1 -1
  31. package/dist/sync-core.d.ts +16 -0
  32. package/dist/sync-core.d.ts.map +1 -1
  33. package/dist/sync-core.js +38 -0
  34. package/dist/sync-core.js.map +1 -1
  35. package/dist/sync-progress.d.ts +18 -1
  36. package/dist/sync-progress.d.ts.map +1 -1
  37. package/dist/sync-progress.js +113 -7
  38. package/dist/sync-progress.js.map +1 -1
  39. package/dist/sync-progress.test.js +32 -2
  40. package/dist/sync-progress.test.js.map +1 -1
  41. package/dist/watcher.d.ts +14 -5
  42. package/dist/watcher.d.ts.map +1 -1
  43. package/dist/watcher.js +40 -12
  44. package/dist/watcher.js.map +1 -1
  45. package/dist/watcher.test.js +45 -1
  46. package/dist/watcher.test.js.map +1 -1
  47. 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";
@@ -2979,7 +2979,6 @@ describe("share", () => {
2979
2979
  e.type === "scope-excluded" ||
2980
2980
  e.type === "scope-materialization-gap" ||
2981
2981
  e.type === "ignore-excluded" ||
2982
- e.type === "skip-invalid-scoped-key-summary" ||
2983
2982
  e.type === "not-shipped" ||
2984
2983
  e.type === "push-refused-server-owned-summary" ||
2985
2984
  e.type === "delete-refused-bulk-asymmetry")
@@ -7420,7 +7419,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
7420
7419
  });
7421
7420
  });
7422
7421
  });
7423
- describe("scope-invalid key hardening on push (doubled companies/ tree — incident 2026-07-11)", () => {
7422
+ describe("nested companies directories in a company vault", () => {
7424
7423
  let tmpDir;
7425
7424
  let stateDir;
7426
7425
  beforeEach(() => {
@@ -7449,9 +7448,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7449
7448
  fs.rmSync(stateDir, { recursive: true, force: true });
7450
7449
  delete process.env.HQ_STATE_DIR;
7451
7450
  });
7452
- it("uploads canonical company skill/audit keys but summarizes every doubled-tree key without widening cross-company acceptance", async () => {
7453
- // A real company file is rooted at companies/<slug>/, so these canonical
7454
- // paths MUST become bucket-relative keys and be planned for upload.
7451
+ it("plans every nested companies/ file for upload as vault-relative content", async () => {
7455
7452
  const companyRoot = path.join(tmpDir, "companies", "acme");
7456
7453
  const canonicalSkill = path.join(companyRoot, "skills", "signals");
7457
7454
  const canonicalAudit = path.join(companyRoot, "audit", "chat-skills", "review");
@@ -7459,17 +7456,17 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7459
7456
  fs.mkdirSync(canonicalAudit, { recursive: true });
7460
7457
  fs.writeFileSync(path.join(canonicalSkill, "SKILL.md"), "skill");
7461
7458
  fs.writeFileSync(path.join(canonicalAudit, "SKILL.md"), "audit");
7462
- // Conversely, a literal companies/<slug>/ segment BELOW the company root
7463
- // is a stale doubled tree. A different slug is also rejected: accepting it
7464
- // would permit a cross-company key in acme's already-scoped bucket.
7465
- const foreign = path.join(companyRoot, "companies", "aardvark", "skills", "foreign");
7466
- fs.mkdirSync(foreign, { recursive: true });
7467
- fs.writeFileSync(path.join(foreign, "SKILL.md"), "foreign");
7468
- for (let index = 0; index < 24; index++) {
7469
- const doubledAudit = path.join(companyRoot, "companies", "acme", "audit", "chat-skills", `stale-${index.toString().padStart(2, "0")}`);
7470
- fs.mkdirSync(doubledAudit, { recursive: true });
7471
- fs.writeFileSync(path.join(doubledAudit, "SKILL.md"), "stale");
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");
7472
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");
7473
7470
  const events = [];
7474
7471
  const result = await share({
7475
7472
  paths: [companyRoot],
@@ -7479,27 +7476,16 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7479
7476
  onEvent: (e) => events.push(e),
7480
7477
  });
7481
7478
  expect(result.aborted).toBe(false);
7482
- expect(result.filesUploaded).toBe(2);
7479
+ expect(result.filesUploaded).toBe(28);
7483
7480
  const uploadedKeys = vi.mocked(uploadFile).mock.calls.map((c) => c[2]);
7484
7481
  expect(uploadedKeys).toEqual(expect.arrayContaining([
7485
7482
  "skills/signals/SKILL.md",
7486
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
7487
  ]));
7488
- expect(uploadedKeys).not.toContain("companies/aardvark/skills/foreign/SKILL.md");
7489
- // One bounded summary retains the true total (25) while preventing the
7490
- // per-path event flood on every watch pass. The foreign key appears in the
7491
- // sample, pinning the cross-company tenant-boundary rejection directly.
7492
7488
  expect(events.filter((e) => e.type === "skip-invalid-scoped-key")).toEqual([]);
7493
- expect(events.filter((e) => e.type === "skip-invalid-scoped-key-summary")).toEqual([
7494
- {
7495
- type: "skip-invalid-scoped-key-summary",
7496
- count: 25,
7497
- samplePaths: expect.arrayContaining([
7498
- "companies/aardvark/skills/foreign/SKILL.md",
7499
- ]),
7500
- errorCode: "INVALID_KEY_COMPANIES_SCOPED",
7501
- },
7502
- ]);
7503
7489
  expect(events.some((e) => e.type === "error")).toBe(false);
7504
7490
  });
7505
7491
  it("never plans an upload for macOS Finder Icon\\r files — silent skip, valid files still ship", async () => {
@@ -7567,11 +7553,9 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7567
7553
  ]);
7568
7554
  expect(events.some((e) => e.type === "error")).toBe(false);
7569
7555
  });
7570
- it("tombstones (journal drop, no remote HEAD/DELETE) a companies/ journal key in company mode instead of re-uploading", async () => {
7571
- // Delete-propagation on a poisoned journal entry: HEAD/DeleteObject on a
7572
- // companies/-prefixed key would be rejected by the presign transport's
7573
- // server validator, so the delete plan must route it straight to
7574
- // 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.
7575
7559
  const companyRoot = path.join(tmpDir, "companies", "acme");
7576
7560
  fs.mkdirSync(companyRoot, { recursive: true });
7577
7561
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
@@ -7598,16 +7582,17 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7598
7582
  propagateDeletePolicy: "currency-gated",
7599
7583
  });
7600
7584
  expect(result.aborted).toBe(false);
7601
- // 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.
7602
7587
  expect(result.filesTombstoned).toBe(1);
7603
7588
  expect(result.filesDeleted).toBe(0);
7604
- expect(headRemoteFile).not.toHaveBeenCalled();
7589
+ expect(headRemoteFile).toHaveBeenCalledWith(expect.anything(), "companies/acme/knowledge/poison.md");
7605
7590
  expect(deleteRemoteFile).not.toHaveBeenCalled();
7606
7591
  expect(uploadFile).not.toHaveBeenCalled();
7607
7592
  const journal = journalAt(journalPath);
7608
7593
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
7609
7594
  });
7610
- 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 () => {
7611
7596
  const companyRoot = path.join(tmpDir, "companies", "acme");
7612
7597
  const linkKey = "companies/acme/knowledge/unreadable-link";
7613
7598
  const linkPath = path.join(companyRoot, ...linkKey.split("/"));
@@ -7629,44 +7614,28 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7629
7614
  },
7630
7615
  },
7631
7616
  }));
7632
- const realReadlinkSync = fs.readlinkSync;
7633
- let linkReadCount = 0;
7634
- const readlinkSpy = vi
7635
- .spyOn(fs, "readlinkSync")
7636
- .mockImplementation(((candidate) => {
7637
- if (candidate === linkPath && ++linkReadCount === 2) {
7638
- throw errnoError("EINVAL");
7639
- }
7640
- return realReadlinkSync(candidate);
7641
- }));
7642
7617
  const events = [];
7643
- try {
7644
- const result = await share({
7645
- paths: [companyRoot],
7646
- company: "acme",
7647
- vaultConfig: mockConfig,
7648
- hqRoot: tmpDir,
7649
- skipUnchanged: true,
7650
- propagateDeletes: true,
7651
- propagateDeletePolicy: "currency-gated",
7652
- onEvent: (event) => events.push(event),
7653
- });
7654
- expect(linkReadCount).toBe(2);
7655
- expect(result.filesTombstoned).toBe(0);
7656
- expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
7657
- expect(events).toContainEqual({
7658
- type: "not-shipped",
7659
- reason: "unreadable-link",
7660
- count: 1,
7661
- samplePaths: [linkKey],
7662
- });
7663
- expect(events.some((event) => event.type === "error")).toBe(false);
7664
- const journal = journalAt(journalPath);
7665
- expect(journal.files[linkKey]).toBeDefined();
7666
- }
7667
- finally {
7668
- readlinkSpy.mockRestore();
7669
- }
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();
7670
7639
  });
7671
7640
  it("personal-mode push still uploads companies/{slug}/ keys (local non-cloud companies are legitimate)", async () => {
7672
7641
  const localCo = path.join(tmpDir, "companies", "localco");
@@ -7688,29 +7657,6 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7688
7657
  expect(events.filter((e) => e.type === "skip-invalid-scoped-key")).toEqual([]);
7689
7658
  });
7690
7659
  });
7691
- // ── Pure-function unit coverage: isForbiddenCompanyVaultKey ───────────────────
7692
- //
7693
- // Contract: in a COMPANY vault (personalMode=false) any key under `companies/`
7694
- // is a doubly-scoped corrupt object and must be refused; in a PERSONAL vault
7695
- // (personalMode=true) `companies/` keys are legitimate and handled by the
7696
- // dedicated personalMode branch, so this guard must NOT fire there.
7697
- describe("isForbiddenCompanyVaultKey (company-vault double-scope contract)", () => {
7698
- it.each([
7699
- // Company vault: companies/* keys are corrupt → forbidden.
7700
- ["companies/frogbear/drafts/reports/signals-2026-06-15.html", false, true],
7701
- ["companies/acme/knowledge/readme.md", false, true],
7702
- ["companies/manifest.yaml", false, true],
7703
- // Company vault: bucket-relative keys are legitimate → allowed.
7704
- ["docs/handoff.md", false, false],
7705
- ["knowledge/readme.md", false, false],
7706
- // Personal vault: companies/* keys are NOT forbidden here (handled by the
7707
- // personalMode branch, which carries its own nuanced gating).
7708
- ["companies/frogbear/drafts/reports/signals-2026-06-15.html", true, false],
7709
- ["docs/handoff.md", true, false],
7710
- ])("key=%s personalMode=%s → forbidden=%s", (key, personalMode, expected) => {
7711
- expect(isForbiddenCompanyVaultKey(key, personalMode)).toBe(expected);
7712
- });
7713
- });
7714
7660
  // ── delete-refusal rendering ───────────────────────────────────────────────
7715
7661
  //
7716
7662
  // Every refusal cause shares ONE event type (`delete-refused-stale-etag`),