@wgroovy/sf-jwt 1.2.0 → 1.3.0

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.
package/README.md CHANGED
@@ -307,15 +307,19 @@ Every input accepts its content directly, a path to read it from, or `-` for std
307
307
  | `--payload <json>` | The claims to sign | required |
308
308
  | `--header <json>` | The header to sign | `{"alg":…,"typ":"JWT"}` |
309
309
  | `--key <path>` | Private key PEM; `-` reads stdin | unsigned when absent |
310
+ | `--iat` | Set `iat` to the current epoch second | — |
310
311
  | `--exp <seconds>` | Set `exp` to now plus this many seconds | — |
311
312
  | `--exp-at <epoch>` | Set `exp` absolutely; overrides `--exp` | — |
313
+ | `--jti` | Set `jti` to a fresh v4 UUID | — |
312
314
  | `--json` | `{token, header, claims}` on stdout | off |
313
315
 
314
316
  The key decides the algorithm and the header only names it. Leave `alg` out and it is filled in from the key — `RS256` for RSA, `ES256`/`ES384`/`ES512` by curve, `EdDSA` for Ed25519 and Ed448, `PS256` for a key that declares itself RSA-PSS. Name an algorithm the key cannot perform and it is refused with exit 2, rather than attempted: a P-384 key will happily produce bytes for an `ES256` request, and nothing on earth would accept the result.
315
317
 
