@smileid/usesmileid 12.0.0 → 12.0.1
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/CHANGELOG.md +4 -0
- package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.d.ts +11 -0
- package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.d.ts.map +1 -0
- package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.js +17 -0
- package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.js.map +1 -0
- package/dist/core/exception/capture_validation_exceptions.d.ts +1 -0
- package/dist/core/exception/capture_validation_exceptions.d.ts.map +1 -1
- package/dist/core/exception/capture_validation_exceptions.js +1 -0
- package/dist/core/exception/capture_validation_exceptions.js.map +1 -1
- package/dist/core/exception/index.d.ts +1 -0
- package/dist/core/exception/index.d.ts.map +1 -1
- package/dist/core/exception/index.js +1 -0
- package/dist/core/exception/index.js.map +1 -1
- package/dist/core/exception/use_smile_id_error_code.d.ts +1 -0
- package/dist/core/exception/use_smile_id_error_code.d.ts.map +1 -1
- package/dist/core/exception/use_smile_id_error_code.js +1 -0
- package/dist/core/exception/use_smile_id_error_code.js.map +1 -1
- package/dist/core/models/config.d.ts +19 -0
- package/dist/core/models/config.d.ts.map +1 -1
- package/dist/core/models/document_type.d.ts +1 -1
- package/dist/core/models/document_type.d.ts.map +1 -1
- package/dist/core/models/document_type.js +2 -2
- package/dist/core/models/document_type.js.map +1 -1
- package/dist/core/models/global_privacy_control.d.ts +36 -0
- package/dist/core/models/global_privacy_control.d.ts.map +1 -0
- package/dist/core/models/global_privacy_control.js +39 -0
- package/dist/core/models/global_privacy_control.js.map +1 -0
- package/dist/core/models/network_config.d.ts +2 -2
- package/dist/core/models/network_config.js +2 -2
- package/dist/data/api/interceptors/smile_auth_interceptor.d.ts.map +1 -1
- package/dist/data/api/interceptors/smile_auth_interceptor.js +3 -17
- package/dist/data/api/interceptors/smile_auth_interceptor.js.map +1 -1
- package/dist/data/api/smile_id_api.d.ts +1 -1
- package/dist/data/api/smile_id_api.d.ts.map +1 -1
- package/dist/data/dsl/builder/retry_config_builder.d.ts +4 -4
- package/dist/data/dsl/builder/retry_config_builder.d.ts.map +1 -1
- package/dist/data/dsl/builder/retry_config_builder.js +6 -6
- package/dist/data/dsl/builder/retry_config_builder.js.map +1 -1
- package/dist/data/smile_id_urls.d.ts +1 -1
- package/dist/data/smile_id_urls.js +1 -1
- package/dist/data/smile_id_urls.js.map +1 -1
- package/dist/data/util/retry_util.d.ts +1 -1
- package/dist/data/util/retry_util.d.ts.map +1 -1
- package/dist/data/util/retry_util.js +5 -0
- package/dist/data/util/retry_util.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/metadata/builder_configuration.d.ts +5 -0
- package/dist/metadata/builder_configuration.d.ts.map +1 -1
- package/dist/metadata/builder_configuration.js +1 -0
- package/dist/metadata/builder_configuration.js.map +1 -1
- package/dist/metadata/metadata_configuration_mappers.d.ts +2 -3
- package/dist/metadata/metadata_configuration_mappers.d.ts.map +1 -1
- package/dist/metadata/metadata_configuration_mappers.js +13 -6
- package/dist/metadata/metadata_configuration_mappers.js.map +1 -1
- package/dist/metadata/use_smile_id_metadata_factory.d.ts.map +1 -1
- package/dist/metadata/use_smile_id_metadata_factory.js +14 -5
- package/dist/metadata/use_smile_id_metadata_factory.js.map +1 -1
- package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.d.ts +5 -2
- package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.d.ts.map +1 -1
- package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.js +8 -3
- package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.js.map +1 -1
- package/dist/presentation/components/use_smile_id_instructions_hero.d.ts.map +1 -1
- package/dist/presentation/components/use_smile_id_instructions_hero.js +8 -3
- package/dist/presentation/components/use_smile_id_instructions_hero.js.map +1 -1
- package/dist/presentation/flow/dsl/builder/config_builder.d.ts +16 -0
- package/dist/presentation/flow/dsl/builder/config_builder.d.ts.map +1 -1
- package/dist/presentation/flow/dsl/builder/config_builder.js +18 -0
- package/dist/presentation/flow/dsl/builder/config_builder.js.map +1 -1
- package/dist/presentation/flow/dsl/builder/screens_builder.d.ts +3 -6
- package/dist/presentation/flow/dsl/builder/screens_builder.d.ts.map +1 -1
- package/dist/presentation/flow/dsl/builder/screens_builder.js +3 -6
- package/dist/presentation/flow/dsl/builder/screens_builder.js.map +1 -1
- package/dist/presentation/flow/flow_navigation_manager.d.ts +5 -1
- package/dist/presentation/flow/flow_navigation_manager.d.ts.map +1 -1
- package/dist/presentation/flow/flow_navigation_manager.js +7 -1
- package/dist/presentation/flow/flow_navigation_manager.js.map +1 -1
- package/dist/presentation/flow/model/instructions_defaults.d.ts.map +1 -1
- package/dist/presentation/flow/model/instructions_defaults.js +1 -1
- package/dist/presentation/flow/model/instructions_defaults.js.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_capture_screen.d.ts.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_capture_screen.js +13 -3
- package/dist/presentation/flow/screen/orchestrated_capture_screen.js.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_consent_screen.d.ts.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_consent_screen.js +3 -1
- package/dist/presentation/flow/screen/orchestrated_consent_screen.js.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_preview_screen.d.ts.map +1 -1
- package/dist/presentation/flow/screen/orchestrated_preview_screen.js +0 -8
- package/dist/presentation/flow/screen/orchestrated_preview_screen.js.map +1 -1
- package/dist/presentation/flow/use_smile_id_flow.d.ts.map +1 -1
- package/dist/presentation/flow/use_smile_id_flow.js +10 -4
- package/dist/presentation/flow/use_smile_id_flow.js.map +1 -1
- package/dist/presentation/flow/validation/flow_validator.d.ts.map +1 -1
- package/dist/presentation/flow/validation/flow_validator.js +42 -20
- package/dist/presentation/flow/validation/flow_validator.js.map +1 -1
- package/dist/presentation/screen/helpers/capture_ui_state.d.ts +6 -6
- package/dist/presentation/screen/helpers/capture_ui_state.d.ts.map +1 -1
- package/dist/presentation/screen/helpers/capture_ui_state.js +19 -19
- package/dist/presentation/screen/helpers/capture_ui_state.js.map +1 -1
- package/dist/presentation/screen/helpers/ess_turn_haptics.d.ts +2 -2
- package/dist/presentation/screen/helpers/ess_turn_haptics.d.ts.map +1 -1
- package/dist/presentation/screen/helpers/ess_turn_haptics.js +6 -6
- package/dist/presentation/screen/helpers/ess_turn_haptics.js.map +1 -1
- package/dist/presentation/screen/helpers/scan_state_to_overlay_state.d.ts +5 -5
- package/dist/presentation/screen/helpers/scan_state_to_overlay_state.d.ts.map +1 -1
- package/dist/presentation/screen/helpers/scan_state_to_overlay_state.js +11 -11
- package/dist/presentation/screen/helpers/scan_state_to_overlay_state.js.map +1 -1
- package/dist/presentation/screen/helpers/use_capture_orientation_lock.d.ts +13 -3
- package/dist/presentation/screen/helpers/use_capture_orientation_lock.d.ts.map +1 -1
- package/dist/presentation/screen/helpers/use_capture_orientation_lock.js +48 -6
- package/dist/presentation/screen/helpers/use_capture_orientation_lock.js.map +1 -1
- package/dist/presentation/screen/use_smile_id_capture_screen.d.ts +4 -2
- package/dist/presentation/screen/use_smile_id_capture_screen.d.ts.map +1 -1
- package/dist/presentation/screen/use_smile_id_capture_screen.js +36 -15
- package/dist/presentation/screen/use_smile_id_capture_screen.js.map +1 -1
- package/dist/presentation/screen/use_smile_id_consent_screen.d.ts +7 -0
- package/dist/presentation/screen/use_smile_id_consent_screen.d.ts.map +1 -1
- package/dist/presentation/screen/use_smile_id_consent_screen.js +6 -5
- package/dist/presentation/screen/use_smile_id_consent_screen.js.map +1 -1
- package/dist/presentation/screen/use_smile_id_document_back_instructions.d.ts.map +1 -1
- package/dist/presentation/screen/use_smile_id_document_back_instructions.js +5 -13
- package/dist/presentation/screen/use_smile_id_document_back_instructions.js.map +1 -1
- package/dist/presentation/screen/use_smile_id_document_preview_screen.js +1 -1
- package/dist/presentation/screen/use_smile_id_document_preview_screen.js.map +1 -1
- package/dist/presentation/screen/use_smile_id_preview_screen.d.ts +1 -21
- package/dist/presentation/screen/use_smile_id_preview_screen.d.ts.map +1 -1
- package/dist/presentation/screen/use_smile_id_preview_screen.js +3 -14
- package/dist/presentation/screen/use_smile_id_preview_screen.js.map +1 -1
- package/dist/presentation/screen/use_smile_id_processing_screen.js +1 -1
- package/dist/presentation/screen/use_smile_id_processing_screen.js.map +1 -1
- package/dist/presentation/utils/locale_utils.d.ts +10 -4
- package/dist/presentation/utils/locale_utils.d.ts.map +1 -1
- package/dist/presentation/utils/locale_utils.js +13 -5
- package/dist/presentation/utils/locale_utils.js.map +1 -1
- package/dist/testing/fake_face_analyzer.d.ts +43 -0
- package/dist/testing/fake_face_analyzer.d.ts.map +1 -0
- package/dist/testing/fake_face_analyzer.js +52 -0
- package/dist/testing/fake_face_analyzer.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +7 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/scenario_expectations.d.ts +28 -0
- package/dist/testing/scenario_expectations.d.ts.map +1 -0
- package/dist/testing/scenario_expectations.js +65 -0
- package/dist/testing/scenario_expectations.js.map +1 -0
- package/dist/testing/scenario_loader.d.ts +11 -0
- package/dist/testing/scenario_loader.d.ts.map +1 -0
- package/dist/testing/scenario_loader.js +64 -0
- package/dist/testing/scenario_loader.js.map +1 -0
- package/dist/testing/scripted_scan_results.d.ts +17 -0
- package/dist/testing/scripted_scan_results.d.ts.map +1 -0
- package/dist/testing/scripted_scan_results.js +52 -0
- package/dist/testing/scripted_scan_results.js.map +1 -0
- package/package.json +11 -4
- package/src/__tests__/dsl.test.ts +51 -1
- package/src/__tests__/setup_react_native.ts +17 -2
- package/src/__tests__/snapshots/__snapshots__/screens_structural.test.tsx.snap +5261 -0
- package/src/__tests__/snapshots/screens_structural.test.tsx +230 -0
- package/src/camera/__tests__/use_smile_id_camera_system.test.ts +103 -0
- package/src/core/exception/__tests__/builder_validation_exceptions.test.ts +67 -0
- package/src/core/exception/agent_mode_with_enhanced_liveness_exception.ts +20 -0
- package/src/core/exception/capture_validation_exceptions.ts +1 -0
- package/src/core/exception/index.ts +1 -0
- package/src/core/exception/use_smile_id_error_code.ts +1 -0
- package/src/core/models/__tests__/document_type.test.ts +2 -2
- package/src/core/models/__tests__/flow_models.test.ts +26 -0
- package/src/core/models/config.ts +22 -0
- package/src/core/models/document_type.ts +2 -2
- package/src/core/models/global_privacy_control.ts +45 -0
- package/src/core/models/network_config.ts +2 -2
- package/src/data/api/__tests__/smile_id_api.test.ts +1 -1
- package/src/data/api/interceptors/__tests__/gzip_interceptor.test.ts +83 -0
- package/src/data/api/interceptors/smile_auth_interceptor.ts +3 -18
- package/src/data/compression/__tests__/payload_compressor_real_jpeg.test.ts +6 -2
- package/src/data/dsl/builder/__tests__/config_builders.test.ts +62 -0
- package/src/data/dsl/builder/retry_config_builder.ts +6 -6
- package/src/data/factory/__tests__/retry_gzip_mac_composition.test.ts +163 -0
- package/src/data/factory/__tests__/signed_job_submission_headers.test.ts +1 -1
- package/src/data/repository/__tests__/network_retries.test.ts +1 -1
- package/src/data/repository/__tests__/use_smile_id_repository.test.ts +1 -1
- package/src/data/security/__tests__/request_canonicalizer.test.ts +132 -0
- package/src/data/smile_id_urls.ts +1 -1
- package/src/data/util/__tests__/retry_util.test.ts +25 -0
- package/src/data/util/retry_util.ts +9 -1
- package/src/index.ts +6 -4
- package/src/metadata/__tests__/metadata_configuration_mappers.test.ts +39 -0
- package/src/metadata/__tests__/metadata_configuration_objects.test.ts +2 -1
- package/src/metadata/__tests__/metadata_device_context.test.ts +3 -3
- package/src/metadata/__tests__/metadata_device_os_android.test.ts +18 -0
- package/src/metadata/__tests__/metadata_golden_fixture.json +1 -1
- package/src/metadata/__tests__/metadata_payload_golden.test.ts +4 -4
- package/src/metadata/__tests__/use_smile_id_metadata.test.ts +1 -1
- package/src/metadata/builder_configuration.ts +6 -0
- package/src/metadata/metadata_configuration_mappers.ts +21 -7
- package/src/metadata/use_smile_id_metadata_factory.ts +17 -6
- package/src/presentation/assets/si_document_green_book_hero_animation.json +1 -4668
- package/src/presentation/assets/si_document_hero_animation.json +1 -4799
- package/src/presentation/assets/si_document_hero_back_animation.json +1 -2039
- package/src/presentation/assets/si_document_passport_hero_animation.json +1 -14491
- package/src/presentation/assets/si_ess_all_directions_hero_animation.json +1 -0
- package/src/presentation/assets/si_ess_animation_all_directions.json +1 -6589
- package/src/presentation/assets/si_selfie_hero_animation.json +1 -1439
- package/src/presentation/assets/si_smile_to_capture_animation.json +1 -1439
- package/src/presentation/components/__tests__/use_smile_id_enhanced_smart_selfie_clip.test.tsx +9 -4
- package/src/presentation/components/__tests__/use_smile_id_instructions_hero.test.tsx +44 -0
- package/src/presentation/components/use_smile_id_enhanced_smart_selfie_clip.tsx +9 -3
- package/src/presentation/components/use_smile_id_instructions_hero.tsx +10 -0
- package/src/presentation/flow/__tests__/document_flow_navigation.test.ts +3 -3
- package/src/presentation/flow/__tests__/flow_navigation_manager.test.ts +49 -0
- package/src/presentation/flow/dsl/builder/__tests__/config_builder.test.ts +25 -0
- package/src/presentation/flow/dsl/builder/config_builder.ts +21 -0
- package/src/presentation/flow/dsl/builder/screens_builder.ts +3 -6
- package/src/presentation/flow/flow_navigation_manager.ts +6 -1
- package/src/presentation/flow/model/instructions_defaults.ts +1 -1
- package/src/presentation/flow/screen/orchestrated_capture_screen.tsx +18 -3
- package/src/presentation/flow/screen/orchestrated_consent_screen.tsx +5 -0
- package/src/presentation/flow/screen/orchestrated_preview_screen.tsx +0 -12
- package/src/presentation/flow/use_smile_id_flow.tsx +11 -4
- package/src/presentation/flow/validation/__tests__/flow_validator.test.ts +61 -1
- package/src/presentation/flow/validation/flow_validator.ts +55 -26
- package/src/presentation/screen/__tests__/capture_ui_state.test.ts +66 -47
- package/src/presentation/screen/__tests__/ess_turn_haptics.test.ts +15 -11
- package/src/presentation/screen/__tests__/scan_state_to_overlay_state.test.ts +47 -27
- package/src/presentation/screen/__tests__/use_smile_id_capture_screen.test.tsx +176 -14
- package/src/presentation/screen/__tests__/use_smile_id_document_back_instructions.test.tsx +1 -1
- package/src/presentation/screen/__tests__/use_smile_id_preview_screen.test.tsx +10 -40
- package/src/presentation/screen/helpers/__tests__/use_capture_orientation_lock.test.tsx +212 -0
- package/src/presentation/screen/helpers/capture_ui_state.ts +27 -27
- package/src/presentation/screen/helpers/ess_turn_haptics.ts +9 -9
- package/src/presentation/screen/helpers/scan_state_to_overlay_state.ts +13 -13
- package/src/presentation/screen/helpers/use_capture_orientation_lock.ts +69 -7
- package/src/presentation/screen/use_smile_id_capture_screen.tsx +47 -19
- package/src/presentation/screen/use_smile_id_consent_screen.tsx +16 -5
- package/src/presentation/screen/use_smile_id_document_back_instructions.tsx +14 -23
- package/src/presentation/screen/use_smile_id_document_preview_screen.tsx +1 -1
- package/src/presentation/screen/use_smile_id_preview_screen.tsx +13 -45
- package/src/presentation/screen/use_smile_id_processing_screen.tsx +1 -1
- package/src/presentation/utils/__tests__/locale_utils.test.ts +46 -5
- package/src/presentation/utils/locale_utils.ts +13 -4
- package/src/testing/__tests__/fake_face_analyzer.test.ts +42 -0
- package/src/testing/__tests__/scenario_expectations.test.ts +110 -0
- package/src/testing/__tests__/scenario_loader.test.ts +64 -0
- package/src/testing/fake_face_analyzer.ts +82 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/scenario_expectations.ts +87 -0
- package/src/testing/scenario_loader.ts +83 -0
- package/src/testing/scripted_scan_results.ts +66 -0
- package/dist/presentation/components/use_smile_id_face_silhouette_overlay.d.ts +0 -15
- package/dist/presentation/components/use_smile_id_face_silhouette_overlay.d.ts.map +0 -1
- package/dist/presentation/components/use_smile_id_face_silhouette_overlay.js +0 -19
- package/dist/presentation/components/use_smile_id_face_silhouette_overlay.js.map +0 -1
- package/src/presentation/components/use_smile_id_face_silhouette_overlay.tsx +0 -27
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import axios, { AxiosHeaders, type InternalAxiosRequestConfig } from "axios";
|
|
3
|
+
|
|
4
|
+
import { createGzipInterceptor } from "../gzip_interceptor";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pins `createGzipInterceptor`'s wire shape: it stamps `Accept-Encoding: gzip` on every request
|
|
8
|
+
* (never reading `config.data`), leaves the body unmodified (no compression, no Content-Type/
|
|
9
|
+
* Length), never sets `Content-Encoding`, and overwrites any existing `Accept-Encoding`.
|
|
10
|
+
*/
|
|
11
|
+
describe("createGzipInterceptor", () => {
|
|
12
|
+
function attachAndRun(
|
|
13
|
+
config: InternalAxiosRequestConfig,
|
|
14
|
+
): Promise<InternalAxiosRequestConfig> {
|
|
15
|
+
const client = axios.create();
|
|
16
|
+
createGzipInterceptor().attach(client);
|
|
17
|
+
const handlers = (
|
|
18
|
+
client.interceptors.request as unknown as {
|
|
19
|
+
handlers: {
|
|
20
|
+
fulfilled?: (
|
|
21
|
+
cfg: InternalAxiosRequestConfig,
|
|
22
|
+
) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
|
|
23
|
+
}[];
|
|
24
|
+
}
|
|
25
|
+
).handlers;
|
|
26
|
+
const fulfilled = handlers[handlers.length - 1]?.fulfilled;
|
|
27
|
+
if (!fulfilled) throw new Error("gzip interceptor did not register");
|
|
28
|
+
return Promise.resolve(fulfilled(config));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
it("stamps Accept-Encoding: gzip on a bodyless request", async () => {
|
|
32
|
+
const result = await attachAndRun({
|
|
33
|
+
headers: new AxiosHeaders(),
|
|
34
|
+
} as InternalAxiosRequestConfig);
|
|
35
|
+
|
|
36
|
+
expect(result.headers.get("Accept-Encoding")).toBe("gzip");
|
|
37
|
+
expect(result.data).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("stamps the header without touching a JSON string body", async () => {
|
|
41
|
+
const result = await attachAndRun({
|
|
42
|
+
headers: new AxiosHeaders(),
|
|
43
|
+
data: '{"hello":"world"}',
|
|
44
|
+
} as InternalAxiosRequestConfig);
|
|
45
|
+
|
|
46
|
+
expect(result.headers.get("Accept-Encoding")).toBe("gzip");
|
|
47
|
+
// The body is not compressed or otherwise transformed.
|
|
48
|
+
expect(result.data).toBe('{"hello":"world"}');
|
|
49
|
+
expect(result.headers.get("Content-Type")).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("stamps the header without touching a multipart FormData body", async () => {
|
|
53
|
+
// A minimal FormData-like stand-in; the interceptor never inspects
|
|
54
|
+
// `config.data`'s shape, so this is enough to prove pass-through.
|
|
55
|
+
const formData = { getParts: () => [] };
|
|
56
|
+
const result = await attachAndRun({
|
|
57
|
+
headers: new AxiosHeaders(),
|
|
58
|
+
data: formData,
|
|
59
|
+
} as unknown as InternalAxiosRequestConfig);
|
|
60
|
+
|
|
61
|
+
expect(result.headers.get("Accept-Encoding")).toBe("gzip");
|
|
62
|
+
expect(result.data).toBe(formData);
|
|
63
|
+
expect(result.headers.get("Content-Type")).toBeUndefined();
|
|
64
|
+
expect(result.headers.get("Content-Length")).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("never stamps Content-Encoding — the request body itself is never compressed", async () => {
|
|
68
|
+
const result = await attachAndRun({
|
|
69
|
+
headers: new AxiosHeaders(),
|
|
70
|
+
data: '{"hello":"world"}',
|
|
71
|
+
} as InternalAxiosRequestConfig);
|
|
72
|
+
|
|
73
|
+
expect(result.headers.get("Content-Encoding")).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("overwrites an Accept-Encoding value set by an earlier interceptor", async () => {
|
|
77
|
+
const result = await attachAndRun({
|
|
78
|
+
headers: new AxiosHeaders({ "Accept-Encoding": "identity" }),
|
|
79
|
+
} as InternalAxiosRequestConfig);
|
|
80
|
+
|
|
81
|
+
expect(result.headers.get("Accept-Encoding")).toBe("gzip");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AxiosInstance, InternalAxiosRequestConfig } from "axios";
|
|
2
|
-
import {
|
|
2
|
+
import { sourceSdkIdentifier } from "@smileid/usesmileid_bridge";
|
|
3
3
|
|
|
4
4
|
import type { PartnerConfig } from "../../../core/models/network_config";
|
|
5
5
|
import { currentRequestTimestamp } from "../../security/request_timestamp";
|
|
@@ -26,22 +26,7 @@ import type { TokenHolder } from "../auth/token_holder";
|
|
|
26
26
|
* by the next outgoing request.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
const SDK_VERSION = "12.0.
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Source-SDK identifier, suffixed with the runtime platform so the server can
|
|
33
|
-
* attribute the request and resolve the matching per-SDK signing secret.
|
|
34
|
-
*/
|
|
35
|
-
function sourceSdk(): string {
|
|
36
|
-
switch (Platform.OS) {
|
|
37
|
-
case "android":
|
|
38
|
-
return "react-native-expo (android)";
|
|
39
|
-
case "ios":
|
|
40
|
-
return "react-native-expo (iOS)";
|
|
41
|
-
default:
|
|
42
|
-
return "react-native-expo";
|
|
43
|
-
}
|
|
44
|
-
}
|
|
29
|
+
const SDK_VERSION = "12.0.1";
|
|
45
30
|
|
|
46
31
|
export type SmileAuthInterceptorOptions = {
|
|
47
32
|
readonly partnerConfig: PartnerConfig;
|
|
@@ -73,7 +58,7 @@ export class SmileAuthInterceptor {
|
|
|
73
58
|
config: InternalAxiosRequestConfig,
|
|
74
59
|
): Promise<InternalAxiosRequestConfig> {
|
|
75
60
|
config.headers.set("smileid-token", this._tokenHolder.current);
|
|
76
|
-
config.headers.set("smileid-source-sdk",
|
|
61
|
+
config.headers.set("smileid-source-sdk", sourceSdkIdentifier());
|
|
77
62
|
config.headers.set("smileid-source-sdk-version", SDK_VERSION);
|
|
78
63
|
config.headers.set("smileid-partner-id", this._partnerConfig.partnerId);
|
|
79
64
|
if (this._partnerConfig.callbackUrl) {
|
|
@@ -125,7 +125,9 @@ describe("PayloadCompressor with real JPEG bytes", () => {
|
|
|
125
125
|
|
|
126
126
|
it(
|
|
127
127
|
"compresses an inflated selfie+liveness bundle under the caps and keeps files decodable",
|
|
128
|
-
|
|
128
|
+
// Real JPEG (re)compression is CPU-heavy; under v8 coverage instrumentation on CI it runs
|
|
129
|
+
// ~2.5x slower (~120s), so allow generous headroom over the ~120s per-test CI runtime.
|
|
130
|
+
{ timeout: 300_000 },
|
|
129
131
|
async () => {
|
|
130
132
|
const selfieUri = "file:///capture/selfie.jpg";
|
|
131
133
|
const livenessUri = "file:///capture/liveness.jpg";
|
|
@@ -172,7 +174,9 @@ describe("PayloadCompressor with real JPEG bytes", () => {
|
|
|
172
174
|
|
|
173
175
|
it(
|
|
174
176
|
"compresses a document-heavy bundle back-first and lands under the guarantee caps",
|
|
175
|
-
|
|
177
|
+
// Real JPEG (re)compression is CPU-heavy; under v8 coverage instrumentation on CI it runs
|
|
178
|
+
// ~2.5x slower (~120s), so allow generous headroom over the ~120s per-test CI runtime.
|
|
179
|
+
{ timeout: 300_000 },
|
|
176
180
|
async () => {
|
|
177
181
|
const frontUri = "file:///capture/front.jpg";
|
|
178
182
|
const backUri = "file:///capture/back.jpg";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { CacheConfigBuilder } from "../cache_config_builder";
|
|
4
|
+
import { RetryConfigBuilder, RetryStrategy } from "../retry_config_builder";
|
|
5
|
+
|
|
6
|
+
/** Retry/cache DSL builders: pin the documented defaults, frozen output, and overrides. */
|
|
7
|
+
describe("RetryConfigBuilder", () => {
|
|
8
|
+
it("builds a frozen config with the documented defaults", () => {
|
|
9
|
+
const config = new RetryConfigBuilder().build();
|
|
10
|
+
|
|
11
|
+
expect(config).toEqual({
|
|
12
|
+
maxAttempts: 1,
|
|
13
|
+
strategy: RetryStrategy.none,
|
|
14
|
+
initialDelay: 1,
|
|
15
|
+
multiplier: 2,
|
|
16
|
+
maxDelay: 32,
|
|
17
|
+
jitter: 0.2,
|
|
18
|
+
fixedDelay: 1,
|
|
19
|
+
});
|
|
20
|
+
expect(Object.isFrozen(config)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("forces maxAttempts to 1 for none, whatever the field says", () => {
|
|
24
|
+
const builder = new RetryConfigBuilder();
|
|
25
|
+
builder.maxAttempts = 5;
|
|
26
|
+
|
|
27
|
+
expect(builder.build().maxAttempts).toBe(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("applies the attempt budget once a strategy opts in", () => {
|
|
31
|
+
const builder = new RetryConfigBuilder();
|
|
32
|
+
builder.strategy = RetryStrategy.exponential;
|
|
33
|
+
|
|
34
|
+
expect(builder.build().maxAttempts).toBe(3);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("carries overridden fields through build()", () => {
|
|
38
|
+
const builder = new RetryConfigBuilder();
|
|
39
|
+
builder.maxAttempts = 5;
|
|
40
|
+
builder.strategy = RetryStrategy.fixed;
|
|
41
|
+
builder.fixedDelay = 4;
|
|
42
|
+
|
|
43
|
+
const config = builder.build();
|
|
44
|
+
|
|
45
|
+
expect(config.maxAttempts).toBe(5);
|
|
46
|
+
expect(config.strategy).toBe(RetryStrategy.fixed);
|
|
47
|
+
expect(config.fixedDelay).toBe(4);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("CacheConfigBuilder", () => {
|
|
52
|
+
it("defaults to enabled with a 50 MB ceiling", () => {
|
|
53
|
+
const config = new CacheConfigBuilder().build();
|
|
54
|
+
|
|
55
|
+
expect(config).toEqual({ enabled: true, maxSize: 50 * 1024 * 1024 });
|
|
56
|
+
expect(Object.isFrozen(config)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("megabytes() converts MB to bytes", () => {
|
|
60
|
+
expect(new CacheConfigBuilder().megabytes(10)).toBe(10 * 1024 * 1024);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -8,16 +8,16 @@ export { RetryStrategy };
|
|
|
8
8
|
/**
|
|
9
9
|
* DSL builder for request retry behaviour.
|
|
10
10
|
*
|
|
11
|
-
* Defaults to
|
|
12
|
-
* 32 s ceiling, ±20% jitter.
|
|
13
|
-
* `strategy = RetryStrategy.fixed`.
|
|
11
|
+
* Defaults to no retry. Set `strategy` to opt in, and the remaining fields
|
|
12
|
+
* then apply: 3 attempts, 1 s initial delay, 32 s ceiling, ±20% jitter.
|
|
13
|
+
* `fixedDelay` is honoured only when `strategy = RetryStrategy.fixed`.
|
|
14
14
|
*/
|
|
15
15
|
export class RetryConfigBuilder {
|
|
16
|
-
/**
|
|
16
|
+
/** Attempts per request including the first; applies once `strategy` opts in. */
|
|
17
17
|
maxAttempts = 3;
|
|
18
18
|
|
|
19
19
|
/** Backoff strategy between attempts: exponential, fixed, or none. */
|
|
20
|
-
strategy: RetryStrategy = RetryStrategy.
|
|
20
|
+
strategy: RetryStrategy = RetryStrategy.none;
|
|
21
21
|
|
|
22
22
|
/** Delay before the first retry, in seconds (exponential strategy). */
|
|
23
23
|
initialDelay = 1;
|
|
@@ -45,7 +45,7 @@ export class RetryConfigBuilder {
|
|
|
45
45
|
|
|
46
46
|
build(): RetryConfig {
|
|
47
47
|
return Object.freeze({
|
|
48
|
-
maxAttempts: this.maxAttempts,
|
|
48
|
+
maxAttempts: this.strategy === RetryStrategy.none ? 1 : this.maxAttempts,
|
|
49
49
|
strategy: this.strategy,
|
|
50
50
|
initialDelay: this.initialDelay,
|
|
51
51
|
multiplier: this.multiplier,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
AxiosError,
|
|
4
|
+
type AxiosResponse,
|
|
5
|
+
type InternalAxiosRequestConfig,
|
|
6
|
+
} from "axios";
|
|
7
|
+
|
|
8
|
+
import { JobType } from "../../../core/models/job_type";
|
|
9
|
+
import type { NetworkConfig } from "../../../core/models/network_config";
|
|
10
|
+
import { makeNetworkingFactory } from "../networking_factory";
|
|
11
|
+
import { SmileIDCryptoManager } from "../../security/smile_id_crypto_manager";
|
|
12
|
+
import {
|
|
13
|
+
REQUEST_MAC_HEADER,
|
|
14
|
+
REQUEST_MAC_OPT_IN_KEY,
|
|
15
|
+
REQUEST_TIMESTAMP_HEADER,
|
|
16
|
+
} from "../../api/interceptors/smile_request_mac_interceptor";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Standing-guard composition test for token-refresh × gzip × request-MAC on a
|
|
20
|
+
* 401 → refresh → retry. `TokenRefreshInterceptor` re-issues the config, re-running the whole
|
|
21
|
+
* chain (auth re-stamps the token, MAC re-signs, gzip re-stamps `Accept-Encoding`). If a future
|
|
22
|
+
* change made gzip replace `config.data` with compressed bytes, the MAC would sign a body the
|
|
23
|
+
* canonicalizer no longer sees as FormData → `{}`. Pins the retried MAC against the uncompressed
|
|
24
|
+
* body so that regression fails loud.
|
|
25
|
+
*/
|
|
26
|
+
describe("token-refresh x gzip x request-MAC composition", () => {
|
|
27
|
+
const initialToken = "token-1";
|
|
28
|
+
const refreshedToken = "token-2";
|
|
29
|
+
const signingSecret = "test-signing-secret";
|
|
30
|
+
const cryptoManager = new SmileIDCryptoManager(signingSecret);
|
|
31
|
+
|
|
32
|
+
type FormDataPart = { fieldName: string; string?: string; uri?: string };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Stand-in for RN's `FormData`. Besides `getParts()` (what the canonicalizer reads), it
|
|
36
|
+
* satisfies axios's `isFormData()` (`Symbol.toStringTag` + `append`) so axios's request
|
|
37
|
+
* transform leaves it untouched instead of JSON-stringifying it.
|
|
38
|
+
*/
|
|
39
|
+
class FakeFormData {
|
|
40
|
+
constructor(private readonly parts: readonly FormDataPart[]) {}
|
|
41
|
+
get [Symbol.toStringTag](): string {
|
|
42
|
+
return "FormData";
|
|
43
|
+
}
|
|
44
|
+
append(): void {
|
|
45
|
+
/* not exercised — only presence is checked by axios's isFormData() */
|
|
46
|
+
}
|
|
47
|
+
getParts(): readonly FormDataPart[] {
|
|
48
|
+
return this.parts;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function submissionBody(): FakeFormData {
|
|
53
|
+
return new FakeFormData([
|
|
54
|
+
{ fieldName: "country", string: "NG" },
|
|
55
|
+
{ fieldName: "partner_params", string: '{"flow":"enhanced"}' },
|
|
56
|
+
{
|
|
57
|
+
fieldName: "selfie_image",
|
|
58
|
+
uri: "data:application/octet-stream;base64,c2VsZmll",
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Flattens the observed AxiosHeaders to a lower-cased record, matching how
|
|
64
|
+
* RequestCanonicalizer lower-cases header names before signing. */
|
|
65
|
+
function headersRecord(
|
|
66
|
+
headers: InternalAxiosRequestConfig["headers"],
|
|
67
|
+
): Record<string, string> {
|
|
68
|
+
const out: Record<string, string> = {};
|
|
69
|
+
for (const [key, value] of Object.entries(
|
|
70
|
+
headers as unknown as Record<string, unknown>,
|
|
71
|
+
)) {
|
|
72
|
+
if (value != null) out[key.toLowerCase()] = String(value);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function makeConfig(): NetworkConfig {
|
|
78
|
+
return {
|
|
79
|
+
config: {
|
|
80
|
+
jobType: JobType.smartSelfieEnrollment,
|
|
81
|
+
token: initialToken,
|
|
82
|
+
onTokenExpired: async () => refreshedToken,
|
|
83
|
+
partnerConfig: {
|
|
84
|
+
partnerId: "partner-1",
|
|
85
|
+
callbackUrl: "https://partner.example.com/cb",
|
|
86
|
+
useSandbox: true,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
it("signs the refresh-retried request's MAC over the uncompressed submission body", async () => {
|
|
93
|
+
const requestsSeen: InternalAxiosRequestConfig[] = [];
|
|
94
|
+
const { client } = makeNetworkingFactory(makeConfig(), () => signingSecret);
|
|
95
|
+
|
|
96
|
+
// 401 first, 200 on the retry. A custom adapter bypasses axios's validateStatus, so it
|
|
97
|
+
// must reject non-2xx itself for TokenRefreshInterceptor's error handler to see it.
|
|
98
|
+
client.defaults.adapter = (
|
|
99
|
+
requestConfig: InternalAxiosRequestConfig,
|
|
100
|
+
): Promise<AxiosResponse> => {
|
|
101
|
+
requestsSeen.push(requestConfig);
|
|
102
|
+
const isFirstCall = requestsSeen.length === 1;
|
|
103
|
+
const response: AxiosResponse = {
|
|
104
|
+
status: isFirstCall ? 401 : 200,
|
|
105
|
+
statusText: isFirstCall ? "Unauthorized" : "OK",
|
|
106
|
+
data: isFirstCall ? { error: "unauthorized" } : { jobId: "job-1" },
|
|
107
|
+
headers: {},
|
|
108
|
+
config: requestConfig,
|
|
109
|
+
};
|
|
110
|
+
if (isFirstCall) {
|
|
111
|
+
return Promise.reject(
|
|
112
|
+
new AxiosError(
|
|
113
|
+
"Request failed with status code 401",
|
|
114
|
+
AxiosError.ERR_BAD_REQUEST,
|
|
115
|
+
requestConfig,
|
|
116
|
+
undefined,
|
|
117
|
+
response,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return Promise.resolve(response);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const body = submissionBody();
|
|
125
|
+
const response = await client.post("/v3/enhanced_kyc", body, {
|
|
126
|
+
[REQUEST_MAC_OPT_IN_KEY]: true,
|
|
127
|
+
} as Partial<InternalAxiosRequestConfig>);
|
|
128
|
+
|
|
129
|
+
expect(response.status).toBe(200);
|
|
130
|
+
expect(requestsSeen).toHaveLength(2);
|
|
131
|
+
|
|
132
|
+
const retried = requestsSeen[1]!;
|
|
133
|
+
const retriedHeaders = headersRecord(retried.headers);
|
|
134
|
+
|
|
135
|
+
expect(retriedHeaders["smileid-token"]).toBe(refreshedToken);
|
|
136
|
+
expect(retriedHeaders["accept-encoding"]).toBe("gzip");
|
|
137
|
+
// gzip is hint-only — it never compresses the body, so no Content-Encoding claim.
|
|
138
|
+
expect(retriedHeaders["content-encoding"]).toBeUndefined();
|
|
139
|
+
|
|
140
|
+
const retriedMac = retriedHeaders[REQUEST_MAC_HEADER];
|
|
141
|
+
const retriedTimestamp = retriedHeaders[REQUEST_TIMESTAMP_HEADER];
|
|
142
|
+
expect(retriedMac).toBeDefined();
|
|
143
|
+
expect(retriedTimestamp).toBeDefined();
|
|
144
|
+
|
|
145
|
+
// Recompute the server-verified MAC over the retried headers and the real body — gzip
|
|
146
|
+
// is hint-only (never replaces config.data), so `body` is the bytes that shipped.
|
|
147
|
+
const expectedMac = await cryptoManager.sign(
|
|
148
|
+
retriedTimestamp!,
|
|
149
|
+
retriedHeaders,
|
|
150
|
+
body,
|
|
151
|
+
);
|
|
152
|
+
expect(retriedMac).toBe(expectedMac);
|
|
153
|
+
|
|
154
|
+
// And it must not match a MAC computed against an empty body — the
|
|
155
|
+
// signature of the regression this test guards against.
|
|
156
|
+
const emptyBodyMac = await cryptoManager.sign(
|
|
157
|
+
retriedTimestamp!,
|
|
158
|
+
retriedHeaders,
|
|
159
|
+
undefined,
|
|
160
|
+
);
|
|
161
|
+
expect(retriedMac).not.toBe(emptyBodyMac);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -51,7 +51,7 @@ const CONSENT: ConsentInformation = Object.freeze({
|
|
|
51
51
|
// BVN path short-circuits before the API serializes it, but the field
|
|
52
52
|
// is required on `BaseJobRequest` so the fixture must supply one.
|
|
53
53
|
const METADATA = new UseSmileIDMetadata({
|
|
54
|
-
sdkVersion: "12.0.
|
|
54
|
+
sdkVersion: "12.0.1",
|
|
55
55
|
sdk: "react-native-expo",
|
|
56
56
|
deviceOs: "",
|
|
57
57
|
deviceModel: "",
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { RequestCanonicalizer } from "../request_canonicalizer";
|
|
4
|
+
import { SmileIDCryptoManager } from "../smile_id_crypto_manager";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Golden-vector suite for `RequestCanonicalizer`: pins the exact canonical bytes and the resulting
|
|
8
|
+
* `smileid-request-mac` for a fixed key across the shapes `canonicalize()` distinguishes. This
|
|
9
|
+
* SDK's own contract with the server, not a cross-SDK guarantee.
|
|
10
|
+
*/
|
|
11
|
+
const TIMESTAMP = "1700000000000";
|
|
12
|
+
const SIGNING_SECRET = "golden-vector-secret";
|
|
13
|
+
const cryptoManager = new SmileIDCryptoManager(SIGNING_SECRET);
|
|
14
|
+
|
|
15
|
+
const BASE_HEADERS = {
|
|
16
|
+
"smileid-token": "golden-token",
|
|
17
|
+
"smileid-request-timestamp": TIMESTAMP,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type FormDataPart = { fieldName: string; string?: string; uri?: string };
|
|
21
|
+
|
|
22
|
+
/** Minimal stand-in for React Native's `FormData` (only `getParts` is read). */
|
|
23
|
+
function formData(parts: readonly FormDataPart[]) {
|
|
24
|
+
return { getParts: () => parts };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function decode(bytes: Uint8Array): string {
|
|
28
|
+
return Buffer.from(bytes).toString("utf-8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MULTIPART_BODY = formData([
|
|
32
|
+
{ fieldName: "country", string: "NG" },
|
|
33
|
+
{ fieldName: "allow_new_enroll", string: "true" },
|
|
34
|
+
{ fieldName: "partner_params", string: '{"job_id":"j1"}' },
|
|
35
|
+
{
|
|
36
|
+
fieldName: "selfie_image",
|
|
37
|
+
uri: "data:application/octet-stream;base64,c2VsZmll",
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
// Same logical fields as MULTIPART_BODY, in reverse part order.
|
|
42
|
+
const MULTIPART_BODY_REORDERED = formData([
|
|
43
|
+
{
|
|
44
|
+
fieldName: "selfie_image",
|
|
45
|
+
uri: "data:application/octet-stream;base64,c2VsZmll",
|
|
46
|
+
},
|
|
47
|
+
{ fieldName: "partner_params", string: '{"job_id":"j1"}' },
|
|
48
|
+
{ fieldName: "allow_new_enroll", string: "true" },
|
|
49
|
+
{ fieldName: "country", string: "NG" },
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const MIXED_CASE_HEADERS = {
|
|
53
|
+
"Smileid-Token": "golden-token",
|
|
54
|
+
"SMILEID-REQUEST-TIMESTAMP": TIMESTAMP,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe("RequestCanonicalizer golden vectors", () => {
|
|
58
|
+
it("(a) canonicalizes a bodyless request to the signed headers + `{}`", async () => {
|
|
59
|
+
const bytes = await RequestCanonicalizer.canonicalize(
|
|
60
|
+
BASE_HEADERS,
|
|
61
|
+
undefined,
|
|
62
|
+
);
|
|
63
|
+
expect(decode(bytes)).toBe(
|
|
64
|
+
'{"smileid-request-timestamp":"1700000000000","smileid-token":"golden-token"}{}',
|
|
65
|
+
);
|
|
66
|
+
expect(await cryptoManager.sign(TIMESTAMP, BASE_HEADERS, undefined)).toBe(
|
|
67
|
+
"qikeRL0jNa7bZKUZdoN7tY30VvVy4ol/hcMdyg4PU4w=",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("(b) canonicalizes a raw JSON-string body the same as a bodyless request — only FormData-shaped bodies are reconstructed", async () => {
|
|
72
|
+
const bytes = await RequestCanonicalizer.canonicalize(
|
|
73
|
+
BASE_HEADERS,
|
|
74
|
+
'{"hello":"world"}',
|
|
75
|
+
);
|
|
76
|
+
// Same canonical bytes as case (a): a JSON string isn't FormData-like,
|
|
77
|
+
// so canonicalBody() falls through to the same `{}` as no body at all.
|
|
78
|
+
expect(decode(bytes)).toBe(
|
|
79
|
+
'{"smileid-request-timestamp":"1700000000000","smileid-token":"golden-token"}{}',
|
|
80
|
+
);
|
|
81
|
+
expect(
|
|
82
|
+
await cryptoManager.sign(TIMESTAMP, BASE_HEADERS, '{"hello":"world"}'),
|
|
83
|
+
).toBe("qikeRL0jNa7bZKUZdoN7tY30VvVy4ol/hcMdyg4PU4w=");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("(c) canonicalizes a multipart FormData body: scalar, boolean-coerced, JSON, and file parts", async () => {
|
|
87
|
+
const bytes = await RequestCanonicalizer.canonicalize(
|
|
88
|
+
BASE_HEADERS,
|
|
89
|
+
MULTIPART_BODY,
|
|
90
|
+
);
|
|
91
|
+
expect(decode(bytes)).toBe(
|
|
92
|
+
'{"smileid-request-timestamp":"1700000000000","smileid-token":"golden-token"}' +
|
|
93
|
+
'{"allow_new_enroll":true,"country":"NG","partner_params":{"job_id":"j1"},"selfie_image":"c2VsZmll"}',
|
|
94
|
+
);
|
|
95
|
+
expect(
|
|
96
|
+
await cryptoManager.sign(TIMESTAMP, BASE_HEADERS, MULTIPART_BODY),
|
|
97
|
+
).toBe("eNJqKR45aNiou4JM0yVRNYrTdhfg1VblWvsbrRndoLQ=");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("(d) part ordering does not change the canonical bytes or the MAC", async () => {
|
|
101
|
+
const bytes = await RequestCanonicalizer.canonicalize(
|
|
102
|
+
BASE_HEADERS,
|
|
103
|
+
MULTIPART_BODY_REORDERED,
|
|
104
|
+
);
|
|
105
|
+
expect(decode(bytes)).toBe(
|
|
106
|
+
'{"smileid-request-timestamp":"1700000000000","smileid-token":"golden-token"}' +
|
|
107
|
+
'{"allow_new_enroll":true,"country":"NG","partner_params":{"job_id":"j1"},"selfie_image":"c2VsZmll"}',
|
|
108
|
+
);
|
|
109
|
+
expect(
|
|
110
|
+
await cryptoManager.sign(
|
|
111
|
+
TIMESTAMP,
|
|
112
|
+
BASE_HEADERS,
|
|
113
|
+
MULTIPART_BODY_REORDERED,
|
|
114
|
+
),
|
|
115
|
+
).toBe("eNJqKR45aNiou4JM0yVRNYrTdhfg1VblWvsbrRndoLQ=");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("(e) header key casing does not change the canonical bytes or the MAC", async () => {
|
|
119
|
+
const bytes = await RequestCanonicalizer.canonicalize(
|
|
120
|
+
MIXED_CASE_HEADERS,
|
|
121
|
+
undefined,
|
|
122
|
+
);
|
|
123
|
+
// Identical to case (a): canonicalHeaders() lower-cases every header
|
|
124
|
+
// name before signing, so casing at the call site is not observable.
|
|
125
|
+
expect(decode(bytes)).toBe(
|
|
126
|
+
'{"smileid-request-timestamp":"1700000000000","smileid-token":"golden-token"}{}',
|
|
127
|
+
);
|
|
128
|
+
expect(
|
|
129
|
+
await cryptoManager.sign(TIMESTAMP, MIXED_CASE_HEADERS, undefined),
|
|
130
|
+
).toBe("qikeRL0jNa7bZKUZdoN7tY30VvVy4ol/hcMdyg4PU4w=");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* kept so the production host can diverge without touching call sites.
|
|
6
6
|
*/
|
|
7
7
|
export const SmileIDUrls = {
|
|
8
|
-
sandbox: "https://
|
|
8
|
+
sandbox: "https://testapi.smileidentity.com/",
|
|
9
9
|
production: "https://api.smileidentity.com/",
|
|
10
10
|
forConfig(useSandbox: boolean): string {
|
|
11
11
|
return useSandbox ? this.sandbox : this.production;
|
|
@@ -154,4 +154,29 @@ describe("withRetry", () => {
|
|
|
154
154
|
});
|
|
155
155
|
expect(block).toHaveBeenCalledTimes(3); // maxAttempts
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
it('strategy="none" never retries, even with an attempt budget', async () => {
|
|
159
|
+
// A config can carry both; the strategy wins.
|
|
160
|
+
const block = vi.fn().mockRejectedValue(makeAxiosError(503));
|
|
161
|
+
const config: RetryConfig = {
|
|
162
|
+
...exponentialConfig,
|
|
163
|
+
strategy: RetryStrategy.none,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
await expect(withRetry(config, block)).rejects.toMatchObject({
|
|
167
|
+
response: { status: 503 },
|
|
168
|
+
});
|
|
169
|
+
expect(block).toHaveBeenCalledTimes(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("makes one attempt and surfaces the real error when maxAttempts is 0", async () => {
|
|
173
|
+
// `attempt < 0` never ran the body, so the old code rejected with `undefined`.
|
|
174
|
+
const block = vi.fn().mockRejectedValue(makeAxiosError(503));
|
|
175
|
+
const config: RetryConfig = { ...exponentialConfig, maxAttempts: 0 };
|
|
176
|
+
|
|
177
|
+
await expect(withRetry(config, block)).rejects.toMatchObject({
|
|
178
|
+
response: { status: 503 },
|
|
179
|
+
});
|
|
180
|
+
expect(block).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
157
182
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { isAxiosError } from "axios";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
RetryStrategy,
|
|
4
|
+
type RetryConfig,
|
|
5
|
+
} from "../../core/models/network_config";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Retry helper. Retries `block` until success or `maxAttempts`
|
|
@@ -15,6 +18,11 @@ export async function withRetry<T>(
|
|
|
15
18
|
config: RetryConfig,
|
|
16
19
|
block: (attempt: number) => Promise<T>,
|
|
17
20
|
): Promise<T> {
|
|
21
|
+
// Without this the loop below never runs and rejects with `undefined`.
|
|
22
|
+
if (config.strategy === RetryStrategy.none || config.maxAttempts <= 1) {
|
|
23
|
+
return block(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
let lastError: unknown;
|
|
19
27
|
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
|
|
20
28
|
try {
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ export {
|
|
|
27
27
|
// subclass holds a stable `errorCode` and `severity` so partner
|
|
28
28
|
// tooling can pivot on either without parsing the message text.
|
|
29
29
|
export {
|
|
30
|
+
AgentModeWithEnhancedLivenessException,
|
|
30
31
|
BuilderValidationException,
|
|
31
32
|
ConsentDeniedException,
|
|
32
33
|
DuplicateScreenTypeException,
|
|
@@ -225,6 +226,11 @@ export {
|
|
|
225
226
|
type UseSmileIDResult,
|
|
226
227
|
} from "./core/models/result";
|
|
227
228
|
export type { ConfigOptions } from "./core/models/config";
|
|
229
|
+
export {
|
|
230
|
+
GlobalPrivacyControl,
|
|
231
|
+
GLOBAL_PRIVACY_CONTROL_WIRE_KEY,
|
|
232
|
+
globalPrivacyControlFromWire,
|
|
233
|
+
} from "./core/models/global_privacy_control";
|
|
228
234
|
export { LogLevel } from "./core/models/network_config";
|
|
229
235
|
export type {
|
|
230
236
|
CacheConfig,
|
|
@@ -374,10 +380,6 @@ export {
|
|
|
374
380
|
UseSmileIDDocumentOverlay,
|
|
375
381
|
type UseSmileIDDocumentOverlayProps,
|
|
376
382
|
} from "./presentation/components/use_smile_id_document_overlay";
|
|
377
|
-
export {
|
|
378
|
-
UseSmileIDFaceSilhouetteOverlay,
|
|
379
|
-
type UseSmileIDFaceSilhouetteOverlayProps,
|
|
380
|
-
} from "./presentation/components/use_smile_id_face_silhouette_overlay";
|
|
381
383
|
export {
|
|
382
384
|
UseSmileIDInstructionsHero,
|
|
383
385
|
type UseSmileIDInstructionsHeroProps,
|