diff --git a/demo/issuer.ts b/demo/issuer.ts index a242ecf..3b98667 100644 --- a/demo/issuer.ts +++ b/demo/issuer.ts @@ -1,4 +1,4 @@ -import { DisclosureFrame, Hasher, Signer, base64encode } from '@meeco/sd-jwt'; +import { DisclosureFrame, Hasher, Signer, base64encode, decodeSDJWT } from '@meeco/sd-jwt'; import { createHash } from 'crypto'; import { JWTHeaderParameters, JWTPayload, KeyLike, SignJWT, importJWK } from 'jose'; import { @@ -90,6 +90,8 @@ async function main() { }; const result = await issuer.createVCSDJWT(vcClaims, payload, sdVCClaimsDisclosureFrame); + const sdjwtvc = decodeSDJWT(result); + console.log(sdjwtvc.disclosures); console.log(result); } diff --git a/src/errors.ts b/src/errors.ts index 7306f03..3e7b8cf 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,5 +1,205 @@ +export enum SDJWTVCErrorCode { + DefaultError = 'UNKNOWN_ERROR', // default in case no code is provided + InvalidIssuer = 'INVALID_ISSUER', + InvalidIssuedAt = 'INVALID_ISSUED_AT', + InvalidCallback = 'INVALID_CALLBACK', + InvalidAlgorithm = 'INVALID_ALGORITHM', + InvalidPayload = 'INVALID_PAYLOAD', +} + export class SDJWTVCError extends Error { - constructor(message: any) { - super(message); + protected code: SDJWTVCErrorCode; + protected errorType: ErrorType; + protected extraInfo: Record; + + constructor(errorType: ErrorType, extraInfo: Record = {}) { + const errorInfo = ERROR_REGISTRY[errorType]; + + super(errorInfo.message); + + this.errorType = errorType; + this.code = errorInfo.code; + this.extraInfo = extraInfo; + + this.name = this.constructor.name; + } + + getResponse(): string | Record { + return this.message; + } + + getCode(): SDJWTVCErrorCode { + return this.code; + } + + getErrorType(): ErrorType { + return this.errorType; + } + + getExtraInfo(): Record { + return this.extraInfo; + } + + equals(exception: SDJWTVCError) { + return ( + this.getErrorType() === exception.getErrorType() && + JSON.stringify(this.getExtraInfo()) === JSON.stringify(exception.getExtraInfo()) + ); } } +export type ErrorType = keyof typeof ERROR_REGISTRY; + +/** + * TODO: update error messages and codes + */ + +const ERROR_REGISTRY = { + hasher_callback_function_is_required: { + message: 'Hasher callback function is required', + code: SDJWTVCErrorCode.InvalidCallback, + }, + hasher_algorithm_is_required: { + message: 'Hasher algorithm is required', + code: SDJWTVCErrorCode.InvalidAlgorithm, + }, + signer_callback_function_is_required: { + message: 'Signer callback function is required', + code: SDJWTVCErrorCode.InvalidCallback, + }, + signer_algorithm_is_required: { + message: 'Signer algorithm is required', + code: SDJWTVCErrorCode.InvalidAlgorithm, + }, + vcClaims_is_required: { + message: 'vcClaims is required', + code: SDJWTVCErrorCode.DefaultError, + }, + sdJWTPayload_is_required: { + message: 'sdJWTPayload is required', + code: SDJWTVCErrorCode.DefaultError, + }, + invalid_issuer_url: { + message: 'Issuer iss (issuer) is required and must be a valid URL', + code: SDJWTVCErrorCode.InvalidIssuer, + }, + invalid_issued_at: { + message: 'Payload iat (Issued at - seconds since Unix epoch) is required and must be a number', + code: SDJWTVCErrorCode.InvalidIssuedAt, + }, + invalid_cnf: { + message: 'Payload cnf is required and must be a JWK format', + code: SDJWTVCErrorCode.DefaultError, + }, + invalid_cnf_jwk: { + message: 'Payload cnf.jwk must be valid JWK format', + code: SDJWTVCErrorCode.DefaultError, + }, + invalid_vct_string: { + message: 'vct value MUST be a case-sensitive string', + code: SDJWTVCErrorCode.DefaultError, + }, + invalid_vct_url: { + message: 'vct value MUST be a valid URL', + code: SDJWTVCErrorCode.InvalidIssuer, + }, + invalid_claims_object: { + message: 'Payload claims is required and must be an object', + code: SDJWTVCErrorCode.DefaultError, + }, + reserved_jwt_payload_key_in_claims: { + message: 'Claim contains reserved JWTPayload key', + code: SDJWTVCErrorCode.DefaultError, + }, + reserved_jwt_payload_key_in_disclosure_frame: { + message: 'Disclosure frame contains reserved JWTPayload key', + code: SDJWTVCErrorCode.DefaultError, + }, + failed_to_create_VCSDJWT: { + message: 'Failed to create VCSDJWT', + code: SDJWTVCErrorCode.DefaultError, + }, + missing_key_binding_verifier_callback_function: { + message: 'Missing key binding verifier callback function', + code: SDJWTVCErrorCode.InvalidCallback, // Use appropriate error code + }, + missing_aud_nonce_iat_or_sd_hash_in_key_binding_JWT: { + message: 'Missing aud, nonce, iat or sd_hash in key binding JWT', + code: SDJWTVCErrorCode.InvalidPayload, // Use appropriate error code + }, + signer_function_is_required: { + message: 'Signer function is required', + code: SDJWTVCErrorCode.InvalidCallback, + }, + algo_used_for_Signer_function_is_required: { + message: 'algo used for Signer function is required', + code: SDJWTVCErrorCode.InvalidAlgorithm, + }, + failed_to_get_Key_Binding_JWT: { + message: 'Failed to get Key Binding JWT', + code: SDJWTVCErrorCode.DefaultError, + }, + invalid_audience_parameter: { + message: 'Invalid audience parameter', + code: SDJWTVCErrorCode.DefaultError, + }, + invalid_sdJWT_parameter: { + message: 'Invalid sdJWT parameter', + code: SDJWTVCErrorCode.DefaultError, + }, + no_holder_public_key_in_SD_JWT: { + message: 'No holder public key in SD-JWT', + code: SDJWTVCErrorCode.DefaultError, + }, + no_disclosures_in_SD_JWT: { + message: 'No disclosures in SD-JWT', + code: SDJWTVCErrorCode.DefaultError, + }, + failed_to_verify_key_binding_JWT: { + message: 'Failed to verify key binding JWT: SD JWT holder public key does not match private key', + code: SDJWTVCErrorCode.DefaultError, + }, + aud_mismatch: { + message: 'aud mismatch', + code: SDJWTVCErrorCode.DefaultError, + }, + nonce_mismatch: { + message: 'nonce mismatch', + code: SDJWTVCErrorCode.DefaultError, + }, + sd_hash_mismatch: { + message: 'sd_hash mismatch', + code: SDJWTVCErrorCode.DefaultError, + }, + unsupported_algorithm: { + message: 'unsupported algorithm', + code: SDJWTVCErrorCode.DefaultError, + }, + invalid_issuer_well_known_url: { + message: 'Invalid issuer well-known URL', + code: SDJWTVCErrorCode.DefaultError, + }, + failed_to_fetch_or_parse_response: { + message: 'Failed to fetch or parse the response from {issuerUrl} as JSON. Error: {error.message}', + code: SDJWTVCErrorCode.DefaultError, + }, + issuer_public_key_jwk_not_found: { + message: 'Issuer public key JWK not found', + code: SDJWTVCErrorCode.DefaultError, + }, + issuer_response_not_found: { + message: 'Issuer response not found', + code: SDJWTVCErrorCode.DefaultError, + }, + issuer_response_does_not_contain_jwks_or_jwks_uri: { + message: 'Issuer response does not contain jwks or jwks_uri', + code: SDJWTVCErrorCode.DefaultError, + }, + issuer_response_from_wellknown_do_not_match_the_expected_issuer: { + message: "The response from the issuer's well-known URI does not match the expected issuer", + code: SDJWTVCErrorCode.InvalidIssuer, + }, + unexpected_url: { + message: 'Unexpected URL', + code: SDJWTVCErrorCode.DefaultError, + }, +} as const; diff --git a/src/holder.ts b/src/holder.ts index 4724034..9118070 100644 --- a/src/holder.ts +++ b/src/holder.ts @@ -1,8 +1,8 @@ import { Disclosure, Hasher, JWK, KeyBindingVerifier, base64encode, decodeJWT, decodeSDJWT } from '@meeco/sd-jwt'; import { SDJWTVCError } from './errors.js'; +import { hasherCallbackFn } from './test-utils/helpers.js'; import { CreateSDJWTPayload, JWT, PresentSDJWTPayload, SD_JWT_FORMAT_SEPARATOR, SignerConfig } from './types.js'; import { defaultHashAlgorithm, isValidUrl } from './util.js'; -import { hasherCallbackFn } from './test-utils/helpers.js'; export class Holder { private signer: SignerConfig; @@ -14,10 +14,10 @@ export class Holder { */ constructor(signer: SignerConfig) { if (!signer?.callback || typeof signer?.callback !== 'function') { - throw new SDJWTVCError('Signer function is required'); + throw new SDJWTVCError('signer_function_is_required'); } if (!signer?.alg || typeof signer?.alg !== 'string') { - throw new SDJWTVCError('algo used for Signer function is required'); + throw new SDJWTVCError('algo_used_for_Signer_function_is_required'); } this.signer = signer; @@ -62,7 +62,7 @@ export class Holder { return { keyBindingJWT: jwt, nonce }; } catch (error: any) { - throw new SDJWTVCError(`Failed to get Key Binding JWT: ${error.message}`); + throw new SDJWTVCError('failed_to_get_Key_Binding_JWT', { reason: error.message }); } } @@ -83,11 +83,11 @@ export class Holder { options?: { nonce?: string; audience?: string; keyBindingVerifyCallbackFn?: KeyBindingVerifier }, ): Promise<{ vcSDJWTWithkeyBindingJWT: JWT; nonce?: string }> { if (options.audience && (typeof options.audience !== 'string' || !isValidUrl(options.audience))) { - throw new SDJWTVCError('Invalid audience parameter'); + throw new SDJWTVCError('invalid_audience_parameter'); } if (typeof sdJWT !== 'string' || !sdJWT.includes(SD_JWT_FORMAT_SEPARATOR)) { - throw new SDJWTVCError('Invalid sdJWT parameter'); + throw new SDJWTVCError('invalid_sdJWT_parameter'); } const [sdJWTPayload, _] = sdJWT.split(SD_JWT_FORMAT_SEPARATOR); @@ -96,7 +96,7 @@ export class Holder { const { jwk: holderPublicKeyJWK } = (jwt.payload as CreateSDJWTPayload).cnf || {}; if (!holderPublicKeyJWK) { - throw new SDJWTVCError('No holder public key in SD-JWT'); + throw new SDJWTVCError('no_holder_public_key_in_SD_JWT'); } let sdHashAlgorithm = jwt.payload['_sd_alg'] as string; @@ -130,7 +130,7 @@ export class Holder { */ revealDisclosures(sdJWT: JWT, disclosedList: Disclosure[]): JWT { if (typeof sdJWT !== 'string' || !sdJWT.includes(SD_JWT_FORMAT_SEPARATOR)) { - throw new SDJWTVCError('No disclosures in SD-JWT'); + throw new SDJWTVCError('no_disclosures_in_SD_JWT'); } const { disclosures } = decodeSDJWT(sdJWT); @@ -174,7 +174,7 @@ export class Holder { try { await keyBindingVerifierCallbackFn(keyBindingJWT, holderPublicKeyJWK); } catch (e) { - throw new SDJWTVCError('Failed to verify key binding JWT: SD JWT holder public key does not match private key'); + throw new SDJWTVCError('failed_to_verify_key_binding_JWT'); } } } diff --git a/src/issuer.spec.ts b/src/issuer.spec.ts index 8785600..3578212 100644 --- a/src/issuer.spec.ts +++ b/src/issuer.spec.ts @@ -1,6 +1,7 @@ import { generateKeyPair } from 'jose'; import { DisclosureFrame, decodeDisclosure, decodeJWT } from '@meeco/sd-jwt'; +import { SDJWTVCError } from './errors'; import { Issuer } from './issuer'; import { hasherCallbackFn, signerCallbackFn } from './test-utils/helpers'; import { @@ -42,7 +43,7 @@ describe('Issuer', () => { }; const payload: CreateSDJWTPayload = { - iat: Date.now(), + iat: Math.floor(Date.now() / 1000), cnf: { jwk: holderPublicKey, }, @@ -121,7 +122,7 @@ describe('Issuer', () => { { callback: () => Promise.resolve(''), alg: supportedAlgorithm.ES256 }, { alg: 'SHA256', callback: undefined }, ), - ).toThrowError('Hasher function is required'); + ).toThrowSDJWTVCError(new SDJWTVCError('hasher_callback_function_is_required')); }); it('should throw an error if hasher alg is not provided', () => { @@ -131,7 +132,7 @@ describe('Issuer', () => { { callback: () => Promise.resolve(''), alg: supportedAlgorithm.ES256 }, { callback: () => '', alg: undefined }, ), - ).toThrowError('algo used for Hasher function is required'); + ).toThrowSDJWTVCError(new SDJWTVCError('hasher_algorithm_is_required')); }); it('should create an instance of Issuer if all required parameters are provided', () => { @@ -148,7 +149,7 @@ describe('Issuer', () => { describe('validateSDJWTPayload', () => { it('should throw an error if iss is missing', () => { const sdJWTPayload = { - iat: Date.now(), + iat: Math.floor(Date.now() / 1000), cnf: { jwk: {}, }, @@ -161,7 +162,7 @@ describe('Issuer', () => { it('should throw an error if iss is not a valid URL', () => { const sdJWTPayload = { - iat: Date.now(), + iat: Math.floor(Date.now() / 1000), cnf: { jwk: {}, }, @@ -202,7 +203,7 @@ describe('Issuer', () => { it('should throw an error if cnf is missing', () => { const sdJWTPayload = { - iat: Date.now(), + iat: Math.floor(Date.now() / 1000), iss: 'https://valid.issuer.url', }; @@ -213,7 +214,7 @@ describe('Issuer', () => { it('should throw an error if cnf.jwk is missing', () => { const sdJWTPayload = { - iat: Date.now(), + iat: Math.floor(Date.now() / 1000), cnf: {}, iss: 'https://valid.issuer.url', }; @@ -225,7 +226,7 @@ describe('Issuer', () => { it('should throw an error if cnf.jwk is not an object', () => { const sdJWTPayload = { - iat: Date.now(), + iat: Math.floor(Date.now() / 1000), cnf: { jwk: 'invalid-jwk', }, @@ -239,7 +240,7 @@ describe('Issuer', () => { it('should throw an error if cnf.jwk is missing kty', () => { const sdJWTPayload = { - iat: Date.now(), + iat: Math.floor(Date.now() / 1000), cnf: { jwk: { crv: 'P-256', @@ -255,7 +256,7 @@ describe('Issuer', () => { it('should throw an error if vct is not a valid String', () => { const sdJWTPayload = { - iat: Date.now(), + iat: Math.floor(Date.now() / 1000), cnf: { jwk: { kty: 'EC', @@ -273,7 +274,7 @@ describe('Issuer', () => { it('should throw an error if vct is not a valid url', () => { const sdJWTPayload = { - iat: Date.now(), + iat: Math.floor(Date.now() / 1000), cnf: { jwk: { kty: 'EC', @@ -291,7 +292,7 @@ describe('Issuer', () => { it('should not throw an error if all properties are valid', () => { const sdJWTPayload = { - iat: Date.now(), + iat: Math.floor(Date.now() / 1000), cnf: { jwk: { kty: 'EC', diff --git a/src/issuer.ts b/src/issuer.ts index 03807c8..93db4a0 100644 --- a/src/issuer.ts +++ b/src/issuer.ts @@ -12,27 +12,32 @@ import { import { isValidUrl } from './util.js'; export class Issuer { + private static readonly SD_JWT_TYP = 'vc+sd-jwt'; private hasher: HasherConfig; private signer: SignerConfig; - private static SD_JWT_TYP = 'vc+sd-jwt'; constructor(signer: SignerConfig, hasher: HasherConfig) { - if (!signer?.callback || typeof signer?.callback !== 'function') { - throw new SDJWTVCError('Signer function is required'); + this.validateSignerConfig(signer); + this.validateHahserConfig(hasher); + this.signer = signer; + this.hasher = hasher; + } + private validateSignerConfig(config: SignerConfig | HasherConfig) { + if (!config.callback || typeof config.callback !== 'function') { + throw new SDJWTVCError('signer_callback_function_is_required'); } - if (!signer?.alg || typeof signer?.alg !== 'string') { - throw new SDJWTVCError('algo used for Signer function is required'); + if (!config.alg || typeof config.alg !== 'string') { + throw new SDJWTVCError('signer_algorithm_is_required'); } + } - if (!hasher?.callback || typeof hasher?.callback !== 'function') { - throw new SDJWTVCError('Hasher function is required'); + private validateHahserConfig(config: SignerConfig | HasherConfig) { + if (!config.callback || typeof config.callback !== 'function') { + throw new SDJWTVCError('hasher_callback_function_is_required'); } - if (!hasher?.alg || typeof hasher?.alg !== 'string') { - throw new SDJWTVCError('algo used for Hasher function is required'); + if (!config.alg || typeof config.alg !== 'string') { + throw new SDJWTVCError('hasher_algorithm_is_required'); } - - this.signer = signer; - this.hasher = hasher; } // write getter for signer and hasher @@ -69,8 +74,8 @@ export class Issuer { */ async createSignedVCSDJWT(opts: CreateSignedJWTOpts): Promise { const { vcClaims, sdJWTPayload, sdVCClaimsDisclosureFrame = {}, saltGenerator, sdJWTHeader } = opts; - if (!vcClaims) throw new SDJWTVCError('vcClaims is required'); - if (!sdJWTPayload) throw new SDJWTVCError('sdJWTPayload is required'); + if (!vcClaims) throw new SDJWTVCError('vcClaims_is_required'); + if (!sdJWTPayload) throw new SDJWTVCError('sdJWTPayload_is_required'); this.validateVCClaims(vcClaims as VCClaims); this.validateSDJWTPayload(sdJWTPayload); @@ -98,7 +103,7 @@ export class Issuer { return jwt; } catch (error: any) { - throw new SDJWTVCError(`Failed to create VCSDJWT: ${error.message}`); + throw new SDJWTVCError('failed_to_create_VCSDJWT', { reason: error.message }); } } @@ -108,20 +113,20 @@ export class Issuer { */ validateSDJWTPayload(sdJWTPayload: SDJWTPayload) { if (!sdJWTPayload.iss || !isValidUrl(sdJWTPayload.iss)) { - throw new SDJWTVCError('Issuer iss (issuer) is required and must be a valid URL'); + throw new SDJWTVCError('invalid_issuer_url'); } if (!sdJWTPayload.iat || typeof sdJWTPayload.iat !== 'number') { - throw new SDJWTVCError('Payload iat (Issued at - seconds since Unix epoch) is required and must be a number'); + throw new SDJWTVCError('invalid_issued_at'); } if (!sdJWTPayload.cnf || typeof sdJWTPayload.cnf !== 'object' || !sdJWTPayload.cnf.jwk) { - throw new SDJWTVCError('Payload cnf is required and must be a JWK format'); + throw new SDJWTVCError('invalid_cnf'); } if (typeof sdJWTPayload.cnf.jwk !== 'object' || typeof sdJWTPayload.cnf.jwk.kty !== 'string') { - throw new SDJWTVCError('Payload cnf.jwk must be valid JWK format'); + throw new SDJWTVCError('invalid_cnf_jwk'); } if (!sdJWTPayload.vct || typeof sdJWTPayload.vct !== 'string') { - throw new SDJWTVCError('vct value MUST be a case-sensitive string'); + throw new SDJWTVCError('invalid_vct_string'); } const prefixes = ['http', 'https', 'https://', 'http://']; @@ -129,7 +134,7 @@ export class Issuer { prefixes.some((prefix) => (sdJWTPayload.vct as string).startsWith(prefix)) && !isValidUrl(sdJWTPayload.vct as any) ) { - throw new SDJWTVCError('vct value MUST be a valid URL'); + throw new SDJWTVCError('invalid_vct_url'); } } @@ -139,12 +144,12 @@ export class Issuer { */ validateVCClaims(claims: VCClaims) { if (!claims || typeof claims !== 'object') { - throw new SDJWTVCError('Payload claims is required and must be an object'); + throw new SDJWTVCError('invalid_claims_object'); } for (const key of ReservedJWTClaimKeys) { if (key in claims) { - throw new SDJWTVCError(`Claim contains reserved JWTPayload key: ${key}`); + throw new SDJWTVCError('reserved_jwt_payload_key_in_claims', { reason: key }); } } } @@ -157,7 +162,7 @@ export class Issuer { if (sdVCClaimsDisclosureFrame?._sd && Array.isArray(sdVCClaimsDisclosureFrame._sd)) { for (const key of sdVCClaimsDisclosureFrame._sd) { if (ReservedJWTClaimKeys.includes(key as any)) { - throw new SDJWTVCError(`Disclosure frame contains reserved JWTPayload key: ${key}`); + throw new SDJWTVCError('reserved_jwt_payload_key_in_disclosure_frame', { reason: key }); } } } diff --git a/src/test-utils/helpers.ts b/src/test-utils/helpers.ts index 8074fb7..ca3e629 100644 --- a/src/test-utils/helpers.ts +++ b/src/test-utils/helpers.ts @@ -21,18 +21,18 @@ export function kbVeriferCallbackFn( if (expectedAud || expectedNonce || expectedSdHash) { if (payload.aud !== expectedAud) { - throw new SDJWTVCError('aud mismatch'); + throw new SDJWTVCError('aud_mismatch'); } if (payload.nonce !== expectedNonce) { - throw new SDJWTVCError('nonce mismatch'); + throw new SDJWTVCError('nonce_mismatch'); } if (payload.sd_hash !== expectedSdHash) { - throw new SDJWTVCError('sd_hash mismatch'); + throw new SDJWTVCError('sd_hash_mismatch'); } } if (!Object.values(supportedAlgorithm).includes(header.alg as supportedAlgorithm)) { - throw new SDJWTVCError('unsupported algorithm'); + throw new SDJWTVCError('unsupported_algorithm'); } const holderKey = await importJWK(holderJWK, header.alg); @@ -46,7 +46,7 @@ export function keyBindingVerifierCallbackFn(): KeyBindingVerifier { const { header } = decodeJWT(kbjwt); if (!Object.values(supportedAlgorithm).includes(header.alg as supportedAlgorithm)) { - throw new SDJWTVCError('unsupported algorithm'); + throw new SDJWTVCError('unsupported_algorithm'); } const holderKey = await importJWK(holderJWK, header.alg); diff --git a/src/test-utils/matchers/error.matcher.ts b/src/test-utils/matchers/error.matcher.ts new file mode 100644 index 0000000..20f1d06 --- /dev/null +++ b/src/test-utils/matchers/error.matcher.ts @@ -0,0 +1,72 @@ +import type { MatcherFunction } from 'expect'; +import { diff } from 'jest-diff'; +import { printExpected, printReceived } from 'jest-matcher-utils'; +import { isDeepStrictEqual } from 'util'; +import { SDJWTVCError } from '../../errors'; + +/** + * Custom matcher for SDJWTVCError to make sure type and extra information is always as expected + */ +export const toThrowSDJWTVCError: MatcherFunction<[exception: unknown]> = (actual: any, expected: SDJWTVCError) => { + let exception = null; + + if (typeof actual === 'function') { + try { + actual(); + } catch (e) { + exception = e; + } + } else { + exception = actual; + } + + if (exception === null) { + return { + message: () => `expected to throw a SDJWTVCError but nothing was thrown`, + pass: false, + }; + } + + if (!(exception instanceof SDJWTVCError)) { + return { + message: () => + `expected ${printReceived(exception.constructor.name)} to be an instance of ${printExpected('SDJWTVCError')}`, + pass: false, + }; + } + + if (expected.getErrorType() !== exception.getErrorType()) { + return { + message: () => + `expected exception.getErrorType() ${printReceived( + exception.getErrorType(), + )} to be equal to ${printExpected(expected.getErrorType())}`, + pass: false, + }; + } + + const expectedExtraInfo = expected.getExtraInfo(); + const actualExtraInfo = exception.getExtraInfo(); + + if (!isDeepStrictEqual(expectedExtraInfo, actualExtraInfo)) { + return { + message: () => + `exception.getExtraInfo() does not match expected value\n${diff(expectedExtraInfo, actualExtraInfo)}`, + pass: false, + }; + } + + return { + message: () => null, + pass: true, + }; +}; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toThrowSDJWTVCError(exception: SDJWTVCError): R; + } + } +} diff --git a/src/util.spec.ts b/src/util.spec.ts index bded968..0fdc135 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -47,7 +47,7 @@ describe('getIssuerPublicKeyFromIss', () => { }), }); } else { - throw new SDJWTVCError(`Unexpected URL: ${url}`); + throw new SDJWTVCError('unexpected_url'); } }); @@ -86,7 +86,7 @@ describe('getIssuerPublicKeyFromIss', () => { }), }); } else { - throw new SDJWTVCError(`Unexpected URL: ${url}`); + throw new SDJWTVCError('unexpected_url'); } }); @@ -194,7 +194,7 @@ describe('getIssuerPublicKeyFromIss', () => { json: () => Promise.resolve({ status: 404, - json: () => Promise.reject(new SDJWTVCError('Issuer response not found')), + json: () => Promise.reject(new SDJWTVCError('issuer_response_not_found')), }), }); diff --git a/src/util.ts b/src/util.ts index 4a740d3..0ae1e69 100644 --- a/src/util.ts +++ b/src/util.ts @@ -41,7 +41,7 @@ export async function getIssuerPublicKeyFromWellKnownURI(sdJwtVC: JWT, issuerPat const wellKnownPath = `.well-known/jwt-issuer/${issuerPath}`; if (!jwt.payload.iss || !isValidUrl(jwt.payload.iss)) { - throw new SDJWTVCError('Invalid issuer well-known URL'); + throw new SDJWTVCError('invalid_issuer_well_known_url'); } const url = new URL(jwt.payload.iss); @@ -53,14 +53,14 @@ export async function getIssuerPublicKeyFromWellKnownURI(sdJwtVC: JWT, issuerPat const response = await fetch(issuerUrl); responseJson = await response.json(); } catch (error) { - throw new SDJWTVCError(`Failed to fetch or parse the response from ${issuerUrl} as JSON. Error: ${error.message}`); + throw new SDJWTVCError('failed_to_fetch_or_parse_response', { reason: `${error.message}` }); } if (!responseJson) { - throw new SDJWTVCError('Issuer response not found'); + throw new SDJWTVCError('issuer_response_not_found'); } if (!responseJson.issuer || responseJson.issuer !== jwt.payload.iss) { - throw new SDJWTVCError("The response from the issuer's well-known URI does not match the expected issuer"); + throw new SDJWTVCError('issuer_response_from_wellknown_do_not_match_the_expected_issuer'); } let issuerPublicKeyJWK: JWK | undefined; @@ -74,7 +74,7 @@ export async function getIssuerPublicKeyFromWellKnownURI(sdJwtVC: JWT, issuerPat } if (!issuerPublicKeyJWK) { - throw new SDJWTVCError('Issuer public key JWK not found'); + throw new SDJWTVCError('issuer_public_key_jwk_not_found'); } return issuerPublicKeyJWK; @@ -89,7 +89,7 @@ export async function getIssuerPublicKeyFromWellKnownURI(sdJwtVC: JWT, issuerPat */ export function getIssuerPublicKeyJWK(jwks: any, kid?: string): JWK | undefined { if (!jwks || !jwks.keys) { - throw new SDJWTVCError('Issuer response does not contain jwks or jwks_uri'); + throw new SDJWTVCError('issuer_response_does_not_contain_jwks_or_jwks_uri'); } if (kid) { diff --git a/src/verifier.ts b/src/verifier.ts index b7502b3..2931a05 100644 --- a/src/verifier.ts +++ b/src/verifier.ts @@ -30,14 +30,14 @@ export class Verifier { const { keyBindingJWT } = decodeSDJWT(sdJWT); if (keyBindingJWT) { if (!kbVeriferCallbackFn) { - throw new SDJWTVCError('Missing key binding verifier callback function'); + throw new SDJWTVCError('missing_key_binding_verifier_callback_function'); } const decodedKeyBindingJWT = decodeJWT(keyBindingJWT); const { payload } = decodedKeyBindingJWT; const { aud, nonce, iat, sd_hash } = payload; if (!aud || !nonce || !iat || !sd_hash) { - throw new SDJWTVCError('Missing aud, nonce, iat or sd_hash in key binding JWT'); + throw new SDJWTVCError('missing_aud_nonce_iat_or_sd_hash_in_key_binding_JWT'); } }