@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.
Files changed (255) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.d.ts +11 -0
  3. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.d.ts.map +1 -0
  4. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.js +17 -0
  5. package/dist/core/exception/agent_mode_with_enhanced_liveness_exception.js.map +1 -0
  6. package/dist/core/exception/capture_validation_exceptions.d.ts +1 -0
  7. package/dist/core/exception/capture_validation_exceptions.d.ts.map +1 -1
  8. package/dist/core/exception/capture_validation_exceptions.js +1 -0
  9. package/dist/core/exception/capture_validation_exceptions.js.map +1 -1
  10. package/dist/core/exception/index.d.ts +1 -0
  11. package/dist/core/exception/index.d.ts.map +1 -1
  12. package/dist/core/exception/index.js +1 -0
  13. package/dist/core/exception/index.js.map +1 -1
  14. package/dist/core/exception/use_smile_id_error_code.d.ts +1 -0
  15. package/dist/core/exception/use_smile_id_error_code.d.ts.map +1 -1
  16. package/dist/core/exception/use_smile_id_error_code.js +1 -0
  17. package/dist/core/exception/use_smile_id_error_code.js.map +1 -1
  18. package/dist/core/models/config.d.ts +19 -0
  19. package/dist/core/models/config.d.ts.map +1 -1
  20. package/dist/core/models/document_type.d.ts +1 -1
  21. package/dist/core/models/document_type.d.ts.map +1 -1
  22. package/dist/core/models/document_type.js +2 -2
  23. package/dist/core/models/document_type.js.map +1 -1
  24. package/dist/core/models/global_privacy_control.d.ts +36 -0
  25. package/dist/core/models/global_privacy_control.d.ts.map +1 -0
  26. package/dist/core/models/global_privacy_control.js +39 -0
  27. package/dist/core/models/global_privacy_control.js.map +1 -0
  28. package/dist/core/models/network_config.d.ts +2 -2
  29. package/dist/core/models/network_config.js +2 -2
  30. package/dist/data/api/interceptors/smile_auth_interceptor.d.ts.map +1 -1
  31. package/dist/data/api/interceptors/smile_auth_interceptor.js +3 -17
  32. package/dist/data/api/interceptors/smile_auth_interceptor.js.map +1 -1
  33. package/dist/data/api/smile_id_api.d.ts +1 -1
  34. package/dist/data/api/smile_id_api.d.ts.map +1 -1
  35. package/dist/data/dsl/builder/retry_config_builder.d.ts +4 -4
  36. package/dist/data/dsl/builder/retry_config_builder.d.ts.map +1 -1
  37. package/dist/data/dsl/builder/retry_config_builder.js +6 -6
  38. package/dist/data/dsl/builder/retry_config_builder.js.map +1 -1
  39. package/dist/data/smile_id_urls.d.ts +1 -1
  40. package/dist/data/smile_id_urls.js +1 -1
  41. package/dist/data/smile_id_urls.js.map +1 -1
  42. package/dist/data/util/retry_util.d.ts +1 -1
  43. package/dist/data/util/retry_util.d.ts.map +1 -1
  44. package/dist/data/util/retry_util.js +5 -0
  45. package/dist/data/util/retry_util.js.map +1 -1
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/metadata/builder_configuration.d.ts +5 -0
  51. package/dist/metadata/builder_configuration.d.ts.map +1 -1
  52. package/dist/metadata/builder_configuration.js +1 -0
  53. package/dist/metadata/builder_configuration.js.map +1 -1
  54. package/dist/metadata/metadata_configuration_mappers.d.ts +2 -3
  55. package/dist/metadata/metadata_configuration_mappers.d.ts.map +1 -1
  56. package/dist/metadata/metadata_configuration_mappers.js +13 -6
  57. package/dist/metadata/metadata_configuration_mappers.js.map +1 -1
  58. package/dist/metadata/use_smile_id_metadata_factory.d.ts.map +1 -1
  59. package/dist/metadata/use_smile_id_metadata_factory.js +14 -5
  60. package/dist/metadata/use_smile_id_metadata_factory.js.map +1 -1
  61. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.d.ts +5 -2
  62. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.d.ts.map +1 -1
  63. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.js +8 -3
  64. package/dist/presentation/components/use_smile_id_enhanced_smart_selfie_clip.js.map +1 -1
  65. package/dist/presentation/components/use_smile_id_instructions_hero.d.ts.map +1 -1
  66. package/dist/presentation/components/use_smile_id_instructions_hero.js +8 -3
  67. package/dist/presentation/components/use_smile_id_instructions_hero.js.map +1 -1
  68. package/dist/presentation/flow/dsl/builder/config_builder.d.ts +16 -0
  69. package/dist/presentation/flow/dsl/builder/config_builder.d.ts.map +1 -1
  70. package/dist/presentation/flow/dsl/builder/config_builder.js +18 -0
  71. package/dist/presentation/flow/dsl/builder/config_builder.js.map +1 -1
  72. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts +3 -6
  73. package/dist/presentation/flow/dsl/builder/screens_builder.d.ts.map +1 -1
  74. package/dist/presentation/flow/dsl/builder/screens_builder.js +3 -6
  75. package/dist/presentation/flow/dsl/builder/screens_builder.js.map +1 -1
  76. package/dist/presentation/flow/flow_navigation_manager.d.ts +5 -1
  77. package/dist/presentation/flow/flow_navigation_manager.d.ts.map +1 -1
  78. package/dist/presentation/flow/flow_navigation_manager.js +7 -1
  79. package/dist/presentation/flow/flow_navigation_manager.js.map +1 -1
  80. package/dist/presentation/flow/model/instructions_defaults.d.ts.map +1 -1
  81. package/dist/presentation/flow/model/instructions_defaults.js +1 -1
  82. package/dist/presentation/flow/model/instructions_defaults.js.map +1 -1
  83. package/dist/presentation/flow/screen/orchestrated_capture_screen.d.ts.map +1 -1
  84. package/dist/presentation/flow/screen/orchestrated_capture_screen.js +13 -3
  85. package/dist/presentation/flow/screen/orchestrated_capture_screen.js.map +1 -1
  86. package/dist/presentation/flow/screen/orchestrated_consent_screen.d.ts.map +1 -1
  87. package/dist/presentation/flow/screen/orchestrated_consent_screen.js +3 -1
  88. package/dist/presentation/flow/screen/orchestrated_consent_screen.js.map +1 -1
  89. package/dist/presentation/flow/screen/orchestrated_preview_screen.d.ts.map +1 -1
  90. package/dist/presentation/flow/screen/orchestrated_preview_screen.js +0 -8
  91. package/dist/presentation/flow/screen/orchestrated_preview_screen.js.map +1 -1
  92. package/dist/presentation/flow/use_smile_id_flow.d.ts.map +1 -1
  93. package/dist/presentation/flow/use_smile_id_flow.js +10 -4
  94. package/dist/presentation/flow/use_smile_id_flow.js.map +1 -1
  95. package/dist/presentation/flow/validation/flow_validator.d.ts.map +1 -1
  96. package/dist/presentation/flow/validation/flow_validator.js +42 -20
  97. package/dist/presentation/flow/validation/flow_validator.js.map +1 -1
  98. package/dist/presentation/screen/helpers/capture_ui_state.d.ts +6 -6
  99. package/dist/presentation/screen/helpers/capture_ui_state.d.ts.map +1 -1
  100. package/dist/presentation/screen/helpers/capture_ui_state.js +19 -19
  101. package/dist/presentation/screen/helpers/capture_ui_state.js.map +1 -1
  102. package/dist/presentation/screen/helpers/ess_turn_haptics.d.ts +2 -2
  103. package/dist/presentation/screen/helpers/ess_turn_haptics.d.ts.map +1 -1
  104. package/dist/presentation/screen/helpers/ess_turn_haptics.js +6 -6
  105. package/dist/presentation/screen/helpers/ess_turn_haptics.js.map +1 -1
  106. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.d.ts +5 -5
  107. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.d.ts.map +1 -1
  108. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.js +11 -11
  109. package/dist/presentation/screen/helpers/scan_state_to_overlay_state.js.map +1 -1
  110. package/dist/presentation/screen/helpers/use_capture_orientation_lock.d.ts +13 -3
  111. package/dist/presentation/screen/helpers/use_capture_orientation_lock.d.ts.map +1 -1
  112. package/dist/presentation/screen/helpers/use_capture_orientation_lock.js +48 -6
  113. package/dist/presentation/screen/helpers/use_capture_orientation_lock.js.map +1 -1
  114. package/dist/presentation/screen/use_smile_id_capture_screen.d.ts +4 -2
  115. package/dist/presentation/screen/use_smile_id_capture_screen.d.ts.map +1 -1
  116. package/dist/presentation/screen/use_smile_id_capture_screen.js +36 -15
  117. package/dist/presentation/screen/use_smile_id_capture_screen.js.map +1 -1
  118. package/dist/presentation/screen/use_smile_id_consent_screen.d.ts +7 -0
  119. package/dist/presentation/screen/use_smile_id_consent_screen.d.ts.map +1 -1
  120. package/dist/presentation/screen/use_smile_id_consent_screen.js +6 -5
  121. package/dist/presentation/screen/use_smile_id_consent_screen.js.map +1 -1
  122. package/dist/presentation/screen/use_smile_id_document_back_instructions.d.ts.map +1 -1
  123. package/dist/presentation/screen/use_smile_id_document_back_instructions.js +5 -13
  124. package/dist/presentation/screen/use_smile_id_document_back_instructions.js.map +1 -1
  125. package/dist/presentation/screen/use_smile_id_document_preview_screen.js +1 -1
  126. package/dist/presentation/screen/use_smile_id_document_preview_screen.js.map +1 -1
  127. package/dist/presentation/screen/use_smile_id_preview_screen.d.ts +1 -21
  128. package/dist/presentation/screen/use_smile_id_preview_screen.d.ts.map +1 -1
  129. package/dist/presentation/screen/use_smile_id_preview_screen.js +3 -14
  130. package/dist/presentation/screen/use_smile_id_preview_screen.js.map +1 -1
  131. package/dist/presentation/screen/use_smile_id_processing_screen.js +1 -1
  132. package/dist/presentation/screen/use_smile_id_processing_screen.js.map +1 -1
  133. package/dist/presentation/utils/locale_utils.d.ts +10 -4
  134. package/dist/presentation/utils/locale_utils.d.ts.map +1 -1
  135. package/dist/presentation/utils/locale_utils.js +13 -5
  136. package/dist/presentation/utils/locale_utils.js.map +1 -1
  137. package/dist/testing/fake_face_analyzer.d.ts +43 -0
  138. package/dist/testing/fake_face_analyzer.d.ts.map +1 -0
  139. package/dist/testing/fake_face_analyzer.js +52 -0
  140. package/dist/testing/fake_face_analyzer.js.map +1 -0
  141. package/dist/testing/index.d.ts +5 -0
  142. package/dist/testing/index.d.ts.map +1 -0
  143. package/dist/testing/index.js +7 -0
  144. package/dist/testing/index.js.map +1 -0
  145. package/dist/testing/scenario_expectations.d.ts +28 -0
  146. package/dist/testing/scenario_expectations.d.ts.map +1 -0
  147. package/dist/testing/scenario_expectations.js +65 -0
  148. package/dist/testing/scenario_expectations.js.map +1 -0
  149. package/dist/testing/scenario_loader.d.ts +11 -0
  150. package/dist/testing/scenario_loader.d.ts.map +1 -0
  151. package/dist/testing/scenario_loader.js +64 -0
  152. package/dist/testing/scenario_loader.js.map +1 -0
  153. package/dist/testing/scripted_scan_results.d.ts +17 -0
  154. package/dist/testing/scripted_scan_results.d.ts.map +1 -0
  155. package/dist/testing/scripted_scan_results.js +52 -0
  156. package/dist/testing/scripted_scan_results.js.map +1 -0
  157. package/package.json +11 -4
  158. package/src/__tests__/dsl.test.ts +51 -1
  159. package/src/__tests__/setup_react_native.ts +17 -2
  160. package/src/__tests__/snapshots/__snapshots__/screens_structural.test.tsx.snap +5261 -0
  161. package/src/__tests__/snapshots/screens_structural.test.tsx +230 -0
  162. package/src/camera/__tests__/use_smile_id_camera_system.test.ts +103 -0
  163. package/src/core/exception/__tests__/builder_validation_exceptions.test.ts +67 -0
  164. package/src/core/exception/agent_mode_with_enhanced_liveness_exception.ts +20 -0
  165. package/src/core/exception/capture_validation_exceptions.ts +1 -0
  166. package/src/core/exception/index.ts +1 -0
  167. package/src/core/exception/use_smile_id_error_code.ts +1 -0
  168. package/src/core/models/__tests__/document_type.test.ts +2 -2
  169. package/src/core/models/__tests__/flow_models.test.ts +26 -0
  170. package/src/core/models/config.ts +22 -0
  171. package/src/core/models/document_type.ts +2 -2
  172. package/src/core/models/global_privacy_control.ts +45 -0
  173. package/src/core/models/network_config.ts +2 -2
  174. package/src/data/api/__tests__/smile_id_api.test.ts +1 -1
  175. package/src/data/api/interceptors/__tests__/gzip_interceptor.test.ts +83 -0
  176. package/src/data/api/interceptors/smile_auth_interceptor.ts +3 -18
  177. package/src/data/compression/__tests__/payload_compressor_real_jpeg.test.ts +6 -2
  178. package/src/data/dsl/builder/__tests__/config_builders.test.ts +62 -0
  179. package/src/data/dsl/builder/retry_config_builder.ts +6 -6
  180. package/src/data/factory/__tests__/retry_gzip_mac_composition.test.ts +163 -0
  181. package/src/data/factory/__tests__/signed_job_submission_headers.test.ts +1 -1
  182. package/src/data/repository/__tests__/network_retries.test.ts +1 -1
  183. package/src/data/repository/__tests__/use_smile_id_repository.test.ts +1 -1
  184. package/src/data/security/__tests__/request_canonicalizer.test.ts +132 -0
  185. package/src/data/smile_id_urls.ts +1 -1
  186. package/src/data/util/__tests__/retry_util.test.ts +25 -0
  187. package/src/data/util/retry_util.ts +9 -1
  188. package/src/index.ts +6 -4
  189. package/src/metadata/__tests__/metadata_configuration_mappers.test.ts +39 -0
  190. package/src/metadata/__tests__/metadata_configuration_objects.test.ts +2 -1
  191. package/src/metadata/__tests__/metadata_device_context.test.ts +3 -3
  192. package/src/metadata/__tests__/metadata_device_os_android.test.ts +18 -0
  193. package/src/metadata/__tests__/metadata_golden_fixture.json +1 -1
  194. package/src/metadata/__tests__/metadata_payload_golden.test.ts +4 -4
  195. package/src/metadata/__tests__/use_smile_id_metadata.test.ts +1 -1
  196. package/src/metadata/builder_configuration.ts +6 -0
  197. package/src/metadata/metadata_configuration_mappers.ts +21 -7
  198. package/src/metadata/use_smile_id_metadata_factory.ts +17 -6
  199. package/src/presentation/assets/si_document_green_book_hero_animation.json +1 -4668
  200. package/src/presentation/assets/si_document_hero_animation.json +1 -4799
  201. package/src/presentation/assets/si_document_hero_back_animation.json +1 -2039
  202. package/src/presentation/assets/si_document_passport_hero_animation.json +1 -14491
  203. package/src/presentation/assets/si_ess_all_directions_hero_animation.json +1 -0
  204. package/src/presentation/assets/si_ess_animation_all_directions.json +1 -6589
  205. package/src/presentation/assets/si_selfie_hero_animation.json +1 -1439
  206. package/src/presentation/assets/si_smile_to_capture_animation.json +1 -1439
  207. package/src/presentation/components/__tests__/use_smile_id_enhanced_smart_selfie_clip.test.tsx +9 -4
  208. package/src/presentation/components/__tests__/use_smile_id_instructions_hero.test.tsx +44 -0
  209. package/src/presentation/components/use_smile_id_enhanced_smart_selfie_clip.tsx +9 -3
  210. package/src/presentation/components/use_smile_id_instructions_hero.tsx +10 -0
  211. package/src/presentation/flow/__tests__/document_flow_navigation.test.ts +3 -3
  212. package/src/presentation/flow/__tests__/flow_navigation_manager.test.ts +49 -0
  213. package/src/presentation/flow/dsl/builder/__tests__/config_builder.test.ts +25 -0
  214. package/src/presentation/flow/dsl/builder/config_builder.ts +21 -0
  215. package/src/presentation/flow/dsl/builder/screens_builder.ts +3 -6
  216. package/src/presentation/flow/flow_navigation_manager.ts +6 -1
  217. package/src/presentation/flow/model/instructions_defaults.ts +1 -1
  218. package/src/presentation/flow/screen/orchestrated_capture_screen.tsx +18 -3
  219. package/src/presentation/flow/screen/orchestrated_consent_screen.tsx +5 -0
  220. package/src/presentation/flow/screen/orchestrated_preview_screen.tsx +0 -12
  221. package/src/presentation/flow/use_smile_id_flow.tsx +11 -4
  222. package/src/presentation/flow/validation/__tests__/flow_validator.test.ts +61 -1
  223. package/src/presentation/flow/validation/flow_validator.ts +55 -26
  224. package/src/presentation/screen/__tests__/capture_ui_state.test.ts +66 -47
  225. package/src/presentation/screen/__tests__/ess_turn_haptics.test.ts +15 -11
  226. package/src/presentation/screen/__tests__/scan_state_to_overlay_state.test.ts +47 -27
  227. package/src/presentation/screen/__tests__/use_smile_id_capture_screen.test.tsx +176 -14
  228. package/src/presentation/screen/__tests__/use_smile_id_document_back_instructions.test.tsx +1 -1
  229. package/src/presentation/screen/__tests__/use_smile_id_preview_screen.test.tsx +10 -40
  230. package/src/presentation/screen/helpers/__tests__/use_capture_orientation_lock.test.tsx +212 -0
  231. package/src/presentation/screen/helpers/capture_ui_state.ts +27 -27
  232. package/src/presentation/screen/helpers/ess_turn_haptics.ts +9 -9
  233. package/src/presentation/screen/helpers/scan_state_to_overlay_state.ts +13 -13
  234. package/src/presentation/screen/helpers/use_capture_orientation_lock.ts +69 -7
  235. package/src/presentation/screen/use_smile_id_capture_screen.tsx +47 -19
  236. package/src/presentation/screen/use_smile_id_consent_screen.tsx +16 -5
  237. package/src/presentation/screen/use_smile_id_document_back_instructions.tsx +14 -23
  238. package/src/presentation/screen/use_smile_id_document_preview_screen.tsx +1 -1
  239. package/src/presentation/screen/use_smile_id_preview_screen.tsx +13 -45
  240. package/src/presentation/screen/use_smile_id_processing_screen.tsx +1 -1
  241. package/src/presentation/utils/__tests__/locale_utils.test.ts +46 -5
  242. package/src/presentation/utils/locale_utils.ts +13 -4
  243. package/src/testing/__tests__/fake_face_analyzer.test.ts +42 -0
  244. package/src/testing/__tests__/scenario_expectations.test.ts +110 -0
  245. package/src/testing/__tests__/scenario_loader.test.ts +64 -0
  246. package/src/testing/fake_face_analyzer.ts +82 -0
  247. package/src/testing/index.ts +15 -0
  248. package/src/testing/scenario_expectations.ts +87 -0
  249. package/src/testing/scenario_loader.ts +83 -0
  250. package/src/testing/scripted_scan_results.ts +66 -0
  251. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.d.ts +0 -15
  252. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.d.ts.map +0 -1
  253. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.js +0 -19
  254. package/dist/presentation/components/use_smile_id_face_silhouette_overlay.js.map +0 -1
  255. 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 { Platform } from "react-native";
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.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", sourceSdk());
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
- { timeout: 120_000 },
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
- { timeout: 120_000 },
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 exponential backoff with 3 attempts, 1 s initial delay,
12
- * 32 s ceiling, ±20% jitter. `fixedDelay` is honoured only when
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
- /** Maximum number of attempts per request, including the first. */
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.exponential;
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
+ });
@@ -36,7 +36,7 @@ function makeBaseConfig(): NetworkConfig {
36
36
  }
37
37
 
38
38
  const metadata = new UseSmileIDMetadata({
39
- sdkVersion: "12.0.0",
39
+ sdkVersion: "12.0.1",
40
40
  sdk: "react-native-expo",
41
41
  deviceOs: "",
42
42
  deviceModel: "",
@@ -34,7 +34,7 @@ const CONSENT: ConsentInformation = Object.freeze({
34
34
  });
35
35
 
36
36
  const METADATA = new UseSmileIDMetadata({
37
- sdkVersion: "12.0.0",
37
+ sdkVersion: "12.0.1",
38
38
  sdk: "react-native-expo",
39
39
  deviceOs: "",
40
40
  deviceModel: "",
@@ -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.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://devapi.smileidentity.com/",
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 type { RetryConfig } from "../../core/models/network_config";
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,