@identity.com/dsr
Version:
The Dynamic Scope Request (DSR) javascript library provides capability around securely requesting credential information between an ID Requester and an ID Holder
1 lines • 7.25 kB
JavaScript
const _=require("lodash"),{isValidGlobalIdentifier,VCCompat:VC}=require("@identity.com/credential-commons"),{services,initServices}=require("./services"),config=services.container.Config,signer=services.container.Signer,SCHEMA_VERSION="1",VALID_OPERATORS=["$eq","$ne","$gt","$gte","$lt","$lte","$mod","$in","$nin","$not","$all","$or","$nor","$and","$regex","$where","$elemMatch","$exists"],VALID_AGGREGATORS=["$limit","$max","$min","$last","$first","$sort"],VALIDATION_MODE={ADVANCED:"ADVANCED",SIMPLE:"SIMPLE"},isLocal=a=>null!==a.match("(http://|https://)?(localhost|127.0.0.*)"),isValidEvidenceChannelDetails=a=>{let b=!0;return b=_.includes(["application/json","image/*","multipart-from"],a.accepts),b=b&&_.includes(["put","post"],a.method),b=b&&!_.isEmpty(a.url)&&(isLocal(a.url)||_.startsWith(a.url,"https")),b},isValidCredentialMeta=(a,b)=>{const c=VC.getCredentialMeta(a);return VC.isMatchCredentialMeta(c,b)};class ScopeRequest{static async credentialsMatchesRequest(a,b,c=!1){let d=!0;const e=_.get(b,"credentialItems");if(_.isEmpty(e))throw new Error("invalid scopeRequest object");if(_.isEmpty(a))throw new Error("empty credentialItems param");return await _.reduce(e,async(b,e)=>{await b;const f=_.find(a,{identifier:e.credential});if(!f)return d=!1,!1;const g=await VC.fromJSON(f,!!f.granted),h=_.get(e,"constraints"),i=g.isMatch(h),j=!c||isValidCredentialMeta(f,h);if(!i||!j)return d=!1,!1},Promise.resolve()),d}static validateConstraint(a){const b=_.keys(a);if(1!==b.length)throw new Error("Invalid Constraint Object - only one operator is allowed");if(!_.includes(VALID_OPERATORS,b[0]))throw new Error(`Invalid Constraint Object - ${b[0]} is not a valid operator`);if(_.isNil(a[b[0]]))throw new Error("Invalid Constraint Object - a constraint value is required");return!0}static validateAggregationFilter(a){const b=_.keys(a);if(1!==b.length)throw new Error("Invalid Constraint Object - only one operator is allowed");if(!_.includes(VALID_AGGREGATORS,b[0]))throw new Error(`Invalid Aggregate Object - ${b[0]} is not a valid filter`);if(_.isNil(a[b[0]]))throw new Error("Invalid Constraint Object - a constraint value is required");return!0}static async isValidCredentialItemIdentifier(a){return isValidGlobalIdentifier(a)}static isValidCredentialIssuer(a){return!!a}static async validateCredentialItems(a){return await _.reduce(a,async(a,b)=>{if(await a,_.isString(b)){const a=await ScopeRequest.isValidCredentialItemIdentifier(b);if(!a)throw new Error(`${b} is not valid CredentialItem identifier`)}else{if(_.isEmpty(b.identifier))throw new Error("CredentialItem identifier is required");const a=await ScopeRequest.isValidCredentialItemIdentifier(b.identifier);if(!a)throw new Error(`${b.identifier} is not valid CredentialItem identifier`);if(!_.isEmpty(b.constraints)){if(!_.isEmpty(b.constraints.meta)){if(!b.constraints.meta.issuer)throw new Error("The META issuer constraint is required");if(!ScopeRequest.isValidCredentialIssuer(b.constraints.meta.issuer.is.$eq))throw new Error(`${b.constraints.meta.issuer.is.$eq} is not a valid issuer`);if(b.constraints.meta.issued&&ScopeRequest.validateConstraint(b.constraints.meta.issued.is),b.constraints.meta.expiry&&ScopeRequest.validateConstraint(b.constraints.meta.expiry.is),b.identifier.startsWith("claim-")&&b.constraints.meta.noClaims)throw new Error("Cannot ask for Claims and also have the flag noClaimss equals true")}_.isEmpty(b.constraints.claims)||_.forEach(b.constraints.claims,a=>{if(_.isEmpty(a.path))throw new Error("Claim path is required");if(_.isEmpty(a.is))throw new Error("Claim constraint is required");ScopeRequest.validateConstraint(a.is)})}_.isEmpty(b.aggregate)||_.forEach(b.aggregate,a=>{ScopeRequest.validateAggregationFilter(a)})}},Promise.resolve()),!0}static validateChannelsConfig(a){if(!a.eventsURL)throw new Error("eventsURL is required");if(!isLocal(a.eventsURL)&&!_.startsWith(a.eventsURL,"https"))throw new Error("only HTTPS is supported for eventsURL");if(a.payloadURL&&!isLocal(a.payloadURL)&&!_.startsWith(a.payloadURL,"https"))throw new Error("only HTTPS is supported for payloadURL");if(a.evidences){if(a.evidences.idDocumentFront&&!isValidEvidenceChannelDetails(a.evidences.idDocumentFront))throw new Error("invalid idDocumentFront channel configuration");if(a.evidences.idDocumentBack&&!isValidEvidenceChannelDetails(a.evidences.idDocumentBack))throw new Error("invalid idDocumentBack channel configuration");if(a.evidences.selfie&&!isValidEvidenceChannelDetails(a.evidences.selfie))throw new Error("invalid selfie channel configuration")}return!0}static validateAppConfig(a){if(_.isEmpty(a.id))throw new Error("app.id is required");if(_.isEmpty(a.name))throw new Error("app.name is required");if(_.isEmpty(a.logo))throw new Error("app.logo is required");if(!_.startsWith(a.logo,"https"))throw new Error("only HTTPS is supported for app.logo");if(_.isEmpty(a.description))throw new Error("app.description is required");if(_.isEmpty(a.primaryColor))throw new Error("app.primaryColor is required");if(_.isEmpty(a.secondaryColor))throw new Error("app.secondaryColor is required");return!0}static validatePartnerConfig(a){if(_.isEmpty(a.id))throw new Error("partner.id is required");if(_.isEmpty(a.signingKeys)||_.isEmpty(a.signingKeys.xpub)||_.isEmpty(a.signingKeys.xprv))throw new Error("Partner public and private signing keys are required");return!0}static validateAuthentication(a){if(!_.isBoolean(a))throw new Error("Invalid value for authentication");return!0}static async create(a,b,c,d,e,f=!0,g="ADVANCED"){let h=[].concat(b);const i=await ScopeRequest.validateCredentialItems(h);return i&&(h=_.cloneDeep(h)),new ScopeRequest(a,b,h,c,d,e,f,g)}constructor(a,b,c,d,e,f,g=!0,h="ADVANCED"){if(this.version=SCHEMA_VERSION,!a)throw Error("uniqueId is required");if(this.id=a,this.requesterInfo={},ScopeRequest.validateAuthentication(g)&&(this.authentication=g),this.timestamp=new Date().toISOString(),this.credentialItems=c,d&&ScopeRequest.validateChannelsConfig(d))this.channels=d;else{const b={eventsURL:`${config.channels.baseEventsURL}/${a}`,payloadURL:`${config.channels.basePayloadURL}/${a}`};ScopeRequest.validateChannelsConfig(b)&&(this.channels=b)}if(e&&ScopeRequest.validateAppConfig(e)?this.requesterInfo.app=e:ScopeRequest.validateAppConfig(config.app)&&(this.requesterInfo.app=config.app),f&&ScopeRequest.validatePartnerConfig(f)){const a=Object.assign({},config);a.partner=f,initServices(a),this.requesterInfo.requesterId=f.id}else ScopeRequest.validatePartnerConfig(config.partner)&&(this.requesterInfo.requesterId=config.partner.id);this.mode=h}toJSON(){return _.omit(this,["partnerConfig"])}}function buildSignedRequestBody(a){const{xprv:b,xpub:c}=services.container.Config.partner.signingKeys,d=signer.sign(a,b);return{payload:a,signature:d.signature,algorithm:d.algorithm,xpub:c}}function verifySignedRequestBody(a,b){if(!a.payload)throw new Error("Request must have a payload object");if(!a.signature)throw new Error("Request must have a signature");if(!a.xpub)throw new Error("Request must have a public key");if(b&&b!==a.xpub)throw new Error("Request public key not match");return signer.verify(a.payload,a.signature,a.xpub)}module.exports={ScopeRequest,buildSignedRequestBody,verifySignedRequestBody,VALIDATION_MODE};