/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import fs from "fs"; import path from "path"; import { Org, AuthInfo } from "@salesforce/core"; import { AuthError } from "./errors.js"; import { graphitiHome } from "./fs-utils.js"; export interface OrgAuth { /** * Captured at resolution time as a liveness check (a missing token means the * auth has expired). NOT used to authenticate requests: the HTTP layer builds * a `Connection` from the alias and lets `@salesforce/core` inject/refresh the * token. Retained for callers that surface token state. */ accessToken: string; instanceUrl: string; username: string; orgId: string; alias: string; } export interface OrgListEntry { alias: string; username: string; instanceUrl: string; orgId: string; type: "scratch" | "non-scratch"; isConnected: boolean; } // `--target-org` accepts both aliases and usernames; SF usernames are email-shaped // (e.g. `dev+build1@scratch-org-12.example.com`), so the charset must include // `@`, `.`, `+`. Leading hyphen and leading dot are still forbidden to defend // against `--target-org -foo` flag injection and path-relative parsing quirks. // Length cap follows RFC 5321 (253) since SF usernames can be long. const ORG_ALIAS_RE = /^[A-Za-z0-9_][A-Za-z0-9_.+@-]{0,252}$/; function assertValidOrgAlias(orgAlias: string): void { if (typeof orgAlias !== "string" || !ORG_ALIAS_RE.test(orgAlias)) { throw new Error( "Invalid org alias or username. Must be 1-253 characters, start with [A-Za-z0-9_], and contain only [A-Za-z0-9_.+@-].", ); } } const authCache = new Map(); export function clearOrgAuthCache(): void { authCache.clear(); } /** * Reads Salesforce org credentials via @salesforce/core. * Results are memoized for the lifetime of the process so repeated calls * within the same command (or an interactive session) only resolve once. */ export async function getOrgAuth(orgAlias: string): Promise { assertValidOrgAlias(orgAlias); const cached = authCache.get(orgAlias); if (cached) return cached; let connection; try { const org = await Org.create({ aliasOrUsername: orgAlias }); connection = org.getConnection(); } catch (err) { const msg = err instanceof Error ? err.message : String(err); throw new AuthError( `Failed to get org info for "${orgAlias}". Is the alias correct? Have you run \`sf org login web --alias ${orgAlias}\`?\n${msg}`, { cause: err }, ); } const fields = connection.getAuthInfo().getFields(); if (!connection.accessToken || !connection.instanceUrl) { throw new AuthError( `Missing accessToken or instanceUrl for "${orgAlias}". Token may have expired -- try \`sf org login web --alias ${orgAlias}\` to re-authenticate.`, ); } const auth: OrgAuth = { accessToken: connection.accessToken, instanceUrl: connection.instanceUrl.replace(/\/+$/, ""), username: fields.username ?? "unknown", orgId: fields.orgId ?? "unknown", alias: orgAlias, }; authCache.set(orgAlias, auth); return auth; } function getConnectedOrgAliases(): Set { const schemasDir = path.join(graphitiHome(), "schemas"); const sessionsDir = path.join(graphitiHome(), "sessions"); const aliases = new Set(); if (fs.existsSync(sessionsDir)) { for (const file of fs.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"))) { try { const raw = JSON.parse(fs.readFileSync(path.join(sessionsDir, file), "utf-8")); if (raw.orgAlias) aliases.add(raw.orgAlias); } catch { /* skip corrupt sessions */ } } } if (fs.existsSync(schemasDir)) { if (fs.readdirSync(schemasDir).some((f) => f.endsWith(".json"))) { aliases.add("__has_schemas__"); } } return aliases; } /** * Lists available Salesforce orgs via AuthInfo.listAllAuthorizations(). * Marks orgs that have a cached graphiti schema or session. */ export async function listOrgs(): Promise { let auths; try { auths = await AuthInfo.listAllAuthorizations(); } catch (err) { const msg = err instanceof Error ? err.message : String(err); throw new Error(`Failed to list orgs: ${msg}`); } const connected = getConnectedOrgAliases(); const entries: OrgListEntry[] = []; for (const a of auths) { const aliases = a.aliases ?? []; const displayId = aliases[0] ?? a.username; if (!displayId) continue; // neither alias nor username — skip const isConnected = aliases.some((al) => connected.has(al)) || (!!a.username && connected.has(a.username)); entries.push({ alias: displayId, username: a.username ?? "unknown", instanceUrl: (a.instanceUrl ?? "").replace(/\/+$/, ""), orgId: a.orgId ?? "", type: a.isScratchOrg ? "scratch" : "non-scratch", isConnected, }); } return entries; }