126 lines
3.4 KiB
TypeScript
126 lines
3.4 KiB
TypeScript
import { getSetCookies } from '@std/http/cookie';
|
|
import { generateTotp } from '@stdext/crypto/totp';
|
|
|
|
export interface API_CLIENT {
|
|
fetch: (url: string, options?: FETCH_OPTIONS, transform?: (obj: any) => any) => Promise<any>;
|
|
}
|
|
|
|
export type API_CONFIG = {
|
|
protocol: string;
|
|
hostname: string;
|
|
port: number;
|
|
prefix: string;
|
|
};
|
|
|
|
const DEFAULT_API_CONFIG: API_CONFIG = {
|
|
protocol: 'http:',
|
|
hostname: 'localhost',
|
|
port: 80,
|
|
prefix: ''
|
|
};
|
|
|
|
export interface RETRY_OPTIONS {
|
|
limit: number;
|
|
methods: string[];
|
|
status_codes: number[];
|
|
}
|
|
|
|
export interface FETCH_OPTIONS extends RequestInit {
|
|
retry?: RETRY_OPTIONS;
|
|
json?: Record<string, any>;
|
|
done?: (response: Response) => void;
|
|
session?: Record<string, any>;
|
|
totp_token?: string;
|
|
}
|
|
|
|
const DEFAULT_TRANSFORM = (response_json: any) => {
|
|
return response_json;
|
|
};
|
|
|
|
export function api(api_config?: Record<string, any>): API_CLIENT {
|
|
const config: API_CONFIG = {
|
|
...DEFAULT_API_CONFIG,
|
|
...(api_config ?? {})
|
|
};
|
|
|
|
const client: API_CLIENT = {
|
|
fetch: async (url: string, options?: FETCH_OPTIONS, transform?: (obj: any) => any): Promise<any> => {
|
|
const prefix: string = `${config.protocol}//${config.hostname}:${config.port}${config.prefix}`;
|
|
const retry: RETRY_OPTIONS = options?.retry ?? {
|
|
limit: 0,
|
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
|
|
status_codes: [500, 502, 503, 504, 521, 522, 524]
|
|
};
|
|
|
|
const content_type = options?.json ? 'application/json' : 'text/plain';
|
|
const headers = new Headers(options?.headers ?? {});
|
|
headers.append('accept', 'application/json');
|
|
if (options?.json || options?.body) {
|
|
headers.append('content-type', content_type);
|
|
}
|
|
if (options?.session) {
|
|
const cookies = getSetCookies(headers);
|
|
|
|
cookies.push({
|
|
name: options.totp_token ?? 'totp',
|
|
value: await generateTotp(options.session.secret),
|
|
maxAge: 30,
|
|
expires: Date.now() + 30_000,
|
|
path: '/'
|
|
});
|
|
|
|
for (const cookie of cookies) {
|
|
headers.append(`x-${cookie.name}`, cookie.value);
|
|
}
|
|
headers.append('cookie', cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; '));
|
|
}
|
|
|
|
const request_options: RequestInit = {
|
|
body: options?.json ? JSON.stringify(options.json, null, 2) : options?.body,
|
|
method: options?.method ?? 'GET',
|
|
credentials: options?.credentials ?? 'include',
|
|
redirect: options?.redirect ?? 'follow',
|
|
headers
|
|
};
|
|
|
|
const response_transform = transform ?? DEFAULT_TRANSFORM;
|
|
|
|
let retries = 0;
|
|
let delay = 1000;
|
|
const resolved_url = `${prefix}${url}`;
|
|
do {
|
|
const response: Response = await fetch(resolved_url, request_options);
|
|
if (
|
|
retries < retry.limit && retry.status_codes.includes(response.status) &&
|
|
retry.methods.includes(request_options.method ?? 'GET')
|
|
) {
|
|
++retries;
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
delay *= 2;
|
|
continue;
|
|
}
|
|
|
|
if (response.status >= 400) {
|
|
const error_response = await response.json();
|
|
throw new Error(
|
|
error_response.error?.message ?? 'Bad Reqeest',
|
|
error_response.error ?? {
|
|
cause: error_response?.cause ?? JSON.stringify(error_response)
|
|
}
|
|
);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const transformed = await response_transform(data);
|
|
|
|
if (options?.done) {
|
|
options.done(response);
|
|
}
|
|
|
|
return transformed;
|
|
} while (retries < retry.limit);
|
|
}
|
|
};
|
|
|
|
return client;
|
|
}
|