@indigoai-us/hq-cloud 6.15.42 → 6.15.44

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/dist/bin/sync-runner-company.d.ts +3 -0
  2. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  3. package/dist/bin/sync-runner-company.js +50 -14
  4. package/dist/bin/sync-runner-company.js.map +1 -1
  5. package/dist/bin/sync-runner-company.test.js +135 -0
  6. package/dist/bin/sync-runner-company.test.js.map +1 -1
  7. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  8. package/dist/bin/sync-runner-watch-loop.js +33 -1
  9. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  10. package/dist/bin/sync-runner.d.ts +5 -5
  11. package/dist/bin/sync-runner.d.ts.map +1 -1
  12. package/dist/bin/sync-runner.js +3 -0
  13. package/dist/bin/sync-runner.js.map +1 -1
  14. package/dist/bin/sync-runner.test.js +96 -2
  15. package/dist/bin/sync-runner.test.js.map +1 -1
  16. package/dist/cli/share.d.ts +5 -17
  17. package/dist/cli/share.d.ts.map +1 -1
  18. package/dist/cli/share.js +16 -77
  19. package/dist/cli/share.js.map +1 -1
  20. package/dist/cli/share.test.js +48 -99
  21. package/dist/cli/share.test.js.map +1 -1
  22. package/dist/cli/sync.d.ts +16 -18
  23. package/dist/cli/sync.d.ts.map +1 -1
  24. package/dist/cli/sync.js +16 -72
  25. package/dist/cli/sync.js.map +1 -1
  26. package/dist/cli/sync.test.js +40 -59
  27. package/dist/cli/sync.test.js.map +1 -1
  28. package/dist/cli/tombstones.d.ts +23 -8
  29. package/dist/cli/tombstones.d.ts.map +1 -1
  30. package/dist/cli/tombstones.js +63 -33
  31. package/dist/cli/tombstones.js.map +1 -1
  32. package/dist/cli/tombstones.test.d.ts +2 -0
  33. package/dist/cli/tombstones.test.d.ts.map +1 -0
  34. package/dist/cli/tombstones.test.js +27 -0
  35. package/dist/cli/tombstones.test.js.map +1 -0
  36. package/dist/cli/watch-event-push-conflict.test.js +7 -0
  37. package/dist/cli/watch-event-push-conflict.test.js.map +1 -1
  38. package/dist/local-path-codec.test.js +20 -0
  39. package/dist/local-path-codec.test.js.map +1 -1
  40. package/dist/s3.d.ts +1 -1
  41. package/dist/s3.d.ts.map +1 -1
  42. package/dist/s3.js +7 -23
  43. package/dist/s3.js.map +1 -1
  44. package/dist/s3.test.js +13 -20
  45. package/dist/s3.test.js.map +1 -1
  46. package/dist/sync-core.d.ts +16 -0
  47. package/dist/sync-core.d.ts.map +1 -1
  48. package/dist/sync-core.js +38 -0
  49. package/dist/sync-core.js.map +1 -1
  50. package/dist/sync-progress.d.ts +18 -1
  51. package/dist/sync-progress.d.ts.map +1 -1
  52. package/dist/sync-progress.js +113 -7
  53. package/dist/sync-progress.js.map +1 -1
  54. package/dist/sync-progress.test.js +32 -2
  55. package/dist/sync-progress.test.js.map +1 -1
  56. package/dist/watcher.d.ts +37 -8
  57. package/dist/watcher.d.ts.map +1 -1
  58. package/dist/watcher.js +137 -23
  59. package/dist/watcher.js.map +1 -1
  60. package/dist/watcher.test.js +116 -9
  61. package/dist/watcher.test.js.map +1 -1
  62. 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";