316
318
  Supported: `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `ES256`, `ES384`, `ES512`, `EdDSA`. HMAC is deliberately absent — every algorithm here is one where the signing key cannot also verify, which is what keeps `--key` and `--verify` honest about which is which. ADR-0007 records the boundary.
317
319
 
318
- Apart from `exp`, nothing is added to your payload. `--exp` and `--exp-at` overwrite an `exp` already in it, and unlike everywhere else they are read from the flag only: an exported `SF_JWT_EXP` cannot quietly edit signed material.
320
+ Apart from `iat`, `exp` and `jti`, nothing is added to your payload, and none of those unless you name it. Each overwrites a claim of the same name already there, in the position its author gave it: `--iat` stamps the current epoch second, `--exp`/`--exp-at` an expiry, and `--jti` a fresh v4 UUID. The two time stamps read the same instant, so `exp` lands exactly `--exp` seconds after the `iat` beside it.
321
+
322
+ Unlike everywhere else, these are read from the flag only. `SF_JWT_EXP` and `SF_JWT_JTI` are ambient configuration for the Salesforce flows, which honour them, and neither may quietly edit signed material here; `--iat` has no environment form at all.
319
323
 
320
324
  With no `--key` and no `SF_JWT_KEY_FILE` or `SF_JWT_PRIVATE_KEY`, the payload is encoded but not signed:
321
325
 
package/dist/sf-jwt.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import{parseArgs as At}from"node:util";import{constants as Ge,sign as ze,verify as Ze}from"node:crypto";var h=class extends Error{constructor(t,{hint:n}={}){super(t),this.name="UsageError",this.exitCode=2,this.hint=n}},J=class extends Error{constructor(t,{cause:n,url:o}={}){super(t,{cause:n}),this.name="TransportError",this.exitCode=3,this.url=o}};var{RSA_PKCS1_PADDING:G,RSA_PKCS1_PSS_PADDING:z}=Ge,A="none",he={RS256:{family:"rsa",hash:"sha256",options:{padding:G}},RS384:{family:"rsa",hash:"sha384",options:{padding:G}},RS512:{family:"rsa",hash:"sha512",options:{padding:G}},PS256:{family:"rsa",hash:"sha256",options:{padding:z,saltLength:32}},PS384:{family:"rsa",hash:"sha384",options:{padding:z,saltLength:48}},PS512:{family:"rsa",hash:"sha512",options:{padding:z,saltLength:64}},ES256:{family:"ec",curve:"prime256v1",hash:"sha256",options:{dsaEncoding:"ieee-p1363"}},ES384:{family:"ec",curve:"secp384r1",hash:"sha384",options:{dsaEncoding:"ieee-p1363"}},ES512:{family:"ec",curve:"secp521r1",hash:"sha512",options:{dsaEncoding:"ieee-p1363"}},EdDSA:{family:"edwards",hash:null,options:{}}},C=Object.keys(he),Qe=new Set(["rsa","rsa-pss"]),fe=new Set(["ed25519","ed448"]),et={prime256v1:"ES256",secp384r1:"ES384",secp521r1:"ES512"},ue=e=>Buffer.from(e).toString("base64url");function tt(e){let t=e.asymmetricKeyType;return Qe.has(t)?"rsa":t==="ec"?"ec":fe.has(t)?"edwards":null}function Z(e){let t=e.asymmetricKeyType;if(t==="rsa")return"RS256";if(t==="rsa-pss")return"PS256";if(fe.has(t))return"EdDSA";if(t==="ec"){let n=e.asymmetricKeyDetails?.namedCurve,o=et[n];if(o)return o;throw new h(`No JWT algorithm exists for the EC curve ${n??"this key uses"}.`,{hint:`JWS defines only P-256, P-384 and P-521. Supported: ${C.join(", ")}.`})}throw new h(`A ${t??"key of an unrecognised type"} cannot sign a JWT.`,{hint:"Use an RSA, EC (P-256/P-384/P-521), Ed25519 or Ed448 private key."})}function R(e,t){if(t===A)throw new h('alg "none" cannot be requested.',{hint:"Omit --key and every SF_JWT key variable to produce an unsigned token; encode writes the none itself."});let n=he[t];if(!n){let r=/^HS(256|384|512)$/.test(t)?"sf-jwt signs asymmetrically only, so that a signing key can never verify. An HMAC secret does both.":`Supported: ${C.join(", ")}.`;throw new h(`Unsupported algorithm "${t}".`,{hint:r})}if(tt(e)!==n.family)throw new h(`The header asks for ${t}, but the key is ${e.asymmetricKeyType??"of an unrecognised type"}.`,{hint:`${t} needs ${{rsa:"an RSA",ec:"an EC",edwards:"an Ed25519 or Ed448"}[n.family]} key.`});if(n.curve){let r=e.asymmetricKeyDetails?.namedCurve;if(r!==n.curve)throw new h(`The header asks for ${t}, but the key's curve is ${r??"unknown"}.`,{hint:`${t} is defined only over ${n.curve}.`})}return n}var me=(e,t)=>`${ue(JSON.stringify(e))}.${ue(JSON.stringify(t))}`;function U(e,t,n){let o=R(n,e.alg),r=me(e,t),s=ze(o.hash,Buffer.from(r),{key:n,...o.options});return`${r}.${s.toString("base64url")}`}var ye=(e,t)=>`${me(e,t)}.`;function pe(e,t){let n;try{n=Buffer.from(e,"base64url").toString("utf8")}catch{throw new h(`The token's ${t} is not valid base64url.`)}let o;try{o=JSON.parse(n)}catch{throw new h(`The token's ${t} does not contain JSON.`,{hint:"A JWT carries base64url-encoded JSON in its first two segments."})}if(typeof o!="object"||o===null||Array.isArray(o))throw new h(`The token's ${t} is not a JSON object.`);return o}function Q(e){let n=e.trim().split(".");if(n.length!==3)throw new h(`A JWT has three dot-separated segments; this has ${n.length}.`,{hint:n.length===5?"Five segments means a JWE. This tool reads signed tokens, not encrypted ones.":void 0});let[o,r,s]=n,a=pe(o,"header");return{header:a,claims:pe(r,"payload"),signature:s,signingInput:`${o}.${r}`,signed:s!==""&&a.alg!==A}}var nt=1e11,ot=e=>{try{return new Date(e*1e3).toISOString()}catch{return null}};function ge(e,t=Date.now()){let n=Math.floor(t/1e3),o={};for(let r of["iat","nbf","exp"]){let s=e[r];typeof s!="number"||!Number.isFinite(s)||(o[r]={value:s,at:ot(s),deltaSeconds:s-n,looksLikeMilliseconds:s>=nt})}return{...o,expired:o.exp?o.exp.deltaSeconds<=0:null,notYetValid:o.nbf?o.nbf.deltaSeconds>0:null}}function Se({header:e,signingInput:t,signature:n},o){let r=R(o,e.alg);return Ze(r.hash,Buffer.from(t),{key:o,...r.options},Buffer.from(n,"base64url"))}var rt={alg:"RS256",typ:"JWT"};function we({iss:e,sub:t,aud:n,expiresInSeconds:o,expiresAt:r,now:s=Date.now()}){let a=Math.floor(s/1e3),i=r??a+o;return t?{iss:e,sub:t,aud:n,exp:i}:{iss:e,aud:n,exp:i}}function ee(e,t){return U(rt,e,t)}var st=[{id:"assertion-expired",error:"invalid_grant",match:"expired authorization code",cause:"Salesforce considered the assertion already expired when it arrived. Despite the wording, this is not about an authorization code.",remediation:["The exp claim must be in the future when Salesforce receives it. Check the clock drift reported above \u2014 a machine running fast enough will mint assertions that are already stale.","Confirm exp is in seconds, not milliseconds. A millisecond value is read as a date far in the past or future.","If you passed --exp-at, check the epoch you supplied is the time you meant."]},{id:"invalid-signature",error:"invalid_grant",match:"invalid signature",cause:"The signature did not verify against the certificate registered on the app.",remediation:['The private key you signed with does not match the uploaded certificate. Re-upload the .crt generated from this exact key under Flow Enablement (Connected Apps: "Use digital signatures").',"Confirm you are pointed at the org that holds this app \u2014 a sandbox copy carries its own certificate."]},{id:"user-not-approved",error:"invalid_grant",match:"user hasn\u2019t approved this consumer",cause:"The Subject User has never authorized this app, and the app is not configured to pre-authorize users.",remediation:['External Client App Manager \u2192 your app \u2192 Policies \u2192 Edit \u2192 OAuth Policies \u2192 set Permitted Users to "Admin approved users are pre-authorized". (Connected Apps: Manage \u2192 Edit Policies.)',"Then assign the Subject User\u2019s Profile or a Permission Set under App Policies, otherwise pre-authorization applies to nobody."]},{id:"invalid-audience",error:"invalid_grant",match:"invalid audience",cause:"The aud claim does not match the endpoint that received the assertion.",remediation:["Production and Developer Edition orgs use https://login.salesforce.com; sandboxes use https://test.salesforce.com (or pass --sandbox).","Experience Cloud sites need the full site URL as aud, including its path."]},{id:"ip-restricted",error:"invalid_grant",match:"ip restricted",cause:"The app or the user\u2019s profile blocked the calling IP address.",remediation:['External Client App Manager \u2192 your app \u2192 Policies \u2192 Edit \u2192 OAuth Policies \u2192 IP Relaxation \u2192 "Relax IP restrictions". (Connected Apps: Manage \u2192 Edit Policies.)',"Alternatively add this machine\u2019s address to the profile\u2019s Login IP Ranges."]},{id:"inactive-user",error:"invalid_grant",match:"inactive user",cause:"The Subject User exists but is deactivated in this org.",remediation:["Reactivate the user, or point --sub at an active integration user."]},{id:"user-locked-out",error:"invalid_grant",match:"user is locked out",cause:"The Subject User is locked out of the org.",remediation:["Unlock the user in Setup \u2192 Users, then retry."]},{id:"invalid-assertion",error:"invalid_grant",match:"invalid assertion",cause:"Salesforce could not parse the assertion at all.",remediation:["The iss, aud, and exp claims must all be present, and exp must be a number.",'The header must be {"alg":"RS256"}; Salesforce rejects other algorithms outright.']},{id:"invalid-client-id",error:"invalid_client_id",cause:"The iss claim is not a consumer key this org recognises.",remediation:["Copy the consumer key from External Client App Manager \u2192 your app \u2192 Settings \u2192 OAuth Settings, and check for truncation \u2014 they are long. (Connected Apps: App Manager \u2192 View.)","Confirm the org matches: a sandbox app has a different consumer key from its production original.","A newly created or modified app can take up to 10 minutes to become usable."]},{id:"invalid-app-access",error:"invalid_app_access",cause:"The app exists, but this user is not permitted to use it.",remediation:["Assign the Subject User\u2019s Profile or a Permission Set to the app under Policies \u2192 App Policies. (Connected Apps: Manage \u2192 Profiles / Permission Sets.)"]},{id:"app-not-found-no-subject",error:"app_not_found",when:e=>e!==null&&!("sub"in e),cause:"The assertion named no subject. Despite the wording, the app is fine \u2014 Salesforce returns this when it cannot resolve who the token would be for.",remediation:["Pass --sub with the username of the Salesforce user the token should act as.","Salesforce documents the subject as an Experience Cloud concern, but a standard org rejects an assertion without one (ADR-0004), so supply it everywhere."]},{id:"app-not-found",error:"app_not_found",cause:"Salesforce could not resolve an app for this assertion.",remediation:["Confirm the consumer key came from this org \u2014 a sandbox app has a different key from its production original.","A newly created or modified app can take up to 10 minutes to become usable."]},{id:"inactive-org",error:"inactive_org",cause:"The org itself is inactive, expired, or locked.",remediation:["Check whether the org (often a trial or scratch org) has expired."]},{id:"unsupported-grant-type",error:"unsupported_grant_type",cause:"The org or app does not permit the JWT bearer grant.",remediation:['External Client App Manager \u2192 your app \u2192 Settings \u2192 OAuth \u2192 Flow Enablement \u2192 tick "Enable JWT Bearer Flow" and upload the certificate. (Connected Apps: tick "Use digital signatures".)',"Confirm the JWT bearer flow is among the app\u2019s permitted flows."]},{id:"invalid-grant-generic",error:"invalid_grant",cause:"Salesforce rejected the assertion without a description this tool recognises.",remediation:["The usual suspects: an expiry already in the past, a key that does not match the uploaded certificate, or a user who is not pre-authorized on the app."]}],W=e=>(e??"").toLowerCase().replaceAll("\u2019","'");function Te({error:e,errorDescription:t}={},n=null){let o=W(e),r=W(t);return st.find(s=>W(s.error)===o&&(!s.match||r.includes(W(s.match)))&&(!s.when||s.when(n)))??null}import{readFile as it}from"node:fs/promises";var N="-";async function M(e){let t=[];for await(let n of e)t.push(n);return Buffer.concat(t).toString("utf8")}function at(e){let t=e.trim().split(".");if(t.length!==3||!/^[A-Za-z0-9_-]+$/.test(t[0]))return!1;try{let n=JSON.parse(Buffer.from(t[0],"base64url").toString("utf8"));return typeof n=="object"&&n!==null&&!Array.isArray(n)}catch{return!1}}var ct=e=>/^\s*\{/.test(e);function dt(e,t){return e===N?"stdin":t==="json"&&ct(e)||t==="token"&&at(e)?"inline":"path"}async function L(e,{flag:t,kind:n,stdin:o=process.stdin}){let r=dt(e,n);if(r==="inline")return{text:e,source:"inline"};if(r==="stdin")return{text:await M(o),source:"stdin"};try{return{text:await it(e,"utf8"),source:e}}catch(s){throw new h(`Could not read --${t} at ${e}: ${s.code??s.message}`,{hint:n==="json"?"A value starting with { is read as JSON; anything else is read as a path.":"A value is read as a token when its first segment decodes to a JWT header, and as a path otherwise \u2014 so if you meant a token, that token is malformed."})}}function te(e,t){let n;try{n=JSON.parse(e)}catch(o){throw new h(`--${t} is not valid JSON.`,{hint:o.message})}if(typeof n!="object"||n===null||Array.isArray(n))throw new h(`--${t} must be a JSON object, got ${Array.isArray(n)?"an array":typeof n}.`);return n}function ne(e,t){let n=t.filter(o=>e[o]===N);if(n.length>1)throw new h(`--${n[0]} and --${n[1]} both asked to read stdin.`,{hint:"Only one input can come from a pipe. Put the other in a file, or pass it inline."})}import{createPrivateKey as _e,createPublicKey as ke}from"node:crypto";import{readFile as oe}from"node:fs/promises";import{createInterface as lt}from"node:readline";var be=2048,ut=["BEGIN ENCRYPTED PRIVATE KEY","Proc-Type: 4,ENCRYPTED"],pt={prime256v1:"P-256",secp384r1:"P-384",secp521r1:"P-521"};function Ee(e){return ut.some(t=>e.includes(t))}function $e(e,{input:t=process.stdin,output:n=process.stderr}={}){return new Promise((o,r)=>{if(!t.isTTY){r(new h("The private key is passphrase-protected and no passphrase was supplied.",{hint:"Set SF_JWT_KEY_PASSPHRASE, or run from an interactive terminal to be prompted."}));return}let s=lt({input:t,output:n,terminal:!0}),a=!1;s._writeToOutput=i=>{a||n.write(i)},s.question(e,i=>{s.close(),n.write(`
3
- `),o(i)}),a=!0})}async function ht({keyPath:e,keyFileEnv:t,inlineKeyEnv:n,stdin:o}){if(e===N)return{pem:await M(o),source:"stdin"};if(e)try{return{pem:await oe(e,"utf8"),source:e}}catch(r){throw new h(`Could not read the private key at ${e}: ${r.code??r.message}`)}if(t)try{return{pem:await oe(t,"utf8"),source:`${t} (SF_JWT_KEY_FILE)`}}catch(r){throw new h(`Could not read SF_JWT_KEY_FILE at ${t}: ${r.code??r.message}`)}if(n)return{pem:n,source:"SF_JWT_PRIVATE_KEY"};throw new h("No private key supplied.",{hint:"Pass --key <path>, set SF_JWT_KEY_FILE or SF_JWT_PRIVATE_KEY, or pipe a PEM with --key -"})}function K(e){let t=e.asymmetricKeyDetails??{};return{type:e.asymmetricKeyType??null,bits:t.modulusLength??null,curve:t.namedCurve??null}}function D({type:e,bits:t,curve:n}){return e==="rsa"||e==="rsa-pss"?`${e==="rsa-pss"?"RSA-PSS":"RSA"} ${t}-bit`:e==="ec"?`EC ${pt[n]??n}`:e==="ed25519"?"Ed25519":e==="ed448"?"Ed448":e??"of an unrecognised type"}function ft(e){let t=e.asymmetricKeyType;if(t!=="rsa"&&t!=="rsa-pss")throw new h(`The private key is ${t??"of an unrecognised type"}, but RS256 requires RSA.`,{hint:"Generate one with: openssl genrsa -out server.key 2048"});let n=e.asymmetricKeyDetails?.modulusLength??0;if(n<be)throw new h(`The private key is ${n}-bit; Salesforce requires at least ${be}.`);return{type:t,bits:n}}async function Y({keyPath:e,keyFileEnv:t,inlineKeyEnv:n,passphraseEnv:o,stdin:r=process.stdin,prompt:s=$e,assertUsable:a=ft}={}){let{pem:i,source:d}=await ht({keyPath:e,keyFileEnv:t,inlineKeyEnv:n,stdin:r}),u=Ee(i),c=u?o??await s("Private key passphrase: "):void 0,p;try{p=_e(c?{key:i,passphrase:c}:i)}catch(l){let m=u?"the passphrase is wrong or the key is malformed":"it is not a readable PEM private key (an encrypted key needs a passphrase)";throw new h(`Could not load the private key from ${d} \u2014 ${m}.`,{hint:l.message})}return a(p),{keyObject:p,source:d,encrypted:u,...K(p)}}async function ve({path:e,passphraseEnv:t,stdin:n=process.stdin,prompt:o=$e}={}){let r,s;if(e===N)({pem:r,source:s}={pem:await M(n),source:"stdin"});else try{r=await oe(e,"utf8"),s=e}catch(d){throw new h(`Could not read --verify at ${e}: ${d.code??d.message}`)}let a=r.includes("BEGIN CERTIFICATE"),i=r.includes("PRIVATE KEY");try{if(i){let d=Ee(r)?t??await o("Private key passphrase: "):void 0,u=_e(d?{key:r,passphrase:d}:r);return{keyObject:ke(u),source:s,kind:"private key"}}return{keyObject:ke(r),source:s,kind:a?"certificate":"public key"}}catch(d){throw new h(`Could not read verification material from ${s}.`,{hint:`Expected a PEM X.509 certificate, public key, or private key. ${d.message}`})}}var mt=new Set(["authorization","cookie","set-cookie"]),yt=new Set(["access_token","refresh_token","id_token"]),gt=new Set(["assertion","client_secret"]),P={reset:"\x1B[0m",dim:"\x1B[2m",green:"\x1B[32m",red:"\x1B[31m",yellow:"\x1B[33m",alarm:"\x1B[1;41;97m"};function re(e=process.stderr,t=process.env){return t.NO_COLOR?!1:t.FORCE_COLOR?!0:!!e.isTTY&&t.TERM!=="dumb"}function se(e){let t=n=>o=>e&&o?`${n}${o}${P.reset}`:o;return{enabled:e,dim:t(P.dim),green:t(P.green),red:t(P.red),yellow:t(P.yellow),alarm:t(P.alarm)}}function St(e=process.stderr,t=process.env){return!!e.isTTY&&!t.CI}function B(e,t){return typeof e!="string"||e.length===0?"(empty)":t?e:`${e.slice(0,12)}\u2026 [${e.length} chars, redacted \u2014 not a TTY]`}function xe(e,t){return Object.entries(e??{}).map(([n,o])=>{let r=mt.has(n.toLowerCase())?B(o,t):o;return`${n}: ${r}`})}function wt(e,t){return e?[...new URLSearchParams(e)].map(([n,o])=>{let r=gt.has(n)?B(o,t):o;return`${n}=${r}`}):["(no body)"]}function Ae(e,t){if(!e)return["(empty body)"];let n;try{n=JSON.parse(e)}catch{return e.split(`
2
+ import{parseArgs as It}from"node:util";import{constants as Xe,randomUUID as Ge,sign as ze,verify as Ze}from"node:crypto";var h=class extends Error{constructor(t,{hint:n}={}){super(t),this.name="UsageError",this.exitCode=2,this.hint=n}},F=class extends Error{constructor(t,{cause:n,url:o}={}){super(t,{cause:n}),this.name="TransportError",this.exitCode=3,this.url=o}};var{RSA_PKCS1_PADDING:G,RSA_PKCS1_PSS_PADDING:z}=Xe,A="none",fe={RS256:{family:"rsa",hash:"sha256",options:{padding:G}},RS384:{family:"rsa",hash:"sha384",options:{padding:G}},RS512:{family:"rsa",hash:"sha512",options:{padding:G}},PS256:{family:"rsa",hash:"sha256",options:{padding:z,saltLength:32}},PS384:{family:"rsa",hash:"sha384",options:{padding:z,saltLength:48}},PS512:{family:"rsa",hash:"sha512",options:{padding:z,saltLength:64}},ES256:{family:"ec",curve:"prime256v1",hash:"sha256",options:{dsaEncoding:"ieee-p1363"}},ES384:{family:"ec",curve:"secp384r1",hash:"sha384",options:{dsaEncoding:"ieee-p1363"}},ES512:{family:"ec",curve:"secp521r1",hash:"sha512",options:{dsaEncoding:"ieee-p1363"}},EdDSA:{family:"edwards",hash:null,options:{}}},C=Object.keys(fe),Qe=new Set(["rsa","rsa-pss"]),me=new Set(["ed25519","ed448"]),et={prime256v1:"ES256",secp384r1:"ES384",secp521r1:"ES512"},ue=e=>Buffer.from(e).toString("base64url");function tt(e){let t=e.asymmetricKeyType;return Qe.has(t)?"rsa":t==="ec"?"ec":me.has(t)?"edwards":null}function Z(e){let t=e.asymmetricKeyType;if(t==="rsa")return"RS256";if(t==="rsa-pss")return"PS256";if(me.has(t))return"EdDSA";if(t==="ec"){let n=e.asymmetricKeyDetails?.namedCurve,o=et[n];if(o)return o;throw new h(`No JWT algorithm exists for the EC curve ${n??"this key uses"}.`,{hint:`JWS defines only P-256, P-384 and P-521. Supported: ${C.join(", ")}.`})}throw new h(`A ${t??"key of an unrecognised type"} cannot sign a JWT.`,{hint:"Use an RSA, EC (P-256/P-384/P-521), Ed25519 or Ed448 private key."})}function R(e,t){if(t===A)throw new h('alg "none" cannot be requested.',{hint:"Omit --key and every SF_JWT key variable to produce an unsigned token; encode writes the none itself."});let n=fe[t];if(!n){let r=/^HS(256|384|512)$/.test(t)?"sf-jwt signs asymmetrically only, so that a signing key can never verify. An HMAC secret does both.":`Supported: ${C.join(", ")}.`;throw new h(`Unsupported algorithm "${t}".`,{hint:r})}if(tt(e)!==n.family)throw new h(`The header asks for ${t}, but the key is ${e.asymmetricKeyType??"of an unrecognised type"}.`,{hint:`${t} needs ${{rsa:"an RSA",ec:"an EC",edwards:"an Ed25519 or Ed448"}[n.family]} key.`});if(n.curve){let r=e.asymmetricKeyDetails?.namedCurve;if(r!==n.curve)throw new h(`The header asks for ${t}, but the key's curve is ${r??"unknown"}.`,{hint:`${t} is defined only over ${n.curve}.`})}return n}var ye=(e,t)=>`${ue(JSON.stringify(e))}.${ue(JSON.stringify(t))}`;function U(e,t,n){let o=R(n,e.alg),r=ye(e,t),s=ze(o.hash,Buffer.from(r),{key:n,...o.options});return`${r}.${s.toString("base64url")}`}var ge=(e,t)=>`${ye(e,t)}.`;function he(e,t){let n;try{n=Buffer.from(e,"base64url").toString("utf8")}catch{throw new h(`The token's ${t} is not valid base64url.`)}let o;try{o=JSON.parse(n)}catch{throw new h(`The token's ${t} does not contain JSON.`,{hint:"A JWT carries base64url-encoded JSON in its first two segments."})}if(typeof o!="object"||o===null||Array.isArray(o))throw new h(`The token's ${t} is not a JSON object.`);return o}function Q(e){let n=e.trim().split(".");if(n.length!==3)throw new h(`A JWT has three dot-separated segments; this has ${n.length}.`,{hint:n.length===5?"Five segments means a JWE. This tool reads signed tokens, not encrypted ones.":void 0});let[o,r,s]=n,a=he(o,"header");return{header:a,claims:he(r,"payload"),signature:s,signingInput:`${o}.${r}`,signed:s!==""&&a.alg!==A}}var ee=()=>Ge(),nt=1e11,ot=e=>{try{return new Date(e*1e3).toISOString()}catch{return null}};function Se(e,t=Date.now()){let n=Math.floor(t/1e3),o={};for(let r of["iat","nbf","exp"]){let s=e[r];typeof s!="number"||!Number.isFinite(s)||(o[r]={value:s,at:ot(s),deltaSeconds:s-n,looksLikeMilliseconds:s>=nt})}return{...o,expired:o.exp?o.exp.deltaSeconds<=0:null,notYetValid:o.nbf?o.nbf.deltaSeconds>0:null}}function we({header:e,signingInput:t,signature:n},o){let r=R(o,e.alg);return Ze(r.hash,Buffer.from(t),{key:o,...r.options},Buffer.from(n,"base64url"))}var rt={alg:"RS256",typ:"JWT"};function Te({iss:e,sub:t,aud:n,expiresInSeconds:o,expiresAt:r,now:s=Date.now()}){let a=Math.floor(s/1e3),i=r??a+o;return t?{iss:e,sub:t,aud:n,exp:i}:{iss:e,aud:n,exp:i}}function te(e,t){return U(rt,e,t)}var st=[{id:"assertion-expired",error:"invalid_grant",match:"expired authorization code",cause:"Salesforce considered the assertion already expired when it arrived. Despite the wording, this is not about an authorization code.",remediation:["The exp claim must be in the future when Salesforce receives it. Check the clock drift reported above \u2014 a machine running fast enough will mint assertions that are already stale.","Confirm exp is in seconds, not milliseconds. A millisecond value is read as a date far in the past or future.","If you passed --exp-at, check the epoch you supplied is the time you meant."]},{id:"invalid-signature",error:"invalid_grant",match:"invalid signature",cause:"The signature did not verify against the certificate registered on the app.",remediation:['The private key you signed with does not match the uploaded certificate. Re-upload the .crt generated from this exact key under Flow Enablement (Connected Apps: "Use digital signatures").',"Confirm you are pointed at the org that holds this app \u2014 a sandbox copy carries its own certificate."]},{id:"user-not-approved",error:"invalid_grant",match:"user hasn\u2019t approved this consumer",cause:"The Subject User has never authorized this app, and the app is not configured to pre-authorize users.",remediation:['External Client App Manager \u2192 your app \u2192 Policies \u2192 Edit \u2192 OAuth Policies \u2192 set Permitted Users to "Admin approved users are pre-authorized". (Connected Apps: Manage \u2192 Edit Policies.)',"Then assign the Subject User\u2019s Profile or a Permission Set under App Policies, otherwise pre-authorization applies to nobody."]},{id:"invalid-audience",error:"invalid_grant",match:"invalid audience",cause:"The aud claim does not match the endpoint that received the assertion.",remediation:["Production and Developer Edition orgs use https://login.salesforce.com; sandboxes use https://test.salesforce.com (or pass --sandbox).","Experience Cloud sites need the full site URL as aud, including its path."]},{id:"ip-restricted",error:"invalid_grant",match:"ip restricted",cause:"The app or the user\u2019s profile blocked the calling IP address.",remediation:['External Client App Manager \u2192 your app \u2192 Policies \u2192 Edit \u2192 OAuth Policies \u2192 IP Relaxation \u2192 "Relax IP restrictions". (Connected Apps: Manage \u2192 Edit Policies.)',"Alternatively add this machine\u2019s address to the profile\u2019s Login IP Ranges."]},{id:"inactive-user",error:"invalid_grant",match:"inactive user",cause:"The Subject User exists but is deactivated in this org.",remediation:["Reactivate the user, or point --sub at an active integration user."]},{id:"user-locked-out",error:"invalid_grant",match:"user is locked out",cause:"The Subject User is locked out of the org.",remediation:["Unlock the user in Setup \u2192 Users, then retry."]},{id:"invalid-assertion",error:"invalid_grant",match:"invalid assertion",cause:"Salesforce could not parse the assertion at all.",remediation:["The iss, aud, and exp claims must all be present, and exp must be a number.",'The header must be {"alg":"RS256"}; Salesforce rejects other algorithms outright.']},{id:"invalid-client-id",error:"invalid_client_id",cause:"The iss claim is not a consumer key this org recognises.",remediation:["Copy the consumer key from External Client App Manager \u2192 your app \u2192 Settings \u2192 OAuth Settings, and check for truncation \u2014 they are long. (Connected Apps: App Manager \u2192 View.)","Confirm the org matches: a sandbox app has a different consumer key from its production original.","A newly created or modified app can take up to 10 minutes to become usable."]},{id:"invalid-app-access",error:"invalid_app_access",cause:"The app exists, but this user is not permitted to use it.",remediation:["Assign the Subject User\u2019s Profile or a Permission Set to the app under Policies \u2192 App Policies. (Connected Apps: Manage \u2192 Profiles / Permission Sets.)"]},{id:"app-not-found-no-subject",error:"app_not_found",when:e=>e!==null&&!("sub"in e),cause:"The assertion named no subject. Despite the wording, the app is fine \u2014 Salesforce returns this when it cannot resolve who the token would be for.",remediation:["Pass --sub with the username of the Salesforce user the token should act as.","Salesforce documents the subject as an Experience Cloud concern, but a standard org rejects an assertion without one (ADR-0004), so supply it everywhere."]},{id:"app-not-found",error:"app_not_found",cause:"Salesforce could not resolve an app for this assertion.",remediation:["Confirm the consumer key came from this org \u2014 a sandbox app has a different key from its production original.","A newly created or modified app can take up to 10 minutes to become usable."]},{id:"inactive-org",error:"inactive_org",cause:"The org itself is inactive, expired, or locked.",remediation:["Check whether the org (often a trial or scratch org) has expired."]},{id:"unsupported-grant-type",error:"unsupported_grant_type",cause:"The org or app does not permit the JWT bearer grant.",remediation:['External Client App Manager \u2192 your app \u2192 Settings \u2192 OAuth \u2192 Flow Enablement \u2192 tick "Enable JWT Bearer Flow" and upload the certificate. (Connected Apps: tick "Use digital signatures".)',"Confirm the JWT bearer flow is among the app\u2019s permitted flows."]},{id:"invalid-grant-generic",error:"invalid_grant",cause:"Salesforce rejected the assertion without a description this tool recognises.",remediation:["The usual suspects: an expiry already in the past, a key that does not match the uploaded certificate, or a user who is not pre-authorized on the app."]}],W=e=>(e??"").toLowerCase().replaceAll("\u2019","'");function ke({error:e,errorDescription:t}={},n=null){let o=W(e),r=W(t);return st.find(s=>W(s.error)===o&&(!s.match||r.includes(W(s.match)))&&(!s.when||s.when(n)))??null}import{readFile as it}from"node:fs/promises";var D="-";async function M(e){let t=[];for await(let n of e)t.push(n);return Buffer.concat(t).toString("utf8")}function at(e){let t=e.trim().split(".");if(t.length!==3||!/^[A-Za-z0-9_-]+$/.test(t[0]))return!1;try{let n=JSON.parse(Buffer.from(t[0],"base64url").toString("utf8"));return typeof n=="object"&&n!==null&&!Array.isArray(n)}catch{return!1}}var ct=e=>/^\s*\{/.test(e);function dt(e,t){return e===D?"stdin":t==="json"&&ct(e)||t==="token"&&at(e)?"inline":"path"}async function L(e,{flag:t,kind:n,stdin:o=process.stdin}){let r=dt(e,n);if(r==="inline")return{text:e,source:"inline"};if(r==="stdin")return{text:await M(o),source:"stdin"};try{return{text:await it(e,"utf8"),source:e}}catch(s){throw new h(`Could not read --${t} at ${e}: ${s.code??s.message}`,{hint:n==="json"?"A value starting with { is read as JSON; anything else is read as a path.":"A value is read as a token when its first segment decodes to a JWT header, and as a path otherwise \u2014 so if you meant a token, that token is malformed."})}}function ne(e,t){let n;try{n=JSON.parse(e)}catch(o){throw new h(`--${t} is not valid JSON.`,{hint:o.message})}if(typeof n!="object"||n===null||Array.isArray(n))throw new h(`--${t} must be a JSON object, got ${Array.isArray(n)?"an array":typeof n}.`);return n}function oe(e,t){let n=t.filter(o=>e[o]===D);if(n.length>1)throw new h(`--${n[0]} and --${n[1]} both asked to read stdin.`,{hint:"Only one input can come from a pipe. Put the other in a file, or pass it inline."})}import{createPrivateKey as Ee,createPublicKey as be}from"node:crypto";import{readFile as re}from"node:fs/promises";import{createInterface as lt}from"node:readline";var _e=2048,pt=["BEGIN ENCRYPTED PRIVATE KEY","Proc-Type: 4,ENCRYPTED"],ut={prime256v1:"P-256",secp384r1:"P-384",secp521r1:"P-521"};function $e(e){return pt.some(t=>e.includes(t))}function ve(e,{input:t=process.stdin,output:n=process.stderr}={}){return new Promise((o,r)=>{if(!t.isTTY){r(new h("The private key is passphrase-protected and no passphrase was supplied.",{hint:"Set SF_JWT_KEY_PASSPHRASE, or run from an interactive terminal to be prompted."}));return}let s=lt({input:t,output:n,terminal:!0}),a=!1;s._writeToOutput=i=>{a||n.write(i)},s.question(e,i=>{s.close(),n.write(`
3
+ `),o(i)}),a=!0})}async function ht({keyPath:e,keyFileEnv:t,inlineKeyEnv:n,stdin:o}){if(e===D)return{pem:await M(o),source:"stdin"};if(e)try{return{pem:await re(e,"utf8"),source:e}}catch(r){throw new h(`Could not read the private key at ${e}: ${r.code??r.message}`)}if(t)try{return{pem:await re(t,"utf8"),source:`${t} (SF_JWT_KEY_FILE)`}}catch(r){throw new h(`Could not read SF_JWT_KEY_FILE at ${t}: ${r.code??r.message}`)}if(n)return{pem:n,source:"SF_JWT_PRIVATE_KEY"};throw new h("No private key supplied.",{hint:"Pass --key <path>, set SF_JWT_KEY_FILE or SF_JWT_PRIVATE_KEY, or pipe a PEM with --key -"})}function K(e){let t=e.asymmetricKeyDetails??{};return{type:e.asymmetricKeyType??null,bits:t.modulusLength??null,curve:t.namedCurve??null}}function N({type:e,bits:t,curve:n}){return e==="rsa"||e==="rsa-pss"?`${e==="rsa-pss"?"RSA-PSS":"RSA"} ${t}-bit`:e==="ec"?`EC ${ut[n]??n}`:e==="ed25519"?"Ed25519":e==="ed448"?"Ed448":e??"of an unrecognised type"}function ft(e){let t=e.asymmetricKeyType;if(t!=="rsa"&&t!=="rsa-pss")throw new h(`The private key is ${t??"of an unrecognised type"}, but RS256 requires RSA.`,{hint:"Generate one with: openssl genrsa -out server.key 2048"});let n=e.asymmetricKeyDetails?.modulusLength??0;if(n<_e)throw new h(`The private key is ${n}-bit; Salesforce requires at least ${_e}.`);return{type:t,bits:n}}async function Y({keyPath:e,keyFileEnv:t,inlineKeyEnv:n,passphraseEnv:o,stdin:r=process.stdin,prompt:s=ve,assertUsable:a=ft}={}){let{pem:i,source:d}=await ht({keyPath:e,keyFileEnv:t,inlineKeyEnv:n,stdin:r}),p=$e(i),c=p?o??await s("Private key passphrase: "):void 0,u;try{u=Ee(c?{key:i,passphrase:c}:i)}catch(l){let m=p?"the passphrase is wrong or the key is malformed":"it is not a readable PEM private key (an encrypted key needs a passphrase)";throw new h(`Could not load the private key from ${d} \u2014 ${m}.`,{hint:l.message})}return a(u),{keyObject:u,source:d,encrypted:p,...K(u)}}async function xe({path:e,passphraseEnv:t,stdin:n=process.stdin,prompt:o=ve}={}){let r,s;if(e===D)({pem:r,source:s}={pem:await M(n),source:"stdin"});else try{r=await re(e,"utf8"),s=e}catch(d){throw new h(`Could not read --verify at ${e}: ${d.code??d.message}`)}let a=r.includes("BEGIN CERTIFICATE"),i=r.includes("PRIVATE KEY");try{if(i){let d=$e(r)?t??await o("Private key passphrase: "):void 0,p=Ee(d?{key:r,passphrase:d}:r);return{keyObject:be(p),source:s,kind:"private key"}}return{keyObject:be(r),source:s,kind:a?"certificate":"public key"}}catch(d){throw new h(`Could not read verification material from ${s}.`,{hint:`Expected a PEM X.509 certificate, public key, or private key. ${d.message}`})}}var mt=new Set(["authorization","cookie","set-cookie"]),yt=new Set(["access_token","refresh_token","id_token"]),gt=new Set(["assertion","client_secret"]),P={reset:"\x1B[0m",dim:"\x1B[2m",green:"\x1B[32m",red:"\x1B[31m",yellow:"\x1B[33m",alarm:"\x1B[1;41;97m"};function se(e=process.stderr,t=process.env){return t.NO_COLOR?!1:t.FORCE_COLOR?!0:!!e.isTTY&&t.TERM!=="dumb"}function ie(e){let t=n=>o=>e&&o?`${n}${o}${P.reset}`:o;return{enabled:e,dim:t(P.dim),green:t(P.green),red:t(P.red),yellow:t(P.yellow),alarm:t(P.alarm)}}function St(e=process.stderr,t=process.env){return!!e.isTTY&&!t.CI}function B(e,t){return typeof e!="string"||e.length===0?"(empty)":t?e:`${e.slice(0,12)}\u2026 [${e.length} chars, redacted \u2014 not a TTY]`}function Ae(e,t){return Object.entries(e??{}).map(([n,o])=>{let r=mt.has(n.toLowerCase())?B(o,t):o;return`${n}: ${r}`})}function wt(e,t){return e?[...new URLSearchParams(e)].map(([n,o])=>{let r=gt.has(n)?B(o,t):o;return`${n}=${r}`}):["(no body)"]}function Ie(e,t){if(!e)return["(empty body)"];let n;try{n=JSON.parse(e)}catch{return e.split(`
4
4
  `)}if(typeof n!="object"||n===null||Array.isArray(n))return JSON.stringify(n,null,2).split(`
5
5
  `);let o=Object.fromEntries(Object.entries(n).map(([r,s])=>[r,yt.has(r)&&typeof s=="string"?B(s,t):s]));return JSON.stringify(o,null,2).split(`
6
- `)}var Tt=e=>Math.min(100,Math.max(56,e??76));function O({verbose:e=!1,stream:t=process.stderr,env:n=process.env,showSecrets:o,colors:r}={}){let s=o??St(t,n),a=se(r??re(t,n)),i=Tt(t.columns),d=l=>t.write(`${a.dim(l)}
7
- `),u=l=>{e&&d(l)},c=l=>{if(!e)return;d("");let m=`\u2500\u2500 ${l} `;d(m+"\u2500".repeat(Math.max(3,i-m.length)))},p=l=>{if(!(!e||l.length===0)){d("");for(let m of l)d(` ${m}`)}};return{verbose:e,printSecrets:s,style:a,section:c,detail(l,m){u(` ${`${l}:`.padEnd(14)}${m}`)},secret(l,m){u(` ${`${l}:`.padEnd(14)}${B(m,s)}`)},warn(l){t.write(`${a.yellow(` warning: ${l}`)}
8
- `)},httpRequest({method:l,url:m,headers:y,body:g},w="HTTP"){if(e){c(`${w} request`),d(` ${l} ${m}`);for(let S of xe(y,s))d(` ${S}`);if(g){let S=String(y?.["content-type"]??"").includes("json");p(S?Ae(g,s):wt(g,s))}}},httpResponse({status:l,statusText:m,headers:y,rawBody:g,elapsedMs:w},S="HTTP"){if(e){c(`${S} response`),d(` HTTP ${l}${m?` ${m}`:""} \u2014 ${w}ms`);for(let f of xe(y,s))d(` ${f}`);p(Ae(g,s))}}}}var kt="urn:ietf:params:oauth:grant-type:jwt-bearer";function Ie(e){return`${e.replace(/\/+$/,"")}/services/oauth2/token`}function Pe(e){return`${e.replace(/\/+$/,"")}/services/oauth2/userinfo`}function je(e,t=Date.now()){if(!e)return null;let n=Date.parse(e);return Number.isNaN(n)?null:Math.round((t-n)/1e3)}async function ie(e,t,{timeoutMs:n,fetchImpl:o,now:r}){let s=performance.now(),a=r(),i;try{i=await o(e,{...t,signal:AbortSignal.timeout(n)})}catch(p){let l=p.name==="TimeoutError"?`no response within ${n}ms`:p.message;throw new J(`Could not reach ${e} \u2014 ${l}.`,{cause:p,url:e})}let d=r(),u=await i.text(),c;try{c=JSON.parse(u)}catch{c=null}return{request:{method:t.method,url:e,headers:t.headers,body:t.body},ok:i.ok,status:i.status,statusText:i.statusText,headers:Object.fromEntries(i.headers),body:c,rawBody:u,serverDate:i.headers.get("date"),localMidpointMs:Math.round((a+d)/2),elapsedMs:Math.round(performance.now()-s)}}function Ne({tokenUrl:e,assertion:t,timeoutMs:n,fetchImpl:o=fetch,now:r=Date.now}){let s=new URLSearchParams({grant_type:kt,assertion:t});return ie(e,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded",accept:"application/json"},body:s.toString()},{timeoutMs:n,fetchImpl:o,now:r})}function De({probeUrl:e,token:t,payload:n,timeoutMs:o,fetchImpl:r=fetch,now:s=Date.now}){return ie(e,{method:"POST",headers:{authorization:`Bearer ${t}`,"content-type":"application/json",accept:"application/json"},body:JSON.stringify(n)},{timeoutMs:o,fetchImpl:r,now:s})}function Oe({userInfoUrl:e,accessToken:t,timeoutMs:n,fetchImpl:o=fetch,now:r=Date.now}){return ie(e,{method:"GET",headers:{authorization:`Bearer ${t}`,accept:"application/json"}},{timeoutMs:n,fetchImpl:o,now:r})}import{randomUUID as Fe}from"node:crypto";var bt=/^00D[A-Za-z0-9]{12}([A-Za-z0-9]{3})?$/,ae=".my.salesforce.com",ce=".my.salesforce-scrt.com",_t="/telephony/v1/voiceCalls",Et=/\.(com|net|org|edu|gov|mil|io|uk)$/i,$t="sf-jwt-probe",Je=()=>Fe();function vt(e){let t=e.trim().replace(/^[a-z]+:\/\//i,"").replace(/\/.*$/,"");if(!t)throw new h("--my-domain is empty.");if(t.endsWith(ce))return`https://${t}`;if(t.endsWith(ae))return`https://${t.slice(0,-ae.length)}${ce}`;if(Et.test(t))throw new h(`--my-domain must be a My Domain host, got "${e}".`,{hint:`Pass the {MyDomain}${ae} host, or just the {MyDomain} part alone. Lightning, Experience Cloud and login hosts have no telephony equivalent to derive.`});return`https://${t}${ce}`}var Ce=e=>`${vt(e)}${_t}`;function Re({callCenterApiName:e,now:t=Date.now(),jti:n=null}){return{callCenterApiName:e,callSubtype:$t,from:"+18669483147",initiationMethod:"Inbound",participants:[{participantKey:"4081456688",type:"END_USER"}],startTime:new Date(t).toISOString().replace(/\.\d{3}Z$/,"Z"),to:"+14152988103",vendorCallKey:`sf-jwt:${n??Fe()}`}}function Ue({status:e,body:t}){return e===400?{outcome:"accepted",cause:"Salesforce authenticated the token, then rejected the deliberately invalid probe payload. No record was created."}:e===401?{outcome:"rejected",cause:"Salesforce refused the token.",remediation:["Confirm the certificate on the Call Center record matches the signing key in use.","Confirm --iss is the org ID of the org holding that Call Center, and --sub its API Name.","Check this machine\u2019s clock: iat in the future or exp in the past both read as a bad token."]}:e>=200&&e<300?{outcome:"accepted",created:t?.voiceCallId??null,cause:"Salesforce authenticated the token \u2014 but it accepted the probe payload and created a VoiceCall record, which it should have rejected."}:e===403?{outcome:"inconclusive",cause:"Salesforce answered 403. The token may have authenticated and been denied on permissions; the probe cannot tell those apart."}:e===404?{outcome:"inconclusive",cause:"Salesforce answered 404, which points at --my-domain or at Salesforce Voice not being provisioned on this org, rather than at the token."}:{outcome:"inconclusive",cause:`Salesforce answered ${e}, which says nothing definite about the token.`}}function We({iss:e,sub:t,expiresInSeconds:n,expiresAt:o,jti:r,now:s=Date.now()}){let a=Math.floor(s/1e3),i={iat:a,iss:e,sub:t,exp:o??a+n};return r?{...i,jti:r}:i}function Me({iss:e,sub:t}){if(!e)throw new h("Missing --iss (the Salesforce org ID).",{hint:"An org ID starts with 00D and is 15 or 18 characters."});if(!bt.test(e))throw new h(`--iss must be a Salesforce org ID, got "${e}".`,{hint:"An org ID starts with 00D and is 15 or 18 characters. A Consumer Key is not one \u2014 that belongs to jwt-bearer-flow."});if(!t)throw new h("Missing --sub (the Salesforce CallCenter API Name).",{hint:"This API documents sub as required, and nothing here contacts Salesforce to reject it for you."});if(t.includes("@"))throw new h(`--sub must be a CallCenter API Name, got "${t}".`,{hint:'An API name cannot contain "@". A Salesforce username belongs to jwt-bearer-flow.'})}var I="1.2.0";var Be="https://login.salesforce.com",He="https://test.salesforce.com",Ve=86400,V=1e4,It=30,qe=3600,_="jwt-bearer-flow",k="scv-auth-token",b="encode",v="decode",Le=[_,k,b,v],Pt={iss:{type:"string"},sub:{type:"string"},aud:{type:"string"},exp:{type:"string"},"exp-at":{type:"string"},jti:{type:"boolean"},"my-domain":{type:"string"},"no-probe":{type:"boolean"},key:{type:"string"},header:{type:"string"},payload:{type:"string"},jwt:{type:"string"},verify:{type:"string"},"token-url":{type:"string"},timeout:{type:"string"},sandbox:{type:"boolean"},"skip-userinfo":{type:"boolean"},json:{type:"boolean"},verbose:{type:"boolean",short:"v"},help:{type:"boolean",short:"h"},version:{type:"boolean"}},jt=["help","version","verbose","json"],Ke={[_]:["iss","sub","aud","sandbox","exp","exp-at","key","token-url","timeout","skip-userinfo"],[k]:["iss","sub","exp","exp-at","jti","my-domain","no-probe","key","timeout"],[b]:["header","payload","key","exp","exp-at"],[v]:["jwt","verify"]},Nt=`sf-jwt ${I} \u2014 mint, inspect and validate JSON Web Tokens.
6
+ `)}var Tt=e=>Math.min(100,Math.max(56,e??76));function O({verbose:e=!1,stream:t=process.stderr,env:n=process.env,showSecrets:o,colors:r}={}){let s=o??St(t,n),a=ie(r??se(t,n)),i=Tt(t.columns),d=l=>t.write(`${a.dim(l)}
7
+ `),p=l=>{e&&d(l)},c=l=>{if(!e)return;d("");let m=`\u2500\u2500 ${l} `;d(m+"\u2500".repeat(Math.max(3,i-m.length)))},u=l=>{if(!(!e||l.length===0)){d("");for(let m of l)d(` ${m}`)}};return{verbose:e,printSecrets:s,style:a,section:c,detail(l,m){p(` ${`${l}:`.padEnd(14)}${m}`)},secret(l,m){p(` ${`${l}:`.padEnd(14)}${B(m,s)}`)},warn(l){t.write(`${a.yellow(` warning: ${l}`)}
8
+ `)},httpRequest({method:l,url:m,headers:y,body:g},w="HTTP"){if(e){c(`${w} request`),d(` ${l} ${m}`);for(let S of Ae(y,s))d(` ${S}`);if(g){let S=String(y?.["content-type"]??"").includes("json");u(S?Ie(g,s):wt(g,s))}}},httpResponse({status:l,statusText:m,headers:y,rawBody:g,elapsedMs:w},S="HTTP"){if(e){c(`${S} response`),d(` HTTP ${l}${m?` ${m}`:""} \u2014 ${w}ms`);for(let f of Ae(y,s))d(` ${f}`);u(Ie(g,s))}}}}var kt="urn:ietf:params:oauth:grant-type:jwt-bearer";function Pe(e){return`${e.replace(/\/+$/,"")}/services/oauth2/token`}function je(e){return`${e.replace(/\/+$/,"")}/services/oauth2/userinfo`}function De(e,t=Date.now()){if(!e)return null;let n=Date.parse(e);return Number.isNaN(n)?null:Math.round((t-n)/1e3)}async function ae(e,t,{timeoutMs:n,fetchImpl:o,now:r}){let s=performance.now(),a=r(),i;try{i=await o(e,{...t,signal:AbortSignal.timeout(n)})}catch(u){let l=u.name==="TimeoutError"?`no response within ${n}ms`:u.message;throw new F(`Could not reach ${e} \u2014 ${l}.`,{cause:u,url:e})}let d=r(),p=await i.text(),c;try{c=JSON.parse(p)}catch{c=null}return{request:{method:t.method,url:e,headers:t.headers,body:t.body},ok:i.ok,status:i.status,statusText:i.statusText,headers:Object.fromEntries(i.headers),body:c,rawBody:p,serverDate:i.headers.get("date"),localMidpointMs:Math.round((a+d)/2),elapsedMs:Math.round(performance.now()-s)}}function Ne({tokenUrl:e,assertion:t,timeoutMs:n,fetchImpl:o=fetch,now:r=Date.now}){let s=new URLSearchParams({grant_type:kt,assertion:t});return ae(e,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded",accept:"application/json"},body:s.toString()},{timeoutMs:n,fetchImpl:o,now:r})}function Oe({probeUrl:e,token:t,payload:n,timeoutMs:o,fetchImpl:r=fetch,now:s=Date.now}){return ae(e,{method:"POST",headers:{authorization:`Bearer ${t}`,"content-type":"application/json",accept:"application/json"},body:JSON.stringify(n)},{timeoutMs:o,fetchImpl:r,now:s})}function Je({userInfoUrl:e,accessToken:t,timeoutMs:n,fetchImpl:o=fetch,now:r=Date.now}){return ae(e,{method:"GET",headers:{authorization:`Bearer ${t}`,accept:"application/json"}},{timeoutMs:n,fetchImpl:o,now:r})}import{randomUUID as bt}from"node:crypto";var _t=/^00D[A-Za-z0-9]{12}([A-Za-z0-9]{3})?$/,ce=".my.salesforce.com",de=".my.salesforce-scrt.com",Et="/telephony/v1/voiceCalls",$t=/\.(com|net|org|edu|gov|mil|io|uk)$/i,vt="sf-jwt-probe";function xt(e){let t=e.trim().replace(/^[a-z]+:\/\//i,"").replace(/\/.*$/,"");if(!t)throw new h("--my-domain is empty.");if(t.endsWith(de))return`https://${t}`;if(t.endsWith(ce))return`https://${t.slice(0,-ce.length)}${de}`;if($t.test(t))throw new h(`--my-domain must be a My Domain host, got "${e}".`,{hint:`Pass the {MyDomain}${ce} host, or just the {MyDomain} part alone. Lightning, Experience Cloud and login hosts have no telephony equivalent to derive.`});return`https://${t}${de}`}var Fe=e=>`${xt(e)}${Et}`;function Ce({callCenterApiName:e,now:t=Date.now(),jti:n=null}){return{callCenterApiName:e,callSubtype:vt,from:"+18669483147",initiationMethod:"Inbound",participants:[{participantKey:"4081456688",type:"END_USER"}],startTime:new Date(t).toISOString().replace(/\.\d{3}Z$/,"Z"),to:"+14152988103",vendorCallKey:`sf-jwt:${n??bt()}`}}function Re({status:e,body:t}){return e===400?{outcome:"accepted",cause:"Salesforce authenticated the token, then rejected the deliberately invalid probe payload. No record was created."}:e===401?{outcome:"rejected",cause:"Salesforce refused the token.",remediation:["Confirm the certificate on the Call Center record matches the signing key in use.","Confirm --iss is the org ID of the org holding that Call Center, and --sub its API Name.","Check this machine\u2019s clock: iat in the future or exp in the past both read as a bad token."]}:e>=200&&e<300?{outcome:"accepted",created:t?.voiceCallId??null,cause:"Salesforce authenticated the token \u2014 but it accepted the probe payload and created a VoiceCall record, which it should have rejected."}:e===403?{outcome:"inconclusive",cause:"Salesforce answered 403. The token may have authenticated and been denied on permissions; the probe cannot tell those apart."}:e===404?{outcome:"inconclusive",cause:"Salesforce answered 404, which points at --my-domain or at Salesforce Voice not being provisioned on this org, rather than at the token."}:{outcome:"inconclusive",cause:`Salesforce answered ${e}, which says nothing definite about the token.`}}function Ue({iss:e,sub:t,expiresInSeconds:n,expiresAt:o,jti:r,now:s=Date.now()}){let a=Math.floor(s/1e3),i={iat:a,iss:e,sub:t,exp:o??a+n};return r?{...i,jti:r}:i}function We({iss:e,sub:t}){if(!e)throw new h("Missing --iss (the Salesforce org ID).",{hint:"An org ID starts with 00D and is 15 or 18 characters."});if(!_t.test(e))throw new h(`--iss must be a Salesforce org ID, got "${e}".`,{hint:"An org ID starts with 00D and is 15 or 18 characters. A Consumer Key is not one \u2014 that belongs to jwt-bearer-flow."});if(!t)throw new h("Missing --sub (the Salesforce CallCenter API Name).",{hint:"This API documents sub as required, and nothing here contacts Salesforce to reject it for you."});if(t.includes("@"))throw new h(`--sub must be a CallCenter API Name, got "${t}".`,{hint:'An API name cannot contain "@". A Salesforce username belongs to jwt-bearer-flow.'})}var I="1.3.0";var Ye="https://login.salesforce.com",Be="https://test.salesforce.com",He=86400,V=1e4,Pt=30,Ve=3600,_="jwt-bearer-flow",k="scv-auth-token",b="encode",v="decode",Me=[_,k,b,v],jt={iss:{type:"string"},sub:{type:"string"},aud:{type:"string"},iat:{type:"boolean"},exp:{type:"string"},"exp-at":{type:"string"},jti:{type:"boolean"},"my-domain":{type:"string"},"no-probe":{type:"boolean"},key:{type:"string"},header:{type:"string"},payload:{type:"string"},jwt:{type:"string"},verify:{type:"string"},"token-url":{type:"string"},timeout:{type:"string"},sandbox:{type:"boolean"},"skip-userinfo":{type:"boolean"},json:{type:"boolean"},verbose:{type:"boolean",short:"v"},help:{type:"boolean",short:"h"},version:{type:"boolean"}},Dt=["help","version","verbose","json"],Le={[_]:["iss","sub","aud","sandbox","exp","exp-at","key","token-url","timeout","skip-userinfo"],[k]:["iss","sub","exp","exp-at","jti","my-domain","no-probe","key","timeout"],[b]:["header","payload","key","iat","exp","exp-at","jti"],[v]:["jwt","verify"]},Nt=`sf-jwt ${I} \u2014 mint, inspect and validate JSON Web Tokens.
9
9
 
10
10
  Signs and decodes JWTs of any shape, and specialises in the two Salesforce flows
11
11
  that consume them, where the org's configuration is what's under test.
@@ -37,9 +37,9 @@ Claims
37
37
  --sub <username> Salesforce user to act as; optional [env: SF_JWT_SUB]
38
38
  here, but orgs reject an assertion
39
39
  that omits it
40
- --aud <url> Audience (default ${Be}) [env: SF_JWT_AUD]
41
- --sandbox Shorthand for --aud ${He} [env: SF_JWT_SANDBOX]
42
- --exp <seconds> Seconds from now (default ${Ve}) [env: SF_JWT_EXP]
40
+ --aud <url> Audience (default ${Ye}) [env: SF_JWT_AUD]
41
+ --sandbox Shorthand for --aud ${Be} [env: SF_JWT_SANDBOX]
42
+ --exp <seconds> Seconds from now (default ${He}) [env: SF_JWT_EXP]
43
43
  --exp-at <epoch> Absolute expiry; overrides --exp [env: SF_JWT_EXP_AT]
44
44
 
45
45
  Request
@@ -64,7 +64,7 @@ Exit codes
64
64
  1 Salesforce rejected the assertion (a setup problem)
65
65
  2 Usage error (bad arguments, unreadable key)
66
66
  3 Transport failure (DNS, TLS, timeout)
67
- `,Dt=`sf-jwt ${I} \u2014 mint a Salesforce Telephony Integration REST API token.
67
+ `,Ot=`sf-jwt ${I} \u2014 mint a Salesforce Telephony Integration REST API token.
68
68
 
69
69
  Signs an SCV Auth Token, then presents it to the telephony API to confirm Salesforce
70
70
  accepts it. The token is itself the bearer credential \u2014 nothing is exchanged for it.
@@ -90,7 +90,7 @@ Required
90
90
  or login one. Not required if you pass --no-probe
91
91
 
92
92
  Claims
93
- --exp <seconds> Seconds from now (default ${qe}) [env: SF_JWT_EXP]
93
+ --exp <seconds> Seconds from now (default ${Ve}) [env: SF_JWT_EXP]
94
94
  --exp-at <epoch> Absolute expiry; overrides --exp [env: SF_JWT_EXP_AT]
95
95
  --jti Add a unique JWT ID [env: SF_JWT_JTI]
96
96
  Salesforce treats a JTI as replay-protected, and the probe
@@ -120,11 +120,11 @@ Exit codes
120
120
  0 Token minted, and either accepted or not conclusively judged
121
121
  1 Salesforce refused the token
122
122
  2 Usage error (bad arguments, unreadable key)
123
- `,Ot=`sf-jwt ${I} \u2014 sign an arbitrary JWT.
123
+ `,Jt=`sf-jwt ${I} \u2014 sign an arbitrary JWT.
124
124
 
125
125
  Takes the header and payload you give it and signs them. Nothing about the claims is
126
- checked or assumed: no Salesforce meaning is read into iss or sub, and no claim is added
127
- except exp, and then only if you ask.
126
+ checked or assumed: no Salesforce meaning is read into iss or sub, and nothing is added
127
+ to your payload except iat, exp and jti, and then only if you ask by name.
128
128
 
129
129
  Usage
130
130
  sf-jwt ${b} --payload < json | path | - > [--header < json | path | - >] [--key < path | - >]
@@ -137,9 +137,12 @@ Claims
137
137
  --header <json> The header to sign default {"alg":\u2026,"typ":"JWT"}
138
138
  alg is filled in from the key when you leave it out, and an alg
139
139
  the key cannot perform is refused rather than attempted
140
+ --iat Set iat to the current epoch second; overwrites any iat in
141
+ the payload
140
142
  --exp <seconds> Set exp to now plus this many seconds; overwrites any exp in
141
143
  the payload. Not read from the environment
142
144
  --exp-at <epoch> Set exp absolutely; overrides --exp
145
+ --jti Set jti to a fresh v4 UUID; overwrites any jti in the payload
143
146
 
144
147
  Signing
145
148
  --key <path> Private key PEM; "-" reads stdin [env: SF_JWT_KEY_FILE]
@@ -202,29 +205,29 @@ Exit codes
202
205
  0 Token decoded; if a key was given, it matched
203
206
  1 The key did not match, or the token carries no signature to match
204
207
  2 Usage error (not a JWT, unreadable material, an alg this tool cannot verify)
205
- `;function Jt(e){return!Object.entries(e).some(([t,n])=>t.startsWith("SF_JWT_")&&n)}function Ct(e){if(e.length===0)return _;let[t,...n]=e;if(!Le.includes(t))throw new h(`Unknown subcommand "${t}".`,{hint:`Expected one of: ${Le.join(", ")}.`});if(n.length>0)throw new h(`Unexpected argument "${n[0]}".`);return t}function Rt(e,t){let n=new Set([...jt,...Ke[e]]);for(let o of Object.keys(t)){if(n.has(o))continue;let r=Object.entries(Ke).filter(([,s])=>s.includes(o)).map(([s])=>s);throw new h(`--${o} does not apply to ${e}.`,{hint:r.length>0?`It belongs to ${r.join(" and ")}.`:void 0})}}function $(e,t){if(e===void 0)return;let n=Number(e);if(!Number.isInteger(n))throw new h(`--${t} must be an integer, got "${e}".`);return n}function H(e){let t=Math.abs(e),n=o=>String(Number(o.toFixed(1)));return t<60?`${e}s`:t<3600?`${n(e/60)}m`:t<86400?`${n(e/3600)}h`:`${n(e/86400)}d`}function Ut(e,t){let n=e.sandbox||t.SF_JWT_SANDBOX==="true",o=e.aud??t.SF_JWT_AUD??(n?He:Be),r=e.iss??t.SF_JWT_ISS,s=e.sub??t.SF_JWT_SUB;if(!r)throw new h("Missing --iss (the External Client App consumer key).");return{iss:r,sub:s,aud:o,expiresInSeconds:$(e.exp??t.SF_JWT_EXP,"exp")??Ve,expiresAt:$(e["exp-at"]??t.SF_JWT_EXP_AT,"exp-at"),keyPath:e.key,keyFileEnv:t.SF_JWT_KEY_FILE,inlineKeyEnv:t.SF_JWT_PRIVATE_KEY,passphraseEnv:t.SF_JWT_KEY_PASSPHRASE,tokenUrl:e["token-url"]??t.SF_JWT_TOKEN_URL??Ie(o),timeoutMs:$(e.timeout??t.SF_JWT_TIMEOUT,"timeout")??V,skipUserInfo:e["skip-userinfo"]??!1,json:e.json??!1,verbose:e.verbose??!1}}function Wt(e,t){return{probe:!(e["no-probe"]||t.SF_JWT_NO_PROBE==="true"),myDomain:e["my-domain"]??t.SF_JWT_MY_DOMAIN,iss:e.iss??t.SF_JWT_ISS,sub:e.sub??t.SF_JWT_SUB,expiresInSeconds:$(e.exp??t.SF_JWT_EXP,"exp")??qe,expiresAt:$(e["exp-at"]??t.SF_JWT_EXP_AT,"exp-at"),jti:e.jti||t.SF_JWT_JTI==="true",timeoutMs:$(e.timeout??t.SF_JWT_TIMEOUT,"timeout")??V,keyPath:e.key,keyFileEnv:t.SF_JWT_KEY_FILE,inlineKeyEnv:t.SF_JWT_PRIVATE_KEY,passphraseEnv:t.SF_JWT_KEY_PASSPHRASE,json:e.json??!1,verbose:e.verbose??!1}}function Mt(e,t){return{header:e.header,payload:e.payload,expiresInSeconds:$(e.exp,"exp"),expiresAt:$(e["exp-at"],"exp-at"),signing:!!(e.key||t.SF_JWT_KEY_FILE||t.SF_JWT_PRIVATE_KEY),keyPath:e.key,keyFileEnv:t.SF_JWT_KEY_FILE,inlineKeyEnv:t.SF_JWT_PRIVATE_KEY,passphraseEnv:t.SF_JWT_KEY_PASSPHRASE,json:e.json??!1,verbose:e.verbose??!1}}function Lt(e,t){return{jwt:e.jwt,verifyPath:e.verify,passphraseEnv:t.SF_JWT_KEY_PASSPHRASE,json:e.json??!1,verbose:e.verbose??!1}}function Kt(e,t){if(!e)return{alg:t?Z(t):A,typ:"JWT"};let{alg:n,...o}=e;if(!t)return{alg:A,...o};let r=n??Z(t);return R(t,r),{alg:r,...o}}function Yt(e,{expiresInSeconds:t,expiresAt:n,now:o}){return n!==void 0?{...e,exp:n}:t!==void 0?{...e,exp:Math.floor(o/1e3)+t}:{...e}}function Bt(e,{write:t,style:n}){let{outcome:o,http:r,salesforce:s,diagnosis:a,identity:i,userInfo:d,clockSkew:u}=e,c=p=>t(n.dim(p));if(o==="accepted")t(`${n.green("PASS")} ${n.dim(`Salesforce issued an access token (HTTP ${r.status}, ${r.elapsedMs}ms).`)}`),i&&(c(` user: ${i.username}`),c(` user id: ${i.userId}`),c(` org id: ${i.organizationId}`)),e.instanceUrl&&c(` instance: ${e.instanceUrl}`),d&&(t(""),t(n.yellow(` Identity unconfirmed: /userinfo returned HTTP ${d.status}.`)),c(` ${d.rawBody}`),c(" The token itself was issued, so this does not affect the result above."));else if(t(`${n.red("FAIL")} ${n.dim(`Salesforce rejected the assertion (HTTP ${r.status}, ${r.elapsedMs}ms).`)}`),c(` ${s.error??"unknown error"} \u2014 ${s.errorDescription??"no description"}`),a){t(""),c("Likely cause"),c(` ${a.cause}`),t(""),c("What to do");for(let p of a.remediation)c(` - ${p}`)}else t(""),c("No diagnosis matched this error. Raw response:"),c(` ${e.rawBody}`);u!==null&&Math.abs(u)>=It&&(t(""),t(n.yellow(`Clock drift: this machine is ${Math.abs(u)}s ${u>0?"ahead of":"behind"} Salesforce. Drift alone can invalidate an otherwise correct assertion.`)))}function Ht({probe:e,myDomain:t}){if(e&&!t)throw new h("Missing --my-domain, which is where the token gets verified.",{hint:"Pass the org\u2019s My Domain host, or --no-probe to mint without verifying."})}function Vt(e,{warn:t,write:n,style:o}){if(e.error)return t(`Token unverified: ${e.error}`),0;let{outcome:r,cause:s,created:a,remediation:i}=e.diagnosis,{label:d,paint:u}={accepted:{label:"PASS",paint:o.green},rejected:{label:"FAIL",paint:o.red},inconclusive:{label:"????",paint:o.yellow}}[r];n(`${u(d)} ${o.dim(`probe ${e.probeUrl} \u2014 HTTP ${e.status} in ${e.elapsedMs}ms`)}`),n(o.dim(` ${s}`));for(let c of i??[])n(o.dim(` - ${c}`));if(a!=null){n("");for(let c of["The probe created a VoiceCall record. It was built so that Salesforce would",`reject it, and Salesforce did not. Delete ${a} and open an issue \u2014`,"this tool must not write to your org."])n(o.alarm(`!!!! ${c}`))}return r==="rejected"?1:0}async function qt(e,{env:t,stdout:n,stderr:o,stdin:r,fetchImpl:s,now:a}){let i=Wt(e,t),d=O({verbose:i.verbose,stream:o,env:t});Me(i),Ht(i);let u=i.probe?Ce(i.myDomain):null;d.section("Private key");let c=await Y({...i,stdin:r});d.detail("source",c.source),d.detail("type",`${D(c)}${c.encrypted?", passphrase-protected":""}`);let p=a(),l=We({...i,jti:i.jti?Je():void 0,now:p}),m=l.exp-l.iat;d.section("Claims");for(let[S,f]of Object.entries(l))d.detail(S,f);d.detail("exp at",`${new Date(l.exp*1e3).toISOString()} (in ${H(m)})`);let y=ee(l,c.keyObject),g=null;if(i.probe){let S=Re({callCenterApiName:i.sub,now:p,jti:l.jti});try{let f=await De({...i,probeUrl:u,token:y,payload:S,fetchImpl:s,now:a});d.httpRequest(f.request,"Probe"),d.httpResponse(f,"Probe"),g={probeUrl:u,status:f.status,elapsedMs:f.elapsedMs,vendorCallKey:S.vendorCallKey,rawBody:f.rawBody,diagnosis:Ue(f),error:null}}catch(f){g={probeUrl:u,error:f.message,diagnosis:null}}}let w=!!(n.isTTY&&o.isTTY);return(i.verbose||w)&&o.write(`
208
+ `;function Ct(e){return!Object.entries(e).some(([t,n])=>t.startsWith("SF_JWT_")&&n)}function Rt(e){if(e.length===0)return _;let[t,...n]=e;if(!Me.includes(t))throw new h(`Unknown subcommand "${t}".`,{hint:`Expected one of: ${Me.join(", ")}.`});if(n.length>0)throw new h(`Unexpected argument "${n[0]}".`);return t}function Ut(e,t){let n=new Set([...Dt,...Le[e]]);for(let o of Object.keys(t)){if(n.has(o))continue;let r=Object.entries(Le).filter(([,s])=>s.includes(o)).map(([s])=>s);throw new h(`--${o} does not apply to ${e}.`,{hint:r.length>0?`It belongs to ${r.join(" and ")}.`:void 0})}}function $(e,t){if(e===void 0)return;let n=Number(e);if(!Number.isInteger(n))throw new h(`--${t} must be an integer, got "${e}".`);return n}function H(e){let t=Math.abs(e),n=o=>String(Number(o.toFixed(1)));return t<60?`${e}s`:t<3600?`${n(e/60)}m`:t<86400?`${n(e/3600)}h`:`${n(e/86400)}d`}function Wt(e,t){let n=e.sandbox||t.SF_JWT_SANDBOX==="true",o=e.aud??t.SF_JWT_AUD??(n?Be:Ye),r=e.iss??t.SF_JWT_ISS,s=e.sub??t.SF_JWT_SUB;if(!r)throw new h("Missing --iss (the External Client App consumer key).");return{iss:r,sub:s,aud:o,expiresInSeconds:$(e.exp??t.SF_JWT_EXP,"exp")??He,expiresAt:$(e["exp-at"]??t.SF_JWT_EXP_AT,"exp-at"),keyPath:e.key,keyFileEnv:t.SF_JWT_KEY_FILE,inlineKeyEnv:t.SF_JWT_PRIVATE_KEY,passphraseEnv:t.SF_JWT_KEY_PASSPHRASE,tokenUrl:e["token-url"]??t.SF_JWT_TOKEN_URL??Pe(o),timeoutMs:$(e.timeout??t.SF_JWT_TIMEOUT,"timeout")??V,skipUserInfo:e["skip-userinfo"]??!1,json:e.json??!1,verbose:e.verbose??!1}}function Mt(e,t){return{probe:!(e["no-probe"]||t.SF_JWT_NO_PROBE==="true"),myDomain:e["my-domain"]??t.SF_JWT_MY_DOMAIN,iss:e.iss??t.SF_JWT_ISS,sub:e.sub??t.SF_JWT_SUB,expiresInSeconds:$(e.exp??t.SF_JWT_EXP,"exp")??Ve,expiresAt:$(e["exp-at"]??t.SF_JWT_EXP_AT,"exp-at"),jti:e.jti||t.SF_JWT_JTI==="true",timeoutMs:$(e.timeout??t.SF_JWT_TIMEOUT,"timeout")??V,keyPath:e.key,keyFileEnv:t.SF_JWT_KEY_FILE,inlineKeyEnv:t.SF_JWT_PRIVATE_KEY,passphraseEnv:t.SF_JWT_KEY_PASSPHRASE,json:e.json??!1,verbose:e.verbose??!1}}function Lt(e,t){return{header:e.header,payload:e.payload,stampIssuedAt:!!e.iat,stampJwtId:!!e.jti,expiresInSeconds:$(e.exp,"exp"),expiresAt:$(e["exp-at"],"exp-at"),signing:!!(e.key||t.SF_JWT_KEY_FILE||t.SF_JWT_PRIVATE_KEY),keyPath:e.key,keyFileEnv:t.SF_JWT_KEY_FILE,inlineKeyEnv:t.SF_JWT_PRIVATE_KEY,passphraseEnv:t.SF_JWT_KEY_PASSPHRASE,json:e.json??!1,verbose:e.verbose??!1}}function Kt(e,t){return{jwt:e.jwt,verifyPath:e.verify,passphraseEnv:t.SF_JWT_KEY_PASSPHRASE,json:e.json??!1,verbose:e.verbose??!1}}function Yt(e,t){if(!e)return{alg:t?Z(t):A,typ:"JWT"};let{alg:n,...o}=e;if(!t)return{alg:A,...o};let r=n??Z(t);return R(t,r),{alg:r,...o}}function Bt(e,{stampIssuedAt:t,stampJwtId:n,expiresInSeconds:o,expiresAt:r,now:s}){let a=Math.floor(s/1e3),i={...e};return t&&(i.iat=a),r!==void 0?i.exp=r:o!==void 0&&(i.exp=a+o),n&&(i.jti=ee()),i}function Ht(e,{write:t,style:n}){let{outcome:o,http:r,salesforce:s,diagnosis:a,identity:i,userInfo:d,clockSkew:p}=e,c=u=>t(n.dim(u));if(o==="accepted")t(`${n.green("PASS")} ${n.dim(`Salesforce issued an access token (HTTP ${r.status}, ${r.elapsedMs}ms).`)}`),i&&(c(` user: ${i.username}`),c(` user id: ${i.userId}`),c(` org id: ${i.organizationId}`)),e.instanceUrl&&c(` instance: ${e.instanceUrl}`),d&&(t(""),t(n.yellow(` Identity unconfirmed: /userinfo returned HTTP ${d.status}.`)),c(` ${d.rawBody}`),c(" The token itself was issued, so this does not affect the result above."));else if(t(`${n.red("FAIL")} ${n.dim(`Salesforce rejected the assertion (HTTP ${r.status}, ${r.elapsedMs}ms).`)}`),c(` ${s.error??"unknown error"} \u2014 ${s.errorDescription??"no description"}`),a){t(""),c("Likely cause"),c(` ${a.cause}`),t(""),c("What to do");for(let u of a.remediation)c(` - ${u}`)}else t(""),c("No diagnosis matched this error. Raw response:"),c(` ${e.rawBody}`);p!==null&&Math.abs(p)>=Pt&&(t(""),t(n.yellow(`Clock drift: this machine is ${Math.abs(p)}s ${p>0?"ahead of":"behind"} Salesforce. Drift alone can invalidate an otherwise correct assertion.`)))}function Vt({probe:e,myDomain:t}){if(e&&!t)throw new h("Missing --my-domain, which is where the token gets verified.",{hint:"Pass the org\u2019s My Domain host, or --no-probe to mint without verifying."})}function qt(e,{warn:t,write:n,style:o}){if(e.error)return t(`Token unverified: ${e.error}`),0;let{outcome:r,cause:s,created:a,remediation:i}=e.diagnosis,{label:d,paint:p}={accepted:{label:"PASS",paint:o.green},rejected:{label:"FAIL",paint:o.red},inconclusive:{label:"????",paint:o.yellow}}[r];n(`${p(d)} ${o.dim(`probe ${e.probeUrl} \u2014 HTTP ${e.status} in ${e.elapsedMs}ms`)}`),n(o.dim(` ${s}`));for(let c of i??[])n(o.dim(` - ${c}`));if(a!=null){n("");for(let c of["The probe created a VoiceCall record. It was built so that Salesforce would",`reject it, and Salesforce did not. Delete ${a} and open an issue \u2014`,"this tool must not write to your org."])n(o.alarm(`!!!! ${c}`))}return r==="rejected"?1:0}async function Xt(e,{env:t,stdout:n,stderr:o,stdin:r,fetchImpl:s,now:a}){let i=Mt(e,t),d=O({verbose:i.verbose,stream:o,env:t});We(i),Vt(i);let p=i.probe?Fe(i.myDomain):null;d.section("Private key");let c=await Y({...i,stdin:r});d.detail("source",c.source),d.detail("type",`${N(c)}${c.encrypted?", passphrase-protected":""}`);let u=a(),l=Ue({...i,jti:i.jti?ee():void 0,now:u}),m=l.exp-l.iat;d.section("Claims");for(let[S,f]of Object.entries(l))d.detail(S,f);d.detail("exp at",`${new Date(l.exp*1e3).toISOString()} (in ${H(m)})`);let y=te(l,c.keyObject),g=null;if(i.probe){let S=Ce({callCenterApiName:i.sub,now:u,jti:l.jti});try{let f=await Oe({...i,probeUrl:p,token:y,payload:S,fetchImpl:s,now:a});d.httpRequest(f.request,"Probe"),d.httpResponse(f,"Probe"),g={probeUrl:p,status:f.status,elapsedMs:f.elapsedMs,vendorCallKey:S.vendorCallKey,rawBody:f.rawBody,diagnosis:Re(f),error:null}}catch(f){g={probeUrl:p,error:f.message,diagnosis:null}}}let w=!!(n.isTTY&&o.isTTY);return(i.verbose||w)&&o.write(`
206
209
  `),n.write(i.json?`${JSON.stringify({token:y,claims:l,probe:g},null,2)}
207
210
  `:`${y}
208
211
  `),g?(w&&o.write(`
209
- `),Vt(g,{warn:S=>d.warn(S),write:S=>o.write(`${S}
210
- `),style:d.style})):0}var Ye=e=>typeof e=="object"&&e!==null?JSON.stringify(e):e;async function Xt(e,{env:t,stdout:n,stderr:o,stdin:r,now:s}){let a=Mt(e,t),i=O({verbose:a.verbose,stream:o,env:t});if(!a.payload)throw new h("Missing --payload (the claims to sign).",{hint:"Pass JSON directly, a path to a file holding it, or - to read stdin."});ne(e,["header","payload","key"]);let d=await L(a.payload,{flag:"payload",kind:"json",stdin:r}),u=te(d.text,"payload"),c=null;if(a.header!==void 0){let w=await L(a.header,{flag:"header",kind:"json",stdin:r});c=te(w.text,"header")}let p=null;a.signing&&(i.section("Private key"),p=await Y({...a,stdin:r,assertUsable:K}),i.detail("source",p.source),i.detail("type",`${D(p)}${p.encrypted?", passphrase-protected":""}`));let l=Kt(c,p?.keyObject??null),m=Yt(u,{...a,now:s()});i.section("Header");for(let[w,S]of Object.entries(l))i.detail(w,Ye(S));i.section("Claims");for(let[w,S]of Object.entries(m))i.detail(w,Ye(S));let y=p?U(l,m,p.keyObject):ye(l,m),g=!!(n.isTTY&&o.isTTY);return(a.verbose||g)&&o.write(`
212
+ `),qt(g,{warn:S=>d.warn(S),write:S=>o.write(`${S}
213
+ `),style:d.style})):0}var Ke=e=>typeof e=="object"&&e!==null?JSON.stringify(e):e;async function Gt(e,{env:t,stdout:n,stderr:o,stdin:r,now:s}){let a=Lt(e,t),i=O({verbose:a.verbose,stream:o,env:t});if(!a.payload)throw new h("Missing --payload (the claims to sign).",{hint:"Pass JSON directly, a path to a file holding it, or - to read stdin."});oe(e,["header","payload","key"]);let d=await L(a.payload,{flag:"payload",kind:"json",stdin:r}),p=ne(d.text,"payload"),c=null;if(a.header!==void 0){let w=await L(a.header,{flag:"header",kind:"json",stdin:r});c=ne(w.text,"header")}let u=null;a.signing&&(i.section("Private key"),u=await Y({...a,stdin:r,assertUsable:K}),i.detail("source",u.source),i.detail("type",`${N(u)}${u.encrypted?", passphrase-protected":""}`));let l=Yt(c,u?.keyObject??null),m=Bt(p,{...a,now:s()});i.section("Header");for(let[w,S]of Object.entries(l))i.detail(w,Ke(S));i.section("Claims");for(let[w,S]of Object.entries(m))i.detail(w,Ke(S));let y=u?U(l,m,u.keyObject):ge(l,m),g=!!(n.isTTY&&o.isTTY);return(a.verbose||g)&&o.write(`
211
214
  `),n.write(a.json?`${JSON.stringify({token:y,header:l,claims:m},null,2)}
212
215
  `:`${y}
213
- `),p||(g&&o.write(`
214
- `),i.warn("This token is not signed: no --key, and no SF_JWT_KEY_FILE or SF_JWT_PRIVATE_KEY set."),c?.alg&&c.alg!==A&&i.warn(`Its header asked for ${c.alg}; with nothing to sign with, alg is "${A}".`)),0}function Gt({keyMatch:e,times:t},{write:n,style:o}){let r=" ".repeat(9);if(e){let{label:a,paint:i}=e.outcome==="valid"?{label:"VALID",paint:o.green}:{label:"INVALID",paint:o.red};n(`${i(a)}${" ".repeat(r.length-a.length)}${o.dim(`key match \u2014 ${e.reason}`)}`)}else n(o.dim(`${r}Signature unchecked. Pass --verify <certificate|key> to establish a key match.`));let s=["iat","nbf","exp"].filter(a=>t[a]);if(s.length===0){n(o.dim(`${r}No iat, nbf or exp to report.`));return}for(let a of s){let{at:i,deltaSeconds:d,value:u}=t[a],c=d>=0?`in ${H(d)}`:`${H(-d)} ago`;n(o.dim(`${r}${`${a}:`.padEnd(6)}${i??u} (${c})`))}t.expired&&n(o.yellow(`${r}Expired. That is a fact about the clock, not about the signature.`)),t.notYetValid&&n(o.yellow(`${r}Not yet valid: nbf is in the future.`));for(let a of s)t[a].looksLikeMilliseconds&&n(o.yellow(`${r}${a} looks like milliseconds rather than seconds.`))}async function zt(e,{env:t,stdout:n,stderr:o,stdin:r,now:s}){let a=Lt(e,t),i=O({verbose:a.verbose,stream:o,env:t});if(!a.jwt)throw new h("Missing --jwt (the token to read).",{hint:"Pass the token itself, a path to a file holding it, or - to read stdin."});ne(e,["jwt","verify"]);let d=await L(a.jwt,{flag:"jwt",kind:"token",stdin:r}),u=Q(d.text),c=ge(u.claims,s());i.section("Token"),i.detail("source",d.source),i.detail("alg",u.header.alg??"(absent)"),i.detail("signature",u.signature===""?"(empty)":`${u.signature.length} chars`);let p=null;if(a.verifyPath!==void 0){let y=await ve({path:a.verifyPath,passphraseEnv:a.passphraseEnv,stdin:r});if(i.section("Verification material"),i.detail("source",y.source),i.detail("kind",`${y.kind}, ${D(K(y.keyObject))}`),!u.signed)p={outcome:"invalid",reason:`the token carries no signature to match (alg "${u.header.alg??"absent"}")`};else{if(!u.header.alg)throw new h("The token's header names no alg, so there is no way to verify it.");let g=Se(u,y.keyObject);p={outcome:g?"valid":"invalid",alg:u.header.alg,source:y.source,kind:y.kind,reason:`signature ${g?"verifies":"does not verify"} against ${y.source} (${y.kind}, ${u.header.alg})`}}}let l=!!(n.isTTY&&o.isTTY);(a.verbose||l)&&o.write(`
215
- `);let m=a.json?{header:u.header,claims:u.claims,signed:u.signed,keyMatch:p,times:c}:{header:u.header,claims:u.claims};return n.write(`${JSON.stringify(m,null,2)}
216
+ `),u||(g&&o.write(`
217
+ `),i.warn("This token is not signed: no --key, and no SF_JWT_KEY_FILE or SF_JWT_PRIVATE_KEY set."),c?.alg&&c.alg!==A&&i.warn(`Its header asked for ${c.alg}; with nothing to sign with, alg is "${A}".`)),0}function zt({keyMatch:e,times:t},{write:n,style:o}){let r=" ".repeat(9);if(e){let{label:a,paint:i}=e.outcome==="valid"?{label:"VALID",paint:o.green}:{label:"INVALID",paint:o.red};n(`${i(a)}${" ".repeat(r.length-a.length)}${o.dim(`key match \u2014 ${e.reason}`)}`)}else n(o.dim(`${r}Signature unchecked. Pass --verify <certificate|key> to establish a key match.`));let s=["iat","nbf","exp"].filter(a=>t[a]);if(s.length===0){n(o.dim(`${r}No iat, nbf or exp to report.`));return}for(let a of s){let{at:i,deltaSeconds:d,value:p}=t[a],c=d>=0?`in ${H(d)}`:`${H(-d)} ago`;n(o.dim(`${r}${`${a}:`.padEnd(6)}${i??p} (${c})`))}t.expired&&n(o.yellow(`${r}Expired. That is a fact about the clock, not about the signature.`)),t.notYetValid&&n(o.yellow(`${r}Not yet valid: nbf is in the future.`));for(let a of s)t[a].looksLikeMilliseconds&&n(o.yellow(`${r}${a} looks like milliseconds rather than seconds.`))}async function Zt(e,{env:t,stdout:n,stderr:o,stdin:r,now:s}){let a=Kt(e,t),i=O({verbose:a.verbose,stream:o,env:t});if(!a.jwt)throw new h("Missing --jwt (the token to read).",{hint:"Pass the token itself, a path to a file holding it, or - to read stdin."});oe(e,["jwt","verify"]);let d=await L(a.jwt,{flag:"jwt",kind:"token",stdin:r}),p=Q(d.text),c=Se(p.claims,s());i.section("Token"),i.detail("source",d.source),i.detail("alg",p.header.alg??"(absent)"),i.detail("signature",p.signature===""?"(empty)":`${p.signature.length} chars`);let u=null;if(a.verifyPath!==void 0){let y=await xe({path:a.verifyPath,passphraseEnv:a.passphraseEnv,stdin:r});if(i.section("Verification material"),i.detail("source",y.source),i.detail("kind",`${y.kind}, ${N(K(y.keyObject))}`),!p.signed)u={outcome:"invalid",reason:`the token carries no signature to match (alg "${p.header.alg??"absent"}")`};else{if(!p.header.alg)throw new h("The token's header names no alg, so there is no way to verify it.");let g=we(p,y.keyObject);u={outcome:g?"valid":"invalid",alg:p.header.alg,source:y.source,kind:y.kind,reason:`signature ${g?"verifies":"does not verify"} against ${y.source} (${y.kind}, ${p.header.alg})`}}}let l=!!(n.isTTY&&o.isTTY);(a.verbose||l)&&o.write(`
218
+ `);let m=a.json?{header:p.header,claims:p.claims,signed:p.signed,keyMatch:u,times:c}:{header:p.header,claims:p.claims};return n.write(`${JSON.stringify(m,null,2)}
216
219
  `),l&&o.write(`
217
- `),Gt({keyMatch:p,times:c},{write:y=>o.write(`${y}
218
- `),style:i.style}),p?.outcome==="invalid"?1:0}var Zt={[_]:Nt,[k]:Dt,[b]:Ot,[v]:Ft};async function Xe(e=process.argv.slice(2),{env:t=process.env,stdout:n=process.stdout,stderr:o=process.stderr,stdin:r=process.stdin,fetchImpl:s=fetch,now:a=Date.now}={}){let i=se(re(o,t)),d,u;try{({values:d,positionals:u}=At({args:e,options:Pt,allowPositionals:!0}))}catch(c){return o.write(`${i.red(c.message)}
220
+ `),zt({keyMatch:u,times:c},{write:y=>o.write(`${y}
221
+ `),style:i.style}),u?.outcome==="invalid"?1:0}var Qt={[_]:Nt,[k]:Ot,[b]:Jt,[v]:Ft};async function qe(e=process.argv.slice(2),{env:t=process.env,stdout:n=process.stdout,stderr:o=process.stderr,stdin:r=process.stdin,fetchImpl:s=fetch,now:a=Date.now}={}){let i=ie(se(o,t)),d,p;try{({values:d,positionals:p}=It({args:e,options:jt,allowPositionals:!0}))}catch(c){return o.write(`${i.red(c.message)}
219
222
  ${i.dim("Run with --help for usage.")}
220
- `),2}try{let c=Ct(u);if(d.help||Object.keys(d).length===0&&Jt(t))return n.write(Zt[c]),0;if(d.version)return n.write(`${I}
223
+ `),2}try{let c=Rt(p);if(d.help||Object.keys(d).length===0&&Ct(t))return n.write(Qt[c]),0;if(d.version)return n.write(`${I}
221
224
  `),0;if(o.write(`${i.dim(`sf-jwt ${I}`)}
222
- `),Rt(c,d),c===k)return await qt(d,{env:t,stdout:n,stderr:o,stdin:r,fetchImpl:s,now:a});if(c===b)return await Xt(d,{env:t,stdout:n,stderr:o,stdin:r,now:a});if(c===v)return await zt(d,{env:t,stdout:n,stderr:o,stdin:r,now:a});let p=Ut(d,t),l=O({verbose:p.verbose,stream:o,env:t});l.section("Private key");let m=await Y({...p,stdin:r});l.detail("source",m.source),l.detail("type",`${D(m)}${m.encrypted?", passphrase-protected":""}`);let y=a(),g=we({...p,now:y}),w=g.exp-Math.floor(y/1e3);l.section("Claims");for(let[j,T]of Object.entries(g))l.detail(j,T);l.detail("exp at",`${new Date(g.exp*1e3).toISOString()} (in ${H(w)})`);let S=ee(g,m.keyObject);l.secret("assertion",S);let f=await Ne({...p,assertion:S,fetchImpl:s,now:a});l.httpRequest(f.request,"Token"),l.httpResponse(f,"Token");let F=je(f.serverDate,f.localMidpointMs),x={outcome:f.ok?"accepted":"rejected",tokenUrl:p.tokenUrl,claims:g,lifetimeSeconds:w,http:{status:f.status,elapsedMs:f.elapsedMs},clockSkew:F,clockSkewUncertainty:F===null?null:Math.ceil(f.elapsedMs/2e3)+1,rawBody:f.rawBody,salesforce:{error:f.body?.error,errorDescription:f.body?.error_description},diagnosis:null,identity:null,userInfo:null,instanceUrl:f.body?.instance_url??null};if(f.ok){if(!p.skipUserInfo&&f.body?.access_token){let j=f.body.instance_url??new URL(p.tokenUrl).origin,T=await Oe({userInfoUrl:Pe(j),accessToken:f.body.access_token,timeoutMs:p.timeoutMs,fetchImpl:s,now:a});l.httpRequest(T.request,"Identity"),l.httpResponse(T,"Identity"),T.ok?x.identity={username:T.body?.preferred_username??T.body?.email??"unknown",userId:T.body?.user_id??"unknown",organizationId:T.body?.organization_id??"unknown"}:(x.userInfo={status:T.status,error:T.body?.error??null,errorDescription:T.body?.error_description??null,rawBody:T.rawBody},l.warn(`The token was issued; it is /userinfo that returned ${T.status}.`))}}else x.diagnosis=Te(x.salesforce,g);F!==null&&(l.section("Clock"),l.detail("drift",`${F}s versus Salesforce (\xB1${x.clockSkewUncertainty}s on a ${f.elapsedMs}ms round trip)`));let q=f.ok?0:1;if(p.json)return p.verbose&&o.write(`
225
+ `),Ut(c,d),c===k)return await Xt(d,{env:t,stdout:n,stderr:o,stdin:r,fetchImpl:s,now:a});if(c===b)return await Gt(d,{env:t,stdout:n,stderr:o,stdin:r,now:a});if(c===v)return await Zt(d,{env:t,stdout:n,stderr:o,stdin:r,now:a});let u=Wt(d,t),l=O({verbose:u.verbose,stream:o,env:t});l.section("Private key");let m=await Y({...u,stdin:r});l.detail("source",m.source),l.detail("type",`${N(m)}${m.encrypted?", passphrase-protected":""}`);let y=a(),g=Te({...u,now:y}),w=g.exp-Math.floor(y/1e3);l.section("Claims");for(let[j,T]of Object.entries(g))l.detail(j,T);l.detail("exp at",`${new Date(g.exp*1e3).toISOString()} (in ${H(w)})`);let S=te(g,m.keyObject);l.secret("assertion",S);let f=await Ne({...u,assertion:S,fetchImpl:s,now:a});l.httpRequest(f.request,"Token"),l.httpResponse(f,"Token");let J=De(f.serverDate,f.localMidpointMs),x={outcome:f.ok?"accepted":"rejected",tokenUrl:u.tokenUrl,claims:g,lifetimeSeconds:w,http:{status:f.status,elapsedMs:f.elapsedMs},clockSkew:J,clockSkewUncertainty:J===null?null:Math.ceil(f.elapsedMs/2e3)+1,rawBody:f.rawBody,salesforce:{error:f.body?.error,errorDescription:f.body?.error_description},diagnosis:null,identity:null,userInfo:null,instanceUrl:f.body?.instance_url??null};if(f.ok){if(!u.skipUserInfo&&f.body?.access_token){let j=f.body.instance_url??new URL(u.tokenUrl).origin,T=await Je({userInfoUrl:je(j),accessToken:f.body.access_token,timeoutMs:u.timeoutMs,fetchImpl:s,now:a});l.httpRequest(T.request,"Identity"),l.httpResponse(T,"Identity"),T.ok?x.identity={username:T.body?.preferred_username??T.body?.email??"unknown",userId:T.body?.user_id??"unknown",organizationId:T.body?.organization_id??"unknown"}:(x.userInfo={status:T.status,error:T.body?.error??null,errorDescription:T.body?.error_description??null,rawBody:T.rawBody},l.warn(`The token was issued; it is /userinfo that returned ${T.status}.`))}}else x.diagnosis=ke(x.salesforce,g);J!==null&&(l.section("Clock"),l.detail("drift",`${J}s versus Salesforce (\xB1${x.clockSkewUncertainty}s on a ${f.elapsedMs}ms round trip)`));let q=f.ok?0:1;if(u.json)return u.verbose&&o.write(`
223
226
  `),n.write(`${JSON.stringify({...x,exitCode:q},null,2)}
224
- `),q;let X=f.body?.access_token??null,le=!!(n.isTTY&&o.isTTY);return(p.verbose||X&&le)&&o.write(`
227
+ `),q;let X=f.body?.access_token??null,pe=!!(n.isTTY&&o.isTTY);return(u.verbose||X&&pe)&&o.write(`
225
228
  `),X&&(n.write(`${X}
226
- `),le&&o.write(`
227
- `)),Bt(x,{write:j=>o.write(`${j}
229
+ `),pe&&o.write(`
230
+ `)),Ht(x,{write:j=>o.write(`${j}
228
231
  `),style:l.style}),q}catch(c){if(c.exitCode)return o.write(`${i.red(c.message)}
229
232
  `),c.hint&&o.write(`${i.dim(` ${c.hint}`)}
230
- `),c.exitCode;throw c}}process.exitCode=await Xe(process.argv.slice(2));
233
+ `),c.exitCode;throw c}}process.exitCode=await qe(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wgroovy/sf-jwt",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Sign, decode and verify JSON Web Tokens, with a speciality in Salesforce's OAuth 2.0 JWT bearer flow and Telephony Integration REST API",
5
5
  "license": "MIT",
6
6
  "author": "wgroovy",