@mondaydotcomorg/monday-authorization 3.8.0 → 3.9.4-feat-shaime-fix-timeout-5ff7b3a

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.
Files changed (52) hide show
  1. package/dist/authorization-internal-service.d.ts +12 -8
  2. package/dist/authorization-internal-service.d.ts.map +1 -1
  3. package/dist/authorization-internal-service.js +141 -25
  4. package/dist/authorization-service.d.ts +2 -2
  5. package/dist/authorization-service.d.ts.map +1 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +0 -5
  8. package/dist/esm/authorization-internal-service.d.ts +12 -8
  9. package/dist/esm/authorization-internal-service.d.ts.map +1 -1
  10. package/dist/esm/authorization-internal-service.mjs +141 -26
  11. package/dist/esm/authorization-service.d.ts +2 -2
  12. package/dist/esm/authorization-service.d.ts.map +1 -1
  13. package/dist/esm/constants.d.ts.map +1 -1
  14. package/dist/esm/constants.mjs +0 -5
  15. package/dist/esm/index.d.ts +2 -2
  16. package/dist/esm/index.d.ts.map +1 -1
  17. package/dist/esm/memberships.d.ts.map +1 -1
  18. package/dist/esm/memberships.mjs +11 -2
  19. package/dist/esm/roles-service.d.ts.map +1 -1
  20. package/dist/esm/roles-service.mjs +6 -1
  21. package/dist/esm/types/fetch-options.d.ts +13 -0
  22. package/dist/esm/types/fetch-options.d.ts.map +1 -0
  23. package/dist/esm/types/fetch-options.mjs +1 -0
  24. package/dist/esm/types/scoped-actions-contracts.d.ts +1 -4
  25. package/dist/esm/types/scoped-actions-contracts.d.ts.map +1 -1
  26. package/dist/esm/utils/assignment-schema.d.ts +7 -7
  27. package/dist/esm/utils/authorization.utils.d.ts.map +1 -1
  28. package/dist/esm/utils/authorization.utils.mjs +0 -3
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/memberships.d.ts.map +1 -1
  32. package/dist/memberships.js +11 -2
  33. package/dist/roles-service.d.ts.map +1 -1
  34. package/dist/roles-service.js +6 -1
  35. package/dist/types/fetch-options.d.ts +13 -0
  36. package/dist/types/fetch-options.d.ts.map +1 -0
  37. package/dist/types/fetch-options.js +1 -0
  38. package/dist/types/scoped-actions-contracts.d.ts +1 -4
  39. package/dist/types/scoped-actions-contracts.d.ts.map +1 -1
  40. package/dist/utils/assignment-schema.d.ts +7 -7
  41. package/dist/utils/authorization.utils.d.ts.map +1 -1
  42. package/dist/utils/authorization.utils.js +0 -3
  43. package/package.json +1 -1
  44. package/src/authorization-internal-service.ts +202 -33
  45. package/src/authorization-service.ts +2 -2
  46. package/src/constants.ts +0 -5
  47. package/src/index.ts +2 -2
  48. package/src/memberships.ts +11 -2
  49. package/src/roles-service.ts +6 -1
  50. package/src/types/fetch-options.ts +18 -0
  51. package/src/types/scoped-actions-contracts.ts +1 -11
  52. package/src/utils/authorization.utils.ts +0 -3
@@ -1,54 +1,207 @@
1
1
  import { signAuthorizationHeader } from '@mondaydotcomorg/monday-jwt';
2
- import { fetch, MondayFetchOptions } from '@mondaydotcomorg/monday-fetch';
3
2
  import * as MondayLogger from '@mondaydotcomorg/monday-logger';
4
3
  import {
5
4
  NullableErrorWithType,
6
5
  OnRetryCallback,
6
+ Response,
7
7
  RetryPolicy,
8
- RetryDelayCallback,
8
+ GetTimeout,
9
9
  } from '@mondaydotcomorg/monday-fetch-api';
10
10
  import { IgniteClient } from '@mondaydotcomorg/ignite-sdk';
11
11
  import { BaseRequest } from './types/general';
12
+ import type { AuthorizationFetchOptions } from './types/fetch-options';
12
13
 
