autonomous.contact/utils/api.ts

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;
}