@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.
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +48 -3
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +135 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +22 -6
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +2 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +93 -16
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +5 -17
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +16 -62
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +57 -89
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +16 -5
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +16 -63
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +28 -54
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/local-path-codec.test.js +20 -0
- package/dist/local-path-codec.test.js.map +1 -1
- package/dist/s3.d.ts +1 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +7 -23
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +13 -20
- package/dist/s3.test.js.map +1 -1
- package/dist/sync-core.d.ts +16 -0
- package/dist/sync-core.d.ts.map +1 -1
- package/dist/sync-core.js +38 -0
- package/dist/sync-core.js.map +1 -1
- package/dist/sync-progress.d.ts +18 -1
- package/dist/sync-progress.d.ts.map +1 -1
- package/dist/sync-progress.js +113 -7
- package/dist/sync-progress.js.map +1 -1
- package/dist/sync-progress.test.js +32 -2
- package/dist/sync-progress.test.js.map +1 -1
- package/dist/watcher.d.ts +26 -5
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +93 -13
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +45 -1
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/share.test.js
CHANGED
|
@@ -52,7 +52,7 @@ vi.mock("readline", () => ({
|
|
|
52
52
|
})),
|
|
53
53
|
}));
|
|
54
54
|
import * as readline from "readline";
|
|
55
|
-
import { share,
|
|
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("
|
|
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("
|
|
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
|
|
7459
|
-
|
|
7460
|
-
fs.
|
|
7461
|
-
fs.mkdirSync(
|
|
7462
|
-
fs.writeFileSync(path.join(
|
|
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(
|
|
7479
|
+
expect(result.filesUploaded).toBe(28);
|
|
7473
7480
|
const uploadedKeys = vi.mocked(uploadFile).mock.calls.map((c) => c[2]);
|
|
7474
|
-
expect(uploadedKeys).toEqual([
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
"companies/
|
|
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
|
|
7549
|
-
//
|
|
7550
|
-
//
|
|
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
|
-
//
|
|
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).
|
|
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("
|
|
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
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
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`),
|