← Public packages
@kody/salesforce
Salesforce REST helpers for identity, SOQL, multi-org routing, and error investigation.
src/orgs.ts
303 lines · 9.0 KB · TypeScriptimport { kody } from 'kody:runtime'
import { fetchUserInfo } from './request.ts'
import type { JsonRecord } from './types.ts'
import { requireRecord, requireString } from './types.ts'
export const ORG_MAP_STORAGE_KEY = 'org-map'
export type OrgMapEntry = {
integrationName: string
label?: string | null
instanceHost?: string | null
}
export type OrgMap = Record<string, OrgMapEntry | string>
export type ListedOrg = {
integrationName: string
organizationId: string | null
instanceHost: string | null
userType: string | null
ok: boolean
error?: string
}
export type ListOrgsResult = {
orgs: Array<ListedOrg>
/** organizationId → integrationName (plus optional instance host). Persist this in a private package's packageStorage. */
map: Record<string, OrgMapEntry>
}
export type ResolveOrgInput = {
organizationId?: string
integrationName?: string
/** Caller-owned map, typically loaded from a private package's packageStorage. */
orgMap?: OrgMap
/** Webhook body, Flow email JSON, or any payload that may include an org id. */
payload?: unknown
error?: unknown
body?: unknown
}
export type ResolveOrgResult = {
organizationId: string | null
integrationName: string
source: 'explicit' | 'org-map' | 'live-discovery'
instanceHost: string | null
}
const ORG_ID_PATTERN = /\b(00D[a-zA-Z0-9]{12}(?:[a-zA-Z0-9]{3})?)\b/g
const ORG_ID_KEYS = [
'organizationId',
'organization_id',
'orgId',
'org_id',
'OrganizationId',
'organizationID',
]
export function isSalesforceIntegrationName(name: string): boolean {
return name === 'salesforce' || name.startsWith('salesforce-')
}
export function normalizeOrganizationId(value: string): string {
const id = requireString(value, 'organizationId')
if (!/^00D[a-zA-Z0-9]{12}([a-zA-Z0-9]{3})?$/.test(id)) {
throw new Error('organizationId must be a Salesforce Organization id (00D…).')
}
return id
}
export function extractOrganizationId(...candidates: Array<unknown>): string | null {
for (const candidate of candidates) {
const found = findOrganizationId(candidate, 0, new Set())
if (found) return found
}
return null
}
export function lookupOrgMap(
orgMap: OrgMap | undefined,
organizationId: string,
): OrgMapEntry | null {
if (!orgMap) return null
const direct = orgMap[organizationId]
if (direct) return normalizeMapEntry(direct, organizationId)
const compact = organizationId.slice(0, 15)
const compactHit = orgMap[compact]
if (compactHit) return normalizeMapEntry(compactHit, compact)
for (const [key, value] of Object.entries(orgMap)) {
if (key.slice(0, 15) === compact) return normalizeMapEntry(value, key)
}
return null
}
/**
* List saved Salesforce connections and their organization ids.
* Live `@kody/salesforce` cannot persist this map; a private wrapper should
* store `map` in packageStorage under `org-map` and pass it to resolve-org.
*/
export async function listOrgs(): Promise<ListOrgsResult> {
const names = await listSalesforceIntegrationNames()
const orgs: Array<ListedOrg> = []
const map: Record<string, OrgMapEntry> = {}
for (const integrationName of names) {
try {
const user = await fetchUserInfo(integrationName)
const organizationId =
typeof user.organization_id === 'string' && user.organization_id.length > 0
? user.organization_id
: null
const instanceHost = hostFromUserInfo(user.urls?.rest ?? user.urls?.custom_domain)
orgs.push({
integrationName,
organizationId,
instanceHost,
userType: typeof user.user_type === 'string' ? user.user_type : null,
ok: true,
})
if (organizationId) {
map[organizationId] = { integrationName, instanceHost }
}
} catch (error) {
orgs.push({
integrationName,
organizationId: null,
instanceHost: null,
userType: null,
ok: false,
error: error instanceof Error ? error.message : String(error),
})
}
}
return { orgs, map }
}
export async function resolveOrg(input: ResolveOrgInput): Promise<ResolveOrgResult> {
const explicitName = optionalName(input.integrationName)
const organizationId =
(input.organizationId ? normalizeOrganizationId(input.organizationId) : null) ??
extractOrganizationId(input.payload, input.error, input.body, input)
if (explicitName) {
return {
organizationId,
integrationName: explicitName,
source: 'explicit',
instanceHost: mappedHost(input.orgMap, organizationId),
}
}
if (!organizationId) {
throw new Error(
'Cannot resolve a Salesforce org: pass integrationName, organizationId, or a payload that includes one (organizationId / 00D…). Do not default to "salesforce" when several orgs may be connected.',
)
}
const mapped = lookupOrgMap(input.orgMap, organizationId)
if (mapped) {
return {
organizationId,
integrationName: mapped.integrationName,
source: 'org-map',
instanceHost: mapped.instanceHost ?? null,
}
}
const live = await listOrgs()
const match = live.orgs.find(
(org) =>
org.ok &&
org.organizationId &&
org.organizationId.slice(0, 15) === organizationId.slice(0, 15),
)
if (match?.organizationId) {
return {
organizationId: match.organizationId,
integrationName: match.integrationName,
source: 'live-discovery',
instanceHost: match.instanceHost,
}
}
const known = Object.keys(live.map)
throw new Error(
`No saved Salesforce connection maps to organization ${organizationId}. ` +
(known.length > 0
? `Known org ids: ${known.join(', ')}. Connect the org under a distinct integrationName (salesforce-<client>) and refresh the org-map.`
: 'Connect the org under a distinct integrationName (salesforce-<client>), then call list-orgs and persist the map.'),
)
}
export async function listSalesforceIntegrationNames(): Promise<Array<string>> {
const listed = await kody.integrationList({})
const rows = Array.isArray(listed)
? listed
: Array.isArray((listed as { integrations?: unknown }).integrations)
? (listed as { integrations: Array<unknown> }).integrations
: Array.isArray((listed as { items?: unknown }).items)
? (listed as { items: Array<unknown> }).items
: []
const names = new Set<string>()
for (const row of rows) {
if (!row || typeof row !== 'object') continue
const name = (row as { name?: unknown }).name
if (typeof name === 'string' && isSalesforceIntegrationName(name)) names.add(name)
}
if (names.size === 0) names.add('salesforce')
return [...names].sort()
}
function mappedHost(orgMap: OrgMap | undefined, organizationId: string | null): string | null {
if (!organizationId) return null
return lookupOrgMap(orgMap, organizationId)?.instanceHost ?? null
}
function optionalName(value: unknown): string | undefined {
if (typeof value !== 'string') return undefined
const trimmed = value.trim()
return trimmed.length > 0 ? trimmed : undefined
}
function normalizeMapEntry(value: OrgMapEntry | string, organizationId: string): OrgMapEntry {
if (typeof value === 'string') {
return { integrationName: requireString(value, `orgMap[${organizationId}]`) }
}
return {
integrationName: requireString(
value.integrationName,
`orgMap[${organizationId}].integrationName`,
),
label: typeof value.label === 'string' ? value.label : null,
instanceHost: typeof value.instanceHost === 'string' ? value.instanceHost : null,
}
}
function hostFromUserInfo(value: unknown): string | null {
if (typeof value !== 'string' || value.length === 0) return null
try {
return new URL(value).host
} catch {
return null
}
}
function findOrganizationId(value: unknown, depth: number, seen: Set<unknown>): string | null {
if (value === undefined || value === null || depth > 6) return null
if (typeof value === 'string') {
const trimmed = value.trim()
if (/^00D[a-zA-Z0-9]{12}([a-zA-Z0-9]{3})?$/.test(trimmed)) return trimmed
const match = trimmed.match(ORG_ID_PATTERN)
return match?.[0] ?? null
}
if (typeof value !== 'object') return null
if (seen.has(value)) return null
seen.add(value)
if (Array.isArray(value)) {
for (const item of value.slice(0, 20)) {
const found = findOrganizationId(item, depth + 1, seen)
if (found) return found
}
return null
}
const record = value as JsonRecord
for (const key of ORG_ID_KEYS) {
const field = record[key]
if (typeof field === 'string' && field.trim()) {
try {
return normalizeOrganizationId(field)
} catch {
const nested = findOrganizationId(field, depth + 1, seen)
if (nested) return nested
}
}
}
for (const nested of Object.values(record).slice(0, 30)) {
const found = findOrganizationId(nested, depth + 1, seen)
if (found) return found
}
return null
}
export function requireOrgResolveInput(params: unknown): ResolveOrgInput {
const input = requireRecord(params, 'resolve-org')
return {
organizationId:
typeof input.organizationId === 'string' ? input.organizationId : undefined,
integrationName:
typeof input.integrationName === 'string' ? input.integrationName : undefined,
orgMap: isOrgMap(input.orgMap) ? input.orgMap : undefined,
payload: input.payload,
error: input.error,
body: input.body,
}
}
function isOrgMap(value: unknown): value is OrgMap {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}