@scality/data-browser-library 1.1.8 → 1.1.9
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.
- package/dist/components/providers/DataBrowserProvider.d.ts +1 -1
- package/dist/components/providers/DataBrowserProvider.js +4 -2
- package/dist/hooks/useS3Client.js +4 -5
- package/dist/test/utils/errorHandling.test.js +34 -1
- package/dist/types/index.d.ts +9 -4
- package/dist/utils/errorHandling.d.ts +2 -1
- package/dist/utils/errorHandling.js +11 -1
- package/dist/utils/s3Client.d.ts +3 -9
- package/dist/utils/s3ConfigIdentifier.js +10 -7
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ export interface DataBrowserProviderProps {
|
|
|
17
17
|
}
|
|
18
18
|
export declare const DataBrowserProvider: React.FC<DataBrowserProviderProps>;
|
|
19
19
|
export declare const useDataBrowserConfig: () => import("../../types").S3BrowserConfig & {
|
|
20
|
-
credentials: import("../../types").S3Credentials;
|
|
20
|
+
credentials: import("../../types").S3Credentials | import("../../types").S3CredentialProvider;
|
|
21
21
|
};
|
|
22
22
|
/**
|
|
23
23
|
* Hook to invalidate queries with automatic S3 config identifier prefixing.
|
|
@@ -13,10 +13,12 @@ const DataBrowserContext = /*#__PURE__*/ createContext(DEFAULT_CONTEXT_VALUE);
|
|
|
13
13
|
const useDataBrowserContext = ()=>useContext(DataBrowserContext);
|
|
14
14
|
const DataBrowserProvider = ({ children, queryClient, theme, enableDevtools, getS3Config })=>{
|
|
15
15
|
const currentConfig = getS3Config?.();
|
|
16
|
+
const currentCredentials = currentConfig?.credentials;
|
|
17
|
+
const staticCredentials = 'function' == typeof currentCredentials ? void 0 : currentCredentials;
|
|
16
18
|
const s3ConfigIdentifier = useMemo(()=>currentConfig ? computeS3ConfigIdentifier(currentConfig) : ANONYMOUS_S3_CONFIG_IDENTIFIER, [
|
|
17
19
|
currentConfig?.cacheKey,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
staticCredentials?.accessKeyId,
|
|
21
|
+
staticCredentials?.sessionToken,
|
|
20
22
|
currentConfig?.region,
|
|
21
23
|
currentConfig?.endpoint
|
|
22
24
|
]);
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useMemo, useRef } from "react";
|
|
2
2
|
import { useDataBrowserContext } from "../components/providers/DataBrowserProvider.js";
|
|
3
3
|
import { createS3Client } from "../utils/s3Client.js";
|
|
4
4
|
const useS3Client = ()=>{
|
|
5
5
|
const { s3ConfigIdentifier, getS3Config } = useDataBrowserContext();
|
|
6
6
|
if (!getS3Config) throw new Error('useS3Client: S3 config not available. Ensure DataBrowserProvider has getS3Config prop set.');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}, [
|
|
7
|
+
const getS3ConfigRef = useRef(getS3Config);
|
|
8
|
+
getS3ConfigRef.current = getS3Config;
|
|
9
|
+
return useMemo(()=>createS3Client(getS3ConfigRef.current()), [
|
|
11
10
|
s3ConfigIdentifier
|
|
12
11
|
]);
|
|
13
12
|
};
|
|
@@ -36,6 +36,9 @@ const TestErrors = {
|
|
|
36
36
|
accessDenied: ()=>createMockAWSError('AccessDenied', TEST_CONSTANTS.MESSAGES.ACCESS_DENIED, 403),
|
|
37
37
|
internalError: ()=>createMockAWSError('InternalError', TEST_CONSTANTS.MESSAGES.INTERNAL_ERROR, 500),
|
|
38
38
|
serviceUnavailable: ()=>createMockAWSError('ServiceUnavailable', TEST_CONSTANTS.MESSAGES.SERVICE_UNAVAILABLE, 503),
|
|
39
|
+
expiredToken: ()=>createMockAWSError('ExpiredToken', 'The provided token has expired.', 400),
|
|
40
|
+
expiredTokenException: ()=>createMockAWSError('ExpiredTokenException', 'The security token included in the request is expired', 400),
|
|
41
|
+
tokenRefreshRequired: ()=>createMockAWSError('TokenRefreshRequired', 'Token refresh is required', 400),
|
|
39
42
|
abortError: ()=>{
|
|
40
43
|
const error = new Error('Operation aborted');
|
|
41
44
|
Object.defineProperty(error, 'name', {
|
|
@@ -61,7 +64,8 @@ const TestEnhancedErrors = {
|
|
|
61
64
|
auth: ()=>new EnhancedS3Error('Auth error', 'AuthError', ErrorCategory.AUTHORIZATION, new Error()),
|
|
62
65
|
notFound: ()=>new EnhancedS3Error('Not found', 'NotFoundError', ErrorCategory.NOT_FOUND, new Error()),
|
|
63
66
|
cancelled: ()=>new EnhancedS3Error('Cancelled', 'CancelError', ErrorCategory.CANCELLATION, new Error()),
|
|
64
|
-
unknown: ()=>new EnhancedS3Error('Unknown', 'UnknownError', ErrorCategory.UNKNOWN, new Error())
|
|
67
|
+
unknown: ()=>new EnhancedS3Error('Unknown', 'UnknownError', ErrorCategory.UNKNOWN, new Error()),
|
|
68
|
+
expiredCredentials: ()=>new EnhancedS3Error('Token expired', 'ExpiredToken', ErrorCategory.EXPIRED_CREDENTIALS, new Error())
|
|
65
69
|
};
|
|
66
70
|
describe('ErrorCategory', ()=>{
|
|
67
71
|
test('contains all expected categories', ()=>{
|
|
@@ -71,6 +75,7 @@ describe('ErrorCategory', ()=>{
|
|
|
71
75
|
expect(ErrorCategory.CANCELLATION).toBe('CANCELLATION');
|
|
72
76
|
expect(ErrorCategory.AUTHORIZATION).toBe('AUTHORIZATION');
|
|
73
77
|
expect(ErrorCategory.NOT_FOUND).toBe('NOT_FOUND');
|
|
78
|
+
expect(ErrorCategory.EXPIRED_CREDENTIALS).toBe('EXPIRED_CREDENTIALS');
|
|
74
79
|
expect(ErrorCategory.UNKNOWN).toBe('UNKNOWN');
|
|
75
80
|
});
|
|
76
81
|
});
|
|
@@ -102,6 +107,7 @@ describe('EnhancedS3Error', ()=>{
|
|
|
102
107
|
test('returns true for retryable categories', ()=>{
|
|
103
108
|
expect(TestEnhancedErrors.server().shouldRetry()).toBe(true);
|
|
104
109
|
expect(TestEnhancedErrors.network().shouldRetry()).toBe(true);
|
|
110
|
+
expect(TestEnhancedErrors.expiredCredentials().shouldRetry()).toBe(true);
|
|
105
111
|
});
|
|
106
112
|
test('returns false for non-retryable categories', ()=>{
|
|
107
113
|
expect(TestEnhancedErrors.client().shouldRetry()).toBe(false);
|
|
@@ -192,6 +198,29 @@ describe('createS3Error', ()=>{
|
|
|
192
198
|
expect(result.category).toBe(expectedCategory);
|
|
193
199
|
});
|
|
194
200
|
});
|
|
201
|
+
test('handles expired credential errors as retryable', ()=>{
|
|
202
|
+
const testCases = [
|
|
203
|
+
{
|
|
204
|
+
error: TestErrors.expiredToken(),
|
|
205
|
+
expectedName: 'ExpiredToken'
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
error: TestErrors.expiredTokenException(),
|
|
209
|
+
expectedName: 'ExpiredTokenException'
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
error: TestErrors.tokenRefreshRequired(),
|
|
213
|
+
expectedName: 'TokenRefreshRequired'
|
|
214
|
+
}
|
|
215
|
+
];
|
|
216
|
+
testCases.forEach(({ error, expectedName })=>{
|
|
217
|
+
const result = createS3Error(error);
|
|
218
|
+
expect(result.category).toBe(ErrorCategory.EXPIRED_CREDENTIALS);
|
|
219
|
+
expect(result.name).toBe(expectedName);
|
|
220
|
+
expect(result.statusCode).toBe(400);
|
|
221
|
+
expect(result.shouldRetry()).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
195
224
|
test('handles network errors', ()=>{
|
|
196
225
|
const testCases = [
|
|
197
226
|
TestErrors.networkError(),
|
|
@@ -325,6 +354,10 @@ describe('shouldRetryError', ()=>{
|
|
|
325
354
|
error: TestErrors.internalError(),
|
|
326
355
|
shouldRetry: true
|
|
327
356
|
},
|
|
357
|
+
{
|
|
358
|
+
error: TestErrors.expiredToken(),
|
|
359
|
+
shouldRetry: true
|
|
360
|
+
},
|
|
328
361
|
{
|
|
329
362
|
error: TestErrors.invalidRequest(),
|
|
330
363
|
shouldRetry: false
|
package/dist/types/index.d.ts
CHANGED
|
@@ -50,15 +50,20 @@ export interface S3BrowserConfig extends Omit<S3ClientConfig, 'credentials'> {
|
|
|
50
50
|
*/
|
|
51
51
|
export interface S3Credentials extends AwsCredentialIdentity {
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Async credential provider function.
|
|
55
|
+
* The AWS SDK calls this before each request to resolve credentials.
|
|
56
|
+
*/
|
|
57
|
+
export type S3CredentialProvider = () => Promise<S3Credentials>;
|
|
53
58
|
/**
|
|
54
59
|
* Function type that returns the current S3 configuration with credentials.
|
|
55
60
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
61
|
+
* Credentials can be either a static object or an async provider function.
|
|
62
|
+
* When a provider is used, the AWS SDK calls it before each request,
|
|
63
|
+
* allowing transparent credential refresh.
|
|
59
64
|
*/
|
|
60
65
|
export type GetConfigFunction = () => S3BrowserConfig & {
|
|
61
|
-
credentials: S3Credentials;
|
|
66
|
+
credentials: S3Credentials | S3CredentialProvider;
|
|
62
67
|
};
|
|
63
68
|
export type S3ErrorCode = 'AccessDenied' | 'NoSuchBucket' | 'NoSuchKey' | 'InvalidBucketName' | 'BucketNotFound' | 'NetworkError' | 'UnknownError';
|
|
64
69
|
export interface S3Error extends Error {
|
|
@@ -10,6 +10,7 @@ export declare enum ErrorCategory {
|
|
|
10
10
|
CANCELLATION = "CANCELLATION",// User cancelled - don't retry
|
|
11
11
|
AUTHORIZATION = "AUTHORIZATION",// Permission issues - don't retry
|
|
12
12
|
NOT_FOUND = "NOT_FOUND",// Resource doesn't exist - don't retry
|
|
13
|
+
EXPIRED_CREDENTIALS = "EXPIRED_CREDENTIALS",// Temporary credentials expired - can retry
|
|
13
14
|
UNKNOWN = "UNKNOWN"
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
@@ -25,7 +26,7 @@ export declare class EnhancedS3Error extends Error {
|
|
|
25
26
|
constructor(message: string, name: string, category: ErrorCategory, originalError: Error, statusCode?: number, metadata?: S3ServiceException['$metadata'], context?: Record<string, unknown>);
|
|
26
27
|
/**
|
|
27
28
|
* Determines if this error should be retried based on its category.
|
|
28
|
-
*
|
|
29
|
+
* Server errors, network issues, and expired credentials are retryable.
|
|
29
30
|
*/
|
|
30
31
|
shouldRetry(): boolean;
|
|
31
32
|
}
|
|
@@ -6,6 +6,7 @@ var errorHandling_ErrorCategory = /*#__PURE__*/ function(ErrorCategory) {
|
|
|
6
6
|
ErrorCategory["CANCELLATION"] = "CANCELLATION";
|
|
7
7
|
ErrorCategory["AUTHORIZATION"] = "AUTHORIZATION";
|
|
8
8
|
ErrorCategory["NOT_FOUND"] = "NOT_FOUND";
|
|
9
|
+
ErrorCategory["EXPIRED_CREDENTIALS"] = "EXPIRED_CREDENTIALS";
|
|
9
10
|
ErrorCategory["UNKNOWN"] = "UNKNOWN";
|
|
10
11
|
return ErrorCategory;
|
|
11
12
|
}({});
|
|
@@ -26,9 +27,17 @@ class EnhancedS3Error extends Error {
|
|
|
26
27
|
Object.setPrototypeOf(this, EnhancedS3Error.prototype);
|
|
27
28
|
}
|
|
28
29
|
shouldRetry() {
|
|
29
|
-
return "SERVER_ERROR" === this.category || "NETWORK_ERROR" === this.category;
|
|
30
|
+
return "SERVER_ERROR" === this.category || "NETWORK_ERROR" === this.category || "EXPIRED_CREDENTIALS" === this.category;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
const EXPIRED_CREDENTIAL_ERROR_NAMES = new Set([
|
|
34
|
+
'ExpiredToken',
|
|
35
|
+
'ExpiredTokenException',
|
|
36
|
+
'TokenRefreshRequired'
|
|
37
|
+
]);
|
|
38
|
+
function isExpiredCredentialError(errorName) {
|
|
39
|
+
return EXPIRED_CREDENTIAL_ERROR_NAMES.has(errorName);
|
|
40
|
+
}
|
|
32
41
|
function classifyByStatusCode(statusCode) {
|
|
33
42
|
if (statusCode >= 400 && statusCode < 500) {
|
|
34
43
|
if (401 === statusCode || 403 === statusCode) return "AUTHORIZATION";
|
|
@@ -47,6 +56,7 @@ function createS3Error(error, context) {
|
|
|
47
56
|
if (error instanceof Error && 'AbortError' === error.name) return new EnhancedS3Error('Operation was cancelled', 'AbortError', "CANCELLATION", error, void 0, void 0, context);
|
|
48
57
|
if (error instanceof NoSuchBucket || error instanceof NoSuchKey || error instanceof NoSuchUpload) return new EnhancedS3Error(error.message, error.name, "NOT_FOUND", error, error.$metadata?.httpStatusCode, error.$metadata, context);
|
|
49
58
|
if (error instanceof InvalidRequest || error instanceof InvalidObjectState || error instanceof InvalidWriteOffset || error instanceof ObjectAlreadyInActiveTierError || error instanceof ObjectNotInActiveTierError) return new EnhancedS3Error(error.message, error.name, "CLIENT_ERROR", error, error.$metadata?.httpStatusCode, error.$metadata, context);
|
|
59
|
+
if (isS3ServiceException(error) && isExpiredCredentialError(error.name)) return new EnhancedS3Error(error.message, error.name, "EXPIRED_CREDENTIALS", error, error.$metadata?.httpStatusCode, error.$metadata, context);
|
|
50
60
|
if (isS3ServiceException(error)) {
|
|
51
61
|
const category = error.$metadata?.httpStatusCode ? classifyByStatusCode(error.$metadata.httpStatusCode) : "UNKNOWN";
|
|
52
62
|
return new EnhancedS3Error(error.message, error.name, category, error, error.$metadata?.httpStatusCode, error.$metadata, context);
|
package/dist/utils/s3Client.d.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
-
import type {
|
|
3
|
-
/**
|
|
4
|
-
* S3Client configuration with credentials
|
|
5
|
-
*/
|
|
6
|
-
export interface S3ClientConfigWithProxy extends S3BrowserConfig {
|
|
7
|
-
credentials: S3Credentials;
|
|
8
|
-
}
|
|
2
|
+
import type { GetConfigFunction } from '../types';
|
|
9
3
|
/**
|
|
10
4
|
* Create S3 client with optional proxy middleware
|
|
11
5
|
*
|
|
12
|
-
* @param config - S3 configuration with credentials and optional proxy config
|
|
6
|
+
* @param config - S3 configuration with credentials (static or provider) and optional proxy config
|
|
13
7
|
* @returns Configured S3Client instance
|
|
14
8
|
*/
|
|
15
|
-
export declare const createS3Client: (config:
|
|
9
|
+
export declare const createS3Client: (config: ReturnType<GetConfigFunction>) => S3Client;
|
|
@@ -38,13 +38,16 @@ const computeS3ConfigIdentifier = (config, fallbackIdentifier = ANONYMOUS_S3_CON
|
|
|
38
38
|
const cacheKey = config?.cacheKey?.trim();
|
|
39
39
|
if (cacheKey) parts.push(cacheKey);
|
|
40
40
|
else {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
41
|
+
const credentials = config?.credentials;
|
|
42
|
+
if ('object' == typeof credentials && null !== credentials) {
|
|
43
|
+
const accessKeyId = credentials.accessKeyId?.trim();
|
|
44
|
+
if (accessKeyId) {
|
|
45
|
+
parts.push(accessKeyId);
|
|
46
|
+
const sessionToken = credentials.sessionToken?.trim();
|
|
47
|
+
if (sessionToken) {
|
|
48
|
+
const hash = hashSessionToken(sessionToken);
|
|
49
|
+
parts.push(`session:${hash}`);
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
}
|