From 82e8b5600b22bf1d5f33152ee4ae1010fe441739 Mon Sep 17 00:00:00 2001 From: Johnson Tse Date: Mon, 6 Apr 2026 13:28:52 -0400 Subject: [PATCH 1/3] [RECINF-1032] FIPS support for BOYI --- .../byoiclient-encrypt-recording.js | 90 +++++++++++++------ 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js b/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js index 08a17de..d38dc51 100644 --- a/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js +++ b/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js @@ -3,7 +3,6 @@ const platformClient = require('purecloud-platform-client-v2'); const winston = require('winston'); const fs = require('fs'); const path = require('path') -const forge = require('node-forge'); const {spawn} = require('child_process'); const clientId = process.env.GENESYS_CLOUD_CLIENT_ID; @@ -67,39 +66,74 @@ async function convertDerToRSAPublicKey(opensslPath, derFileName, pemFileName) { }); } -// Generate a key pair and save the certificate -async function generateX509Certificate(keyId, pemFileName, certFileName) { - - // The key length must be 2048 - var keys = forge.pki.rsa.generateKeyPair(2048); - - // Replace the RSA public key - let pemBody = fs.readFileSync(pemFileName); - keys.publicKey = forge.pki.publicKeyFromPem(pemBody); - - // Generate X509 certificate with new public key - var cert = forge.pki.createCertificate(); - cert.publicKey = keys.publicKey; - - // Make sure the serialNumber starts with "00" and does not contain '-' - cert.serialNumber = '00' + keyId.replace(/-/g, ''); - - // Sign the X509 certificate - cert.sign(keys.privateKey); +// Run an OpenSSL command and return a promise that resolves to true on success +function runOpenssl(opensslPath, parameters) { + return new Promise((resolve) => { + let opensslProcess = spawn(opensslPath, parameters); + opensslProcess.on('close', (code) => { + if (code == 0) { + resolve(true); + } else { + logger.error(`OpenSSL returned ${code} for command: openssl ${parameters[0]}`); + resolve(false); + } + }); + }); +} - // Save the certificate pem file - let certificatePem = forge.pki.certificateToPem(cert); - var stream = fs.createWriteStream(certFileName); - stream.write(certificatePem); +// Generate an X509 certificate embedding the Genesys public key, using OpenSSL (FIPS-validated module) with SHA-256 signing +async function generateX509Certificate(opensslPath, keyId, pemFileName, certFileName) { + let throwawayKeyFile = certFileName.replace('.cert.pem', '.throwaway.key'); + let csrFile = certFileName.replace('.cert.pem', '.csr'); + + // Serial number must start with "00" and not contain dashes + let serialHex = '0x00' + keyId.replace(/-/g, ''); + + // Step 1: Generate a throwaway 2048-bit RSA private key for certificate signing + let success = await runOpenssl(opensslPath, [ + 'genpkey', '-algorithm', 'RSA', + '-pkeyopt', 'rsa_keygen_bits:2048', + '-out', throwawayKeyFile + ]); + if (!success) return false; + + // Step 2: Create a certificate signing request using the throwaway key + success = await runOpenssl(opensslPath, [ + 'req', '-new', + '-key', throwawayKeyFile, + '-subj', '/CN=recording-encryption', + '-out', csrFile + ]); + if (!success) return false; + + // Step 3: Create self-signed cert with the Genesys public key forced in, signed with SHA-256 + success = await runOpenssl(opensslPath, [ + 'x509', '-req', + '-in', csrFile, + '-force_pubkey', pemFileName, + '-signkey', throwawayKeyFile, + '-set_serial', serialHex, + '-sha256', + '-days', '1', + '-out', certFileName + ]); + + // Clean up throwaway intermediate files + try { fs.unlinkSync(throwawayKeyFile); } catch(e) { /* ignore */ } + try { fs.unlinkSync(csrFile); } catch(e) { /* ignore */ } + + return success; } // >> END byoi-generate-intermediate-key // >> START byoi-perform-encryption -// Invoke openssl command to encrypt the file using the certificate file +// Invoke openssl command to encrypt the file using the certificate file (FIPS 140-3 compliant: AES-256-GCM + RSA-OAEP) async function performEncryption(opensslPath, x509CertificateFilePath, originalFilePath, outputFilePath) { return new Promise((resolve, reject) => { - // openssl cms -encrypt -aes256 -in audio.opus -binary -outform DER -out audio.opus.bin f36f8c85-8922-45fa-a3f1-d197d1176340.cert.pem + // openssl cms -encrypt -aes-256-gcm -keyopt rsa_padding_mode:oaep -in audio.opus -binary -outform DER -out audio.opus.bin cert.pem let parameters = [ - 'cms', '-encrypt', '-aes256', + 'cms', '-encrypt', + '-aes-256-gcm', + '-keyopt', 'rsa_padding_mode:oaep', '-in', originalFilePath, '-binary', '-outform', 'DER', '-out', outputFilePath, x509CertificateFilePath @@ -168,7 +202,7 @@ async function encryptRecording(recordingAudioFile) { // Generate X509 certificate and save the certificate as .cert.pem file let certFileName = path.join(workingDir, keyId + '.cert.pem'); - generateX509Certificate(keyId, pemFileName, certFileName); + await generateX509Certificate(opensslPath, keyId, pemFileName, certFileName); logger.verbose(`Generated X509 certificate file: ${certFileName}`); // Perform CMS encryption using the certificate file From a59f499f890c0dcfcae38b8612a8392f06c1c422 Mon Sep 17 00:00:00 2001 From: Johnson Tse Date: Tue, 1 Sep 2026 10:18:04 -0400 Subject: [PATCH 2/3] [RECINF-1032] Update example --- .../byoiclient-encrypt-recording.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js b/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js index d38dc51..5afa074 100644 --- a/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js +++ b/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js @@ -129,14 +129,22 @@ async function generateX509Certificate(opensslPath, keyId, pemFileName, certFile // Invoke openssl command to encrypt the file using the certificate file (FIPS 140-3 compliant: AES-256-GCM + RSA-OAEP) async function performEncryption(opensslPath, x509CertificateFilePath, originalFilePath, outputFilePath) { return new Promise((resolve, reject) => { - // openssl cms -encrypt -aes-256-gcm -keyopt rsa_padding_mode:oaep -in audio.opus -binary -outform DER -out audio.opus.bin cert.pem + // openssl cms -encrypt -aes-256-gcm -recip f36f8c85-8922-45fa-a3f1-d197d1176340.cert.pem -keyopt rsa_padding_mode:oaep -keyopt rsa_oaep_md:sha256 -in audio.opus -binary -outform DER -out audio.opus.bin + // The recipient certificate must be supplied with -recip and must appear BEFORE -keyopt. + // -keyopt applies to the preceding recipient, so passing the certificate as a trailing + // argument instead would make OpenSSL fail with "No key specified". + // rsa_oaep_md:sha256 is required as well as rsa_padding_mode:oaep. Without it OpenSSL omits the + // RSAESOAEPparams from the CMS envelope, which means SHA-1 by default (RFC 8017), and Genesys Cloud + // will not be able to decrypt the recording. Both flags require OpenSSL 3.0 or later. let parameters = [ 'cms', '-encrypt', '-aes-256-gcm', + '-recip', x509CertificateFilePath, '-keyopt', 'rsa_padding_mode:oaep', + '-keyopt', 'rsa_oaep_md:sha256', '-in', originalFilePath, '-binary', '-outform', 'DER', - '-out', outputFilePath, x509CertificateFilePath + '-out', outputFilePath ]; let opensslProcess = spawn(opensslPath, parameters); From 4ab1d9ca343c8df1679bb9f08c90d905a5547197 Mon Sep 17 00:00:00 2001 From: Johnson Tse Date: Tue, 1 Sep 2026 10:46:09 -0400 Subject: [PATCH 3/3] [RECINF-1032] Split into multiple versions for dev guide reference --- .../byoiclient-encrypt-recording-cipherv2.js | 242 ++++++++++++++++++ .../byoiclient-encrypt-recording.js | 100 +++----- .../byoiclient-upload-recordings-cipherv2.js | 130 ++++++++++ .../byoiclient-upload-recordings.js | 1 - 4 files changed, 401 insertions(+), 72 deletions(-) create mode 100644 platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording-cipherv2.js create mode 100644 platform/integrations/byoi-integration-guide/byoiclient-upload-recordings-cipherv2.js diff --git a/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording-cipherv2.js b/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording-cipherv2.js new file mode 100644 index 0000000..b7ed13d --- /dev/null +++ b/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording-cipherv2.js @@ -0,0 +1,242 @@ + +const platformClient = require('purecloud-platform-client-v2'); +const winston = require('winston'); +const fs = require('fs'); +const path = require('path') +const {spawn} = require('child_process'); + +const clientId = process.env.GENESYS_CLOUD_CLIENT_ID; +const clientSecret = process.env.GENESYS_CLOUD_CLIENT_SECRET; + +// Set the Genesys Cloud region +const client = platformClient.ApiClient.instance; +client.setEnvironment(platformClient.PureCloudRegionHosts.us_east_1); + +// Create API instance +const recordingApi = new platformClient.RecordingApi(); + +// Use winston for logging +const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + winston.format.timestamp(), winston.format.prettyPrint() + ), + transports: [ + new winston.transports.File({ + filename: `./byoiclient-encrypt-recording.log`, + }), + // Output to console for error logs + new winston.transports.Console({ + level: 'error', + }) + ] +}); + + +/* Demonstrate the process to encrypt a recording: + * + * Login OAuth client with client ID and secret + * Fetch the most recent recording key + * Generate X509 certificate + * Encrypt the recording file using the certificate + * Catch and log any error + */ +// >> START byoi-generate-intermediate-key-cipherv2 +// Use OpenSSL 3.0.x to convert DER to RSA Public Key +async function convertDerToRSAPublicKey(opensslPath, derFileName, pemFileName) { + return new Promise((resolve, reject) => { + // openssl rsa -RSAPublicKey_in -in derFileName.der -inform DER -outform PEM -out pemFileName.pem + let parameters = [ + 'rsa', '-RSAPublicKey_in', + '-in', derFileName, + '-inform', 'DER', '-outform', 'PEM', + '-out', pemFileName + ]; + + let opensslProcess = spawn(opensslPath, parameters); + opensslProcess.on('close', (code) => { + if (code == 0) { + resolve(true); + } else { + logger.error(`OpenSSL returned ${code} when converting public key`); + opensslProcess.stdout.pipe(process.stdout); + resolve(null); + } + }); + }); +} + +// Run an OpenSSL command and return a promise that resolves to true on success +function runOpenssl(opensslPath, parameters) { + return new Promise((resolve) => { + let opensslProcess = spawn(opensslPath, parameters); + opensslProcess.on('close', (code) => { + if (code == 0) { + resolve(true); + } else { + logger.error(`OpenSSL returned ${code} for command: openssl ${parameters[0]}`); + resolve(false); + } + }); + }); +} + +// Generate an X509 certificate embedding the Genesys public key, using OpenSSL (FIPS-validated module) with SHA-256 signing +async function generateX509Certificate(opensslPath, keyId, pemFileName, certFileName) { + let throwawayKeyFile = certFileName.replace('.cert.pem', '.throwaway.key'); + let csrFile = certFileName.replace('.cert.pem', '.csr'); + + // Serial number must start with "00" and not contain dashes + let serialHex = '0x00' + keyId.replace(/-/g, ''); + + // Step 1: Generate a throwaway 2048-bit RSA private key for certificate signing + let success = await runOpenssl(opensslPath, [ + 'genpkey', '-algorithm', 'RSA', + '-pkeyopt', 'rsa_keygen_bits:2048', + '-out', throwawayKeyFile + ]); + if (!success) return false; + + // Step 2: Create a certificate signing request using the throwaway key + success = await runOpenssl(opensslPath, [ + 'req', '-new', + '-key', throwawayKeyFile, + '-subj', '/CN=recording-encryption', + '-out', csrFile + ]); + if (!success) return false; + + // Step 3: Create self-signed cert with the Genesys public key forced in, signed with SHA-256 + success = await runOpenssl(opensslPath, [ + 'x509', '-req', + '-in', csrFile, + '-force_pubkey', pemFileName, + '-signkey', throwawayKeyFile, + '-set_serial', serialHex, + '-sha256', + '-days', '1', + '-out', certFileName + ]); + + // Clean up throwaway intermediate files + try { fs.unlinkSync(throwawayKeyFile); } catch(e) { /* ignore */ } + try { fs.unlinkSync(csrFile); } catch(e) { /* ignore */ } + + return success; +} +// >> END byoi-generate-intermediate-key-cipherv2 +// >> START byoi-perform-encryption-cipherv2 +// Invoke openssl command to encrypt the file using the certificate file (FIPS 140-3 compliant: AES-256-GCM + RSA-OAEP) +async function performEncryption(opensslPath, x509CertificateFilePath, originalFilePath, outputFilePath) { + return new Promise((resolve, reject) => { + // openssl cms -encrypt -aes-256-gcm -recip f36f8c85-8922-45fa-a3f1-d197d1176340.cert.pem -keyopt rsa_padding_mode:oaep -keyopt rsa_oaep_md:sha256 -in audio.opus -binary -outform DER -out audio.opus.bin + // The recipient certificate must be supplied with -recip and must appear BEFORE -keyopt. + // -keyopt applies to the preceding recipient, so passing the certificate as a trailing + // argument instead would make OpenSSL fail with "No key specified". + // rsa_oaep_md:sha256 is required as well as rsa_padding_mode:oaep. Without it OpenSSL omits the + // RSAESOAEPparams from the CMS envelope, which means SHA-1 by default (RFC 8017), and Genesys Cloud + // will not be able to decrypt the recording. Both flags require OpenSSL 3.0 or later. + let parameters = [ + 'cms', '-encrypt', + '-aes-256-gcm', + '-recip', x509CertificateFilePath, + '-keyopt', 'rsa_padding_mode:oaep', + '-keyopt', 'rsa_oaep_md:sha256', + '-in', originalFilePath, + '-binary', '-outform', 'DER', + '-out', outputFilePath + ]; + + let opensslProcess = spawn(opensslPath, parameters); + opensslProcess.on('close', (code) => { + if (code == 0) { + resolve(true); + } else { + logger.error(`OpenSSL returned ${code} when performing encryption`); + opensslProcess.stdout.pipe(process.stdout); + resolve(false); + } + }); + }); +} +// >> END byoi-perform-encryption-cipherv2 +// Encrypt recording file (e.g. recordingFile.opus => recordingFile.opus.bin) +async function encryptRecording(recordingAudioFile) { + try { + + // Specify the path to OpenSSL executable. Please specify the absolute path if there are multiple OpenSSL versions. + // This example uses OpenSSL 3.0.7 to perform CMS encryption. Other equivalent CMS encryption tools should also work + // but these have not been tested. Run "openssl version" to get the version + const opensslPath = 'openssl'; + + // Create folder for saving temp files + let workingDir = path.join(__dirname, 'temp'); + + // Create the folders if they do not exist + if(!fs.existsSync(workingDir)) { + fs.mkdirSync(workingDir); + } +// >> START byoi-get-encryption-key-cipherv2 + // Login OAuth client + let response = await client.loginClientCredentialsGrant(clientId, clientSecret); + logger.verbose('Login successfully'); + + // Fetch the most recent recording key by specifying page 1 with a page size of 1 + // Ensure that only a single key is returned + response = await recordingApi.getRecordingRecordingkeys({ + 'pageSize': 1, + 'pageNumber': 1 + }); + logger.verbose(response); + if(response.total !== 1) { + logger.error('Failed to retrieve public recording key'); + return false; + } + + let keyId = response.entities[0].id; + let publicKey = response.entities[0].keydataSummary; +// >> END byoi-get-encryption-key-cipherv2 +// >> START byoi-encrypt-file-cipherv2 + // OpenSSL CMS encryption requires X509 certificate, so the public key needs to be converted to X509 certificate + // Save the public key to the binary DER as .der file + let derFileName = path.join(workingDir, keyId + '.der'); + let keyDer = Buffer.from(publicKey, 'base64'); + var stream = fs.createWriteStream(derFileName); + stream.write(keyDer); + + // Convert DER to RSA Public Key and save it as .pem file + let pemFileName = path.join(workingDir, keyId + '.pem'); + await convertDerToRSAPublicKey(opensslPath, derFileName, pemFileName); + + // Generate X509 certificate and save the certificate as .cert.pem file + let certFileName = path.join(workingDir, keyId + '.cert.pem'); + await generateX509Certificate(opensslPath, keyId, pemFileName, certFileName); + logger.verbose(`Generated X509 certificate file: ${certFileName}`); + + // Perform CMS encryption using the certificate file + let fileToEncrypt = path.join(__dirname, recordingAudioFile); + let outputFile = fileToEncrypt + '.bin'; + let isFileEncrypted = await performEncryption(opensslPath, certFileName, fileToEncrypt, outputFile); +// >> END byoi-encrypt-file-cipherv2 + if(isFileEncrypted) { + logger.info(`File ${fileToEncrypt} has been encrypted into ${outputFile}`); + } + else { + logger.error(`Failed to encrypt file ${fileToEncrypt}`); + } + } + catch(err) { + // Directly logging an error in winston would result in empty string + if (err instanceof Error) { + logger.error(`${err.stack || err}`); + } + // Handle failure response + else { + logger.error(err); + } + } +} + +let recordingAudioFile = 'recordingAudio.opus'; + +encryptRecording(recordingAudioFile); diff --git a/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js b/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js index 5afa074..08a17de 100644 --- a/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js +++ b/platform/integrations/byoi-integration-guide/byoiclient-encrypt-recording.js @@ -3,6 +3,7 @@ const platformClient = require('purecloud-platform-client-v2'); const winston = require('winston'); const fs = require('fs'); const path = require('path') +const forge = require('node-forge'); const {spawn} = require('child_process'); const clientId = process.env.GENESYS_CLOUD_CLIENT_ID; @@ -66,85 +67,42 @@ async function convertDerToRSAPublicKey(opensslPath, derFileName, pemFileName) { }); } -// Run an OpenSSL command and return a promise that resolves to true on success -function runOpenssl(opensslPath, parameters) { - return new Promise((resolve) => { - let opensslProcess = spawn(opensslPath, parameters); - opensslProcess.on('close', (code) => { - if (code == 0) { - resolve(true); - } else { - logger.error(`OpenSSL returned ${code} for command: openssl ${parameters[0]}`); - resolve(false); - } - }); - }); -} +// Generate a key pair and save the certificate +async function generateX509Certificate(keyId, pemFileName, certFileName) { + + // The key length must be 2048 + var keys = forge.pki.rsa.generateKeyPair(2048); + + // Replace the RSA public key + let pemBody = fs.readFileSync(pemFileName); + keys.publicKey = forge.pki.publicKeyFromPem(pemBody); + + // Generate X509 certificate with new public key + var cert = forge.pki.createCertificate(); + cert.publicKey = keys.publicKey; + + // Make sure the serialNumber starts with "00" and does not contain '-' + cert.serialNumber = '00' + keyId.replace(/-/g, ''); + + // Sign the X509 certificate + cert.sign(keys.privateKey); -// Generate an X509 certificate embedding the Genesys public key, using OpenSSL (FIPS-validated module) with SHA-256 signing -async function generateX509Certificate(opensslPath, keyId, pemFileName, certFileName) { - let throwawayKeyFile = certFileName.replace('.cert.pem', '.throwaway.key'); - let csrFile = certFileName.replace('.cert.pem', '.csr'); - - // Serial number must start with "00" and not contain dashes - let serialHex = '0x00' + keyId.replace(/-/g, ''); - - // Step 1: Generate a throwaway 2048-bit RSA private key for certificate signing - let success = await runOpenssl(opensslPath, [ - 'genpkey', '-algorithm', 'RSA', - '-pkeyopt', 'rsa_keygen_bits:2048', - '-out', throwawayKeyFile - ]); - if (!success) return false; - - // Step 2: Create a certificate signing request using the throwaway key - success = await runOpenssl(opensslPath, [ - 'req', '-new', - '-key', throwawayKeyFile, - '-subj', '/CN=recording-encryption', - '-out', csrFile - ]); - if (!success) return false; - - // Step 3: Create self-signed cert with the Genesys public key forced in, signed with SHA-256 - success = await runOpenssl(opensslPath, [ - 'x509', '-req', - '-in', csrFile, - '-force_pubkey', pemFileName, - '-signkey', throwawayKeyFile, - '-set_serial', serialHex, - '-sha256', - '-days', '1', - '-out', certFileName - ]); - - // Clean up throwaway intermediate files - try { fs.unlinkSync(throwawayKeyFile); } catch(e) { /* ignore */ } - try { fs.unlinkSync(csrFile); } catch(e) { /* ignore */ } - - return success; + // Save the certificate pem file + let certificatePem = forge.pki.certificateToPem(cert); + var stream = fs.createWriteStream(certFileName); + stream.write(certificatePem); } // >> END byoi-generate-intermediate-key // >> START byoi-perform-encryption -// Invoke openssl command to encrypt the file using the certificate file (FIPS 140-3 compliant: AES-256-GCM + RSA-OAEP) +// Invoke openssl command to encrypt the file using the certificate file async function performEncryption(opensslPath, x509CertificateFilePath, originalFilePath, outputFilePath) { return new Promise((resolve, reject) => { - // openssl cms -encrypt -aes-256-gcm -recip f36f8c85-8922-45fa-a3f1-d197d1176340.cert.pem -keyopt rsa_padding_mode:oaep -keyopt rsa_oaep_md:sha256 -in audio.opus -binary -outform DER -out audio.opus.bin - // The recipient certificate must be supplied with -recip and must appear BEFORE -keyopt. - // -keyopt applies to the preceding recipient, so passing the certificate as a trailing - // argument instead would make OpenSSL fail with "No key specified". - // rsa_oaep_md:sha256 is required as well as rsa_padding_mode:oaep. Without it OpenSSL omits the - // RSAESOAEPparams from the CMS envelope, which means SHA-1 by default (RFC 8017), and Genesys Cloud - // will not be able to decrypt the recording. Both flags require OpenSSL 3.0 or later. + // openssl cms -encrypt -aes256 -in audio.opus -binary -outform DER -out audio.opus.bin f36f8c85-8922-45fa-a3f1-d197d1176340.cert.pem let parameters = [ - 'cms', '-encrypt', - '-aes-256-gcm', - '-recip', x509CertificateFilePath, - '-keyopt', 'rsa_padding_mode:oaep', - '-keyopt', 'rsa_oaep_md:sha256', + 'cms', '-encrypt', '-aes256', '-in', originalFilePath, '-binary', '-outform', 'DER', - '-out', outputFilePath + '-out', outputFilePath, x509CertificateFilePath ]; let opensslProcess = spawn(opensslPath, parameters); @@ -210,7 +168,7 @@ async function encryptRecording(recordingAudioFile) { // Generate X509 certificate and save the certificate as .cert.pem file let certFileName = path.join(workingDir, keyId + '.cert.pem'); - await generateX509Certificate(opensslPath, keyId, pemFileName, certFileName); + generateX509Certificate(keyId, pemFileName, certFileName); logger.verbose(`Generated X509 certificate file: ${certFileName}`); // Perform CMS encryption using the certificate file diff --git a/platform/integrations/byoi-integration-guide/byoiclient-upload-recordings-cipherv2.js b/platform/integrations/byoi-integration-guide/byoiclient-upload-recordings-cipherv2.js new file mode 100644 index 0000000..5a588b9 --- /dev/null +++ b/platform/integrations/byoi-integration-guide/byoiclient-upload-recordings-cipherv2.js @@ -0,0 +1,130 @@ +const platformClient = require('purecloud-platform-client-v2'); +const winston = require('winston'); + +const fetch = require('node-fetch'); +const fs = require('fs'); +const md5 = require('md5-file'); +const archiver = require('archiver'); + +const clientId = process.env.GENESYS_CLOUD_CLIENT_ID; +const clientSecret = process.env.GENESYS_CLOUD_CLIENT_SECRET; + +// Set the Genesys Cloud region +const client = platformClient.ApiClient.instance; +client.setEnvironment(platformClient.PureCloudRegionHosts.us_east_1); + +// Create API instance +const uploadsApi = new platformClient.UploadsApi(); + +// Use winston for logging +const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + winston.format.timestamp(), winston.format.prettyPrint() + ), + transports: [ + new winston.transports.File({ + filename: `./byoiclient-upload-recordings.log`, + }), + // Output to console for error logs + new winston.transports.Console({ + level: 'error', + }) + ] +}); + + +/* Demonstrate the process to upload recordings: + * + * Login OAuth client with client ID and secret + * Create zip with the recording and metadata file + * Load the zip file and create the MD5 hash + * Obtain a presigned URL + * Upload the file to the presigned URL + * Catch and log any error + */ + +async function uploadRecordings(fileName) { + try { + let response = await client.loginClientCredentialsGrant(clientId, clientSecret); + logger.verbose('Login successfully'); + +// >> START byoi-zip-file-cipherv2 + var output = fs.createWriteStream(fileName); + var archive = archiver('zip', { + store: true, + // uploaded zip file must be uncompressed, setting compression level to 0 + zlib: { level: 0 } + }); + + archive.on('error', function(err) { + logger.error(err); + }); + + archive.pipe(output); + + // the recording and metadata file must be stored at the root level in the zip file + archive.file('./myfile.opus.bin', {name: 'myfile.opus.bin'}); + archive.file('./metadata.json', {name: 'metadata.json'}); + + // wait for streams to complete + archive.finalize(); +// >> END byoi-zip-file-cipherv2 +// >> START byoi-get-presigned-url-cipherv2 + // Get base64-encoded 128-bit MD5 digest of the file content + let md5sum = await md5(fileName); + let md5Base64 = Buffer.from(md5sum, 'hex').toString('base64'); + logger.verbose(md5Base64); + + // Get the presigned URL + // signedUrlTimeoutSeconds is optional for the number of seconds the presigned URL is valid for (1-604800). + // The default value is 600 seconds if it is not provided + response = await uploadsApi.postUploadsRecordings({ + 'fileName': fileName, + 'contentMd5': md5Base64, + 'signedUrlTimeoutSeconds': 600 + }); + logger.info(response); +// >> END byoi-get-presigned-url-cipherv2 + // Get the presigned URL from the response + let presignedUploadUrl = response.url; + // Save the headers in returned response for the following upload + let responseHeaders = response.headers; +// >> START byoi-upload-file-cipherv2 + // Upload the file to the presigned URL + const fileContent = fs.readFileSync(fileName); + logger.info(`Upload ${fileName} to the presigned URL`) + response = await fetch(presignedUploadUrl, { + method: 'PUT', + headers: responseHeaders, + body: fileContent + }); + let responseBody = await response.text(); + logger.info(responseBody); + return responseBody; +// >> END byoi-upload-file-cipherv2 + } + catch(err) { + // Directly logging an error in winston would result in empty string + if (err instanceof Error) { + logger.error(`${err.stack || err}`); + } + // Handle failure response + else { + logger.error(err); + } + } +} + +// The recording file to be uploaded +/* Note: The recording file and metadata.json shall be at the top level of the zip file. + Zipping up the folder that contains the recording file and the metadata.json would cause a validation failure + + recordingExample.zip + |--- audioExampleRecording.opus.bin + |--- metadata.json + */ + +const fileName = 'recordingExample.zip'; + +uploadRecordings(fileName); diff --git a/platform/integrations/byoi-integration-guide/byoiclient-upload-recordings.js b/platform/integrations/byoi-integration-guide/byoiclient-upload-recordings.js index 26a41c1..05086c6 100644 --- a/platform/integrations/byoi-integration-guide/byoiclient-upload-recordings.js +++ b/platform/integrations/byoi-integration-guide/byoiclient-upload-recordings.js @@ -4,7 +4,6 @@ const winston = require('winston'); const fetch = require('node-fetch'); const fs = require('fs'); const md5 = require('md5-file'); -const fs = require('fs'); const archiver = require('archiver'); const clientId = process.env.GENESYS_CLOUD_CLIENT_ID;