@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.
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +48 -12
- 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.d.ts +0 -5
- 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 +7 -2
- 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 -77
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +45 -99
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +16 -18
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +16 -72
- 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 +14 -5
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +40 -12
- 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";
|
|
@@ -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("
|
|
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("
|
|
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
|
-
//
|
|
7463
|
-
//
|
|
7464
|
-
//
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
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(
|
|
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
|
|
7571
|
-
//
|
|
7572
|
-
//
|
|
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
|
-
//
|
|
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).
|
|
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("
|
|
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
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
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`),
|