@indigoai-us/hq-cloud 6.15.97 → 6.15.99

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 (48) hide show
  1. package/dist/bin/sync-runner-auth-wait.d.ts +40 -0
  2. package/dist/bin/sync-runner-auth-wait.d.ts.map +1 -0
  3. package/dist/bin/sync-runner-auth-wait.js +70 -0
  4. package/dist/bin/sync-runner-auth-wait.js.map +1 -0
  5. package/dist/bin/sync-runner-auth-wait.test.d.ts +2 -0
  6. package/dist/bin/sync-runner-auth-wait.test.d.ts.map +1 -0
  7. package/dist/bin/sync-runner-auth-wait.test.js +148 -0
  8. package/dist/bin/sync-runner-auth-wait.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  10. package/dist/bin/sync-runner-company.js +13 -6
  11. package/dist/bin/sync-runner-company.js.map +1 -1
  12. package/dist/bin/sync-runner-company.test.js +74 -0
  13. package/dist/bin/sync-runner-company.test.js.map +1 -1
  14. package/dist/bin/sync-runner-planning.d.ts.map +1 -1
  15. package/dist/bin/sync-runner-planning.js +10 -2
  16. package/dist/bin/sync-runner-planning.js.map +1 -1
  17. package/dist/bin/sync-runner-watch-loop.js +5 -5
  18. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  19. package/dist/bin/sync-runner.d.ts +25 -2
  20. package/dist/bin/sync-runner.d.ts.map +1 -1
  21. package/dist/bin/sync-runner.js +123 -7
  22. package/dist/bin/sync-runner.js.map +1 -1
  23. package/dist/bin/sync-runner.test.js +327 -8
  24. package/dist/bin/sync-runner.test.js.map +1 -1
  25. package/dist/cli/share.d.ts.map +1 -1
  26. package/dist/cli/share.js +48 -6
  27. package/dist/cli/share.js.map +1 -1
  28. package/dist/cli/share.test.js +37 -0
  29. package/dist/cli/share.test.js.map +1 -1
  30. package/dist/cli/sync.d.ts.map +1 -1
  31. package/dist/cli/sync.js +47 -5
  32. package/dist/cli/sync.js.map +1 -1
  33. package/dist/cognito-auth.d.ts +32 -0
  34. package/dist/cognito-auth.d.ts.map +1 -1
  35. package/dist/cognito-auth.js +46 -1
  36. package/dist/cognito-auth.js.map +1 -1
  37. package/dist/cognito-auth.test.js +49 -0
  38. package/dist/cognito-auth.test.js.map +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/outposts/self-deploy.d.ts.map +1 -1
  44. package/dist/outposts/self-deploy.js +15 -3
  45. package/dist/outposts/self-deploy.js.map +1 -1
  46. package/dist/outposts/self-deploy.test.js +29 -1
  47. package/dist/outposts/self-deploy.test.js.map +1 -1
  48. package/package.json +1 -1
@@ -11,6 +11,7 @@ import * as fs from "fs";
11
11
  import * as os from "os";
12
12
  import * as path from "path";
13
13
  import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, resolveSkipCompanies, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, SESSIONS_SCOPE_PREFIX, defaultCollectTelemetry, AUTH_REQUIRED_PASS_EXIT, } from "./sync-runner.js";
14
+ import { AUTH_WAIT_INITIAL_RECHECK_MS, AUTH_WAIT_RESUME_MESSAGE, AUTH_WAIT_START_MESSAGE, AUTH_WAIT_STEADY_RECHECK_MS, } from "./sync-runner-auth-wait.js";
14
15
  import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
15
16
  import { setBandwidthGovernorForTesting } from "../bandwidth.js";
16
17
  import { FakeClock, TreeWatcher, } from "../watcher.js";
