@ -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 < GenericVerifiableCredential > =
{
claim : { "@type" : "" } ,
handleId : "" ,
id : "" ,
issuedAt : "" ,
issuer : "" ,
} ;
export const BLANK_GENERIC_SERVER_RECORD : GenericCredWrapper < GenericVerifiableCredential > = {
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 < string , unknown > ) . 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 < GenericVerifiableCredential > ,
) = > {
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 giver Info + " 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 < string > {
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." } ;
}
}