snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
The Rocket.Chat Enterprise Edition (EE) license (the "EE License")
Copyright (c) 2015-2023 Rocket.Chat Technologies Corp.
With regard to the Rocket.Chat Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Rocket.Chat Subscription Terms of Service,
available at https://rocket.chat/terms (the "EE Terms"), or other agreement
governing the use of the Software, as agreed by you and Rocket.Chat, and otherwise
have a valid Rocket.Chat Enterprise Edition subscription for the correct number
of user seats. Subject to the foregoing sentence, you are free to modify this
Software and publish patches to the Software. You agree that Rocket.Chat and/or
its licensors (as applicable) retain all right, title and interest in and to all
such modifications and/or patches, and all such modifications and/or patches may
only be used, copied, modified, displayed, distributed, or otherwise exploited
with a valid Rocket.Chat Enterprise Edition subscription for the correct number
of user seats. Notwithstanding the foregoing, you may copy and modify the Software
for development and testing purposes, without requiring a Subscription. You agree
that Rocket.Chat and/or its licensors (as applicable) retain all right, title and
interest in and to all such modifications. You are not granted any other rights
beyond what is expressly stated herein. Subject to the foregoing, it is forbidden
to copy, merge, publish, distribute, sublicense, and/or sell the Software.
This EE License applies only to the part of this Software that is not distributed
as part of Rocket.Chat Community Edition (CE). Any part of this Software distributed
as part of Rocket.Chat CE or is served client-side as an image, font, cascading
stylesheet (CSS), file which produces or is compiled, arranged, augmented, or combined
into client-side JavaScript, in whole or in part, is copyrighted under the MIT Expat
license. The full text of this EE License shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
For all third-party components incorporated into the Rocket.Chat Software,
those components are licensed under the original license provided by the owner
of the applicable component.

View File

@@ -0,0 +1,163 @@
import fetchMock from 'jest-fetch-mock';
import { RestClient } from '../src/index';
beforeAll(() => {
fetchMock.enableMocks();
});
afterAll(() => {
fetchMock.disableMocks();
});
beforeEach(() => {
fetchMock.mockIf(/^https?:\/\/example.com.*$/, async (req) => {
if (req.headers.get('x-2fa-code') === '2FA_CODE') {
return {
status: 200,
body: JSON.stringify({
status: 'success',
data: {
userId: 'foo',
email: 'foo',
username: 'foo',
},
}),
};
}
if (req.headers.get('x-2fa-code') === 'WRONG_2FA_CODE') {
return {
status: 400,
body: JSON.stringify({
errorType: 'totp-invalid',
message: 'Invalid TOTP provided',
details: {
method: 'totp',
},
}),
};
}
return {
status: 400,
body: JSON.stringify({
errorType: 'totp-required',
details: {
method: 'totp',
},
}),
};
});
fetchMock.doMock();
});
const isResponse = (e: any): e is Response => {
expect(e).toBeInstanceOf(Response);
return true;
};
test('if the 2fa handler is not provided, it should throw an error', async () => {
const client = new RestClient({
baseUrl: 'https://example.com',
});
try {
await client.post('/v1/login', { user: 'foo', username: 'foo', email: 'foo', password: 'foo', code: 'foo' });
} catch (error) {
if (!isResponse(error)) {
throw error;
}
expect(error.status).toBe(400);
const body = await error.json();
expect(body).toMatchObject({
errorType: 'totp-required',
details: {
method: 'totp',
},
});
}
});
test('if the 2fa handler is provided, and fails if should throw the error thrown by the handler', async () => {
const fn = jest.fn();
const client = new RestClient({
baseUrl: 'https://example.com',
});
client.handleTwoFactorChallenge((e) => {
fn(e);
throw new Error('foo');
});
await expect(client.post('/v1/login', { user: 'foo', username: 'foo', email: 'foo', password: 'foo', code: 'foo' })).rejects.toThrow(
new Error('foo'),
);
expect(fn).toHaveBeenCalledTimes(1);
});
test('if the 2fa handler is provided it should resolves', async () => {
const fn = jest.fn();
const client = new RestClient({
baseUrl: 'https://example.com',
});
client.handleTwoFactorChallenge(() => {
fn();
return Promise.resolve('2FA_CODE');
});
const result = await client.post('/v1/login', { user: 'foo', username: 'foo', email: 'foo', password: 'foo', code: 'foo' });
expect(result).toMatchObject({
status: 'success',
data: {
userId: 'foo',
email: 'foo',
username: 'foo',
},
});
expect(fn).toHaveBeenCalledTimes(1);
});
test('should be ask for 2fa code again if the code is wrong', async () => {
const fn = jest.fn();
const client = new RestClient({
baseUrl: 'https://example.com',
});
let retries = 0;
client.handleTwoFactorChallenge(() => {
fn();
if (!retries) {
retries++;
return Promise.resolve('WRONG_2FA_CODE');
}
return Promise.resolve('2FA_CODE');
});
const result = await client.post('/v1/login', { user: 'foo', username: 'foo', email: 'foo', password: 'foo', code: 'foo' });
expect(result).toMatchObject({
status: 'success',
data: {
userId: 'foo',
email: 'foo',
username: 'foo',
},
});
expect(fn).toHaveBeenCalledTimes(2);
});

