/**
* Methods for creating and managing NTP instances and tickets
*
* @module ElvClient/NTP
*/
const UrlJoin = require("url-join");
const Ethers = require("ethers");
const {
ValidateAddress,
ValidateDate,
ValidateObject,
ValidatePresence
} = require("../Validation");
const FormatNTPInfo = info => {
const params = info.pm || {};
let response = {
ntpId: info.id,
ntpClass: `Class ${info.de}`,
tenantId: info.ti,
kmsId: info.ki,
objectId: params.qid,
updatedAt: parseInt(info.ts),
startTime: parseInt(params.vat),
endTime: parseInt(params.exp),
ticketLength: params.sen,
maxRedemptions: params.ntp,
maxTickets: info.mx
};
if(typeof info.cnt !== "undefined") {
response.issuedTickets = info.cnt;
}
return response;
};
/**
* Issue an n-time-password (NTP) instance. This instance contains a specification for the tickets (AKA codes) to be issued, including
* the target(s) to be authorized, how many tickets can be issued, and when and how many times tickets can be redeemed.
*
* Note: For date types (startTime/endTime), you may specify the date in any format parsable by JavaScript's `new Date()` constructor,
* including Unix epoch timestamps and ISO strings
*
* @see <a href="#IssueNTPCode">IssueNTPCode</a>
*
* @methodGroup NTP Instances
* @namedParams
* @param {string} tenantId - The ID of the tenant in which to create the NTP instance
* @param {string} objectId - ID of the object for the tickets to be authorized to
* @param {Array<string>=} groupAddresses - List of group addresses for the tickets to inherit permissions from
* @param {number=} ntpClass=4 - Class of NTP instance to create
* @param {number=} maxTickets=0 - The maximum number of tickets that may be issued for this instance (if 0, no limit)
* @param {number=} maxRedemptions=100 - The maximum number of times each ticket may be redeemed
* @param {string|number=} startTime - The time when issued tickets can be redeemed
* @param {string|number=} endTime - The time when issued tickets can no longer be redeemed
* @param {number=} ticketLength=6 - The number of characters in each ticket code
*
* @return {Promise<string>} - The ID of the NTP instance. This ID can be used when issuing tickets (See IssueNTPCode)
*/
exports.CreateNTPInstance = async function({
tenantId,
objectId,
groupAddresses,
ntpClass=4,
maxTickets=0,
maxRedemptions=100,
startTime,
endTime,
ticketLength=6
}) {
ValidatePresence("tenantId", tenantId);
ValidatePresence("objectId or groupAddresses", objectId || groupAddresses);
if(objectId) { ValidateObject(objectId); }
if(groupAddresses) { groupAddresses.forEach(address => ValidateAddress(address)); }
startTime = ValidateDate(startTime);
endTime = ValidateDate(endTime);
let paramsJSON = [`ntp:${parseInt(maxRedemptions)}`, `sen:${parseInt(ticketLength)}`];
if(objectId) {
paramsJSON.push(`qid:${objectId}`);
} else if(groupAddresses) {
const groupIds = groupAddresses.map(address => `igrp${this.utils.AddressToHash(address)}`);
paramsJSON.push(`gid:${groupIds.join(",")}`);
}
if(startTime) {
paramsJSON.push(`vat:${startTime}`);
}
if(endTime) {
paramsJSON.push(`exp:${endTime}`);
}
return await this.authClient.MakeKMSCall({
tenantId,
methodName: "elv_createOTPInstance",
params: [
tenantId,
ntpClass,
JSON.stringify(paramsJSON),
parseInt(maxTickets),
Date.now()
],
paramTypes: [
"string",
"int",
"string",
"int",
"int"
]
});
};
/**
* Update the attributes of the specified NTP instance. Only the specified attributes will be updated ; others will be unchanged.
*
* @methodGroup NTP Instances
* @namedParams
* @param {string} tenantId - The ID of the tenant in which this NTP instance was created
* @param {string} ntpId - The ID of the NTP instance to update
* @param {number=} maxTickets=0 - The maximum number of tickets that may be issued for this instance (if 0, no limit)
* @param {number=} maxRedemptions=100 - The maximum number of times each ticket may be redeemed
* @param {string|number=} startTime - The time when issued tickets can be redeemed
* @param {string|number=} endTime - The time when issued tickets can no longer be redeemed
*
* @return {Object} - Info about the NTP Instance
*/
exports.UpdateNTPInstance = async function({
tenantId,
ntpId,
maxTickets=0,
maxRedemptions=100,
startTime,
endTime,
}) {
ValidatePresence("tenantId", tenantId);
ValidatePresence("ntpId", ntpId);
startTime = ValidateDate(startTime);
endTime = ValidateDate(endTime);
let paramsJSON = [];
if(maxRedemptions) {
paramsJSON.push(`ntp:${parseInt(maxRedemptions)}`);
}
if(startTime) {
paramsJSON.push(`vat:${startTime}`);
}
if(endTime) {
paramsJSON.push(`exp:${endTime}`);
}
if(maxTickets) {
paramsJSON.push(`max:${parseInt(maxTickets)}`);
}
const {Ret} = await this.authClient.MakeKMSCall({
tenantId,
methodName: "elv_updateOTPInstance",
params: [
tenantId,
ntpId,
"update",
JSON.stringify(paramsJSON),
Date.now()
],
paramTypes: [
"string",
"string",
"string",
"string",
"int"
]
});
return FormatNTPInfo(JSON.parse(Ret));
};
/**
* Suspend the specified NTP instance. All tickets issued for this instance will be considered expired.
*
* To reactivate the NTP instance, reset the expiration date using `UpdateNTPInstance`
*
* @methodGroup NTP Instances
* @namedParams
* @param {string} tenantId - The ID of the tenant in which this NTP instance was created
* @param {string} ntpId - The ID of the NTP instance
*/
exports.SuspendNTPInstance = async function({tenantId, ntpId}) {
ValidatePresence("tenantId", tenantId);
ValidatePresence("ntpId", ntpId);
await this.authClient.MakeKMSCall({
tenantId,
methodName: "elv_updateOTPInstance",
params: [
tenantId,
ntpId,
"cancel",
"[]",
Date.now()
],
paramTypes: [
"string",
"string",
"string",
"string",
"int"
]
});
};
/**
* Delete the specified NTP instance. This action cannot be undone.
*
* @methodGroup NTP Instances
* @namedParams
* @param {string} tenantId - The ID of the tenant in which this NTP instance was created
* @param {string} ntpId - The ID of the NTP instance
*/
exports.DeleteNTPInstance = async function({tenantId, ntpId}) {
ValidatePresence("tenantId", tenantId);
ValidatePresence("ntpId", ntpId);
await this.authClient.MakeKMSCall({
tenantId,
methodName: "elv_updateOTPInstance",
params: [
tenantId,
ntpId,
"delete",
"[]",
Date.now()
],
paramTypes: [
"string",
"string",
"string",
"string",
"int"
]
});
};
/**
* Retrieve info for NTP instances in the specified tenant
*
* @methodGroup NTP Instances
* @namedParams
* @param {string} tenantId - The ID of the tenant
* @param {number=} count=10 - Maximum number of results to return
* @param {number=} offset=0 - Offset from which to return results
*
* @return {Object} - List of NTP instances along with pagination information
*/
exports.ListNTPInstances = async function({tenantId, count=10, offset=0}) {
ValidatePresence("tenantId", tenantId);
const {Defs, Total} = await this.authClient.MakeKMSCall({
tenantId,
methodName: "elv_listOTPInfo",
params: [
tenantId,
offset,
count
],
paramTypes: [
"string",
"int",
"int"
],
signature: false
});
return {
ntpInstances: Object.values(Defs || {}).map(FormatNTPInfo),
start: offset,
end: Math.min(Total, offset + count),
total: Total
};
};
/**
* Retrieve a info about the specified NTP instance
*
* @methodGroup NTP Instances
* @namedParams
* @param {string} tenantId - The ID of the tenant
* @param {string} ntpId - The ID of the NTP instance
*
* @return {Object} - Info about the NTP instance
*/
exports.NTPInstance = async function({tenantId, ntpId}) {
ValidatePresence("tenantId", tenantId);
ValidatePresence("ntpId", ntpId);
return FormatNTPInfo(
await this.authClient.MakeKMSCall({
tenantId,
methodName: "elv_getOTPInfo",
params: [
tenantId,
ntpId
],
paramTypes: [
"string",
"string"
],
signature: false
})
);
};
/**
* Issue a ticket from the specified NTP ID
*
* @see <a href="#CreateNTPInstance">CreateNTPInstance</a>
*
* @methodGroup Tickets
* @namedParams
* @param {string} tenantId - The ID of the tenant in the NTP instance was created
* @param {string} ntpId - The ID of the NTP instance from which to issue a ticket
* @param {string=} email - The email address associated with this ticket. If specified, the email address will have to
* be provided along with the ticket code in order to redeem the ticket.
* @param {number=} maxRedemptions - Maximum number of times this ticket may be redeemed. If less than the max redemptions
* of the NTP instance, the lower limit will be used.
*
* @return {Promise<Object>} - The generated ticket code and additional information about the ticket.
*/
exports.IssueNTPCode = async function({tenantId, ntpId, email, maxRedemptions}) {
ValidatePresence("tenantId", tenantId);
ValidatePresence("ntpId", ntpId);
let options = [];
if(email) {
options.push(`eml:${email}`);
}
if(maxRedemptions) {
options.push(`cnt:${parseInt(maxRedemptions)}`);
}
const params = [tenantId, ntpId, JSON.stringify(options), Date.now()];
const paramTypes = ["string", "string", "string", "uint"];
return await this.authClient.MakeKMSCall({
methodName: "elv_issueOTPCode",
params,
paramTypes
});
};
/**
* Identical to IssueNTPCode, but the token is also signed by the current user.
*
* @see <a href="#IssueNTPCode">IssueNTPCode</a>
*
* @methodGroup Tickets
* @namedParams
* @param {string} tenantId - The ID of the tenant in the NTP instance was created
* @param {string} ntpId - The ID of the NTP instance from which to issue a ticket
* @param {string=} email - The email address associated with this ticket. If specified, the email address will have to
* be provided along with the ticket code in order to redeem the ticket.
* @param {number=} maxRedemptions - Maximum number of times this ticket may be redeemed. If less than the max redemptions
* of the NTP instance, the lower limit will be used.
*
* @return {Promise<Object>} - The generated signed ticket code and additional information about the ticket.
*/
exports.IssueSignedNTPCode = async function({tenantId, ntpId, email, maxRedemptions}) {
let result = await this.IssueNTPCode({tenantId, ntpId, email, maxRedemptions});
if(result.token) {
const signature = await this.authClient.Sign(Ethers.utils.keccak256(Ethers.utils.toUtf8Bytes(result.token)));
const multiSig = this.utils.FormatSignature(signature);
result.token = `${result.token}.${this.utils.B64(multiSig)}`;
}
return result;
};
/**
* Redeem the specified ticket/code to authorize the client. Must provide either issuer or tenantId and ntpId
*
* @methodGroup Tickets
* @namedParams
* @param {string=} issuer - Issuer to authorize against
* @param {string=} tenantId - The ID of the tenant from which the ticket was issued
* @param {string=} ntpId - The ID of the NTP instance from which the ticket was issued
* @param {string} code - Access code
* @param {string=} email - Email address associated with the code
* @param {boolean=} includeNTPId - If specified, the response will include both the target object ID as well as the NTP ID associated with the ticket
*
* @return {Promise<string|Object>} - The object ID which the ticket is authorized to, or an object containing the object ID and NTP ID if `includeNTPId` is specified
*/
exports.RedeemCode = async function({issuer, tenantId, ntpId, code, email, includeNTPId=false}) {
const wallet = this.GenerateWallet();
issuer = issuer || "";
if(!this.signer) {
this.SetSigner({
signer: wallet.AddAccountFromMnemonic({mnemonic: wallet.GenerateMnemonic()})
});
}
if(issuer.startsWith("iq__")) {
ValidateObject(issuer);
} else if(issuer && !issuer.replace(/^\//, "").startsWith("otp/ntp/iten")) {
throw Error("Invalid issuer: " + issuer);
} else {
// Ticket API
ValidatePresence("issuer or tenantId", issuer || tenantId);
if(!issuer) {
issuer = UrlJoin("/otp", "ntp", tenantId, ntpId || "");
}
try {
const token = await this.authClient.GenerateChannelContentToken({
issuer,
code,
email
});
this.SetStaticToken({token});
const response = JSON.parse(this.utils.FromB64(token));
return includeNTPId ? { objectId: response.qid, ntpId: response.oid } : response.qid;
} catch(error) {
this.Log("Failed to redeem code:", true);
this.Log(error, true);
throw error;
/*
if((error.body || "").toString().includes("exceed configured maximum")) {
throw Error("Code exceeded maximum number of uses");
} else {
throw Error("Invalid code");
}
*/
}
}
// Site selector
const objectId = issuer;
const libraryId = await this.ContentObjectLibraryId({objectId});
const Hash = (code) => {
const chars = code.split("").map(code => code.charCodeAt(0));
return chars.reduce((sum, char, i) => (chars[i + 1] ? (sum * 2) + char * chars[i+1] * (i + 1) : sum + char), 0).toString();
};
const codeHash = Hash(code);
const codeInfo = await this.ContentObjectMetadata({libraryId, objectId, metadataSubtree: `public/codes/${codeHash}`});
if(!codeInfo){
this.Log(`Code redemption failed:\n\t${issuer}\n\t${code}`);
throw Error("Invalid code: " + code);
}
const { ak, sites, info } = codeInfo;
const signer = await wallet.AddAccountFromEncryptedPK({
encryptedPrivateKey: this.utils.FromB64(ak),
password: code
});
this.SetSigner({signer});
// Ensure wallet is initialized
await this.userProfileClient.WalletAddress();
return {
addr: this.utils.FormatAddress(signer.address),
sites,
info: info || {}
};
};