diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index b260cb6ff..862cfa051 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -20,7 +20,6 @@ runs: ${{ runner.os }}-yarn- - name: Install dependencies - if: steps.yarn-cache.outputs.cache-hit != 'true' run: | yarn install --frozen-lockfile shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab69a22fc..f5e6c8ea4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,8 @@ name: Build API Example -run-name: ${{ github.actor }} triggered this job + on: workflow_dispatch: + jobs: build-android: runs-on: ubuntu-latest @@ -13,8 +14,7 @@ jobs: - name: SetupExample run: | - yarn install --frozen-lockfile - working-directory: example + yarn example install --frozen-lockfile - name: Modify APP ID run: | @@ -31,7 +31,7 @@ jobs: with: name: AgoraRtcExample path: | - example/android/**/*.apk + example/android/app/build/outputs/apk/release/*.apk build-ios: runs-on: macos-latest @@ -43,14 +43,23 @@ jobs: - uses: actions/cache@v3 with: - path: example/ios/Pods + path: | + **/Pods key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} restore-keys: | - ${{ runner.os }}-pods- + ${{ runner.os }}-pods + + - uses: hendrikmuhs/ccache-action@v1.2 + with: + max-size: 1024M + key: ${{ runner.os }}-ccache + restore-keys: | + ${{ runner.os }}-ccache - name: SetupExample run: | - yarn + yarn example install --frozen-lockfile + yarn example pods - name: Modify APP ID run: | @@ -87,10 +96,6 @@ jobs: mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles - - uses: hendrikmuhs/ccache-action@v1.2 - with: - max-size: 5G - - name: Fastlane build env: CC: clang diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..401f83d5d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + - 3.x + pull_request: + branches: + - main + - 3.x + - release/* + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Lint files + run: yarn lint + + - name: Typecheck files + run: | + yarn example install --frozen-lockfile + yarn typescript + + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Build package + run: yarn prepare diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml new file mode 100644 index 000000000..dc9efd47a --- /dev/null +++ b/.github/workflows/gitleaks.yml @@ -0,0 +1,21 @@ +name: gitleaks + +on: + pull_request: + push: + workflow_dispatch: + schedule: + - cron: "0 4 * * *" # run once a day at 4 AM + +jobs: + scan: + name: gitleaks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # Only required for Organizations, not personal accounts. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dcb132638..e72eccf84 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,5 +1,5 @@ name: Publish to NPM -run-name: ${{ github.actor }} triggered this job + on: workflow_dispatch: inputs: @@ -22,6 +22,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + token: ${{ secrets.GH_TOKEN }} - name: Setup uses: ./.github/actions/setup @@ -30,12 +31,14 @@ jobs: run: | npm set "//registry.npmjs.org/:_authToken" ${{ secrets.NPM_TOKEN }} - - if: ${{ inputs.dry-run }} + - name: Dry Run Release + if: ${{ inputs.dry-run }} run: | yarn release ${{ inputs.increment }} -d --ci - - if: ${{ !inputs.dry-run }} + - name: NPM Publish + if: ${{ !inputs.dry-run }} run: | git config --global user.email "${{ secrets.GIT_EMAIL }}" git config --global user.name "${{ secrets.GIT_USERNAME }}" - yarn release ${{ inputs.increment }} --ci + yarn release ${{ inputs.increment }} --ci --npm.allowSameVersion diff --git a/android/src/main/java/io/agora/rtc/base/MediaObserver.kt b/android/src/main/java/io/agora/rtc/base/MediaObserver.kt index b763cedeb..3c34cc2fa 100644 --- a/android/src/main/java/io/agora/rtc/base/MediaObserver.kt +++ b/android/src/main/java/io/agora/rtc/base/MediaObserver.kt @@ -1,17 +1,18 @@ package io.agora.rtc.base +import android.util.Base64 import androidx.annotation.IntRange import io.agora.rtc.IMetadataObserver -import java.util.* +import java.util.Collections import java.util.concurrent.atomic.AtomicInteger class MediaObserver( private val emit: (data: Map?) -> Unit ) : IMetadataObserver { private var maxMetadataSize = AtomicInteger(1024) - private var metadataList = Collections.synchronizedList(mutableListOf()) + private var metadataList = Collections.synchronizedList(mutableListOf()) - fun addMetadata(metadata: String) { + fun addMetadata(metadata: ByteArray) { metadataList.add(metadata) } @@ -21,7 +22,7 @@ class MediaObserver( override fun onReadyToSendMetadata(timeStampMs: Long): ByteArray? { if (metadataList.size > 0) { - return metadataList.removeAt(0).toByteArray() + return metadataList.removeAt(0) } return null } @@ -33,7 +34,9 @@ class MediaObserver( override fun onMetadataReceived(buffer: ByteArray, uid: Int, timeStampMs: Long) { emit( hashMapOf( - "data" to arrayListOf(String(buffer), uid.toUInt().toLong(), timeStampMs) + "data" to arrayListOf( + Base64.encodeToString(buffer, Base64.DEFAULT), uid.toUInt().toLong(), timeStampMs + ) ) ) } diff --git a/android/src/main/java/io/agora/rtc/base/RtcChannel.kt b/android/src/main/java/io/agora/rtc/base/RtcChannel.kt index 3e3cbc48b..022724f9b 100644 --- a/android/src/main/java/io/agora/rtc/base/RtcChannel.kt +++ b/android/src/main/java/io/agora/rtc/base/RtcChannel.kt @@ -1,5 +1,6 @@ package io.agora.rtc.base +import android.util.Base64 import io.agora.rtc.IMetadataObserver import io.agora.rtc.RtcChannel import io.agora.rtc.RtcEngine @@ -479,7 +480,7 @@ class RtcChannelManager( override fun sendMetadata(params: Map, callback: Callback) { callback.resolve(mediaObserverMap[params["channelId"] as String]) { - it.addMetadata(params["metadata"] as String) + it.addMetadata(Base64.decode(params["metadata"] as String, Base64.DEFAULT)) Unit } } @@ -541,7 +542,7 @@ class RtcChannelManager( callback.code( RtcChannelManager[params["channelId"] as String]?.sendStreamMessage( (params["streamId"] as Number).toInt(), - (params["message"] as String).toByteArray() + Base64.decode((params["message"] as String), Base64.DEFAULT) ) ) } diff --git a/android/src/main/java/io/agora/rtc/base/RtcChannelEvent.kt b/android/src/main/java/io/agora/rtc/base/RtcChannelEvent.kt index 058745d4b..e4411af63 100644 --- a/android/src/main/java/io/agora/rtc/base/RtcChannelEvent.kt +++ b/android/src/main/java/io/agora/rtc/base/RtcChannelEvent.kt @@ -1,5 +1,6 @@ package io.agora.rtc.base +import android.util.Base64 import androidx.annotation.IntRange import io.agora.rtc.IRtcChannelEventHandler import io.agora.rtc.IRtcEngineEventHandler @@ -319,7 +320,8 @@ class RtcChannelEventHandler( rtcChannel, uid.toUInt().toLong(), streamId, - data?.let { String(it, Charsets.UTF_8) }) + Base64.encodeToString(data, Base64.DEFAULT) + ) } override fun onStreamMessageError( diff --git a/android/src/main/java/io/agora/rtc/base/RtcEngine.kt b/android/src/main/java/io/agora/rtc/base/RtcEngine.kt index 936ba444d..6fd47804b 100644 --- a/android/src/main/java/io/agora/rtc/base/RtcEngine.kt +++ b/android/src/main/java/io/agora/rtc/base/RtcEngine.kt @@ -1,6 +1,7 @@ package io.agora.rtc.base import android.content.Context +import android.util.Base64 import io.agora.rtc.* import io.agora.rtc.internal.EncryptionConfig import io.agora.rtc.mediaio.AgoraDefaultSource @@ -1369,7 +1370,7 @@ open class RtcEngineManager( override fun sendMetadata(params: Map, callback: Callback) { callback.resolve(mediaObserver) { - it.addMetadata(params["metadata"] as String) + it.addMetadata(Base64.decode(params["metadata"] as String, Base64.DEFAULT)) } } @@ -1526,7 +1527,7 @@ open class RtcEngineManager( callback.code( engine?.sendStreamMessage( (params["streamId"] as Number).toInt(), - (params["message"] as String).toByteArray() + Base64.decode((params["message"] as String), Base64.DEFAULT) ) ) } diff --git a/android/src/main/java/io/agora/rtc/base/RtcEngineEvent.kt b/android/src/main/java/io/agora/rtc/base/RtcEngineEvent.kt index 15bffff55..62b9c2e14 100644 --- a/android/src/main/java/io/agora/rtc/base/RtcEngineEvent.kt +++ b/android/src/main/java/io/agora/rtc/base/RtcEngineEvent.kt @@ -1,6 +1,7 @@ package io.agora.rtc.base import android.graphics.Rect +import android.util.Base64 import androidx.annotation.IntRange import io.agora.rtc.AgoraMediaRecorder import io.agora.rtc.Constants @@ -478,7 +479,8 @@ class RtcEngineEventHandler( RtcEngineEvents.StreamMessage, uid.toUInt().toLong(), streamId, - data?.let { String(it, Charsets.UTF_8) }) + Base64.encodeToString(data, Base64.DEFAULT) + ) } override fun onStreamMessageError( diff --git a/example/src/examples/advanced/StreamMessage/StreamMessage.tsx b/example/src/examples/advanced/StreamMessage/StreamMessage.tsx index cdfdfffa5..f29eb3eb5 100644 --- a/example/src/examples/advanced/StreamMessage/StreamMessage.tsx +++ b/example/src/examples/advanced/StreamMessage/StreamMessage.tsx @@ -10,6 +10,7 @@ import { View, } from 'react-native'; +import { Buffer } from 'buffer'; import RtcEngine, { ChannelProfile, ClientRole, @@ -102,12 +103,16 @@ export default class StreamMessage extends Component<{}, State, any> { }); this._engine?.addListener('StreamMessage', (uid, streamId, data) => { console.info('UserOffline', uid, streamId, data); - Alert.alert(`Receive from uid:${uid}`, `StreamId ${streamId}:${data}`, [ - { - text: 'Ok', - onPress: () => {}, - }, - ]); + Alert.alert( + `Receive from uid:${uid}`, + `StreamId ${streamId}:${data.toString()}`, + [ + { + text: 'Ok', + onPress: () => {}, + }, + ] + ); }); this._engine?.addListener( 'StreamMessageError', @@ -210,7 +215,7 @@ export default class StreamMessage extends Component<{}, State, any> { new DataStreamConfig(true, true) ); - await this._engine?.sendStreamMessage(streamId!, message); + await this._engine?.sendStreamMessage(streamId!, Buffer.from(message)); this.setState({ message: '' }); }; } diff --git a/ios/RCTAgora/Base/MediaObserver.swift b/ios/RCTAgora/Base/MediaObserver.swift index 514fb21d0..e49bc236c 100644 --- a/ios/RCTAgora/Base/MediaObserver.swift +++ b/ios/RCTAgora/Base/MediaObserver.swift @@ -12,13 +12,13 @@ import Foundation class MediaObserver: NSObject { private var emitter: (_ data: [String: Any?]?) -> Void private var maxMetadataSize = 1024 - private var metadataList = [String]() + private var metadataList = [Data]() init(_ emitter: @escaping (_ data: [String: Any?]?) -> Void) { self.emitter = emitter } - func addMetadata(_ metadata: String) { + func addMetadata(_ metadata: Data) { metadataList.append(metadata) } @@ -34,7 +34,7 @@ extension MediaObserver: AgoraMediaMetadataDataSource { func readyToSendMetadata(atTimestamp _: TimeInterval) -> Data? { if metadataList.count > 0 { - return metadataList.remove(at: 0).data(using: .utf8) + return metadataList.remove(at: 0) } return nil } @@ -43,7 +43,7 @@ extension MediaObserver: AgoraMediaMetadataDataSource { extension MediaObserver: AgoraMediaMetadataDelegate { func receiveMetadata(_ data: Data, fromUser uid: Int, atTimestamp timestamp: TimeInterval) { emitter([ - "data": [String(data: data, encoding: .utf8) ?? "", uid, timestamp], + "data": [data.base64EncodedString(options: .endLineWithLineFeed), uid, timestamp], ]) } } diff --git a/ios/RCTAgora/Base/RtcChannel.swift b/ios/RCTAgora/Base/RtcChannel.swift index 3062b6b94..74f53721f 100644 --- a/ios/RCTAgora/Base/RtcChannel.swift +++ b/ios/RCTAgora/Base/RtcChannel.swift @@ -337,7 +337,7 @@ class RtcChannelManager: NSObject, RtcChannelInterface { @objc func sendMetadata(_ params: NSDictionary, _ callback: Callback) { callback.resolve(mediaObserverMap[params["channelId"] as! String]) { - $0.addMetadata(params["metadata"] as! String) + $0.addMetadata(Data(base64Encoded: (params["metadata"] as! String), options: .ignoreUnknownCharacters)!) } } @@ -386,7 +386,7 @@ class RtcChannelManager: NSObject, RtcChannelInterface { } @objc func sendStreamMessage(_ params: NSDictionary, _ callback: Callback) { - callback.code(self[params["channelId"] as! String]?.sendStreamMessage((params["streamId"] as! NSNumber).intValue, data: (params["message"] as! String).data(using: .utf8)!)) + callback.code(self[params["channelId"] as! String]?.sendStreamMessage((params["streamId"] as! NSNumber).intValue, data: Data(base64Encoded: (params["message"] as! String), options: .ignoreUnknownCharacters)!)) } @objc func enableRemoteSuperResolution(_ params: NSDictionary, _ callback: Callback) { diff --git a/ios/RCTAgora/Base/RtcChannelEvent.swift b/ios/RCTAgora/Base/RtcChannelEvent.swift index 2dfb950a4..b2718de2d 100644 --- a/ios/RCTAgora/Base/RtcChannelEvent.swift +++ b/ios/RCTAgora/Base/RtcChannelEvent.swift @@ -214,7 +214,7 @@ extension RtcChannelEventHandler: AgoraRtcChannelDelegate { } public func rtcChannel(_ rtcChannel: AgoraRtcChannel, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) { - callback(RtcChannelEvents.StreamMessage, rtcChannel, uid, streamId, String(data: data, encoding: .utf8)) + callback(RtcChannelEvents.StreamMessage, rtcChannel, uid, streamId, data.base64EncodedString(options: .endLineWithLineFeed)) } public func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOccurStreamMessageErrorFromUid uid: UInt, streamId: Int, error: Int, missed: Int, cached: Int) { diff --git a/ios/RCTAgora/Base/RtcEngine.swift b/ios/RCTAgora/Base/RtcEngine.swift index e194ca11c..624264d07 100644 --- a/ios/RCTAgora/Base/RtcEngine.swift +++ b/ios/RCTAgora/Base/RtcEngine.swift @@ -1061,7 +1061,7 @@ class RtcEngineManager: NSObject, RtcEngineInterface { @objc func sendMetadata(_ params: NSDictionary, _ callback: Callback) { callback.resolve(mediaObserver) { - $0.addMetadata(params["metadata"] as! String) + $0.addMetadata(Data(base64Encoded: (params["metadata"] as! String), options: .ignoreUnknownCharacters)!) } } @@ -1209,7 +1209,7 @@ class RtcEngineManager: NSObject, RtcEngineInterface { } @objc func sendStreamMessage(_ params: NSDictionary, _ callback: Callback) { - callback.code(engine?.sendStreamMessage((params["streamId"] as! NSNumber).intValue, data: (params["message"] as! String).data(using: .utf8)!)) + callback.code(engine?.sendStreamMessage((params["streamId"] as! NSNumber).intValue, data: Data(base64Encoded: (params["message"] as! String), options: .ignoreUnknownCharacters)!)) } @objc func setVoiceBeautifierParameters(_ params: NSDictionary, _ callback: Callback) { diff --git a/ios/RCTAgora/Base/RtcEngineEvent.swift b/ios/RCTAgora/Base/RtcEngineEvent.swift index 413f5a514..811604fba 100644 --- a/ios/RCTAgora/Base/RtcEngineEvent.swift +++ b/ios/RCTAgora/Base/RtcEngineEvent.swift @@ -414,7 +414,7 @@ extension RtcEngineEventHandler: AgoraRtcEngineDelegate, AgoraMediaRecorderDeleg } public func rtcEngine(_: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) { - callback(RtcEngineEvents.StreamMessage, uid, streamId, String(data: data, encoding: .utf8)) + callback(RtcEngineEvents.StreamMessage, uid, streamId, data.base64EncodedString(options: .endLineWithLineFeed)) } public func rtcEngine(_: AgoraRtcEngineKit, didOccurStreamMessageErrorFromUid uid: UInt, streamId: Int, error: Int, missed: Int, cached: Int) { diff --git a/package.json b/package.json index 4c7103c2f..1925c9a7a 100644 --- a/package.json +++ b/package.json @@ -169,5 +169,8 @@ "commitizen": { "path": "./node_modules/cz-conventional-changelog" } + }, + "dependencies": { + "buffer": "^6.0.3" } } diff --git a/src/common/RtcChannel.native.ts b/src/common/RtcChannel.native.ts index 709ca03fb..f7e650748 100644 --- a/src/common/RtcChannel.native.ts +++ b/src/common/RtcChannel.native.ts @@ -3,6 +3,8 @@ import { NativeEventEmitter, NativeModules, } from 'react-native'; +import { Buffer } from 'buffer'; +import base64 from 'base64-js'; import type { ChannelMediaOptions, @@ -145,6 +147,11 @@ export default class RtcChannel implements RtcChannelInterface { const callback = (res: any) => { const { channelId, data } = res; if (channelId === this.channelId) { + if (event === 'StreamMessage') { + data[3] = Buffer.from(data[3], 'base64'); + } else if (event === 'MetadataReceived') { + data[0] = Buffer.from(data[0], 'base64'); + } // @ts-ignore listener(...data); } @@ -846,8 +853,10 @@ export default class RtcChannel implements RtcChannelInterface { * * @param metadata The metadata to be sent. */ - sendMetadata(metadata: string): Promise { - return this._callMethod('sendMetadata', { metadata }); + sendMetadata(metadata: Uint8Array): Promise { + return this._callMethod('sendMetadata', { + metadata: base64.fromByteArray(metadata), + }); } /** @@ -1067,8 +1076,11 @@ export default class RtcChannel implements RtcChannelInterface { * * @param message The message data. */ - sendStreamMessage(streamId: number, message: string): Promise { - return this._callMethod('sendStreamMessage', { streamId, message }); + sendStreamMessage(streamId: number, message: Uint8Array): Promise { + return this._callMethod('sendStreamMessage', { + streamId, + message: base64.fromByteArray(message), + }); } /** @@ -1438,7 +1450,7 @@ interface RtcMediaMetadataInterface { setMaxMetadataSize(size: number): Promise; - sendMetadata(metadata: string): Promise; + sendMetadata(metadata: Uint8Array): Promise; } /** @@ -1472,5 +1484,5 @@ interface RtcStreamMessageInterface { createDataStreamWithConfig(config: DataStreamConfig): Promise; - sendStreamMessage(streamId: number, message: string): Promise; + sendStreamMessage(streamId: number, message: Uint8Array): Promise; } diff --git a/src/common/RtcEngine.native.ts b/src/common/RtcEngine.native.ts index 48590c775..3cb5959cb 100644 --- a/src/common/RtcEngine.native.ts +++ b/src/common/RtcEngine.native.ts @@ -3,6 +3,8 @@ import { NativeEventEmitter, NativeModules, } from 'react-native'; +import { Buffer } from 'buffer'; +import base64 from 'base64-js'; import { AudioRecordingConfiguration, @@ -301,6 +303,11 @@ export default class RtcEngine implements RtcEngineInterface { const callback = (res: any) => { const { channelId, data } = res; if (channelId === undefined) { + if (event === 'StreamMessage') { + data[2] = Buffer.from(data[2], 'base64'); + } else if (event === 'MetadataReceived') { + data[0] = Buffer.from(data[0], 'base64'); + } // @ts-ignore listener(...data); } @@ -2494,8 +2501,10 @@ export default class RtcEngine implements RtcEngineInterface { * * @param metadata The metadata to be sent. */ - sendMetadata(metadata: string): Promise { - return RtcEngine._callMethod('sendMetadata', { metadata }); + sendMetadata(metadata: Uint8Array): Promise { + return RtcEngine._callMethod('sendMetadata', { + metadata: base64.fromByteArray(metadata), + }); } /** @@ -3111,8 +3120,11 @@ export default class RtcEngine implements RtcEngineInterface { * @param streamId ID of the sent data stream returned by the [`createDataStream`]{@link createDataStream} method. * @param message Sent data. */ - sendStreamMessage(streamId: number, message: string): Promise { - return RtcEngine._callMethod('sendStreamMessage', { streamId, message }); + sendStreamMessage(streamId: number, message: Uint8Array): Promise { + return RtcEngine._callMethod('sendStreamMessage', { + streamId, + message: base64.fromByteArray(message), + }); } /** @@ -4632,7 +4644,7 @@ interface RtcMediaMetadataInterface { setMaxMetadataSize(size: number): Promise; - sendMetadata(metadata: string): Promise; + sendMetadata(metadata: Uint8Array): Promise; } /** @@ -4746,7 +4758,7 @@ interface RtcStreamMessageInterface { createDataStreamWithConfig(config: DataStreamConfig): Promise; - sendStreamMessage(streamId: number, message: string): Promise; + sendStreamMessage(streamId: number, message: Uint8Array): Promise; } /** diff --git a/src/common/RtcEvents.ts b/src/common/RtcEvents.ts index 55af8c269..8dd7ab639 100644 --- a/src/common/RtcEvents.ts +++ b/src/common/RtcEvents.ts @@ -365,7 +365,7 @@ export type StreamMessageCallback = * @param streamId Stream ID. * @param data Data received by the local user. */ - (uid: number, streamId: number, data: string) => void; + (uid: number, streamId: number, data: Uint8Array) => void; export type StreamMessageErrorCallback = /** * @param uid User ID of the remote user sending the data stream. diff --git a/yarn.lock b/yarn.lock index ba243e6c0..58f5eed13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2924,7 +2924,7 @@ buffer@^5.5.0: buffer@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: base64-js "^1.3.1"