@unito/integration-cli 1.0.3 → 1.2.0

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.
@@ -1731,9 +1731,9 @@
1731
1731
  }
1732
1732
  },
1733
1733
  "node_modules/brace-expansion": {
1734
- "version": "1.1.12",
1735
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
1736
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
1734
+ "version": "1.1.13",
1735
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
1736
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
1737
1737
  "dev": true,
1738
1738
  "license": "MIT",
1739
1739
  "dependencies": {
@@ -3205,9 +3205,9 @@
3205
3205
  }
3206
3206
  },
3207
3207
  "node_modules/path-to-regexp": {
3208
- "version": "8.3.0",
3209
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
3210
- "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
3208
+ "version": "8.4.0",
3209
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz",
3210
+ "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==",
3211
3211
  "license": "MIT",
3212
3212
  "funding": {
3213
3213
  "type": "opencollective",
@@ -7,7 +7,7 @@
7
7
  "scripts": {
8
8
  "compile": "tsc --project tsconfig.build.json",
9
9
  "compile:watch": "tsc -w",
10
- "dev": "tsx watch src/index.ts",
10
+ "dev": "NODE_ENV=development tsx watch src/index.ts",
11
11
  "test": "NODE_ENV=test tsx --test --test-name-pattern=${ONLY:-.*} $(find test -type f -name '*.test.ts')",
12
12
  "lint": "eslint --config eslint.config.mjs --fix src test && prettier --write src test",
13
13
  "ci:test": "npm run test"
@@ -202,6 +202,12 @@
202
202
  },
203
203
  "responseContentType": {
204
204
  "$ref": "#/$defs/authorizationResponseContentType"
205
+ },
206
+ "pkceCodeChallengeMethod": {
207
+ "type": "string",
208
+ "description": "The PKCE code challenge method (RFC 7636). When set, the OAuth2 flow will use Proof Key for Code Exchange.",
209
+ "enum": ["S256", "plain"],
210
+ "tsEnumNames": ["S256", "PLAIN"]
205
211
  }
206
212
  }
207
213
  }
@@ -30,7 +30,6 @@ class Oauth2 extends core_1.Command {
30
30
  description: 'the environment of the platform',
31
31
  options: Object.values(globalConfiguration_1.Environment),
32
32
  default: globalConfiguration_1.Environment.Production,
33
- hidden: true,
34
33
  })(),