@@ -363,6 +363,9 @@ function setupFetchMock() {
363
363
  if (urlStr.includes("/sts/vend")) {
364
364
  return { ok: true, status: 200, json: async () => mockVendResponse, text: async () => "" };
365
365
  }
366
+ if (urlStr.includes("/v1/files/tombstones")) {
367
+ return { ok: true, status: 200, json: async () => ({ tombstones: [] }), text: async () => "" };
368
+ }
366
369
  return { ok: false, status: 404, text: async () => "Not found" };
367
370
  });
368
371
  vi.stubGlobal("fetch", fetchMock);
@@ -2979,7 +2982,6 @@ describe("share", () => {
2979
2982
  e.type === "scope-excluded" ||
2980
2983
  e.type === "scope-materialization-gap" ||
2981
2984
  e.type === "ignore-excluded" ||
2982
- e.type === "skip-invalid-scoped-key-summary" ||
2983
2985
  e.type === "not-shipped" ||
2984
2986
  e.type === "push-refused-server-owned-summary" ||
2985
2987
  e.type === "delete-refused-bulk-asymmetry")
@@ -7420,7 +7422,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
7420
7422
  });
7421
7423
  });
7422
7424
  });
7423
- describe("scope-invalid key hardening on push (doubled companies/ tree — incident 2026-07-11)", () => {
7425
+ describe("nested companies directories in a company vault", () => {
7424
7426
  let tmpDir;
7425
7427
  let stateDir;
7426
7428
  beforeEach(() => {
@@ -7449,9 +7451,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7449
7451
  fs.rmSync(stateDir, { recursive: true, force: true });
7450
7452
  delete process.env.HQ_STATE_DIR;
7451
7453
  });
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.
7454
+ it("plans every nested companies/ file for upload as vault-relative content", async () => {
7455
7455
  const companyRoot = path.join(tmpDir, "companies", "acme");
7456
7456
  const canonicalSkill = path.join(companyRoot, "skills", "signals");
7457
7457
  const canonicalAudit = path.join(companyRoot, "audit", "chat-skills", "review");
@@ -7459,17 +7459,17 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7459
7459
  fs.mkdirSync(canonicalAudit, { recursive: true });
7460
7460
  fs.writeFileSync(path.join(canonicalSkill, "SKILL.md"), "skill");
7461
7461
  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");
7462
+ // This matches the live path shape: a literal `companies/acme/...` below
7463
+ // the acme vault root is content, not a scope selector. All 25 objects
7464
+ // must be planned for upload rather than summarized as invalid.
7465
+ for (let index = 0; index < 25; index++) {
7466
+ const nestedAudit = path.join(companyRoot, "companies", "acme", "audit", "chat-skills", `stale-${index.toString().padStart(2, "0")}`);
7467
+ fs.mkdirSync(nestedAudit, { recursive: true });
7468
+ fs.writeFileSync(path.join(nestedAudit, "SKILL.md"), "nested");
7472
7469
  }
7470
+ const midPath = path.join(companyRoot, "audit", "companies", "notes.md");
7471
+ fs.mkdirSync(path.dirname(midPath), { recursive: true });
7472
+ fs.writeFileSync(midPath, "ordinary segment");
7473
7473
  const events = [];
7474
7474
  const result = await share({
7475
7475
  paths: [companyRoot],
@@ -7479,27 +7479,16 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7479
7479
  onEvent: (e) => events.push(e),
7480
7480
  });
7481
7481
  expect(result.aborted).toBe(false);
7482
- expect(result.filesUploaded).toBe(2);
7482
+ expect(result.filesUploaded).toBe(28);
7483
7483
  const uploadedKeys = vi.mocked(uploadFile).mock.calls.map((c) => c[2]);
7484
7484
  expect(uploadedKeys).toEqual(expect.arrayContaining([
7485
7485
  "skills/signals/SKILL.md",
7486
7486
  "audit/chat-skills/review/SKILL.md",
7487
+ "companies/acme/audit/chat-skills/stale-00/SKILL.md",
7488
+ "companies/acme/audit/chat-skills/stale-24/SKILL.md",
7489
+ "audit/companies/notes.md",
7487
7490
  ]));
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
7491
  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
7492
  expect(events.some((e) => e.type === "error")).toBe(false);
7504
7493
  });
7505
7494
  it("never plans an upload for macOS Finder Icon\\r files — silent skip, valid files still ship", async () => {
@@ -7567,11 +7556,9 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7567
7556
  ]);
7568
7557
  expect(events.some((e) => e.type === "error")).toBe(false);
7569
7558
  });
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.
7559
+ it("tombstones an absent nested companies/ key after normal remote verification", async () => {
7560
+ // A nested `companies/` directory is a normal company-vault path, so its
7561
+ // stale journal entry follows the ordinary HEAD-confirmed cleanup path.
7575
7562
  const companyRoot = path.join(tmpDir, "companies", "acme");
7576
7563
  fs.mkdirSync(companyRoot, { recursive: true });
7577
7564
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
@@ -7598,16 +7585,17 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7598
7585
  propagateDeletePolicy: "currency-gated",
7599
7586
  });
7600
7587
  expect(result.aborted).toBe(false);
7601
- // Tombstoned: journal entry dropped without any remote HEAD or DELETE.
7588
+ // The remote HEAD confirms this normal vault-relative key is absent, then
7589
+ // the journal entry is tombstoned without re-uploading it.
7602
7590
  expect(result.filesTombstoned).toBe(1);
7603
7591
  expect(result.filesDeleted).toBe(0);
7604
- expect(headRemoteFile).not.toHaveBeenCalled();
7592
+ expect(headRemoteFile).toHaveBeenCalledWith(expect.anything(), "companies/acme/knowledge/poison.md");
7605
7593
  expect(deleteRemoteFile).not.toHaveBeenCalled();
7606
7594
  expect(uploadFile).not.toHaveBeenCalled();
7607
7595
  const journal = journalAt(journalPath);
7608
7596
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
7609
7597
  });
