@unito/integration-cli 0.62.4 → 0.62.5

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.
@@ -2,7 +2,7 @@ import * as openUrl from 'openurl';
2
2
  import { Oauth2 } from '../configurationTypes';
3
3
  import { Environment } from '../resources/globalConfiguration';
4
4
  export declare const open: typeof openUrl;
5
- export declare const HTML_ERROR_MSG = "<!doctype html><head><title>Unito</title></head><body style=\"text-align:center\"> An unknown error occured </body>";
5
+ export declare const HTML_ERROR_MSG: (errorMsg?: string) => string;
6
6
  export declare const HTML_SUCCESS_MSG = "<!doctype html><head><title>Unito</title></head><body style=\"text-align:center\"> Redirected to the CLI successfully </body>";
7
7
  export interface Oauth2Credentials {
8
8
  clientId: string;
@@ -59,6 +59,7 @@ declare class OAuth2Service {
59
59
  * @param res The express Response object.
60
60
  */
61
61
  private handleCallback;
62
+ private parseOAuth2Response;
62
63
  /**
63
64
  * Waits for the authorization code to be set.
64
65
  * @returns A promise that resolves when the code is set.
@@ -13,7 +13,8 @@ const errors_1 = require("../errors");
13
13
  const globalConfiguration_1 = require("../resources/globalConfiguration");
14
14
  // It allows to stub openUrl library in the test
15
15
  exports.open = openUrl;
16
- exports.HTML_ERROR_MSG = `<!doctype html><head><title>Unito</title></head><body style="text-align:center"> An unknown error occured </body>`;
16
+ const HTML_ERROR_MSG = (errorMsg = '') => `<!doctype html><head><title>Unito</title></head><body style="text-align:center"> An unknown error occured. ${errorMsg}</body>`;
17
+ exports.HTML_ERROR_MSG = HTML_ERROR_MSG;
17
18
  exports.HTML_SUCCESS_MSG = `<!doctype html><head><title>Unito</title></head><body style="text-align:center"> Redirected to the CLI successfully </body>`;
18
19
  const AUTHORIZATION_RESPONSE_QUERY_PARAMS = ['code', 'state', 'error', 'error_description', 'error_uri'];
19
20
  class OAuth2Service {
@@ -97,7 +98,7 @@ class OAuth2Service {
97
98
  const error = req.query.error;
98
99
  if (error) {
99
100
  res.setHeader('Content-Type', 'text/html');
100
- res.send(exports.HTML_ERROR_MSG);
101
+ res.send((0, exports.HTML_ERROR_MSG)(error));
101
102
  return;
102
103
  }
103
104
  // We keep all the non-standard query parameters of the authorization response
@@ -138,10 +139,7 @@ class OAuth2Service {
138
139
  body: this.encodeBody(tokenRequestPayload, this.requestContentType),
139
140
  method: 'POST',
140
141
  });
141
- if (tokenResponse.status !== 200) {
142
- throw new errors_1.FailedToRetrieveAccessTokenError(await tokenResponse.text());
143
- }
144
- const response = await tokenResponse.json();
142
+ const response = await this.parseOAuth2Response(tokenResponse);
145
143
  this.oauth2Response = {
146
144
  accessToken: response.access_token,
147
145
  refreshToken: response.refresh_token,
@@ -150,13 +148,45 @@ class OAuth2Service {
150
148
  res.send(exports.HTML_SUCCESS_MSG);
151
149
  }
152
150
  catch (error) {
151
+ const err = error;
153
152
  if (!res.headersSent) {
154
153
  res.setHeader('Content-Type', 'text/html');
155
- res.send(exports.HTML_ERROR_MSG);
154
+ res.send((0, exports.HTML_ERROR_MSG)(err?.message));
156
155
  }
157
- throw new errors_1.FailedToRetrieveAccessTokenError(JSON.stringify(error));
156
+ throw new errors_1.FailedToRetrieveAccessTokenError(err?.message, err);
158
157
  }
159
158
  }
159
+ async parseOAuth2Response(response) {
160
+ const contentType = response.headers.get('content-type');
161
+ let responseObj = {};
162
+ // Try JSON first if content-type indicates JSON
163
+ if (contentType?.includes('application/json')) {
164
+ try {
165
+ responseObj = await response.json();
166
+ }
167
+ catch (e) {
168
+ // Fallback to text if JSON parse fails
169
+ console.warn('Failed to parse JSON response, falling back to form-urlencoded');
170
+ }
171
+ }
172
+ else {
173
+ // Handle form-urlencoded
174
+ const text = await response.text();
175
+ try {
176
+ // Try to parse as JSON anyway (some servers send wrong content-type)
177
+ responseObj = JSON.parse(text);
178
+ }
179
+ catch (e) {
180
+ // Parse as form-urlencoded
181
+ responseObj = Object.fromEntries(new URLSearchParams(text));
182
+ }
183
+ }
184
+ if (responseObj.error || response.status !== 200) {
185
+ const errorMsg = responseObj.error_description || responseObj.error;
186
+ throw new errors_1.FailedToRetrieveAccessTokenError(errorMsg, responseObj);
187
+ }
188
+ return responseObj;
189
+ }
160
190
  /**
161
191
  * Waits for the authorization code to be set.
162
192
  * @returns A promise that resolves when the code is set.
@@ -214,17 +244,15 @@ class OAuth2Service {
214
244
  body: this.encodeBody(bodyData, this.requestContentType),
215
245
  method: 'POST',
216
246
  });
217
- if (tokenResponse.status !== 200) {
218
- throw new errors_1.FailedToRetrieveAccessTokenError(await tokenResponse.text());
219
- }
220
- const response = await tokenResponse.json();
247
+ const response = await this.parseOAuth2Response(tokenResponse);
221
248
  return {
222
249
  accessToken: response.access_token,
223
250
  refreshToken: response.refresh_token,
224
251
  };
225
252
  }
226
253
  catch (error) {
227
- throw new errors_1.FailedToRetrieveAccessTokenError(JSON.stringify(error));
254
+ const err = error;
255
+ throw new errors_1.FailedToRetrieveAccessTokenError(err?.message, err);
228
256
  }
229
257
  }
230
258
  /**
@@ -40,7 +40,12 @@ describe('OAuth2Helper', () => {
40
40
  },
41
41
  };
42
42
  oauth2Helper = new oauth2_1.default(authorizationInfo, globalConfiguration_1.Environment.Production, { foo: 'fooValue', bar: 'barValue' });
43
- fetchStub = sinon_1.default.stub().resolves({ json: sinon_1.default.stub().resolves({}), status: 200 });
43
+ fetchStub = sinon_1.default.stub().resolves({
44
+ json: sinon_1.default.stub().resolves({}),
45
+ status: 200,
46
+ text: sinon_1.default.stub().resolves(''),
47
+ headers: new Headers(),
48
+ });
44
49
  sinon_1.default.stub(oauth2Helper, 'startServer').resolves('http://localhost:5050');
45
50
  sinon_1.default.stub(oauth2Helper, 'stopServer');
46
51
  sinon_1.default.replace(global, 'fetch', fetchStub);
@@ -117,21 +122,68 @@ describe('OAuth2Helper', () => {
117
122
  (0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
118
123
  }
119
124
  sinon_1.default.assert.calledOnce(res.send);
120
- sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG);
125
+ sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG('Failed to retrieve access token'));
121
126
  });
122
127
  it('handles errors - response', async () => {
123
128
  const code = 'test-code';
124
129
  const req = { query: { code } };
125
130
  const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub() };
126
- fetchStub.resolves({ status: 500 });
131
+ fetchStub.resolves({
132
+ status: 500,
133
+ headers: new Headers({ 'content-type': 'application/x-www-form-urlencoded' }),
134
+ text: sinon_1.default.stub().resolves(''),
135
+ });
136
+ try {
137
+ await oauth2Helper['handleCallback'](req, res);
138
+ }
139
+ catch (err) {
140
+ (0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
141
+ }
142
+ sinon_1.default.assert.calledOnce(res.send);
143
+ sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG());
144
+ });
145
+ it('handles form-urlencoded error responses', async () => {
146
+ const code = 'test-code';
147
+ const req = { query: { code } };
148
+ const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub(), headersSent: false };
149
+ const errorText = 'error=incorrect_client_credentials&error_description=Invalid+credentials';
150
+ fetchStub.resolves({
151
+ status: 400,
152
+ headers: new Headers({ 'content-type': 'application/x-www-form-urlencoded' }),
153
+ text: sinon_1.default.stub().resolves(errorText),
154
+ });
127
155
  try {
128
156
  await oauth2Helper['handleCallback'](req, res);
157
+ strict_1.default.fail('Should have thrown an error');
129
158
  }
130
159
  catch (err) {
131
160
  (0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
161
+ (0, strict_1.default)(err.message.includes('Invalid credentials'));
132
162
  }
133
163
  sinon_1.default.assert.calledOnce(res.send);
134
- sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG);
164
+ sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG('Invalid credentials'));
165
+ });
166
+ it('handles JSON error responses', async () => {
167
+ const code = 'test-code';
168
+ const req = { query: { code } };
169
+ const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub(), headersSent: false };
170
+ const errorJson = { error: 'invalid_request', error_description: 'Bad request' };
171
+ fetchStub.resolves({
172
+ status: 400,
173
+ headers: new Headers({ 'content-type': 'application/json' }),
174
+ json: sinon_1.default.stub().resolves(errorJson),
175
+ text: sinon_1.default.stub().resolves(JSON.stringify(errorJson)),
176
+ });
177
+ try {
178
+ await oauth2Helper['handleCallback'](req, res);
179
+ strict_1.default.fail('Should have thrown an error');
180
+ }
181
+ catch (err) {
182
+ (0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
183
+ (0, strict_1.default)(err.message.includes('Bad request'));
184
+ }
185
+ sinon_1.default.assert.calledOnce(res.send);
186
+ sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG('Bad request'));
135
187
  });
136
188
  });
137
189
  describe('updateToken', () => {
@@ -139,7 +191,12 @@ describe('OAuth2Helper', () => {
139
191
  const refreshToken = 'test-refresh-token';
140
192
  const accessToken = 'test-access-token';
141
193
  const fetchResponse = { access_token: accessToken, refresh_token: refreshToken };
142
- fetchStub.resolves({ status: 200, json: sinon_1.default.stub().resolves(fetchResponse) });
194
+ fetchStub.resolves({
195
+ status: 200,
196
+ json: sinon_1.default.stub().resolves(fetchResponse),
197
+ text: sinon_1.default.stub().resolves(''),
198
+ headers: new Headers({ 'content-type': 'application/json' }),
199
+ });
143
200
  const result = await oauth2Helper.updateToken(refreshToken);
144
201
  strict_1.default.deepEqual(result, { accessToken, refreshToken });
145
202
  sinon_1.default.assert.calledOnce(fetchStub);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.62.4",
2
+ "version": "0.62.5",
3
3
  "commands": {
4
4
  "activity": {
5
5
  "id": "activity",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-cli",
3
- "version": "0.62.4",
3
+ "version": "0.62.5",
4
4
  "description": "Integration CLI",
5
5
  "bin": {
6
6
  "integration-cli": "./bin/run"