diff --git a/.npmignore b/.npmignore index eeaa57b..dffce70 100644 --- a/.npmignore +++ b/.npmignore @@ -5,4 +5,6 @@ cdk.out src client clisrc -!lib/construct/src \ No newline at end of file +!lib/construct/src +*.githhub +cli \ No newline at end of file diff --git a/README.md b/README.md index 5587893..015fe4f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The Test Client validates the event you will send to ensure it matches the Schem Test that your captured events match a published JSON Schema specification. -#### Event Monitoring Client - Collect Events to a WebUI in realtime +#### COMING SOON: Event Monitoring Client - Collect Events to a WebUI in realtime A web application that can connect to your WebSocket, allowing you to inspect, save and copy the published events. Useful when developing for instant feedback without adding lots of console logs to your Jest tests or logging out through compute. @@ -30,7 +30,15 @@ A web application that can connect to your WebSocket, allowing you to inspect, s ## Using the EventNet CDK Construct -Install and import the construct. +Install: + +```bash + +yarn add @leighton-digitial/eventnet + +``` + +& Import the construct: ```Typescript import { EventNet } from "@leighton-digital/event-net/lib/construct/"; @@ -41,12 +49,12 @@ Set up the EventNet instance with the EventBridge instance. ```Typescript const eventNet = new EventNet( this, - "event-net", + "EventNet", { prefix: stackName, eventBusName: eventBusName, includeOutput: true, // optional, default is false - invludeLogs: true // optional, default is false + includeLogs: true // optional, default is false } ); ``` @@ -68,7 +76,7 @@ const eventNet = new EventNet( - The Test Client collects the events from the WebSocket. ```Typescript -import { EventNetClient } from "@leighton-digital/event-net"; +import { EventNetClient } from "@leighton-digital/eventnet"; ``` The EventNet client expects your `prefix` from the CDK construct to be passed in as `'--stack=my-service-dev'`. This corresponds to the Cloud Formation stack name produced by CDK. i.e.: @@ -83,7 +91,7 @@ If you are using multiple Event Buses, we strongly recommend basing the names fr ```Typescript -import { EventNetClient, stackName } from "@leighton-digital/event-net"; +import { EventNetClient, stackName } from "@leighton-digital/eventnet"; describe("Test Producer > ", () => { @@ -111,7 +119,7 @@ describe("Test Producer > ", () => { await eventNet.waitForClosedSocket() // Use the Jest assertion to check - // the event against the JSONschema, + // the event against the JSONSchema, // more on this below expect(events[0].detail).toMatchJsonSchema(EventSpec); @@ -131,7 +139,7 @@ describe("Test Producer > ", () => { ```Typescript -import { EventNetClient } from "@leighton-digital/event-net"; +import { EventNetClient } from "@leighton-digital/eventnet"; import * as EventSpec from "../../jsonSpecs/someSchema-v1.json"; describe("Test Consumer > ", () => { @@ -149,9 +157,9 @@ describe("Test Consumer > ", () => { }, // Use the Jest assertion to check - // the event against the JSONschema, + // the event against the JSONSchema, // more on this below - expect(Event.Detail).toMatchJsonSchema(EventSpec); + expect(Event).toMatchJsonSchema(EventSpec); // send the event to the EventBrdge instance const sendEvents = await eventNet.sendEvent(Event, EventBusName) @@ -190,12 +198,12 @@ expect(singleMessage).toMatchJsonSchema(EventSpec); --- -## Using the EventNet Web Client +## COMING SOON: Using the EventNet Web Client First, you need to run the client: ``` -eventNet start +eventnet start ``` This will open a web client on url: http://localhost:3000 diff --git a/package.json b/package.json index 67359a5..69345a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@leighton-digital/eventnet", - "version": "0.0.1", + "version": "0.0.3", "main": "lib/index.js", "types": "lib/index.d.ts", "scripts": { @@ -28,7 +28,7 @@ "@types/uuid": "^9.0.2", "@types/ws": "^8.5.4", "@types/yargs": "^17.0.24", - "aws-cdk-lib": "2.79.1", + "aws-cdk-lib": "2.113.0", "constructs": "^10.0.0", "jest": "^29.5.0", "release-it": "^16.1.0", @@ -36,18 +36,16 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "aws-cdk-lib": "2.79.1", + "aws-cdk-lib": "2.113.0", "constructs": "^10.0.0" }, "dependencies": { - "@aws-cdk/aws-apigatewayv2-alpha": "^2.79.1-alpha.0", - "@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.79.1-alpha.0", - "@aws-sdk/client-dynamodb": "^3.348.0", - "@aws-sdk/lib-dynamodb": "^3.348.0", "@aws-sdk/client-apigatewaymanagementapi": "^3.454.0", - "@aws-sdk/credential-providers": "^3.454.0", - "@aws-sdk/client-ssm": "^3.454.0", + "@aws-sdk/client-dynamodb": "^3.348.0", "@aws-sdk/client-eventbridge": "^3.454.0", + "@aws-sdk/client-ssm": "^3.454.0", + "@aws-sdk/credential-providers": "^3.465.0", + "@aws-sdk/lib-dynamodb": "^3.348.0", "@types/figlet": "^1.5.6", "@types/inquirer": "^9.0.3", "@types/shelljs": "^0.8.12", @@ -61,10 +59,7 @@ "ws": "^8.13.0", "yargs": "^17.7.2" }, - "bin": { - "eventnet": "cli/index.js" - }, - "description": "\"EventNet", + "description": "Several tools for working with AWS EventBridge and the events published through it.", "keywords": [ "CDK", "Jest", diff --git a/src/construct/index.ts b/src/construct/index.ts index e1ec619..b62d978 100644 --- a/src/construct/index.ts +++ b/src/construct/index.ts @@ -1,6 +1,6 @@ import { Construct } from "constructs"; -import { WebSocketLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha"; -import * as apigwv2 from "@aws-cdk/aws-apigatewayv2-alpha"; +import { WebSocketLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations"; +import * as apigwv2 from "aws-cdk-lib/aws-apigatewayv2"; import * as cdk from "aws-cdk-lib"; import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; import * as events from "aws-cdk-lib/aws-events"; @@ -83,6 +83,7 @@ export class EventNet extends Construct { entry: path.join(path.resolve(__dirname), `/src/lambda/connected.js`), environment: { TABLE_NAME: table.tableName, + REGION: region, }, memorySize: 1024, architecture: lambda.Architecture.ARM_64, @@ -113,6 +114,7 @@ export class EventNet extends Construct { ), environment: { TABLE_NAME: table.tableName, + REGION: region, }, memorySize: 1024, architecture: lambda.Architecture.ARM_64, @@ -163,6 +165,7 @@ export class EventNet extends Construct { TABLE_NAME: table.tableName, API_ID: webSocketApi.apiId, STAGE: wsStage.stageName, + REGION: region, }, memorySize: 1024, architecture: lambda.Architecture.ARM_64, @@ -176,6 +179,14 @@ export class EventNet extends Construct { } ); + if (props.includeLogs) { + new LogGroup(this, `nodeJsFunctionLogGroupDataHandler`, { + logGroupName: `/aws/lambda/${eventNetFunction.functionName}`, + removalPolicy: cdk.RemovalPolicy.DESTROY, + retention: RetentionDays.ONE_WEEK, + }); + } + rule.addTarget( new targets.LambdaFunction(eventNetFunction, { retryAttempts: 3, diff --git a/src/construct/src/lambda/connected.ts b/src/construct/src/lambda/connected.ts index 406364c..c3a5a0a 100644 --- a/src/construct/src/lambda/connected.ts +++ b/src/construct/src/lambda/connected.ts @@ -1,5 +1,5 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; -import { BatchWriteCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import { APIGatewayProxyEvent } from "aws-lambda"; @@ -9,30 +9,22 @@ const client = new DynamoDBClient({ const docClient = DynamoDBDocumentClient.from(client); export const main = async (event: APIGatewayProxyEvent) => { + console.log({ event }); const tableName = process.env.TABLE_NAME; if (!tableName) { throw new Error("tableName not specified in process.env.TABLE_NAME"); } - const writeRequest = { - RequestItems: { - [`${process.env.TABLE_NAME}`]: [ - { - PutRequest: { - Item: { - PK: event.requestContext.connectionId, - }, - }, - }, - ], + const putItem = { + TableName: process.env.TABLE_NAME, + Item: { + PK: event.requestContext.connectionId, }, }; try { - await await docClient.send( - new BatchWriteCommand(writeRequest) - );; + await docClient.send(new PutCommand(putItem)); } catch (err) { return { statusCode: 500, diff --git a/src/construct/src/lambda/data.ts b/src/construct/src/lambda/data.ts index 749d001..7a148a8 100644 --- a/src/construct/src/lambda/data.ts +++ b/src/construct/src/lambda/data.ts @@ -1,6 +1,10 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb"; -import { ApiGatewayManagementApiClient, GetConnectionCommand, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi"; +import { + ApiGatewayManagementApiClient, + GetConnectionCommand, + PostToConnectionCommand, +} from "@aws-sdk/client-apigatewaymanagementapi"; const client = new DynamoDBClient({ region: process.env.AWS_REGION, @@ -9,7 +13,7 @@ const docClient = DynamoDBDocumentClient.from(client); const apiClient = new ApiGatewayManagementApiClient({ region: process.env.AWS_REGION, - endpoint: `https://${process.env.API_ID}.execute-api.${process.env.AWS_REGION}.amazonaws.com/${process.env.STAGE}` + endpoint: `https://${process.env.API_ID}.execute-api.${process.env.AWS_REGION}.amazonaws.com/${process.env.STAGE}`, }); export const main = async (event: any): Promise => { @@ -26,17 +30,20 @@ export const main = async (event: any): Promise => { } try { - connectionData = await docClient.send(new ScanCommand({ TableName: tableName, ProjectionExpression: "PK" })); + connectionData = await docClient.send( + new ScanCommand({ TableName: tableName, ProjectionExpression: "PK" }) + ); } catch (e) { return { statusCode: 500, body: e }; } + console.log({ connectionData }); + const postCalls = (connectionData.Items ?? []).map(async ({ PK }) => { try { const input = { ConnectionId: PK, Data: JSON.stringify(event) }; - const command = new PostToConnectionCommand(input); - apiClient.send(command); + await apiClient.send(command); } catch (e) { console.log(e); throw e; diff --git a/src/utils/AWSConfig/index.ts b/src/utils/AWSConfig/index.ts index 65d1d5c..e318882 100644 --- a/src/utils/AWSConfig/index.ts +++ b/src/utils/AWSConfig/index.ts @@ -1,6 +1,7 @@ import { loadArg } from "../options/args"; const { getDefaultRoleAssumerWithWebIdentity } = require("@aws-sdk/client-sts"); const { defaultProvider } = require("@aws-sdk/credential-provider-node"); +const { fromIni } = require("@aws-sdk/credential-providers"); export const stackName = loadArg({ cliArg: "stack", @@ -13,8 +14,26 @@ export const region = loadArg({ defaultValue: "eu-west-2", }); -const provider = defaultProvider({ - roleAssumerWithWebIdentity: getDefaultRoleAssumerWithWebIdentity({ region }), +const profile = loadArg({ + cliArg: "profile", + processEnvName: "PROFILE", }); -export const AWSConfig = { credentialDefaultProvider: provider }; +let AWSConfigObject: any = { + provider: defaultProvider({ + roleAssumerWithWebIdentity: getDefaultRoleAssumerWithWebIdentity({ + region, + }), + }), +}; + +if (profile) { + AWSConfigObject = { + credentials: fromIni({ + profile, + }), + region, + }; +} + +export const AWSConfig = AWSConfigObject; diff --git a/src/utils/JSONSchemaClient/index.ts b/src/utils/JSONSchemaClient/index.ts index ad1f1df..761e245 100644 --- a/src/utils/JSONSchemaClient/index.ts +++ b/src/utils/JSONSchemaClient/index.ts @@ -18,7 +18,7 @@ export default { toMatchSchema(theEvent: any, theSchema: any) { try { var validate = ajv.compile(theSchema); - var valid = validate(theEvent.detail); + var valid = validate(theEvent.detail || theEvent.Detail); if (!valid) { let warning: string = "Event"; validate.errors.forEach((msg: any) => { diff --git a/src/utils/WSClient/index.ts b/src/utils/WSClient/index.ts index 3f8b2e4..5eed6b1 100644 --- a/src/utils/WSClient/index.ts +++ b/src/utils/WSClient/index.ts @@ -1,6 +1,9 @@ import { AWSConfig, stackName } from "../AWSConfig"; import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; -import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge"; +import { + EventBridgeClient, + PutEventsCommand, +} from "@aws-sdk/client-eventbridge"; const clientSSM = new SSMClient(AWSConfig); const clientEventBridge = new EventBridgeClient(AWSConfig); @@ -20,7 +23,7 @@ export default class EventNetClient { this.WsClient = client; } - static async create (prefix?: string) { + static async create(prefix?: string) { let socketName = `${stackName.toLowerCase()}-eventNet-WS-URL`; if (prefix) { socketName = `${prefix}-eventNet-WS-URL`; @@ -46,11 +49,11 @@ export default class EventNetClient { return response.Parameter.Value; }; - public async closeClient () { + public async closeClient() { return await this.WsClient.close(); } - private waitForState (socket: any, state: any) { + private waitForState(socket: any, state: any) { const self = this; return new Promise(function (resolve) { setTimeout(function () { @@ -63,33 +66,21 @@ export default class EventNetClient { }); } - public waitForOpenSocket () { + public waitForOpenSocket() { const state = this.WsClient.OPEN; return this.waitForState(this.WsClient, state); } - public waitForClosedSocket () { + public waitForClosedSocket() { const state = this.WsClient.CLOSED; return this.waitForState(this.WsClient, state); } - public clearEventHistory () { + public clearEventHistory() { this.WsMessagesStore = []; } - public async validateAndSendEvent (event: any, schema: any) { - const self = this; - var validate = ajv.compile(schema); - var valid = validate(schema.detail); - if (!valid) { - let warning: string = "Event"; - validate.errors.forEach((msg: any) => { - warning = warning + ` ${msg.message}`; - }); - - throw new Error("Event does not match schema"); - } - + public async sendEvent(event: any) { const ents = { Entries: [ { @@ -100,12 +91,11 @@ export default class EventNetClient { }, ], }; - self.WsClient.close(); const command = new PutEventsCommand(ents); return await clientEventBridge.send(command); } - public matchEnvelope ( + public matchEnvelope( source: string, type: string, total = 1, diff --git a/yarn.lock b/yarn.lock index 13b5cc5..24767c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,30 +10,20 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@aws-cdk/asset-awscli-v1@^2.2.165": +"@aws-cdk/asset-awscli-v1@^2.2.201": version "2.2.201" resolved "https://registry.yarnpkg.com/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.201.tgz#a7b51d3ecc8ff3ca9798269eda3a1db2400b506a" integrity sha512-INZqcwDinNaIdb5CtW3ez5s943nX5stGBQS6VOP2JDlOFP81hM3fds/9NDknipqfUkZM43dx+HgVvkXYXXARCQ== -"@aws-cdk/asset-kubectl-v20@^2.1.1": +"@aws-cdk/asset-kubectl-v20@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz#d8e20b5f5dc20128ea2000dc479ca3c7ddc27248" integrity sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg== -"@aws-cdk/asset-node-proxy-agent-v5@^2.0.139": - version "2.0.166" - resolved "https://registry.yarnpkg.com/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.166.tgz#467507db141cd829ff8aa9d6ea5519310a4276b8" - integrity sha512-j0xnccpUQHXJKPgCwQcGGNu4lRiC1PptYfdxBIH1L4dRK91iBxtSQHESRQX+yB47oGLaF/WfNN/aF3WXwlhikg== - -"@aws-cdk/aws-apigatewayv2-alpha@^2.79.1-alpha.0": - version "2.79.1-alpha.0" - resolved "https://registry.yarnpkg.com/@aws-cdk/aws-apigatewayv2-alpha/-/aws-apigatewayv2-alpha-2.79.1-alpha.0.tgz#1647ff672559b5758ba8d0f6eedbb41b44e11aba" - integrity sha512-B5MDJjIa7DmykRxLYu1k2tnLNxllg4hwRWWgsMn+2xgXyZ0xmmdrxCXaAUx+SBiCxxpvumS3ukAGq7u5eYHvvA== - -"@aws-cdk/aws-apigatewayv2-integrations-alpha@^2.79.1-alpha.0": - version "2.79.1-alpha.0" - resolved "https://registry.yarnpkg.com/@aws-cdk/aws-apigatewayv2-integrations-alpha/-/aws-apigatewayv2-integrations-alpha-2.79.1-alpha.0.tgz#804a104fee7e6653c4f2d91e7889275a02806b4f" - integrity sha512-pfr8pEADDoa6xtB7ZUFR0tBEDlRa0RJ4lqUcHzXbUcBuTBsbuIs5jxvwaf//ZJyRqEJDlblMRAT3WABZlaY0bQ== +"@aws-cdk/asset-node-proxy-agent-v6@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz#6dc9b7cdb22ff622a7176141197962360c33e9ac" + integrity sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg== "@aws-crypto/crc32@3.0.0": version "3.0.0" @@ -520,7 +510,7 @@ "@smithy/types" "^2.5.0" tslib "^2.5.0" -"@aws-sdk/credential-providers@^3.454.0": +"@aws-sdk/credential-providers@^3.465.0": version "3.465.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-providers/-/credential-providers-3.465.0.tgz#841000b3d548fac20df3011d7945af0283d1bcf9" integrity sha512-mtndyew33Fnv30zVCQLBkqvUeFvjAlgAe3yM/10U//dxsOW3pfYWZ6sMzDbuXHLCyROQXJqZfnsQKQs0rOaO0Q== @@ -2190,22 +2180,22 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -aws-cdk-lib@2.79.1: - version "2.79.1" - resolved "https://registry.yarnpkg.com/aws-cdk-lib/-/aws-cdk-lib-2.79.1.tgz#d7e9ba9cea7af0318a1a3cf54dec6e39d1bbdf8d" - integrity sha512-iOPT9hbpP7z9otAzAgSr3HksjZe/QQXdyE7P1A4EESAzmLktOGfzzHydJqU1wfE9BbgK4ioCS0dJ3EaCCtWGpg== +aws-cdk-lib@2.113.0: + version "2.113.0" + resolved "https://registry.yarnpkg.com/aws-cdk-lib/-/aws-cdk-lib-2.113.0.tgz#dacc835f11c6df14bbe04746085f070bdde99eac" + integrity sha512-wZYooUd8nb3ADFtg1dSnsRt1MJmeqEK8cKOnPfCGCfm852YnVdb0qIEclURCQ4ygDJSuqiw9CE+xWVeTQTG6Dw== dependencies: - "@aws-cdk/asset-awscli-v1" "^2.2.165" - "@aws-cdk/asset-kubectl-v20" "^2.1.1" - "@aws-cdk/asset-node-proxy-agent-v5" "^2.0.139" + "@aws-cdk/asset-awscli-v1" "^2.2.201" + "@aws-cdk/asset-kubectl-v20" "^2.1.2" + "@aws-cdk/asset-node-proxy-agent-v6" "^2.0.1" "@balena/dockerignore" "^1.0.2" case "1.6.3" fs-extra "^11.1.1" - ignore "^5.2.4" + ignore "^5.3.0" jsonschema "^1.4.1" minimatch "^3.1.2" - punycode "^2.3.0" - semver "^7.5.0" + punycode "^2.3.1" + semver "^7.5.4" table "^6.8.1" yaml "1.10.2" @@ -3568,7 +3558,7 @@ ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.4: +ignore@^5.2.4, ignore@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== @@ -5106,7 +5096,7 @@ punycode@1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== -punycode@^2.1.0, punycode@^2.3.0: +punycode@^2.1.0, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -5403,7 +5393,7 @@ semver-diff@^4.0.0: dependencies: semver "^7.3.5" -semver@7.5.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.0, semver@^7.5.3, semver@^7.5.4: +semver@7.5.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==