robinhood-nodets
Version:
Comprehensive TypeScript API wrapper for the Robinhood private API
209 lines • 8.2 kB
JavaScript
import fetch from "node-fetch";
import { robinhoodApiBaseUrl, clientId, endpoints } from "./constants.js";
import { v4 as uuidv4 } from "uuid";
const defaultHeaders = {
Accept: "*/*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
origin: "https://robinhood.com",
referer: "https://robinhood.com/",
"X-Robinhood-API-Version": "1.431.4",
Connection: "keep-alive",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0",
"x-timezone-id": "America/New_York",
};
// Store authentication states for tracking ongoing workflows
const workflowStates = new Map();
function saveWorkflowState(id, state) {
workflowStates.set(id, state);
}
function getWorkflowState(id) {
return workflowStates.get(id);
}
function deleteWorkflowState(id) {
workflowStates.delete(id);
}
// **START AUTHENTICATION**
export async function authenticate(credentials) {
console.log("🔑 Starting authentication...");
let { username, password, deviceToken } = credentials;
if (!deviceToken) {
deviceToken = uuidv4();
}
console.log("🆔 Device token:", deviceToken);
const headers = {
...defaultHeaders,
"Content-Type": "application/x-www-form-urlencoded",
};
const payload = {
client_id: clientId,
grant_type: "password",
username,
password,
device_token: deviceToken,
scope: "internal",
challenge_type: "sms",
expires_in: 86400,
long_session: true,
create_read_only_secondary_token: true,
token_request_path: "/login",
try_passkeys: false,
};
try {
const response = await fetch(robinhoodApiBaseUrl + endpoints.login, {
method: "POST",
headers,
body: new URLSearchParams(payload),
});
const data = (await response.json());
console.log("🔐 Auth Response:", data);
if (data.verification_workflow) {
const workflowId = uuidv4();
// ✅ Call user_machine endpoint
const machineResponse = await fetch(`${robinhoodApiBaseUrl}/pathfinder/user_machine/`, {
method: "POST",
headers: { ...defaultHeaders, "Content-Type": "application/json" },
body: JSON.stringify({
device_id: deviceToken,
flow: "suv",
input: { workflow_id: data.verification_workflow.id },
}),
});
const machineData = (await machineResponse.json());
console.log("🤖 Machine Response:", machineData);
if (!machineData.id) {
return { status: "error", message: "❌ Machine ID not found!" };
}
// ✅ Fetch challenge details
const inquiriesResponse = await fetch(`${robinhoodApiBaseUrl}/pathfinder/inquiries/${machineData.id}/user_view/`);
const inquiriesData = (await inquiriesResponse.json());
console.log("🔍 Inquiries Response:", inquiriesData);
const challenge = inquiriesData.context?.sheriff_challenge;
console.log("🛡️ Challenge:", challenge);
if (!challenge)
return { status: "error", message: "❌ Challenge not found!" };
// ✅ Save workflow state
saveWorkflowState(workflowId, {
workflowId: data.verification_workflow.id,
machineId: machineData.id,
deviceToken,
challengeId: challenge.id,
challengeType: challenge.type,
username,
password,
});
if (challenge.type === "sms") {
return {
status: "awaiting_input",
workflow_id: workflowId,
message: "📩 Enter the SMS code:",
};
}
else {
return {
status: "awaiting_input",
workflow_id: workflowId,
message: "📲 Confirm device approval and press Enter:",
};
}
}
if (data.access_token) {
return {
status: "success",
tokenData: { access_token: data.access_token },
};
}
return { status: "error", message: data.detail };
}
catch (error) {
console.error("⚠️ Auth Error:", error);
throw new Error(`Authentication failed: ${error.message}`);
}
}
// **SUBMIT USER INPUT (SMS OR DEVICE APPROVAL)**
export async function submitChallenge(workflowId, userInput) {
const state = getWorkflowState(workflowId);
if (!state)
throw new Error("❌ Invalid workflow ID!");
try {
if (state.challengeType === "sms") {
console.log("📩 Sending SMS code...");
const smsResponse = await fetch(`${robinhoodApiBaseUrl}/challenge/${state.challengeId}/respond/`, {
method: "POST",
headers: {
...defaultHeaders,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ response: userInput || "" }),
});
const smsData = (await smsResponse.json());
if (smsData.status !== "validated") {
throw new Error("❌ SMS validation failed!");
}
}
else {
console.log("📲 Checking device approval...");
const pollResponse = await fetch(`${robinhoodApiBaseUrl}/push/${state.challengeId}/get_prompts_status/`, {
headers: defaultHeaders,
});
const pollData = (await pollResponse.json());
console.log("🛂 Poll Response:", pollData);
if (pollData.challenge_status !== "validated") {
throw new Error("❌ Device approval failed!");
}
}
return await finalizeAuthentication(state);
}
catch (error) {
console.error("⚠️ Submission Error:", error);
throw error;
}
}
// **FINALIZE AUTHENTICATION**
export async function finalizeAuthentication(state) {
console.log("✅ Challenge validated! Sending final approval...");
// ✅ Step 1: Send a POST request to finalize the verification
const finalizeResponse = await fetch(`${robinhoodApiBaseUrl}/pathfinder/inquiries/${state.machineId}/user_view/`, {
method: "POST",
headers: {
...defaultHeaders,
"Content-Type": "application/json",
},
body: JSON.stringify({
sequence: 0,
user_input: { status: "continue" },
}),
});
const finalizeData = await finalizeResponse.json();
console.log("🔄 Final Approval Response:", finalizeData);
// ✅ Step 2: Request the final authentication token
console.log("🔓 Requesting final authentication token...");
const tokenResponse = await fetch(robinhoodApiBaseUrl + endpoints.login, {
method: "POST",
headers: {
...defaultHeaders,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: clientId,
grant_type: "password",
device_token: state.deviceToken,
scope: "internal",
expires_in: "86400",
create_read_only_secondary_token: "true",
token_request_path: "/login",
try_passkeys: "false",
username: state.username,
password: state.password,
long_session: "true",
}),
});
const tokenData = (await tokenResponse.json());
console.log("🔑 Auth Token Response:", tokenData);
if (!tokenData.access_token) {
throw new Error("❌ Failed to retrieve access token.");
}
deleteWorkflowState(state.workflowId); // ✅ Clean up workflow state
return { status: "success", tokenData };
}
//# sourceMappingURL=auth.js.map