@@ -25,7 +26,7 @@ import { getStateStoreSnapshotRecoveryCountForTest, resetStateStoreSnapshotRecov
25
26
  import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
26
27
  import { isCoveredByAny } from "../prefix-coalesce.js";
27
28
  import { VaultAuthError, VaultNotFoundError } from "../vault-client.js";
28
- import { CognitoRefreshError } from "../cognito-auth.js";
29
+ import { CognitoAuthError, CognitoRefreshError } from "../cognito-auth.js";
29
30
  import { reconcileCompanyManifest, } from "../manifest-reconcile.js";
30
31
  import yaml from "js-yaml";
31
32
  // ---------------------------------------------------------------------------
@@ -323,6 +324,20 @@ describe("auth", () => {
323
324
  ]);
324
325
  expect(deps.stdout.events()).toEqual([]);
325
326
  });
327
+ it("honors authRequiredExitCode when token fetch fails", async () => {
328
+ const deps = makeDeps({
329
+ getAccessToken: vi.fn().mockRejectedValue(new Error("no cached tokens")),
330
+ authRequiredExitCode: 18,
331
+ });
332
+ const code = await runRunner(["--companies"], deps);
333
+ expect(code).toBe(18);
334
+ expect(deps.stderr.events()).toEqual([
335
+ {
336
+ type: "auth-error",
337
+ message: "Your HQ session needs a quick refresh. Sign in again to keep sync moving.",
338
+ },
339
+ ]);
340
+ });
326
341
  it("keeps the watch pass retryable when initial refresh fails transiently", async () => {
327
342
  const deps = makeDeps({
328
343
  getAccessToken: vi
@@ -403,6 +418,123 @@ describe("auth", () => {
403
418
  });
404
419
  });
405
420
  // ---------------------------------------------------------------------------
421
+ // mid-pass identity refusal (HQ-DESKTOP-5S)
422
+ //
423
+ // The up-front token resolves fine; the Cognito refusal surfaces LATER — while
424
+ // planning the personal target, or mid-fanout when the pull leg re-resolves the
425
+ // bearer through the getter. A terminal refusal (reauth required) must clear the
426
+ // session and emit exactly one auth-error, exiting 0. A retryable refusal must
427
+ // exit TRANSIENT_NETWORK_EXIT (75) without clearing the session, and NEITHER may
428
+ // ever be recorded as a per-file/per-company error (which would exit 2).
429
+ // ---------------------------------------------------------------------------
430
+ describe("mid-pass identity refusal", () => {
431
+ const REAUTH = "Your HQ session needs a quick refresh. Sign in again to keep sync moving.";
432
+ const personalEntity = {
433
+ uid: "ent_person_1",
434
+ type: "person",
435
+ bucketName: "hq-vault-personal-1",
436
+ status: "active",
437
+ };
438
+ it("planning: a terminal refusal clears the session and exits 0 with one auth-error", async () => {
439
+ const deps = makeDeps({
440
+ createVaultClient: () => makeVaultStub({
441
+ listPersons: () => Promise.reject(new CognitoRefreshError(REAUTH, true, 401, "fp-planning")),
442
+ }),
443
+ });
444
+ const code = await runRunner(["--personal"], deps);
445
+ expect(code).toBe(0);
446
+ expect(deps.clearSession).toHaveBeenCalledTimes(1);
447
+ expect(deps.clearSession).toHaveBeenCalledWith("fp-planning");
448
+ expect(deps.stderr.events()).toEqual([
449
+ { type: "auth-error", message: REAUTH },
450
+ ]);
451
+ });
452
+ it("planning: a retryable refusal exits TRANSIENT_NETWORK_EXIT and keeps the session", async () => {
453
+ const deps = makeDeps({
454
+ createVaultClient: () => makeVaultStub({
455
+ listPersons: () => Promise.reject(new CognitoRefreshError("Cognito refresh failed (503): service unavailable", false, 503, "fp-planning-retry")),
456
+ }),
457
+ });
458
+ const code = await runRunner(["--personal"], deps);
459
+ expect(code).toBe(TRANSIENT_NETWORK_EXIT);
460
+ expect(deps.clearSession).not.toHaveBeenCalled();
461
+ // Never an auth-error: a retryable blip must not sign the user out.
462
+ expect(deps.stderr.events().some((e) => e.type === "auth-error")).toBe(false);
463
+ });
464
+ it("mid-fanout: a terminal refusal from the pull leg clears the session and exits 0", async () => {
465
+ const refusal = new CognitoRefreshError(REAUTH, true, 401, "fp-fanout");
466
+ const deps = makeDeps({
467
+ sync: vi.fn().mockRejectedValue(refusal),
468
+ createVaultClient: () => makeVaultStub({ listPersons: () => Promise.resolve([personalEntity]) }),
469
+ });
470
+ const code = await runRunner(["--personal"], deps);
471
+ expect(code).toBe(0);
472
+ expect(deps.clearSession).toHaveBeenCalledTimes(1);
473
+ expect(deps.clearSession).toHaveBeenCalledWith("fp-fanout");
474
+ const stderr = deps.stderr.events();
475
+ expect(stderr.filter((e) => e.type === "auth-error")).toEqual([
476
+ { type: "auth-error", message: REAUTH },
477
+ ]);
478
+ // The refusal is NEVER fanned out into a per-file/per-company error event.
479
+ expect(stderr.some((e) => e.type === "error")).toBe(false);
480
+ });
481
+ it("mid-fanout: a bare CognitoAuthError from the pull leg is terminal too", async () => {
482
+ const refusal = new CognitoAuthError("interactive login required");
483
+ const deps = makeDeps({
484
+ sync: vi.fn().mockRejectedValue(refusal),
485
+ createVaultClient: () => makeVaultStub({ listPersons: () => Promise.resolve([personalEntity]) }),
486
+ });
487
+ const code = await runRunner(["--personal"], deps);
488
+ expect(code).toBe(0);
489
+ expect(deps.clearSession).toHaveBeenCalledTimes(1);
490
+ expect(deps.stderr.events().filter((e) => e.type === "auth-error")).toEqual([{ type: "auth-error", message: REAUTH }]);
491
+ expect(deps.stderr.events().some((e) => e.type === "error")).toBe(false);
492
+ });
493
+ it("mid-fanout: a retryable refusal exits TRANSIENT_NETWORK_EXIT, keeps the session, and never exits 2", async () => {
494
+ const refusal = new CognitoRefreshError("Cognito refresh failed (503): service unavailable", false, 503, "fp-fanout-retry");
495
+ const deps = makeDeps({
496
+ sync: vi.fn().mockRejectedValue(refusal),
497
+ createVaultClient: () => makeVaultStub({ listPersons: () => Promise.resolve([personalEntity]) }),
498
+ });
499
+ const code = await runRunner(["--personal"], deps);
500
+ expect(code).toBe(TRANSIENT_NETWORK_EXIT);
501
+ expect(code).not.toBe(PARTIAL_SYNC_EXIT);
502
+ expect(deps.clearSession).not.toHaveBeenCalled();
503
+ const stderr = deps.stderr.events();
504
+ expect(stderr.some((e) => e.type === "auth-error")).toBe(false);
505
+ // A session-level refusal must not be recorded as a company error (exit 2).
506
+ expect(stderr.some((e) => e.type === "error")).toBe(false);
507
+ });
508
+ it("company planning: a terminal refusal from entity lookup clears the session and exits 0", async () => {
509
+ // buildFanoutPlan resolves each company via entity.get through the bearer
510
+ // getter; a refusal there must propagate to the run-level boundary, not be
511
+ // swallowed into a deferred/best-effort target.
512
+ const deps = makeDeps({
513
+ createVaultClient: () => makeVaultStub({
514
+ memberships: [{ companyUid: "cmp_x" }],
515
+ entityGet: () => Promise.reject(new CognitoRefreshError(REAUTH, true, 401, "fp-plan")),
516
+ }),
517
+ });
518
+ const code = await runRunner(["--companies"], deps);
519
+ expect(code).toBe(0);
520
+ expect(deps.clearSession).toHaveBeenCalledTimes(1);
521
+ expect(deps.clearSession).toHaveBeenCalledWith("fp-plan");
522
+ expect(deps.stderr.events().filter((e) => e.type === "auth-error")).toEqual([{ type: "auth-error", message: REAUTH }]);
523
+ });
524
+ it("company planning: a retryable refusal from entity lookup exits 75 and keeps the session", async () => {
525
+ const deps = makeDeps({
526
+ createVaultClient: () => makeVaultStub({
527
+ memberships: [{ companyUid: "cmp_x" }],
528
+ entityGet: () => Promise.reject(new CognitoRefreshError("Cognito refresh failed (503): service unavailable", false, 503)),
529
+ }),
530
+ });
531
+ const code = await runRunner(["--companies"], deps);
532
+ expect(code).toBe(TRANSIENT_NETWORK_EXIT);
533
+ expect(deps.clearSession).not.toHaveBeenCalled();
534
+ expect(deps.stderr.events().some((e) => e.type === "auth-error")).toBe(false);
535
+ });
536
+ });
537
+ // ---------------------------------------------------------------------------
406
538
  // memberships retry (3x with backoff, then abort)
407
539
  // ---------------------------------------------------------------------------
408
540
  //
@@ -6814,21 +6946,49 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6814
6946
  expect(code).toBe(1);
6815
6947
  expect(watcher.disposed).toBe(false);
6816
6948
  });
