@authme/identity-verification 2.8.38 → 2.8.42

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/index.cjs CHANGED
@@ -29087,6 +29087,7 @@ const fasRecognitionResultMapping = fasRecognitionResult => {
29087
29087
  [liveness.FasRecognitionResult.Pass]: core.StatusDescription.Pass,
29088
29088
  [liveness.FasRecognitionResult.Error]: core.StatusDescription.Error,
29089
29089
  [liveness.FasRecognitionResult.NeedMoreFrame]: core.StatusDescription.NeedMoreFrame,
29090
+ [liveness.FasRecognitionResult.Motion]: core.StatusDescription.Motion,
29090
29091
  [liveness.FasRecognitionResult.NeedFaceToCamera]: core.StatusDescription.NeedFaceToCamera
29091
29092
  };
29092
29093
  return (_a = fasServiceStageMap[fasRecognitionResult]) !== null && _a !== void 0 ? _a : core.StatusDescription.Failed;
@@ -29200,7 +29201,12 @@ const sendFrame = (canvasSizeInfo, canvas, video, frameCallback, fps, bas64Forma
29200
29201
  const ctx = canvas.getContext('2d', {
29201
29202
  willReadFrequently: true
29202
29203
  });
29203
- return source$.pipe(rxjs.mergeMap(() => rxjs.animationFrames().pipe(limitFPS(fps), rxjs.filter(() => received && !(flags === null || flags === void 0 ? void 0 : flags.animating)), rxjs.tap(() => received = false), rxjs.tap(() => util.clearCanvas(canvas)), rxjs.map(() => util.getImageData(canvas, ctx, video, canvasSizeInfo, bas64Format, imageType)), rxjs.mergeMap(imageData => rxjs.from(frameCallback(imageData.data, imageData.base64, cardType, type)).pipe(rxjs.catchError(e => {
29204
+ return source$.pipe(rxjs.mergeMap(() => rxjs.animationFrames().pipe(limitFPS(fps), rxjs.filter(() => received && !(flags === null || flags === void 0 ? void 0 : flags.animating)), rxjs.tap(() => received = false), rxjs.tap(() => util.clearCanvas(canvas)), rxjs.map(() => util.getImageData(canvas, ctx, video, canvasSizeInfo, bas64Format, imageType)), rxjs.tap(imageData => {
29205
+ // 如果 imageData 為 null(canvas 尺寸無效),重置 received 讓下一幀可以繼續
29206
+ if (imageData === null) {
29207
+ received = true;
29208
+ }
29209
+ }), rxjs.filter(imageData => imageData !== null), rxjs.mergeMap(imageData => rxjs.from(frameCallback(imageData.data, imageData.base64, cardType, type)).pipe(rxjs.catchError(e => {
29204
29210
  // send to fast, ignore
29205
29211
  if (e instanceof core.AuthmeError && e.code === core.ErrorCode.RECOGNITION_NOT_AVAILABLE) {
29206
29212
  return rxjs.EMPTY;
@@ -29362,78 +29368,6 @@ const modal = arg => {
29362
29368
  authmeContainer.appendChild(domModal);
29363
29369
  };
29364
29370
 
29365
- const popupView = arg => {
29366
- const authmeContainer = document.querySelector('.authme-container');
29367
- if (!authmeContainer) {
29368
- console.error('modal: authmeContainer not found');
29369
- return;
29370
- }
29371
- const uiThemeConfig = util.Storage.getItem('themeConfig');
29372
- function removePopview() {
29373
- var _a;
29374
- (_a = document.querySelector('.video-container__popupview')) === null || _a === void 0 ? void 0 : _a.remove();
29375
- }
29376
- const domPopupView = document.createElement('div');
29377
- const domPopupViewContainer = document.createElement('div');
29378
- const domTitleContainer = document.createElement('div');
29379
- const domTitle = document.createElement('div');
29380
- const domContentContainer = document.createElement('div');
29381
- const domContent = document.createElement('div');
29382
- const domFooterContainer = document.createElement('div');
29383
- const domConfirm = document.createElement('div');
29384
- const domCancel = document.createElement('div');
29385
- domPopupView.classList.add('video-container__popupview');
29386
- domPopupViewContainer.classList.add('video-container__popupview-container');
29387
- domTitleContainer.classList.add('video-container__popupview-title-container');
29388
- domTitle.classList.add('video-container__popupview-title');
29389
- domContentContainer.classList.add('video-container__popupview-content-container');
29390
- domContent.classList.add('video-container__popupview-content');
29391
- domFooterContainer.classList.add('video-container__popupview-footer-container');
29392
- domConfirm.classList.add('video-container__popupview-confirm');
29393
- domCancel.classList.add('video-container__popupview-cancel');
29394
- domPopupViewContainer.style.backgroundColor = uiThemeConfig.popupView.backgroundColor;
29395
- domPopupViewContainer.style.borderRadius = `${uiThemeConfig.popupView.cornerRadius}px`;
29396
- domPopupViewContainer.style.width = `${parseFloat(uiThemeConfig.popupView.width) * 100}%`;
29397
- util.uiThemeText(domTitle, uiThemeConfig.titleOne);
29398
- util.uiThemeText(domContent, uiThemeConfig.bodyTwo);
29399
- console.log('deviceType', uiThemeConfig.deviceType);
29400
- if (uiThemeConfig.deviceType === 'mobile') {
29401
- util.uiThemeSmallButton(domConfirm, uiThemeConfig.smallermMajorButton);
29402
- } else {
29403
- util.uiThemeButton(domConfirm, uiThemeConfig.majorButton);
29404
- }
29405
- if (arg.cancel) {
29406
- util.uiThemeSmallButton(domCancel, uiThemeConfig.smallerMinorButton);
29407
- domCancel.innerHTML = arg.cancel;
29408
- domCancel.addEventListener('click', () => __awaiter(void 0, void 0, void 0, function* () {
29409
- if (arg.onCancel) {
29410
- arg.onCancel();
29411
- }
29412
- removePopview();
29413
- }));
29414
- }
29415
- domConfirm.addEventListener('click', () => __awaiter(void 0, void 0, void 0, function* () {
29416
- if (arg.onConfirm) {
29417
- arg.onConfirm();
29418
- }
29419
- removePopview();
29420
- }));
29421
- domTitle.innerHTML = arg.title;
29422
- domContent.innerHTML = arg.content;
29423
- domConfirm.innerHTML = arg.confirm;
29424
- domPopupViewContainer.appendChild(domTitleContainer);
29425
- domTitleContainer.appendChild(domTitle);
29426
- domPopupViewContainer.appendChild(domContentContainer);
29427
- domContentContainer.appendChild(domContent);
29428
- domPopupViewContainer.appendChild(domFooterContainer);
29429
- if (arg.cancel) {
29430
- domFooterContainer.appendChild(domCancel);
29431
- }
29432
- domFooterContainer.appendChild(domConfirm);
29433
- domPopupView.appendChild(domPopupViewContainer);
29434
- authmeContainer.appendChild(domPopupView);
29435
- };
29436
-
29437
29371
  function startLiveness(config) {
29438
29372
  return __awaiter(this, void 0, void 0, function* () {
29439
29373
  const translateService = core.getTranslateInstance();
@@ -29457,7 +29391,7 @@ function startLiveness(config) {
29457
29391
  setStatusEvent$2(core.StatusEvent.Passive);
29458
29392
  setStatusView$1(core.StatusView.Init);
29459
29393
  let engineInited = false;
29460
- let expiredIn;
29394
+ let lastFasStatus = null;
29461
29395
  // render view
29462
29396
  // const {
29463
29397
  // container,
@@ -29495,44 +29429,6 @@ function startLiveness(config) {
29495
29429
  };
29496
29430
  const canvas = document.createElement('canvas');
29497
29431
  let uiThemeConfig = util.themeConfigDefault;
29498
- // let sdkFlowTimeout: NodeJS.Timeout;
29499
- let sdkFlowTimeout = null;
29500
- const timeout$ = new rxjs.Subject();
29501
- // function makeSDKFlowTimeout(expiredIn: number) {
29502
- // return setTimeout(async () => {
29503
- // asyncShowErrorMessage(
29504
- // translateService.translate('sdk.general.error.timeout.content'),
29505
- // false
29506
- // );
29507
- // await waitTime(3 * TIME_UNIT.SECOND);
29508
- // timeout$.next({
29509
- // isSuccess: false as const,
29510
- // code: `${ErrorCode.ID_RECOGNITION_TIMEOUT}`,
29511
- // message: new AuthmeError(ErrorCode.ID_RECOGNITION_TIMEOUT).message,
29512
- // });
29513
- // }, expiredIn * TIME_UNIT.SECOND);
29514
- // }
29515
- function makeSDKFlowTimeout(expiredIn) {
29516
- return rxjs.timer(expiredIn * util.TIME_UNIT.SECOND).pipe(rxjs.switchMap(() => new rxjs.Observable(observer => {
29517
- config.onDestroy();
29518
- popupView({
29519
- title: translateService.translate('sdk.general.error.timeout.title'),
29520
- content: translateService.translate('sdk.general.error.timeout.content'),
29521
- confirm: translateService.translate('sdk.general.confirm'),
29522
- onConfirm: () => {
29523
- observer.next(true);
29524
- observer.complete();
29525
- }
29526
- });
29527
- }))).subscribe(() => {
29528
- timeout$.next({
29529
- isSuccess: false,
29530
- code: `${core.ErrorCode.ID_RECOGNITION_TIMEOUT}`,
29531
- message: new core.AuthmeError(core.ErrorCode.ID_RECOGNITION_TIMEOUT).message,
29532
- data: {}
29533
- });
29534
- });
29535
- }
29536
29432
  function initOptions() {
29537
29433
  return __awaiter(this, void 0, void 0, function* () {
29538
29434
  const themeConfigApi = yield config.getOptionConfig();
@@ -29568,7 +29464,6 @@ function startLiveness(config) {
29568
29464
  // 需要等到介紹頁完成後,才開始執行 config.init (engine init 流程)。
29569
29465
  const init$ = rxjs.defer(() => config.init()).pipe(rxjs.tap(res => {
29570
29466
  livenessConfig = res.parameters;
29571
- expiredIn = res.expiredIn;
29572
29467
  }), rxjs.catchError(error => __awaiter(this, void 0, void 0, function* () {
29573
29468
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
29574
29469
  const EVENT_NAME_ERROR_CODE = 7;
@@ -29626,6 +29521,9 @@ function startLiveness(config) {
29626
29521
  })));
29627
29522
  const applyTextByResult = result => {
29628
29523
  // debugLog('fas-response', result);
29524
+ if (lastFasStatus !== result.eStatus) {
29525
+ lastFasStatus = result.eStatus;
29526
+ }
29629
29527
  sendStatusDescription$2(fasRecognitionResultMapping(result.eStatus));
29630
29528
  switch (result.eStatus) {
29631
29529
  case liveness.FasRecognitionResult.NoFace:
@@ -29683,6 +29581,10 @@ function startLiveness(config) {
29683
29581
  uiComponentLiveness.statusText.textContent = translateService.translate('sdk.general.verify.success');
29684
29582
  setBorderStatus('pass');
29685
29583
  break;
29584
+ case liveness.FasRecognitionResult.Motion:
29585
+ uiComponentLiveness.statusText.textContent = translateService.translate('sdk.liveness.detection.motion');
29586
+ setBorderStatus('pass');
29587
+ break;
29686
29588
  case liveness.FasRecognitionResult.Pass:
29687
29589
  if (result.eStage === liveness.EAuthMeFASServiceStage.Scale) {
29688
29590
  setBorderStatus(null);
@@ -29774,9 +29676,6 @@ function startLiveness(config) {
29774
29676
  }
29775
29677
  setCorrectViewHeight();
29776
29678
  }), rxjs.switchMap(() => init$), rxjs.switchMap(() => requestCamera$), rxjs.tap(() => {
29777
- if (expiredIn) {
29778
- sdkFlowTimeout = makeSDKFlowTimeout(expiredIn);
29779
- }
29780
29679
  engineInited = true;
29781
29680
  }), rxjs.switchMap(() => rxjs.from(util.waitTime(1)).pipe(rxjs.tap(() => uiComponentBasic.videoContainer.style.zIndex = '101'))), rxjs.switchMap(() => rxjs.from(util.getCanvasSize(uiComponentBasic.video))), rxjs.switchMap(canvasSizeInfo => rxjs.from(config.onStart(canvasSizeInfo)).pipe(rxjs.tap(params => {
29782
29681
  const {
@@ -29826,26 +29725,16 @@ function startLiveness(config) {
29826
29725
  // ),
29827
29726
  rxjs.filter(({
29828
29727
  result
29829
- }) => (result.eStatus === liveness.FasRecognitionResult.Pass || result.eStatus === liveness.FasRecognitionResult.Failed) && result.eStage === liveness.EAuthMeFASServiceStage.Done), rxjs.map(data => {
29830
- // console.log('data', data);
29728
+ }) => (result.eStatus === liveness.FasRecognitionResult.Pass || result.eStatus === liveness.FasRecognitionResult.Failed || result.eStatus === liveness.FasRecognitionResult.DepthFake) && result.eStage === liveness.EAuthMeFASServiceStage.Done), rxjs.take(1),
29729
+ // 只處理第一個成功結果,防止 engine 繼續運行導致重複上傳
29730
+ rxjs.map(data => {
29731
+ // DepthFake 和 Failed 都視為失敗
29732
+ const isSuccess = data.result.eStatus === liveness.FasRecognitionResult.Pass;
29831
29733
  return Object.assign(Object.assign({}, data), {
29832
- isSuccess: true
29734
+ isSuccess
29833
29735
  });
29834
- }), rxjs.raceWith(timeout$) // 確保 timeout 之後一定會有失敗結果
29835
- )))),
29836
- // switchMap((resp) =>
29837
- // window.navigator.onLine
29838
- // ? of(resp)
29839
- // : from(
29840
- // asyncOnLineShowErrorMessage(
29841
- // translateService.translate('sdk.general.error.alert.offline'),
29842
- // translateService.translate('sdk.general.error.retry'),
29843
- // false
29844
- // )
29845
- // ).pipe(map(() => resp))
29846
- // ),
29847
- rxjs.tap(res => {
29848
- // console.log('res', res);
29736
+ }))))), rxjs.tap(res => {
29737
+ // Engine timeout 會在 res.isSuccess 為 false 時發生
29849
29738
  if (res.isSuccess) {
29850
29739
  sendStatusDescription$2(core.StatusDescription.UploadingStart);
29851
29740
  util.startSpinner({
@@ -29854,12 +29743,10 @@ function startLiveness(config) {
29854
29743
  backgroundOpaque: true
29855
29744
  });
29856
29745
  } else {
29857
- console.warn('Timeout 發生,返回錯誤結果');
29746
+ // Engine 返回 timeout 或其他失敗狀態
29858
29747
  throw new core.AuthmeError(core.ErrorCode.ID_RECOGNITION_TIMEOUT);
29859
29748
  }
29860
- }), rxjs.filter(res => res.isSuccess),
29861
- // **只執行成功的情況**
29862
- rxjs.switchMap(() => rxjs.defer(() => config.onSuccess())));
29749
+ }), rxjs.filter(res => res.isSuccess), rxjs.switchMap(() => rxjs.defer(() => config.onSuccess())));
29863
29750
  return rxjs.of({}).pipe(() => {
29864
29751
  eventListenerService$2.start();
29865
29752
  setStatusView$1(core.StatusView.Init);
@@ -29962,11 +29849,6 @@ function startLiveness(config) {
29962
29849
  } finally {
29963
29850
  uiComponentBasic.video.srcObject = null;
29964
29851
  }
29965
- // if (sdkFlowTimeout) clearTimeout(sdkFlowTimeout);
29966
- if (sdkFlowTimeout) {
29967
- sdkFlowTimeout.unsubscribe();
29968
- sdkFlowTimeout = null;
29969
- }
29970
29852
  eventListenerService$2.stop();
29971
29853
  uiComponentBasic.container.remove();
29972
29854
  unsubscribe$.next();
@@ -30491,18 +30373,20 @@ const renderOCRMask = params => {
30491
30373
  borderOpacity = params.strokeOpacity;
30492
30374
  }
30493
30375
  if (params.type === 'bordered') {
30494
- const rectX = newCardPoints[0].x * windowWidth;
30495
- const rectY = newCardPoints[0].y * windowHeight;
30496
- const rectWidth = newCardPoints[1].x * windowWidth - rectX;
30497
- const rectHeight = newCardPoints[3].y * windowHeight - rectY;
30498
- pathOfFrame.setAttribute('visibility', 'hidden');
30499
- rectOfFrame.setAttribute('visibility', 'visible');
30500
- rectOfFrame.setAttribute('x', rectX.toString());
30501
- rectOfFrame.setAttribute('y', rectY.toString());
30502
- rectOfFrame.setAttribute('width', rectWidth.toString());
30503
- rectOfFrame.setAttribute('height', rectHeight.toString());
30504
- rectOfFrame.setAttribute('stroke-opacity', borderOpacity.toString());
30505
- svg.appendChild(rectOfFrame);
30376
+ // 使用 path 畫傾斜的四邊形框線(支援防偽的傾斜效果)
30377
+ rectOfFrame.setAttribute('visibility', 'hidden');
30378
+ pathOfFrame.setAttribute('visibility', 'visible');
30379
+ // 計算四個角的座標
30380
+ const points = newCardPoints.map(p => ({
30381
+ x: p.x * windowWidth,
30382
+ y: p.y * windowHeight
30383
+ }));
30384
+ // 使用 path 畫閉合的四邊形
30385
+ const dBordered = `M ${points[0].x},${points[0].y} ` + `L ${points[1].x},${points[1].y} ` + `L ${points[2].x},${points[2].y} ` + `L ${points[3].x},${points[3].y} Z`;
30386
+ pathOfFrame.setAttribute('d', dBordered);
30387
+ pathOfFrame.setAttribute('stroke-opacity', borderOpacity.toString());
30388
+ pathOfFrame.setAttribute('stroke-width', '3');
30389
+ svg.appendChild(pathOfFrame);
30506
30390
  } else {
30507
30391
  rectOfFrame.setAttribute('visibility', 'hidden');
30508
30392
  pathOfFrame.setAttribute('visibility', 'visible');
@@ -30592,11 +30476,10 @@ const renderOCRMask = params => {
30592
30476
  path.setAttribute('d', d);
30593
30477
  // Set the same path for the border to match the mask exactly
30594
30478
  borderPath.setAttribute('d', d);
30479
+ // Anti-fraud 模式下:隱藏 pathOfFrame 和 rectOfFrame,只顯示 borderPath(傾斜的框)
30595
30480
  if (antiFraud) {
30596
- setBorderType({
30597
- type: borderType,
30598
- mirrored: _mirrored
30599
- });
30481
+ pathOfFrame.setAttribute('visibility', 'hidden');
30482
+ rectOfFrame.setAttribute('visibility', 'hidden');
30600
30483
  }
30601
30484
  };
30602
30485
  function setBorderSuccess(color, opacity) {
@@ -30619,12 +30502,13 @@ const renderOCRMask = params => {
30619
30502
  if (borderType === 'bordered') {
30620
30503
  rectOfFrame.setAttribute('stroke', color);
30621
30504
  rectOfFrame.setAttribute('stroke-opacity', opacity.toString());
30622
- borderPath.setAttribute('stroke', color);
30623
- borderPath.setAttribute('stroke-opacity', opacity.toString());
30624
30505
  } else {
30625
30506
  pathOfFrame.setAttribute('stroke', color);
30626
30507
  pathOfFrame.setAttribute('stroke-opacity', opacity.toString());
30627
30508
  }
30509
+ // 同時更新 borderPath (用於 anti-fraud 模式的傾斜框線)
30510
+ borderPath.setAttribute('stroke', color);
30511
+ borderPath.setAttribute('stroke-opacity', opacity.toString());
30628
30512
  }
30629
30513
  function frameImage(faceMode, zIndex, base64, color, opacity) {
30630
30514
  color = color !== null && color !== void 0 ? color : OcrFrame.imageColor;
@@ -31922,6 +31806,78 @@ const fraudScanIntroPage = arg => {
31922
31806
  authmeContainer.appendChild(domModal);
31923
31807
  };
31924
31808
 
31809
+ const popupView = arg => {
31810
+ const authmeContainer = document.querySelector('.authme-container');
31811
+ if (!authmeContainer) {
31812
+ console.error('modal: authmeContainer not found');
31813
+ return;
31814
+ }
31815
+ const uiThemeConfig = util.Storage.getItem('themeConfig');
31816
+ function removePopview() {
31817
+ var _a;
31818
+ (_a = document.querySelector('.video-container__popupview')) === null || _a === void 0 ? void 0 : _a.remove();
31819
+ }
31820
+ const domPopupView = document.createElement('div');
31821
+ const domPopupViewContainer = document.createElement('div');
31822
+ const domTitleContainer = document.createElement('div');
31823
+ const domTitle = document.createElement('div');
31824
+ const domContentContainer = document.createElement('div');
31825
+ const domContent = document.createElement('div');
31826
+ const domFooterContainer = document.createElement('div');
31827
+ const domConfirm = document.createElement('div');
31828
+ const domCancel = document.createElement('div');
31829
+ domPopupView.classList.add('video-container__popupview');
31830
+ domPopupViewContainer.classList.add('video-container__popupview-container');
31831
+ domTitleContainer.classList.add('video-container__popupview-title-container');
31832
+ domTitle.classList.add('video-container__popupview-title');
31833
+ domContentContainer.classList.add('video-container__popupview-content-container');
31834
+ domContent.classList.add('video-container__popupview-content');
31835
+ domFooterContainer.classList.add('video-container__popupview-footer-container');
31836
+ domConfirm.classList.add('video-container__popupview-confirm');
31837
+ domCancel.classList.add('video-container__popupview-cancel');
31838
+ domPopupViewContainer.style.backgroundColor = uiThemeConfig.popupView.backgroundColor;
31839
+ domPopupViewContainer.style.borderRadius = `${uiThemeConfig.popupView.cornerRadius}px`;
31840
+ domPopupViewContainer.style.width = `${parseFloat(uiThemeConfig.popupView.width) * 100}%`;
31841
+ util.uiThemeText(domTitle, uiThemeConfig.titleOne);
31842
+ util.uiThemeText(domContent, uiThemeConfig.bodyTwo);
31843
+ console.log('deviceType', uiThemeConfig.deviceType);
31844
+ if (uiThemeConfig.deviceType === 'mobile') {
31845
+ util.uiThemeSmallButton(domConfirm, uiThemeConfig.smallermMajorButton);
31846
+ } else {
31847
+ util.uiThemeButton(domConfirm, uiThemeConfig.majorButton);
31848
+ }
31849
+ if (arg.cancel) {
31850
+ util.uiThemeSmallButton(domCancel, uiThemeConfig.smallerMinorButton);
31851
+ domCancel.innerHTML = arg.cancel;
31852
+ domCancel.addEventListener('click', () => __awaiter(void 0, void 0, void 0, function* () {
31853
+ if (arg.onCancel) {
31854
+ arg.onCancel();
31855
+ }
31856
+ removePopview();
31857
+ }));
31858
+ }
31859
+ domConfirm.addEventListener('click', () => __awaiter(void 0, void 0, void 0, function* () {
31860
+ if (arg.onConfirm) {
31861
+ arg.onConfirm();
31862
+ }
31863
+ removePopview();
31864
+ }));
31865
+ domTitle.innerHTML = arg.title;
31866
+ domContent.innerHTML = arg.content;
31867
+ domConfirm.innerHTML = arg.confirm;
31868
+ domPopupViewContainer.appendChild(domTitleContainer);
31869
+ domTitleContainer.appendChild(domTitle);
31870
+ domPopupViewContainer.appendChild(domContentContainer);
31871
+ domContentContainer.appendChild(domContent);
31872
+ domPopupViewContainer.appendChild(domFooterContainer);
31873
+ if (arg.cancel) {
31874
+ domFooterContainer.appendChild(domCancel);
31875
+ }
31876
+ domFooterContainer.appendChild(domConfirm);
31877
+ domPopupView.appendChild(domPopupViewContainer);
31878
+ authmeContainer.appendChild(domPopupView);
31879
+ };
31880
+
31925
31881
  const ocrResultModal = arg => {
31926
31882
  let modifiedDetails = {};
31927
31883
  const authmeContainer = document.querySelector('.authme-container');
@@ -32142,6 +32098,10 @@ const ocrResultModal = arg => {
32142
32098
  }
32143
32099
 
32144
32100
  function parseRegex(input, forceUnicode = false) {
32101
+ // 處理 undefined 或 null 輸入
32102
+ if (!input) {
32103
+ return null;
32104
+ }
32145
32105
  try {
32146
32106
  let pattern = input;
32147
32107
  let flags = '';
@@ -32436,6 +32396,7 @@ function startOCR(config) {
32436
32396
  fraudRetryTimes: 1,
32437
32397
  fraudTimeout: 52,
32438
32398
  fraudMaxFps: 2,
32399
+ ocrMaxFps: 2,
32439
32400
  captureTimeout: config.ocrConfig.captureTimeout
32440
32401
  };
32441
32402
  let cardSizeInfo = {
@@ -32729,9 +32690,9 @@ function startOCR(config) {
32729
32690
  uiComponentOCRMask.setCardBorderColor('error');
32730
32691
  break;
32731
32692
  case idRecognition.EAuthMeIDCardAntiFraudStatus.Error:
32732
- uiComponentOCR.statusText.textContent = translateService.translate('sdk.general.error.alert.serverError');
32733
- util.showErrorMessage(translateService.translate('sdk.general.error.alert.serverError'), false);
32693
+ uiComponentOCR.statusText.textContent = translateService.translate('sdk.general.verify.error.serverError');
32734
32694
  sendStatusDescription$1(core.StatusDescription.Error);
32695
+ uiComponentOCRMask.setCardBorderColor('error');
32735
32696
  break;
32736
32697
  case idRecognition.EAuthMeIDCardAntiFraudStatus.NeedDeformationFrontal:
32737
32698
  needDeformationCount++;
@@ -32825,16 +32786,32 @@ function startOCR(config) {
32825
32786
  return ocrSendFrameAnimation;
32826
32787
  }
32827
32788
  }), rxjs.map(x => x.result), rxjs.concatMap(x => __awaiter(this, void 0, void 0, function* () {
32789
+ // V9: 檢測 stage 變化,如果 stage 變化了,先顯示 pass 顏色
32790
+ // V9 Engine 不再返回 StagePass 狀態,而是直接切換到下一個 stage
32791
+ const stageChanged = x.eStage !== currentAntiFraudStage && currentAntiFraudStage !== undefined && x.eStage !== idRecognition.EAuthMeIDCardAntiFraudStage.Done;
32792
+ if (stageChanged) {
32793
+ // 顯示 pass 顏色表示上一個 stage 完成
32794
+ uiComponentOCRMask.setCardBorderColor('pass');
32795
+ uiComponentOCR.statusText.textContent = translateService.translate('sdk.general.verify.error.stagePass');
32796
+ previosAntiFraudAnimateTime = Date.now();
32797
+ // 等待一段時間讓用戶看到顏色變化
32798
+ yield new Promise(resolve => setTimeout(resolve, 1500));
32799
+ yield stopAnimate();
32800
+ needDeformationCount = 0;
32801
+ previosAntiFraudAnimateTime = 0;
32802
+ // 更新 currentAntiFraudStage 並設定新的 stage 框線
32803
+ currentAntiFraudStage = x.eStage;
32804
+ // 調用 cardRotateByStage 更新框線位置,但傳入 mandatoryRotate = true
32805
+ yield cardRotateByStage(x.eStage, true);
32806
+ // 跳過這一幀的 applyTextByResult,直接返回
32807
+ return x;
32808
+ }
32828
32809
  yield applyTextByResult(x);
32829
32810
  cardRotateByStage(x.eStage);
32830
32811
  if (isTutorialFinish) {
32831
32812
  // 文字更新移到 cardRotateAnimationProcess 中,確保與動畫同步
32832
32813
  x.eStage;
32833
32814
  }
32834
- if (x.eStage !== currentAntiFraudStage) {
32835
- needDeformationCount = 0;
32836
- previosAntiFraudAnimateTime = 0;
32837
- }
32838
32815
  if (!uiThemeConfig.isFraudAnimationLoadingPageEnabled) {
32839
32816
  // if (config.ocrConfig.disableTutorial) {
32840
32817
  util.stopSpinner();
@@ -32919,6 +32896,12 @@ function startOCR(config) {
32919
32896
  sendStatusDescription$1(core.StatusDescription.Reflective);
32920
32897
  uiComponentOCRMask.setCardBorderColor('error');
32921
32898
  break;
32899
+ case idRecognition.EAuthMeCardOCRStatus.Gray:
32900
+ // V9 新增: Gray 狀態表示灰階或顏色異常,顯示與 Blur 類似的提示
32901
+ uiComponentOCR.statusText.textContent = translateService.translate('sdk.general.verify.error.blur');
32902
+ sendStatusDescription$1(core.StatusDescription.Gray);
32903
+ uiComponentOCRMask.setCardBorderColor('error');
32904
+ break;
32922
32905
  case idRecognition.EAuthMeCardOCRStatus.Blur:
32923
32906
  uiComponentOCR.statusText.textContent = translateService.translate('sdk.general.verify.error.blur');
32924
32907
  sendStatusDescription$1(core.StatusDescription.Blur);
@@ -32946,6 +32929,19 @@ function startOCR(config) {
32946
32929
  sendStatusDescription$1(core.StatusDescription.Reflective);
32947
32930
  uiComponentOCRMask.setCardBorderColor('error');
32948
32931
  break;
32932
+ case idRecognition.EAuthMeMRZServiceStatus.Blur:
32933
+ // V9 新增: MRZ Blur 狀態
32934
+ uiComponentOCR.statusText.textContent = translateService.translate('sdk.general.verify.error.blur');
32935
+ sendStatusDescription$1(core.StatusDescription.Blur);
32936
+ uiComponentOCRMask.setCardBorderColor('error');
32937
+ blurCount++;
32938
+ break;
32939
+ case idRecognition.EAuthMeMRZServiceStatus.MRZNotFound:
32940
+ // V9 新增: MRZ區域未找到(卡片存在但無法識別MRZ區)
32941
+ uiComponentOCR.statusText.textContent = translateService.translate('sdk.general.verify.error.wrongCardType');
32942
+ sendStatusDescription$1(core.StatusDescription.WrongCardType);
32943
+ uiComponentOCRMask.setCardBorderColor('error');
32944
+ break;
32949
32945
  }
32950
32946
  if (blurCount === 2) {
32951
32947
  showCameraSwitchButton(deviceMetas);
@@ -33078,7 +33074,12 @@ function startOCR(config) {
33078
33074
  const ctx = canvas.getContext('2d', {
33079
33075
  willReadFrequently: true
33080
33076
  });
33081
- return source$.pipe(rxjs.mergeMap(() => rxjs.animationFrames().pipe(limitFPS(fps), rxjs.filter(() => received && !ocrSendFrameAnimation), rxjs.tap(() => received = false), rxjs.tap(() => util.clearCanvas(canvas)), rxjs.map(() => util.getImageData(canvas, ctx, video, canvasSizeInfo, bas64Format, imageType)), rxjs.mergeMap(imageData => rxjs.from(frameCallback(imageData.data, imageData.base64, cardType, type)).pipe(rxjs.catchError(e => {
33077
+ return source$.pipe(rxjs.mergeMap(() => rxjs.animationFrames().pipe(limitFPS(fps), rxjs.filter(() => received && !ocrSendFrameAnimation), rxjs.tap(() => received = false), rxjs.tap(() => util.clearCanvas(canvas)), rxjs.map(() => util.getImageData(canvas, ctx, video, canvasSizeInfo, bas64Format, imageType)), rxjs.tap(imageData => {
33078
+ // 如果 imageData 為 null(canvas 尺寸無效),重置 received 讓下一幀可以繼續
33079
+ if (imageData === null) {
33080
+ received = true;
33081
+ }
33082
+ }), rxjs.filter(imageData => imageData !== null), rxjs.mergeMap(imageData => rxjs.from(frameCallback(imageData.data, imageData.base64, cardType, type)).pipe(rxjs.catchError(e => {
33082
33083
  // send to fast, ignore
33083
33084
  if (e instanceof core.AuthmeError && e.code === core.ErrorCode.RECOGNITION_NOT_AVAILABLE) {
33084
33085
  return rxjs.EMPTY;
@@ -33090,7 +33091,7 @@ function startOCR(config) {
33090
33091
  })), rxjs.tap(() => received = true))))));
33091
33092
  };
33092
33093
  const autoCapture = canvasSizeInfo => {
33093
- return rxjs.of(canvasSizeInfo).pipe(handleOcrSendFrame(canvasSizeInfo, uiComponentOCR.image, uiComponentBasic.video, config.recognition, 30, false, config.ocrConfig.resultImageFormat, cardType, type), rxjs.tap(x => applyTextByResult(x.result)), rxjs.filter(({
33094
+ return rxjs.of(canvasSizeInfo).pipe(handleOcrSendFrame(canvasSizeInfo, uiComponentOCR.image, uiComponentBasic.video, config.recognition, ocrEngineConfig.ocrMaxFps, false, config.ocrConfig.resultImageFormat, cardType, type), rxjs.tap(x => applyTextByResult(x.result)), rxjs.filter(({
33094
33095
  result
33095
33096
  }) => result.eStatus === idRecognition.EAuthMeCardOCRStatus.Pass || result.eStatus === idRecognition.EAuthMeMRZServiceStatus.Success), rxjs.take(1), rxjs.tap(() => {
33096
33097
  if (countdownCaptureTimer && countdownCaptureTimer.end && !countdownCaptureTimer.end()) {
@@ -33108,38 +33109,47 @@ function startOCR(config) {
33108
33109
  util.hideElement(uiComponentOCR.scanAnimationContainer);
33109
33110
  }), rxjs.map(() => resp))), rxjs.switchMap(({
33110
33111
  result
33111
- }) => rxjs.from(type === idRecognition.EAuthMeCardClass.Passport && config.ocrConfig.disablePassportConfirm || !config.ocrConfig.confirmPageEnabled ? rxjs.of(false) : checkConfirmImage(result.imageData, result.iWidth, result.iHeight)).pipe(rxjs.switchMap(needRetry => {
33112
- if (countdownCaptureTimer && countdownCaptureTimer.end) {
33113
- if (needRetry && !countdownCaptureTimer.end()) {
33114
- countdownCaptureTimer = countdownTimer(captureTimeoutTimer, () => {
33115
- util.showElement(captureBtn);
33116
- toastManualCapture = toast({
33117
- message: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_19494_3109)"><path d="M19.5 19.5H4.5C4.10218 19.5 3.72064 19.342 3.43934 19.0607C3.15804 18.7794 3 18.3978 3 18V7.5C3 7.10218 3.15804 6.72064 3.43934 6.43934C3.72064 6.15804 4.10218 6 4.5 6H7.5L9 3.75H15L16.5 6H19.5C19.8978 6 20.2794 6.15804 20.5607 6.43934C20.842 6.72064 21 7.10218 21 7.5V18C21 18.3978 20.842 18.7794 20.5607 19.0607C20.2794 19.342 19.8978 19.5 19.5 19.5Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 15.75C13.864 15.75 15.375 14.239 15.375 12.375C15.375 10.511 13.864 9 12 9C10.136 9 8.625 10.511 8.625 12.375C8.625 14.239 10.136 15.75 12 15.75Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g><defs><clipPath id="clip0_19494_3109"><rect width="24" height="24" fill="white"/></clipPath></defs></svg>' + translateService.translate('sdk.general.manualCapture'),
33118
- transition: true
33112
+ }) => {
33113
+ // V9: 檢查卡片圖像是否可用
33114
+ const cardResult = result;
33115
+ const hasValidImage = cardResult.imageData && cardResult.iWidth > 0 && cardResult.iHeight > 0;
33116
+ if (!hasValidImage) {
33117
+ console.warn('[OCR] Card image not available, skipping confirm page. iWidth:', cardResult.iWidth, 'iHeight:', cardResult.iHeight, 'imageData:', cardResult.imageData ? 'exists' : 'null');
33118
+ }
33119
+ const shouldSkipConfirm = type === idRecognition.EAuthMeCardClass.Passport && config.ocrConfig.disablePassportConfirm || !config.ocrConfig.confirmPageEnabled || !hasValidImage; // V9: 如果沒有卡片圖像,跳過確認頁
33120
+ return rxjs.from(shouldSkipConfirm ? rxjs.of(false) : checkConfirmImage(cardResult.imageData, cardResult.iWidth, cardResult.iHeight)).pipe(rxjs.switchMap(needRetry => {
33121
+ if (countdownCaptureTimer && countdownCaptureTimer.end) {
33122
+ if (needRetry && !countdownCaptureTimer.end()) {
33123
+ countdownCaptureTimer = countdownTimer(captureTimeoutTimer, () => {
33124
+ util.showElement(captureBtn);
33125
+ toastManualCapture = toast({
33126
+ message: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_19494_3109)"><path d="M19.5 19.5H4.5C4.10218 19.5 3.72064 19.342 3.43934 19.0607C3.15804 18.7794 3 18.3978 3 18V7.5C3 7.10218 3.15804 6.72064 3.43934 6.43934C3.72064 6.15804 4.10218 6 4.5 6H7.5L9 3.75H15L16.5 6H19.5C19.8978 6 20.2794 6.15804 20.5607 6.43934C20.842 6.72064 21 7.10218 21 7.5V18C21 18.3978 20.842 18.7794 20.5607 19.0607C20.2794 19.342 19.8978 19.5 19.5 19.5Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 15.75C13.864 15.75 15.375 14.239 15.375 12.375C15.375 10.511 13.864 9 12 9C10.136 9 8.625 10.511 8.625 12.375C8.625 14.239 10.136 15.75 12 15.75Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g><defs><clipPath id="clip0_19494_3109"><rect width="24" height="24" fill="white"/></clipPath></defs></svg>' + translateService.translate('sdk.general.manualCapture'),
33127
+ transition: true
33128
+ });
33119
33129
  });
33120
- });
33121
- countdownCaptureTimer.init();
33122
- }
33123
- if (!needRetry) {
33124
- countdownCaptureTimer.clear();
33125
- util.hideElement(captureBtn);
33130
+ countdownCaptureTimer.init();
33131
+ }
33132
+ if (!needRetry) {
33133
+ countdownCaptureTimer.clear();
33134
+ util.hideElement(captureBtn);
33135
+ }
33126
33136
  }
33127
- }
33128
- util.startSpinner({
33129
- text: translateService.translate('sdk.general.uploading'),
33130
- statement: translateService.translate('sdk.general.footer'),
33131
- backgroundOpaque: true
33132
- });
33133
- return needRetry ? rxjs.of(true) : rxjs.from(config.confirmImage({
33134
- type,
33135
- cardType
33136
- })).pipe(rxjs.tap(() => sendStatusAction$1(core.StatusAction.Uploading)), rxjs.map(confirmResp => !confirmResp));
33137
- }), rxjs.tap(() => {
33138
- // hideElement(uiComponentOCR.confirmImageContainer);
33139
- util.hideElement(uiComponentOCR.confirmContainer);
33140
- util.stopSpinner();
33141
- showVideoElement();
33142
- }), rxjs.switchMap(needRetry => needRetry ? recognition(true) : rxjs.of(true)))));
33137
+ util.startSpinner({
33138
+ text: translateService.translate('sdk.general.uploading'),
33139
+ statement: translateService.translate('sdk.general.footer'),
33140
+ backgroundOpaque: true
33141
+ });
33142
+ return needRetry ? rxjs.of(true) : rxjs.from(config.confirmImage({
33143
+ type,
33144
+ cardType
33145
+ })).pipe(rxjs.tap(() => sendStatusAction$1(core.StatusAction.Uploading)), rxjs.map(confirmResp => !confirmResp));
33146
+ }), rxjs.tap(() => {
33147
+ // hideElement(uiComponentOCR.confirmImageContainer);
33148
+ util.hideElement(uiComponentOCR.confirmContainer);
33149
+ util.stopSpinner();
33150
+ showVideoElement();
33151
+ }), rxjs.switchMap(needRetry => needRetry ? recognition(true) : rxjs.of(true)));
33152
+ }));
33143
33153
  };
33144
33154
  const recognition = retry => {
33145
33155
  return init(retry).pipe(rxjs.tap(() => {
@@ -33431,7 +33441,7 @@ function startOCR(config) {
33431
33441
  const cardType = currentType('get', null).cardType;
33432
33442
  const ctx = uiComponentOCR.image.getContext('2d');
33433
33443
  const imageData = util.getImageData(uiComponentOCR.image, ctx, uiComponentBasic.video, canvasSizeInfo, false, config.ocrConfig.resultImageFormat);
33434
- const imageBlob = util.UintArrayToBlob(canvasSizeInfo.width, canvasSizeInfo.height, imageData.data);
33444
+ const imageBlob = imageData ? util.UintArrayToBlob(canvasSizeInfo.width, canvasSizeInfo.height, imageData.data) : undefined;
33435
33445
  const cancelResultObj = {
33436
33446
  isSuccess: false,
33437
33447
  code: `${core.ErrorCode.USER_CANCEL}`,
@@ -33782,7 +33792,7 @@ function startOCR(config) {
33782
33792
  function cardRotateByStage(stage, mandatoryRotate = false, point) {
33783
33793
  return __awaiter(this, void 0, void 0, function* () {
33784
33794
  if ((stage === idRecognition.EAuthMeIDCardAntiFraudStage.Done || stage === currentAntiFraudStage) && mandatoryRotate === false) return;
33785
- const cardMatchROI = point ? point : config.getCardMatchROI ? yield config.getCardMatchROI() : null;
33795
+ const cardMatchROI = point ? point : config.getCardMatchROI ? yield config.getCardMatchROI(stage) : null;
33786
33796
  if (!cardMatchROI || cardMatchROI.length == 0) throw new core.AuthmeError(core.ErrorCode.SDK_INTERNAL_ERROR, 'getCardMatchROI is null');
33787
33797
  uiComponentOCRMask.setCardBorderColor('error');
33788
33798
  // 只有左右翻轉時需要正規化座標(確保左右邊平行)
@@ -34516,6 +34526,11 @@ class LivenessVerifyModule {
34516
34526
  }
34517
34527
  }
34518
34528
 
34529
+ /**
34530
+ * Debug flag for FAS (Face Authentication Service) encryption flow.
34531
+ * Set to true to enable detailed logging for debugging encryption issues.
34532
+ */
34533
+ const DEBUG_FAS = false;
34519
34534
  function handleUploadError$1(_error) {
34520
34535
  return new Promise(resolve => {
34521
34536
  util.uploadModal({
@@ -34553,12 +34568,18 @@ class LivenessModule {
34553
34568
  let id = '';
34554
34569
  let pubKey = '';
34555
34570
  let shouldEncrypt = false;
34571
+ // 保存 API 返回的參數,避免在 onStart 中被覆蓋
34572
+ let apiTimeoutSec;
34573
+ let apiFasThreshold;
34556
34574
  const encryptDataBase64 = data => __awaiter(this, void 0, void 0, function* () {
34557
34575
  const dataString = JSON.stringify(data);
34558
34576
  const encoder = new TextEncoder();
34559
34577
  const uint8Array = encoder.encode(dataString);
34560
- // TODO check encrypt function
34561
34578
  const resultEncrypt = yield this.fasService.encryptBlob(uint8Array, pubKey);
34579
+ if (!resultEncrypt) {
34580
+ console.error('[encryptDataBase64] encryptBlob returned null/empty');
34581
+ throw new core.AuthmeError(core.ErrorCode.SDK_INTERNAL_ERROR, 'encryptBlob failed');
34582
+ }
34562
34583
  return resultEncrypt;
34563
34584
  });
34564
34585
  const handleUpload = (id, frameList, resultList, meta, config, shouldEncrypt, encryptDataBase64) => __awaiter(this, void 0, void 0, function* () {
@@ -34580,7 +34601,6 @@ class LivenessModule {
34580
34601
  try {
34581
34602
  const result = yield rxjs.firstValueFrom(yield startLiveness({
34582
34603
  getOptionConfig: () => __awaiter(this, void 0, void 0, function* () {
34583
- console.log('config.deviceType', config.deviceType);
34584
34604
  const res = yield liveness.LivenessAPI.IdentityVerification.option(config.deviceType);
34585
34605
  const themeId = res.themeId;
34586
34606
  if (!themeId) {
@@ -34595,6 +34615,10 @@ class LivenessModule {
34595
34615
  id = resp.id;
34596
34616
  pubKey = resp.parameters.pubKey;
34597
34617
  shouldEncrypt = (_a = resp.shouldEncrypt) !== null && _a !== void 0 ? _a : shouldEncrypt;
34618
+ if (DEBUG_FAS) ;
34619
+ // 根據 shouldEncrypt 決定是否設置 report key
34620
+ // shouldEncrypt = true: 清空 report key,讓 getReport() 返回未加密的 base64,由 SDK 加密整個 payload
34621
+ // shouldEncrypt = false: 設置 report key,讓 getReport() 返回 engine 加密的 report
34598
34622
  if (!shouldEncrypt) {
34599
34623
  yield this.fasService.setPublicKeyForJson(pubKey);
34600
34624
  } else {
@@ -34609,8 +34633,11 @@ class LivenessModule {
34609
34633
  // fRight: 0.7,
34610
34634
  // fBottom: 0.7,
34611
34635
  // };
34612
- params.timeoutSec = resp.parameters.fasTimeout || params.timeoutSec;
34613
- params.fFASTh = resp.parameters.fasThreshold || params.fFASTh;
34636
+ // 保存 API 參數,在 onStart 中使用
34637
+ apiTimeoutSec = resp.parameters.fasTimeout || params.timeoutSec;
34638
+ apiFasThreshold = resp.parameters.fasThreshold || params.fFASTh;
34639
+ params.timeoutSec = apiTimeoutSec;
34640
+ params.fFASTh = apiFasThreshold;
34614
34641
  yield this.fasService.setParams(params);
34615
34642
  yield this.fasService.setStage(resp.parameters.fasStages.map(x => `EAuthMeFASServiceStage_${x}`));
34616
34643
  // return resp.parameters;
@@ -34653,8 +34680,15 @@ class LivenessModule {
34653
34680
  params.faceROI.fRight = 1 - (1 - widthPercent) / 2;
34654
34681
  params.faceROI.fTop = (1 - heightPercent) / 2;
34655
34682
  params.faceROI.fBottom = 1 - (1 - heightPercent) / 2;
34656
- yield this.fasService.setParams(params);
34683
+ // 保留 API 設定的 timeout 和 threshold,避免被 getParams() 的預設值覆蓋
34684
+ if (apiTimeoutSec !== undefined) {
34685
+ params.timeoutSec = apiTimeoutSec;
34686
+ }
34687
+ if (apiFasThreshold !== undefined) {
34688
+ params.fFASTh = apiFasThreshold;
34689
+ }
34657
34690
  yield this.fasService.setFrameSize(frameWidth, frameHeight);
34691
+ yield this.fasService.setParams(params);
34658
34692
  yield this.fasService.startSession();
34659
34693
  return params;
34660
34694
  }),
@@ -34809,12 +34843,27 @@ class LivenessModule {
34809
34843
  data: meta
34810
34844
  };
34811
34845
  if (shouldEncrypt) {
34812
- postData.data = atob(meta);
34846
+ // shouldEncrypt = true 時,沒有設置 report key,
34847
+ // getReport() 返回的是 base64 編碼的未加密 JSON
34848
+ // 需要先 atob 解碼成純 JSON 字串(保持為字串,與 Android SDK 一致)
34849
+ // 然後用 encryptDataBase64 加密整個 postData
34850
+ try {
34851
+ const decodedMeta = atob(meta);
34852
+ if (DEBUG_FAS) ;
34853
+ postData.data = decodedMeta; // 保持為字串,與 Android 一致
34854
+ } catch (e) {
34855
+ console.error('[uploadMeta] atob failed:', e);
34856
+ // 如果 atob 失敗,可能已經是純 JSON 字串,保持原樣
34857
+ }
34858
+
34859
+ const encrypted = yield encryptDataBase64(postData);
34813
34860
  return liveness.LivenessAPI.IdentityVerification.uploadMeta({
34814
34861
  id: id,
34815
- encryptedBase64String: yield encryptDataBase64(postData)
34862
+ encryptedBase64String: encrypted
34816
34863
  });
34817
34864
  } else {
34865
+ // shouldEncrypt = false 時,已設置 report key,
34866
+ // getReport() 返回的是 engine 加密過的 base64 字符串
34818
34867
  return liveness.LivenessAPI.IdentityVerification.uploadMeta(postData);
34819
34868
  }
34820
34869
  // return LivenessAPI.IdentityVerification.uploadMeta({
@@ -35116,13 +35165,19 @@ class MRZModule {
35116
35165
  pubKey = resp.parameters.pubKey;
35117
35166
  scanId = resp.scanId;
35118
35167
  uploadFullFrame = (_c = (_b = resp.parameters.fraud) === null || _b === void 0 ? void 0 : _b.collectAllFrames) !== null && _c !== void 0 ? _c : config.uploadFullFrame;
35119
- if (resp.parameters.pubKey) {
35168
+ // 根據 shouldEncrypt 決定是否設置 report key
35169
+ // shouldEncrypt = true: 清空 report key,讓 getReport() 返回未加密的 base64,由 SDK 加密整個 payload
35170
+ // shouldEncrypt = false: 設置 report key,讓 getReport() 返回 engine 加密的 report
35171
+ if (!shouldEncrypt) {
35120
35172
  this.engine.setPublicKeyForJson(pubKey);
35173
+ } else {
35174
+ this.engine.setPublicKeyForJson('');
35121
35175
  }
35122
35176
  yield this.mrzService.init();
35123
35177
  yield util.waitTime(100);
35124
35178
  return Object.assign(Object.assign({}, resp.parameters), {
35125
- expiredIn: resp.expiredIn
35179
+ expiredIn: resp.expiredIn,
35180
+ ocrMaxFps: 2
35126
35181
  });
35127
35182
  }),
35128
35183
  ocrStart: (points, type, setBorderType, setCardBorderColor, setBorderSuccess, scanAnimationContainer, successAnimationContainer, frameImage, frameText, faceMode, cardType, retry = false) => __awaiter(this, void 0, void 0, function* () {
@@ -35187,29 +35242,44 @@ class MRZModule {
35187
35242
  // 1. 最後結果統一使用 getFinalResult 取得的結果。
35188
35243
  // 2. 由於 engine-lib 限制,getFinalResult 必須要在 stop 之後呼叫。
35189
35244
  const finalResult = yield this.mrzService.getFinalResult();
35190
- latestTField = JSON.parse(yield this.mrzService.toJson(finalResult));
35191
- if (latestTField['birthDateCheckDigit']) {
35192
- delete latestTField['birthDateCheckDigit'];
35193
- }
35194
- if (latestTField['documentNumberCheckDigit']) {
35195
- delete latestTField['documentNumberCheckDigit'];
35196
- }
35197
- if (latestTField['expiryDateCheckDigit']) {
35198
- delete latestTField['expiryDateCheckDigit'];
35199
- }
35200
- if (latestTField['optionaldataCheckDigit']) {
35201
- delete latestTField['optionaldataCheckDigit'];
35202
- }
35203
- if (latestTField['overallCheckDigit']) {
35204
- delete latestTField['overallCheckDigit'];
35205
- }
35206
- switch (latestTField.gender) {
35207
- case 'M':
35208
- latestTField.gender = 'male';
35209
- break;
35210
- case 'F':
35211
- latestTField.gender = 'female';
35212
- break;
35245
+ if (finalResult) {
35246
+ const rawField = JSON.parse(yield this.mrzService.toJson(finalResult));
35247
+ // V9 API 回傳 snake_case 欄位,轉換為 camelCase 以匹配翻譯 key
35248
+ const snakeToCamelMap = {
35249
+ birth_date: 'birthDate',
35250
+ expiry_date: 'expiryDate',
35251
+ document_number: 'documentNumber',
35252
+ document_type: 'documentType',
35253
+ given_name: 'givenName',
35254
+ personal_number: 'personalNumber',
35255
+ sex: 'gender',
35256
+ surname: 'surname',
35257
+ nationality: 'nationality',
35258
+ country: 'country'
35259
+ };
35260
+ // 需要刪除的欄位 (check digit)
35261
+ const fieldsToDelete = ['birthDateCheckDigit', 'documentNumberCheckDigit', 'expiryDateCheckDigit', 'optionaldataCheckDigit', 'overallCheckDigit', 'birth_date_check_digit', 'document_number_check_digit', 'expiry_date_check_digit', 'optional_data_check_digit', 'overall_check_digit'];
35262
+ // 轉換欄位名稱並過濾 check digit
35263
+ latestTField = {};
35264
+ for (const [key, value] of Object.entries(rawField)) {
35265
+ if (fieldsToDelete.includes(key)) {
35266
+ continue; // 跳過 check digit 欄位
35267
+ }
35268
+
35269
+ const newKey = snakeToCamelMap[key] || key;
35270
+ latestTField[newKey] = value;
35271
+ }
35272
+ // 性別轉換
35273
+ if (latestTField === null || latestTField === void 0 ? void 0 : latestTField.gender) {
35274
+ switch (latestTField.gender) {
35275
+ case 'M':
35276
+ latestTField.gender = 'male';
35277
+ break;
35278
+ case 'F':
35279
+ latestTField.gender = 'female';
35280
+ break;
35281
+ }
35282
+ }
35213
35283
  }
35214
35284
  } else if (uploadFullFrame) {
35215
35285
  const image = util.UintArrayToBlob(frameWidth, frameHeight, data, virtualCanvas);
@@ -35345,6 +35415,8 @@ class MRZModule {
35345
35415
  getAntiFraudStageList: () => []
35346
35416
  }));
35347
35417
  } catch (error) {
35418
+ console.error('[MRZ Module] Error caught:', error);
35419
+ console.error('[MRZ Module] Error stack:', error === null || error === void 0 ? void 0 : error.stack);
35348
35420
  const catchedError = error instanceof core.AuthmeError ? error : new core.AuthmeError(core.ErrorCode.SDK_INTERNAL_ERROR, error);
35349
35421
  return {
35350
35422
  isSuccess: false,
@@ -35357,8 +35429,98 @@ class MRZModule {
35357
35429
  }
35358
35430
  }
35359
35431
 
35360
- const F_IMAGE_BLUE_TH = 'fImageBlurTh';
35432
+ // V9: 卡片 OCR 參數常數名稱
35433
+ const F_IMAGE_BLUR_TH = 'fImageBlurTh';
35434
+ const F_IDCARD_COLOR_TH = 'fIdCardColorTh';
35435
+ const F_IMAGE_REFLECTIVE_TRIGGER_TH = 'fImageReflectiveTriggerTh';
35436
+ const F_IMAGE_REFLECTIVE_MASK_TH = 'fImageReflectiveMaskTh';
35437
+ const F_CARD_MATCH_TH = 'fCardMatchTh';
35438
+ const F_CARD_CLASSIFICATION_TH = 'fCardClassificationTh';
35439
+ // V9: 不同卡片類型的預設參數 (參考 Android SDK)
35440
+ // 邏輯說明:
35441
+ // - idCardColorTh: color_detect.score > threshold 為彩色 (Pass), < threshold 為灰階 (Gray)
35442
+ // - imageReflectiveTriggerTh: 反光分數 > threshold 判定為反光
35443
+ // - imageBlurTh: 模糊分數 > threshold 判定為模糊
35444
+ const CARD_OCR_PARAMS_MAP = {
35445
+ // 身分證正面: 高反光容限 (塑膠卡)
35446
+ TWN_IDCard_Front: {
35447
+ idCardColorTh: 15,
35448
+ imageBlurTh: 750,
35449
+ imageReflectiveTriggerTh: 1.05,
35450
+ imageReflectiveMaskTh: 1.5,
35451
+ cardMatchTh: 0.3,
35452
+ cardClassificationTh: 0.62655
35453
+ },
35454
+ // 身分證背面: 高反光容限 (塑膠卡)
35455
+ TWN_IDCard_Back: {
35456
+ idCardColorTh: 4.5,
35457
+ imageBlurTh: 750,
35458
+ imageReflectiveTriggerTh: 1.05,
35459
+ imageReflectiveMaskTh: 1.5,
35460
+ cardMatchTh: 0.3,
35461
+ cardClassificationTh: 0.62655
35462
+ },
35463
+ // 健保卡正面: 中等反光容限
35464
+ TWN_HealthCard_Front: {
35465
+ idCardColorTh: 10,
35466
+ imageBlurTh: 750,
35467
+ imageReflectiveTriggerTh: 0.35,
35468
+ imageReflectiveMaskTh: 0.45,
35469
+ cardMatchTh: 0.3,
35470
+ cardClassificationTh: 0.62655
35471
+ },
35472
+ // 駕照正面: 低反光觸發 (紙質)
35473
+ TWN_DriverLicense_Front: {
35474
+ idCardColorTh: 15,
35475
+ imageBlurTh: 750,
35476
+ imageReflectiveTriggerTh: 0.16,
35477
+ imageReflectiveMaskTh: 0.45,
35478
+ cardMatchTh: 0.3,
35479
+ cardClassificationTh: 0.62655
35480
+ },
35481
+ // 駕照背面: 極低反光觸發
35482
+ TWN_DriverLicense_Back: {
35483
+ idCardColorTh: 8,
35484
+ imageBlurTh: 600,
35485
+ imageReflectiveTriggerTh: 0.045,
35486
+ imageReflectiveMaskTh: 0.45,
35487
+ cardMatchTh: 0.75,
35488
+ cardClassificationTh: 0.62655
35489
+ },
35490
+ // 居留證正面
35491
+ TWN_ResidentCard_Front: {
35492
+ idCardColorTh: 0,
35493
+ imageBlurTh: 600,
35494
+ imageReflectiveTriggerTh: 0.045,
35495
+ imageReflectiveMaskTh: 0.45,
35496
+ cardMatchTh: 0.75,
35497
+ cardClassificationTh: 0.62655
35498
+ },
35499
+ // 居留證背面
35500
+ TWN_ResidentCard_Back: {
35501
+ idCardColorTh: 0,
35502
+ imageBlurTh: 600,
35503
+ imageReflectiveTriggerTh: 0.045,
35504
+ imageReflectiveMaskTh: 0.45,
35505
+ cardMatchTh: 0.75,
35506
+ cardClassificationTh: 0.62655
35507
+ }
35508
+ };
35509
+ // V9: 預設參數 (用於未定義的卡片類型)
35510
+ const DEFAULT_CARD_OCR_PARAMS = {
35511
+ idCardColorTh: 0,
35512
+ imageBlurTh: 750,
35513
+ imageReflectiveTriggerTh: 0.05,
35514
+ imageReflectiveMaskTh: 0.025,
35515
+ cardMatchTh: 0.3,
35516
+ cardClassificationTh: 0.62655
35517
+ };
35361
35518
  const DEFAULT_ANTI_FRAUD_TIMEOUT = 40;
35519
+ // V9: OCR 結果需要過濾掉的欄位 (重複/內部欄位)
35520
+ // 這些欄位可能是內部檢核數字或與其他欄位重複
35521
+ const OCR_FIELDS_TO_FILTER = ['backSideId', 'reverseId', 'dob', 'firstName', 'lastName', 'fullName', 'optionalData2' // 內部欄位
35522
+ ];
35523
+
35362
35524
  function unionMerge(a, b) {
35363
35525
  const entries = Object.entries(a).concat(Object.entries(b));
35364
35526
  // union merge
@@ -35423,8 +35585,8 @@ function handleUploadError(error) {
35423
35585
  });
35424
35586
  });
35425
35587
  }
35426
- // 目前不需要隨機,若未來需要隨機可更新。
35427
- const antiFraudStageListMap = [[engine.EAuthMeIDCardAntiFraudStage.Left, engine.EAuthMeIDCardAntiFraudStage.Right, engine.EAuthMeIDCardAntiFraudStage.Up, engine.EAuthMeIDCardAntiFraudStage.Down, engine.EAuthMeIDCardAntiFraudStage.Left, engine.EAuthMeIDCardAntiFraudStage.Right, engine.EAuthMeIDCardAntiFraudStage.Up, engine.EAuthMeIDCardAntiFraudStage.Down, engine.EAuthMeIDCardAntiFraudStage.Left, engine.EAuthMeIDCardAntiFraudStage.Right, engine.EAuthMeIDCardAntiFraudStage.Up, engine.EAuthMeIDCardAntiFraudStage.Down]
35588
+ // V9: Engine 會自動重複 stages,只需要傳入基本的 4 個方向
35589
+ const antiFraudStageListMap = [[engine.EAuthMeIDCardAntiFraudStage.Left, engine.EAuthMeIDCardAntiFraudStage.Right, engine.EAuthMeIDCardAntiFraudStage.Up, engine.EAuthMeIDCardAntiFraudStage.Down]
35428
35590
  // [
35429
35591
  // EAuthMeIDCardAntiFraudStage.Left,
35430
35592
  // EAuthMeIDCardAntiFraudStage.Right,
@@ -35742,12 +35904,17 @@ class OCRModule {
35742
35904
  pubKey = resp.parameters.pubKey;
35743
35905
  uploadFullFrame = (_c = (_b = resp.parameters.fraud) === null || _b === void 0 ? void 0 : _b.collectAllFrames) !== null && _c !== void 0 ? _c : config.uploadFullFrame;
35744
35906
  fraudTimeout = (_e = (_d = resp.parameters.fraud) === null || _d === void 0 ? void 0 : _d.totalTimeout) !== null && _e !== void 0 ? _e : DEFAULT_ANTI_FRAUD_TIMEOUT;
35907
+ // 根據 shouldEncrypt 決定是否設置 report key
35908
+ // shouldEncrypt = true: 清空 report key,讓 getReport() 返回未加密的 base64,由 SDK 加密整個 payload
35909
+ // shouldEncrypt = false: 設置 report key,讓 getReport() 返回 engine 加密的 report
35745
35910
  if (!shouldEncrypt) {
35746
35911
  this.engine.setPublicKeyForJson(pubKey);
35747
35912
  } else {
35748
35913
  this.engine.setPublicKeyForJson('');
35749
35914
  }
35750
35915
  if (config.type === idRecognition.IdRecognitionCardType.IDCard && config.needAntiFraud) {
35916
+ // 先設定 stage 再 init,避免使用預設的 Frontal stage
35917
+ yield this.antiFraudInstance.setStage(antiFraudStageList);
35751
35918
  yield this.antiFraudInstance.init();
35752
35919
  } else if (config.type === idRecognition.IdRecognitionCardType.ResidentCard) {
35753
35920
  // workaround: resident card need MRZ, refactor later.
@@ -35759,7 +35926,8 @@ class OCRModule {
35759
35926
  yield util.waitTime(100);
35760
35927
  return Object.assign(Object.assign({}, resp.parameters), {
35761
35928
  expiredIn: resp.expiredIn,
35762
- captureTimeout: config.captureTimeout
35929
+ captureTimeout: config.captureTimeout,
35930
+ ocrMaxFps: 2
35763
35931
  });
35764
35932
  }),
35765
35933
  ocrStart: (points, type, setBorderType, setCardBorderColor, setBorderSuccess, scanAnimationContainer, successAnimationContainer, frameImage, frameText, faceMode, cardType, retry = false) => __awaiter(this, void 0, void 0, function* () {
@@ -35770,9 +35938,14 @@ class OCRModule {
35770
35938
  _service instanceof idRecognition.CardOCR ? yield _service.setType(type) : null;
35771
35939
  const newParams = {};
35772
35940
  const oldParams = yield this.ocrService.getParams();
35773
- newParams[F_IMAGE_BLUE_TH] = 750;
35774
- newParams['fCardMatchTh'] = 0.5;
35775
- newParams['fImageReflectiveTriggerTh'] = 0.15;
35941
+ // V9: 根據卡片類型設置參數 (參考 Android SDK)
35942
+ const cardParams = CARD_OCR_PARAMS_MAP[cardType] || DEFAULT_CARD_OCR_PARAMS;
35943
+ newParams[F_IMAGE_BLUR_TH] = cardParams.imageBlurTh;
35944
+ newParams[F_IDCARD_COLOR_TH] = cardParams.idCardColorTh;
35945
+ newParams[F_IMAGE_REFLECTIVE_TRIGGER_TH] = cardParams.imageReflectiveTriggerTh;
35946
+ newParams[F_IMAGE_REFLECTIVE_MASK_TH] = cardParams.imageReflectiveMaskTh;
35947
+ newParams[F_CARD_MATCH_TH] = cardParams.cardMatchTh;
35948
+ newParams[F_CARD_CLASSIFICATION_TH] = cardParams.cardClassificationTh;
35776
35949
  yield this.ocrService.setParams(Object.assign(Object.assign({}, oldParams), newParams));
35777
35950
  if (cardTypes.map(idRecognition.mapCardtypeToAuthmeClass).filter(n => n == engine.EAuthMeCardClass.Unknown).length > 0) {
35778
35951
  this.ocrService.setOption({
@@ -35936,9 +36109,12 @@ class OCRModule {
35936
36109
  const newParams = {};
35937
36110
  const oldParams = yield _service.getParams();
35938
36111
  if (config.antiFraudIMetalTagValidCountTh !== false) newParams['iMetalTagValidCountTh'] = config.antiFraudIMetalTagValidCountTh;
36112
+ // V9: 根據卡片類型設置參數 (參考 Android SDK)
36113
+ const cardParams = CARD_OCR_PARAMS_MAP[cardType] || DEFAULT_CARD_OCR_PARAMS;
35939
36114
  newParams['timeoutSec'] = 1000;
35940
- newParams['fCardMatchTh'] = 0.5;
35941
- newParams['fImageReflectiveTriggerTh'] = 0.15;
36115
+ newParams[F_IDCARD_COLOR_TH] = cardParams.idCardColorTh;
36116
+ newParams[F_IMAGE_REFLECTIVE_TRIGGER_TH] = cardParams.imageReflectiveTriggerTh;
36117
+ newParams[F_CARD_MATCH_TH] = cardParams.cardMatchTh;
35942
36118
  newParams['fImageThicknessTh'] = 0.4;
35943
36119
  newParams['fCardDeformationTh'] = 10;
35944
36120
  newParams['enableCardInROI'] = 1;
@@ -36278,8 +36454,16 @@ class OCRModule {
36278
36454
  });
36279
36455
  const ocrOriginImg = util.UintArrayToBlob(frameWidth, frameHeight, data, virtualCanvas, config.resultImageFormat);
36280
36456
  const eClass = cardType !== null && cardType !== void 0 ? cardType : '';
36281
- if (result.eStatus === idRecognition.EAuthMeCardOCRStatus.Pass && result.imageData && !!docInfos[eClass].docId) {
36282
- const resultOcrImg = util.UintArrayToBlob(result.iWidth, result.iHeight, result === null || result === void 0 ? void 0 : result.imageData, virtualCanvas, config.resultImageFormat);
36457
+ if (result.eStatus === idRecognition.EAuthMeCardOCRStatus.Pass && !!docInfos[eClass].docId) {
36458
+ // V9: 如果有卡片圖像就使用它,否則使用原始幀圖像
36459
+ let resultOcrImg;
36460
+ if (result.imageData && result.iWidth > 0 && result.iHeight > 0) {
36461
+ resultOcrImg = util.UintArrayToBlob(result.iWidth, result.iHeight, result.imageData, virtualCanvas, config.resultImageFormat);
36462
+ } else {
36463
+ // V9 沒有返回卡片圖像,使用原始幀圖像作為替代
36464
+ console.warn('[OCR] V9: no card image available, using original frame image');
36465
+ resultOcrImg = ocrOriginImg;
36466
+ }
36283
36467
  docInfos[eClass].ocrImg = resultOcrImg;
36284
36468
  docInfos[eClass].ocrOriginImg = ocrOriginImg;
36285
36469
  yield _service.stop();
@@ -36365,6 +36549,12 @@ class OCRModule {
36365
36549
  } else {
36366
36550
  ocrOriginImg = docInfos[option.cardType].ocrOriginImg;
36367
36551
  }
36552
+ // V9: 檢查 ocrOriginImg 是否有效
36553
+ if (!ocrOriginImg) {
36554
+ console.error('[OCR] confirmImage: ocrOriginImg is null or undefined, cannot proceed');
36555
+ console.error('[OCR] confirmImage: option.cardType =', option.cardType, 'docInfos[cardType] =', docInfos[option.cardType]);
36556
+ return false;
36557
+ }
36368
36558
  // const base64Image = await blobToBase64(ocrOriginImg);
36369
36559
  // console.log('confirmImage', base64Image);
36370
36560
  const requestImg = yield encryptImageBase64(ocrOriginImg);
@@ -36491,6 +36681,8 @@ class OCRModule {
36491
36681
  const item2 = res.fields.find(item => item.name === item1.name);
36492
36682
  return item2 ? Object.assign(Object.assign({}, item1), item2) : item1;
36493
36683
  }).concat(res.fields.filter(item2 => !ocrResultFilds.some(item1 => item1.name === item2.name)));
36684
+ // V9: 過濾掉重複/內部欄位
36685
+ ocrResultFilds = ocrResultFilds.filter(item => !OCR_FIELDS_TO_FILTER.includes(item.name));
36494
36686
  delete docInfos[option.cardType];
36495
36687
  return true;
36496
36688
  } catch (error) {
@@ -36510,6 +36702,13 @@ class OCRModule {
36510
36702
  } else {
36511
36703
  ocrOriginImg = docInfos[cardType].ocrOriginImg;
36512
36704
  }
36705
+ // V9: 檢查 ocrOriginImg 是否有效
36706
+ if (!ocrOriginImg) {
36707
+ console.warn('[OCR] ocrCancel: ocrOriginImg is null, skipping upload');
36708
+ yield this.ocrService.stop();
36709
+ docInfos[cardType].docId = '';
36710
+ return true;
36711
+ }
36513
36712
  const requestImg = yield encryptImageBase64(ocrOriginImg);
36514
36713
  yield this.ocrService.stop();
36515
36714
  const report = yield this.ocrService.getReport();
@@ -36556,7 +36755,9 @@ class OCRModule {
36556
36755
  }
36557
36756
  }),
36558
36757
  antiFraudStart: (points, setBorderType, setCardBorderColor, setBorderSuccess, scanAnimationContainer, successAnimationContainer, frameImage, frameText, faceMode) => __awaiter(this, void 0, void 0, function* () {
36559
- yield this.antiFraudInstance.init();
36758
+ // setStage() 如果服務已初始化會自動觸發 reinit
36759
+ // 不需要再呼叫 init()
36760
+ yield this.antiFraudInstance.setStage(antiFraudStageList);
36560
36761
  yield this.antiFraudInstance.setFrameSize(frameWidth, frameHeight);
36561
36762
  yield this.antiFraudInstance.setMaskPosition(points.map(([x, y]) => [Number(x.toFixed(2)), Number(y.toFixed(2))]));
36562
36763
  const newParams = {};
@@ -36564,7 +36765,6 @@ class OCRModule {
36564
36765
  if (config.antiFraudIMetalTagValidCountTh !== false) newParams['iMetalTagValidCountTh'] = config.antiFraudIMetalTagValidCountTh;
36565
36766
  newParams['timeoutSec'] = fraudTimeout;
36566
36767
  yield this.antiFraudInstance.setParams(Object.assign(Object.assign({}, oldParams), newParams));
36567
- yield this.antiFraudInstance.setStage(antiFraudStageList);
36568
36768
  yield this.antiFraudInstance.startSession();
36569
36769
  const twnidCardFront = engine.EAuthMeCardClass.TWN_IDCard_Front;
36570
36770
  docInfos[twnidCardFront] = {
@@ -36825,8 +37025,10 @@ class OCRModule {
36825
37025
  yield (_u = this.ocrService) === null || _u === void 0 ? void 0 : _u.destroy();
36826
37026
  yield (_v = this.antiFraudInstance) === null || _v === void 0 ? void 0 : _v.destroy();
36827
37027
  }),
36828
- getCardMatchROI: () => __awaiter(this, void 0, void 0, function* () {
36829
- return yield this.antiFraudInstance.getCardMatchROI();
37028
+ getCardMatchROI: stage => __awaiter(this, void 0, void 0, function* () {
37029
+ return yield this.antiFraudInstance.getCardMatchROI(stage ? {
37030
+ stage
37031
+ } : undefined);
36830
37032
  })
36831
37033
  }), eventNameWrong$.pipe(rxjs.tap(() => {
36832
37034
  throw new core.AuthmeError(core.ErrorCode.EVENT_NAME_WRONG);
@@ -37343,8 +37545,8 @@ class AuthmeIdentityVerification extends engine.AuthmeFunctionModule {
37343
37545
  }
37344
37546
 
37345
37547
  var name = "authme/sdk";
37346
- var version$1 = "2.8.38";
37347
- var date = "2026-01-21T02:32:26+0000";
37548
+ var version$1 = "2.8.42";
37549
+ var date = "2026-02-03T09:36:05+0000";
37348
37550
  var packageInfo = {
37349
37551
  name: name,
37350
37552
  version: version$1,