@friggframework/core 2.0.0-next.64 → 2.0.0-next.66

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,37 +2,117 @@ const { Requester } = require('./requester');
2
2
  const { get } = require('../../assertions');
3
3
  const { ModuleConstants } = require('../ModuleConstants');
4
4
 
5
+ /**
6
+ * OAuth 2.0 Requester - Base class for API modules using OAuth 2.0 authentication.
7
+ *
8
+ * Supports multiple OAuth 2.0 grant types:
9
+ * - `authorization_code` (default): Standard OAuth flow with user consent
10
+ * - `client_credentials`: Server-to-server authentication without user
11
+ * - `password`: Resource Owner Password Credentials grant
12
+ *
13
+ * @extends Requester
14
+ *
15
+ * @example
16
+ * // Authorization Code flow (default)
17
+ * const api = new MyApi({ grant_type: 'authorization_code' });
18
+ * const authUrl = api.getAuthorizationUri();
19
+ * // After user authorizes...
20
+ * await api.getTokenFromCode(code);
21
+ *
22
+ * @example
23
+ * // Client Credentials flow
24
+ * const api = new MyApi({
25
+ * grant_type: 'client_credentials',
26
+ * client_id: process.env.CLIENT_ID,
27
+ * client_secret: process.env.CLIENT_SECRET,
28
+ * audience: 'https://api.example.com',
29
+ * });
30
+ * await api.getTokenFromClientCredentials();
31
+ */
5
32
  class OAuth2Requester extends Requester {
6
33
 
7
34
  static requesterType = ModuleConstants.authType.oauth2;
8
35
 
36
+ /**
37
+ * Creates an OAuth2Requester instance.
38
+ *
39
+ * @param {Object} params - Configuration parameters
40
+ * @param {string} [params.grant_type='authorization_code'] - OAuth grant type:
41
+ * 'authorization_code', 'client_credentials', or 'password'
42
+ * @param {string} [params.client_id] - OAuth client ID
43
+ * @param {string} [params.client_secret] - OAuth client secret
44
+ * @param {string} [params.redirect_uri] - OAuth redirect URI for authorization code flow
45
+ * @param {string} [params.scope] - OAuth scopes (space-separated)
46
+ * @param {string} [params.authorizationUri] - Authorization endpoint URL
47
+ * @param {string} [params.tokenUri] - Token endpoint URL for exchanging codes/credentials
48
+ * @param {string} [params.baseURL] - Base URL for API requests
49
+ * @param {string} [params.access_token] - Existing access token
50
+ * @param {string} [params.refresh_token] - Existing refresh token
51
+ * @param {Date} [params.accessTokenExpire] - Access token expiration date
52
+ * @param {Date} [params.refreshTokenExpire] - Refresh token expiration date
53
+ * @param {string} [params.audience] - Token audience (for client_credentials)
54
+ * @param {string} [params.username] - Username (for password grant)
55
+ * @param {string} [params.password] - Password (for password grant)
56
+ * @param {string} [params.state] - OAuth state parameter for CSRF protection
57
+ */
9
58
  constructor(params) {
10
59
  super(params);
60
+ /** @type {string} Delegate type for token update notifications */
11
61
  this.DLGT_TOKEN_UPDATE = 'TOKEN_UPDATE';
62
+ /** @type {string} Delegate type for token deauthorization notifications */
12
63
  this.DLGT_TOKEN_DEAUTHORIZED = 'TOKEN_DEAUTHORIZED';
13
64
 
14
65
  this.delegateTypes.push(this.DLGT_TOKEN_UPDATE);
15
66
  this.delegateTypes.push(this.DLGT_TOKEN_DEAUTHORIZED);
16
67
 
68
+ /** @type {string} OAuth grant type */
17
69
  this.grant_type = get(params, 'grant_type', 'authorization_code');
70
+ /** @type {string|null} OAuth client ID */
18
71
  this.client_id = get(params, 'client_id', null);
72
+ /** @type {string|null} OAuth client secret */
19
73
  this.client_secret = get(params, 'client_secret', null);
74
+ /** @type {string|null} OAuth redirect URI */
20
75
  this.redirect_uri = get(params, 'redirect_uri', null);
76
+ /** @type {string|null} OAuth scopes */
21
77
  this.scope = get(params, 'scope', null);
78
+ /** @type {string|null} Authorization endpoint URL */
22
79
  this.authorizationUri = get(params, 'authorizationUri', null);
80
+ /** @type {string|null} Token endpoint URL */
81
+ this.tokenUri = get(params, 'tokenUri', null);
82
+ /** @type {string|null} Base URL for API requests */
23
83
  this.baseURL = get(params, 'baseURL', null);
84
+ /** @type {string|null} Current access token */
24
85
  this.access_token = get(params, 'access_token', null);
86
+ /** @type {string|null} Current refresh token */
25
87
  this.refresh_token = get(params, 'refresh_token', null);
88
+ /** @type {Date|null} Access token expiration */
26
89
  this.accessTokenExpire = get(params, 'accessTokenExpire', null);
90
+ /** @type {Date|null} Refresh token expiration */
27
91
  this.refreshTokenExpire = get(params, 'refreshTokenExpire', null);
92
+ /** @type {string|null} Token audience */
28
93
  this.audience = get(params, 'audience', null);
94
+ /** @type {string|null} Username for password grant */
29
95
  this.username = get(params, 'username', null);
96
+ /** @type {string|null} Password for password grant */
30
97
  this.password = get(params, 'password', null);
98
+ /** @type {string|null} OAuth state for CSRF protection */
31
99
  this.state = get(params, 'state', null);
32
100
 
101
+ /** @type {boolean} Whether this requester supports token refresh */
33
102
  this.isRefreshable = true;
34
103
  }
35
104
 
105
+ /**
106
+ * Sets OAuth tokens and calculates expiration times.
107
+ * Notifies delegates of token update via DLGT_TOKEN_UPDATE.
108
+ *
109
+ * @param {Object} params - Token response from OAuth server
110
+ * @param {string} params.access_token - The access token
111
+ * @param {string} [params.refresh_token] - The refresh token (if provided)
112
+ * @param {number} [params.expires_in] - Access token lifetime in seconds
113
+ * @param {number} [params.x_refresh_token_expires_in] - Refresh token lifetime in seconds
114
+ * @returns {Promise<void>}
115
+ */
36
116
  async setTokens(params) {
37
117
  this.access_token = get(params, 'access_token');
38
118
  this.refresh_token = get(params, 'refresh_token', null);
@@ -49,10 +129,20 @@ class OAuth2Requester extends Requester {
49
129
  await this.notify(this.DLGT_TOKEN_UPDATE);
50
130
  }
51
131
 
132
+ /**
133
+ * Gets the OAuth authorization URL for initiating the authorization code flow.
134
+ *
135
+ * @returns {string|null} The authorization URL
136
+ */
52
137
  getAuthorizationUri() {
53
138
  return this.authorizationUri;
54
139
  }
55
140
 
141
+ /**
142
+ * Returns authorization requirements for this OAuth flow.
143
+ *
144
+ * @returns {{url: string|null, type: string}} Authorization requirements
145
+ */
56
146
  getAuthorizationRequirements() {
57
147
  return {
58
148
  url: this.getAuthorizationUri(),
@@ -60,8 +150,13 @@ class OAuth2Requester extends Requester {
60
150
  };
61
151
  }
62
152
 
63
- // this.client_id, this.client_secret, this.redirect_uri, and this.tokenUri
64
- // will need to be defined in the child class before super(params)
153
+ /**
154
+ * Exchanges an authorization code for access and refresh tokens.
155
+ * Requires client_id, client_secret, redirect_uri, and tokenUri to be set.
156
+ *
157
+ * @param {string} code - The authorization code from the OAuth callback
158
+ * @returns {Promise<Object>} Token response containing access_token, refresh_token, etc.
159
+ */
65
160
  async getTokenFromCode(code) {
66
161
  const params = new URLSearchParams();
67
162
  params.append('grant_type', 'authorization_code');
@@ -82,9 +177,14 @@ class OAuth2Requester extends Requester {
82
177
  return response;
83
178
  }
84
179
 
85
- // REPLACE getTokenFromCode IN THE CHILD IF NEEDED
86
- // this.client_id, this.client_secret, this.redirect_uri, and this.tokenUri
87
- // will need to be defined in the child class before super(params)
180
+ /**
181
+ * Exchanges an authorization code for tokens using Basic Auth header.
182
+ * Alternative to getTokenFromCode() for OAuth servers requiring Basic Auth.
183
+ * Override getTokenFromCode() in child class to use this instead.
184
+ *
185
+ * @param {string} code - The authorization code from the OAuth callback
186
+ * @returns {Promise<Object>} Token response containing access_token, refresh_token, etc.
187
+ */
88
188
  async getTokenFromCodeBasicAuthHeader(code) {
89
189
  const params = new URLSearchParams();
90
190
  params.append('grant_type', 'authorization_code');
@@ -108,8 +208,14 @@ class OAuth2Requester extends Requester {
108
208
  return response;
109
209
  }
110
210
 
111
- // this.client_id, this.client_secret, this.redirect_uri, and this.tokenUri
112
- // will need to be defined in the child class before super(params)
211
+ /**
212
+ * Refreshes the access token using the refresh token.
213
+ * Used for authorization_code and password grant types.
214
+ *
215
+ * @param {Object} refreshTokenObject - Object containing refresh_token
216
+ * @param {string} refreshTokenObject.refresh_token - The refresh token
217
+ * @returns {Promise<Object>} New token response
218
+ */
113
219
  async refreshAccessToken(refreshTokenObject) {
114
220
  this.access_token = undefined;
115
221
  const params = new URLSearchParams();
@@ -131,7 +237,16 @@ class OAuth2Requester extends Requester {
131
237
  return response;
132
238
  }
133
239
 
240
+ /**
241
+ * Adds OAuth Bearer token to request headers.
242
+ * Clears any existing Authorization header first to prevent stale tokens
243
+ * from being reused after failed refresh attempts.
244
+ *
245
+ * @param {Object} headers - Headers object to modify
246
+ * @returns {Promise<Object>} Headers with Authorization added
247
+ */
134
248
  async addAuthHeaders(headers) {
249
+ delete headers.Authorization;
135
250
  if (this.access_token) {
136
251
  headers.Authorization = `Bearer ${this.access_token}`;
137
252
  }
@@ -139,29 +254,51 @@ class OAuth2Requester extends Requester {
139
254
  return headers;
140
255
  }
141
256
 
257
+ /**
258
+ * Checks if the requester has valid authentication.
259
+ *
260
+ * @returns {boolean} True if authenticated with valid tokens
261
+ */
142
262
  isAuthenticated() {
143
- return (
144
- this.accessToken !== null &&
145
- this.refreshToken !== null &&
263
+ return !!(
264
+ this.access_token !== null &&
265
+ this.refresh_token !== null &&
146
266
  this.accessTokenExpire &&
147
267
  this.refreshTokenExpire
148
268
  );
149
269
  }
150
270
 
271
+ /**
272
+ * Refreshes authentication based on the configured grant type.
273
+ * - For authorization_code/password: Uses refreshAccessToken() with refresh_token
274
+ * - For client_credentials: Uses getTokenFromClientCredentials() to get new token
275
+ *
276
+ * On failure, notifies delegates via DLGT_INVALID_AUTH.
277
+ *
278
+ * @returns {Promise<boolean>} True if refresh succeeded, false if failed
279
+ */
151
280
  async refreshAuth() {
152
281
  try {
153
- if (this.grantType !== 'client_credentials') {
282
+ if (this.grant_type !== 'client_credentials') {
154
283
  await this.refreshAccessToken({
155
284
  refresh_token: this.refresh_token,
156
285
  });
157
286
  } else {
158
287
  await this.getTokenFromClientCredentials();
159
288
  }
289
+ return true;
160
290
  } catch {
161
291
  await this.notify(this.DLGT_INVALID_AUTH);
292
+ return false;
162
293
  }
163
294
  }
164
295
 
296
+ /**
297
+ * Obtains tokens using the Resource Owner Password Credentials grant.
298
+ * Requires username and password to be set.
299
+ *
300
+ * @returns {Promise<Object|undefined>} Token response or undefined on error
301
+ */
165
302
  async getTokenFromUsernamePassword() {
166
303
  try {
167
304
  const url = this.tokenUri;
@@ -188,6 +325,13 @@ class OAuth2Requester extends Requester {
188
325
  }
189
326
  }
190
327
 
328
+ /**
329
+ * Obtains tokens using the Client Credentials grant.
330
+ * Used for server-to-server authentication without a user context.
331
+ * Requires client_id, client_secret, and optionally audience to be set.
332
+ *
333
+ * @returns {Promise<Object|undefined>} Token response or undefined on error
334
+ */
191
335
  async getTokenFromClientCredentials() {
192
336
  try {
193
337
  const url = this.tokenUri;
@@ -75,8 +75,10 @@ class Requester extends Delegate {
75
75
  await this.notify(this.DLGT_INVALID_AUTH);
76
76
  } else {
77
77
  this.refreshCount++;
78
- await this.refreshAuth();
79
- return this._request(url, options, i + 1); // Retries
78
+ const refreshSucceeded = await this.refreshAuth();
79
+ if (refreshSucceeded) {
80
+ return this._request(url, options, i + 1);
81
+ }
80
82
  }
81
83
  }
82
84
 
@@ -108,7 +110,6 @@ class Requester extends Delegate {
108
110
  }
109
111
 
110
112
  async _post(options, stringify = true) {
111
- console.log('options', options);
112
113
  const fetchOptions = {
113
114
  method: 'POST',
114
115
  credentials: 'include',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0-next.64",
4
+ "version": "2.0.0-next.66",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0-next.64",
42
- "@friggframework/prettier-config": "2.0.0-next.64",
43
- "@friggframework/test": "2.0.0-next.64",
41
+ "@friggframework/eslint-config": "2.0.0-next.66",
42
+ "@friggframework/prettier-config": "2.0.0-next.66",
43
+ "@friggframework/test": "2.0.0-next.66",
44
44
  "@prisma/client": "^6.17.0",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "d7e3b2b756a0a5db13bcc4322de99c29910c4d37"
83
+ "gitHead": "33a0a9416970c03aa3eec7d5f295590cc08d7f98"
84
84
  }