diff --git a/lambda/qbo-oauth/index.ts b/lambda/qbo-oauth/index.ts index 8a94211..a9bd725 100644 --- a/lambda/qbo-oauth/index.ts +++ b/lambda/qbo-oauth/index.ts @@ -7,13 +7,27 @@ import { const secretsClient = new SecretsManagerClient({}); const QBO_SECRET_ARN = process.env.QBO_SECRET_ARN!; -const QBO_CLIENT_ID = process.env.QBO_CLIENT_ID!; -const QBO_CLIENT_SECRET = process.env.QBO_CLIENT_SECRET!; const REDIRECT_URI = process.env.REDIRECT_URI!; // https://bot.seahaven.com/qbo/callback // Intuit discovery document — endpoints resolved at runtime per Intuit requirements const DISCOVERY_URL = 'https://developer.api.intuit.com/.well-known/openid_configuration'; +// Client credentials — read from Secrets Manager at runtime so updates +// don't require a redeploy +interface QBOClientCreds { clientId: string; clientSecret: string } +let cachedCreds: QBOClientCreds | undefined; + +async function getClientCreds(): Promise { + if (!cachedCreds) { + const res = await secretsClient.send( + new GetSecretValueCommand({ SecretId: QBO_SECRET_ARN }), + ); + const secret = JSON.parse(res.SecretString!); + cachedCreds = { clientId: secret.clientId, clientSecret: secret.clientSecret }; + } + return cachedCreds; +} + interface DiscoveryDocument { authorization_endpoint: string; token_endpoint: string; @@ -104,11 +118,11 @@ function clearStateCookie(): string { // ── /qbo/connect — redirect to Intuit OAuth ────────────────────────────────── async function handleConnect(): Promise { - const discovery = await getDiscovery(); + const [discovery, creds] = await Promise.all([getDiscovery(), getClientCreds()]); const state = crypto.randomUUID(); const params = new URLSearchParams({ - client_id: QBO_CLIENT_ID, + client_id: creds.clientId, response_type: 'code', scope: SCOPES, redirect_uri: REDIRECT_URI, @@ -145,8 +159,8 @@ async function handleCallback( return redirect('/qbo/launch?error=missing_params', clearCookie); } - const discovery = await getDiscovery(); - const credentials = Buffer.from(`${QBO_CLIENT_ID}:${QBO_CLIENT_SECRET}`).toString('base64'); + const [discovery, creds] = await Promise.all([getDiscovery(), getClientCreds()]); + const credentials = Buffer.from(`${creds.clientId}:${creds.clientSecret}`).toString('base64'); const tokenRes = await fetch(discovery.token_endpoint, { method: 'POST', @@ -179,8 +193,8 @@ async function handleCallback( new PutSecretValueCommand({ SecretId: QBO_SECRET_ARN, SecretString: JSON.stringify({ - clientId: QBO_CLIENT_ID, - clientSecret: QBO_CLIENT_SECRET, + clientId: creds.clientId, + clientSecret: creds.clientSecret, refreshToken: tokens.refresh_token, realmId, }), @@ -208,8 +222,8 @@ async function handleDisconnect(): Promise { // Revoke the token at Intuit if we have one if (refreshToken) { - const discovery = await getDiscovery(); - const credentials = Buffer.from(`${QBO_CLIENT_ID}:${QBO_CLIENT_SECRET}`).toString('base64'); + const [discovery, creds] = await Promise.all([getDiscovery(), getClientCreds()]); + const credentials = Buffer.from(`${creds.clientId}:${creds.clientSecret}`).toString('base64'); try { await fetch(discovery.revocation_endpoint, { @@ -230,8 +244,8 @@ async function handleDisconnect(): Promise { new PutSecretValueCommand({ SecretId: QBO_SECRET_ARN, SecretString: JSON.stringify({ - clientId: QBO_CLIENT_ID, - clientSecret: QBO_CLIENT_SECRET, + clientId: creds.clientId, + clientSecret: creds.clientSecret, refreshToken: '', realmId: '', }), diff --git a/lib/constructs/slack-handler.ts b/lib/constructs/slack-handler.ts index 651f4a6..b78fccc 100644 --- a/lib/constructs/slack-handler.ts +++ b/lib/constructs/slack-handler.ts @@ -123,8 +123,6 @@ export class SlackHandlerConstruct extends Construct { memorySize: 256, environment: { QBO_SECRET_ARN: qboSecret.secretArn, - QBO_CLIENT_ID: '{{resolve:secretsmanager:seahaven/qbo/oauth:SecretString:clientId}}', - QBO_CLIENT_SECRET: '{{resolve:secretsmanager:seahaven/qbo/oauth:SecretString:clientSecret}}', REDIRECT_URI: 'https://bot.seahaven.com/qbo/callback', }, vpc: props.vpc,