@investorid/identity-sdk
Version:
Interact with BlockChain Identities.
459 lines • 20.4 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = __importDefault(require("axios"));
const utils_1 = require("ethers/utils");
const Config_1 = require("../core/Config");
const Claim_interface_1 = require("./Claim.interface");
const SignerModule_1 = require("../core/SignerModule");
const ethers_1 = require("ethers");
const Utils_1 = require("../core/utils/Utils");
class Claim {
/**
* Standardize a Claim Issuer provider response for a claim.
* Even if the standard is to return camelCase properties, some may return snake_case.
* @param data
*/
static standardizeProviderResponse(data) {
if (!data) {
return {};
}
if (data._id) {
data.id = data._id;
}
if (data.issuance_date) {
data.issuanceDate = data.issuance_date;
}
if (data.issuanceDate) {
data.issuanceDate = new Date(data.issuanceDate);
}
if (data.emission_date) {
data.emissionDate = data.emission_date;
}
if (data.emissionDate) {
data.emissionDate = new Date(data.emissionDate);
}
if (data.public_data) {
data.publicData = data.public_data;
}
if (!data.publicData) {
data.publicData = {};
}
if (data.private_data) {
data.privateData = data.private_data;
}
if (!data.privateData) {
data.privateData = {};
}
data.type = Claim_interface_1.ClaimType[data.type.toUpperCase()] || data.type;
return data;
}
/**
* Generate the hash of a claim using the provided data.
* @param type
* @param emissionDate
* @param publicData
* @param privateData
*/
static generateHash(type, emissionDate, publicData, privateData) {
if (publicData && privateData && emissionDate && type) {
return utils_1.sha256(utils_1.toUtf8Bytes(JSON.stringify({ type, emissionDate, privateData, publicData })));
}
else {
throw new Error("Can't generate the Claim Hash because some data is missing.");
}
}
static generateBlockchainHash(address, topic, data) {
return utils_1.solidityKeccak256(['address', 'uint256', 'bytes'], [address, topic, data]);
}
/**
* Instantiate a new claim from an URI.
* @param uri
*/
static createFromURI(uri) {
return __awaiter(this, void 0, void 0, function* () {
Utils_1.guardSecuredURI(uri);
const claimData = yield axios_1.default({
method: 'GET',
baseURL: uri,
}).then(res => Claim.standardizeProviderResponse(res.data)).catch(err => {
if (err.response && err.response.status === 404) {
return null;
}
throw new Error('Could not retrieve claim from Provider.');
});
return new Claim(Object.assign({}, (claimData || {}), { uri }));
});
}
/**
* Generate the blockchain hash of a claim and signs it with the provided signer or the default signer of the SDK.
* @param type
* @param address
* @param data
* @param [signer]
* @returns Claim signature.
*/
static sign(type, address, data, signer) {
return __awaiter(this, void 0, void 0, function* () {
let _signer = signer;
if (!_signer) {
const defaultSigner = Config_1.getProvider();
if (!ethers_1.Signer.isSigner(defaultSigner)) {
throw new Config_1.InvalidProviderError('A signer is required to generate a claim signature.');
}
_signer = new SignerModule_1.SignerModule(defaultSigner);
}
if (!SignerModule_1.SignerModule.isSignerModule(_signer)) {
throw new Config_1.InvalidProviderError('A signer is required to generate a claim signature.');
}
if (!Utils_1.isHexString(data)) {
throw new Error('Claim data to sign must be a valid hex string.');
}
const claimHash = utils_1.solidityKeccak256(['address', 'uint256', 'bytes'], [address, type, data]);
return _signer.signMessage(utils_1.arrayify(claimHash));
});
}
/**
* Verify the signature of a claim.
* The standard signature of a claim is the keccak256 hash of (identityAddress, type, data) prefixed and signed.
* The data argument is exactly the content of the data claim field stored in blockchain (caution to hex padding).
* Data is expected to be an hexString.
* Using the IdentitySDK, call `IdentitySDK.utils.toHex('data')`.
* Using web3utils, call `web3utils.asciiToHex('data)`.
* Using ethersjs, call `Ethers.utils.hexlify(Ethers.utils.toUtf8Bytes('data'))`
* @param signingKey
* @param type
* @param address Address of identity contract.
* @param data
* @param signature
*/
static verifySignature(signingKey, type, address, data, signature) {
return __awaiter(this, void 0, void 0, function* () {
if (!Utils_1.isHexString(data)) {
throw new Error('Claim data to sign must be a valid hex string.');
}
const hash = utils_1.solidityKeccak256(['address', 'uint256', 'bytes'], [address, type, data]);
try {
const signerAddress = yield utils_1.verifyMessage(utils_1.arrayify(hash), signature);
return signerAddress === signingKey;
}
catch (err) {
return false;
}
});
}
/**
* Create a new Claim Object from a ClaimData (got from BlockChain Identity Contract).
* Use #.createFromURI() to fetch from an URI.
* @param claim
*/
constructor(claim) {
if (claim) {
Object.assign(this, claim);
}
}
/**
* Complete an AccessGrant challenge to validate a PERSISTENT grant or obtain an access token with an IMMEDIATE grant.
* @param accessGrant
* @param [signer] Signer to sign the challenge if signature is not provided in accessGrant.
*/
completeAccessChallenge(accessGrant, signer) {
return __awaiter(this, void 0, void 0, function* () {
if (accessGrant.signature && signer) {
throw new Error('A signer is not expected when the accessGrant signature is provided.');
}
else if (!accessGrant.signature) {
if (!signer) {
throw new Error('A signer is expected or the accessGrant signature must be provided.');
}
if (!signer || !signer.signMessage || !signer.getPublicKey) {
throw new Error('Provided signer has no .signMessage(message: string) or .getPublicKey() function.');
}
try {
accessGrant.signature = yield signer.signMessage(accessGrant.challenge);
}
catch (err) {
throw new Error('Provided signer .signMessage(message: string) function threw an error when called.');
}
if (!accessGrant.signature) {
throw new Error('Provided signer does not expose a .signMessage(message: string) function which returns signature: string.');
}
}
const result = yield axios_1.default({
method: 'POST',
baseURL: accessGrant.uri,
url: '/validations',
data: {
signature: accessGrant.signature,
},
}).then(res => res.data);
if (!result.access) {
throw new Error('Could not validate the access grant request.');
}
if (accessGrant.type === Claim_interface_1.AccessGrantType.PERSISTENT) {
return Object.assign({}, accessGrant, { status: Claim_interface_1.AccessGrantStatus.CONFIRMED });
}
else if (accessGrant.type === Claim_interface_1.AccessGrantType.IMMEDIATE) {
if (!result.access_token) {
throw new Error('Provided response is not standard. Expecting: { access: true, access_token: string }.');
}
return {
access: result.access,
accessToken: result.access_token,
};
}
else {
throw new Error('Unknown accessGrant Type.');
}
});
}
/**
* Retrieve public data of the claim.
* Will fetch a GET on the Claim URI, with ?private_data=true&access_token=<...>.
* Use the .requestAccessToken() method to get an Access Token, and .requestAccess() to request access to the claim data.
* @param accessToken
*/
getPrivateData(accessToken) {
return __awaiter(this, void 0, void 0, function* () {
Utils_1.guardSecuredURI(this.uri);
if (this.privateData) {
return this.privateData;
}
const result = yield axios_1.default({
method: 'GET',
baseURL: this.uri,
params: {
private_data: true,
access_token: accessToken,
},
}).then(res => Claim.standardizeProviderResponse(res.data)).catch((err) => {
if (err.response && err.response.status === 404) {
return {};
}
throw new Error('Could not retrieve claim from Provider.');
});
if (result.privateData) {
const hash = this.generateHash(result.type, result.emissionDate, result.publicData || {}, result.privateData || {});
if (hash !== this.data) {
throw new Error('Claim data retrieved did not match the hash in the block chain claim data field.');
}
}
this.issuanceDate = result.issuanceDate;
this.emissionDate = result.emissionDate;
this.status = result.status;
this.privateData = result.privateData;
this.publicData = result.publicData;
return this.privateData || {};
});
}
/**
* Retrieve public data of the claim.
* Will fetch a GET on the Claim URI.
*/
getPublicData() {
return __awaiter(this, void 0, void 0, function* () {
Utils_1.guardSecuredURI(this.uri);
if (this.publicData) {
return this.publicData;
}
const result = yield axios_1.default({
method: 'GET',
baseURL: this.uri,
}).then(res => Claim.standardizeProviderResponse(res.data)).catch(err => {
if (err.response && err.response.status === 404) {
return null;
}
throw new Error('Could not retrieve claim from Provider.');
});
if (result) {
this.issuanceDate = result.issuanceDate ? new Date(result.issuanceDate) : undefined;
this.emissionDate = result.emissionDate ? new Date(result.emissionDate) : undefined;
this.status = result.status;
this.publicData = result.publicData;
}
if (!this.publicData) {
this.publicData = {};
}
return this.publicData;
});
}
/**
* Generate the hash of the claim data if it was populated with private data.
* If all data are not provided as arguments, data fromm the Claim object will be used.
* Note that to verify a claim complete data, you will obviously need to have access to its private data.
* Use the .populate() method to fetch all the public and private data if available.
* @param type
* @param emissionDate
* @param publicData
* @param privateData
*/
generateHash(type, emissionDate, publicData, privateData) {
if (publicData && privateData && emissionDate && type) {
return Claim.generateHash(type, emissionDate, publicData, privateData);
}
else {
if (!this.publicData || !this.privateData || !this.emissionDate) {
throw new Error("Can't generate the Claim Hash because some data is missing. Call .populate() first to retrieve data from the Claim Issuer.");
}
return this.generateHash(this.type, this.emissionDate, this.publicData, this.privateData);
}
}
/**
* Request access to a Claim private data.
* If the signer already possess a persistent Access Grant, this should return the existing access grant to request access token with.
* To require a PERSISTENT access grant, the signer is required. It must expose the getPublicKey() function.
* @param accessType
* @param signer Required if access grant type is PERSISTENT.
*/
requestAccess(accessType = Claim_interface_1.AccessGrantType.IMMEDIATE, signer) {
return __awaiter(this, void 0, void 0, function* () {
Utils_1.guardSecuredURI(this.uri);
let payload = {};
if (accessType === Claim_interface_1.AccessGrantType.PERSISTENT) {
if (!signer || !signer.getPublicKey) {
throw new Error('Provided signer does not expose a .getPublicKey() function which returns { key: string: type: string, signingMethod: string }.');
}
let publicKey;
try {
publicKey = yield signer.getPublicKey();
}
catch (err) {
throw new Error('Provided signer .getPublicKey() function threw an error when called.');
}
if (!publicKey) {
throw new Error('Provided signer does not expose a .getPublicKey() function which returns { key: string: type: string, signingMethod: string }.');
}
payload.key = publicKey.key;
payload.key_type = publicKey.type;
payload.key_signing_method = publicKey.signingMethod;
}
else if (accessType === Claim_interface_1.AccessGrantType.IMMEDIATE) {
}
else {
throw new Error('Invalid AccessGrantType requested.');
}
const result = yield axios_1.default({
method: 'POST',
baseURL: this.uri,
url: '/access-requests',
}).then(res => res.data);
if (!result.address || !result.challenge || !result.claim || !result.date || !result.status || !result.type || !result.uri || (!result.id && !result._id)) {
throw new Error('Provider response for AccessGrant is not a standard response.');
}
return {
address: result.address,
challenge: result.challenge,
claimId: result.claim,
date: new Date(result.date),
status: Claim_interface_1.AccessGrantStatus[result.status] || result.status,
type: Claim_interface_1.AccessGrantType[result.type] || result.type,
uri: result.uri,
id: result.id || result._id,
key: result.key,
keyType: result.key_type,
keySigningMethod: result.key_signing_method,
};
});
}
/**
* Use a confirmed AccessGrant to obtain an AccessToken.
* @param accessGrant
* @param signer
*/
requestAccessToken(accessGrant, signer) {
return __awaiter(this, void 0, void 0, function* () {
if (accessGrant.type !== Claim_interface_1.AccessGrantType.PERSISTENT) {
throw new Error('accessGrant type is not PERSISTENT.');
}
if (accessGrant.status !== Claim_interface_1.AccessGrantStatus.CONFIRMED) {
throw new Error("accessGrant status is not CONFIRMED. It can't be used to obtain an AccessToken.");
}
if (!signer || !signer.signMessage || !signer.getPublicKey) {
throw new Error('Provided signer has no .signMessage(message: string) and .getPublicKey() function.');
}
let publicKey;
try {
publicKey = yield signer.getPublicKey();
}
catch (err) {
throw new Error('Provided signer .getPublicKey() function threw an error when called.');
}
if (publicKey.key !== accessGrant.key || publicKey.type !== accessGrant.keyType || publicKey.signingMethod !== accessGrant.keySigningMethod) {
throw new Error('Provided signed PublicKey is not the one related to this AccessGrant.');
}
let signature;
try {
signature = yield signer.signMessage(accessGrant.challenge);
}
catch (err) {
throw new Error('Provided signer .signMessage(message: string) function threw an error when called.');
}
if (!signature) {
throw new Error('Provided signer does not expose a .signMessage(message: string) function which returns signature: string.');
}
const result = yield axios_1.default({
method: 'POST',
baseURL: accessGrant.uri,
url: '/validations',
data: {
signature,
},
}).then(res => res.data);
if (!result.access || !result.access_token) {
throw new Error('Could not request the token.');
}
return {
access: result.access,
accessToken: result.access_token,
};
});
}
/**
* Populate Claim Object with public and eventually private data.
* Getting the private data requires an accessToken.
* @param accessToken
*/
populate(accessToken) {
return __awaiter(this, void 0, void 0, function* () {
if (accessToken) {
yield this.getPrivateData(accessToken);
}
else {
yield this.getPublicData();
}
});
}
/**
* Sign the claim.
* It will update the signature property of the claim. The signature can only be generated for a claim that as a type, an address and data.
* @param signer Signer module to use. If null, use default signer of SDK.
*/
sign(signer) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.type) {
throw new Error('Claim has no type defined, thus it cannot be signed.');
}
if (!this.address) {
throw new Error('Claim has no address defined, thus it cannot be signed.');
}
if (!this.data) {
throw new Error('Claim has no data defined, thus it cannot be signed.');
}
const signature = yield Claim.sign(this.type, this.address, this.data, signer);
this.signature = signature;
return signature;
});
}
}
exports.Claim = Claim;
//# sourceMappingURL=Claim.js.map