@indigoai-us/hq-cli 5.36.3 → 5.36.4

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.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Onboarding identity-link guard (DEV-1695 / DEV-1701 / DEV-1721).
3
+ *
4
+ * Failure mode this guards against:
5
+ *
6
+ * The onboarding orchestrator's `create-person` step resolves the caller's
7
+ * person entity by an email-derived slug GLOBALLY (not scoped to the caller's
8
+ * Cognito identity). When a user signs back in under a DIFFERENT Cognito
9
+ * `sub` — e.g. a new Google account, an email change, or a linked-IdP sub
10
+ * swap — the slug lookup still finds the person row created under the ORIGINAL
11
+ * sub and records it in the checkpoint's `personUid`. `create-person` is then
12
+ * marked complete and is never re-validated against the live identity.
13
+ *
14
+ * On every `hq onboard resume`, `create-person` is skipped as "already
15
+ * complete" while the stale `personUid` is carried forward. The server
16
+ * correctly refuses to bootstrap a company membership for a person the caller
17
+ * does not own (it resolves the caller's person by the live Cognito sub), so
18
+ * resume re-fails at `bootstrap-membership` every single time — an infinite
19
+ * loop with a misleading downstream error.
20
+ *
21
+ * This module is the detection half of the fix: given the local checkpoint and
22
+ * the set of person entities ACTUALLY owned by the current caller (the server
23
+ * scopes `/entity/by-type/person` by the live Cognito sub), it reports whether
24
+ * the checkpoint adopted a person the caller no longer owns. The resume command
25
+ * uses it to surface a recoverable, actionable error instead of looping.
26
+ */
27
+ import type { OnboardingCheckpoint } from "@indigoai-us/hq-onboarding";
28
+ export type OnboardingIdentityCheck = {
29
+ kind: "ok";
30
+ } | {
31
+ kind: "mismatch";
32
+ personUid: string;
33
+ message: string;
34
+ };
35
+ /**
36
+ * Detect the person-entity-vs-Cognito-sub mismatch that wedges
37
+ * `hq onboard resume` into an infinite loop.
38
+ *
39
+ * Returns `{ kind: "ok" }` whenever the flow should proceed normally:
40
+ * - there is no checkpoint, or
41
+ * - the checkpoint never recorded an adopted person (`personUid` unset, or
42
+ * `create-person` not yet completed), or
43
+ * - the adopted person is among those owned by the current caller.
44
+ *
45
+ * Returns `{ kind: "mismatch", ... }` only when the checkpoint completed
46
+ * `create-person` with a `personUid` that the current caller does NOT own —
47
+ * the exact state that loops forever at `bootstrap-membership`.
48
+ */
49
+ export declare function detectOnboardingIdentityMismatch(input: {
50
+ checkpoint: OnboardingCheckpoint | null;
51
+ ownedPersonUids: readonly string[];
52
+ }): OnboardingIdentityCheck;
53
+ //# sourceMappingURL=onboard-identity-guard.d.ts.map
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Onboarding identity-link guard (DEV-1695 / DEV-1701 / DEV-1721).
3
+ *
4
+ * Failure mode this guards against:
5
+ *
6
+ * The onboarding orchestrator's `create-person` step resolves the caller's
7
+ * person entity by an email-derived slug GLOBALLY (not scoped to the caller's
8
+ * Cognito identity). When a user signs back in under a DIFFERENT Cognito
9
+ * `sub` — e.g. a new Google account, an email change, or a linked-IdP sub
10
+ * swap — the slug lookup still finds the person row created under the ORIGINAL
11
+ * sub and records it in the checkpoint's `personUid`. `create-person` is then
12
+ * marked complete and is never re-validated against the live identity.
13
+ *
14
+ * On every `hq onboard resume`, `create-person` is skipped as "already
15
+ * complete" while the stale `personUid` is carried forward. The server
16
+ * correctly refuses to bootstrap a company membership for a person the caller
17
+ * does not own (it resolves the caller's person by the live Cognito sub), so
18
+ * resume re-fails at `bootstrap-membership` every single time — an infinite
19
+ * loop with a misleading downstream error.
20
+ *
21
+ * This module is the detection half of the fix: given the local checkpoint and
22
+ * the set of person entities ACTUALLY owned by the current caller (the server
23
+ * scopes `/entity/by-type/person` by the live Cognito sub), it reports whether
24
+ * the checkpoint adopted a person the caller no longer owns. The resume command
25
+ * uses it to surface a recoverable, actionable error instead of looping.
26
+ */
27
+ /**
28
+ * Build the human-facing recovery message for a detected mismatch.
29
+ *
30
+ * Only the two recovery paths that ACTUALLY work are offered. Deleting the
31
+ * checkpoint and re-running `create-company` does NOT help: the email-derived
32
+ * slug would re-adopt the same other-owned person row, so it is deliberately
33
+ * not suggested.
34
+ */
35
+
36
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5e986156-276e-5583-a43d-74696c122d40")}catch(e){}}();
37
+ function buildMismatchMessage(personUid) {
38
+ return [
39
+ `Onboarding can't continue — an identity-link mismatch is blocking resume.`,
40
+ ``,
41
+ `Your saved onboarding checkpoint is linked to person record ${personUid},`,
42
+ `but that record is owned by a different sign-in than the one you're using`,
43
+ `now. This usually means onboarding was started under one identity (one`,
44
+ `Google account / email) and later resumed under a different one.`,
45
+ ``,
46
+ `Because the person record belongs to the original sign-in, resume can't`,
47
+ `bootstrap your company membership and would otherwise retry forever.`,
48
+ ``,
49
+ `To recover, do ONE of the following:`,
50
+ ` 1. Sign out and sign back in with your ORIGINAL onboarding identity,`,
51
+ ` then re-run 'hq onboard resume'.`,
52
+ ` 2. Ask an HQ admin to relink person ${personUid} to your current`,
53
+ ` sign-in (reference Linear DEV-1695), then re-run 'hq onboard resume'.`,
54
+ ``,
55
+ `Nothing was changed — your data is safe.`,
56
+ ].join("\n");
57
+ }
58
+ /**
59
+ * Detect the person-entity-vs-Cognito-sub mismatch that wedges
60
+ * `hq onboard resume` into an infinite loop.
61
+ *
62
+ * Returns `{ kind: "ok" }` whenever the flow should proceed normally:
63
+ * - there is no checkpoint, or
64
+ * - the checkpoint never recorded an adopted person (`personUid` unset, or
65
+ * `create-person` not yet completed), or
66
+ * - the adopted person is among those owned by the current caller.
67
+ *
68
+ * Returns `{ kind: "mismatch", ... }` only when the checkpoint completed
69
+ * `create-person` with a `personUid` that the current caller does NOT own —
70
+ * the exact state that loops forever at `bootstrap-membership`.
71
+ */
72
+ export function detectOnboardingIdentityMismatch(input) {
73
+ const { checkpoint, ownedPersonUids } = input;
74
+ if (!checkpoint)
75
+ return { kind: "ok" };
76
+ const adopted = checkpoint.personUid;
77
+ if (!adopted)
78
+ return { kind: "ok" };
79
+ // Mirror the orchestrator's `isStepComplete`: a personUid that predates the
80
+ // create-person step completing has not been committed as the adopted
81
+ // identity yet, so don't treat it as a mismatch.
82
+ if (!checkpoint.completedSteps?.includes("create-person")) {
83
+ return { kind: "ok" };
84
+ }
85
+ if (ownedPersonUids.includes(adopted))
86
+ return { kind: "ok" };
87
+ return {
88
+ kind: "mismatch",
89
+ personUid: adopted,
90
+ message: buildMismatchMessage(adopted),
91
+ };
92
+ }
93
+ //# sourceMappingURL=onboard-identity-guard.js.map
94
+ //# debugId=5e986156-276e-5583-a43d-74696c122d40
@@ -19,10 +19,12 @@
19
19
  * browser-OAuth flow opens automatically.
20
20
  */
21
21
 
22
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="38856701-42d8-587b-9969-8da51ede7cd4")}catch(e){}}();
22
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="8f6c7f48-0ce5-55f3-bfd9-b4a6040adb0b")}catch(e){}}();
23
23
  import chalk from "chalk";
