@indigoai-us/hq-cloud 6.11.18 → 6.11.20

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 (44) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js.map +1 -1
  4. package/dist/cli/rescue-core.js +1 -1
  5. package/dist/cli/rescue-core.js.map +1 -1
  6. package/dist/cli/share.js +25 -19
  7. package/dist/cli/share.js.map +1 -1
  8. package/dist/cli/share.test.js +63 -9
  9. package/dist/cli/share.test.js.map +1 -1
  10. package/dist/company-resolver.test.js.map +1 -1
  11. package/dist/ignore.d.ts.map +1 -1
  12. package/dist/ignore.js +5 -2
  13. package/dist/ignore.js.map +1 -1
  14. package/dist/ignore.test.js +8 -0
  15. package/dist/ignore.test.js.map +1 -1
  16. package/dist/lib/conflict-file.js +1 -1
  17. package/dist/lib/conflict-file.js.map +1 -1
  18. package/dist/prefix-coalesce.d.ts.map +1 -1
  19. package/dist/prefix-coalesce.js +0 -3
  20. package/dist/prefix-coalesce.js.map +1 -1
  21. package/dist/s3.test.js +0 -1
  22. package/dist/s3.test.js.map +1 -1
  23. package/dist/sync/push-receiver.test.js.map +1 -1
  24. package/dist/vault-client.d.ts +1 -1
  25. package/dist/vault-client.d.ts.map +1 -1
  26. package/dist/vault-client.js +1 -1
  27. package/dist/vault-client.js.map +1 -1
  28. package/dist/vault-client.test.js +15 -0
  29. package/dist/vault-client.test.js.map +1 -1
  30. package/eslint.config.js +67 -0
  31. package/package.json +5 -1
  32. package/src/bin/sync-runner.ts +0 -1
  33. package/src/cli/rescue-core.ts +1 -1
  34. package/src/cli/share.test.ts +79 -11
  35. package/src/cli/share.ts +24 -20
  36. package/src/company-resolver.test.ts +1 -1
  37. package/src/ignore.test.ts +13 -0
  38. package/src/ignore.ts +5 -2
  39. package/src/lib/conflict-file.ts +1 -1
  40. package/src/prefix-coalesce.ts +0 -4
  41. package/src/s3.test.ts +0 -2
  42. package/src/sync/push-receiver.test.ts +1 -1
  43. package/src/vault-client.test.ts +20 -0
  44. package/src/vault-client.ts +2 -2
package/src/cli/share.ts CHANGED
@@ -1156,14 +1156,7 @@ async function executeUploads(
1156
1156
  remoteMeta = await headRemoteFile(run.ctx, relativePath);
1157
1157
  } catch (headErr) {
1158
1158
  if (isAccessDenied(headErr)) {
1159
- run.emit({
1160
- type: "error",
1161
- path: relativePath,
1162
- message:
1163
- "skipped: outside granted ACL scope (server returned 403 " +
1164
- "SCOPE_EXCEEDS_PARENT / access denied on HEAD). Grant this path " +
1165
- "to push it, or it stays local-only.",
1166
- });
1159
+ run.scopeExcludedSet.add(relativePath);
1167
1160
  return;
1168
1161
  }
1169
1162
  throw headErr;
@@ -1325,16 +1318,14 @@ async function executeUploads(
1325
1318
  counters.filesSkipped++;
1326
1319
  return;
1327
1320
  }
1321
+ if (isAccessDenied(err)) {
1322
+ run.scopeExcludedSet.add(relativePath);
1323
+ return;
1324
+ }
1328
1325
  run.emit({
1329
1326
  type: "error",
1330
1327
  path: relativePath,
1331
- message: isAccessDenied(err)
1332
- ? "skipped: outside granted ACL scope (server returned 403 " +
1333
- "SCOPE_EXCEEDS_PARENT / access denied on PUT). Grant this path " +
1334
- "to push it, or it stays local-only."
1335
- : err instanceof Error
1336
- ? err.message
1337
- : String(err),
1328
+ message: err instanceof Error ? err.message : String(err),
1338
1329
  });
1339
1330
  }
1340
1331
  };
