import ZenidSDK, {AssetUrls} from "./internal/zenid-sdk.js"; import {zenidLog} from "./zenid-log.js"; import {CameraFacingMode, GiveMeResponse, ICamera, IVerifier, IVisualizer, OnFrameProcessedCallback, OnVerificationFailedCallback} from "../interfaces/verification-system-interfaces.js"; import { AcceptableInput, DocumentVerifierSettings, DocumentVerifierStateContainerForPublicData, EncodedImage, MsLivenessGiveMeResponse, MsLivenessStateContainerForPublicData, SdkWizardStep, SetupInfo, UploadReadyData, UploadSampleBasicInfo, VerifierSettingsBase, VerifierStateContainerForPublicDataBase } from "./generated/zenid-types.generated.js"; import { Country, DocumentCodes, DocumentRole, PageCodes, SampleType, SdkResponseType, SdkVerifierType, SupportedLanguages, VerifierState } from "./generated/zenid-enums.generated.js"; import {VerifierFactory} from "./verifiers/verifier-factory.js"; import {base64ToUint8Array} from "./helpers/binary-helpers.js"; import {BackendApi, SampleToUpload} from "../platforms/web/backend-api.js"; import {ZenidModel, zenidModels} from "./generated/zenid-models.generated.js"; import SdkWizardStepWithSettings, {WizardLogic} from "./wizard-logic.js"; import {CameraExplorer} from "../platforms/web/camera-explorer.js"; import {EmptyVisualizerImplementation} from "../platforms/web/web-visualizer.js"; class featureSettings { constructor( public feature: SdkVerifierType, public settings: VerifierSettingsBase ) { } } export class ZenidController { setupInfo: SetupInfo | null = null; wizard: WizardLogic = new WizardLogic(); camera!: ICamera; visualizer: IVisualizer = new EmptyVisualizerImplementation(); protected resourcePath!: string; protected useSingleThread: boolean = false; protected assetUrls?: AssetUrls; protected challengeToken: string = ""; // Customer identifier of the person being verified, sent with initSdk and stored on the SDK // session - lets the customer find the session even when no sample is uploaded. protected customData?: string; private verifier: IVerifier | null = null; private lastSettings: featureSettings | null = null; protected backendApi!: BackendApi; private snapshot: UploadReadyData | null = null; constructor() { } // PUBLIC METHODS async cameraPermissionDenied(): Promise { return !await CameraExplorer.isAnyCameraAvailable(); } canMakeSnapshots(): boolean { if (!this.verifier) return false; const verifiersEnablingSnapshots: SdkVerifierType[] = [SdkVerifierType.Selfie, SdkVerifierType.Document, SdkVerifierType.LicensePlate]; return verifiersEnablingSnapshots.includes(this.verifier.getVerifierType()); } async canSwitchCamera(): Promise { if (!this.verifier || !await CameraExplorer.canSwitchCamera(this.verifier.facingMode())) return false; const verifiersWithoutCameraControl: SdkVerifierType[] = [SdkVerifierType.MsLiveness]; return !verifiersWithoutCameraControl.includes(this.verifier.getVerifierType()); } createVerifierByType(verifierType: SdkVerifierType): IVerifier { return VerifierFactory.getVerifier(verifierType); } async demandAnotherPageOfDocumentIfNeeded(): Promise { let feature = this.verifier?.getVerifierType(); if (feature !== SdkVerifierType.Document) return; let status = await this.verifier!.getStateContainer() as DocumentVerifierStateContainerForPublicData; if (!status.DocumentCode || !status.PageCode || !status.SupportedPageCount) { return; } // Check if second page is needed using SupportedPageCount if (status.SupportedPageCount <= 1) return; // no second page needed let getDocumentStep = (page: PageCodes) => { let newSettings = Object.assign(new DocumentVerifierSettings(), this.lastSettings?.settings) if (newSettings) newSettings.AcceptableInput = { PossibleDocuments: [{ DocumentCode: status.DocumentCode, Page: page }]}; let step = new SdkWizardStep(); step.VerifierType = SdkVerifierType.Document; return new SdkWizardStepWithSettings(step, newSettings); } this.wizard?.demandSecondPageOfDocument(getDocumentStep(status.PageCode), getDocumentStep(status.PageCode === PageCodes.F ? PageCodes.B : PageCodes.F)); } async enableDebugVisualization(isEnabled: boolean): Promise { await ZenidSDK.Instance.setDebugVisualization(isEnabled); } getAllowedCountries(): Country[] { const countries = this.getAllowedModels() .map(model => model.country) .filter((value, index, self) => self.indexOf(value) === index); //this is .distinct() // Sort with CZ first, SK second, then alphabetically const priority: Record = { [Country.Cz]: 0, [Country.Sk]: 1 }; return countries.sort((a, b) => (priority[a] ?? 999) - (priority[b] ?? 999) || a.localeCompare(b)); } getAllowedDocuments(): DocumentCodes[] { return this.setupInfo!.AllowedDocumentCodes ?? []; } getAllowedRoles(country: Country | null): DocumentRole[] { return this.getAllowedModels() .filter(model => country === null || model.country === country) .map(model => model.documentRole) .filter((value, index, self) => self.indexOf(value) === index); //this is .distinct(); } getAcceptableModels(acceptableInput: AcceptableInput | undefined): ZenidModel[] | undefined { if (!acceptableInput || !acceptableInput.PossibleDocuments || acceptableInput.PossibleDocuments.length === 0) { return undefined; } return this.getAllowedModels() .filter(model => acceptableInput.PossibleDocuments?.some(doc => (!doc.DocumentCode || doc.DocumentCode === model.documentCode) && (!doc.Page || doc.Page === model.pageCode) && (!doc.Country || doc.Country === model.country) && (!doc.Role || doc.Role === model.documentRole))); } async getSample(sampleId: string): Promise { try { return await this.backendApi.getSample(sampleId); } catch (error) { zenidLog.error('Error fetching sample:', error); throw error; } } getSupportedPageCount(documentCode: DocumentCodes): Promise { return ZenidSDK.Instance.getSupportedPageCount(documentCode); } async init(backendApi: BackendApi, absoluteModelsUrl: string, useSingleThread: boolean = false): Promise { this.backendApi = backendApi; this.resourcePath = absoluteModelsUrl; this.useSingleThread = useSingleThread; if (!useSingleThread) this.checkCrossOriginIsolation(); // Start SDK setup and camera initialization in parallel. // assetUrls (optional CDN override for wasm/glue) is read from this.assetUrls — derived classes set it before calling super.init(). const sdkSetupPromise = ZenidSDK.Instance.setup(this.resourcePath, useSingleThread, this.assetUrls); const cameraInitPromise = CameraExplorer.init(); // Wait for SDK setup to complete before setting visualizer version await sdkSetupPromise; // Set global visualizer version await ZenidSDK.Instance.setVisualizerVersion(2); // Wait for all parallel operations to complete await this.initSdk(); } async investigate(sampleIds: string[]): Promise { return await this.backendApi.investigateSamples(sampleIds, false); } /** * Helper function to detect mobile devices (iOS and Android) * @returns {boolean} True if running on a mobile device, false otherwise */ isMobileDevice(): boolean { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent); } /** * Helper function to detect iOS devices * @returns {boolean} True if running on iOS, false otherwise */ isIos(): boolean { return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream; } async loadAndRunVerifier(verifierType: SdkVerifierType, settings: string | VerifierSettingsBase = {}, onFrameProcessed?: OnFrameProcessedCallback, onVerificationFailed?: OnVerificationFailedCallback): Promise { try { await this.loadVerifier(verifierType, settings); return await this.runVerifier(onFrameProcessed, onVerificationFailed); } catch (error) { zenidLog.error('Error loading and running verifier:', error); } return null; } async loadVerifier(verifierType: SdkVerifierType, settings: string | VerifierSettingsBase = {}): Promise { try { // Stop current verifier if (this.verifier !== null) { await this.verifier.stopProcessingLoop(); } // Create new example with selected verifier type this.verifier = this.createVerifierByType(verifierType); let settingsObj: VerifierSettingsBase = (typeof settings === "string") ? JSON.parse(settings) : settings; this.lastSettings = new featureSettings(verifierType, settingsObj); // Not awaited: runVerifier opens the camera while the models load; the frame loop waits for it. this.verifier.initOrRestart(settings).catch(error => zenidLog.error('Error initializing verifier:', error)); return this.verifier; } catch (error) { zenidLog.error('Error initializing verifier:', error); return null; } } async onGiveMe(state: VerifierStateContainerForPublicDataBase): Promise { // Handle torch commands first (before switch) if (state.GiveMeWhat === SdkResponseType.TorchOn) { await this.camera?.setTorch(true); return "{}"; } if (state.GiveMeWhat === SdkResponseType.TorchOff) { await this.camera?.setTorch(false); return "{}"; } switch (state.GiveMeWhat) { case SdkResponseType.MsLiveness: return JSON.stringify(await this.startMsLiveness(state)); case SdkResponseType.Photo: { let documentState = state as DocumentVerifierStateContainerForPublicData; // Raw pixels go straight to the verifier — no JPEG encode + base64 + JSON + // decode round trip (the EncodedImage protocol remains for the mobile SDKs). const rawPhoto = await this.camera.getHighResolutionFrameAsRawImage(documentState.GiveMePhotoResolution); if (rawPhoto) { zenidLog.info(`Photo captured (raw), ${rawPhoto.width}×${rawPhoto.height}`); return { rawPhoto }; } // A failed capture aborts the run. Returning null would leave the verifier // waiting in GiveMe and retry the capture forever. throw new Error('Failed to capture camera frame'); } } return null; } async restartVerifierWithPreviousSettings(): Promise { if (!this.verifier) { zenidLog.error("No verifier to restart"); return; } await this.verifier.restartWithPreviousSettings(); } async runVerifier(onFrameProcessed?: OnFrameProcessedCallback, onVerificationFailed?: OnVerificationFailedCallback): Promise { try { if (!this.verifier) { zenidLog.error('Verifier not initialized'); return null; } if (!this.lastSettings) { zenidLog.error('You have to load verifier first'); return null; } this.snapshot = null; await this.setupVisualElements(); // Both processing loops report the terminal state through this callback before they // return null, so this is where a failure becomes distinguishable from a run ended by stop(). const trackFailure: OnFrameProcessedCallback = async (state, verifier) => { if (state.State === VerifierState.Failed) await onVerificationFailed?.(state); await onFrameProcessed?.(state, verifier); }; let verifierResult = await this.verifier!.processFramesFromCamera(this.camera, this.visualizer, (state) => this.onGiveMe(state), trackFailure); if (!verifierResult && this.snapshot) return this.snapshot; return verifierResult; } catch (error) { // Central sink for verification-run errors: everything thrown by the verifier loop, // onGiveMe, or the camera converges here. The logger renders Error message + stack; // non-Error throws are stringified, so the cause is always recorded. zenidLog.error('Verification run aborted by an unhandled error:', error); // An aborted run (failed model download, camera that will not start) is terminal too. await onVerificationFailed?.({ State: VerifierState.Failed, FeedbackText: error instanceof Error ? error.message : String(error) }); } return null; } async selectProfile(value: string) { await ZenidSDK.Instance.selectProfile(value); await this.initRequiredSteps(value); } async setLanguage(language: SupportedLanguages): Promise { await ZenidSDK.Instance.setLanguage(language); } async setSetupInfo(): Promise { this.setupInfo = await this.backendApi.getSetupInfo(); const disabledVerifiers: SdkVerifierType[] = [ SdkVerifierType.IQSHologram ]; this.setupInfo.AllowedSdkVerifiers = this.setupInfo.AllowedSdkVerifiers?.filter(v => !disabledVerifiers.includes(v)); await this.initRequiredSteps(this.setupInfo?.Profiles?.[0]); } async stop(): Promise { await this.camera?.stop(); await this.verifier?.stopProcessingLoop(); } async switchCamera(): Promise { await this.camera.switchCamera(); } async takeImmediateSnapshot(): Promise { let verifierType = this.verifier?.getVerifierType(); if (!verifierType) { throw new Error('Verifier is not initialized'); } if (!this.canMakeSnapshots()) { throw new Error('Immediate snapshot is not supported for this verifier type'); } let photo: EncodedImage = await this.takeNativePhoto(this.verifier!.facingMode()); if (!photo?.Data || photo.Data == "") { throw new Error('No camera frame available'); } this.snapshot = { ImagePreview: photo.Data, SignedSamplePackage: photo.Data, SampleType: this.getExpectedSampleType()!, }; // then stop await this.stop(); } async takeNativePhoto(facingMode: CameraFacingMode): Promise { throw new Error('Not implemented'); } /** * Capture one JPEG photo at the camera's photo resolution — the ImageCapture API where it is * available, a second high-resolution stream otherwise. Used for manual snapshots. */ async takeHighResolutionPhoto(facingMode: CameraFacingMode, resolution?: number): Promise { throw new Error('Not implemented'); } async uploadSample(): Promise { if (this.snapshot) return await this.onSuccess(); const verifierOutput = await this.verifier!.getStateContainer() as VerifierStateContainerForPublicDataBase; if (verifierOutput === null) { zenidLog.warn('Not expected: no output from verifier'); } else { await this.camera.stop(); switch (verifierOutput?.State) { case VerifierState.Failed: zenidLog.info("Verification failed."); break; case VerifierState.InProgress: zenidLog.info("Verification aborted."); break; case VerifierState.Success: return await this.onSuccess(); default: zenidLog.info("Verification completed"); } } return null; } async verifyAndUpload(verifierType: SdkVerifierType, settings: string | VerifierSettingsBase = "", onFrameProcessed?: OnFrameProcessedCallback, onVerificationFailed?: OnVerificationFailedCallback): Promise { await this.loadAndRunVerifier(verifierType, settings, onFrameProcessed, onVerificationFailed); return await this.uploadSample(); } // PROTECTED METHODS protected async initSdk(): Promise { let estimatedMemoryUsage: any = await ZenidSDK.Instance.measureAvailableRAM(); // Parse the result as a tuple of [available RAM, total RAM] let obj = JSON.parse(estimatedMemoryUsage); zenidLog.info('contiguousMB:', obj.contiguousMB); zenidLog.info('fragmentedMB:', obj.fragmentedMB); if (obj.fragmentedMB < 128 || obj.contiguousMB < 120) zenidLog.warn('Estimated memory usage is very low, some functionality will be unavailable.'); this.challengeToken = await ZenidSDK.Instance.getChallengeToken(); zenidLog.info("Challenge token: ", this.challengeToken); // request initSdk const responseToken = await this.backendApi.initSdk(this.challengeToken, this.customData); const result = await ZenidSDK.Instance.authorize(responseToken); // Throwing is what tells the caller the backend refused the token. Returning quietly only moves // the failure to the first verifier, far from the wrong URL or API key that caused it. if (!result) throw new Error('ZenID authorization failed: the backend rejected the challenge token.'); } protected async setupVisualElements(): Promise { if (!this.camera || !this.visualizer) { throw new Error('Camera or visualizer not set'); } await this.camera.init(); await this.visualizer.init(); } protected async startMsLiveness(state: MsLivenessStateContainerForPublicData): Promise { throw new Error("Not implemented!"); } // PRIVATE METHODS private checkCrossOriginIsolation(): void { if (!self.crossOriginIsolated) { const errorMessage = 'Cross-origin isolation is not enabled. Please enable it to use the SDK.'; zenidLog.error(errorMessage); throw new Error(errorMessage); } } private getAllowedModels(): ZenidModel[] { return zenidModels .filter(model => this.setupInfo!.AllowedDocumentCodes?.some(dc => model.documentCode === dc)); } private getExpectedSampleType(): SampleType | null { switch (this.verifier?.getVerifierType()) { case SdkVerifierType.Document: case SdkVerifierType.Hologram: case SdkVerifierType.IQSHologram: return SampleType.DocumentPicture; case SdkVerifierType.FaceLiveness: case SdkVerifierType.Selfie: return SampleType.Selfie; case SdkVerifierType.LicensePlate: return SampleType.LicensePlate; default: return null; } } private async initRequiredSteps(selectedProfile?: string) { let steps: SdkWizardStep[] = []; if (!selectedProfile || !this.setupInfo || !this.setupInfo.RequiredSteps) { steps = await ZenidSDK.Instance.getSdkWizardSteps(); } else { steps = this.setupInfo?.RequiredSteps?.[selectedProfile] ?? []; } this.wizard?.setRequiredSteps(...steps); } private async onSuccess(): Promise { let data: UploadReadyData = this.snapshot ?? await this.verifier!.getUploadReadyData(); let sampleToUpload = new SampleToUpload(base64ToUint8Array(data.SignedSamplePackage!)); if (this.snapshot) { if (data.SampleType) sampleToUpload.set(data.SampleType); let documentState = await this.verifier?.getStateContainer() as DocumentVerifierStateContainerForPublicData; if (documentState && documentState.DocumentCode && documentState.PageCode) { sampleToUpload.set(documentState.DocumentCode); sampleToUpload.set(documentState.PageCode); } else { const acceptableInput = (this.verifier?.getSettings() as DocumentVerifierSettings)?.AcceptableInput; if (acceptableInput) { sampleToUpload.set(Object.assign(new AcceptableInput(), acceptableInput)); } } } try { const response = await this.backendApi.uploadSample(sampleToUpload); if (!response || response.ErrorCode) { throw new Error(`apiError: ${response.ErrorCode} ${response.ErrorText}`); } return response; } catch (error) { zenidLog.error('Error uploading file:', error); throw error; } } }