@epsbv/oauth-sdk 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/error.ts ADDED
@@ -0,0 +1,82 @@
1
+ export class FetchError extends Error {
2
+ constructor(cause: unknown) {
3
+ super("Failed to send request", {
4
+ cause
5
+ });
6
+ }
7
+ }
8
+
9
+ export class RequestError extends Error {
10
+ public code: string;
11
+ public description: string | null;
12
+ public uri: string | null;
13
+ public state: string | null;
14
+
15
+ constructor(code: string, description: string | null, uri: string | null, state: string | null) {
16
+ super(`OAuth request error: ${code}`);
17
+
18
+ this.code = code;
19
+ this.description = description;
20
+ this.uri = uri;
21
+ this.state = state;
22
+ }
23
+
24
+ public static create(result: object) : RequestError{
25
+ let code: string;
26
+
27
+ if ("error" in result && typeof result.error === "string") {
28
+ code = result.error;
29
+ }
30
+ else {
31
+ throw new Error("Invalid error response");
32
+ }
33
+
34
+ let description: string | null = null;
35
+ let uri: string | null = null;
36
+ let state: string | null = null;
37
+
38
+ if ("error_description" in result) {
39
+ if (typeof result.error_description !== "string") {
40
+ throw new Error("Invalid data");
41
+ }
42
+ description = result.error_description;
43
+ }
44
+
45
+ if ("error_uri" in result) {
46
+ if (typeof result.error_uri !== "string") {
47
+ throw new Error("Invalid data");
48
+ }
49
+
50
+ uri = result.error_uri;
51
+ }
52
+ if ("state" in result) {
53
+ if (typeof result.state !== "string") {
54
+ throw new Error("Invalid data");
55
+ }
56
+
57
+ state = result.state;
58
+ }
59
+
60
+ return new RequestError(code, description, uri, state)
61
+ }
62
+ }
63
+
64
+ export class UnexpectedResponseError extends Error {
65
+ public status: number;
66
+
67
+ constructor(responseStatus: number) {
68
+ super("Unexpected error response");
69
+ this.status = responseStatus;
70
+ }
71
+ }
72
+
73
+ export class UnexpectedErrorResponseBodyError extends Error {
74
+ public status: number;
75
+ public data: unknown;
76
+
77
+ constructor(status: number, data: unknown) {
78
+ super("Unexpected error response body");
79
+ this.status = status;
80
+ this.data = data;
81
+ }
82
+ }
@@ -0,0 +1,74 @@
1
+ import { Device, Identity } from "./types";
2
+ import { ApiClient } from "../client";
3
+ import { ClientOptions, Language } from "../types";
4
+ import { apiUrl } from "../const";
5
+
6
+ export * from './types'
7
+
8
+ export class IdentityClient extends ApiClient {
9
+ constructor({ token }: Omit<ClientOptions, 'baseUrl'>) {
10
+ super({ token, baseUrl: apiUrl })
11
+ }
12
+
13
+ get info() {
14
+ return new Identity$Info(this.options)
15
+ }
16
+
17
+ get avatar() {
18
+ return new Identity$Avatar(this.options)
19
+ }
20
+
21
+ get devices() {
22
+ return new Identity$Devices(this.options)
23
+ }
24
+ }
25
+
26
+ class Identity$Info extends ApiClient {
27
+ constructor(options: ClientOptions) {
28
+ super(options)
29
+ }
30
+
31
+ async get() {
32
+ return await this.fetchUrlEncoded<Identity>('/e/identity', 'GET')
33
+ }
34
+
35
+ async delete() {
36
+ return await this.fetchUrlEncoded('/e/identity', 'DELETE')
37
+ }
38
+
39
+ async update(body: { first_name: string, last_name: string, language: Language }): Promise<{}> {
40
+ return await this.fetchUrlEncoded('/e/identity', 'PUT', { body })
41
+ }
42
+ }
43
+
44
+ class Identity$Avatar extends ApiClient {
45
+ constructor(options: ClientOptions) {
46
+ super(options)
47
+ }
48
+
49
+ async get() {
50
+ return await this.fetchUrlEncoded<string>('/e/identity/avatar', 'GET')
51
+ }
52
+
53
+ async delete() {
54
+ return await this.fetchUrlEncoded('/e/identity/avatar', 'DELETE')
55
+ }
56
+
57
+ async update(body: { avatar: File }): Promise<{}> {
58
+ return await this.fetchMultipart('/e/identity/avatar', 'PUT', { body })
59
+ }
60
+ }
61
+
62
+ class Identity$Devices extends ApiClient {
63
+ constructor(options: ClientOptions) {
64
+ super(options)
65
+ }
66
+
67
+ async get() {
68
+ return await this.fetchUrlEncoded<Device[]>('/e/identity/devices', 'GET')
69
+ }
70
+
71
+ async delete({ id }: { id: string }) {
72
+ return await this.fetchUrlEncoded(`/e/identity/devices/${id}`, 'DELETE')
73
+ }
74
+ }
@@ -0,0 +1,52 @@
1
+ import { Language, Status } from "../types"
2
+
3
+ // export enum OrginizationType {
4
+ // Admin = "admin",
5
+ // Distributor = "distributor",
6
+ // Builder = "builder"
7
+ // }
8
+
9
+ // export enum OrganizationRole {
10
+ // Ceo = "ceo",
11
+ // Admin = "admin",
12
+ // Member = "member"
13
+ // }
14
+
15
+ // export interface Originization {
16
+ // id: string
17
+ // name: string
18
+ // type: OrginizationType
19
+ // role: OrganizationRole
20
+ // email: string
21
+ // avatar: string | undefined
22
+ // status: Status
23
+ // parent: string,
24
+ // language: Language,
25
+ // created_at: Date,
26
+ // updated_at: Date,
27
+ // }
28
+
29
+ export interface Identity {
30
+ uid: string,
31
+ admin: boolean,
32
+ email: string,
33
+ avatar?: string,
34
+ first_name: string,
35
+ last_name: string,
36
+ language: Language,
37
+
38
+ provider: 'google' | 'internal' | 'microsoft' | 'apple',
39
+ provider_id: string | undefined
40
+
41
+ created_at: Date,
42
+ updated_at: Date,
43
+ verified_at?: Date,
44
+ }
45
+
46
+ export interface Device {
47
+ id: string,
48
+ name: string,
49
+ type: string,
50
+ created_at: Date,
51
+ activity_at: Date
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * as oauth from './oauth'
2
+ export * as tenant from './tenant'
3
+ export * as identity from './identity'
4
+ export * from './types'
@@ -0,0 +1,137 @@
1
+ import { OAuthTokens, OAuthUrlOptions, type OAuthClientOptions } from "./types";
2
+ import { FetchMethod, FetchOptions, fetchUrlEncoded } from "../utils/fetch";
3
+ import { apiUrl, oauthUrl } from "../const";
4
+ import { encodeCredentials } from "../utils/secret";
5
+
6
+
7
+ export { generateState, generateCodeVerifier, createS256CodeChallenge } from '../utils/secret'
8
+
9
+ const authorizationEndpoint = `${oauthUrl}/oauth/auth`;
10
+
11
+ export class OAuthClient {
12
+ private clientId: string;
13
+ private redirectURI: string;
14
+ private credentials: string
15
+
16
+ constructor({ clientId, clientSecret, redirectURI }: OAuthClientOptions) {
17
+ this.clientId = clientId;
18
+ this.redirectURI = redirectURI;
19
+
20
+ this.credentials = encodeCredentials(clientId, clientSecret);
21
+ }
22
+
23
+ // Generate an authorization URL
24
+ public createAuthorizationURL(options: OAuthUrlOptions): URL {
25
+ return this.createAuthorizationURLWithPKCE(options)
26
+ }
27
+
28
+ private createAuthorizationURLWithPKCE({ state, code_challenge_method, code_challenge, scope, response_type, access_type, include_granted_scopes, prompt }: OAuthUrlOptions) {
29
+ const url = new URL(authorizationEndpoint);
30
+
31
+ url.searchParams.set('state', state)
32
+ url.searchParams.set('client_id', this.clientId);
33
+ url.searchParams.set('access_type', access_type ?? 'online'); // determines 'refresh' or 'access' token
34
+ url.searchParams.set('response_type', response_type); // determines if parameters returned are in fragment identifier or search params
35
+ url.searchParams.set('redirect_uri', this.redirectURI);
36
+
37
+ if (prompt) {
38
+ url.searchParams.set('prompt', prompt);
39
+ }
40
+
41
+ if (include_granted_scopes != undefined) {
42
+ url.searchParams.set('include_granted_scopes', `${include_granted_scopes}`);
43
+ }
44
+
45
+ if (code_challenge_method) {
46
+ url.searchParams.set('code_challenge_method', code_challenge_method)
47
+
48
+ if (code_challenge) {
49
+ url.searchParams.set('code_challenge', code_challenge)
50
+ }
51
+ }
52
+
53
+ if (scope.length > 0) {
54
+ url.searchParams.set('scope', scope.join(' '))
55
+ }
56
+
57
+ return url;
58
+ }
59
+
60
+ public async getToken(code: string, codeVerifier?: string) {
61
+ // Retreive the token from the OAuth server
62
+ try {
63
+ return await this.fetch<OAuthTokens>('/e/oauth/token', 'POST', {
64
+ body: {
65
+ grant_type: 'authorization_code',
66
+ code,
67
+ redirect_uri: this.redirectURI,
68
+ code_verifier: codeVerifier
69
+ }
70
+ })
71
+
72
+ // const { header } = jwt.decode(token, { complete: true })
73
+ // const { keys } = await this.fetch<Jwks>('/oauth/jwks', 'GET')
74
+
75
+ // const certificate = keys[0];
76
+
77
+ // console.log(keys)
78
+
79
+ // if (certificate) {
80
+ // jwt.verify(
81
+ // token,
82
+ // `-----BEGIN PUBLIC KEY-----\n${certificate.x5c[0]}\n-----END PUBLIC KEY-----`, {
83
+ // algorithms: [certificate.alg]
84
+ // })
85
+
86
+ // }
87
+ }
88
+ catch (error) {
89
+ throw new Error(error)
90
+ }
91
+ }
92
+
93
+ public async refreshAccessToken(refresh_token: string) {
94
+ try {
95
+ return await this.fetch<OAuthTokens>('/e/oauth/token', 'POST', {
96
+ body: {
97
+ grant_type: 'refresh_token',
98
+ refresh_token: refresh_token,
99
+ }
100
+ })
101
+
102
+ // const { header } = jwt.decode(token, { complete: true })
103
+ // const { keys } = await this.fetch<Jwks>('/oauth/jwks', 'GET')
104
+
105
+ // const certificate = keys[0];
106
+
107
+ // console.log(keys)
108
+
109
+ // if (certificate) {
110
+ // jwt.verify(
111
+ // token,
112
+ // `-----BEGIN PUBLIC KEY-----\n${certificate.x5c[0]}\n-----END PUBLIC KEY-----`, {
113
+ // algorithms: [certificate.alg]
114
+ // })
115
+
116
+ // }
117
+ }
118
+ catch (error) {
119
+ throw new Error(error)
120
+ }
121
+ }
122
+
123
+ public async revokeToken(token: string) {
124
+ await fetchUrlEncoded(`${apiUrl}/e/oauth/revoke`, 'POST', {
125
+ body: { token }
126
+ })
127
+ }
128
+
129
+ async fetch<T>(endpoint: string, method: FetchMethod, params?: FetchOptions): Promise<T> {
130
+ const headers = new Headers(params?.headers ?? {});
131
+
132
+ headers.append('User-Agent', 'eps/oauth-client')
133
+ headers.append('Authorization', `Bearer ${this.credentials}`)
134
+
135
+ return await fetchUrlEncoded<T>(`${apiUrl}${endpoint}`, method, { body: params?.body ?? null, headers });
136
+ }
137
+ }
@@ -0,0 +1,114 @@
1
+ import { Algorithm } from "jsonwebtoken";
2
+
3
+ export type OAuthScope = 'email' | 'profile'
4
+
5
+ export interface OAuthClientOptions {
6
+ clientId: string,
7
+ clientSecret: string,
8
+ redirectURI: string
9
+ }
10
+
11
+ export interface OAuthUrlOptions {
12
+ /**
13
+ * Recommended. Indicates whether your application can refresh access tokens
14
+ * when the user is not present at the browser. Valid parameter values are
15
+ * 'online', which is the default value, and 'offline'. Set the value to
16
+ * 'offline' if your application needs to refresh access tokens when the user
17
+ * is not present at the browser. This value instructs the Google
18
+ * authorization server to return a refresh token and an access token the
19
+ * first time that your application exchanges an authorization code for
20
+ * tokens.
21
+ */
22
+ access_type?: 'online' | 'offline';
23
+ /**
24
+ * Defaults back to 'code''.
25
+ */
26
+ response_type: 'code' | 'token';
27
+ /**
28
+ * Required. A space-delimited list of scopes that identify the resources that
29
+ * your application could access on the user's behalf. These values inform the
30
+ * consent screen that Google displays to the user. Scopes enable your
31
+ * application to only request access to the resources that it needs while
32
+ * also enabling users to control the amount of access that they grant to your
33
+ * application. Thus, there is an inverse relationship between the number of
34
+ * scopes requested and the likelihood of obtaining user consent. The
35
+ * OAuth 2.0 API Scopes document provides a full list of scopes that you might
36
+ * use to access Google APIs. We recommend that your application request
37
+ * access to authorization scopes in context whenever possible. By requesting
38
+ * access to user data in context, via incremental authorization, you help
39
+ * users to more easily understand why your application needs the access it is
40
+ * requesting.
41
+ */
42
+ scope?: OAuthScope[];
43
+ /**
44
+ * Recommended. Specifies any string value that your application uses to
45
+ * maintain state between your authorization request and the authorization
46
+ * server's response. The server returns the exact value that you send as a
47
+ * name=value pair in the hash (#) fragment of the 'redirect_uri' after the
48
+ * user consents to or denies your application's access request. You can use
49
+ * this parameter for several purposes, such as directing the user to the
50
+ * correct resource in your application, sending nonces, and mitigating
51
+ * cross-site request forgery. Since your redirect_uri can be guessed, using a
52
+ * state value can increase your assurance that an incoming connection is the
53
+ * result of an authentication request. If you generate a random string or
54
+ * encode the hash of a cookie or another value that captures the client's
55
+ * state, you can validate the response to additionally ensure that the
56
+ * request and response originated in the same browser, providing protection
57
+ * against attacks such as cross-site request forgery. See the OpenID Connect
58
+ * documentation for an example of how to create and confirm a state token.
59
+ */
60
+ state?: string;
61
+ /**
62
+ * Optional. Enables applications to use incremental authorization to request
63
+ * access to additional scopes in context. If you set this parameter's value
64
+ * to true and the authorization request is granted, then the new access token
65
+ * will also cover any scopes to which the user previously granted the
66
+ * application access. See the incremental authorization section for examples.
67
+ */
68
+ include_granted_scopes?: boolean;
69
+ /**
70
+ * Optional. A space-delimited, case-sensitive list of prompts to present the
71
+ * user. If you don't specify this parameter, the user will be prompted only
72
+ * the first time your app requests access. Possible values are:
73
+ *
74
+ * 'none' - Donot display any authentication or consent screens. Must not be
75
+ * specified with other values.
76
+ * 'consent' - Prompt the user for consent.
77
+ * 'select_account' - Prompt the user to select an account.
78
+ */
79
+ prompt?: 'consent' | 'select_account';
80
+ /**
81
+ * Recommended. Specifies what method was used to encode a 'code_verifier'
82
+ * that will be used during authorization code exchange. This parameter must
83
+ * be used with the 'code_challenge' parameter. The value of the
84
+ * 'code_challenge_method' defaults to "plain" if not present in the request
85
+ * that includes a 'code_challenge'. The only supported values for this
86
+ * parameter are "S256" or "plain".
87
+ */
88
+ code_challenge_method?: 'S256' | 'plain';
89
+ /**
90
+ * Recommended. Specifies an encoded 'code_verifier' that will be used as a
91
+ * server-side challenge during authorization code exchange. This parameter
92
+ * must be used with the 'code_challenge' parameter described above.
93
+ */
94
+ code_challenge?: string;
95
+ }
96
+
97
+ export interface OAuthTokens {
98
+ access_token: string,
99
+ refresh_token?: string
100
+ }
101
+
102
+ export interface Jwks {
103
+ keys: Jwk[]
104
+ }
105
+
106
+ export interface Jwk {
107
+ alg: Algorithm,
108
+ e: string,
109
+ kid: string,
110
+ kty: string
111
+ n: string,
112
+ use: string
113
+ x5c: string[]
114
+ }
@@ -0,0 +1,211 @@
1
+ import { ApiClient } from "../client";
2
+ import { apiUrl } from "../const";
3
+ import { ClientOptions, Language } from "../types";
4
+ import { Client, ClientType, Tenant, TenantRole, TenantUser } from "./types";
5
+
6
+ export * from './types'
7
+
8
+ // interface List<T> {
9
+ // page: number,
10
+ // page_count: number
11
+ // items: T[]
12
+ // }
13
+
14
+ export class TenantClient extends ApiClient {
15
+ constructor({ token }: Omit<ClientOptions, 'baseUrl'>) {
16
+ super({ token, baseUrl: apiUrl })
17
+ }
18
+
19
+ get info() {
20
+ return new Tenant$Info(this.options)
21
+ }
22
+
23
+ get avatar() {
24
+ return new Tenant$Avatar(this.options)
25
+ }
26
+
27
+ get users() {
28
+ return new Tenant$Users(this.options)
29
+ }
30
+
31
+ get partners() {
32
+ return new Tenant$Partners(this.options)
33
+ }
34
+
35
+ // get clients() {
36
+ // return null
37
+ // }
38
+
39
+
40
+
41
+
42
+
43
+
44
+ // async info() {
45
+ // return await this.fetchUrlEncoded<Tenant>('/e/tenant/info', 'GET')
46
+ // }
47
+
48
+ // async role() {
49
+ // return await this.fetchUrlEncoded<Tenant>('/tenant/role', 'GET')
50
+ // }
51
+
52
+ // async children(page?: number) {
53
+ // return await this.fetchUrlEncoded<List<Tenant>>(`/tenant/children?page=${page ?? 0}`, 'GET')
54
+ // }
55
+
56
+ // members() {
57
+ // return {
58
+ // list: () => this.fetchUrlEncoded<TenantMember[]>('/tenant/members', 'GET'),
59
+ // get: (uid: string) => this.fetchUrlEncoded<TenantMember>(`/tenant/members/${uid}`, 'GET'),
60
+ // }
61
+ // }
62
+
63
+ // clients() {
64
+ // return {
65
+ // list: () => this.fetchUrlEncoded<Client[]>('/tenant/clients', 'GET'),
66
+ // get: (id: string) => this.fetchUrlEncoded<Client>(`/tenant/clients/${id}`, 'GET'),
67
+ // // delete: (id: string) => this.fetch(`/tenant/clients/${id}`, 'DELETE'),
68
+ // // create: (name: string, type: ClientType, redirect_uri: string) => this.fetch<Client>(`/tenant/clients`, 'POST', { body: { name, type, redirect_uri } }),
69
+ // // update: (id: string, body: { name: string, redirect_uri: string }) => this.fetch<any>(`/tenant/clients/${id}`, 'PUT', { body }),
70
+ // }
71
+ // }
72
+ }
73
+
74
+ class Tenant$Partners extends ApiClient {
75
+ constructor(options: ClientOptions) {
76
+ super(options)
77
+ }
78
+
79
+ get users() {
80
+ return new Tenant$Partners$Users(this.options)
81
+ }
82
+
83
+ get avatar() {
84
+ return new Tenant$Partners$Avatar(this.options)
85
+ }
86
+
87
+ async list() {
88
+ return await this.fetchUrlEncoded<Tenant[]>(`/e/tenant/partners`, 'GET')
89
+ }
90
+
91
+ async get(params: { id: string }) {
92
+ return await this.fetchUrlEncoded<Tenant>(`/e/tenant/partners/${params.id}`, 'GET')
93
+ }
94
+
95
+ // async create(params: { email: string, org_name: string, org_email: string, org_language: Language }) {
96
+ // return await this.fetchUrlEncoded<Tenant>(`/e/tenant/partners`, 'POST', {
97
+ // body: params
98
+ // })
99
+ // }
100
+
101
+ async update(params: { id: string, data: { name: string, email: string, language: Language } }) {
102
+ return await this.fetchUrlEncoded(`/e/tenant/partners/${params.id}`, 'PUT', {
103
+ body: params.data
104
+ })
105
+ }
106
+
107
+ async delete(params: { id: string }) {
108
+ return await this.fetchUrlEncoded(`/e/tenant/partners/${params.id}`, 'DELETE')
109
+ }
110
+
111
+ }
112
+
113
+ class Tenant$Partners$Avatar extends ApiClient {
114
+ constructor(options: ClientOptions) {
115
+ super(options)
116
+ }
117
+
118
+ async get(params: { id: string }) {
119
+ return await this.fetchUrlEncoded<string>(`/e/tenant/partners/${params.id}/avatar`, 'GET')
120
+ }
121
+
122
+ async delete(params: { id: string }) {
123
+ return await this.fetchUrlEncoded(`/e/tenant/partners/${params.id}/avatar`, 'DELETE')
124
+ }
125
+
126
+ async update(params: { id: string, data: { avatar: File } }): Promise<{}> {
127
+ return await this.fetchMultipart(`/e/tenant/partners/${params.id}/avatar`, 'PUT', { body: params.data })
128
+ }
129
+ }
130
+
131
+ class Tenant$Partners$Users extends ApiClient {
132
+ constructor(options: ClientOptions) {
133
+ super(options)
134
+ }
135
+
136
+ async list(params: { id: string }) {
137
+ return await this.fetchUrlEncoded<TenantUser>(`/e/tenant/partners/${params.id}/users`, 'GET')
138
+ }
139
+
140
+ async get(params: { id: string, uid: string }) {
141
+ return await this.fetchUrlEncoded<TenantUser>(`/e/tenant/partners/${params.id}/users/${params.uid}`, 'GET')
142
+ }
143
+
144
+ async update(params: { id: string, uid: string, data: { first_name: string, last_name: string, role: TenantRole, language: Language } }) {
145
+ return await this.fetchUrlEncoded(`/e/tenant/partners/${params.id}/users/${params.uid}`, 'PUT', {
146
+ body: params.data
147
+ })
148
+ }
149
+
150
+ // async delete(params: { id: string, uid: string }) {
151
+ // return await this.fetchUrlEncoded(`/e/tenant/partners/${params.id}/users/${params.uid}`, 'DELETE')
152
+ // }
153
+ }
154
+
155
+ class Tenant$Users extends ApiClient {
156
+ constructor(options: ClientOptions) {
157
+ super(options)
158
+ }
159
+
160
+ async list() {
161
+ return await this.fetchUrlEncoded<TenantUser>(`/e/tenant/users`, 'GET')
162
+ }
163
+
164
+ async get(params: { uid: string }) {
165
+ return await this.fetchUrlEncoded<TenantUser>(`/e/tenant/users/${params.uid}`, 'GET')
166
+ }
167
+
168
+ async update(params: { uid: string, data: { first_name: string, last_name: string, role: TenantRole, language: Language } }) {
169
+ return await this.fetchUrlEncoded(`/e/tenant/users/${params.uid}`, 'PUT', {
170
+ body: params.data
171
+ })
172
+ }
173
+
174
+ async delete(params: { uid: string }) {
175
+ return await this.fetchUrlEncoded(`/e/tenant/users/${params.uid}`, 'DELETE')
176
+ }
177
+ }
178
+
179
+ class Tenant$Info extends ApiClient {
180
+ constructor(options: ClientOptions) {
181
+ super(options)
182
+ }
183
+
184
+ async get() {
185
+ return await this.fetchUrlEncoded<Tenant>(`/e/tenant`, 'GET')
186
+ }
187
+
188
+ async update(data: { name: string, email: string, language: Language }) {
189
+ await this.fetchUrlEncoded(`/e/tenant`, 'PUT', {
190
+ body: data
191
+ })
192
+ }
193
+ }
194
+
195
+ class Tenant$Avatar extends ApiClient {
196
+ constructor(options: ClientOptions) {
197
+ super(options)
198
+ }
199
+
200
+ async get() {
201
+ return await this.fetchUrlEncoded<string>('/e/tenant/avatar', 'GET')
202
+ }
203
+
204
+ async delete() {
205
+ return await this.fetchUrlEncoded('/e/tenant/avatar', 'DELETE')
206
+ }
207
+
208
+ async update(body: { avatar: File }): Promise<{}> {
209
+ return await this.fetchMultipart('/e/tenant/avatar', 'PUT', { body })
210
+ }
211
+ }