diff --git a/src/interfaces/common.ts b/src/interfaces/common.ts index e450ccb7..0b00eed6 100644 --- a/src/interfaces/common.ts +++ b/src/interfaces/common.ts @@ -1,6 +1,6 @@ // similar to VerifiableCredentialSubject... maybe rename this export interface GenericVerifiableCredential { - "@context"?: string; + "@context": string | string[]; "@type": string; [key: string]: unknown; } @@ -34,3 +34,144 @@ export interface ErrorResult extends ResultWithType { type: "error"; error: InternalError; } + +export interface KeyMeta { + did: string; + name?: string; + publicKeyHex: string; + mnemonic: string; + derivationPath: string; + registered?: boolean; + profileImageUrl?: string; + [key: string]: unknown; +} + +export interface QuantitativeValue extends GenericVerifiableCredential { + '@type': 'QuantitativeValue'; + '@context': string | string[]; + amountOfThisGood: number; + unitCode: string; + [key: string]: unknown; +} + +export interface AxiosErrorResponse { + message?: string; + response?: { + data?: { + error?: { + message?: string; + }; + [key: string]: unknown; + }; + status?: number; + config?: unknown; + }; + config?: unknown; + [key: string]: unknown; +} + +export interface UserInfo { + did: string; + name: string; + publicEncKey: string; + registered: boolean; + profileImageUrl?: string; + nextPublicEncKeyHash?: string; +} + +export interface CreateAndSubmitClaimResult { + success: boolean; + error?: string; + handleId?: string; +} + +export interface PlanSummaryRecord { + handleId: string; + issuer: string; + claim: GenericVerifiableCredential; + [key: string]: unknown; +} + +export interface Agent { + identifier?: string; + did?: string; + [key: string]: unknown; +} + +export interface ClaimObject { + '@type': string; + '@context'?: string | string[]; + fulfills?: Array<{ + '@type': string; + identifier?: string; + [key: string]: unknown; + }>; + object?: GenericVerifiableCredential; + agent?: Agent; + participant?: { + identifier?: string; + [key: string]: unknown; + }; + identifier?: string; + [key: string]: unknown; +} + +export interface VerifiableCredentialClaim { + '@context': string | string[]; + '@type': string; + type: string[]; + credentialSubject: ClaimObject; + [key: string]: unknown; +} + +export interface GiveVerifiableCredential extends GenericVerifiableCredential { + '@type': 'GiveAction'; + '@context': string | string[]; + object?: GenericVerifiableCredential; + agent?: Agent; + participant?: { + identifier?: string; + [key: string]: unknown; + }; + fulfills?: Array<{ + '@type': string; + identifier?: string; + [key: string]: unknown; + }>; + [key: string]: unknown; +} + +export interface OfferVerifiableCredential extends GenericVerifiableCredential { + '@type': 'OfferAction'; + '@context': string | string[]; + object?: GenericVerifiableCredential; + agent?: Agent; + participant?: { + identifier?: string; + [key: string]: unknown; + }; + itemOffered?: { + description?: string; + isPartOf?: { + '@type': string; + identifier: string; + [key: string]: unknown; + }; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export interface RegisterVerifiableCredential extends GenericVerifiableCredential { + '@type': 'RegisterAction'; + '@context': string | string[]; + agent: { + identifier: string; + }; + object: string; + participant?: { + identifier: string; + }; + identifier?: string; + [key: string]: unknown; +} diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 56a377eb..0a74c92a 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -40,15 +40,20 @@ import { import { createEndorserJwtForKey, KeyMeta } from "../libs/crypto/vc"; import { - GiveVerifiableCredential, - OfferVerifiableCredential, - RegisterVerifiableCredential, - GenericVerifiableCredential, GenericCredWrapper, - PlanSummaryRecord, + GenericVerifiableCredential, + AxiosErrorResponse, UserInfo, CreateAndSubmitClaimResult, -} from "../interfaces"; + PlanSummaryRecord, + GiveVerifiableCredential, + OfferVerifiableCredential, + RegisterVerifiableCredential, + ClaimObject, + VerifiableCredentialClaim, + Agent, + QuantitativeValue +} from "../interfaces/common"; import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; @@ -100,14 +105,16 @@ export const CONTACT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact/confirm/"; */ export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/"; -export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper = - { - claim: { "@type": "" }, - handleId: "", - id: "", - issuedAt: "", - issuer: "", - }; +export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper = { + claim: { + "@context": SCHEMA_ORG_CONTEXT, + "@type": "" + }, + handleId: "", + id: "", + issuedAt: "", + issuer: "", +}; // This is used to check for hidden info. // See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6 @@ -140,6 +147,14 @@ export function isEmptyOrHiddenDid(did?: string): boolean { return !did || did === HIDDEN_DID; } +// Add these interfaces at the top of the file +interface ErrorResponse { + error?: string; + message?: string; + status?: number; + [key: string]: unknown; +} + /** * Recursively tests strings within an object/array against a test function * @param {Function} func - Test function to apply to strings @@ -182,37 +197,21 @@ export function isEmptyOrHiddenDid(did?: string): boolean { * }; * testRecursivelyOnStrings(isHiddenDid, obj); // Returns: true */ -function testRecursivelyOnStrings( - func: (arg0: unknown) => boolean, +const testRecursivelyOnStrings = ( input: unknown, -): boolean { - // Test direct string values - if (Object.prototype.toString.call(input) === "[object String]") { - return func(input); - } - // Recursively test objects and arrays - else if (input instanceof Object) { - if (!Array.isArray(input)) { - // Handle plain objects - for (const key in input) { - if (testRecursivelyOnStrings(func, input[key])) { - return true; - } - } - } else { - // Handle arrays - for (const value of input) { - if (testRecursivelyOnStrings(func, value)) { - return true; - } - } - } - return false; - } else { - // Non-string, non-object values can't contain strings - return false; + test: (s: string) => boolean, +): boolean => { + if (typeof input === "string") { + return test(input); + } else if (Array.isArray(input)) { + return input.some((item) => testRecursivelyOnStrings(item, test)); + } else if (input && typeof input === "object") { + return Object.values(input as Record).some((value) => + testRecursivelyOnStrings(value, test) + ); } -} + return false; +}; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function containsHiddenDid(obj: any) { @@ -553,7 +552,11 @@ export async function setPlanInCache( * @returns {string|undefined} User-friendly message or undefined if none found */ export function serverMessageForUser(error: unknown): string | undefined { - return error?.response?.data?.error?.message; + if (error && typeof error === 'object' && 'response' in error) { + const err = error as AxiosErrorResponse; + return err.response?.data?.error?.message; + } + return undefined; } /** @@ -575,18 +578,22 @@ export function errorStringForLog(error: unknown) { // --- property '_value' closes the circle } let fullError = "" + error + " - JSON: " + stringifiedError; - const errorResponseText = JSON.stringify(error.response); - // for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions) - if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) { - // add error.response stuff - if (R.equals(error?.config, error?.response?.config)) { - // but exclude "config" because it's already in there - const newErrorResponseText = JSON.stringify( - R.omit(["config"] as never[], error.response), - ); - fullError += " - .response w/o same config JSON: " + newErrorResponseText; - } else { - fullError += " - .response JSON: " + errorResponseText; + + if (error && typeof error === 'object' && 'response' in error) { + const err = error as AxiosErrorResponse; + const errorResponseText = JSON.stringify(err.response); + // for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions) + if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) { + // add error.response stuff + if (err.response?.config && err.config && R.equals(err.config, err.response.config)) { + // but exclude "config" because it's already in there + const newErrorResponseText = JSON.stringify( + R.omit(["config"] as never[], err.response), + ); + fullError += " - .response w/o same config JSON: " + newErrorResponseText; + } else { + fullError += " - .response JSON: " + errorResponseText; + } } } return fullError; @@ -652,70 +659,89 @@ export function hydrateGive( unitCode?: string, fulfillsProjectHandleId?: string, fulfillsOfferHandleId?: string, - isTrade: boolean = false, // remove, because this app is all for gifting + isTrade: boolean = false, imageUrl?: string, providerPlanHandleId?: string, lastClaimId?: string, ): GiveVerifiableCredential { - // Remember: replace values or erase if it's null - const vcClaim: GiveVerifiableCredential = vcClaimOrig ? R.clone(vcClaimOrig) : { "@context": SCHEMA_ORG_CONTEXT, "@type": "GiveAction", + object: undefined, + agent: undefined, + fulfills: [] }; if (lastClaimId) { - // this is an edit vcClaim.lastClaimId = lastClaimId; delete vcClaim.identifier; } - vcClaim.agent = fromDid ? { identifier: fromDid } : undefined; - vcClaim.recipient = toDid ? { identifier: toDid } : undefined; + if (fromDid) { + vcClaim.agent = { identifier: fromDid }; + } + if (toDid) { + vcClaim.recipient = { identifier: toDid }; + } vcClaim.description = description || undefined; - vcClaim.object = - amount && !isNaN(amount) - ? { amountOfThisGood: amount, unitCode: unitCode || "HUR" } - : undefined; + + if (amount && !isNaN(amount)) { + const quantitativeValue: QuantitativeValue = { + "@context": SCHEMA_ORG_CONTEXT, + "@type": "QuantitativeValue", + amountOfThisGood: amount, + unitCode: unitCode || "HUR" + }; + vcClaim.object = quantitativeValue; + } - // ensure fulfills is an array + // Initialize fulfills array if not present if (!Array.isArray(vcClaim.fulfills)) { - vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : []; + vcClaim.fulfills = []; } - // ... and replace or add each element, ending with Trade or Donate - // I realize the following doesn't change any elements that are not PlanAction or Offer or Trade/Action. + + // Filter and add fulfills elements vcClaim.fulfills = vcClaim.fulfills.filter( - (elem) => elem["@type"] !== "PlanAction", + (elem: { '@type': string }) => elem["@type"] !== "PlanAction" ); + if (fulfillsProjectHandleId) { vcClaim.fulfills.push({ "@type": "PlanAction", - identifier: fulfillsProjectHandleId, + identifier: fulfillsProjectHandleId }); } + vcClaim.fulfills = vcClaim.fulfills.filter( - (elem) => elem["@type"] !== "Offer", + (elem: { '@type': string }) => elem["@type"] !== "Offer" ); + if (fulfillsOfferHandleId) { vcClaim.fulfills.push({ "@type": "Offer", - identifier: fulfillsOfferHandleId, + identifier: fulfillsOfferHandleId }); } - // do Trade/Donate last because current endorser.ch only looks at the first for plans & offers + vcClaim.fulfills = vcClaim.fulfills.filter( - (elem) => - elem["@type"] !== "DonateAction" && elem["@type"] !== "TradeAction", + (elem: { '@type': string }) => + elem["@type"] !== "DonateAction" && elem["@type"] !== "TradeAction" ); - vcClaim.fulfills.push({ "@type": isTrade ? "TradeAction" : "DonateAction" }); + + vcClaim.fulfills.push({ + "@type": isTrade ? "TradeAction" : "DonateAction" + }); vcClaim.image = imageUrl || undefined; - vcClaim.provider = providerPlanHandleId - ? { "@type": "PlanAction", identifier: providerPlanHandleId } - : undefined; + if (providerPlanHandleId) { + vcClaim.provider = { + "@type": "PlanAction", + identifier: providerPlanHandleId + }; + } return vcClaim; } @@ -828,29 +854,38 @@ export function hydrateOffer( validThrough?: string, lastClaimId?: string, ): OfferVerifiableCredential { - // Remember: replace values or erase if it's null - const vcClaim: OfferVerifiableCredential = vcClaimOrig ? R.clone(vcClaimOrig) : { "@context": SCHEMA_ORG_CONTEXT, - "@type": "Offer", + "@type": "OfferAction", + object: undefined, + agent: undefined, + itemOffered: {} }; if (lastClaimId) { - // this is an edit vcClaim.lastClaimId = lastClaimId; delete vcClaim.identifier; } - vcClaim.offeredBy = fromDid ? { identifier: fromDid } : undefined; - vcClaim.recipient = toDid ? { identifier: toDid } : undefined; + if (fromDid) { + vcClaim.agent = { identifier: fromDid }; + } + if (toDid) { + vcClaim.recipient = { identifier: toDid }; + } vcClaim.description = conditionDescription || undefined; - vcClaim.includesObject = - amount && !isNaN(amount) - ? { amountOfThisGood: amount, unitCode: unitCode || "HUR" } - : undefined; + if (amount && !isNaN(amount)) { + const quantitativeValue: QuantitativeValue = { + "@context": SCHEMA_ORG_CONTEXT, + "@type": "QuantitativeValue", + amountOfThisGood: amount, + unitCode: unitCode || "HUR" + }; + vcClaim.object = quantitativeValue; + } if (itemDescription || fulfillsProjectHandleId) { vcClaim.itemOffered = vcClaim.itemOffered || {}; @@ -858,10 +893,11 @@ export function hydrateOffer( if (fulfillsProjectHandleId) { vcClaim.itemOffered.isPartOf = { "@type": "PlanAction", - identifier: fulfillsProjectHandleId, + identifier: fulfillsProjectHandleId }; } } + vcClaim.validThrough = validThrough || undefined; return vcClaim; @@ -990,20 +1026,17 @@ export async function createAndSubmitClaim( }, }); - return { type: "success", response }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { + return { success: true, handleId: response.data?.handleId }; + } catch (error: unknown) { logger.error("Error submitting claim:", error); const errorMessage: string = serverMessageForUser(error) || - error.message || + (error && typeof error === 'object' && 'message' in error ? String(error.message) : undefined) || "Got some error submitting the claim. Check your permissions, network, and error logs."; return { - type: "error", - error: { - error: errorMessage, - }, + success: false, + error: errorMessage }; } } @@ -1033,10 +1066,10 @@ export async function generateEndorserJwtUrlForAccount( } // Add the next key -- not recommended for the QR code for such a high resolution - if (isContact && account?.mnemonic && account?.derivationPath) { - const newDerivPath = nextDerivationPath(account.derivationPath as string); + if (isContact) { + const newDerivPath = nextDerivationPath(account.derivationPath); const nextPublicHex = deriveAddress( - account.mnemonic as string, + account.mnemonic, newDerivPath, )[2]; const nextPublicEncKey = Buffer.from(nextPublicHex, "hex"); @@ -1110,13 +1143,12 @@ const claimSummary = ( claim: GenericVerifiableCredential | GenericCredWrapper, ) => { if (!claim) { - // to differentiate from "something" above return "something"; } let specificClaim: GenericVerifiableCredential; if ('claim' in claim) { // It's a GenericCredWrapper - specificClaim = claim.claim; + specificClaim = claim.claim as GenericVerifiableCredential; } else { // It's already a GenericVerifiableCredential specificClaim = claim; @@ -1156,92 +1188,77 @@ export const claimSpecialDescription = ( ) => { let claim = record.claim; if ('claim' in claim) { - // it's a nested GenericCredWrapper - claim = claim.claim; + claim = claim.claim as GenericVerifiableCredential; } const issuer = didInfo(record.issuer, activeDid, identifiers, contacts); - const type = claim["@type"] || "UnknownType"; + const claimObj = claim as ClaimObject; + const type = claimObj["@type"] || "UnknownType"; if (type === "AgreeAction") { - return issuer + " agreed with " + claimSummary(claim.object as GenericVerifiableCredential); + return issuer + " agreed with " + claimSummary(claimObj.object as GenericVerifiableCredential); } else if (isAccept(claim)) { - return issuer + " accepted " + claimSummary(claim.object as GenericVerifiableCredential); + return issuer + " accepted " + claimSummary(claimObj.object as GenericVerifiableCredential); } else if (type === "GiveAction") { - // agent.did is for legacy data, before March 2023 - const giver = claim.agent?.identifier || claim.agent?.did; - const giverInfo = didInfo(giver, activeDid, identifiers, contacts); - let gaveAmount = claim.object?.amountOfThisGood - ? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) + const giveClaim = claim as GiveVerifiableCredential; + const agent: Agent = giveClaim.agent || { identifier: undefined, did: undefined }; + const agentDid = agent.did || agent.identifier; + const contactInfo = agentDid + ? didInfo(agentDid, activeDid, identifiers, contacts) + : "someone"; + const offering = giveClaim.object + ? " " + claimSummary(giveClaim.object) : ""; - if (claim.description) { - if (gaveAmount) { - gaveAmount = gaveAmount + ", and also: "; - } - gaveAmount = gaveAmount + claim.description; - } - if (!gaveAmount) { - gaveAmount = "something not described"; - } - // recipient.did is for legacy data, before March 2023 - const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did; - const gaveRecipientInfo = gaveRecipientId - ? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts) + const recipient = giveClaim.participant?.identifier; + const recipientInfo = recipient + ? " to " + didInfo(recipient, activeDid, identifiers, contacts) : ""; - return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount; + return contactInfo + " gave" + offering + recipientInfo; } else if (type === "JoinAction") { - // agent.did is for legacy data, before March 2023 - const agent = claim.agent?.identifier || claim.agent?.did; - const contactInfo = didInfo(agent, activeDid, identifiers, contacts); - - let eventOrganizer = - claim.event && claim.event.organizer && claim.event.organizer.name; - eventOrganizer = eventOrganizer || ""; - let eventName = claim.event && claim.event.name; - eventName = eventName ? " " + eventName : ""; - let fullEvent = eventOrganizer + eventName; - fullEvent = fullEvent ? " attended the " + fullEvent : ""; - - let eventDate = claim.event && claim.event.startTime; - eventDate = eventDate ? " at " + eventDate : ""; - return contactInfo + fullEvent + eventDate; + const joinClaim = claim as ClaimObject; + const agent: Agent = joinClaim.agent || { identifier: undefined, did: undefined }; + const agentDid = agent.did || agent.identifier; + const contactInfo = agentDid + ? didInfo(agentDid, activeDid, identifiers, contacts) + : "someone"; + const object = joinClaim.object as GenericVerifiableCredential; + const objectInfo = object ? " " + claimSummary(object) : ""; + return contactInfo + " joined" + objectInfo; } else if (isOffer(claim)) { - const offerer = claim.offeredBy?.identifier; - const contactInfo = didInfo(offerer, activeDid, identifiers, contacts); - let offering = ""; - if (claim.includesObject) { - offering += - " " + - displayAmount( - claim.includesObject.unitCode, - claim.includesObject.amountOfThisGood, - ); - } - if (claim.itemOffered?.description) { - offering += ", saying: " + claim.itemOffered?.description; - } - // recipient.did is for legacy data, before March 2023 - const offerRecipientId = - claim.recipient?.identifier || claim.recipient?.did; + const offerClaim = claim as OfferVerifiableCredential; + const agent: Agent = offerClaim.agent || { identifier: undefined, did: undefined }; + const agentDid = agent.did || agent.identifier; + const contactInfo = agentDid + ? didInfo(agentDid, activeDid, identifiers, contacts) + : "someone"; + const offering = offerClaim.object + ? " " + claimSummary(offerClaim.object) + : ""; + const offerRecipientId = offerClaim.participant?.identifier; const offerRecipientInfo = offerRecipientId ? " to " + didInfo(offerRecipientId, activeDid, identifiers, contacts) : ""; return contactInfo + " offered" + offering + offerRecipientInfo; } else if (type === "PlanAction") { - const claimer = claim.agent?.identifier || record.issuer; - const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts); - return claimerInfo + " announced a project: " + claim.name; + const planClaim = claim as ClaimObject; + const agent: Agent = planClaim.agent || { identifier: undefined, did: undefined }; + const agentDid = agent.did || agent.identifier; + const contactInfo = agentDid + ? didInfo(agentDid, activeDid, identifiers, contacts) + : "someone"; + const object = planClaim.object as GenericVerifiableCredential; + const objectInfo = object ? " " + claimSummary(object) : ""; + return contactInfo + " planned" + objectInfo; } else if (type === "Tenure") { - // party.did is for legacy data, before March 2023 - const claimer = claim.party?.identifier || claim.party?.did; - const contactInfo = didInfo(claimer, activeDid, identifiers, contacts); - const polygon = claim.spatialUnit?.geo?.polygon || ""; - return ( - contactInfo + - " possesses [" + - polygon.substring(0, polygon.indexOf(" ")) + - "...]" - ); + const tenureClaim = claim as ClaimObject; + const agent: Agent = tenureClaim.agent || { identifier: undefined, did: undefined }; + const agentDid = agent.did || agent.identifier; + const contactInfo = agentDid + ? didInfo(agentDid, activeDid, identifiers, contacts) + : "someone"; + const object = tenureClaim.object as GenericVerifiableCredential; + const objectInfo = object ? " " + claimSummary(object) : ""; + return contactInfo + " has tenure" + objectInfo; } else { return ( issuer + @@ -1289,9 +1306,7 @@ export async function createEndorserJwtVcFromClaim( export async function createInviteJwt( activeDid: string, - contact?: Contact, - inviteId?: string, - expiresIn?: number, + contact: Contact, ): Promise { const vcClaim: RegisterVerifiableCredential = { "@context": SCHEMA_ORG_CONTEXT, @@ -1302,19 +1317,19 @@ export async function createInviteJwt( if (contact) { vcClaim.participant = { identifier: contact.did }; } - if (inviteId) { - vcClaim.identifier = inviteId; - } + // Make a payload for the claim - const vcPayload = { + const vcPayload: { vc: VerifiableCredentialClaim } = { vc: { "@context": ["https://www.w3.org/2018/credentials/v1"], + "@type": "VerifiableCredential", type: ["VerifiableCredential"], - credentialSubject: vcClaim, + credentialSubject: vcClaim as unknown as ClaimObject, // Type assertion needed due to object being string }, }; + // Create a signature using private key of identity - const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn); + const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload); return vcJwt; } @@ -1324,21 +1339,40 @@ export async function register( axios: Axios, contact: Contact, ): Promise<{ success?: boolean; error?: string }> { - const vcJwt = await createInviteJwt(activeDid, contact); - - const url = apiServer + "/api/v2/claim"; - const resp = await axios.post(url, { jwtEncoded: vcJwt }); - if (resp.data?.success?.handleId) { - return { success: true }; - } else if (resp.data?.success?.embeddedRecordError) { - let message = - "There was some problem with the registration and so it may not be complete."; - if (typeof resp.data.success.embeddedRecordError == "string") { - message += " " + resp.data.success.embeddedRecordError; + try { + const vcJwt = await createInviteJwt(activeDid, contact); + const url = apiServer + "/api/v2/claim"; + const resp = await axios.post<{ + success?: { + handleId?: string; + embeddedRecordError?: string; + }; + error?: string; + message?: string; + }>(url, { jwtEncoded: vcJwt }); + + if (resp.data?.success?.handleId) { + return { success: true }; + } else if (resp.data?.success?.embeddedRecordError) { + let message = "There was some problem with the registration and so it may not be complete."; + if (typeof resp.data.success.embeddedRecordError === "string") { + message += " " + resp.data.success.embeddedRecordError; + } + return { error: message }; + } else { + logger.error("Registration error:", JSON.stringify(resp.data)); + return { error: "Got a server error when registering." }; + } + } catch (error: unknown) { + if (error && typeof error === 'object') { + const err = error as AxiosErrorResponse; + const errorMessage = err.message || + (err.response?.data && typeof err.response.data === 'object' && 'message' in err.response.data + ? (err.response.data as { message: string }).message + : undefined); + logger.error("Registration error:", errorMessage || JSON.stringify(err)); + return { error: errorMessage || "Got a server error when registering." }; } - return { error: message }; - } else { - logger.error("Registration error:", JSON.stringify(resp.data)); return { error: "Got a server error when registering." }; } }