diff --git a/poller-lambdas/src/index.ts b/poller-lambdas/src/index.ts index ceaa9b74..19d52219 100644 --- a/poller-lambdas/src/index.ts +++ b/poller-lambdas/src/index.ts @@ -1,4 +1,7 @@ -import { GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; +import { + GetSecretValueCommand, + PutSecretValueCommand, +} from '@aws-sdk/client-secrets-manager'; import type { SendMessageCommandInput } from '@aws-sdk/client-sqs'; import { SendMessageCommand } from '@aws-sdk/client-sqs'; import type { PollerId } from '../../shared/pollers'; @@ -14,12 +17,13 @@ const pollerWrapper = (pollerFunction: PollFunction) => async ({ Records }: HandlerInputSqsPayload) => { const startTimeEpochMillis = Date.now(); + const secretName = getEnvironmentVariableOrCrash( + POLLER_LAMBDA_ENV_VAR_KEYS.SECRET_NAME, + ); const secret = await secretsManager .send( new GetSecretValueCommand({ - SecretId: getEnvironmentVariableOrCrash( - POLLER_LAMBDA_ENV_VAR_KEYS.SECRET_NAME, - ), + SecretId: secretName, }), ) .then((_) => _.SecretString); @@ -90,6 +94,17 @@ const pollerWrapper = MessageBody: output.valueForNextPoll, }); } + + if (output.newSecretValue) { + // set new value in secrets manager + console.log(`Updating secret value for ${secretName}`); + await secretsManager.send( + new PutSecretValueCommand({ + SecretId: secretName, + SecretString: output.newSecretValue, + }), + ); + } }) .catch((error) => { console.error('FAILED', error); diff --git a/poller-lambdas/src/pollers/reuters/auth.ts b/poller-lambdas/src/pollers/reuters/auth.ts index d142f323..53ca1967 100644 --- a/poller-lambdas/src/pollers/reuters/auth.ts +++ b/poller-lambdas/src/pollers/reuters/auth.ts @@ -6,8 +6,6 @@ const AuthSchema = z.object({ token_type: z.string(), }); -type AuthData = z.infer; - const scopes = 'https://api.thomsonreuters.com/auth/reutersconnect.contentapi.read https://api.thomsonreuters.com/auth/reutersconnect.contentapi.write'; const authUrl = 'https://auth.thomsonreuters.com/oauth/token'; @@ -17,7 +15,7 @@ const audience = '7a14b6a2-73b8-4ab2-a610-80fb9f40f769'; export async function auth( clientId: string, clientSecret: string, -): Promise { +): Promise { const req = new Request(authUrl, { method: 'POST', headers: { @@ -26,9 +24,14 @@ export async function auth( body: `grant_type=${grantType}&client_id=${clientId}&client_secret=${clientSecret}&audience=${audience}&scope=${encodeURIComponent(scopes)}`, }); try { + console.log('Requesting new auth token from Reuters'); const response = await fetch(req); const data = (await response.json()) as unknown; - return AuthSchema.parse(data); + const { access_token, expires_in } = AuthSchema.parse(data); + console.log( + `Received new auth token from Reuters, expires in ${expires_in} seconds`, + ); + return access_token; } catch (error) { console.error(error); throw new Error('Failed to get auth token from Reuters'); diff --git a/poller-lambdas/src/pollers/reuters/reutersPoller.ts b/poller-lambdas/src/pollers/reuters/reutersPoller.ts index 8f12c316..2b08b343 100644 --- a/poller-lambdas/src/pollers/reuters/reutersPoller.ts +++ b/poller-lambdas/src/pollers/reuters/reutersPoller.ts @@ -178,6 +178,7 @@ function itemResponseToIngestionLambdaInput( const SecretValueSchema = z.object({ CLIENT_ID: z.string(), CLIENT_SECRET: z.string(), + ACCESS_TOKEN: z.string().optional(), }); export const reutersPoller = (async ( @@ -188,49 +189,56 @@ export const reutersPoller = (async ( if (!parsedSecret.success) { throw new Error('Failed to parse secret value for Reuters poller'); } - const { CLIENT_ID, CLIENT_SECRET } = parsedSecret.data; - const { access_token } = await auth( - CLIENT_ID, - CLIENT_SECRET, - ); /** @todo: the tokens are quite long-lived so we should check that there aren't any problems requesting one on each invocation. */ + const { CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN } = parsedSecret.data; - const searchResponse = await fetch( - 'https://api.reutersconnect.com/content/graphql', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${access_token}`, - }, - body: JSON.stringify({ - query: textItemsSearchQuery, - }), - }, - ); + let accessToken = ACCESS_TOKEN ?? (await auth(CLIENT_ID, CLIENT_SECRET)); - const searchData = SearchDataSchema.parse(await searchResponse.json()); - - const itemsToFetch = searchData.data.search.items - .map((item) => item.versionedGuid) - .filter((guid): guid is string => guid !== undefined); - - const itemResponses = await Promise.all( - itemsToFetch.map(async (itemId) => { - const itemResponse = await fetch( + async function fetchWithReauth(query: string) { + let searchResponse = await fetch( + 'https://api.reutersconnect.com/content/graphql', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + query: query, + }), + }, + ); + if (searchResponse.status === 401 || searchResponse.status === 419) { + const newAccessToken = await auth(CLIENT_ID, CLIENT_SECRET); + accessToken = newAccessToken; + searchResponse = await fetch( 'https://api.reutersconnect.com/content/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${access_token}`, + Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ - query: itemQuery(itemId), + query: textItemsSearchQuery, }), }, ); - return itemResponseSchema.parse(await itemResponse.json()); - }), + } + return await searchResponse.json(); + } + + const searchData = SearchDataSchema.parse( + await fetchWithReauth(textItemsSearchQuery), + ); + + const itemsToFetch = searchData.data.search.items + .map((item) => item.versionedGuid) + .filter((guid): guid is string => guid !== undefined); + + const itemResponses = await Promise.all( + itemsToFetch.map(async (itemId) => + itemResponseSchema.parse(await fetchWithReauth(itemQuery(itemId))), + ), ); return { diff --git a/poller-lambdas/src/types.ts b/poller-lambdas/src/types.ts index adfe8ae6..e1e68d65 100644 --- a/poller-lambdas/src/types.ts +++ b/poller-lambdas/src/types.ts @@ -7,6 +7,7 @@ export type PollerInput = string; export interface CorePollerOutput { payloadForIngestionLambda: IngestorPayload[] | IngestorPayload; valueForNextPoll: PollerInput; + newSecretValue?: SecretValue; } export type LongPollOutput = CorePollerOutput;