Skip to content

Commit

Permalink
feat: captcha support on sdk plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
cpaulve-1A committed Dec 13, 2023
1 parent e70c893 commit 58fa7e6
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 18 deletions.
2 changes: 1 addition & 1 deletion packages/@ama-sdk/core/src/clients/api-fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export class ApiFetchClient implements ApiClient {
// Execute call
try {

const controller = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
const controller = new AbortController();
if (controller) {
options.signal = controller.signal;
}
Expand Down
1 change: 1 addition & 0 deletions packages/@ama-sdk/core/src/plugins/core/fetch-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface FetchPluginContext {
/** Api Client processing the call the the API */
apiClient: ApiClient;

// TODO Now supported for all the o modern browser - should become mandatory in @ama-sdk/[email protected]
/** Abort controller to abort fetch call */
controller?: AbortController;
}
Expand Down
110 changes: 96 additions & 14 deletions packages/@ama-sdk/core/src/plugins/timeout/timeout.fetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,71 @@
import { ResponseTimeoutError } from '../../fwk/errors';
import { FetchCall, FetchPlugin, FetchPluginContext } from '../core';
import {ResponseTimeoutError} from '../../fwk/errors';
import {FetchCall, FetchPlugin, FetchPluginContext} from '../core';

/**
* Representation of an Imperva Captcha message
*/
type ImpervaCaptchaMessageData = {
impervaChallenge: {
type: 'captcha';
status: 'started' | 'ended';
timeout: number;
url: string;
};
};

/**
* Type to describe the timer status of the {@see TimeoutFetch} plugin.
* Today, only the stop and restart of the timer is supported which match the following events:
* - stop: stop the timeout timer
* - start: reset the timer and restart it
*/
export type TimeoutPauseStatus = 'stop' | 'start';

/**
* Check if a message can be cast as an {@link ImpervaCaptchaMessage}
* @param message
*/
function isImpervaCaptchaMessage(message: any): message is ImpervaCaptchaMessageData {
return Object.prototype.hasOwnProperty.call(message, 'impervaChallenge') &&
Object.prototype.hasOwnProperty.call(message.impervaChallenge, 'status') &&
Object.prototype.hasOwnProperty.call(message.impervaChallenge, 'type') && message.impervaChallenge.type === 'captcha';
}

/**
* Event handler that will emit event to pause the timeout
* Today the timeout only
*/
export type TimeoutPauseEventHandler = ((timeoutPauseCallback: (timeoutStatus: TimeoutPauseStatus) => void, context: any) => () => void);
/**
* Factory to generate a {@see TimeoutPauseEventHandler} depending on various configurations
*/
export type TimeoutPauseEventHandlerFactory<T> = (config?: Partial<T>) => TimeoutPauseEventHandler;

/**
* Captures Imperva captcha events and calls the event callback
* It can only be used for browser's integrating imperva captcha
*
* @param config: list of host names that can trigger a captcha event
*
* @return removeEventListener
*/
export const impervaCaptchaEventHandlerFactory: TimeoutPauseEventHandlerFactory<{ whiteListedHostNames: string[] }> = (config) =>
(timeoutPauseCallback: (timeoutStatus: TimeoutPauseStatus) => void) => {
const onImpervaCaptcha = ((event: MessageEvent<any>) => {
const originHostname = (new URL(event.origin)).hostname;
if (originHostname !== location.hostname && (config?.whiteListedHostNames || []).indexOf(originHostname) === -1) {
return;
}
const message = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
if (message && isImpervaCaptchaMessage(message)) {
timeoutPauseCallback(message.impervaChallenge.status === 'started' ? 'stop' : 'start');
}
});
addEventListener('message', onImpervaCaptcha);
return () => {
removeEventListener('message', onImpervaCaptcha);
};
};

/**
* Plugin to fire an exception on timeout
Expand All @@ -8,44 +74,60 @@ export class TimeoutFetch implements FetchPlugin {

/** Fetch timeout (in millisecond) */
public timeout: number;
private timerSubscription: ((pauseStatus: TimeoutPauseStatus) => void)[] = [];
private timerPauseState: 'stop' | 'start' = 'start';

/**
* Timeout Fetch plugin.
*
* @param timeout Timeout in millisecond
* @param timeoutPauseEvents Events that will trigger the pause and reset of the timeout
*/
constructor(timeout = 60000) {
constructor(timeout = 60000, private timeoutPauseEvents: TimeoutPauseEventHandler[] = []) {
this.timeout = timeout;
this.timeoutPauseEvents.forEach((timeoutPauseEvent) => {
timeoutPauseEvent((pausedStatus: TimeoutPauseStatus) => {
this.timerPauseState = pausedStatus;
this.timerSubscription.forEach((timer) => timer.call(this, pausedStatus));
}, this);
});
}

public load(context: FetchPluginContext) {
return {
transform: (fetchCall: FetchCall) =>
// eslint-disable-next-line no-async-promise-executor
new Promise<Response>(async (resolve, reject) => {
let didTimeOut = false;

const timer = setTimeout(() => {
didTimeOut = true;
const timeoutCallback = () => {
reject(new ResponseTimeoutError(`in ${this.timeout}ms`));
if (context.controller) {
context.controller.abort();
// Fetch abort controller is now supported by all modern browser and node 15+. It should always be defined
context.controller?.abort();
};
let timer = this.timerPauseState ? undefined : setTimeout(timeoutCallback, this.timeout);
const timerCallback = (pauseStatus: TimeoutPauseStatus) => {
if (timer && pauseStatus === 'stop') {
clearTimeout(timer);
timer = undefined;
} else if (!timer && pauseStatus === 'start') {
timer = setTimeout(timeoutCallback, this.timeout);
}
}, this.timeout);
};
this.timerSubscription.push(timerCallback);

try {
const response = await fetchCall;

if (!didTimeOut) {
if (!context.controller?.signal.aborted) {
resolve(response);
}
} catch (ex) {
reject(ex);
} finally {
clearTimeout(timer);
if (timer) {
clearTimeout(timer);
}
this.timerSubscription = this.timerSubscription.filter(callback => timerCallback !== callback);
}
})
};
}

}
6 changes: 3 additions & 3 deletions packages/@ama-sdk/core/src/plugins/timeout/timeout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('Timeout Fetch Plugin', () => {
it('should reject on timeout', async () => {
const plugin = new TimeoutFetch(100);

const runner = plugin.load({} as any);
const runner = plugin.load({controller: new AbortController()} as any);
const call = new Promise<any>((resolve) => setTimeout(() => resolve(undefined), 1000));

const callback = jest.fn();
Expand All @@ -20,7 +20,7 @@ describe('Timeout Fetch Plugin', () => {
it('should not reject on fetch rejection', async () => {
const plugin = new TimeoutFetch(6000);

const runner = plugin.load({} as any);
const runner = plugin.load({controller: new AbortController()} as any);
const call = new Promise<any>((_resolve, reject) => setTimeout(() => reject(new EmptyResponseError('')), 100));


Expand All @@ -33,7 +33,7 @@ describe('Timeout Fetch Plugin', () => {
it('should forward the fetch response', async () => {
const plugin = new TimeoutFetch(2000);

const runner = plugin.load({} as any);
const runner = plugin.load({controller: new AbortController()} as any);
const call = new Promise<any>((resolve) => setTimeout(() => resolve({test: true}), 100));

const promise = runner.transform(call);
Expand Down

0 comments on commit 58fa7e6

Please sign in to comment.