@unito/integration-cli 0.62.3 → 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,12 +139,7 @@ class OAuth2Service {
138
139
  body: this.encodeBody(tokenRequestPayload, this.requestContentType),
139
140
  method: 'POST',
140
141
  });
141
- if (tokenResponse.status !== 200) {
142
- res.setHeader('Content-Type', 'text/html');
143
- res.send(exports.HTML_ERROR_MSG);
144
- return;
145
- }
146
- const response = await tokenResponse.json();
142
+ const response = await this.parseOAuth2Response(tokenResponse);
147
143
  this.oauth2Response = {
148
144
  accessToken: response.access_token,
149
145
  refreshToken: response.refresh_token,
@@ -152,11 +148,44 @@ class OAuth2Service {
152
148
  res.send(exports.HTML_SUCCESS_MSG);
153
149
  }
154
150
  catch (error) {
151
+ const err = error;
155
152
  if (!res.headersSent) {
156
153
  res.setHeader('Content-Type', 'text/html');
157
- res.send(exports.HTML_ERROR_MSG);
154
+ res.send((0, exports.HTML_ERROR_MSG)(err?.message));
155
+ }
156
+ throw new errors_1.FailedToRetrieveAccessTokenError(err?.message, err);
157
+ }
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');
158
170
  }
159
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;
160
189
  }
161
190
  /**
162
191
  * Waits for the authorization code to be set.
@@ -215,17 +244,15 @@ class OAuth2Service {
215
244
  body: this.encodeBody(bodyData, this.requestContentType),
216
245
  method: 'POST',
217
246
  });
218
- if (tokenResponse.status !== 200) {
219
- throw new errors_1.FailedToRetrieveAccessTokenError(await tokenResponse.text());
220
- }
221
- const response = await tokenResponse.json();
247
+ const response = await this.parseOAuth2Response(tokenResponse);
222
248
  return {
223
249
  accessToken: response.access_token,
224
250
  refreshToken: response.refresh_token,
225
251
  };
226
252
  }
227
253
  catch (error) {
228
- throw new errors_1.FailedToRetrieveAccessTokenError(JSON.stringify(error));
254
+ const err = error;
255
+ throw new errors_1.FailedToRetrieveAccessTokenError(err?.message, err);
229
256
  }
230
257
  }
231
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);
@@ -110,18 +115,75 @@ describe('OAuth2Helper', () => {
110
115
  const req = { query: { code } };
111
116
  const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub() };
112
117
  fetchStub.rejects(new Error('Failed to retrieve access token'));
113
- await oauth2Helper['handleCallback'](req, res);
118
+ try {
119
+ await oauth2Helper['handleCallback'](req, res);
120
+ }
121
+ catch (err) {
122
+ (0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
123
+ }
114
124
  sinon_1.default.assert.calledOnce(res.send);
115
- 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'));
116
126
  });
117
127
  it('handles errors - response', async () => {
118
128
  const code = 'test-code';
119
129
  const req = { query: { code } };
120
130
  const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub() };
121
- fetchStub.resolves({ status: 500 });
122
- await oauth2Helper['handleCallback'](req, res);
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
+ }
123
142
  sinon_1.default.assert.calledOnce(res.send);
124
- sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG);
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
+ });
155
+ try {
156
+ await oauth2Helper['handleCallback'](req, res);
157
+ strict_1.default.fail('Should have thrown an error');
158
+ }
159
+ catch (err) {
160
+ (0, strict_1.default)(err instanceof errors_1.FailedToRetrieveAccessTokenError);
161
+ (0, strict_1.default)(err.message.includes('Invalid credentials'));
162
+ }
163
+ sinon_1.default.assert.calledOnce(res.send);
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'));
125
187
  });
126
188
  });
127
189
  describe('updateToken', () => {
@@ -129,7 +191,12 @@ describe('OAuth2Helper', () => {
129
191
  const refreshToken = 'test-refresh-token';
130
192
  const accessToken = 'test-access-token';
131
193
  const fetchResponse = { access_token: accessToken, refresh_token: refreshToken };
132
- 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
+ });
133
200
  const result = await oauth2Helper.updateToken(refreshToken);
134
201
  strict_1.default.deepEqual(result, { accessToken, refreshToken });
135
202
  sinon_1.default.assert.calledOnce(fetchStub);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.62.3",
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.3",
3
+ "version": "0.62.5",
4
4
  "description": "Integration CLI",
5
5
  "bin": {
6
6
  "integration-cli": "./bin/run"