From 647fa6f1281cd1b5e48539d7513d1529e3f2c505 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Wed, 15 Jan 2025 13:53:16 +0100 Subject: [PATCH 1/3] chore: improve timeout handling for fetch call Signed-off-by: Mirko Mollik --- packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts | 13 +--- packages/sd-jwt-vc/src/test/vct.spec.ts | 69 ++++++++++++++------ 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts b/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts index 0832961..1050cdd 100644 --- a/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts +++ b/packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts @@ -167,17 +167,10 @@ export class SDJwtVcInstance extends SDJwtInstance { * @returns */ private async fetch(url: string, integrity?: string): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout( - () => controller.abort(), - this.userConfig.timeout ?? 10000, - ); - try { const response = await fetch(url, { - signal: controller.signal, + signal: AbortSignal.timeout(this.userConfig.timeout ?? 10000), }); - if (!response.ok) { const errorText = await response.text(); throw new Error( @@ -187,12 +180,10 @@ export class SDJwtVcInstance extends SDJwtInstance { await this.validateIntegrity(response.clone(), url, integrity); return response.json() as Promise; } catch (error) { - if (error instanceof DOMException && error.name === 'AbortError') { + if (error instanceof DOMException && error.name === 'TimeoutError') { throw new Error(`Request to ${url} timed out`); } throw error; - } finally { - clearTimeout(timeoutId); } } diff --git a/packages/sd-jwt-vc/src/test/vct.spec.ts b/packages/sd-jwt-vc/src/test/vct.spec.ts index 3189e64..1d7b8fc 100644 --- a/packages/sd-jwt-vc/src/test/vct.spec.ts +++ b/packages/sd-jwt-vc/src/test/vct.spec.ts @@ -1,6 +1,6 @@ import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; import type { DisclosureFrame, Signer, Verifier } from '@sd-jwt/types'; -import { describe, test, beforeAll, afterAll } from 'vitest'; +import { describe, test, beforeAll, afterAll, expect } from 'vitest'; import { SDJwtVcInstance } from '..'; import type { SdJwtVcPayload } from '../sd-jwt-vc-payload'; import Crypto from 'node:crypto'; @@ -41,7 +41,7 @@ const restHandlers = [ }; return HttpResponse.json(res); }), - http.get('http://exmaple.com/example', () => { + http.get('http://example.com/example', () => { const res: TypeMetadataFormat = { vct: 'http://example.com/example', name: 'ExampleCredentialType', @@ -53,6 +53,13 @@ const restHandlers = [ }; return HttpResponse.json(res); }), + http.get('http://example.com/timeout', () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(HttpResponse.json({})); + }, 10000); + }); + }), ]; //this value could be generated on demand to make it easier when changing the values @@ -62,7 +69,7 @@ const vctIntegrity = const server = setupServer(...restHandlers); const iss = 'ExampleIssuer'; -const vct = 'http://exmaple.com/example'; +const vct = 'http://example.com/example'; const iat = new Date().getTime() / 1000; const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); @@ -84,6 +91,26 @@ const createSignerVerifier = () => { }; describe('App', () => { + const { signer, verifier } = createSignerVerifier(); + + const sdjwt = new SDJwtVcInstance({ + signer, + signAlg: 'EdDSA', + verifier, + hasher: digest, + hashAlg: 'sha-256', + saltGenerator: generateSalt, + loadTypeMetadataFormat: true, + timeout: 1000, + }); + + const claims = { + firstname: 'John', + }; + const disclosureFrame = { + _sd: ['firstname'], + }; + beforeAll(() => server.listen({ onUnhandledRequest: 'warn' })); afterAll(() => server.close()); @@ -91,24 +118,6 @@ describe('App', () => { afterEach(() => server.resetHandlers()); test('VCT Validation', async () => { - const { signer, verifier } = createSignerVerifier(); - const sdjwt = new SDJwtVcInstance({ - signer, - signAlg: 'EdDSA', - verifier, - hasher: digest, - hashAlg: 'sha-256', - saltGenerator: generateSalt, - loadTypeMetadataFormat: true, - }); - - const claims = { - firstname: 'John', - }; - const disclosureFrame = { - _sd: ['firstname'], - }; - const expectedPayload: SdJwtVcPayload = { iat, iss, @@ -124,5 +133,23 @@ describe('App', () => { await sdjwt.verify(encodedSdjwt); }); + test('VCT Validation with timeout', async () => { + const vct = 'http://example.com/timeout'; + const expectedPayload: SdJwtVcPayload = { + iat, + iss, + vct, + ...claims, + }; + const encodedSdjwt = await sdjwt.issue( + expectedPayload, + disclosureFrame as unknown as DisclosureFrame, + ); + + expect(sdjwt.verify(encodedSdjwt)).rejects.toThrowError( + `Request to ${vct} timed out`, + ); + }); + //TODO: we need tests with an embedded schema, extended and maybe also to test the errors when schema information is not available or the integrity is not valid }); From 2f8ed37b716968e21034052b42d87f6103815ab0 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Wed, 15 Jan 2025 13:54:16 +0100 Subject: [PATCH 2/3] chore: remove empty line Signed-off-by: Mirko Mollik --- packages/types/src/type.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/types/src/type.ts b/packages/types/src/type.ts index 8de9d7c..0f0ac62 100644 --- a/packages/types/src/type.ts +++ b/packages/types/src/type.ts @@ -32,7 +32,6 @@ export const IANA_HASH_ALGORITHMS = [ export type HashAlgorithm = (typeof IANA_HASH_ALGORITHMS)[number]; - export type SDJWTConfig = { omitTyp?: boolean; hasher?: Hasher; @@ -171,15 +170,15 @@ type Frame = Payload extends Array ? Record> & SD & DECOY : SD & DECOY : Payload extends Record - ? NonNever< - { - [K in keyof Payload]?: Payload[K] extends object - ? Frame - : never; - } & SD & - DECOY - > - : SD & DECOY; + ? NonNever< + { + [K in keyof Payload]?: Payload[K] extends object + ? Frame + : never; + } & SD & + DECOY + > + : SD & DECOY; /** * This is a disclosureFrame type that is used to represent the structure of what is being disclosed. From 3bad32f3b603d50a7ab02cf503b0af3a7dbf7116 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Wed, 15 Jan 2025 13:54:34 +0100 Subject: [PATCH 3/3] chore: format Signed-off-by: Mirko Mollik --- packages/types/src/type.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/types/src/type.ts b/packages/types/src/type.ts index 0f0ac62..62b5429 100644 --- a/packages/types/src/type.ts +++ b/packages/types/src/type.ts @@ -170,15 +170,15 @@ type Frame = Payload extends Array ? Record> & SD & DECOY : SD & DECOY : Payload extends Record - ? NonNever< - { - [K in keyof Payload]?: Payload[K] extends object - ? Frame - : never; - } & SD & - DECOY - > - : SD & DECOY; + ? NonNever< + { + [K in keyof Payload]?: Payload[K] extends object + ? Frame + : never; + } & SD & + DECOY + > + : SD & DECOY; /** * This is a disclosureFrame type that is used to represent the structure of what is being disclosed.