@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.
- package/dist/bin/sync-runner-company.d.ts +3 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +50 -14
- 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 +33 -1
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +5 -5
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +3 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +96 -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 +48 -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 +40 -59
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cli/tombstones.d.ts +23 -8
- package/dist/cli/tombstones.d.ts.map +1 -1
- package/dist/cli/tombstones.js +63 -33
- package/dist/cli/tombstones.js.map +1 -1
- package/dist/cli/tombstones.test.d.ts +2 -0
- package/dist/cli/tombstones.test.d.ts.map +1 -0
- package/dist/cli/tombstones.test.js +27 -0
- package/dist/cli/tombstones.test.js.map +1 -0
- package/dist/cli/watch-event-push-conflict.test.js +7 -0
- package/dist/cli/watch-event-push-conflict.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 +37 -8
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +137 -23
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +116 -9
- 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";
|
|
@@ -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("
|
|
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("
|
|
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
|
-
//
|
|
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");
|
|
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(
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
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).
|
|
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("
|
|
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
|
-
|
|
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
|
-
}
|
|
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`),
|