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
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:
2732
packages/api-client/CHANGELOG.md
Normal file
2732
packages/api-client/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
43
packages/api-client/LICENSE
Normal file
43
packages/api-client/LICENSE
Normal 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.
|
||||
163
packages/api-client/__tests__/2fahandling.spec.ts
Normal file
163
packages/api-client/__tests__/2fahandling.spec.ts
Normal 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);
|
||||
});
|
||||
7
packages/api-client/jest.config.ts
Normal file
7
packages/api-client/jest.config.ts
Normal 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;
|
||||
43
packages/api-client/package.json
Normal file
43
packages/api-client/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
4
packages/api-client/src/Credentials.ts
Normal file
4
packages/api-client/src/Credentials.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type Credentials = {
|
||||
'X-User-Id': string;
|
||||
'X-Auth-Token': string;
|
||||
};
|
||||
92
packages/api-client/src/RestClientInterface.ts
Normal file
92
packages/api-client/src/RestClientInterface.ts
Normal 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;
|
||||
}
|
||||
24
packages/api-client/src/errors.ts
Normal file
24
packages/api-client/src/errors.ts
Normal 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)
|
||||
);
|
||||
};
|
||||
305
packages/api-client/src/index.ts
Normal file
305
packages/api-client/src/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
9
packages/api-client/tsconfig.json
Normal file
9
packages/api-client/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@rocket.chat/tsconfig/client.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user