35
34
  reauth: core_1.Flags.boolean({
36
35
  description: `triggers a new oauth2 flow to overwrite the test account's current credentials`,
@@ -28,9 +28,8 @@ class Publish extends baseCommand_1.BaseCommand {
28
28
  default: GlobalConfiguration.Environment.Production,
29
29
  })(),
30
30
  'registry-only': core_1.Flags.boolean({
31
- description: '(advanced) only update the registry',
31
+ description: 'only update the registry without publishing code',
32
32
  default: false,
33
- hidden: true,
34
33
  exclusive: ['preview', 'live-preview'],
35
34
  }),
36
35
  preview: core_1.Flags.boolean({
@@ -168,6 +168,7 @@ export interface Oauth2 {
168
168
  refreshRequestParameters?: AuthorizationRequestParameters;
169
169
  requestContentType?: RequestContentType;
170
170
  responseContentType?: RequestContentType;
171
+ pkceCodeChallengeMethod?: PkceCodeChallengeMethod;
171
172
  }
172
173
  /**
173
174
  * The extra information used to communicate with the provider
@@ -209,6 +210,13 @@ export declare enum RequestContentType {
209
210
  URL_ENCODED = "application/x-www-form-urlencoded",
210
211
  JSON = "application/json"
211
212
  }
213
+ /**
214
+ * The PKCE code challenge method (RFC 7636). When set, the OAuth2 flow will use Proof Key for Code Exchange.
215
+ */
216
+ export declare enum PkceCodeChallengeMethod {
217
+ S256 = "S256",
218
+ PLAIN = "plain"
219
+ }
212
220
  export declare enum Type {
213
221
  BOOLEAN = "boolean",
214
222
  NUMBER = "number",
@@ -6,7 +6,7 @@
6
6
  * and run json-schema-to-typescript to regenerate this file.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.Format = exports.Type = exports.RequestContentType = exports.GrantType = exports.Method = void 0;
9
+ exports.Format = exports.Type = exports.PkceCodeChallengeMethod = exports.RequestContentType = exports.GrantType = exports.Method = void 0;
10
10
  /**
11
11
  * The method of authorization
12
12
  */
@@ -33,6 +33,14 @@ var RequestContentType;
33
33
  RequestContentType["URL_ENCODED"] = "application/x-www-form-urlencoded";
34
34
  RequestContentType["JSON"] = "application/json";
35
35
  })(RequestContentType || (exports.RequestContentType = RequestContentType = {}));
36
+ /**
37
+ * The PKCE code challenge method (RFC 7636). When set, the OAuth2 flow will use Proof Key for Code Exchange.
38
+ */
39
+ var PkceCodeChallengeMethod;
40
+ (function (PkceCodeChallengeMethod) {
41
+ PkceCodeChallengeMethod["S256"] = "S256";
42
+ PkceCodeChallengeMethod["PLAIN"] = "plain";
43
+ })(PkceCodeChallengeMethod || (exports.PkceCodeChallengeMethod = PkceCodeChallengeMethod = {}));
36
44
  var Type;
37
45
  (function (Type) {
38
46
  Type["BOOLEAN"] = "boolean";
@@ -41,6 +41,8 @@ declare class OAuth2Service {
41
41
  private refreshRequestParameters;
42
42
  private credentialPayload;
43
43
  private legacyRedirectUrl;
44
+ private pkceCodeChallengeMethod;
45
+ private codeVerifier;
44
46
  /**
45
47
  * Constructs an instance of OAuthHelper.
46
48
  * @param clientId The client ID for your OAuth application.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HTML_SUCCESS_MSG = exports.HTML_ERROR_MSG = exports.open = void 0;
4
4
  const tslib_1 = require("tslib");
5
+ const node_crypto_1 = tslib_1.__importDefault(require("node:crypto"));
5
6
  const express_1 = tslib_1.__importDefault(require("express"));
6
7
  const cors_1 = tslib_1.__importDefault(require("cors"));
7
8
  const openUrl = tslib_1.__importStar(require("openurl"));
@@ -33,6 +34,8 @@ class OAuth2Service {
33
34
  refreshRequestParameters;
34
35
  credentialPayload;
35
36
  legacyRedirectUrl;
37
+ pkceCodeChallengeMethod;
38
+ codeVerifier;
36
39
  /**
37
40
  * Constructs an instance of OAuthHelper.
38
41
  * @param clientId The client ID for your OAuth application.
@@ -42,7 +45,7 @@ class OAuth2Service {
42
45
  * @param providerTokenUrl The URL for the token endpoint of the provider.
43
46
  */
44
47
  constructor(authorizationInfo, environment = globalConfiguration_1.Environment.Production, credentialPayload) {
45
- const { clientId, clientSecret, authorizationUrl, scopes, tokenUrl, grantType, requestContentType, refreshRequestParameters, tokenRequestParameters, legacyRedirectUrl, } = authorizationInfo;
48
+ const { clientId, clientSecret, authorizationUrl, scopes, tokenUrl, grantType, requestContentType, refreshRequestParameters, tokenRequestParameters, legacyRedirectUrl, pkceCodeChallengeMethod, } = authorizationInfo;
46
49
  this.clientId = clientId;
47
50
  this.clientSecret = clientSecret;
48
51
  this.providerAuthorizationUrl = authorizationUrl;
@@ -53,6 +56,7 @@ class OAuth2Service {
53
56
  this.tokenRequestParameters = tokenRequestParameters;
54
57
  this.refreshRequestParameters = refreshRequestParameters;
55
58
  this.legacyRedirectUrl = legacyRedirectUrl;
59
+ this.pkceCodeChallengeMethod = pkceCodeChallengeMethod;
56
60
  this.environment = environment;
57
61
  this.credentialPayload = credentialPayload;
58
62
  if (!Object.values(configurationTypes_1.RequestContentType).includes(this.requestContentType)) {
@@ -94,6 +98,14 @@ class OAuth2Service {
94
98
  authorizationParams.set('state', state);
95
99
  authorizationParams.set('response_type', 'code');
96
100
  authorizationParams.set('redirect_uri', this.redirectUri);
101
+ if (this.grantType === configurationTypes_1.GrantType.AUTHORIZATION_CODE && this.pkceCodeChallengeMethod) {
102
+ this.codeVerifier = node_crypto_1.default.randomBytes(32).toString('base64url');
103
+ const codeChallenge = this.pkceCodeChallengeMethod === 'S256'
104
+ ? node_crypto_1.default.createHash('sha256').update(this.codeVerifier).digest('base64url')
105
+ : this.codeVerifier;
106
+ authorizationParams.set('code_challenge', codeChallenge);
107
+ authorizationParams.set('code_challenge_method', this.pkceCodeChallengeMethod);
108
+ }
97
109
  const delimiter = this.providerAuthorizationUrl.includes('?') ? '&' : '?';
98
110
  const authorizationUrlTemplate = `${this.providerAuthorizationUrl}${delimiter}${authorizationParams.toString()}`;
99
111
  const authorizationUrl = (0, template_1.expandTemplate)(authorizationUrlTemplate, {
@@ -139,6 +151,7 @@ class OAuth2Service {
139
151
  redirect_uri: this.redirectUri,
140
152
  ...(this.clientId && { client_id: this.clientId }),
141
153
  ...(this.clientSecret && { client_secret: this.clientSecret }),
154
+ ...(this.grantType === configurationTypes_1.GrantType.AUTHORIZATION_CODE && this.codeVerifier && { code_verifier: this.codeVerifier }),
142
155
  };
143
156
  if (this.grantType === configurationTypes_1.GrantType.JWT_BEARER && this.clientSecret) {
144
157
  tokenRequestPayload.client_assertion = this.clientSecret;
@@ -73,6 +73,57 @@ describe('OAuth2Helper', () => {
73
73
  sinon_1.default.assert.calledOnce(openSpy);
74
74
  sinon_1.default.assert.calledWith(openSpy, 'https://pro-fooValue-der.com/oauth/authorize?query1=barValue&client_id=your-client-id&scope=scope1+scope2&state=eyJjbGlDYWxsYmFja1VybCI6Ii9vYXV0aDIvY2FsbGJhY2sifQ%3D%3D&response_type=code&redirect_uri=https%3A%2F%2Fintegrations-platform.unito.io%2Fcredentials%2Fnew%2Foauth2%2Fcallback');
75
75
  });
76
+ describe('with pkceCodeChallengeMethod', () => {
77
+ it('adds code_challenge and code_challenge_method to authorization URL for S256', async () => {
78
+ const authorizationInfo = {
79
+ clientId: 'your-client-id',
80
+ clientSecret: 'your-client-secret',
81
+ authorizationUrl: 'https://provider.com/oauth/authorize',
82
+ scopes: [{ name: 'scope1' }],
83
+ tokenUrl: 'https://provider.com/oauth/token',
84
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
85
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
86
+ pkceCodeChallengeMethod: configurationTypes_1.PkceCodeChallengeMethod.S256,
87
+ };
88
+ const pkceHelper = new oauth2_1.default(authorizationInfo, globalConfiguration_1.Environment.Production);
89
+ sinon_1.default.stub(pkceHelper, 'startServer').resolves('http://localhost:5050');
90
+ sinon_1.default.stub(pkceHelper, 'stopServer');
91
+ await pkceHelper.authorize();
92
+ sinon_1.default.assert.calledOnce(openSpy);
93
+ const url = new URL(openSpy.getCall(0).args[0]);
94
+ strict_1.default.equal(url.searchParams.get('code_challenge_method'), 'S256');
95
+ strict_1.default.ok(url.searchParams.get('code_challenge'), 'code_challenge should be present');
96
+ strict_1.default.ok(url.searchParams.get('code_challenge').length > 0, 'code_challenge should not be empty');
97
+ });
98
+ it('uses verifier directly as challenge for plain method', async () => {
99
+ const authorizationInfo = {
100
+ clientId: 'your-client-id',
101
+ clientSecret: 'your-client-secret',
102
+ authorizationUrl: 'https://provider.com/oauth/authorize',
103
+ scopes: [{ name: 'scope1' }],
104
+ tokenUrl: 'https://provider.com/oauth/token',
105
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
106
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
107
+ pkceCodeChallengeMethod: configurationTypes_1.PkceCodeChallengeMethod.PLAIN,
108
+ };
109
+ const pkceHelper = new oauth2_1.default(authorizationInfo, globalConfiguration_1.Environment.Production);
110
+ sinon_1.default.stub(pkceHelper, 'startServer').resolves('http://localhost:5050');
111
+ sinon_1.default.stub(pkceHelper, 'stopServer');
112
+ await pkceHelper.authorize();
113
+ sinon_1.default.assert.calledOnce(openSpy);
114
+ const url = new URL(openSpy.getCall(0).args[0]);
115
+ strict_1.default.equal(url.searchParams.get('code_challenge_method'), 'plain');
116
+ // For plain method, the code_verifier stored on the instance should equal the code_challenge
117
+ strict_1.default.equal(url.searchParams.get('code_challenge'), pkceHelper['codeVerifier']);
118
+ });
119
+ it('does not add PKCE params when codeChallengeMethod is not set', async () => {
120
+ await oauth2Helper.authorize();
121
+ sinon_1.default.assert.calledOnce(openSpy);
122
+ const url = new URL(openSpy.getCall(0).args[0]);
123
+ strict_1.default.equal(url.searchParams.get('code_challenge'), null);
124
+ strict_1.default.equal(url.searchParams.get('code_challenge_method'), null);
125
+ });
126
+ });
76
127
  describe('with legacyRedirectUrl', () => {
77
128
  it('uses legacyRedirectUrl as redirect_uri and adds redirectToIPS to state', async () => {
78
129
  oauth2Helper['legacyRedirectUrl'] = 'http://localhost:3000/api/auth/notion/callback';
@@ -318,6 +369,43 @@ describe('OAuth2Helper', () => {
318
369
  sinon_1.default.assert.calledOnce(res.send);
319
370
  sinon_1.default.assert.calledWith(res.send, oauth2Namespace.HTML_ERROR_MSG('Bad request'));
320
371
  });
372
+ describe('with PKCE', () => {
373
+ it('includes code_verifier in token request body when codeChallengeMethod is set', async () => {
374
+ const authorizationInfo = {
375
+ clientId: 'your-client-id',
376
+ clientSecret: 'your-client-secret',
377
+ authorizationUrl: 'https://provider.com/oauth/authorize',
378
+ scopes: [{ name: 'scope1' }],
379
+ tokenUrl: 'https://provider.com/oauth/token',
380
+ grantType: configurationTypes_1.GrantType.AUTHORIZATION_CODE,
381
+ requestContentType: configurationTypes_1.RequestContentType.URL_ENCODED,
382
+ pkceCodeChallengeMethod: configurationTypes_1.PkceCodeChallengeMethod.S256,
383
+ };
384
+ const pkceHelper = new oauth2_1.default(authorizationInfo, globalConfiguration_1.Environment.Production);
385
+ sinon_1.default.stub(pkceHelper, 'startServer').resolves('http://localhost:5050');
386
+ sinon_1.default.stub(pkceHelper, 'stopServer');
387
+ // authorize() generates the code_verifier
388
+ await pkceHelper.authorize();
389
+ const storedVerifier = pkceHelper['codeVerifier'];
390
+ strict_1.default.ok(storedVerifier, 'codeVerifier should be set after authorize()');
391
+ const code = 'test-code';
392
+ const req = { query: { code } };
393
+ const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub() };
394
+ await pkceHelper['handleCallback'](req, res);
395
+ sinon_1.default.assert.calledOnce(fetchStub);
396
+ const body = new URLSearchParams(fetchStub.getCall(0).args[1].body);
397
+ strict_1.default.equal(body.get('code_verifier'), storedVerifier);
398
+ });
399
+ it('does not include code_verifier when codeChallengeMethod is not set', async () => {
400
+ const code = 'test-code';
401
+ const req = { query: { code } };
402
+ const res = { send: sinon_1.default.stub(), setHeader: sinon_1.default.stub() };
403
+ await oauth2Helper['handleCallback'](req, res);
404
+ sinon_1.default.assert.calledOnce(fetchStub);
405
+ const body = new URLSearchParams(fetchStub.getCall(0).args[1].body);
406
+ strict_1.default.equal(body.get('code_verifier'), null);
407
+ });
408
+ });
321
409
  describe('with legacyRedirectUrl', () => {
322
410
  it('uses legacyRedirectUrl as redirect_uri in token request', async () => {
323
411
  oauth2Helper['legacyRedirectUrl'] = 'http://localhost:3000/api/auth/notion/callback';
@@ -562,7 +562,6 @@
562
562
  },
563
563
  "environment": {
564
564
  "description": "the environment of the platform",
565
- "hidden": true,
566
565
  "name": "environment",
567
566
  "default": "production",
568
567
  "hasDynamicHelp": false,
@@ -635,12 +634,11 @@
635
634
  "type": "option"
636
635
  },
637
636
  "registry-only": {
638
- "description": "(advanced) only update the registry",
637
+ "description": "only update the registry without publishing code",
639
638
  "exclusive": [
640
639
  "preview",
641
640
  "live-preview"
642
641
  ],
643
- "hidden": true,
644
642
  "name": "registry-only",
645
643
  "allowNo": false,
646
644
  "type": "boolean"
@@ -1007,5 +1005,5 @@
1007
1005
  ]
1008
1006
  }
1009
1007
  },
1010
- "version": "1.0.3"
1008
+ "version": "1.2.0"
1011
1009
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-cli",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "Integration CLI",
5
5
  "bin": {
6
6
  "integration-cli": "./bin/run"
@@ -39,7 +39,7 @@
39
39
  "@ngrok/ngrok": "^1.7.0",
40
40
  "@oazapfts/runtime": "1.x",
41
41
  "@oclif/core": "3.x",
42
- "@unito/integration-debugger": "^0.29.1",
42
+ "@unito/integration-debugger": "^0.30.0",
43
43
  "ajv": "8.x",
44
44
  "ajv-formats": "3.x",
45
45
  "better-ajv-errors": "1.x",