@friggframework/core 2.0.0-next.100 → 2.0.0-next.101

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.
@@ -220,11 +220,11 @@ class IntegrationBase {
220
220
  /**
221
221
  * Returns the modules as object with keys as module names.
222
222
  * Uses the keys from Definition.modules to attach modules correctly.
223
- *
223
+ *
224
224
  * Example:
225
225
  * Definition.modules = { attio: {...}, quo: { definition: { getName: () => 'quo-attio' } } }
226
226
  * Module with getName()='quo-attio' gets attached as this.quo (not this['quo-attio'])
227
- *
227
+ *
228
228
  * @private
229
229
  * @param {Array} integrationModules - Array of module instances
230
230
  * @returns {Object} The modules object
@@ -791,8 +791,18 @@ class IntegrationBase {
791
791
  if (!this.id) return;
792
792
 
793
793
  if (delegateString === 'CREDENTIAL_INVALIDATED') {
794
+ const detail =
795
+ object?.reason || object?.statusCode
796
+ ? ` (status ${object?.statusCode ?? '?'}: ${
797
+ object?.reason ?? 'no reason given'
798
+ })`
799
+ : '';
794
800
  console.log(
795
- `[Frigg] Module ${notifier?.name || '?'} reported invalid credentials for integration ${this.id} — marking ERROR`
801
+ `[Frigg] Module ${
802
+ notifier?.name || '?'
803
+ } reported invalid credentials for integration ${
804
+ this.id
805
+ } — marking ERROR${detail}`
796
806
  );
797
807
  await this.persistStatus('ERROR');
798
808
  return;
@@ -801,7 +811,11 @@ class IntegrationBase {
801
811
  if (delegateString === 'CREDENTIAL_VALIDATED') {
802
812
  if (this.status !== 'ERROR') return;
803
813
  console.log(
804
- `[Frigg] Module ${notifier?.name || '?'} reported valid credentials for integration ${this.id} — clearing ERROR → ENABLED`
814
+ `[Frigg] Module ${
815
+ notifier?.name || '?'
816
+ } reported valid credentials for integration ${
817
+ this.id
818
+ } — clearing ERROR → ENABLED`
805
819
  );
806
820
  await this.persistStatus('ENABLED');
807
821
  }
package/modules/module.js CHANGED
@@ -147,15 +147,24 @@ class Module extends Delegate {
147
147
  } else if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) {
148
148
  await this.deauthorize();
149
149
  } else if (delegateString === this.api.DLGT_INVALID_AUTH) {
150
- await this.markCredentialsInvalid();
150
+ await this.markCredentialsInvalid(object);
151
151
  }
152
152
  }
153
153
 
154
- async markCredentialsInvalid() {
154
+ async markCredentialsInvalid(diagnosticInfo = null) {
155
155
  if (!this.credential) return;
156
156
 
157
157
  if (!this.credential.id) return;
158
158
 
159
+ if (diagnosticInfo) {
160
+ console.error(
161
+ `[Frigg] Module ${this.name} credentials rejected (status ${
162
+ diagnosticInfo.statusCode ?? '?'
163
+ }):`,
164
+ diagnosticInfo.message ?? diagnosticInfo
165
+ );
166
+ }
167
+
159
168
  await this.credentialRepository.updateAuthenticationStatus(
160
169
  this.credential.id,
161
170
  false
@@ -181,6 +190,10 @@ class Module extends Delegate {
181
190
  await this.notify(this.DLGT_CREDENTIAL_INVALIDATED, {
182
191
  credentialId: this.credential.id,
183
192
  moduleName: this.name,
193
+ ...(diagnosticInfo && {
194
+ reason: diagnosticInfo.message,
195
+ statusCode: diagnosticInfo.statusCode,
196
+ }),
184
197
  });
185
198
  } catch (err) {
186
199
  console.error(
@@ -4,6 +4,7 @@ const { FetchError } = require('../../errors');
4
4
  const { get } = require('../../assertions');
5
5
 
6
6
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
7
+ const MAX_AUTH_RETRIES = 3;
7
8
 
8
9
  class Requester extends Delegate {
9
10
  constructor(params) {
@@ -11,6 +12,7 @@ class Requester extends Delegate {
11
12
  this.backOff = get(params, 'backOff', [1, 3, 10, 30, 60, 180]);
12
13
  this.isRefreshable = false;
13
14
  this.refreshCount = 0;
15
+ this.authGraceRetryCount = 0;
14
16
  this.DLGT_INVALID_AUTH = 'INVALID_AUTH';
15
17
  this.delegateTypes.push(this.DLGT_INVALID_AUTH);
16
18
  this.agent = get(params, 'agent', null);
@@ -58,7 +60,16 @@ class Requester extends Delegate {
58
60
  return resp.text();
59
61
  };
60
62
 
61
- async _request(url, options, i = 0) {
63
+ /**
64
+ * @param {string} url - The request URL, relative or absolute.
65
+ * @param {Object} options - Fetch options (method, headers, body, query,
66
+ * returnFullRes, etc.) built by the `_get`/`_post`/`_patch`/`_put`/
67
+ * `_delete` wrappers.
68
+ * @param {number} attempt - 0-based count of retries already made for
69
+ * this call. Indexes `this.backOff` for the next delay and is passed
70
+ * back in on each recursive retry.
71
+ */
72
+ async _request(url, options, attempt = 0) {
62
73
  let encodedUrl = encodeURI(url);
63
74
  if (options.query) {
64
75
  let queryBuild = '?';
@@ -116,11 +127,11 @@ class Requester extends Delegate {
116
127
  // stall at batch scale.
117
128
  const isTimeout =
118
129
  e?.name === 'AbortError' || e?.type === 'aborted';
119
- if (e?.code === 'ECONNRESET' && i < this.backOff.length) {
130
+ if (e?.code === 'ECONNRESET' && attempt < this.backOff.length) {
120
131
  clearRequestTimer();
121
- const delay = this.backOff[i] * 1000;
132
+ const delay = this.backOff[attempt] * 1000;
122
133
  await new Promise((resolve) => setTimeout(resolve, delay));
123
- return this._request(url, options, i + 1);
134
+ return this._request(url, options, attempt + 1);
124
135
  }
125
136
  const fetchError = await FetchError.create({
126
137
  resource: encodedUrl,
@@ -143,30 +154,57 @@ class Requester extends Delegate {
143
154
  const { status } = response;
144
155
 
145
156
  // If the status is retriable and there are back off requests left, retry the request
146
- if ((status === 429 || status >= 500) && i < this.backOff.length) {
157
+ if (
158
+ (status === 429 || status >= 500) &&
159
+ attempt < this.backOff.length
160
+ ) {
147
161
  clearRequestTimer();
148
- const delay = this.backOff[i] * 1000;
162
+ const delay = this.backOff[attempt] * 1000;
149
163
  await new Promise((resolve) => setTimeout(resolve, delay));
150
- return this._request(url, options, i + 1);
164
+ return this._request(url, options, attempt + 1);
151
165
  }
152
166
 
153
167
  if (status === 401) {
154
168
  if (!this.isRefreshable) {
155
- await this.notify(this.DLGT_INVALID_AUTH);
156
- return;
169
+ // Up to MAX_AUTH_RETRIES grace retries before invalidating
170
+ // — a 401 alone isn't proof the credential is bad.
171
+ if (
172
+ this.authGraceRetryCount < MAX_AUTH_RETRIES &&
173
+ this.authGraceRetryCount < this.backOff.length
174
+ ) {
175
+ const delay =
176
+ this.backOff[this.authGraceRetryCount] * 1000;
177
+ this.authGraceRetryCount++;
178
+ clearRequestTimer();
179
+ await new Promise((resolve) =>
180
+ setTimeout(resolve, delay)
181
+ );
182
+ return this._request(url, options, attempt + 1);
183
+ }
184
+
185
+ throw await this._invalidateAuth(
186
+ encodedUrl,
187
+ options,
188
+ response
189
+ );
157
190
  }
158
191
 
159
- if (this.refreshCount === 0) {
192
+ if (this.refreshCount < MAX_AUTH_RETRIES) {
160
193
  this.refreshCount++;
161
194
  const refreshSucceeded = await this.refreshAuth();
162
195
  if (refreshSucceeded) {
163
196
  clearRequestTimer();
164
- return this._request(url, options, i + 1);
197
+ return this._request(url, options, attempt + 1);
165
198
  }
166
199
 
167
- await this.notify(this.DLGT_INVALID_AUTH);
168
- return;
200
+ throw await this._invalidateAuth(
201
+ encodedUrl,
202
+ options,
203
+ response
204
+ );
169
205
  }
206
+
207
+ throw await this._invalidateAuth(encodedUrl, options, response);
170
208
  }
171
209
 
172
210
  // If the error wasn't retried, throw. FetchError.create reads
@@ -188,6 +226,7 @@ class Requester extends Delegate {
188
226
  // a later 401 in the same Requester lifetime can attempt refresh
189
227
  // again instead of silently falling through.
190
228
  this.refreshCount = 0;
229
+ this.authGraceRetryCount = 0;
191
230
 
192
231
  // parsedBody consumes the response body stream. If the server
193
232
  // stalls mid-stream the timer (still armed) aborts it.
@@ -204,6 +243,16 @@ class Requester extends Delegate {
204
243
  }
205
244
  }
206
245
 
246
+ async _invalidateAuth(encodedUrl, options, response) {
247
+ const fetchError = await FetchError.create({
248
+ resource: encodedUrl,
249
+ init: options,
250
+ response,
251
+ });
252
+ await this.notify(this.DLGT_INVALID_AUTH, fetchError);
253
+ return fetchError;
254
+ }
255
+
207
256
  _maybeFlagTimeoutDuringBodyRead(err, timeoutMs) {
208
257
  if (!err || typeof err !== 'object') return err;
209
258
  if (err.isTimeout) return err;
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.100",
4
+ "version": "2.0.0-next.101",
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.100",
42
- "@friggframework/prettier-config": "2.0.0-next.100",
43
- "@friggframework/test": "2.0.0-next.100",
41
+ "@friggframework/eslint-config": "2.0.0-next.101",
42
+ "@friggframework/prettier-config": "2.0.0-next.101",
43
+ "@friggframework/test": "2.0.0-next.101",
44
44
  "@prisma/client": "^6.19.3",
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": "a44d68e8abdcc9dcae8fb6c97ad9c8c602317711"
83
+ "gitHead": "63e9d85c59dc39bb745091911c8149a0de2b3cff"
84
84
  }