13
14
  const INTERNAL_APP_NAME = 'internal_ms';
14
15
  export const MAX_RETRIES = 3;
15
16
  export const RETRY_DELAY_MS = 20;
16
17
  export const logger = MondayLogger.getLogger();
17
18
 
18
- const defaultMondayFetchOptions: MondayFetchOptions = {
19
+ export const IGNITE_RETRY_CONFIG_KEY = 'authorization_retry_config';
20
+
21
+ let igniteClient: IgniteClient | undefined;
22
+
23
+ type RetryConfig = {
24
+ retries: number;
25
+ baseDelayMs: number;
26
+ exponentBase: number;
27
+ jitterMinMs: number;
28
+ jitterMaxMs: number;
29
+ /**
30
+ * Array of status matchers used by RetryPolicy.retryOn.
31
+ * Supported forms:
32
+ * - "429" (exact status)
33
+ * - "5**" (wildcards; '*' matches any digit) => 5xx
34
+ * - "5*9" (wildcards; '*' matches any digit) => 509, 519, ... 599
35
+ */
36
+ retryOnStatusPatterns: string[];
37
+ };
38
+
39
+ const defaultRetryConfig: RetryConfig = {
19
40
  retries: MAX_RETRIES,
20
- callback: logOnFetchFail,
41
+ baseDelayMs: RETRY_DELAY_MS,
42
+ exponentBase: 2,
43
+ jitterMinMs: 0,
44
+ jitterMaxMs: 1000,
45
+ retryOnStatusPatterns: ['429', '5**'],
21
46
  };
22
47
 
23
- export const onRetryCallback: OnRetryCallback = (attempt: number, error?: NullableErrorWithType) => {
24
- if (attempt == MAX_RETRIES) {
25
- logger.error({ tag: 'authorization-service', attempt, error }, 'Authorization attempt failed');
48
+ /**
49
+ * Sanitizes retry configuration values to ensure they are within valid ranges
50
+ */
51
+ function sanitizeRetryConfig(config: RetryConfig): RetryConfig {
52
+ return {
53
+ ...config,
54
+ retries: Math.max(0, Math.min(5, config.retries)),
55
+ exponentBase: Math.max(1, Math.min(100, config.exponentBase)),
56
+ jitterMinMs: Math.max(0, config.jitterMinMs),
57
+ };
58
+ }
59
+
60
+ function getRetryConfig(): RetryConfig {
61
+ if (!igniteClient) {
62
+ return defaultRetryConfig;
63
+ }
64
+ try {
65
+ const config = igniteClient.configuration.getObjectValue<RetryConfig>(IGNITE_RETRY_CONFIG_KEY, defaultRetryConfig);
66
+ return sanitizeRetryConfig(config);
67
+ } catch (error) {
68
+ logger.error(
69
+ { tag: 'authorization-service', error, key: IGNITE_RETRY_CONFIG_KEY, defaultValue: defaultRetryConfig },
70
+ 'Failed to get ignite retry config, using defaults'
71
+ );
72
+ return defaultRetryConfig;
73
+ }
74
+ }
75
+
76
+ function randomIntInclusive(min: number, max: number): number {
77
+ if (max <= min) {
78
+ return min;
79
+ }
80
+ return Math.floor(min + Math.random() * (max - min + 1));
81
+ }
82
+
83
+ type StatusMatcher = (status: number) => boolean;
84
+
85
+ function buildHttpStatusMatchers(patterns: string[]): StatusMatcher[] {
86
+ const matchers: StatusMatcher[] = [];
87
+
88
+ const addWildcardMatcher = (wildcard: string) => {
89
+ // Normalized 3-char pattern with digits or '*'
90
+ matchers.push((status: number) => {
91
+ const s = String(status);
92
+ if (s.length !== 3) {
93
+ return false;
94
+ }
95
+ for (let i = 0; i < 3; i++) {
96
+ const w = wildcard[i];
97
+ const c = s[i];
98
+ if (w === '*') {
99
+ continue;
100
+ }
101
+ if (w !== c) {
102
+ return false;
103
+ }
104
+ }
105
+ return true;
106
+ });
107
+ };
108
+
109
+ for (const raw of patterns ?? []) {
110
+ const p = String(raw).trim();
111
+ if (!p) {
112
+ continue;
113
+ }
114
+
115
+ // Exact numeric status (e.g. "429")
116
+ if (/^\d+$/.test(p)) {
117
+ const exact = Number(p);
118
+ if (Number.isFinite(exact)) {
119
+ matchers.push((status: number) => status === exact);
120
+ }
121
+ continue;
122
+ }
123
+
124
+ // Wildcards for 3-digit HTTP codes:
125
+ // - "5**" => 5xx
126
+ // - "5*9" => 5?9
127
+ if (/^[0-9*]+$/.test(p) && p.includes('*')) {
128
+ const normalized = p.length < 3 ? p.padEnd(3, '*') : p;
129
+ if (normalized.length !== 3) {
130
+ logger.warn(
131
+ { tag: 'authorization-service', pattern: p },
132
+ 'Invalid retry status wildcard pattern (expected a 3-character pattern like "5**" or "5*9")'
133
+ );
134
+ } else {
135
+ addWildcardMatcher(normalized);
136
+ }
137
+ continue;
138
+ }
139
+
140
+ logger.warn(
141
+ { tag: 'authorization-service', pattern: p },
142
+ 'Invalid retry status pattern (supported: exact code like "429" or wildcard like "5**")'
143
+ );
144
+ }
145
+ return matchers;
146
+ }
147
+
148
+ function shouldRetryOnResponseStatus(
149
+ responseOrStatus: Response | null | undefined,
150
+ statusMatchers: StatusMatcher[]
151
+ ): boolean {
152
+ const status = responseOrStatus?.status;
153
+ if (typeof status !== 'number') {
154
+ return false;
155
+ }
156
+ return statusMatchers.some(matcher => matcher(status));
157
+ }
158
+
159
+ export const onRetryCallback: OnRetryCallback = (
160
+ attempt: number,
161
+ error?: NullableErrorWithType,
162
+ response?: Response | null,
163
+ isTimeoutError?: boolean
164
+ ) => {
165
+ const effectiveMaxRetries = getRetryConfig().retries;
166
+ if (attempt == effectiveMaxRetries) {
167
+ logger.error(
168
+ { tag: 'authorization-service', attempt, error, response, isTimeoutError },
169
+ 'Authorization attempt failed'
170
+ );
26
171
  } else {
27
- logger.info({ tag: 'authorization-service', attempt, error }, 'Authorization attempt failed, trying again');
172
+ logger.info(
173
+ { tag: 'authorization-service', attempt, error, response, isTimeoutError },
174
+ 'Authorization attempt failed, trying again'
175
+ );
28
176
  }
29
177
  };
30
178
 
31
179
  /**
32
180
  * Exponential backoff retry delay callback
33
- * Calculates delay as: baseDelay * 2^(attemptCount - 1)
181
+ * Calculates delay as: baseDelay * (exponentBase)^(attemptCount - 1) + jitter(min..max)
34
182
  * Example: attempt 1 -> 100ms, attempt 2 -> 200ms, attempt 3 -> 400ms
35
183
  */
36
- export const calcDelayDurationInMs: RetryDelayCallback = ({ attemptCount }) => {
37
- return RETRY_DELAY_MS * Math.pow(2, attemptCount - 1);
184
+ export const calcDelayDurationInMs = ({ attemptCount }: { attemptCount: number }): number => {
185
+ const { baseDelayMs, exponentBase, jitterMinMs, jitterMaxMs } = getRetryConfig();
186
+ const jitterMin = Math.min(jitterMinMs, jitterMaxMs);
187
+ const jitterMax = Math.max(jitterMinMs, jitterMaxMs);
188
+ const expDelay = baseDelayMs * Math.pow(exponentBase, attemptCount - 1);
189
+ const jitter = randomIntInclusive(jitterMin, jitterMax);
190
+ return expDelay + jitter;
38
191
  };
39
192
 
40
- function logOnFetchFail(retriesLeft: number, error: Error) {
41
- if (retriesLeft == 0) {
42
- logger.error({ retriesLeft, error }, `Authorization attempt failed due to ${error.message}`);
43
- } else {
44
- logger.info({ retriesLeft, error }, `Authorization attempt failed due to ${error.message}, trying again`);
45
- }
46
- }
47
-
48
- let mondayFetchOptions: MondayFetchOptions = defaultMondayFetchOptions;
193
+ let mondayFetchOptions: AuthorizationFetchOptions = {
194
+ retries: defaultRetryConfig.retries,
195
+ };
49
196
 
50
197
  export class AuthorizationInternalService {
51
- static igniteClient?: IgniteClient;
198
+ static get igniteClient(): IgniteClient | undefined {
199
+ return igniteClient;
200
+ }
201
+
202
+ static set igniteClient(client: IgniteClient | undefined) {
203
+ igniteClient = client;
204
+ }
52
205
  static skipAuthorization(requset: BaseRequest): void {
53
206
  requset.authorizationSkipPerformed = true;
54
207
  }
@@ -63,7 +216,7 @@ export class AuthorizationInternalService {
63
216
  }
64
217
  }
65
218
 
66
- static throwOnHttpErrorIfNeeded(response: Awaited<ReturnType<typeof fetch>>, placement: string): void {
219
+ static throwOnHttpErrorIfNeeded(response: Response, placement: string): void {
67
220
  if (response.ok) {
68
221
  return;
69
222
  }
@@ -89,25 +242,28 @@ export class AuthorizationInternalService {
89
242
  return signAuthorizationHeader({ appName: INTERNAL_APP_NAME, accountId, userId });
90
243
  }
91
244
 
92
- static setRequestFetchOptions(customMondayFetchOptions: MondayFetchOptions) {
245
+ static setRequestFetchOptions(customMondayFetchOptions: AuthorizationFetchOptions) {
246
+ const sanitizedOptions: AuthorizationFetchOptions = { ...customMondayFetchOptions };
247
+ if (sanitizedOptions.retries !== undefined) {
248
+ sanitizedOptions.retries = Math.max(0, Math.min(5, sanitizedOptions.retries));
249
+ }
93
250
  mondayFetchOptions = {
94
- ...defaultMondayFetchOptions,
95
- ...customMondayFetchOptions,
251
+ ...defaultRetryConfig,
252
+ ...sanitizedOptions,
96
253
  };
97
254
  }
98
255
 
99
- static getRequestFetchOptions(): MondayFetchOptions {
256
+ static getRequestFetchOptions(): AuthorizationFetchOptions {
100
257
  return mondayFetchOptions;
101
258
  }
102
259
 
103
260
  static setIgniteClient(client: IgniteClient) {
104
- this.igniteClient = client;
261
+ igniteClient = client;
105
262
  }
106
263
 
107
264
  static getRequestTimeout() {
108
265
  const isDevEnv = process.env.NODE_ENV === 'development';
109
266
  const defaultTimeout = isDevEnv ? 60000 : 2000;
110
-
111
267
  if (!this.igniteClient) {
112
268
  return defaultTimeout;
113
269
  }
@@ -133,12 +289,25 @@ export class AuthorizationInternalService {
133
289
 
134
290
  static getRetriesPolicy(): RetryPolicy {
135
291
  const fetchOptions = AuthorizationInternalService.getRequestFetchOptions();
136
- const retryDelayMS = calcDelayDurationInMs;
292
+ const retryConfig = getRetryConfig();
293
+ const statusMatchers = buildHttpStatusMatchers(retryConfig.retryOnStatusPatterns);
294
+ const defaultRetryOn = (_attempt, _error, response, isTimeoutError) => {
295
+ return isTimeoutError ? true : shouldRetryOnResponseStatus(response ?? undefined, statusMatchers);
296
+ };
297
+ // Sanitize retries from fetchOptions: clamp between 0 and 5
298
+ const rawMaxRetries = fetchOptions?.retries ?? retryConfig.retries;
299
+ const effectiveMaxRetries = Math.max(0, Math.min(5, rawMaxRetries));
300
+ const defaultGetTimeout: GetTimeout = timeoutsCount =>
301
+ calcDelayDurationInMs({ attemptCount: timeoutsCount }) + AuthorizationInternalService.getRequestTimeout();
302
+
137
303
  return {
138
- useRetries: !!fetchOptions.retries,
139
- maxRetries: fetchOptions.retries ?? 0,
140
- onRetry: onRetryCallback,
141
- retryDelayMS,
304
+ useRetries: fetchOptions?.useRetries ?? true,
305
+ maxRetries: effectiveMaxRetries,
306
+ retryDelayMS: fetchOptions?.retryDelayMS ?? calcDelayDurationInMs,
307
+ onRetry: fetchOptions?.callback ?? onRetryCallback,
308
+ retryOn: fetchOptions?.retryOn ?? defaultRetryOn,
309
+ timeoutRetries: fetchOptions?.timeoutRetries ?? effectiveMaxRetries,
310
+ getTimeout: fetchOptions?.getTimeout ?? defaultGetTimeout,
142
311
  };
143
312
  }
144
313
  }
@@ -1,5 +1,4 @@
1
1
  import { performance } from 'perf_hooks';
2
- import { MondayFetchOptions } from '@mondaydotcomorg/monday-fetch';
3
2
  import { Api } from '@mondaydotcomorg/trident-backend-api';
4
3
  import { HttpFetcherError } from '@mondaydotcomorg/monday-fetch-api';
5
4
  import { getIgniteClient, IgniteClient } from '@mondaydotcomorg/ignite-sdk';
@@ -17,6 +16,7 @@ import { getAttributionsFromApi, getProfile } from './attributions-service';
17
16
  import { GraphApi } from './clients/graph-api';
18
17
  import { PlatformApi } from './clients/platform-api';
19
18
  import { scopeToResource } from './utils/authorization.utils';
19
+ import { AuthorizationFetchOptions } from './types/fetch-options';
20
20
 
21
21
  const GRANTED_FEATURE_CACHE_EXPIRATION_SECONDS = 5 * 60;
22
22
  const PLATFORM_AUTHORIZE_PATH = '/internal_ms/authorization/authorize';
@@ -29,7 +29,7 @@ export interface AuthorizeResponse {
29
29
  unauthorizedObjects?: AuthorizationObject[];
30
30
  }
31
31
 
32
- export function setRequestFetchOptions(customMondayFetchOptions: MondayFetchOptions) {
32
+ export function setRequestFetchOptions(customMondayFetchOptions: AuthorizationFetchOptions) {
33
33
  AuthorizationInternalService.setRequestFetchOptions(customMondayFetchOptions);
34
34
  }
35
35
 
package/src/constants.ts CHANGED
@@ -15,11 +15,6 @@ export const ERROR_MESSAGES = {
15
15
  } as const;
16
16
 
17
17
  export const DEFAULT_FETCH_OPTIONS: RecursivePartial<FetcherConfig> = {
18
- retryPolicy: {
19
- useRetries: true,
20
- maxRetries: 3,
21
- retryDelayMS: 10,
22
- },
23
18
  logPolicy: {
24
19
  logErrors: 'error',
25
20
  logRequests: 'info',
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { MondayFetchOptions } from '@mondaydotcomorg/monday-fetch';
2
1
  import { setPrometheus } from './prometheus-service';
3
2
  import { setIgniteClient, setRedisClient, setRequestFetchOptions } from './authorization-service';
4
3
  import { initializeMetrics, MetricsClient } from './metrics-service';
5
4
  import * as TestKit from './testKit';
5
+ import { AuthorizationFetchOptions } from './types/fetch-options';
6
6
 
7
7
  interface MetricsInitOptions {
8
8
  client?: MetricsClient;
@@ -14,7 +14,7 @@ interface MetricsInitOptions {
14
14
 
15
15
  export interface InitOptions {
16
16
  prometheus?: any;
17
- mondayFetchOptions?: MondayFetchOptions;
17
+ mondayFetchOptions?: AuthorizationFetchOptions;
18
18
  redisClient?: any;
19
19
  grantedFeatureRedisExpirationInSeconds?: number;
20
20
  metrics?: MetricsInitOptions;
@@ -8,6 +8,7 @@ import {
8
8
  } from 'types/memberships';
9
9
  import { handleApiError } from 'utils/api-error-handler';
10
10
  import { getAttributionsFromApi } from './attributions-service';
11
+ import { AuthorizationInternalService } from './authorization-internal-service';
11
12
 
12
13
  import { APP_NAME, DEFAULT_FETCH_OPTIONS, ERROR_MESSAGES } from './constants';
13
14
 
@@ -69,7 +70,11 @@ export class MembershipsService {
69
70
  },
70
71
  body: JSON.stringify({ memberships }),
71
72
  },
72
- this.fetchOptions
73
+ {
74
+ ...this.fetchOptions,
75
+ timeout: AuthorizationInternalService.getRequestTimeout(),
76
+ retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
77
+ }
73
78
  );
74
79
  } catch (err) {
75
80
  return handleApiError(err, 'authorization', 'upsertMemberships');
@@ -101,7 +106,11 @@ export class MembershipsService {
101
106
  },
102
107
  body: JSON.stringify({ memberships }),
103
108
  },
104
- this.fetchOptions
109
+ {
110
+ ...this.fetchOptions,
111
+ timeout: AuthorizationInternalService.getRequestTimeout(),
112
+ retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
113
+ }
105
114
  );
106
115
  } catch (err) {
107
116
  return handleApiError(err, 'authorization', 'deleteMemberships');
@@ -2,6 +2,7 @@ import { Api, FetcherConfig, HttpClient } from '@mondaydotcomorg/trident-backend
2
2
  import { HttpFetcherError, RecursivePartial } from '@mondaydotcomorg/monday-fetch-api';
3
3
  import { RoleCreateRequest, RolesResponse, RoleUpdateRequest } from 'types/roles';
4
4
  import { getAttributionsFromApi } from 'attributions-service';
5
+ import { AuthorizationInternalService } from './authorization-internal-service';
5
6
  import { APP_NAME, DEFAULT_FETCH_OPTIONS, ERROR_MESSAGES } from './constants';
6
7
 
7
8
  const API_PATH = '/roles/account/{accountId}';
@@ -113,7 +114,11 @@ export class RolesService {
113
114
  },
114
115
  body: method === 'GET' ? undefined : body,
115
116
  },
116
- this.fetchOptions
117
+ {
118
+ ...this.fetchOptions,
119
+ timeout: AuthorizationInternalService.getRequestTimeout(),
120
+ retryPolicy: AuthorizationInternalService.getRetriesPolicy(),
121
+ }
117
122
  );
118
123
  } catch (err) {
119
124
  if (err instanceof HttpFetcherError) {
@@ -0,0 +1,18 @@
1
+ import type {
2
+ ShouldRetryCallback,
3
+ RetryDelayCallback,
4
+ OnRetryCallback,
5
+ GetTimeout,
6
+ } from '@mondaydotcomorg/monday-fetch-api';
7
+
8
+ interface FetchOptions {
9
+ callback: OnRetryCallback;
10
+ useRetries: boolean;
11
+ retries: number;
12
+ retryDelayMS: number | RetryDelayCallback;
13
+ retryOn: ShouldRetryCallback;
14
+ getTimeout: GetTimeout;
15
+ timeoutRetries: number;
16
+ }
17
+
18
+ export type AuthorizationFetchOptions = Partial<FetchOptions>;
@@ -18,17 +18,7 @@ export interface AccountScope {
18
18
  accountId: number;
19
19
  }
20
20
 
21
- export interface CredentialsSharedConfigScope {
22
- credentialsSharedConfigId: number;
23
- }
24
-
25
- export type ScopeOptions =
26
- | WorkspaceScope
27
- | BoardScope
28
- | PulseScope
29
- | AccountProductScope
30
- | AccountScope
31
- | CredentialsSharedConfigScope;
21
+ export type ScopeOptions = WorkspaceScope | BoardScope | PulseScope | AccountProductScope | AccountScope;
32
22
 
33
23
  export interface Translation {
34
24
  key: string;
@@ -28,9 +28,6 @@ export function scopeToResource(scope: ScopeOptions): { resourceType: ResourceTy
28
28
  if ('accountId' in scope) {
29
29
  return { resourceType: 'account', resourceId: scope.accountId };
30
30
  }
31
- if ('credentialsSharedConfigId' in scope) {
32
- return { resourceType: 'credentials_shared_config', resourceId: scope.credentialsSharedConfigId };
33
- }
34
31
 
35
32
  throw new Error('Unsupported scope provided');
36
33
  }