diff --git a/src/lib/utils/asleep.ts b/src/lib/utils/asleep.ts new file mode 100644 index 00000000..695dca98 --- /dev/null +++ b/src/lib/utils/asleep.ts @@ -0,0 +1,38 @@ +/** + * Asynchronously sleep for a specified duration with optional abort signal support. + * + * @param ms - The duration to sleep in milliseconds + * @param signal - Optional AbortSignal to cancel the sleep operation + * @returns A promise that resolves after the specified duration or rejects if aborted + * @throws Error if the operation is aborted via the signal + * + * @remarks + * This function should be replaced when the `@keetanetwork/keetanet-client` + * `KeetaNet.lib.Utils.Helper.asleep` method is updated to support `AbortSignal`. + */ +export function asleep(ms: number, signal?: AbortSignal): Promise { + return(new Promise((resolve, reject) => { + let abortHandler: (() => void) | undefined; + + // Check if already aborted + if (signal?.aborted) { + reject(new Error('Sleep aborted')); + return; + } + + const timeout = setTimeout(() => { + if (abortHandler) { + signal?.removeEventListener('abort', abortHandler); + } + resolve(); + }, ms); + + if (signal) { + abortHandler = () => { + clearTimeout(timeout); + reject(new Error('Sleep aborted')); + }; + signal.addEventListener('abort', abortHandler, { once: true }); + } + })); +} diff --git a/src/services/kyc/client.test.ts b/src/services/kyc/client.test.ts index 3cf30fa4..e3be40b7 100644 --- a/src/services/kyc/client.test.ts +++ b/src/services/kyc/client.test.ts @@ -283,3 +283,608 @@ test('KYC Anchor Client Test', async function() { break; } }, 30000); + +test('KYC Anchor Client - waitForCertificates with immediate success', async function() { + /* + * Enable Debug logging if requested + */ + const loggerBase = DEBUG ? console : undefined; + const logger = loggerBase ? { logger: loggerBase } : {}; + + /* + * Create an account to use for the node + */ + const seed = 'B56AA6594977F94A8D40099674ADFACF34E1208ED965E5F7E76EE6D8A2E2744E'; + const account = KeetaNet.lib.Account.fromSeed(seed, 0); + + /* + * Start a KeetaNet Node and get the UserClient for it + */ + const { userClient: client } = await createNodeAndClient(account); + + /* + * Create a dummy Root CA + */ + const rootCAAccount = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + const rootCABuilder = new KeetaNet.lib.Utils.Certificate.CertificateBuilder({ + subjectPublicKey: rootCAAccount, + issuerDN: [{ name: 'commonName', value: 'Root CA' }], + subjectDN: [{ name: 'commonName', value: 'Root CA' }], + issuer: rootCAAccount, + serial: 1, + validFrom: new Date(Date.now() - 30_000), + validTo: new Date(Date.now() + 120_000) + }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const rootCA = await rootCABuilder.build(); + + const kycCAAccount = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + const kycCABuilder = new KeetaNet.lib.Utils.Certificate.CertificateBuilder({ + subjectPublicKey: kycCAAccount, + issuerDN: [{ name: 'commonName', value: 'Root CA' }], + subjectDN: [{ name: 'commonName', value: 'Intermediate/KYC CA' }], + issuer: rootCAAccount, + serial: 2, + isCA: true, + validFrom: new Date(Date.now() - 30_000), + validTo: new Date(Date.now() + 120_000) + }); + const kycCA = await kycCABuilder.build(); + + /* + * Start a testing KYC Anchor HTTP Server with immediate certificate issuance + */ + const signer = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + + const verifications = new Map; + const certificates = new Map(); + // eslint-disable-next-line prefer-const + let serverURL: string; + await using server = new KeetaNetKYCAnchorHTTPServer({ + signer: signer, + ca: kycCA, + client: client, + kyc: { + countryCodes: ['US'], + verificationStarted: async function(request) { + const id = crypto.randomUUID(); + verifications.set(id, request); + + // Immediately create a certificate for immediate success test + const userAccount = KeetaNet.lib.Account.fromPublicKeyString(request.account).assertAccount(); + const certificateBuilder = new KYCCertificateBuilder({ + subject: userAccount, + subjectDN: [{ name: 'commonName', value: 'KYC Verified User' }], + issuerDN: kycCA.subjectDN, + issuer: kycCAAccount, + serial: 3, + validFrom: new Date(Date.now() - 30_000), + validTo: new Date(Date.now() + 120_000) + }); + certificateBuilder.setAttribute('fullName', true, 'John Doe'); + const builtCertificate = await certificateBuilder.build(); + certificates.set(id, builtCertificate.toPEM()); + + return({ + ok: true, + id: id, + expectedCost: { + min: '0', + max: '0', + token: client.baseToken.publicKeyString.get() + } + }); + }, + getCertificates: async function(verificationID) { + const request = verifications.get(verificationID); + if (request === undefined) { + throw(new KeetaAnchorKYCErrors.VerificationNotFound(`Verification ID ${verificationID} not found`)); + } + + const certificate = certificates.get(verificationID); + if (certificate === undefined) { + throw(new KeetaAnchorKYCErrors.CertificateNotFound('Certificate not ready yet')); + } + + return([{ + certificate: certificate, + intermediates: [kycCA.toPEM()] + }]); + } + }, + kycProviderURL: function(verificationID: string) { + return(new URL(`/provider/${verificationID}`, serverURL).toString()); + }, + ...logger + }); + + await server.start(); + serverURL = server.url; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const results = await client.setInfo({ + description: 'KYC Anchor Test Root', + name: 'TEST', + metadata: KeetaAnchorResolver.Metadata.formatMetadata({ + version: 1, + services: { + kyc: { + Test: await server.serviceMetadata() + } + } + }) + }); + + const kycClient = new KeetaNetAnchor.KYC.Client(client, { + root: account, + ...logger + }); + + const providers = await kycClient.createVerification({ + countryCodes: ['US'], + account: account + }); + + expect(providers.length).toBeGreaterThan(0); + + const provider = providers[0]; + if (provider === undefined) { + throw(new Error('internal error: no providers available')); + } + + const verification = await provider.startVerification(); + + // Test waitForCertificates - should succeed immediately + const result = await verification.waitForCertificates(500, 10000); + + expect(result.ok).toBe(true); + expect(result.results.length).toBeGreaterThan(0); + loggerBase?.log('waitForCertificates succeeded immediately'); +}, 30000); + +test('KYC Anchor Client - waitForCertificates with delayed success', async function() { + /* + * Enable Debug logging if requested + */ + const loggerBase = DEBUG ? console : undefined; + const logger = loggerBase ? { logger: loggerBase } : {}; + + /* + * Create an account to use for the node + */ + const seed = 'B56AA6594977F94A8D40099674ADFACF34E1208ED965E5F7E76EE6D8A2E2744E'; + const account = KeetaNet.lib.Account.fromSeed(seed, 0); + + /* + * Start a KeetaNet Node and get the UserClient for it + */ + const { userClient: client } = await createNodeAndClient(account); + + /* + * Create a dummy Root CA + */ + const rootCAAccount = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + const rootCABuilder = new KeetaNet.lib.Utils.Certificate.CertificateBuilder({ + subjectPublicKey: rootCAAccount, + issuerDN: [{ name: 'commonName', value: 'Root CA' }], + subjectDN: [{ name: 'commonName', value: 'Root CA' }], + issuer: rootCAAccount, + serial: 1, + validFrom: new Date(Date.now() - 30_000), + validTo: new Date(Date.now() + 120_000) + }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const rootCA = await rootCABuilder.build(); + + const kycCAAccount = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + const kycCABuilder = new KeetaNet.lib.Utils.Certificate.CertificateBuilder({ + subjectPublicKey: kycCAAccount, + issuerDN: [{ name: 'commonName', value: 'Root CA' }], + subjectDN: [{ name: 'commonName', value: 'Intermediate/KYC CA' }], + issuer: rootCAAccount, + serial: 2, + isCA: true, + validFrom: new Date(Date.now() - 30_000), + validTo: new Date(Date.now() + 120_000) + }); + const kycCA = await kycCABuilder.build(); + + /* + * Start a testing KYC Anchor HTTP Server with delayed certificate issuance + */ + const signer = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + + const verifications = new Map; + const certificates = new Map(); + const certificateIssuanceTimes = new Map(); + // eslint-disable-next-line prefer-const + let serverURL: string; + await using server = new KeetaNetKYCAnchorHTTPServer({ + signer: signer, + ca: kycCA, + client: client, + kyc: { + countryCodes: ['US'], + verificationStarted: async function(request) { + const id = crypto.randomUUID(); + verifications.set(id, request); + + // Schedule certificate to be available after 2 seconds + certificateIssuanceTimes.set(id, Date.now() + 2000); + + return({ + ok: true, + id: id, + expectedCost: { + min: '0', + max: '0', + token: client.baseToken.publicKeyString.get() + } + }); + }, + getCertificates: async function(verificationID) { + const request = verifications.get(verificationID); + if (request === undefined) { + throw(new KeetaAnchorKYCErrors.VerificationNotFound(`Verification ID ${verificationID} not found`)); + } + + let certificate = certificates.get(verificationID); + if (certificate === undefined) { + // Check if it's time to issue the certificate + const issuanceTime = certificateIssuanceTimes.get(verificationID); + if (issuanceTime && Date.now() >= issuanceTime) { + const userAccount = KeetaNet.lib.Account.fromPublicKeyString(request.account).assertAccount(); + const certificateBuilder = new KYCCertificateBuilder({ + subject: userAccount, + subjectDN: [{ name: 'commonName', value: 'KYC Verified User' }], + issuerDN: kycCA.subjectDN, + issuer: kycCAAccount, + serial: 3, + validFrom: new Date(Date.now() - 30_000), + validTo: new Date(Date.now() + 120_000) + }); + certificateBuilder.setAttribute('fullName', true, 'Jane Smith'); + const builtCertificate = await certificateBuilder.build(); + certificate = builtCertificate.toPEM(); + certificates.set(verificationID, certificate); + } else { + throw(new KeetaAnchorKYCErrors.CertificateNotFound('Certificate not ready yet')); + } + } + + return([{ + certificate: certificate, + intermediates: [kycCA.toPEM()] + }]); + } + }, + kycProviderURL: function(verificationID: string) { + return(new URL(`/provider/${verificationID}`, serverURL).toString()); + }, + ...logger + }); + + await server.start(); + serverURL = server.url; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const results = await client.setInfo({ + description: 'KYC Anchor Test Root', + name: 'TEST', + metadata: KeetaAnchorResolver.Metadata.formatMetadata({ + version: 1, + services: { + kyc: { + Test: await server.serviceMetadata() + } + } + }) + }); + + const kycClient = new KeetaNetAnchor.KYC.Client(client, { + root: account, + ...logger + }); + + const providers = await kycClient.createVerification({ + countryCodes: ['US'], + account: account + }); + + expect(providers.length).toBeGreaterThan(0); + + const provider = providers[0]; + if (provider === undefined) { + throw(new Error('internal error: no providers available')); + } + + const verification = await provider.startVerification(); + + // Test waitForCertificates - should succeed after 2 seconds of polling + const startTime = Date.now(); + const result = await verification.waitForCertificates(500, 10000); + const elapsed = Date.now() - startTime; + + expect(result.ok).toBe(true); + expect(result.results.length).toBeGreaterThan(0); + expect(elapsed).toBeGreaterThanOrEqual(2000); // Should have waited at least 2 seconds + expect(elapsed).toBeLessThan(10000); // Should not have timed out + loggerBase?.log(`waitForCertificates succeeded after ${elapsed}ms`); +}, 30000); + +test('KYC Anchor Client - waitForCertificates timeout', async function() { + /* + * Enable Debug logging if requested + */ + const loggerBase = DEBUG ? console : undefined; + const logger = loggerBase ? { logger: loggerBase } : {}; + + /* + * Create an account to use for the node + */ + const seed = 'B56AA6594977F94A8D40099674ADFACF34E1208ED965E5F7E76EE6D8A2E2744E'; + const account = KeetaNet.lib.Account.fromSeed(seed, 0); + + /* + * Start a KeetaNet Node and get the UserClient for it + */ + const { userClient: client } = await createNodeAndClient(account); + + /* + * Create a dummy Root CA + */ + const rootCAAccount = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + const rootCABuilder = new KeetaNet.lib.Utils.Certificate.CertificateBuilder({ + subjectPublicKey: rootCAAccount, + issuerDN: [{ name: 'commonName', value: 'Root CA' }], + subjectDN: [{ name: 'commonName', value: 'Root CA' }], + issuer: rootCAAccount, + serial: 1, + validFrom: new Date(Date.now() - 30_000), + validTo: new Date(Date.now() + 120_000) + }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const rootCA = await rootCABuilder.build(); + + const kycCAAccount = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + const kycCABuilder = new KeetaNet.lib.Utils.Certificate.CertificateBuilder({ + subjectPublicKey: kycCAAccount, + issuerDN: [{ name: 'commonName', value: 'Root CA' }], + subjectDN: [{ name: 'commonName', value: 'Intermediate/KYC CA' }], + issuer: rootCAAccount, + serial: 2, + isCA: true, + validFrom: new Date(Date.now() - 30_000), + validTo: new Date(Date.now() + 120_000) + }); + const kycCA = await kycCABuilder.build(); + + /* + * Start a testing KYC Anchor HTTP Server that never issues certificates + */ + const signer = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + + const verifications = new Map; + // eslint-disable-next-line prefer-const + let serverURL: string; + await using server = new KeetaNetKYCAnchorHTTPServer({ + signer: signer, + ca: kycCA, + client: client, + kyc: { + countryCodes: ['US'], + verificationStarted: async function(request) { + const id = crypto.randomUUID(); + verifications.set(id, request); + + return({ + ok: true, + id: id, + expectedCost: { + min: '0', + max: '0', + token: client.baseToken.publicKeyString.get() + } + }); + }, + getCertificates: async function(verificationID) { + const request = verifications.get(verificationID); + if (request === undefined) { + throw(new KeetaAnchorKYCErrors.VerificationNotFound(`Verification ID ${verificationID} not found`)); + } + + // Always return certificate not found + throw(new KeetaAnchorKYCErrors.CertificateNotFound('Certificate not ready yet')); + } + }, + kycProviderURL: function(verificationID: string) { + return(new URL(`/provider/${verificationID}`, serverURL).toString()); + }, + ...logger + }); + + await server.start(); + serverURL = server.url; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const results = await client.setInfo({ + description: 'KYC Anchor Test Root', + name: 'TEST', + metadata: KeetaAnchorResolver.Metadata.formatMetadata({ + version: 1, + services: { + kyc: { + Test: await server.serviceMetadata() + } + } + }) + }); + + const kycClient = new KeetaNetAnchor.KYC.Client(client, { + root: account, + ...logger + }); + + const providers = await kycClient.createVerification({ + countryCodes: ['US'], + account: account + }); + + expect(providers.length).toBeGreaterThan(0); + + const provider = providers[0]; + if (provider === undefined) { + throw(new Error('internal error: no providers available')); + } + + const verification = await provider.startVerification(); + + // Test waitForCertificates with timeout - should throw timeout error + await expect(async () => { + await verification.waitForCertificates(500, 2000); // Short timeout for test + }).rejects.toThrow(/Timeout waiting for KYC certificates/); + + loggerBase?.log('waitForCertificates correctly timed out'); +}, 30000); + +test('KYC Anchor Client - waitForCertificates abort signal', async function() { + /* + * Enable Debug logging if requested + */ + const loggerBase = DEBUG ? console : undefined; + const logger = loggerBase ? { logger: loggerBase } : {}; + + /* + * Create an account to use for the node + */ + const seed = 'B56AA6594977F94A8D40099674ADFACF34E1208ED965E5F7E76EE6D8A2E2744E'; + const account = KeetaNet.lib.Account.fromSeed(seed, 0); + + /* + * Start a KeetaNet Node and get the UserClient for it + */ + const { userClient: client } = await createNodeAndClient(account); + + /* + * Create a dummy Root CA + */ + const rootCAAccount = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + const rootCABuilder = new KeetaNet.lib.Utils.Certificate.CertificateBuilder({ + subjectPublicKey: rootCAAccount, + issuerDN: [{ name: 'commonName', value: 'Root CA' }], + subjectDN: [{ name: 'commonName', value: 'Root CA' }], + issuer: rootCAAccount, + serial: 1, + validFrom: new Date(Date.now() - 30_000), + validTo: new Date(Date.now() + 120_000) + }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const rootCA = await rootCABuilder.build(); + + const kycCAAccount = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + const kycCABuilder = new KeetaNet.lib.Utils.Certificate.CertificateBuilder({ + subjectPublicKey: kycCAAccount, + issuerDN: [{ name: 'commonName', value: 'Root CA' }], + subjectDN: [{ name: 'commonName', value: 'Intermediate/KYC CA' }], + issuer: rootCAAccount, + serial: 2, + isCA: true, + validFrom: new Date(Date.now() - 30_000), + validTo: new Date(Date.now() + 120_000) + }); + const kycCA = await kycCABuilder.build(); + + /* + * Start a testing KYC Anchor HTTP Server that never issues certificates + */ + const signer = KeetaNet.lib.Account.fromSeed(KeetaNet.lib.Account.generateRandomSeed(), 0); + + const verifications = new Map; + // eslint-disable-next-line prefer-const + let serverURL: string; + await using server = new KeetaNetKYCAnchorHTTPServer({ + signer: signer, + ca: kycCA, + client: client, + kyc: { + countryCodes: ['US'], + verificationStarted: async function(request) { + const id = crypto.randomUUID(); + verifications.set(id, request); + + return({ + ok: true, + id: id, + expectedCost: { + min: '0', + max: '0', + token: client.baseToken.publicKeyString.get() + } + }); + }, + getCertificates: async function(verificationID) { + const request = verifications.get(verificationID); + if (request === undefined) { + throw(new KeetaAnchorKYCErrors.VerificationNotFound(`Verification ID ${verificationID} not found`)); + } + + // Always return certificate not found + throw(new KeetaAnchorKYCErrors.CertificateNotFound('Certificate not ready yet')); + } + }, + kycProviderURL: function(verificationID: string) { + return(new URL(`/provider/${verificationID}`, serverURL).toString()); + }, + ...logger + }); + + await server.start(); + serverURL = server.url; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const results = await client.setInfo({ + description: 'KYC Anchor Test Root', + name: 'TEST', + metadata: KeetaAnchorResolver.Metadata.formatMetadata({ + version: 1, + services: { + kyc: { + Test: await server.serviceMetadata() + } + } + }) + }); + + const kycClient = new KeetaNetAnchor.KYC.Client(client, { + root: account, + ...logger + }); + + const providers = await kycClient.createVerification({ + countryCodes: ['US'], + account: account + }); + + expect(providers.length).toBeGreaterThan(0); + + const provider = providers[0]; + if (provider === undefined) { + throw(new Error('internal error: no providers available')); + } + + const verification = await provider.startVerification(); + + // Test waitForCertificates with abort signal + const abortController = new AbortController(); + + // Abort after 1 second + setTimeout(() => { + abortController.abort(); + }, 1000); + + await expect(async () => { + await verification.waitForCertificates(500, 10000, abortController.signal); + }).rejects.toThrow(/aborted/); + + loggerBase?.log('waitForCertificates correctly aborted'); +}, 30000); diff --git a/src/services/kyc/client.ts b/src/services/kyc/client.ts index 43fcef2c..ee93219a 100644 --- a/src/services/kyc/client.ts +++ b/src/services/kyc/client.ts @@ -4,6 +4,7 @@ import { createIs } from 'typia'; import { getDefaultResolver } from '../../config.js'; import { Certificate as KYCCertificate } from '../../lib/certificates.js'; +import { KeetaAnchorError } from '../../lib/error.js'; import type { Client as KeetaNetClient, @@ -23,6 +24,7 @@ import type Resolver from '../../lib/resolver.ts'; import type { ServiceMetadata } from '../../lib/resolver.ts'; import crypto from '../../lib/utils/crypto.js'; import { validateURL } from '../../lib/utils/url.js'; +import { asleep } from '../../lib/utils/asleep.js'; const PARANOID = true; @@ -70,6 +72,11 @@ type KeetaKYCAnchorClientGetCertificateResponse = ({ reason: string; }); +/** + * The successful response type for certificate retrieval operations + */ +type KeetaKYCAnchorClientGetCertificateSuccessResponse = Extract; + type KeetaKYCAnchorClientCreateVerificationRequest = Omit & { account: InstanceType; }; @@ -277,18 +284,62 @@ class KeetaKYCVerification { /** * Wait for the certificates to be available, polling at the given interval * and timing out after the given timeout period. + * + * @param pollInterval - The interval in milliseconds between polling attempts (default: 500ms) + * @param timeout - The maximum time in milliseconds to wait for certificates (default: 600000ms = 10 minutes) + * @param signal - Optional AbortSignal to cancel the waiting operation + * @returns A promise that resolves with the certificate response + * @throws Error if timeout is reached or operation is aborted */ - async waitForCertificates(pollInterval: number = 500, timeout: number = 600000): Promise { - for (const startTime = Date.now(); Date.now() - startTime < timeout; ) { + async waitForCertificates(pollInterval: number = 500, timeout: number = 600000, signal?: AbortSignal): Promise { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + // Check abort signal before each attempt + if (signal?.aborted) { + throw(new Error('Certificate wait aborted')); + } + try { - return(await this.getCertificates()); + const result = await this.getCertificates(); + + if (result.ok) { + // Successfully retrieved certificates + return(result); + } + + // Handle retryable response (e.g., certificate not ready yet) + this.logger?.debug(`Certificate not ready for request ${this.id}, will retry after ${result.retryAfter}ms. Reason: ${result.reason}`); + + // Use the server-provided retry delay, but respect the poll interval as a minimum + const waitTime = Math.max(result.retryAfter, pollInterval); + + // Wait before retrying, checking abort signal periodically + await asleep(waitTime, signal); + } catch (getCertificatesError) { - /* XXX:TODO */ - throw(getCertificatesError); + // Check if this is a KeetaAnchorError with retryable property + if (KeetaAnchorError.isInstance(getCertificatesError)) { + if (!getCertificatesError.retryable) { + // Non-retryable error, rethrow immediately + this.logger?.error(`Permanent error fetching certificates for request ${this.id}: ${getCertificatesError.message}`); + throw(getCertificatesError); + } + // Retryable error, continue to next iteration after a delay + this.logger?.debug(`Retryable error fetching certificates for request ${this.id}, will retry after ${pollInterval}ms: ${getCertificatesError.message}`); + await asleep(pollInterval, signal); + } else { + // Unknown error type, rethrow as it might be fatal + this.logger?.error(`Unexpected error fetching certificates for request ${this.id}:`, getCertificatesError); + throw(getCertificatesError); + } } } - throw(new Error('Timeout waiting for KYC certificates')); + + // Timeout reached + throw(new Error(`Timeout waiting for KYC certificates (${timeout}ms elapsed)`)); } + } /** @@ -454,18 +505,6 @@ class KeetaKYCAnchorClient { } }); - /* - * Handle retryable errors by passing them up to the caller to - * retry. - */ - if (response.status === 404) { - return({ - ok: false, - retryAfter: 500, - reason: 'Certificate not found' - }); - } - /* * Handle other errors as fatal errors that should not be retried. */ @@ -479,7 +518,9 @@ class KeetaKYCAnchorClient { } if (!responseJSON.ok) { - throw(new Error(`KYC certificate request failed: ${responseJSON.error}`)); + // Deserialize error using KeetaAnchorError.fromJSON if possible + const error = await KeetaAnchorError.fromJSON(responseJSON); + throw(error); } return({ diff --git a/src/services/kyc/common.ts b/src/services/kyc/common.ts index 4a8b38fa..503882da 100644 --- a/src/services/kyc/common.ts +++ b/src/services/kyc/common.ts @@ -107,6 +107,7 @@ class KeetaKYCAnchorCertificateNotFoundError extends KeetaAnchorUserError { constructor(message?: string) { super(message ?? 'Certificate not found (pending)'); this.statusCode = 404; + this.retryable = true; Object.defineProperty(this, 'KeetaKYCAnchorCertificateNotFoundErrorObjectTypeID', { value: KeetaKYCAnchorCertificateNotFoundError.KeetaKYCAnchorCertificateNotFoundErrorObjectTypeID,