@@ -2256,12 +2247,25 @@ async function computeDeletePlan(
2256
2247
  for (let i = 0; i < headCandidates.length; i += DELETE_PLAN_HEAD_CONCURRENCY) {
2257
2248
  const chunk = headCandidates.slice(i, i + DELETE_PLAN_HEAD_CONCURRENCY);
2258
2249
  const results = await Promise.all(
2259
- chunk.map(async (c) => ({
2260
- candidate: c,
2261
- remote: await headRemoteFile(ctx, c.key),
2262
- })),
2250
+ chunk.map(async (c) => {
2251
+ try {
2252
+ return {
2253
+ candidate: c,
2254
+ remote: await headRemoteFile(ctx, c.key),
2255
+ scopeExcluded: false,
2256
+ };
2257
+ } catch (err) {
2258
+ if (isAccessDenied(err)) {
2259
+ return { candidate: c, remote: null, scopeExcluded: true };
2260
+ }
2261
+ throw err;
2262
+ }
2263
+ }),
2263
2264
  );
2264
- for (const { candidate, remote } of results) {
2265
+ for (const { candidate, remote, scopeExcluded } of results) {
2266
+ if (scopeExcluded) {
2267
+ continue;
2268
+ }
2265
2269
  if (remote === null) {
2266
2270
  plan.toTombstone.push(candidate.key);
2267
2271
  continue;
@@ -4,7 +4,7 @@
4
4
  * the trailing-slash sibling boundary, and the unattributed (no-match) path.
5
5
  */
6
6
 
7
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
7
+ import { describe, it, expect, afterEach } from "vitest";
8
8
  import * as fs from "fs";
9
9
  import * as os from "os";
10
10
  import * as path from "path";
@@ -49,6 +49,19 @@ describe("createIgnoreFilter", () => {
49
49
  expect(shouldSync(path.join(hqRoot, "node_modules/react/index.js"))).toBe(false);
50
50
  });
51
51
 
52
+ it("repos/ exclusion is root-anchored to hqRoot (DEV-1791)", () => {
53
+ const shouldSync = createIgnoreFilter(hqRoot);
54
+ expect(shouldSync(path.join(hqRoot, "repos/private/hq-cloud/src/x.ts"))).toBe(false);
55
+ expect(shouldSync(path.join(hqRoot, "repos/public/hq-core/README.md"))).toBe(false);
56
+ expect(
57
+ shouldSync(path.join(hqRoot, "companies/lutiq/knowledge/repos/dev-docs-mcp/overview.md")),
58
+ ).toBe(true);
59
+ expect(shouldSync(path.join(hqRoot, "companies/indigo/knowledge/repos/foo/notes.md"))).toBe(
60
+ true,
61
+ );
62
+ expect(shouldSync(path.join(hqRoot, "companies/lutiq/knowledge/repos"), true)).toBe(true);
63
+ });
64
+
52
65
  it("permissive mode: .hqignore patterns are honored", () => {
53
66
  fs.writeFileSync(path.join(hqRoot, ".hqignore"), "companies/*/data/\n");
54
67
  const shouldSync = createIgnoreFilter(hqRoot);
package/src/ignore.ts CHANGED
@@ -119,8 +119,11 @@ export const DEFAULT_IGNORES = [
119
119
  "**/.claude/state/",
120
120
  "**/.claude/audit/",
121
121
 
122
- // HQ repos directory (managed separately, not synced)
123
- "repos/",
122
+ // HQ repos directory (managed separately, not synced). Root-anchored so
123
+ // only hqRoot/repos/ is excluded; bare `repos/` would match any nested
124
+ // directory named repos (for example companies/<co>/knowledge/repos/),
125
+ // silently dropping discover-generated knowledge (DEV-1791).
126
+ "/repos/",
124
127
 
125
128
  // Secrets / env
126
129
  ".env",
@@ -76,6 +76,6 @@ export function buildConflictId(
76
76
  detectedAt: string,
77
77
  ): string {
78
78
  const safeTs = detectedAt.replace(/:/g, "-").replace(/\.\d+/, "");
79
- const safePath = originalRelative.replace(/[\/\\.]/g, "-");
79
+ const safePath = originalRelative.replace(/[/\\.]/g, "-");
80
80
  return `${safePath}-${safeTs}`;
81
81
  }
@@ -42,10 +42,6 @@ const SCOPE_PREFIX_MARKER = "\u0000hq-scope:";
42
42
  const SCOPE_PREFIX_MARKER_PREFIX = `${SCOPE_PREFIX_MARKER}prefix`;
43
43
  const SCOPE_PREFIX_MARKER_EXACT = `${SCOPE_PREFIX_MARKER}exact`;
44
44
 
45
- function isScopePrefixEntry(value: ScopePrefixInput): value is ScopePrefixEntry {
46
- return typeof value !== "string";
47
- }
48
-
49
45
  function inferStringMatch(prefix: string): ScopePrefixMatch {
50
46
  return prefix === "" || prefix.endsWith("/") ? "prefix" : "exact";
51
47
  }
package/src/s3.test.ts CHANGED
@@ -294,8 +294,6 @@ describe("uploadFile", () => {
294
294
  it("preserves the existing created-at on re-upload (NEW-pill ageing window)", async () => {
295
295
  // First upload happened a week ago; second run must keep that timestamp
296
296
  // so the hq-console "NEW" pill doesn't reset on every sync tick.
297
- const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
298
-
299
297
  // Override the FakeS3Client's HeadObject to return the legacy timestamp
300
298
  // for this one test. The mock factory returns a fresh object per send
301
299
  // invocation so we patch at the class level via a one-shot wrapper.
@@ -22,7 +22,7 @@
22
22
  * fake SQS client / in-memory fanout.
23
23
  */
24
24
 
25
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
25
+ import { describe, expect, it } from "vitest";
26
26
 
27
27
  import { encodePushEvent, type PushEvent } from "./push-event.js";
28
28
  import { StaticFlagProvider } from "./feature-flags.js";
@@ -830,6 +830,26 @@ describe("listMyExplicitGrants (US-004)", () => {
830
830
  expect(init.body).toBeUndefined();
831
831
  });
832
832
 
833
+ it("parses creator-sourced grants", async () => {
834
+ const grant: ExplicitGrant = {
835
+ companyUid: "cmp_abc",
836
+ path: "personal/psn_creator",
837
+ permission: "admin",
838
+ source: "creator",
839
+ };
840
+ fetchSpy.mockResolvedValueOnce(
841
+ jsonResponse(200, {
842
+ grants: [grant],
843
+ computedAt: "2026-05-20T12:00:00.000Z",
844
+ }),
845
+ );
846
+
847
+ const grants = await client.listMyExplicitGrants("cmp_abc");
848
+
849
+ expect(grants[0]?.source).toBe("creator");
850
+ expect(grants).toEqual([grant]);
851
+ });
852
+
833
853
  it("URL-encodes the companyUid query parameter", async () => {
834
854
  fetchSpy.mockResolvedValueOnce(
835
855
  jsonResponse(200, { grants: [], computedAt: "2026-05-20T12:00:00.000Z" }),
@@ -187,7 +187,7 @@ export interface CreateEntityResult {
187
187
  * `granteeType: 'company-wide'` row. Both mean "every active member of
188
188
  * this company sees this prefix".
189
189
  */
190
- export type GrantSource = "person" | "email" | "group" | "open";
190
+ export type GrantSource = "creator" | "person" | "email" | "group" | "open";
191
191
 
192
192
  /** Permission level surfaced on a grant row. Matches `AclPermission`. */
193
193
  export type GrantPermission = "read" | "write" | "admin";
@@ -478,7 +478,7 @@ type VaultResponseSchema<T> = z.ZodType<T>;
478
478
  const membershipRoleSchema = z.enum(["owner", "admin", "member", "guest"]);
479
479
  const membershipStatusSchema = z.enum(["pending", "active", "revoked"]);
480
480
  const grantPermissionSchema = z.enum(["read", "write", "admin"]);
481
- const grantSourceSchema = z.enum(["person", "email", "group", "open"]);
481
+ const grantSourceSchema = z.enum(["creator", "person", "email", "group", "open"]);
482
482
  const presignOpSchema = z.enum(["get", "put", "delete"]);
483
483
  const syncModeSchema = z.enum(["shared", "all", "custom"]);
484
484
  const vendPurposeSchema = z.enum(["sync", "browse"]);