Internal engineering documentation for the AWS Secrets Manager side of the SuccessPro JWT API integration - the steps only ActiFi Dev performs. Companion to the client-facing "API User" setup doc (Permission Set / User / External Client App)

The keypair is generated by ActiFi Dev, not the client - the client only ever receives and uploads the public certificate ActiFi sends them.
Order of Operations:
Generate the keypair, send the .crt to the client admin. -> Step 1 below.
Create the Permission Set + API integration User. -> client-facing "API User" doc, steps 1-3.
Create/confirm the External Client App, upload the certificate ActiFi sent in step 1, enable JWT Bearer Flow, confirm scopes. -> client-facing doc, steps 4-6.
Send ActiFi Dev the Consumer Key / Secret. -> client-facing doc, step 7.
Run the aws cli secret upsert script with the .key file (from step 1) and the Consumer Key (from step 4). -> Step 2 below.
Verify every field in the resulting secret (AWS Console → Secrets Manager) stale values are the most common failure. -> Step 3 below.
Confirm the JWT branch is actually taken, not a silent fallback. -> Step 4 below.
openssl req -x509 -sha256 -nodes -days 3650 -newkey rsa:2048 \
-keyout {tenant}_salesforce_api_integration.key \
-out {tenant}_salesforce_api_integration.crt
.crt (public certificate) - send to the client admin for upload to the External Client App (Edit Settings -> OAuth -> Flow Enablement -> Enable JWT Bearer Flow -> Certificate Upload). Never goes into Secrets Manager..key (private key) - Goes to Secrets Manager only (Step 2 below).
roadmap-node/scripts/aws/upsert-aws-identity-secret.ts retrieves the existing ${tenant}_salesforce_api_creds secret, merges in values from local .key/.crt files, and saves it back (creates the secret if it doesn't exist, updates it otherwise - existing fields like sf_sync_username are preserved, not clobbered).
node --require ts-node/register --inspect-brk ./scripts/aws/upsert-aws-identity-secret.ts
Before running: edit the script's secretName and file path placeholders (currently [[TENANT]]) to the actual tenant name.
sf_client_id It only writes sf_private_key and sf_public_certificate. The Consumer Key (received from the client in step 4 of the sequence above) must be added separately - by hand-editing the AWS secret after running the script.
Most tenants already have some legacy ${tenant}_salesforce_api_creds secret in place - its pre-existing sf_client_id and sf_sync_username were almost certainly for something else and will NOT match this setup:
sf_client_id must match the Consumer Key of the exact External Client App the certificate was uploaded to. A stale value means the JWT branch authenticates against the wrong (or nonexistent) app.sf_sync_username must be the exact Username of the integration user created for this setup (e.g. dev+{tenant}@actifi.com). The JWT assertion's sub claim uses this value directly - a stale username authenticates as the wrong user, or fails outright.
${tenant}_salesforce_api_creds (ISalesforceAPICreds, roadmap-node/src/interfaces/salesforce/interfaces.ts:46-55):
Must match the integration user's actual Username; verify not stale
Consumer Key of the target ECA; verify not stale; not written by the script
Written by the script from the .key file
Written by the script from the .crt file; not read by getSalesforceConnection(), harmless either way
Org login URL, e.g. https://test.salesforce.com for a sandbox
Deliberately omitted for new JWT-first tenants
Deliberately omitted for new JWT-first tenants
getSalesforceConnection() (roadmap-node/src/lib/salesforce-util.ts) takes the JWT Bearer path whenever both sf_client_id and sf_private_key are present; otherwise it falls back to legacy conn.login(username, password + securityToken) - the deprecated SOAP path being retired (SP-6386).
For a , we deliberately leave sf_sync_password/ sf_sync_sec_token unpopulated. If the JWT fields are ever missing or wrong, the fallback should rather than silently limp along on a stored legacy credential - that failure is the signal something's misconfigured, not a risk to guard against.
This is different for conn.login() - those may still have real sf_sync_password/sf_sync_sec_token values in place intentionally, as a safety net during transition. Don't strip those without a specific reason to.