6817
- it("an exit-0 auth-required pass stops the unattended loop without reporting a crash", async () => {
6949
+ it("an auth-required watch pass parks in-process instead of exiting", async () => {
6818
6950
  const watcher = makeWatcherStub();
6819
6951
  const runPass = vi.fn().mockResolvedValue(AUTH_REQUIRED_PASS_EXIT);
6820
- const sleep = vi.fn().mockResolvedValue(undefined);
6821
- const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
6952
+ const delays = [];
6953
+ const waiters = [];
6954
+ const sleep = (ms) => {
6955
+ delays.push(ms);
6956
+ return new Promise((resolve) => {
6957
+ waiters.push(resolve);
6958
+ });
6959
+ };
6960
+ const authWaitLog = vi.fn();
6961
+ const getAccessToken = vi.fn(async () => {
6962
+ throw new Error("invalid_grant");
6963
+ });
6964
+ let triggerShutdown = () => { };
6965
+ let settled;
6966
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
6822
6967
  runPass,
6823
6968
  sleep,
6969
+ authWaitLog,
6970
+ getAccessToken,
6824
6971
  clock: new FakeClock(),
6825
6972
  createWatcher: () => watcher,
6826
- onShutdownSignal: () => () => { },
6973
+ startEventSync: vi.fn().mockResolvedValue(null),
6974
+ onShutdownSignal: (handler) => {
6975
+ triggerShutdown = handler;
6976
+ return () => { };
6977
+ },
6978
+ }).then((code) => {
6979
+ settled = code;
6980
+ return code;
6827
6981
  });
6828
- expect(code).toBe(0);
6982
+ await flushLoopMicrotasks(30);
6983
+ expect(settled).toBeUndefined();
6829
6984
  expect(runPass).toHaveBeenCalledTimes(1);
6830
- expect(sleep).not.toHaveBeenCalled();
6831
- expect(watcher.disposed).toBe(false);
6985
+ expect(authWaitLog).toHaveBeenCalledWith(AUTH_WAIT_START_MESSAGE);
6986
+ expect(delays).toEqual([AUTH_WAIT_INITIAL_RECHECK_MS]);
6987
+ expect(getAccessToken).not.toHaveBeenCalled();
6988
+ triggerShutdown();
6989
+ await expect(loop).resolves.toBe(0);
6990
+ expect(runPass).toHaveBeenCalledTimes(1);
6991
+ expect(getAccessToken).not.toHaveBeenCalled();
6832
6992
  });