7610
- it("defers an unreadable scope-invalid symlink tombstone without a fatal error", async () => {
7598
+ it("does not follow a nested symlink that points outside the company vault", async () => {
7611
7599
  const companyRoot = path.join(tmpDir, "companies", "acme");
7612
7600
  const linkKey = "companies/acme/knowledge/unreadable-link";
7613
7601
  const linkPath = path.join(companyRoot, ...linkKey.split("/"));
@@ -7629,44 +7617,28 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7629
7617
  },
7630
7618
  },
7631
7619
  }));
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
7620
  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
- }
7621
+ const result = await share({
7622
+ paths: [companyRoot],
7623
+ company: "acme",
7624
+ vaultConfig: mockConfig,
7625
+ hqRoot: tmpDir,
7626
+ skipUnchanged: true,
7627
+ propagateDeletes: true,
7628
+ propagateDeletePolicy: "currency-gated",
7629
+ onEvent: (event) => events.push(event),
7630
+ });
7631
+ expect(result.filesTombstoned).toBe(0);
7632
+ expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
7633
+ expect(events).toContainEqual({
7634
+ type: "not-shipped",
7635
+ reason: "linked-subtree",
7636
+ count: 1,
7637
+ samplePaths: [linkKey],
7638
+ });
7639
+ expect(events.some((event) => event.type === "error")).toBe(false);
7640
+ const journal = journalAt(journalPath);
7641
+ expect(journal.files[linkKey]).toBeDefined();
7670
7642
  });
7671
7643
  it("personal-mode push still uploads companies/{slug}/ keys (local non-cloud companies are legitimate)", async () => {
7672
7644
  const localCo = path.join(tmpDir, "companies", "localco");
@@ -7688,29 +7660,6 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7688
7660
  expect(events.filter((e) => e.type === "skip-invalid-scoped-key")).toEqual([]);
7689
7661
  });
7690
7662
  });
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
7663
  // ── delete-refusal rendering ───────────────────────────────────────────────
7715
7664
  //
7716
7665
  // Every refusal cause shares ONE event type (`delete-refused-stale-etag`),