smartledger-sdk
Version:
A JavaScript SDK for interacting with the SmartLedger.
1,023 lines (1,020 loc) • 2.52 MB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.SmartLedger = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./src/primitives/index.js"), exports);
__exportStar(require("./src/script/index.js"), exports);
__exportStar(require("./src/script/templates/index.js"), exports);
__exportStar(require("./src/transaction/index.js"), exports);
__exportStar(require("./src/transaction/fee-models/index.js"), exports);
__exportStar(require("./src/transaction/broadcasters/index.js"), exports);
__exportStar(require("./src/transaction/chaintrackers/index.js"), exports);
__exportStar(require("./src/transaction/http/index.js"), exports);
__exportStar(require("./src/messages/index.js"), exports);
__exportStar(require("./src/compat/index.js"), exports);
__exportStar(require("./src/totp/index.js"), exports);
__exportStar(require("./src/wallet/index.js"), exports);
__exportStar(require("./src/wallet/substrates/index.js"), exports);
__exportStar(require("./src/auth/index.js"), exports);
__exportStar(require("./src/overlay-tools/index.js"), exports);
__exportStar(require("./src/storage/index.js"), exports);
__exportStar(require("./src/identity/index.js"), exports);
__exportStar(require("./src/registry/index.js"), exports);
__exportStar(require("./src/kvstore/index.js"), exports);
},{"./src/auth/index.js":11,"./src/compat/index.js":26,"./src/identity/index.js":29,"./src/kvstore/index.js":32,"./src/messages/index.js":35,"./src/overlay-tools/index.js":39,"./src/primitives/index.js":61,"./src/registry/index.js":64,"./src/script/index.js":72,"./src/script/templates/index.js":76,"./src/storage/index.js":80,"./src/totp/index.js":81,"./src/transaction/broadcasters/index.js":93,"./src/transaction/chaintrackers/index.js":97,"./src/transaction/fee-models/index.js":99,"./src/transaction/http/index.js":104,"./src/transaction/index.js":105,"./src/wallet/index.js":113,"./src/wallet/substrates/index.js":122}],2:[function(require,module,exports){
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Peer = void 0;
const SessionManager_js_1 = require("./SessionManager.js");
const index_js_1 = require("./utils/index.js");
const Random_js_1 = __importDefault(require("../primitives/Random.js"));
const Utils = __importStar(require("../primitives/utils.js"));
const AUTH_VERSION = '0.1';
/**
* Represents a peer capable of performing mutual authentication.
* Manages sessions, handles authentication handshakes, certificate requests and responses,
* and sending and receiving general messages over a transport layer.
*
* This version supports multiple concurrent sessions per peer identityKey.
*/
class Peer {
/**
* Creates a new Peer instance
*
* @param {WalletInterface} wallet - The wallet instance used for cryptographic operations.
* @param {Transport} transport - The transport mechanism used for sending and receiving messages.
* @param {RequestedCertificateSet} [certificatesToRequest] - Optional set of certificates to request from a peer during the initial handshake.
* @param {SessionManager} [sessionManager] - Optional SessionManager to be used for managing peer sessions.
* @param {boolean} [autoPersistLastSession] - Whether to auto-persist the session with the last-interacted-with peer. Defaults to true.
*/
constructor(wallet, transport, certificatesToRequest, sessionManager, autoPersistLastSession, originator) {
this.onGeneralMessageReceivedCallbacks = new Map();
this.onCertificatesReceivedCallbacks = new Map();
this.onCertificateRequestReceivedCallbacks = new Map();
this.onInitialResponseReceivedCallbacks = new Map();
// Single shared counter for all callback types
this.callbackIdCounter = 0;
// Whether to auto-persist the session with the last-interacted-with peer
this.autoPersistLastSession = true;
this.wallet = wallet;
this.originator = originator;
this.transport = transport;
this.certificatesToRequest = certificatesToRequest ?? {
certifiers: [],
types: {}
};
this.transport.onData(this.handleIncomingMessage.bind(this)).catch(e => {
throw e;
});
this.sessionManager =
sessionManager != null ? sessionManager : new SessionManager_js_1.SessionManager();
if (autoPersistLastSession === false) {
this.autoPersistLastSession = false;
}
else {
this.autoPersistLastSession = true;
}
}
/**
* Sends a general message to a peer, and initiates a handshake if necessary.
*
* @param {number[]} message - The message payload to send.
* @param {string} [identityKey] - The identity public key of the peer. If not provided, uses lastInteractedWithPeer (if any).
* @param {number} [maxWaitTime] - optional max wait time in ms
* @returns {Promise<void>}
* @throws Will throw an error if the message fails to send.
*/
async toPeer(message, identityKey, maxWaitTime) {
if (this.autoPersistLastSession &&
typeof this.lastInteractedWithPeer === 'string' &&
typeof identityKey !== 'string') {
identityKey = this.lastInteractedWithPeer;
}
const peerSession = await this.getAuthenticatedSession(identityKey, maxWaitTime);
// Prepare the general message
const requestNonce = Utils.toBase64((0, Random_js_1.default)(32));
const { signature } = await this.wallet.createSignature({
data: message,
protocolID: [2, 'auth message signature'],
keyID: `${requestNonce} ${peerSession.peerNonce ?? ''}`,
counterparty: peerSession.peerIdentityKey
}, this.originator);
const generalMessage = {
version: AUTH_VERSION,
messageType: 'general',
identityKey: (await this.wallet.getPublicKey({ identityKey: true }, this.originator))
.publicKey,
nonce: requestNonce,
yourNonce: peerSession.peerNonce,
payload: message,
signature
};
peerSession.lastUpdate = Date.now();
this.sessionManager.updateSession(peerSession);
try {
await this.transport.send(generalMessage);
}
catch (error) {
const e = new Error(`Failed to send message to peer ${peerSession.peerIdentityKey ?? 'unknown'}: ${String(error.message)}`);
e.stack = error.stack;
throw e;
}
}
/**
* Sends a request for certificates to a peer.
* This method allows a peer to dynamically request specific certificates after
* an initial handshake or message has been exchanged.
*
* @param {RequestedCertificateSet} certificatesToRequest - Specifies the certifiers and types of certificates required from the peer.
* @param {string} [identityKey] - The identity public key of the peer. If not provided, the current or last session identity is used.
* @param {number} [maxWaitTime=10000] - Maximum time in milliseconds to wait for the peer session to be authenticated.
* @returns {Promise<void>} Resolves if the certificate request message is successfully sent.
* @throws Will throw an error if the peer session is not authenticated or if sending the request fails.
*/
async requestCertificates(certificatesToRequest, identityKey, maxWaitTime = 10000) {
if (this.autoPersistLastSession &&
typeof this.lastInteractedWithPeer === 'string' &&
typeof identityKey !== 'string') {
identityKey = this.lastInteractedWithPeer;
}
const peerSession = await this.getAuthenticatedSession(identityKey, maxWaitTime);
// Prepare the message
const requestNonce = Utils.toBase64((0, Random_js_1.default)(32));
const { signature } = await this.wallet.createSignature({
data: Utils.toArray(JSON.stringify(certificatesToRequest), 'utf8'),
protocolID: [2, 'auth message signature'],
keyID: `${requestNonce} ${peerSession.peerNonce ?? ''}`,
counterparty: peerSession.peerIdentityKey
}, this.originator);
const certRequestMessage = {
version: AUTH_VERSION,
messageType: 'certificateRequest',
identityKey: (await this.wallet.getPublicKey({ identityKey: true }, this.originator))
.publicKey,
nonce: requestNonce,
initialNonce: peerSession.sessionNonce,
yourNonce: peerSession.peerNonce,
requestedCertificates: certificatesToRequest,
signature
};
// Update last-used timestamp
peerSession.lastUpdate = Date.now();
this.sessionManager.updateSession(peerSession);
try {
await this.transport.send(certRequestMessage);
}
catch (error) {
throw new Error(`Failed to send certificate request message to peer ${peerSession.peerIdentityKey ?? 'unknown'}: ${String(error.message)}`);
}
}
/**
* Retrieves an authenticated session for a given peer identity. If no session exists
* or the session is not authenticated, initiates a handshake to create or authenticate the session.
*
* - If `identityKey` is provided, we look up any existing session for that identity key.
* - If none is found or not authenticated, we do a new handshake.
* - If `identityKey` is not provided, but we have a `lastInteractedWithPeer`, we try that key.
*
* @param {string} [identityKey] - The identity public key of the peer.
* @param {number} [maxWaitTime] - The maximum time in milliseconds to wait for the handshake.
* @returns {Promise<PeerSession>} - A promise that resolves with an authenticated `PeerSession`.
*/
async getAuthenticatedSession(identityKey, maxWaitTime) {
if (this.transport === undefined) {
throw new Error('Peer transport is not connected!');
}
let peerSession;
if (typeof identityKey === 'string') {
peerSession = this.sessionManager.getSession(identityKey);
}
// If that session doesn't exist or isn't authenticated, initiate handshake
if ((peerSession == null) || !peerSession.isAuthenticated) {
// This will create a brand-new session
const sessionNonce = await this.initiateHandshake(identityKey, maxWaitTime);
// Now retrieve it by the sessionNonce
peerSession = this.sessionManager.getSession(sessionNonce);
if ((peerSession == null) || !peerSession.isAuthenticated) {
throw new Error('Unable to establish mutual authentication with peer!');
}
}
return peerSession;
}
/**
* Registers a callback to listen for general messages from peers.
*
* @param {(senderPublicKey: string, payload: number[]) => void} callback - The function to call when a general message is received.
* @returns {number} The ID of the callback listener.
*/
listenForGeneralMessages(callback) {
const callbackID = this.callbackIdCounter++;
this.onGeneralMessageReceivedCallbacks.set(callbackID, callback);
return callbackID;
}
/**
* Removes a general message listener.
*
* @param {number} callbackID - The ID of the callback to remove.
*/
stopListeningForGeneralMessages(callbackID) {
this.onGeneralMessageReceivedCallbacks.delete(callbackID);
}
/**
* Registers a callback to listen for certificates received from peers.
*
* @param {(senderPublicKey: string, certs: VerifiableCertificate[]) => void} callback - The function to call when certificates are received.
* @returns {number} The ID of the callback listener.
*/
listenForCertificatesReceived(callback) {
const callbackID = this.callbackIdCounter++;
this.onCertificatesReceivedCallbacks.set(callbackID, callback);
return callbackID;
}
/**
* Cancels and unsubscribes a certificatesReceived listener.
*
* @param {number} callbackID - The ID of the certificates received callback to cancel.
*/
stopListeningForCertificatesReceived(callbackID) {
this.onCertificatesReceivedCallbacks.delete(callbackID);
}
/**
* Registers a callback to listen for certificates requested from peers.
*
* @param {(requestedCertificates: RequestedCertificateSet) => void} callback - The function to call when a certificate request is received
* @returns {number} The ID of the callback listener.
*/
listenForCertificatesRequested(callback) {
const callbackID = this.callbackIdCounter++;
this.onCertificateRequestReceivedCallbacks.set(callbackID, callback);
return callbackID;
}
/**
* Cancels and unsubscribes a certificatesRequested listener.
*
* @param {number} callbackID - The ID of the requested certificates callback to cancel.
*/
stopListeningForCertificatesRequested(callbackID) {
this.onCertificateRequestReceivedCallbacks.delete(callbackID);
}
/**
* Initiates the mutual authentication handshake with a peer.
*
* @private
* @param {string} [identityKey] - The identity public key of the peer.
* @param {number} [maxWaitTime=10000] - how long to wait for handshake
* @returns {Promise<string>} A promise that resolves to the session nonce.
*/
async initiateHandshake(identityKey, maxWaitTime = 10000) {
const sessionNonce = await (0, index_js_1.createNonce)(this.wallet, undefined, this.originator); // Initial request nonce
// Create the preliminary session (not yet authenticated)
const now = Date.now();
this.sessionManager.addSession({
isAuthenticated: false,
sessionNonce,
peerIdentityKey: identityKey,
lastUpdate: now
});
const initialRequest = {
version: AUTH_VERSION,
messageType: 'initialRequest',
identityKey: (await this.wallet.getPublicKey({ identityKey: true }, this.originator))
.publicKey,
initialNonce: sessionNonce,
requestedCertificates: this.certificatesToRequest
};
await this.transport.send(initialRequest);
return await this.waitForInitialResponse(sessionNonce, maxWaitTime);
}
/**
* Waits for the initial response from the peer after sending an initial handshake request message.
*
* @param {string} sessionNonce - The session nonce created in the initial request.
* @returns {Promise<string>} A promise that resolves with the session nonce when the initial response is received.
*/
async waitForInitialResponse(sessionNonce, maxWaitTime = 10000) {
return await new Promise((resolve, reject) => {
const callbackID = this.listenForInitialResponse(sessionNonce, nonce => {
clearTimeout(timeoutHandle);
this.stopListeningForInitialResponses(callbackID);
resolve(nonce);
});
const timeoutHandle = setTimeout(() => {
this.stopListeningForInitialResponses(callbackID);
reject(new Error('Initial response timed out.'));
}, maxWaitTime);
});
}
/**
* Adds a listener for an initial response message matching a specific initial nonce.
*
* @private
* @param {string} sessionNonce - The session nonce to match.
* @param {(sessionNonce: string) => void} callback - The callback to invoke when the initial response is received.
* @returns {number} The ID of the callback listener.
*/
listenForInitialResponse(sessionNonce, callback) {
const callbackID = this.callbackIdCounter++;
this.onInitialResponseReceivedCallbacks.set(callbackID, {
callback,
sessionNonce
});
return callbackID;
}
/**
* Removes a listener for initial responses.
*
* @private
* @param {number} callbackID - The ID of the callback to remove.
*/
stopListeningForInitialResponses(callbackID) {
this.onInitialResponseReceivedCallbacks.delete(callbackID);
}
/**
* Handles incoming messages from the transport.
*
* @param {AuthMessage} message - The incoming message to process.
* @returns {Promise<void>}
*/
async handleIncomingMessage(message) {
if (typeof message.version !== 'string' || message.version !== AUTH_VERSION) {
throw new Error(`Invalid or unsupported message auth version! Received: ${message.version}, expected: ${AUTH_VERSION}`);
}
switch (message.messageType) {
case 'initialRequest':
await this.processInitialRequest(message);
break;
case 'initialResponse':
await this.processInitialResponse(message);
break;
case 'certificateRequest':
await this.processCertificateRequest(message);
break;
case 'certificateResponse':
await this.processCertificateResponse(message);
break;
case 'general':
await this.processGeneralMessage(message);
break;
default:
throw new Error(`Unknown message type of ${String(message.messageType)} from ${String(message.identityKey)}`);
}
}
/**
* Processes an initial request message from a peer.
*
* @param {AuthMessage} message - The incoming initial request message.
*/
async processInitialRequest(message) {
if (typeof message.identityKey !== 'string' ||
typeof message.initialNonce !== 'string' ||
message.initialNonce === '') {
throw new Error('Missing required fields in initialRequest message.');
}
// Create a new sessionNonce for our side
const sessionNonce = await (0, index_js_1.createNonce)(this.wallet, undefined, this.originator);
const now = Date.now();
// We'll treat this as fully authenticated from *our* perspective (the responding side).
this.sessionManager.addSession({
isAuthenticated: true,
sessionNonce,
peerNonce: message.initialNonce,
peerIdentityKey: message.identityKey,
lastUpdate: now
});
// Possibly handle the peer's requested certs
let certificatesToInclude;
if ((message.requestedCertificates != null) &&
Array.isArray(message.requestedCertificates.certifiers) &&
message.requestedCertificates.certifiers.length > 0) {
if (this.onCertificateRequestReceivedCallbacks.size > 0) {
// Let the application handle it
this.onCertificateRequestReceivedCallbacks.forEach(cb => {
cb(message.identityKey, message.requestedCertificates);
});
}
else {
// Attempt to find automatically
certificatesToInclude = await (0, index_js_1.getVerifiableCertificates)(this.wallet, message.requestedCertificates, message.identityKey, this.originator);
}
}
// Create signature
const { signature } = await this.wallet.createSignature({
data: Utils.toArray(message.initialNonce + sessionNonce, 'base64'),
protocolID: [2, 'auth message signature'],
keyID: `${message.initialNonce} ${sessionNonce}`,
counterparty: message.identityKey
}, this.originator);
const initialResponseMessage = {
version: AUTH_VERSION,
messageType: 'initialResponse',
identityKey: (await this.wallet.getPublicKey({ identityKey: true }, this.originator))
.publicKey,
initialNonce: sessionNonce,
yourNonce: message.initialNonce,
certificates: certificatesToInclude,
requestedCertificates: this.certificatesToRequest,
signature
};
// If we haven't interacted with a peer yet, store this identity as "lastInteracted"
if (this.lastInteractedWithPeer === undefined) {
this.lastInteractedWithPeer = message.identityKey;
}
// Send the response
await this.transport.send(initialResponseMessage);
}
/**
* Processes an initial response message from a peer.
*
* @private
* @param {AuthMessage} message - The incoming initial response message.
* @throws Will throw an error if nonce or signature verification fails.
*/
async processInitialResponse(message) {
const validNonce = await (0, index_js_1.verifyNonce)(message.yourNonce, this.wallet, undefined, this.originator);
if (!validNonce) {
throw new Error(`Initial response nonce verification failed from peer: ${message.identityKey}`);
}
// This is the session we previously created by calling initiateHandshake
const peerSession = this.sessionManager.getSession(message.yourNonce);
if (peerSession == null) {
throw new Error(`Peer session not found for peer: ${message.identityKey}`);
}
// Validate message signature
const dataToVerify = Utils.toArray((peerSession.sessionNonce ?? '') + (message.initialNonce ?? ''), 'base64');
const { valid } = await this.wallet.verifySignature({
data: dataToVerify,
signature: message.signature,
protocolID: [2, 'auth message signature'],
keyID: `${peerSession.sessionNonce ?? ''} ${message.initialNonce ?? ''}`,
counterparty: message.identityKey
}, this.originator);
if (!valid) {
throw new Error(`Unable to verify initial response signature for peer: ${message.identityKey}`);
}
// Now mark the session as authenticated
peerSession.peerNonce = message.initialNonce;
peerSession.peerIdentityKey = message.identityKey;
peerSession.isAuthenticated = true;
peerSession.lastUpdate = Date.now();
this.sessionManager.updateSession(peerSession);
// If the handshake had requested certificates, validate them
if (this.certificatesToRequest?.certifiers?.length > 0 &&
message.certificates?.length > 0) {
await (0, index_js_1.validateCertificates)(this.wallet, message, this.certificatesToRequest, this.originator);
// Notify listeners
this.onCertificatesReceivedCallbacks.forEach(cb => cb(message.identityKey, message.certificates));
}
// Update lastInteractedWithPeer
this.lastInteractedWithPeer = message.identityKey;
// Let the handshake wait-latch know we got our response
this.onInitialResponseReceivedCallbacks.forEach(entry => {
if (entry.sessionNonce === peerSession.sessionNonce) {
entry.callback(peerSession.sessionNonce);
}
});
// The peer might also request certificates from us
if ((message.requestedCertificates != null) &&
Array.isArray(message.requestedCertificates.certifiers) &&
message.requestedCertificates.certifiers.length > 0) {
if (this.onCertificateRequestReceivedCallbacks.size > 0) {
// Let the application handle it
this.onCertificateRequestReceivedCallbacks.forEach(cb => {
cb(message.identityKey, message.requestedCertificates);
});
}
else {
// Attempt auto
const verifiableCertificates = await (0, index_js_1.getVerifiableCertificates)(this.wallet, message.requestedCertificates, message.identityKey, this.originator);
await this.sendCertificateResponse(message.identityKey, verifiableCertificates);
}
}
}
/**
* Processes an incoming certificate request message from a peer.
* Verifies nonce/signature and then possibly sends a certificateResponse.
*
* @param {AuthMessage} message - The certificate request message received from the peer.
* @throws {Error} if nonce or signature is invalid.
*/
async processCertificateRequest(message) {
const validNonce = await (0, index_js_1.verifyNonce)(message.yourNonce, this.wallet, undefined, this.originator);
if (!validNonce) {
throw new Error(`Unable to verify nonce for certificate request message from: ${message.identityKey}`);
}
const peerSession = this.sessionManager.getSession(message.yourNonce);
if (peerSession == null) {
throw new Error(`Session not found for nonce: ${message.yourNonce}`);
}
const { valid } = await this.wallet.verifySignature({
data: Utils.toArray(JSON.stringify(message.requestedCertificates), 'utf8'),
signature: message.signature,
protocolID: [2, 'auth message signature'],
keyID: `${message.nonce ?? ''} ${peerSession.sessionNonce ?? ''}`,
counterparty: peerSession.peerIdentityKey
}, this.originator);
if (!valid) {
throw new Error(`Invalid signature in certificate request message from ${peerSession.peerIdentityKey}`);
}
// Update usage
peerSession.lastUpdate = Date.now();
this.sessionManager.updateSession(peerSession);
if ((message.requestedCertificates != null) &&
Array.isArray(message.requestedCertificates.certifiers) &&
message.requestedCertificates.certifiers.length > 0) {
if (this.onCertificateRequestReceivedCallbacks.size > 0) {
// Let the application handle it
this.onCertificateRequestReceivedCallbacks.forEach(cb => {
cb(message.identityKey, message.requestedCertificates);
});
}
else {
// Attempt auto
const verifiableCertificates = await (0, index_js_1.getVerifiableCertificates)(this.wallet, message.requestedCertificates, message.identityKey, this.originator);
await this.sendCertificateResponse(message.identityKey, verifiableCertificates);
}
}
}
/**
* Sends a certificate response message containing the specified certificates to a peer.
*
* @param {string} verifierIdentityKey - The identity key of the peer requesting the certificates.
* @param {VerifiableCertificate[]} certificates - The list of certificates to include in the response.
* @throws Will throw an error if the transport fails to send the message.
*/
async sendCertificateResponse(verifierIdentityKey, certificates) {
const peerSession = await this.getAuthenticatedSession(verifierIdentityKey);
const requestNonce = Utils.toBase64((0, Random_js_1.default)(32));
const { signature } = await this.wallet.createSignature({
data: Utils.toArray(JSON.stringify(certificates), 'utf8'),
protocolID: [2, 'auth message signature'],
keyID: `${requestNonce} ${peerSession.peerNonce ?? ''}`,
counterparty: peerSession.peerIdentityKey
}, this.originator);
const certificateResponse = {
version: AUTH_VERSION,
messageType: 'certificateResponse',
identityKey: (await this.wallet.getPublicKey({ identityKey: true }, this.originator))
.publicKey,
nonce: requestNonce,
initialNonce: peerSession.sessionNonce,
yourNonce: peerSession.peerNonce,
certificates,
signature
};
// Update usage
peerSession.lastUpdate = Date.now();
this.sessionManager.updateSession(peerSession);
try {
await this.transport.send(certificateResponse);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to send certificate response message to peer ${peerSession.peerIdentityKey ?? 'unknown'}: ${errorMessage}`);
}
}
/**
* Processes a certificate response message from a peer.
*
* @private
* @param {AuthMessage} message - The incoming certificate response message.
* @throws Will throw an error if nonce verification or signature verification fails.
*/
async processCertificateResponse(message) {
const validNonce = await (0, index_js_1.verifyNonce)(message.yourNonce, this.wallet, undefined, this.originator);
if (!validNonce) {
throw new Error(`Unable to verify nonce for certificate response from: ${message.identityKey}`);
}
const peerSession = this.sessionManager.getSession(message.yourNonce);
if (peerSession == null) {
throw new Error(`Session not found for nonce: ${message.yourNonce}`);
}
// Validate message signature
const { valid } = await this.wallet.verifySignature({
data: Utils.toArray(JSON.stringify(message.certificates), 'utf8'),
signature: message.signature,
protocolID: [2, 'auth message signature'],
keyID: `${message.nonce ?? ''} ${peerSession.sessionNonce ?? ''}`,
counterparty: message.identityKey
}, this.originator);
if (!valid) {
throw new Error(`Unable to verify certificate response signature for peer: ${message.identityKey}`);
}
// We also handle optional validation if there's a requestedCertificates field
await (0, index_js_1.validateCertificates)(this.wallet, message, message.requestedCertificates, this.originator);
// Notify any listeners
this.onCertificatesReceivedCallbacks.forEach(cb => {
cb(message.identityKey, message.certificates ?? []);
});
peerSession.lastUpdate = Date.now();
this.sessionManager.updateSession(peerSession);
}
/**
* Processes a general message from a peer.
*
* @private
* @param {AuthMessage} message - The incoming general message.
* @throws Will throw an error if nonce or signature verification fails.
*/
async processGeneralMessage(message) {
const validNonce = await (0, index_js_1.verifyNonce)(message.yourNonce, this.wallet, undefined, this.originator);
if (!validNonce) {
throw new Error(`Unable to verify nonce for general message from: ${message.identityKey}`);
}
const peerSession = this.sessionManager.getSession(message.yourNonce);
if (peerSession == null) {
throw new Error(`Session not found for nonce: ${message.yourNonce}`);
}
const { valid } = await this.wallet.verifySignature({
data: message.payload,
signature: message.signature,
protocolID: [2, 'auth message signature'],
keyID: `${message.nonce ?? ''} ${peerSession.sessionNonce ?? ''}`,
counterparty: peerSession.peerIdentityKey
}, this.originator);
if (!valid) {
throw new Error(`Invalid signature in generalMessage from ${peerSession.peerIdentityKey}`);
}
// Mark last usage
peerSession.lastUpdate = Date.now();
this.sessionManager.updateSession(peerSession);
// Update lastInteractedWithPeer
this.lastInteractedWithPeer = message.identityKey;
// Dispatch callbacks
this.onGeneralMessageReceivedCallbacks.forEach(cb => {
cb(message.identityKey, message.payload ?? []);
});
}
}
exports.Peer = Peer;
},{"../primitives/Random.js":55,"../primitives/utils.js":62,"./SessionManager.js":3,"./utils/index.js":17}],3:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionManager = void 0;
/**
* Manages sessions for peers, allowing multiple concurrent sessions
* per identity key. Primary lookup is always by `sessionNonce`.
*/
class SessionManager {
constructor() {
this.sessionNonceToSession = new Map();
this.identityKeyToNonces = new Map();
}
/**
* Adds a session to the manager, associating it with its sessionNonce,
* and also with its peerIdentityKey (if any).
*
* This does NOT overwrite existing sessions for the same peerIdentityKey,
* allowing multiple concurrent sessions for the same peer.
*
* @param {PeerSession} session - The peer session to add.
*/
addSession(session) {
if (typeof session.sessionNonce !== 'string') {
throw new Error('Invalid session: sessionNonce is required to add a session.');
}
// Use the sessionNonce as the primary key
this.sessionNonceToSession.set(session.sessionNonce, session);
// Also track it by identity key if present
if (typeof session.peerIdentityKey === 'string') {
let nonces = this.identityKeyToNonces.get(session.peerIdentityKey);
if (nonces == null) {
nonces = new Set();
this.identityKeyToNonces.set(session.peerIdentityKey, nonces);
}
nonces.add(session.sessionNonce);
}
}
/**
* Updates a session in the manager (primarily by re-adding it),
* ensuring we record the latest data (e.g., isAuthenticated, lastUpdate, etc.).
*
* @param {PeerSession} session - The peer session to update.
*/
updateSession(session) {
// Remove the old references (if any) and re-add
this.removeSession(session);
this.addSession(session);
}
/**
* Retrieves a session based on a given identifier, which can be:
* - A sessionNonce, or
* - A peerIdentityKey.
*
* If it is a `sessionNonce`, returns that exact session.
* If it is a `peerIdentityKey`, returns the "best" (e.g. most recently updated,
* authenticated) session associated with that peer, if any.
*
* @param {string} identifier - The identifier for the session (sessionNonce or peerIdentityKey).
* @returns {PeerSession | undefined} - The matching peer session, or undefined if not found.
*/
getSession(identifier) {
// Check if this identifier is directly a sessionNonce
const direct = this.sessionNonceToSession.get(identifier);
if (direct != null) {
return direct;
}
// Otherwise, interpret the identifier as an identity key
const nonces = this.identityKeyToNonces.get(identifier);
if ((nonces == null) || nonces.size === 0) {
return undefined;
}
// Pick the "best" session. One sensible approach:
// - Choose an authenticated session if available
// - Among them, pick the most recently updated
let best;
for (const nonce of nonces) {
const s = this.sessionNonceToSession.get(nonce);
if (s == null)
continue;
// We can prefer authenticated sessions
if (best == null) {
best = s;
}
else {
// If we want the "most recently updated" AND isAuthenticated
if ((s.lastUpdate ?? 0) > (best.lastUpdate ?? 0)) {
best = s;
}
}
}
// Optionally, you could also filter out isAuthenticated===false if you only want
// an authenticated session. But for our usage, let's return the latest any session.
return best;
}
/**
* Removes a session from the manager by clearing all associated identifiers.
*
* @param {PeerSession} session - The peer session to remove.
*/
removeSession(session) {
if (typeof session.sessionNonce === 'string') {
this.sessionNonceToSession.delete(session.sessionNonce);
}
if (typeof session.peerIdentityKey === 'string') {
const nonces = this.identityKeyToNonces.get(session.peerIdentityKey);
if (nonces != null) {
nonces.delete(session.sessionNonce ?? '');
if (nonces.size === 0) {
this.identityKeyToNonces.delete(session.peerIdentityKey);
}
}
}
}
/**
* Checks if a session exists for a given identifier (either sessionNonce or identityKey).
*
* @param {string} identifier - The identifier to check.
* @returns {boolean} - True if the session exists, false otherwise.
*/
hasSession(identifier) {
const direct = this.sessionNonceToSession.has(identifier);
if (direct)
return true;
// if not directly a nonce, interpret as identityKey
const nonces = this.identityKeyToNonces.get(identifier);
return !(nonces == null) && nonces.size > 0;
}
}
exports.SessionManager = SessionManager;
},{}],4:[function(require,module,exports){
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const Utils = __importStar(require("../../primitives/utils.js"));
const ProtoWallet_js_1 = __importDefault(require("../../wallet/ProtoWallet.js"));
const Signature_js_1 = __importDefault(require("../../primitives/Signature.js"));
/**
* Represents an Identity Certificate as per the Wallet interface specifications.
*
* This class provides methods to serialize and deserialize certificates, as well as signing and verifying the certificate's signature.
*/
class Certificate {
/**
* Constructs a new Certificate.
*
* @param {Base64String} type - Type identifier for the certificate, base64 encoded string, 32 bytes.
* @param {Base64String} serialNumber - Unique serial number of the certificate, base64 encoded string, 32 bytes.
* @param {PubKeyHex} subject - The public key belonging to the certificate's subject, compressed public key hex string.
* @param {PubKeyHex} certifier - Public key of the certifier who issued the certificate, compressed public key hex string.
* @param {OutpointString} revocationOutpoint - The outpoint used to confirm that the certificate has not been revoked (TXID.OutputIndex), as a string.
* @param {Record<CertificateFieldNameUnder50Bytes, string>} fields - All the fields present in the certificate.
* @param {HexString} signature - Certificate signature by the certifier's private key, DER encoded hex string.
*/
constructor(type, serialNumber, subject, certifier, revocationOutpoint, fields, signature) {
this.type = type;
this.serialNumber = serialNumber;
this.subject = subject;
this.certifier = certifier;
this.revocationOutpoint = revocationOutpoint;
this.fields = fields;
this.signature = signature;
}
/**
* Serializes the certificate into binary format, with or without a signature.
*
* @param {boolean} [includeSignature=true] - Whether to include the signature in the serialization.
* @returns {number[]} - The serialized certificate in binary format.
*/
toBinary(includeSignature = true) {
const writer = new Utils.Writer();
// Write type (Base64String, 32 bytes)
const typeBytes = Utils.toArray(this.type, 'base64');
writer.write(typeBytes);
// Write serialNumber (Base64String, 32 bytes)
const serialNumberBytes = Utils.toArray(this.serialNumber, 'base64');
writer.write(serialNumberBytes);
// Write subject (33 bytes compressed PubKeyHex)
const subjectBytes = Utils.toArray(this.subject, 'hex');
writer.write(subjectBytes);
// Write certifier (33 bytes compressed PubKeyHex)
const certifierBytes = Utils.toArray(this.certifier, 'hex');
writer.write(certifierBytes);
// Write revocationOutpoint (TXID + OutputIndex)
const [txid, outputIndex] = this.revocationOutpoint.split('.');
const txidBytes = Utils.toArray(txid, 'hex');
writer.write(txidBytes);
writer.writeVarIntNum(Number(outputIndex));
// Write fields
// Sort field names lexicographically
const fieldNames = Object.keys(this.fields).sort();
writer.writeVarIntNum(fieldNames.length);
for (const fieldName of fieldNames) {
const fieldValue = this.fields[fieldName];
// Field name
const fieldNameBytes = Utils.toArray(fieldName, 'utf8');
writer.writeVarIntNum(fieldNameBytes.length);
writer.write(fieldNameBytes);
// Field value
const fieldValueBytes = Utils.toArray(fieldValue, 'utf8');
writer.writeVarIntNum(fieldValueBytes.length);
writer.write(fieldValueBytes);
}
// Write signature if included
if (includeSignature && (this.signature ?? '').length > 0) { // ✅ Explicitly handle nullish signature
const signatureBytes = Utils.toArray(this.signature, 'hex'); // ✅ Type assertion ensures it's a string
writer.write(signatureBytes);
}
return writer.toArray();
}
/**
* Deserializes a certificate from binary format.
*
* @param {number[]} bin - The binary data representing the certificate.
* @returns {Certificate} - The deserialized Certificate object.
*/
static fromBinary(bin) {
const reader = new Utils.Reader(bin);
// Read type
const typeBytes = reader.read(32);
const type = Utils.toBase64(typeBytes);
// Read serialNumber
const serialNumberBytes = reader.read(32);
const serialNumber = Utils.toBase64(serialNumberBytes);
// Read subject (33 bytes)
const subjectBytes = reader.read(33);
const subject = Utils.toHex(subjectBytes);
// Read certifier (33 bytes)
const certifierBytes = reader.read(33);
const certifier = Utils.toHex(certifierBytes);
// Read revocationOutpoint
const txidBytes = reader.read(32);
const txid = Utils.toHex(txidBytes);
const outputIndex = reader.readVarIntNum();
const revocationOutpoint = `${txid}.${outputIndex}`;
// Read fields
const numFields = reader.readVarIntNum();
const fields = {};
for (let i = 0; i < numFields; i++) {
// Field name
const fieldNameLength = reader.readVarIntNum();
const fieldNameBytes = reader.read(fieldNameLength);
const fieldName = Utils.toUTF8(fieldNameBytes);
// Field value
const fieldValueLength = reader.readVarIntNum();
const fieldValueBytes = reader.read(fieldValueLength);
const fieldValue = Utils.toUTF8(fieldValueBytes);
fields[fieldName] = fieldValue;
}
// Read signature if present
let signature;
if (!reader.eof()) {
const signatureBytes = reader.read();
const sig = Signature_js_1.default.fromDER(signatureBytes);
signature = sig.toString('hex');
}
return new Certificate(type, serialNumber, subject, certifier, revocationOutpoint, fields, signature);
}
/**
* Verifies the certificate's signature.
*
* @returns {Promise<boolean>} - A promise that resolves to true if the signature is valid.
*/
async verify() {
// A verifier can be any wallet capable of verifying signatures
const verifier = new ProtoWallet_js_1.default('anyone');
const verificationData = this.toBinary(false); // Exclude the signature from the verification data
const signatureHex = this.signature ?? ''; // Provide a fallback value (empty string)
const { valid } = await verifier.verifySignature({
signature: Utils.toArray(signatureHex, 'hex'),
data: verificationData,
protocolID: [2, 'certificate signature'],
keyID: `${this.type} ${this.serialNumber}`,
counterparty: this.certifier // The certifier is the one who signed the certificate
});
return valid;
}
/**
* Signs the certificate using the provided certifier wallet.
*
* @param {Wallet} certifierWallet - The wallet representing the certifier.
* @returns {Promise<void>}
*/
async sign(certifierWallet) {
if (this.signature != null && this.signature.length > 0) { // ✅ Explicitly checking for null/undefined
throw new Error(`Certificate has already been signed! Signature present: ${this.signature}`);
}
// Ensure the certifier declared is the one actually signing
this.certifier = (await certifierWallet.getPublicKey({ identityKey: true })).publicKey;
const preimage = this.toBinary(false); // Exclude the signature when signing
const { signature } = await certifierWallet.createSignature({
data: preimage,
protocolID: [2, 'certificate signature'],
keyID: `${this.type} ${this.serialNumber}`
});
this.signature = Utils.toHex(signature);
}
/**
* Helper function which retrieves the protocol ID and key ID for certificate field encryption.
*
* For master certificate creation, no serial number is provided because entropy is required
* from both the client and the certifier. In this case, the `keyID` is simply the `fieldName`.
*
* For VerifiableCertificates verifier keyring creation, both the serial number and field name are available,
* so the `keyID` is formed by concatenating the `serialNumber` and `fieldName`.
*
* @param fieldName - The name of the field within the certificate to be encrypted.
* @param