View File

@@ -0,0 +1,7 @@
import client from '@rocket.chat/jest-presets/client';
import type { Config } from 'jest';
export default {
preset: client.preset,
setupFilesAfterEnv: [...client.setupFilesAfterEnv],
} satisfies Config;

View File

@@ -0,0 +1,43 @@
{
"name": "@rocket.chat/api-client",
"version": "0.2.56",
"repository": {
"type": "git",
"url": "git+https://github.com/RocketChat/Rocket.Chat.git",
"directory": "packages/api-client"
},
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"build": "tsc",
"dev": "tsc --watch --preserveWatchOutput",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "jest",
"testunit": "jest"
},
"dependencies": {
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/rest-typings": "workspace:^",
"filter-obj": "^3.0.0",
"query-string": "^7.1.3",
"split-on-first": "^3.0.0",
"strict-uri-encode": "^2.0.0"
},
"devDependencies": {
"@rocket.chat/jest-presets": "workspace:~",
"@rocket.chat/tsconfig": "workspace:*",
"@types/jest": "~30.0.0",
"@types/strict-uri-encode": "^2.0.2",
"eslint": "~9.39.5",
"jest": "~30.2.0",
"jest-fetch-mock": "~3.0.3",
"typescript": "~5.9.3"
},
"volta": {
"extends": "../../package.json"
}
}

View File

@@ -0,0 +1,4 @@
export type Credentials = {
'X-User-Id': string;
'X-Auth-Token': string;
};

View File