24
- import { runOnboardCli } from "@indigoai-us/hq-onboarding";
25
- import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
24
+ import { runOnboardCli, readCheckpoint } from "@indigoai-us/hq-onboarding";
25
+ import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
26
+ import { createDefaultVaultClient } from "./cloud-provision.js";
27
+ import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
26
28
  // ---------------------------------------------------------------------------
27
29
  // Command registration
28
30
  // ---------------------------------------------------------------------------
@@ -102,6 +104,36 @@ export function registerOnboardCommand(program) {
102
104
  .action(async (options) => {
103
105
  try {
104
106
  const accessToken = await ensureCognitoToken();
107
+ // Identity-link pre-flight (DEV-1695 / DEV-1701 / DEV-1721): if the
108
+ // saved checkpoint adopted a person entity owned by a DIFFERENT Cognito
109
+ // sign-in than the current one, resume would skip the completed
110
+ // create-person step with that stale personUid and re-fail at
111
+ // bootstrap-membership forever. Detect it up front and surface a
112
+ // recoverable, actionable error instead of looping.
113
+ const checkpoint = await readCheckpoint(options.hqRoot);
114
+ if (checkpoint?.personUid) {
115
+ let ownedPersonUids = null;
116
+ try {
117
+ const client = createDefaultVaultClient(DEFAULT_VAULT_API_URL, accessToken);
118
+ const owned = await client.listMyPersonEntities();
119
+ ownedPersonUids = owned.map((p) => p.uid);
120
+ }
121
+ catch (err) {
122
+ // Best-effort guard: if the pre-flight lookup itself fails (network,
123
+ // auth), don't block resume — but don't swallow it silently either.
124
+ console.warn(chalk.yellow(` (skipping identity pre-flight check: ${err instanceof Error ? err.message : String(err)})`));
125
+ }
126
+ if (ownedPersonUids) {
127
+ const check = detectOnboardingIdentityMismatch({
128
+ checkpoint,
129
+ ownedPersonUids,
130
+ });
131
+ if (check.kind === "mismatch") {
132
+ console.error(chalk.red(`\n✗ Resume blocked:\n\n${check.message}`));
133
+ process.exit(1);
134
+ }
135
+ }
136
+ }
105
137
  const result = await runOnboardCli({
106
138
  mode: "resume",
107
139
  vaultConfig: buildVaultConfig(accessToken),
@@ -142,4 +174,4 @@ export function registerOnboardCommand(program) {
142
174
  });
143
175
  }
144
176
  //# sourceMappingURL=onboard.js.map
145
- //# debugId=38856701-42d8-587b-9969-8da51ede7cd4
177
+ //# debugId=8f6c7f48-0ce5-55f3-bfd9-b4a6040adb0b
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.36.3",
3
+ "version": "5.36.4",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Regression tests for the onboarding identity-link guard
3
+ * (DEV-1695 / DEV-1701 / DEV-1721).
4
+ *
5
+ * Covers the resume-with-mismatched-sub path: a checkpoint that completed
6
+ * `create-person` against a `personUid` the current caller does NOT own must
7
+ * be detected as a recoverable mismatch instead of being allowed to loop
8
+ * forever at bootstrap-membership. Also covers the no-false-positive cases
9
+ * (no checkpoint, no adopted person, create-person not complete, person owned).
10
+ *
11
+ * Pure function — no network, no VaultClient. The resume command supplies the
12
+ * owned-person UID set from the JWT-scoped `/entity/by-type/person` call.
13
+ */
14
+
15
+ import { describe, expect, it } from "vitest";
16
+ import type { OnboardingCheckpoint } from "@indigoai-us/hq-onboarding";
17
+
18
+ import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
19
+
20
+ const OLD_PERSON = "prs_01KRGW9JQ4D2ZHD080B931ZT0J";
21
+ const NEW_PERSON = "prs_01NEWOWNEDBYCURRENTSIGNIN00";
22
+
23
+ function checkpoint(
24
+ overrides: Partial<OnboardingCheckpoint> = {},
25
+ ): OnboardingCheckpoint {
26
+ return {
27
+ mode: "create-company",
28
+ startedAt: "2026-05-13T14:36:45.668Z",
29
+ updatedAt: "2026-06-03T14:04:32.261Z",
30
+ personUid: OLD_PERSON,
31
+ companyUid: "cmp_01KT6WSNM6HGSH3JZGS8X1KXGS",
32
+ companySlug: "maximus",
33
+ completedSteps: ["create-person", "create-company", "provision-bucket"],
34
+ failedStep: "bootstrap-membership",
35
+ ...overrides,
36
+ };
37
+ }
38
+
39
+ describe("detectOnboardingIdentityMismatch", () => {
40
+ it("flags the mismatch when the checkpoint's person is owned by a different sign-in", () => {
41
+ // The Jacob Wuertz case: checkpoint adopted prs_OLD (owned by the original
42
+ // Cognito sub), but the current caller only owns prs_NEW.
43
+ const result = detectOnboardingIdentityMismatch({
44
+ checkpoint: checkpoint(),
45
+ ownedPersonUids: [NEW_PERSON],
46
+ });
47
+
48
+ expect(result.kind).toBe("mismatch");
49
+ if (result.kind !== "mismatch") throw new Error("expected mismatch");
50
+ expect(result.personUid).toBe(OLD_PERSON);
51
+ expect(result.message).toContain(OLD_PERSON);
52
+ // Actionable + references the recovery paths that actually work.
53
+ expect(result.message).toMatch(/DEV-1695/);
54
+ expect(result.message).toMatch(/original/i);
55
+ expect(result.message).toMatch(/relink/i);
56
+ });
57
+
58
+ it("also flags the mismatch when the caller owns NO person entities at all", () => {
59
+ const result = detectOnboardingIdentityMismatch({
60
+ checkpoint: checkpoint(),
61
+ ownedPersonUids: [],
62
+ });
63
+ expect(result.kind).toBe("mismatch");
64
+ });
65
+
66
+ it("passes when the adopted person IS owned by the current caller", () => {
67
+ const result = detectOnboardingIdentityMismatch({
68
+ checkpoint: checkpoint({ personUid: NEW_PERSON }),
69
+ ownedPersonUids: [NEW_PERSON, "prs_01ANOTHERONE0000000000000000"],
70
+ });
71
+ expect(result).toEqual({ kind: "ok" });
72
+ });
73
+
74
+ it("passes when there is no checkpoint", () => {
75
+ const result = detectOnboardingIdentityMismatch({
76
+ checkpoint: null,
77
+ ownedPersonUids: [NEW_PERSON],
78
+ });
79
+ expect(result).toEqual({ kind: "ok" });
80
+ });
81
+
82
+ it("passes when the checkpoint has not adopted a person yet (no personUid)", () => {
83
+ const result = detectOnboardingIdentityMismatch({
84
+ checkpoint: checkpoint({ personUid: undefined, completedSteps: [] }),
85
+ ownedPersonUids: [],
86
+ });
87
+ expect(result).toEqual({ kind: "ok" });
88
+ });
89
+
90
+ it("passes when create-person is not yet complete, even with a personUid present", () => {
91
+ // A personUid that predates the create-person step completing is not yet
92
+ // the committed identity — don't false-positive on it.
93
+ const result = detectOnboardingIdentityMismatch({
94
+ checkpoint: checkpoint({ completedSteps: ["create-company"] }),
95
+ ownedPersonUids: [NEW_PERSON],
96
+ });
97
+ expect(result).toEqual({ kind: "ok" });
98
+ });
99
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Onboarding identity-link guard (DEV-1695 / DEV-1701 / DEV-1721).
3
+ *
4
+ * Failure mode this guards against:
5
+ *
6
+ * The onboarding orchestrator's `create-person` step resolves the caller's
7
+ * person entity by an email-derived slug GLOBALLY (not scoped to the caller's
8
+ * Cognito identity). When a user signs back in under a DIFFERENT Cognito
9
+ * `sub` — e.g. a new Google account, an email change, or a linked-IdP sub
10
+ * swap — the slug lookup still finds the person row created under the ORIGINAL
11
+ * sub and records it in the checkpoint's `personUid`. `create-person` is then
12
+ * marked complete and is never re-validated against the live identity.
13
+ *
14
+ * On every `hq onboard resume`, `create-person` is skipped as "already
15
+ * complete" while the stale `personUid` is carried forward. The server
16
+ * correctly refuses to bootstrap a company membership for a person the caller
17
+ * does not own (it resolves the caller's person by the live Cognito sub), so
18
+ * resume re-fails at `bootstrap-membership` every single time — an infinite
19
+ * loop with a misleading downstream error.
20
+ *
21
+ * This module is the detection half of the fix: given the local checkpoint and
22
+ * the set of person entities ACTUALLY owned by the current caller (the server
23
+ * scopes `/entity/by-type/person` by the live Cognito sub), it reports whether
24
+ * the checkpoint adopted a person the caller no longer owns. The resume command
25
+ * uses it to surface a recoverable, actionable error instead of looping.
26
+ */
27
+
28
+ import type { OnboardingCheckpoint } from "@indigoai-us/hq-onboarding";
29
+
30
+ export type OnboardingIdentityCheck =
31
+ | { kind: "ok" }
32
+ | { kind: "mismatch"; personUid: string; message: string };
33
+
34
+ /**
35
+ * Build the human-facing recovery message for a detected mismatch.
36
+ *
37
+ * Only the two recovery paths that ACTUALLY work are offered. Deleting the
38
+ * checkpoint and re-running `create-company` does NOT help: the email-derived
39
+ * slug would re-adopt the same other-owned person row, so it is deliberately
40
+ * not suggested.
41
+ */
42
+ function buildMismatchMessage(personUid: string): string {
43
+ return [
44
+ `Onboarding can't continue — an identity-link mismatch is blocking resume.`,
45
+ ``,
46
+ `Your saved onboarding checkpoint is linked to person record ${personUid},`,
47
+ `but that record is owned by a different sign-in than the one you're using`,
48
+ `now. This usually means onboarding was started under one identity (one`,
49
+ `Google account / email) and later resumed under a different one.`,
50
+ ``,
51
+ `Because the person record belongs to the original sign-in, resume can't`,
52
+ `bootstrap your company membership and would otherwise retry forever.`,
53
+ ``,
54
+ `To recover, do ONE of the following:`,
55
+ ` 1. Sign out and sign back in with your ORIGINAL onboarding identity,`,
56
+ ` then re-run 'hq onboard resume'.`,
57
+ ` 2. Ask an HQ admin to relink person ${personUid} to your current`,
58
+ ` sign-in (reference Linear DEV-1695), then re-run 'hq onboard resume'.`,
59
+ ``,
60
+ `Nothing was changed — your data is safe.`,
61
+ ].join("\n");
62
+ }
63
+
64
+ /**
65
+ * Detect the person-entity-vs-Cognito-sub mismatch that wedges
66
+ * `hq onboard resume` into an infinite loop.
67
+ *
68
+ * Returns `{ kind: "ok" }` whenever the flow should proceed normally:
69
+ * - there is no checkpoint, or
70
+ * - the checkpoint never recorded an adopted person (`personUid` unset, or
71
+ * `create-person` not yet completed), or
72
+ * - the adopted person is among those owned by the current caller.
73
+ *
74
+ * Returns `{ kind: "mismatch", ... }` only when the checkpoint completed
75
+ * `create-person` with a `personUid` that the current caller does NOT own —
76
+ * the exact state that loops forever at `bootstrap-membership`.
77
+ */
78
+ export function detectOnboardingIdentityMismatch(input: {
79
+ checkpoint: OnboardingCheckpoint | null;
80
+ ownedPersonUids: readonly string[];
81
+ }): OnboardingIdentityCheck {
82
+ const { checkpoint, ownedPersonUids } = input;
83
+
84
+ if (!checkpoint) return { kind: "ok" };
85
+
86
+ const adopted = checkpoint.personUid;
87
+ if (!adopted) return { kind: "ok" };
88
+
89
+ // Mirror the orchestrator's `isStepComplete`: a personUid that predates the
90
+ // create-person step completing has not been committed as the adopted
91
+ // identity yet, so don't treat it as a mismatch.
92
+ if (!checkpoint.completedSteps?.includes("create-person")) {
93
+ return { kind: "ok" };
94
+ }
95
+
96
+ if (ownedPersonUids.includes(adopted)) return { kind: "ok" };
97
+
98
+ return {
99
+ kind: "mismatch",
100
+ personUid: adopted,
101
+ message: buildMismatchMessage(adopted),
102
+ };
103
+ }
@@ -22,12 +22,15 @@
22
22
  import { Command } from "commander";
23
23
  import chalk from "chalk";
24
24
 
25
- import { runOnboardCli } from "@indigoai-us/hq-onboarding";
25
+ import { runOnboardCli, readCheckpoint } from "@indigoai-us/hq-onboarding";
26
26
  import {
27
27
  DEFAULT_HQ_ROOT,
28
+ DEFAULT_VAULT_API_URL,
28
29
  ensureCognitoToken,
29
30
  buildVaultConfig,
30
31
  } from "../utils/cognito-session.js";
32
+ import { createDefaultVaultClient } from "./cloud-provision.js";
33
+ import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
31
34
 
32
35
  // ---------------------------------------------------------------------------
33
36
  // Command registration
@@ -143,6 +146,46 @@ export function registerOnboardCommand(program: Command): void {
143
146
  .action(async (options: { hqRoot: string }) => {
144
147
  try {
145
148
  const accessToken = await ensureCognitoToken();
149
+
150
+ // Identity-link pre-flight (DEV-1695 / DEV-1701 / DEV-1721): if the
151
+ // saved checkpoint adopted a person entity owned by a DIFFERENT Cognito
152
+ // sign-in than the current one, resume would skip the completed
153
+ // create-person step with that stale personUid and re-fail at
154
+ // bootstrap-membership forever. Detect it up front and surface a
155
+ // recoverable, actionable error instead of looping.
156
+ const checkpoint = await readCheckpoint(options.hqRoot);
157
+ if (checkpoint?.personUid) {
158
+ let ownedPersonUids: string[] | null = null;
159
+ try {
160
+ const client = createDefaultVaultClient(
161
+ DEFAULT_VAULT_API_URL,
162
+ accessToken,
163
+ );
164
+ const owned = await client.listMyPersonEntities();
165
+ ownedPersonUids = owned.map((p) => p.uid);
166
+ } catch (err) {
167
+ // Best-effort guard: if the pre-flight lookup itself fails (network,
168
+ // auth), don't block resume — but don't swallow it silently either.
169
+ console.warn(
170
+ chalk.yellow(
171
+ ` (skipping identity pre-flight check: ${
172
+ err instanceof Error ? err.message : String(err)
173
+ })`,
174
+ ),
175
+ );
176
+ }
177
+ if (ownedPersonUids) {
178
+ const check = detectOnboardingIdentityMismatch({
179
+ checkpoint,
180
+ ownedPersonUids,
181
+ });
182
+ if (check.kind === "mismatch") {
183
+ console.error(chalk.red(`\n✗ Resume blocked:\n\n${check.message}`));
184
+ process.exit(1);
185
+ }
186
+ }
187
+ }
188
+
146
189
  const result = await runOnboardCli({
147
190
  mode: "resume",
148
191
  vaultConfig: buildVaultConfig(accessToken),