@indigoai-us/hq-cloud 6.11.0 → 6.11.2
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.d.ts +1 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +31 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +117 -1
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/sync-scope.test.js +31 -0
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +17 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/sync/pull-scope.d.ts.map +1 -1
- package/dist/sync/pull-scope.js +15 -0
- package/dist/sync/pull-scope.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +137 -0
- package/src/bin/sync-runner.ts +30 -1
- package/src/cli/sync-scope.test.ts +37 -0
- package/src/cli/sync.ts +18 -1
- package/src/sync/pull-scope.ts +14 -0
|
@@ -190,6 +190,43 @@ describe("sync — scope-aware download (US-005)", () => {
|
|
|
190
190
|
expect(result.filesOutOfScope).toBe(2);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
// FILE_TOMBSTONE fetch is COMPANY-scoped (GET /v1/files/tombstones?company=…).
|
|
194
|
+
// For the personal vault the target uid is a personUid (prs_…), which the
|
|
195
|
+
// server correctly 403s ("No active membership for caller in company prs_…"),
|
|
196
|
+
// spamming hq-pro's Sentry with a per-user no-membership cluster. The pull
|
|
197
|
+
// must SKIP the fetch for the personal target (delete-resync was never a
|
|
198
|
+
// committed personal-vault feature). Differential: company-mode fetches,
|
|
199
|
+
// personal-mode does not.
|
|
200
|
+
const hitTombstones = (m: ReturnType<typeof setupFetchMock>) =>
|
|
201
|
+
m.mock.calls.some((c: unknown[]) =>
|
|
202
|
+
String(c[0]).includes("/v1/files/tombstones"),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
it("company-mode pull fetches FILE_TOMBSTONEs (baseline for the personal guard)", async () => {
|
|
206
|
+
const fetchMock = setupFetchMock();
|
|
207
|
+
await sync({
|
|
208
|
+
company: "acme",
|
|
209
|
+
vaultConfig: mockConfig,
|
|
210
|
+
hqRoot: tmpDir,
|
|
211
|
+
syncMode: "all",
|
|
212
|
+
prefixSet: [],
|
|
213
|
+
});
|
|
214
|
+
expect(hitTombstones(fetchMock)).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("personal-vault pull SKIPS the company-scoped tombstone fetch (no 403 → no Sentry no-membership spam)", async () => {
|
|
218
|
+
const fetchMock = setupFetchMock();
|
|
219
|
+
await sync({
|
|
220
|
+
company: "acme",
|
|
221
|
+
vaultConfig: mockConfig,
|
|
222
|
+
hqRoot: tmpDir,
|
|
223
|
+
syncMode: "all",
|
|
224
|
+
prefixSet: [],
|
|
225
|
+
personalMode: true,
|
|
226
|
+
});
|
|
227
|
+
expect(hitTombstones(fetchMock)).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
193
230
|
it("is idempotent: a second shared pull downloads and removes nothing", async () => {
|
|
194
231
|
const opts = {
|
|
195
232
|
company: "acme",
|
package/src/cli/sync.ts
CHANGED
|
@@ -708,7 +708,24 @@ export async function sync(options: SyncOptions): Promise<SyncResult> {
|
|
|
708
708
|
// a clean read of `ctx`; best-effort — a failed read degrades to an empty map
|
|
709
709
|
// (no suppression), preserving the pre-fix behavior. ctx.uid is the verified
|
|
710
710
|
// companyUid the tombstone rows are keyed under.
|
|
711
|
-
|
|
711
|
+
//
|
|
712
|
+
// SKIP for the personal vault: its `ctx.uid` is a personUid (`prs_…`), but
|
|
713
|
+
// `GET /v1/files/tombstones?company=…` is COMPANY-scoped server-side
|
|
714
|
+
// (findCallerWithMembership), so a personal-vault request resolves
|
|
715
|
+
// `company=prs_…` to no membership and is correctly rejected with
|
|
716
|
+
// `403 "No active membership for caller in company prs_…"`. That 403 is
|
|
717
|
+
// benign for the pull (it already degrades to the empty map below), but
|
|
718
|
+
// hq-pro captures EVERY one as a Sentry warning — the per-personal-vault
|
|
719
|
+
// no-membership cluster (one Sentry issue per signed-in user). Personal-vault
|
|
720
|
+
// delete-resync was never a committed feature and there is no person-scoped
|
|
721
|
+
// tombstone path, so for the personal target we skip the fetch and use an
|
|
722
|
+
// empty map — byte-for-byte the current degraded behavior, minus the 403 spam.
|
|
723
|
+
// FUTURE FOLLOW-UP (not built here): if personal-vault delete-resync is
|
|
724
|
+
// wanted, it needs a real person-scoped tombstone endpoint + client read.
|
|
725
|
+
const tombstones =
|
|
726
|
+
options.personalMode === true
|
|
727
|
+
? new Map<string, CompanyTombstone>()
|
|
728
|
+
: await fetchCompanyTombstones(vaultConfig, ctx.uid);
|
|
712
729
|
|
|
713
730
|
// Stage 1: classify every remote file against the journal + local disk.
|
|
714
731
|
// Hashing happens here (not in the transfer loop) so the plan event below
|
package/src/sync/pull-scope.ts
CHANGED
|
@@ -82,6 +82,20 @@ export async function resolvePullScope(
|
|
|
82
82
|
const memberships = await client.listMyMemberships();
|
|
83
83
|
const m = memberships.find((x) => x.companyUid === companyUid);
|
|
84
84
|
if (!m) return { syncMode: "all" };
|
|
85
|
+
// Agent memberships (`agt_…#cmp_…`) belong to an agent identity that owns NO
|
|
86
|
+
// person entity. hq-pro's per-membership sync-config endpoint sits behind an
|
|
87
|
+
// up-front person-gate, so an agent caller is rejected with
|
|
88
|
+
// `403 "Caller has no person entity — call POST /entity with type=person
|
|
89
|
+
// first"` BEFORE the route's own ownership check ever runs — captured
|
|
90
|
+
// server-side as the Sentry warning HQ-1R (one event per agent sync pass).
|
|
91
|
+
// Agents never set a scoped sync-config (that is a human-only menubar
|
|
92
|
+
// action) and a failed/absent config already degrades to `all` here, so the
|
|
93
|
+
// call is guaranteed to 403 and change nothing. Skip it for agent
|
|
94
|
+
// memberships and resolve `all` directly — behavior-preserving, minus the
|
|
95
|
+
// 403 spam. (A server-side alternative would be to exempt sync-config from
|
|
96
|
+
// the up-front person-gate and let its own `authorizeSyncConfigAccess`
|
|
97
|
+
// handle agent ownership; tracked as a possible follow-up.)
|
|
98
|
+
if (m.membershipKey?.startsWith("agt_")) return { syncMode: "all" };
|
|
85
99
|
const cfg = await client.getMembershipSyncConfig(m.membershipKey);
|
|
86
100
|
if (cfg.syncMode === "all") return { syncMode: "all" };
|
|
87
101
|
|