@affinidi-tdk/iota-browser
Version:
Browser module to fetch data through Affinidi Iota Framework
238 lines • 9.72 kB
JavaScript
import { toUtf8 } from '@aws-sdk/util-utf8-browser';
import { iot, mqtt5 } from 'aws-iot-device-sdk-v2/dist/browser';
import * as jose from 'jose';
import { v4 as uuidv4 } from 'uuid';
import { EventTypes, signedRequestEventSchema, signedRequestJWTSchema, } from '../validators/events';
import { InternalErrorCode, throwEventError, newUnexpectedError, IotaErrorCode, newIotaError, } from '../validators/error';
import { Environment, EnvironmentUtils, Logger, } from '@affinidi-tdk/common/helpers';
import { once } from 'events';
export class ChannelProvider {
iotEndpoint;
region;
iotaConfigBuilder;
iotaClient;
topicName;
constructor(params) {
this.iotEndpoint = params?.iotEndpoint ?? EnvironmentUtils.fetchIotUrl();
this.region = params?.region ?? EnvironmentUtils.fetchRegion();
}
getClient() {
if (!this.iotaClient) {
throw newIotaError(IotaErrorCode.IOTA_CLIENT_NOT_STARTED);
}
return this.iotaClient;
}
getTopicName() {
if (!this.topicName) {
throw newIotaError(IotaErrorCode.NOT_AUTHENTICATED);
}
return this.topicName;
}
async initialize(credentials) {
Logger.debug('Initializing Iota channel');
const credentialsProvider = new CustomCredentialsProvider({
accessKeyId: credentials.credentials.accessKeyId,
secretAccessKey: credentials.credentials.secretKey,
sessionToken: credentials.credentials.sessionToken,
}, this.region);
this.iotaConfigBuilder = await this.createConfigBuilder(credentialsProvider, credentials.connectionClientId);
this.topicName = `iota/v1/${credentials.connectionClientId}`;
this.iotaClient = await this.startClient();
}
async startClient() {
if (!this.iotaConfigBuilder || !this.topicName) {
throw newIotaError(IotaErrorCode.NOT_AUTHENTICATED);
}
if (!this.iotaClient) {
Logger.debug('Mqtt client has not been started yet');
try {
this.iotaClient = await this.startMqttClient(this.iotaConfigBuilder);
await this.subscribeToTopic(this.iotaClient, this.topicName);
}
catch {
if (this.iotaClient) {
this.iotaClient.stop();
}
throw newIotaError(IotaErrorCode.UNABLE_TO_CONNECT_WITH_PROVIDED_CREDENTIALS);
}
}
return this.iotaClient;
}
async createConfigBuilder(credentialsProvider, iotClientId) {
const configBuilder = iot.AwsIotMqtt5ClientConfigBuilder.newWebsocketMqttBuilderWithSigv4Auth(this.iotEndpoint, {
region: this.region,
credentialsProvider: credentialsProvider,
});
configBuilder
.withSessionBehavior(mqtt5.ClientSessionBehavior.RejoinAlways)
.withConnectProperties({
clientId: iotClientId,
keepAliveIntervalSeconds: 60,
sessionExpiryIntervalSeconds: 60,
});
return configBuilder;
}
async startMqttClient(iotaConfigBuilder) {
Logger.debug('Starting mqtt client');
const config = iotaConfigBuilder.build();
const client = new mqtt5.Mqtt5Client(config);
client.on('messageReceived', (eventData) => {
Logger.debug('Message Received event: ' + JSON.stringify(eventData.message));
if (eventData.message.payload) {
Logger.debug(' with payload: ' + toUtf8(eventData.message.payload));
}
});
if (EnvironmentUtils.fetchEnvironment() != Environment.PRODUCTION) {
this.addDebugListeners(client);
}
const attemptingConnect = once(client, 'attemptingConnect');
const connectionSuccess = once(client, 'connectionSuccess');
client.start();
await attemptingConnect;
await connectionSuccess;
// Keep this listener after first connection so that we dont subscribe twice
client.on('connectionSuccess', (eventData) => {
Logger.debug('Connection Success event');
Logger.debug('Connack: ' + JSON.stringify(eventData.connack));
Logger.debug('Settings: ' + JSON.stringify(eventData.settings));
if (this.topicName) {
Logger.debug('Connection re-established, re-subscribing to topic');
this.subscribeToTopic(client, this.topicName);
}
});
Logger.debug('Mqtt client started');
return client;
}
async subscribeToTopic(client, topicName) {
const packet = {
subscriptions: [
{
topicFilter: topicName,
qos: mqtt5.QoS.AtLeastOnce,
},
],
};
const suback = await client.subscribe(packet);
Logger.debug('Suback result:', suback);
Logger.debug('Subscribed to topic', topicName);
}
getRequest(event) {
let signedRequest, signedRequestJWT;
try {
signedRequest = signedRequestEventSchema.parse(event);
}
catch (e) {
throw newUnexpectedError(InternalErrorCode.SIGNED_REQUEST_EVENT, event.correlationId);
}
try {
const claims = jose.decodeJwt(signedRequest.data.jwt);
signedRequestJWT = signedRequestJWTSchema.parse(claims);
}
catch (e) {
throw newUnexpectedError(InternalErrorCode.SIGNED_REQUEST_JWT, event.correlationId);
}
const request = {
correlationId: signedRequest.correlationId,
payload: {
request: signedRequest.data.jwt,
client_id: signedRequestJWT.client_id,
},
};
return request;
}
async prepareRequest(params) {
const client = this.getClient();
const topicName = this.getTopicName();
const correlationId = params.correlationId ?? uuidv4();
const eventPayload = {
eventType: 'prepareRequest',
queryId: params.queryId,
correlationId,
...(params.audience ? { audience: params.audience } : {}),
};
const publishPacket = {
topicName: topicName,
payload: eventPayload,
qos: mqtt5.QoS.AtLeastOnce,
};
Logger.debug('Publishing prepare request event', publishPacket);
await client.publish(publishPacket);
Logger.debug('Published. Listening for response...');
return new Promise((resolve, reject) => {
client.on('messageReceived', (messageReceivedEvent) => {
if (messageReceivedEvent.message.payload) {
const raw_data = toUtf8(messageReceivedEvent.message.payload);
try {
Logger.debug('Event received', raw_data);
const event = JSON.parse(raw_data);
if (correlationId !== event.correlationId) {
return;
}
if (event.eventType === EventTypes.SignedRequest) {
const request = this.getRequest(event);
Logger.debug('Signed request received', request);
resolve(request);
}
else if (event.eventType === EventTypes.Error) {
throwEventError(event);
}
}
catch (e) {
Logger.debug('Error processing event data');
reject(e);
}
}
});
});
}
prepareRequestWithCallback(params, callback) {
this.prepareRequest(params)
.then((request) => callback(null, request))
.catch((error) => callback(error, null));
}
addDebugListeners(client) {
client.on('error', (error) => {
Logger.debug('Error event: ' + error.toString());
});
client.on('info', (info) => {
Logger.debug('Info event: ' + info.toString());
});
client.on('attemptingConnect', (eventData) => {
Logger.debug('Attempting Connect event', JSON.stringify(eventData));
});
client.on('connectionFailure', (eventData) => {
Logger.debug('Connection failure event: ' + eventData.error.toString());
});
client.on('disconnection', (eventData) => {
Logger.debug('Disconnection event: ' + eventData.error.toString());
if (eventData.disconnect !== undefined) {
Logger.debug('Disconnect packet: ' + JSON.stringify(eventData.disconnect));
}
});
client.on('stopped', (eventData) => {
Logger.debug('Stopped event', JSON.stringify(eventData));
});
}
}
class CustomCredentialsProvider {
awsCredentialIdentity;
region;
constructor(awsCredentialIdentity, region) {
this.awsCredentialIdentity = awsCredentialIdentity;
this.region = region;
}
getCredentials() {
return {
aws_region: this.region,
aws_access_id: this.awsCredentialIdentity.accessKeyId,
aws_secret_key: this.awsCredentialIdentity.secretAccessKey,
aws_sts_token: this.awsCredentialIdentity.sessionToken,
};
}
async refreshCredentials() {
// Logger.debug('Attempting To Refresh Credentials')
// TODO
return Promise.resolve();
}
}
//# sourceMappingURL=channel-provider.js.map