@@ -0,0 +1,92 @@
import type { Serialized } from '@rocket.chat/core-typings';
import type {
MatchPathPattern,
OperationParams,
OperationResult,
PathFor,
PathWithParamsFor,
PathWithoutParamsFor,
} from '@rocket.chat/rest-typings';
type Next<T extends (...args: any[]) => any> = (...args: Parameters<T>) => ReturnType<T>;
export type Middleware<T extends (...args: any[]) => any> = (context: Parameters<T>, next: Next<T>) => ReturnType<T>;
// eslint-disable-next-line @typescript-eslint/naming-convention
export interface RestClientInterface {
get<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithParamsFor<'GET'> = PathWithParamsFor<'GET'>>(
endpoint: TPath,
params: OperationParams<'GET', TPathPattern>,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'GET', TPathPattern>>>;
get<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithoutParamsFor<'GET'> = PathWithoutParamsFor<'GET'>>(
endpoint: TPath,
params?: undefined,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'GET', TPathPattern>>>;
post<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithParamsFor<'POST'> = PathWithParamsFor<'POST'>>(
endpoint: TPath,
params: OperationParams<'POST', TPathPattern>,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'POST', TPathPattern>>>;
post<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithoutParamsFor<'POST'> = PathWithoutParamsFor<'POST'>>(
endpoint: TPath,
params?: undefined,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'POST', TPathPattern>>>;
put<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithParamsFor<'PUT'> = PathWithParamsFor<'PUT'>>(
endpoint: TPath,
params: OperationParams<'PUT', TPathPattern>,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'PUT', TPathPattern>>>;
put<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithoutParamsFor<'PUT'> = PathWithoutParamsFor<'PUT'>>(
endpoint: TPath,
params?: undefined,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'PUT', TPathPattern>>>;
delete<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithParamsFor<'DELETE'> = PathWithParamsFor<'DELETE'>>(
endpoint: TPath,
params: OperationParams<'DELETE', TPathPattern>,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'DELETE', TPathPattern>>>;
delete<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithoutParamsFor<'DELETE'> = PathWithoutParamsFor<'DELETE'>>(
endpoint: TPath,
params?: undefined,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'DELETE', TPathPattern>>>;
upload<TPath extends PathFor<'POST'>>(
endpoint: TPath,
params: void extends OperationParams<'POST', MatchPathPattern<TPath>> ? void : OperationParams<'POST', MatchPathPattern<TPath>>,
events?: {
load?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void;
progress?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void;
abort?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void;
error?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void;
},
options?: Omit<RequestInit, 'method'>,
): XMLHttpRequest;
getCredentials():
| {
'X-User-Id': string;
'X-Auth-Token': string;
}
| undefined;
setCredentials(credentials: undefined | { 'X-User-Id': string; 'X-Auth-Token': string }): void;
use(middleware: Middleware<RestClientInterface['send']>): void;
send(endpoint: string, method: string, options?: Omit<RequestInit, 'method'>): Promise<Response>;
handleTwoFactorChallenge(
cb: (args: { method: 'totp' | 'email' | 'password'; emailOrUsername?: string; invalidAttempt?: boolean }) => Promise<string>,
): void;
}

View File

@@ -0,0 +1,24 @@
const twoFactorMethods = ['totp', 'email', 'password'] as const;
type TwoFactorMethod = (typeof twoFactorMethods)[number];
export const isTotpRequiredError = (error: unknown): error is { error: 'totp-required' } | { errorType: 'totp-required' } =>
typeof error === 'object' &&
((error as { error?: unknown } | undefined)?.error === 'totp-required' ||
(error as { errorType?: unknown } | undefined)?.errorType === 'totp-required');
export const isTotpInvalidError = (error: unknown): error is { error: 'totp-invalid' } | { errorType: 'totp-invalid' } =>
(error as { error?: unknown } | undefined)?.error === 'totp-invalid' ||
(error as { errorType?: unknown } | undefined)?.errorType === 'totp-invalid';
export const isTwoFactorMethod = (method: string): method is TwoFactorMethod => twoFactorMethods.includes(method as TwoFactorMethod);
export const hasRequiredTwoFactorMethod = (error: unknown): error is { details: { method: TwoFactorMethod; emailOrUsername?: string } } => {
const details = error && typeof error === 'object' && 'details' in error && (error.details as unknown);
return (
typeof details === 'object' &&
details !== null &&
typeof (details as { method: unknown }).method === 'string' &&
isTwoFactorMethod((details as { method: string }).method)
);
};

View File

@@ -0,0 +1,305 @@
import type { Serialized } from '@rocket.chat/core-typings';
import type {
MatchPathPattern,
ParamsFor,
OperationResult,
PathFor,
PathWithoutParamsFor,
PathWithParamsFor,
} from '@rocket.chat/rest-typings';
import { stringify } from 'query-string';
import type { Credentials } from './Credentials';
import type { Middleware, RestClientInterface } from './RestClientInterface';
import { hasRequiredTwoFactorMethod, isTotpInvalidError, isTotpRequiredError } from './errors';
export type { RestClientInterface, Credentials };
const pipe =
<T extends (...args: any[]) => any>(fn: T) =>
(...args: Parameters<T>): ReturnType<T> =>
fn(...args);
function buildFormData(data?: Record<string, any> | void, formData = new FormData(), parentKey?: string): FormData {
if (data instanceof FormData) {
return data;
}
if (!data) {
return formData;
}
if (typeof data === 'object' && !(data instanceof File)) {
Object.keys(data).forEach((key) => {
buildFormData(formData, data[key], parentKey ? `${parentKey}[${key}]` : key);
});
} else {
data && parentKey && formData.append(parentKey, data);
}
return formData;
}
const checkIfIsFormData = (data: any = {}): boolean => {
if (data instanceof FormData) {
return true;
}
return Object.values(data).some((value) => {
if (value && typeof value === 'object' && !(value instanceof File)) {
return checkIfIsFormData(value);
}
return value instanceof File;
});
};
export class RestClient implements RestClientInterface {
private twoFactorHandler?: (args: {
method: 'totp' | 'email' | 'password';
emailOrUsername?: string;
invalidAttempt?: boolean;
}) => Promise<string>;
private readonly baseUrl: string;
private headers: Record<string, string> = {};
private credentials: Credentials | undefined;
constructor({ baseUrl, credentials, headers = {} }: { baseUrl: string; credentials?: Credentials; headers?: Record<string, string> }) {
this.baseUrl = `${baseUrl}/api`;
this.setCredentials(credentials);
this.headers = headers;
}
getCredentials(): ReturnType<RestClientInterface['getCredentials']> {
return this.credentials;
}
setCredentials: RestClientInterface['setCredentials'] = (credentials) => {
this.credentials = credentials;
};
get<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithParamsFor<'GET'> = PathWithParamsFor<'GET'>>(
endpoint: TPath,
params: ParamsFor<'GET', TPathPattern>,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'GET', TPathPattern>>>;
get<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithoutParamsFor<'GET'> = PathWithoutParamsFor<'GET'>>(
endpoint: TPath,
params?: undefined,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'GET', TPathPattern>>>;
async get<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathFor<'GET'>>(
endpoint: TPath,
params?: ParamsFor<'GET', TPathPattern>,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'GET', TPathPattern>>> {
if (/\?/.test(endpoint)) {
// throw new Error('Endpoint cannot contain query string');
console.warn('Endpoint cannot contain query string', endpoint);
}
const queryParams = this.getParams(params);
const response = await this.send(`${endpoint}${queryParams ? `?${queryParams}` : ''}`, 'GET', options ?? {});
return response.json();
}
post<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithParamsFor<'POST'> = PathWithParamsFor<'POST'>>(
endpoint: TPath,
params: ParamsFor<'POST', TPathPattern>,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'POST', TPathPattern>>>;
post<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithoutParamsFor<'POST'> = PathWithoutParamsFor<'POST'>>(
endpoint: TPath,
params?: undefined,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'POST', TPathPattern>>>;
async post<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathFor<'POST'>>(
endpoint: TPath,
params?: ParamsFor<'POST', TPathPattern>,
{ headers, ...options }: Omit<RequestInit, 'method'> = {},
): Promise<Serialized<OperationResult<'POST', TPathPattern>>> {
const isFormData = checkIfIsFormData(params);
const response = await this.send(endpoint, 'POST', {
body: isFormData ? buildFormData(params) : JSON.stringify(params),
headers: {
Accept: 'application/json',
...(!isFormData && { 'Content-Type': 'application/json' }),
...headers,
},
...options,
});
// If the server sent no data, return an empty record as we're only expecting objects.
if (response.status === 204) {
return {} as any;
}
return response.json();
}
put<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithParamsFor<'PUT'> = PathWithParamsFor<'PUT'>>(
endpoint: TPath,
params: ParamsFor<'PUT', TPathPattern>,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'PUT', TPathPattern>>>;
put<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithoutParamsFor<'PUT'> = PathWithoutParamsFor<'PUT'>>(
endpoint: TPath,
params?: undefined,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'PUT', TPathPattern>>>;
async put<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathFor<'PUT'>>(
endpoint: TPath,
params?: ParamsFor<'PUT', TPathPattern>,
{ headers, ...options }: Omit<RequestInit, 'method'> = {},
): Promise<Serialized<OperationResult<'PUT', TPathPattern>>> {
const isFormData = checkIfIsFormData(params);
const response = await this.send(endpoint, 'PUT', {
body: isFormData ? buildFormData(params) : JSON.stringify(params),
headers: {
Accept: 'application/json',
...(!isFormData && { 'Content-Type': 'application/json' }),
...headers,
},
...options,
});
return response.json();
}
delete<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithParamsFor<'DELETE'> = PathWithParamsFor<'DELETE'>>(
endpoint: TPath,
params: ParamsFor<'DELETE', TPathPattern>,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'DELETE', TPathPattern>>>;
delete<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathWithoutParamsFor<'DELETE'> = PathWithoutParamsFor<'DELETE'>>(
endpoint: TPath,
params?: undefined,
options?: Omit<RequestInit, 'method'>,
): Promise<Serialized<OperationResult<'DELETE', TPathPattern>>>;
async delete<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathFor<'DELETE'>>(
endpoint: TPath,
_params?: ParamsFor<'DELETE', TPathPattern>,
options: Omit<RequestInit, 'method'> = {},
): Promise<Serialized<OperationResult<'DELETE', TPathPattern>>> {
const response = await this.send(endpoint, 'DELETE', options ?? {});
return response.json();
}
protected getCredentialsAsHeaders(): Record<string, string> {
const credentials = this.getCredentials();
return credentials
? {
'X-User-Id': credentials['X-User-Id'],
'X-Auth-Token': credentials['X-Auth-Token'],
}
: {};
}
send(endpoint: string, method: string, { headers, ...options }: Omit<RequestInit, 'method'> = {}): Promise<Response> {
return fetch(`${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, {
...options,
headers: { ...this.getCredentialsAsHeaders(), ...this.headers, ...headers },
method,
}).then(async (response) => {
if (response.ok) {
return response;
}
if (response.status !== 400) {
return Promise.reject(response);
}
const clone = response.clone();
const error = await clone.json();
if ((isTotpRequiredError(error) || isTotpInvalidError(error)) && hasRequiredTwoFactorMethod(error) && this.twoFactorHandler) {
const method2fa = 'details' in error ? error.details.method : 'password';
const code = await this.twoFactorHandler({
method: method2fa,
emailOrUsername: error.details.emailOrUsername,
invalidAttempt: isTotpInvalidError(error),
});
return this.send(endpoint, method, {
...options,
headers: {
...this.getCredentialsAsHeaders(),
...this.headers,
...headers,
'x-2fa-code': code,
'x-2fa-method': method2fa,
},
});
}
return Promise.reject(response);
});
}
protected getParams(data: Record<string, object | number | string | boolean> | void): string {
return data ? stringify(data, { arrayFormat: 'bracket' }) : '';
}
upload: RestClientInterface['upload'] = (endpoint, params, events, options = {}) => {
if (!params) {
throw new Error('Missing params');
}
const xhr = new XMLHttpRequest();
const data = new FormData();
Object.entries(params as any).forEach(([key, value]) => {
if (value instanceof File) {
data.append(key, value, value.name);
return;
}
value && data.append(key, value as any);
});
xhr.open('POST', `${this.baseUrl}${`/${endpoint}`.replace(/\/+/, '/')}`, true);
Object.entries({ ...this.getCredentialsAsHeaders(), ...options.headers }).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
if (events?.load) {
xhr.upload.addEventListener('load', events.load);
}
if (events?.progress) {
xhr.upload.addEventListener('progress', events.progress);
}
if (events?.error) {
xhr.addEventListener('error', events.error);
}
if (events?.abort) {
xhr.addEventListener('abort', events.abort);
}
xhr.send(data);
return xhr;
};
use(middleware: Middleware<RestClientInterface['send']>): void {
const fn = this.send.bind(this);
this.send = function (this: RestClient, ...context: Parameters<RestClientInterface['send']>): ReturnType<RestClientInterface['send']> {
return middleware(context, pipe(fn));
} as RestClientInterface['send'];
}
handleTwoFactorChallenge(
cb: (args: { method: 'totp' | 'email' | 'password'; emailOrUsername?: string; invalidAttempt?: boolean }) => Promise<string>,
): void {
this.twoFactorHandler = cb;
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "@rocket.chat/tsconfig/client.json",
"compilerOptions": {
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["./src/**/*"]
}