6833
6993
  it("a TRANSIENT_NETWORK_EXIT poll pass does NOT surface as a crash — the loop retries (HQ-SYNC-1W)", async () => {
6834
6994
  // A transient offline blip must keep the watcher alive: the loop logs and
@@ -6985,6 +7145,165 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6985
7145
  await loop;
6986
7146
  });
6987
7147
  });
7148
+ describe("runRunnerWithLoop — watch-mode auth-wait", () => {
7149
+ function makePendingSleep() {
7150
+ const delays = [];
7151
+ const waiters = [];
7152
+ return {
7153
+ delays,
7154
+ sleep: (ms) => {
7155
+ delays.push(ms);
7156
+ return new Promise((resolve) => {
7157
+ waiters.push(resolve);
7158
+ });
7159
+ },
7160
+ tick() {
7161
+ const resolve = waiters.shift();
7162
+ if (!resolve)
7163
+ throw new Error("auth-wait sleep was not pending");
7164
+ resolve();
7165
+ },
7166
+ };
7167
+ }
7168
+ async function waitUntil(pred, label) {
7169
+ for (let i = 0; i < 200; i++) {
7170
+ if (pred())
7171
+ return;
7172
+ await Promise.resolve();
7173
+ if (i % 10 === 9) {
7174
+ await new Promise((resolve) => setImmediate(resolve));
7175
+ }
7176
+ }
7177
+ throw new Error(`timed out waiting for ${label}`);
7178
+ }
7179
+ it("one-shot auth failure still exits with authRequiredExitCode and never waits", async () => {
7180
+ const clock = makePendingSleep();
7181
+ const authWaitLog = vi.fn();
7182
+ const runPass = vi.fn().mockResolvedValue(AUTH_REQUIRED_PASS_EXIT);
7183
+ const startEventSync = vi.fn().mockResolvedValue(null);
7184
+ const code = await runRunnerWithLoop(["--companies", "--hq-root", "/tmp/hq"], {
7185
+ runPass,
7186
+ sleep: clock.sleep,
7187
+ authWaitLog,
7188
+ startEventSync,
7189
+ getAccessToken: async () => {
7190
+ throw new Error("invalid_grant");
7191
+ },
7192
+ onShutdownSignal: () => () => { },
7193
+ });
7194
+ expect(code).toBe(AUTH_REQUIRED_PASS_EXIT);
7195
+ expect(runPass).toHaveBeenCalledTimes(1);
7196
+ expect(authWaitLog).not.toHaveBeenCalled();
7197
+ expect(clock.delays).toEqual([]);
7198
+ expect(startEventSync).not.toHaveBeenCalled();
7199
+ });
7200
+ it("watch-mode auth failure waits in-process, rechecks on the injected cadence, and resumes a full startup", async () => {
7201
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7202
+ const previousEventSync = process.env.HQ_SYNC_EVENT_SYNC;
7203
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = "0";
7204
+ delete process.env.HQ_SYNC_EVENT_SYNC;
7205
+ const clock = makePendingSleep();
7206
+ const authWaitLog = vi.fn();
7207
+ const startEventSync = vi.fn().mockResolvedValue(null);
7208
+ const listMyMemberships = vi.fn(async () => []);
7209
+ let sessionValid = false;
7210
+ const getAccessToken = vi.fn(async () => {
7211
+ if (!sessionValid)
7212
+ throw new Error("invalid_grant");
7213
+ return "tok";
7214
+ });
7215
+ const runnerDeps = makeDeps({
7216
+ getAccessToken,
7217
+ createVaultClient: () => ({
7218
+ ...makeVaultStub(),
7219
+ listMyMemberships,
7220
+ }),
7221
+ });
7222
+ const runPass = vi.fn(async (argv) => runRunner(argv, {
7223
+ ...runnerDeps,
7224
+ authRequiredExitCode: AUTH_REQUIRED_PASS_EXIT,
7225
+ }));
7226
+ let triggerShutdown = () => { };
7227
+ let settled;
7228
+ let loop;
7229
+ try {
7230
+ loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
7231
+ runPass,
7232
+ sleep: clock.sleep,
7233
+ authWaitLog,
7234
+ getAccessToken,
7235
+ startEventSync,
7236
+ createWatcher: () => makeWatcherStub(),
7237
+ createReceiver: () => ({
7238
+ start: async () => { },
7239
+ dispose: async () => { },
7240
+ connected: false,
7241
+ }),
7242
+ onShutdownSignal: (handler) => {
7243
+ triggerShutdown = handler;
7244
+ return () => { };
7245
+ },
7246
+ }).then((code) => {
7247
+ settled = code;
7248
+ return code;
7249
+ });
7250
+ await waitUntil(() => clock.delays.length === 1 && authWaitLog.mock.calls.length >= 1, "auth-wait start");
7251
+ expect(settled).toBeUndefined();
7252
+ expect(runPass).toHaveBeenCalledTimes(1);
7253
+ expect(listMyMemberships).not.toHaveBeenCalled();
7254
+ expect(startEventSync).not.toHaveBeenCalled();
7255
+ expect(getAccessToken).toHaveBeenCalledTimes(1);
7256
+ expect(authWaitLog).toHaveBeenCalledWith(AUTH_WAIT_START_MESSAGE);
7257
+ expect(clock.delays).toEqual([AUTH_WAIT_INITIAL_RECHECK_MS]);
7258
+ expect(runnerDeps.stderr.events()).toEqual([
7259
+ {
7260
+ type: "auth-error",
7261
+ message: "Your HQ session needs a quick refresh. Sign in again to keep sync moving.",
7262
+ },
7263
+ ]);
7264
+ clock.tick();
7265
+ await waitUntil(() => clock.delays.length === 2 && getAccessToken.mock.calls.length >= 2, "steady auth-wait recheck");
7266
+ expect(settled).toBeUndefined();
7267
+ expect(runPass).toHaveBeenCalledTimes(1);
7268
+ expect(listMyMemberships).not.toHaveBeenCalled();
7269
+ expect(startEventSync).not.toHaveBeenCalled();
7270
+ expect(getAccessToken).toHaveBeenCalledTimes(2);
7271
+ expect(clock.delays).toEqual([
7272
+ AUTH_WAIT_INITIAL_RECHECK_MS,
7273
+ AUTH_WAIT_STEADY_RECHECK_MS,
7274
+ ]);
7275
+ sessionValid = true;
7276
+ clock.tick();
7277
+ await waitUntil(() => runPass.mock.calls.length >= 2 &&
7278
+ listMyMemberships.mock.calls.length >= 1 &&
7279
+ startEventSync.mock.calls.length >= 1, "resumed watch startup");
7280
+ expect(settled).toBeUndefined();
7281
+ expect(authWaitLog).toHaveBeenCalledWith(AUTH_WAIT_RESUME_MESSAGE);
7282
+ expect(runPass).toHaveBeenCalledTimes(2);
7283
+ expect(listMyMemberships).toHaveBeenCalled();
7284
+ expect(startEventSync).toHaveBeenCalledTimes(1);
7285
+ triggerShutdown();
7286
+ await expect(loop).resolves.toBe(0);
7287
+ }
7288
+ finally {
7289
+ triggerShutdown();
7290
+ if (loop)
7291
+ await loop.catch(() => undefined);
7292
+ if (previousCoordinator === undefined) {
7293
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7294
+ }
7295
+ else {
7296
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
7297
+ }
7298
+ if (previousEventSync === undefined) {
7299
+ delete process.env.HQ_SYNC_EVENT_SYNC;
7300
+ }
7301
+ else {
7302
+ process.env.HQ_SYNC_EVENT_SYNC = previousEventSync;
7303
+ }
7304
+ }
7305
+ });
7306
+ });
6988
7307
  describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
6989
7308
  it("feeds a watcher-originated V2 drain through the same single-flight guard as the poll", async () => {
6990
7309
  const watcher = makeWatcherStub();