Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add better error typings #103

Merged
merged 13 commits into from
Jun 18, 2024
4 changes: 2 additions & 2 deletions libs/iota-browser/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion libs/iota-browser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@affinidi-tdk/iota-browser",
"version": "0.2.8",
"version": "0.2.9",
"description": "Browser module to fetch data through Affinidi Iota Framework",
"author": "Affinidi",
"repository": {
Expand Down
63 changes: 41 additions & 22 deletions libs/iota-browser/src/helpers/channel-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ import { iot, mqtt5 } from 'aws-iot-device-sdk-v2/dist/browser'
import * as jose from 'jose'
import { v4 as uuidv4 } from 'uuid'
import {
EventTypes,
PrepareRequestEvent,
SignedRequestEvent,
SignedRequestEventSchema,
SignedRequestJWT,
SignedRequestJWTSchema,
} from '../validators/events'
import {
getUnexpectedErrorMessage,
ErrorCode,
throwEventParsingError,
} from '../validators/error'

const DEFAULT_IOT_ENDPOINT =
'a3sq1vuw0cw9an-ats.iot.ap-southeast-1.amazonaws.com'
Expand Down Expand Up @@ -147,6 +156,29 @@ export class ChannelProvider {
await client.subscribe(packet)
}

private getRequest(event: SignedRequestEvent) {
let signedRequest: SignedRequestEvent, signedRequestJWT: SignedRequestJWT
try {
signedRequest = SignedRequestEventSchema.parse(event)
} catch (e) {
throw Error(getUnexpectedErrorMessage(ErrorCode.SIGNED_REQUEST_EVENT))
}
try {
const claims = jose.decodeJwt(signedRequest.data.jwt)
signedRequestJWT = SignedRequestJWTSchema.parse(claims)
} catch (e) {
throw Error(getUnexpectedErrorMessage(ErrorCode.SIGNED_REQUEST_JWT))
}
const request: IotaChannelRequest = {
correlationId: signedRequest.correlationId,
payload: {
request: signedRequest.data.jwt,
client_id: signedRequestJWT.client_id,
},
}
return request
}

async prepareRequest(
params: PrepareRequestParams,
): Promise<IotaChannelRequest> {
Expand Down Expand Up @@ -178,30 +210,17 @@ export class ChannelProvider {
)
try {
const event = JSON.parse(raw_data)
if (
event.eventType === 'signedRequest' &&
correlationId === event.correlationId
) {
// TODO handle Zod errors gracefully
const signedRequest = SignedRequestEventSchema.parse(event)
const claims = jose.decodeJwt(signedRequest.data.jwt)
// TODO Zod validate JWT
if (!claims.client_id && typeof claims.client_id === 'string') {
reject(new Error('Unexpected request claims received'))
}
const client_id = claims.client_id as string
const request: IotaChannelRequest = {
correlationId: signedRequest.correlationId,
payload: {
request: signedRequest.data.jwt,
client_id,
},
}

if (correlationId !== event.correlationId) {
return
}
if (event.eventType === EventTypes.SignedRequest) {
const request = this.getRequest(event)
resolve(request)
} else if (event.eventType === EventTypes.Error) {
throwEventParsingError(event)
}
} catch (error) {
reject(error)
} catch (e) {
reject(e)
}
}
},
Expand Down
57 changes: 39 additions & 18 deletions libs/iota-browser/src/helpers/response-handler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { toUtf8 } from '@aws-sdk/util-utf8-browser'
import { mqtt5 } from 'aws-iot-device-sdk-v2/dist/browser'
import {
EventTypes,
ResponseCallbackEventSchema,
VerifiablePresentation,
VerifiablePresentationSchema,
ResponseCallbackEvent,
} from '../validators/events'
import { ChannelProvider } from './channel-provider'
import {
ErrorCode,
throwEventParsingError,
getUnexpectedErrorMessage,
} from '../validators/error'

export type IotaResponse = {
correlationId: string
Expand All @@ -26,6 +33,31 @@ export class ResponseHandler {
this.channelProvider = channelProvider
}

private getResponseHandler(event: ResponseCallbackEvent) {
let responseCallback: ResponseCallbackEvent, vpToken: VerifiablePresentation
try {
responseCallback = ResponseCallbackEventSchema.parse(event)
} catch (e) {
throw Error(getUnexpectedErrorMessage(ErrorCode.RESPONSE_CALLBACK_EVENT))
}
try {
vpToken = VerifiablePresentationSchema.parse(
JSON.parse(responseCallback.vpToken),
)
} catch (e) {
throw Error(
getUnexpectedErrorMessage(ErrorCode.VERIFIABLE_PRESENTATION_SCHEMA),
)
}
const response: IotaResponse = {
correlationId: responseCallback.correlationId,
vpToken,
// TODO parse presentation submission, same as vpToken
presentationSubmission: responseCallback.presentationSubmission,
}
return response
}

async getResponse(correlationId: string): Promise<IotaResponse> {
const client = this.channelProvider.getClient()
return new Promise((resolve, reject) => {
Expand All @@ -36,27 +68,16 @@ export class ResponseHandler {
const raw_data = toUtf8(
messageReceivedEvent.message.payload as Buffer,
)

try {
const event = JSON.parse(raw_data)
if (
event.eventType === 'response-callback' &&
event.correlationId === correlationId
) {
// TODO handle Zod errors gracefully
const responseCallback =
ResponseCallbackEventSchema.parse(event)
const vpToken = VerifiablePresentationSchema.parse(
JSON.parse(responseCallback.vpToken),
)
const response: IotaResponse = {
correlationId: responseCallback.correlationId,
vpToken,
// TODO parse presentation submission, same as vpToken
presentationSubmission:
responseCallback.presentationSubmission,
}
if (correlationId !== event.correlationId) {
return
}
if (event.eventType === EventTypes.ResponseCallback) {
const response = this.getResponseHandler(event)
resolve(response)
} else if (event.eventType === EventTypes.Error) {
throwEventParsingError(event)
}
} catch (error) {
reject(error)
Expand Down
34 changes: 34 additions & 0 deletions libs/iota-browser/src/validators/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ErrorEvent, ErrorEventSchema } from '../validators/events'

export enum ErrorCode {
'SIGNED_REQUEST_EVENT' = 'SignedRequestEvent',
'SIGNED_REQUEST_JWT' = 'SignedRequestJWT',
'RESPONSE_CALLBACK_EVENT' = 'ResponseCallbackEvent',
'VERIFIABLE_PRESENTATION_SCHEMA' = 'VerifiablePresentationSchema',
'ERROR_EVENT' = 'ErrorEvent',
}

function getIssue(errorEvent: ErrorEvent) {
return errorEvent.error.details![0].issues &&
errorEvent.error.details![0].issues.length > 0
? errorEvent.error.details![0].issues
: errorEvent.error.message
}

export function getUnexpectedErrorMessage(code: ErrorCode) {
return `Unexpected error occured. Error Code: ${code} `
}

export function formatEventError(errorEvent: ErrorEvent): string {
return `Something went wrong. ${getIssue(errorEvent)}. Error Code ${errorEvent.error.httpStatusCode}`
}

export function throwEventParsingError(event: ErrorEvent): never {
let errorEvent: ErrorEvent
try {
errorEvent = ErrorEventSchema.parse(event)
} catch (e) {
throw Error(getUnexpectedErrorMessage(ErrorCode.ERROR_EVENT))
}
throw Error(formatEventError(errorEvent))
}
11 changes: 8 additions & 3 deletions libs/iota-browser/src/validators/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from 'zod'

const EventTypes = {
export const EventTypes = {
PrepareRequest: 'prepareRequest',
SignedRequest: 'signedRequest',
ResponseCallback: 'response-callback',
Expand Down Expand Up @@ -28,6 +28,11 @@ export const SignedRequestEventSchema = BaseEvent.extend({
})
export type SignedRequestEvent = z.infer<typeof SignedRequestEventSchema>

export const SignedRequestJWTSchema = z.object({
client_id: z.string(),
})
export type SignedRequestJWT = z.infer<typeof SignedRequestJWTSchema>

// TODO full VP schema
export const VerifiablePresentationSchema = z.object({
verifiableCredential: z.array(z.object({ credentialSubject: z.any() })),
Expand All @@ -45,12 +50,12 @@ export type ResponseCallbackEvent = z.infer<typeof ResponseCallbackEventSchema>

const ERROR_LOCATION = ['body', 'path', 'query'] as const
const ErrorDetailItem = z.object({
issue: z.string(),
issues: z.string(),
field: z.string().optional(),
value: z.string().optional(),
location: z.enum(ERROR_LOCATION).optional(),
})
const ErrorEventSchema = BaseEvent.extend({
export const ErrorEventSchema = BaseEvent.extend({
eventType: z.literal(EventTypes.Error),
error: z.object({
message: z.string(),
Expand Down
Loading