@cap-kit/ssl-pinning 8.0.0-next.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/Package.swift +1 -1
  2. package/README.md +232 -372
  3. package/android/src/main/java/io/capkit/sslpinning/SSLPinningConfig.kt +48 -3
  4. package/android/src/main/java/io/capkit/sslpinning/SSLPinningError.kt +40 -0
  5. package/android/src/main/java/io/capkit/sslpinning/SSLPinningImpl.kt +137 -90
  6. package/android/src/main/java/io/capkit/sslpinning/SSLPinningPlugin.kt +146 -29
  7. package/android/src/main/java/io/capkit/sslpinning/utils/SSLPinningLogger.kt +30 -38
  8. package/android/src/main/java/io/capkit/sslpinning/utils/SSLPinningUtils.kt +25 -9
  9. package/dist/docs.json +29 -204
  10. package/dist/esm/definitions.d.ts +74 -177
  11. package/dist/esm/definitions.js +12 -6
  12. package/dist/esm/definitions.js.map +1 -1
  13. package/dist/esm/web.d.ts +9 -22
  14. package/dist/esm/web.js +5 -5
  15. package/dist/esm/web.js.map +1 -1
  16. package/dist/plugin.cjs.js +17 -11
  17. package/dist/plugin.cjs.js.map +1 -1
  18. package/dist/plugin.js +17 -11
  19. package/dist/plugin.js.map +1 -1
  20. package/ios/Sources/SSLPinningPlugin/SSLPinningConfig.swift +45 -30
  21. package/ios/Sources/SSLPinningPlugin/SSLPinningDelegate.swift +83 -26
  22. package/ios/Sources/SSLPinningPlugin/SSLPinningError.swift +49 -0
  23. package/ios/Sources/SSLPinningPlugin/SSLPinningImpl.swift +94 -64
  24. package/ios/Sources/SSLPinningPlugin/SSLPinningPlugin.swift +121 -50
  25. package/ios/Sources/SSLPinningPlugin/Utils/SSLPinningLogger.swift +28 -16
  26. package/ios/Sources/SSLPinningPlugin/Utils/SSLPinningUtils.swift +55 -18
  27. package/ios/Sources/SSLPinningPlugin/Version.swift +2 -2
  28. package/package.json +5 -6
@@ -1,22 +1,67 @@
1
1
  package io.capkit.sslpinning
2
2
 
3
+ import android.content.Context
3
4
  import com.getcapacitor.Plugin
4
5
 
6
+ /**
7
+ * Plugin configuration container.
8
+ *
9
+ * This class is responsible for reading and exposing
10
+ * static configuration values defined under the
11
+ * `SSLPinning` key in capacitor.config.ts.
12
+ *
13
+ * Configuration rules:
14
+ * - Read once during plugin initialization
15
+ * - Treated as immutable runtime input
16
+ * - Accessible only from native code
17
+ */
5
18
  class SSLPinningConfig(plugin: Plugin) {
19
+ /**
20
+ * Android application context.
21
+ * Exposed for native components that may require it.
22
+ */
23
+ val context: Context = plugin.context
24
+
25
+ /**
26
+ * Enables verbose native logging.
27
+ *
28
+ * When enabled, additional debug information
29
+ * is printed to Logcat.
30
+ *
31
+ * Default: false
32
+ */
6
33
  val verboseLogging: Boolean
34
+
35
+ /**
36
+ * Default SHA-256 fingerprint used by checkCertificate()
37
+ * when no fingerprint is provided at runtime.
38
+ */
7
39
  val fingerprint: String?
40
+
41
+ /**
42
+ * Default SHA-256 fingerprints used by checkCertificates()
43
+ * when no fingerprints are provided at runtime.
44
+ */
8
45
  val fingerprints: List<String>
9
46
 
10
47
  init {
11
48
  val config = plugin.getConfig()
12
49
 
13
- verboseLogging = config.getBoolean("verboseLogging", false)
50
+ // Verbose logging flag
51
+ verboseLogging =
52
+ config.getBoolean("verboseLogging", false)
14
53
 
54
+ // Single fingerprint (optional)
15
55
  val fp = config.getString("fingerprint")
16
- fingerprint = if (fp.isNullOrBlank()) null else fp
56
+ fingerprint =
57
+ if (!fp.isNullOrBlank()) fp else null
17
58
 
59
+ // Multiple fingerprints (optional)
18
60
  fingerprints =
19
- config.getArray("fingerprints")?.toList()?.mapNotNull { it as? String }
61
+ config.getArray("fingerprints")
62
+ ?.toList()
63
+ ?.mapNotNull { it as? String }
64
+ ?.filter { it.isNotBlank() }
20
65
  ?: emptyList()
21
66
  }
22
67
  }
@@ -0,0 +1,40 @@
1
+ package io.capkit.sslpinning
2
+
3
+ /**
4
+ * Native error model for the SSLPinning plugin (Android).
5
+ *
6
+ * Architectural rules:
7
+ * - Must NOT reference Capacitor APIs
8
+ * - Must NOT reference JavaScript
9
+ * - Must be throwable from the Impl layer
10
+ * - Mapping to JS-facing error codes happens ONLY in the Plugin layer
11
+ */
12
+ sealed class SSLPinningError(
13
+ message: String,
14
+ ) : Throwable(message) {
15
+ /**
16
+ * Feature or capability is not available
17
+ * due to device or configuration limitations.
18
+ */
19
+ class Unavailable(message: String) :
20
+ SSLPinningError(message)
21
+
22
+ /**
23
+ * Required permission was denied or not granted.
24
+ */
25
+ class PermissionDenied(message: String) :
26
+ SSLPinningError(message)
27
+
28
+ /**
29
+ * Plugin failed to initialize or perform
30
+ * a required operation.
31
+ */
32
+ class InitFailed(message: String) :
33
+ SSLPinningError(message)
34
+
35
+ /**
36
+ * Invalid or unsupported input was provided.
37
+ */
38
+ class UnknownType(message: String) :
39
+ SSLPinningError(message)
40
+ }
@@ -1,5 +1,6 @@
1
1
  package io.capkit.sslpinning
2
2
 
3
+ import android.content.Context
3
4
  import io.capkit.sslpinning.utils.SSLPinningLogger
4
5
  import io.capkit.sslpinning.utils.SSLPinningUtils
5
6
  import java.net.URL
@@ -9,157 +10,191 @@ import javax.net.ssl.SSLContext
9
10
  import javax.net.ssl.TrustManager
10
11
  import javax.net.ssl.X509TrustManager
11
12
 
13
+ /**
14
+ * Native Android implementation for the SSLPinning plugin.
15
+ *
16
+ * Responsibilities:
17
+ * - Perform platform-specific SSL pinning logic
18
+ * - Interact with system networking APIs
19
+ * - Throw typed SSLPinningError values on failure
20
+ *
21
+ * Forbidden:
22
+ * - Accessing PluginCall
23
+ * - Referencing Capacitor APIs
24
+ * - Constructing JavaScript payloads
25
+ */
12
26
  class SSLPinningImpl(
13
- private val config: SSLPinningConfig,
27
+ private val context: Context,
14
28
  ) {
15
- // ---- Single fingerprint ----
29
+ /**
30
+ * Cached plugin configuration.
31
+ * Injected once during plugin initialization.
32
+ */
33
+ private lateinit var config: SSLPinningConfig
16
34
 
17
35
  /**
18
- * Validates the SSL certificate of a HTTPS endpoint using a single SHA-256 fingerprint.
19
- *
20
- * This method:
21
- * - Opens a TLS connection to the given URL
22
- * - Extracts the leaf certificate presented by the server
23
- * - Computes its SHA-256 fingerprint
24
- * - Compares it against the provided or configured fingerprint
36
+ * Applies plugin configuration.
25
37
  *
26
- * NOTE:
27
- * - No HTTP request body is sent
28
- * - The certificate trust chain is NOT validated
29
- * - Only the leaf certificate fingerprint is checked
38
+ * This method MUST be called exactly once from the plugin's load() method.
39
+ * It translates static configuration into runtime behavior
40
+ * (e.g. enabling verbose logging).
41
+ */
42
+ fun updateConfig(newConfig: SSLPinningConfig) {
43
+ this.config = newConfig
44
+ SSLPinningLogger.verbose = newConfig.verboseLogging
45
+ SSLPinningLogger.debug(
46
+ "Configuration applied. Verbose logging:",
47
+ newConfig.verboseLogging.toString(),
48
+ )
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Single fingerprint
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /**
56
+ * Validates the SSL certificate of a HTTPS endpoint
57
+ * using a single SHA-256 fingerprint.
30
58
  */
59
+ @Throws(SSLPinningError::class)
31
60
  fun checkCertificate(
32
61
  urlString: String,
33
62
  fingerprintFromArgs: String?,
34
- callback: (Map<String, Any>) -> Unit,
35
- ) {
63
+ ): Map<String, Any> {
36
64
  val fingerprint =
37
65
  fingerprintFromArgs ?: config.fingerprint
38
66
 
39
67
  if (fingerprint == null) {
40
- callback(
41
- mapOf(
42
- "fingerprintMatched" to false,
43
- "error" to "No fingerprint provided (args or config)",
44
- ),
68
+ throw SSLPinningError.Unavailable(
69
+ "No fingerprint provided (args or config)",
45
70
  )
46
- return
47
71
  }
48
72
 
49
- performCheck(urlString, listOf(fingerprint), callback)
73
+ return performCheck(
74
+ urlString = urlString,
75
+ fingerprints = listOf(fingerprint),
76
+ )
50
77
  }
51
78
 
52
- // ---- Multiple fingerprints ----
79
+ // ---------------------------------------------------------------------------
80
+ // Multiple fingerprints
81
+ // ---------------------------------------------------------------------------
53
82
 
54
83
  /**
55
- * Validates the SSL certificate of a HTTPS endpoint using multiple allowed fingerprints.
56
- *
57
- * The certificate is considered valid if **any** of the provided fingerprints
58
- * matches the server's leaf certificate fingerprint.
59
- *
60
- * This is typically used to support certificate rotation.
84
+ * Validates the SSL certificate of a HTTPS endpoint
85
+ * using multiple allowed SHA-256 fingerprints.
61
86
  */
87
+ @Throws(SSLPinningError::class)
62
88
  fun checkCertificates(
63
89
  urlString: String,
64
90
  fingerprintsFromArgs: List<String>?,
65
- callback: (Map<String, Any>) -> Unit,
66
- ) {
91
+ ): Map<String, Any> {
67
92
  val fingerprints =
68
93
  fingerprintsFromArgs?.takeIf { it.isNotEmpty() }
69
94
  ?: config.fingerprints.takeIf { it.isNotEmpty() }
70
95
 
71
96
  if (fingerprints == null) {
72
- callback(
73
- mapOf(
74
- "fingerprintMatched" to false,
75
- "error" to "No fingerprints provided (args or config)",
76
- ),
97
+ throw SSLPinningError.Unavailable(
98
+ "No fingerprints provided (args or config)",
77
99
  )
78
- return
79
100
  }
80
101
 
81
- performCheck(urlString, fingerprints, callback)
102
+ return performCheck(
103
+ urlString = urlString,
104
+ fingerprints = fingerprints,
105
+ )
82
106
  }
83
107
 
84
- // ---- Shared implementation ----
108
+ // ---------------------------------------------------------------------------
109
+ // Shared implementation
110
+ // ---------------------------------------------------------------------------
85
111
 
86
112
  /**
87
- * Shared internal implementation for SSL pinning validation.
113
+ * Performs the actual SSL pinning validation.
114
+ *
115
+ * This method:
116
+ * - Validates the HTTPS URL
117
+ * - Opens a TLS connection
118
+ * - Extracts the server leaf certificate
119
+ * - Compares its SHA-256 fingerprint
120
+ * against the expected ones
88
121
  *
89
- * This method performs the actual TLS handshake and fingerprint comparison.
90
- * It is intentionally isolated to avoid duplication between
91
- * single and multi fingerprint modes.
122
+ * IMPORTANT:
123
+ * - The system trust chain is NOT evaluated
124
+ * - Only fingerprint matching determines acceptance
92
125
  */
126
+ @Throws(SSLPinningError::class)
93
127
  private fun performCheck(
94
128
  urlString: String,
95
129
  fingerprints: List<String>,
96
- callback: (Map<String, Any>) -> Unit,
97
- ) {
98
- val url = SSLPinningUtils.httpsUrl(urlString)
99
- if (url == null) {
100
- callback(
101
- mapOf(
102
- "fingerprintMatched" to false,
103
- "error" to "Invalid HTTPS URL",
104
- "errorCode" to "UNKNOWN_TYPE",
105
- ),
106
- )
107
- return
108
- }
130
+ ): Map<String, Any> {
131
+ val url =
132
+ SSLPinningUtils.httpsUrl(urlString)
133
+ ?: throw SSLPinningError.UnknownType(
134
+ "Invalid HTTPS URL",
135
+ )
136
+
137
+ return try {
138
+ val certificate = getCertificate(url)
109
139
 
110
- try {
111
- val cert = getCertificate(url)
112
140
  val actualFingerprint =
113
141
  SSLPinningUtils.normalizeFingerprint(
114
- SSLPinningUtils.sha256Fingerprint(cert),
142
+ SSLPinningUtils.sha256Fingerprint(certificate),
115
143
  )
116
144
 
117
145
  val normalizedExpected =
118
- fingerprints.map { SSLPinningUtils.normalizeFingerprint(it) }
146
+ fingerprints.map {
147
+ SSLPinningUtils.normalizeFingerprint(it)
148
+ }
119
149
 
120
150
  val matchedFingerprint =
121
- normalizedExpected.firstOrNull { it == actualFingerprint }
151
+ normalizedExpected.firstOrNull {
152
+ it == actualFingerprint
153
+ }
122
154
 
123
155
  val matched = matchedFingerprint != null
124
156
 
125
- SSLPinningLogger.debug("SSLPinning matched:", matched.toString())
157
+ SSLPinningLogger.debug(
158
+ "SSLPinning matched:",
159
+ matched.toString(),
160
+ )
126
161
 
127
- callback(
128
- mapOf(
129
- "actualFingerprint" to actualFingerprint,
130
- "fingerprintMatched" to matched,
131
- "matchedFingerprint" to (matchedFingerprint ?: ""),
132
- ),
162
+ mapOf(
163
+ "actualFingerprint" to actualFingerprint,
164
+ "fingerprintMatched" to matched,
165
+ "matchedFingerprint" to (matchedFingerprint ?: ""),
133
166
  )
167
+ } catch (e: SSLPinningError) {
168
+ throw e
134
169
  } catch (e: Exception) {
135
- SSLPinningLogger.error("Certificate check failed", e)
136
- callback(
137
- mapOf(
138
- "fingerprintMatched" to false,
139
- "error" to e.message.orEmpty(),
140
- "errorCode" to "INIT_FAILED",
141
- ),
170
+ SSLPinningLogger.error(
171
+ "Certificate check failed",
172
+ e,
173
+ )
174
+ throw SSLPinningError.InitFailed(
175
+ e.message ?: "SSL pinning failed",
142
176
  )
143
177
  }
144
178
  }
145
179
 
146
- // ---- Certificate retrieval ----
180
+ // ---------------------------------------------------------------------------
181
+ // Certificate retrieval
182
+ // ---------------------------------------------------------------------------
147
183
 
148
184
  /**
149
- * Opens a TLS connection and extracts the server leaf certificate.
150
- *
151
- * A permissive TrustManager is intentionally used to allow
152
- * inspection of the certificate without enforcing trust validation.
185
+ * Opens a TLS connection and extracts
186
+ * the server leaf certificate.
153
187
  *
154
- * SECURITY NOTE:
155
- * This does NOT bypass SSL pinning security, because the fingerprint
156
- * comparison is performed manually after extraction.
188
+ * A permissive TrustManager is intentionally used
189
+ * to allow certificate inspection without enforcing
190
+ * system trust validation.
157
191
  */
192
+ @Throws(Exception::class)
158
193
  private fun getCertificate(url: URL): Certificate {
159
194
  val trustManagers =
160
195
  arrayOf<TrustManager>(
161
196
  object : X509TrustManager {
162
- override fun getAcceptedIssuers() = arrayOf<java.security.cert.X509Certificate>()
197
+ override fun getAcceptedIssuers() = emptyArray<java.security.cert.X509Certificate>()
163
198
 
164
199
  override fun checkClientTrusted(
165
200
  certs: Array<java.security.cert.X509Certificate>,
@@ -173,16 +208,28 @@ class SSLPinningImpl(
173
208
  },
174
209
  )
175
210
 
176
- val sslContext = SSLContext.getInstance("TLS")
177
- sslContext.init(null, trustManagers, java.security.SecureRandom())
211
+ val sslContext =
212
+ SSLContext.getInstance("TLS")
213
+
214
+ sslContext.init(
215
+ null,
216
+ trustManagers,
217
+ java.security.SecureRandom(),
218
+ )
219
+
220
+ val connection =
221
+ url.openConnection() as HttpsURLConnection
222
+
223
+ connection.sslSocketFactory =
224
+ sslContext.socketFactory
178
225
 
179
- val connection = url.openConnection() as HttpsURLConnection
180
- connection.sslSocketFactory = sslContext.socketFactory
181
226
  connection.connect()
182
227
 
183
- val cert = connection.serverCertificates.first()
228
+ val certificate =
229
+ connection.serverCertificates.first()
230
+
184
231
  connection.disconnect()
185
232
 
186
- return cert
233
+ return certificate
187
234
  }
188
235
  }
@@ -6,53 +6,140 @@ import com.getcapacitor.Plugin
6
6
  import com.getcapacitor.PluginCall
7
7
  import com.getcapacitor.PluginMethod
8
8
  import com.getcapacitor.annotation.CapacitorPlugin
9
- import io.capkit.sslpinning.utils.SSLPinningLogger
10
9
 
10
+ /**
11
+ * Capacitor bridge for the SSLPinning plugin (Android).
12
+ *
13
+ * Responsibilities:
14
+ * - Parse JavaScript input
15
+ * - Call the native implementation
16
+ * - Resolve or reject PluginCall
17
+ * - Map native errors to JS-facing error codes
18
+ */
11
19
  @CapacitorPlugin(name = "SSLPinning")
12
20
  class SSLPinningPlugin : Plugin() {
21
+ // ---------------------------------------------------------------------------
22
+ // Properties
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Immutable plugin configuration.
27
+ * Parsed once during plugin initialization.
28
+ */
29
+ private lateinit var config: SSLPinningConfig
30
+
31
+ /**
32
+ * Native implementation containing
33
+ * platform-specific logic only.
34
+ */
13
35
  private lateinit var implementation: SSLPinningImpl
14
36
 
37
+ // ---------------------------------------------------------------------------
38
+ // Lifecycle
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Called once when the plugin is loaded by the Capacitor bridge.
43
+ *
44
+ * This is the correct place to:
45
+ * - read static configuration
46
+ * - initialize native resources
47
+ * - inject configuration into the implementation
48
+ */
15
49
  override fun load() {
16
- val config = SSLPinningConfig(this)
17
- SSLPinningLogger.verbose = config.verboseLogging
18
- implementation = SSLPinningImpl(config)
50
+ super.load()
51
+
52
+ config = SSLPinningConfig(this)
53
+ implementation = SSLPinningImpl(context)
54
+ implementation.updateConfig(config)
19
55
  }
20
56
 
57
+ // ---------------------------------------------------------------------------
58
+ // Error Mapping
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Maps native SSLPinningError values
63
+ * to JavaScript-facing error codes.
64
+ */
65
+ private fun reject(
66
+ call: PluginCall,
67
+ error: SSLPinningError,
68
+ ) {
69
+ val code =
70
+ when (error) {
71
+ is SSLPinningError.Unavailable -> "UNAVAILABLE"
72
+ is SSLPinningError.PermissionDenied -> "PERMISSION_DENIED"
73
+ is SSLPinningError.InitFailed -> "INIT_FAILED"
74
+ is SSLPinningError.UnknownType -> "UNKNOWN_TYPE"
75
+ }
76
+
77
+ call.reject(error.message, code)
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // SSL Pinning (single fingerprint)
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /**
85
+ * Validates the SSL certificate of a HTTPS endpoint
86
+ * using a single fingerprint.
87
+ */
21
88
  @PluginMethod
22
89
  fun checkCertificate(call: PluginCall) {
23
- val url = call.getString("url") ?: ""
24
- val fingerprint = call.getString("fingerprint")
25
-
26
- if (url.isEmpty()) {
27
- val result = JSObject()
28
- result.put("fingerprintMatched", false)
29
- result.put("error", "Missing url")
30
- call.resolve(result)
90
+ val url: String? = call.getString("url")
91
+ val fingerprint: String? = call.getString("fingerprint")
92
+
93
+ if (url.isNullOrBlank()) {
94
+ call.reject("Missing url", "UNKNOWN_TYPE")
31
95
  return
32
96
  }
33
97
 
34
98
  execute {
35
- implementation.checkCertificate(url, fingerprint) { data ->
36
- val result = JSObject()
37
- for (entry in data.entries) {
38
- result.put(entry.key, entry.value)
99
+ try {
100
+ val result: Map<String, Any> =
101
+ implementation.checkCertificate(
102
+ urlString = url,
103
+ fingerprintFromArgs = fingerprint,
104
+ )
105
+
106
+ val jsResult = JSObject()
107
+ for ((key, value) in result) {
108
+ jsResult.put(key, value)
39
109
  }
40
- call.resolve(result)
110
+
111
+ call.resolve(jsResult)
112
+ } catch (error: SSLPinningError) {
113
+ reject(call, error)
114
+ } catch (error: Exception) {
115
+ call.reject(
116
+ error.message ?: "SSL pinning failed",
117
+ "INIT_FAILED",
118
+ )
41
119
  }
42
120
  }
43
121
  }
44
122
 
123
+ // ---------------------------------------------------------------------------
124
+ // SSL Pinning (multiple fingerprints)
125
+ // ---------------------------------------------------------------------------
126
+
127
+ /**
128
+ * Validates the SSL certificate of a HTTPS endpoint
129
+ * using multiple allowed fingerprints.
130
+ */
45
131
  @PluginMethod
46
132
  fun checkCertificates(call: PluginCall) {
47
- val url = call.getString("url") ?: ""
133
+ val url: String? = call.getString("url")
48
134
 
49
135
  val jsArray: JSArray? = call.getArray("fingerprints")
136
+
50
137
  val fingerprints: List<String>? =
51
138
  if (jsArray != null && jsArray.length() > 0) {
52
139
  val list = ArrayList<String>()
53
140
  for (i in 0 until jsArray.length()) {
54
141
  val value = jsArray.getString(i)
55
- if (!value.isNullOrEmpty()) {
142
+ if (!value.isNullOrBlank()) {
56
143
  list.add(value)
57
144
  }
58
145
  }
@@ -61,22 +148,52 @@ class SSLPinningPlugin : Plugin() {
61
148
  null
62
149
  }
63
150
 
64
- if (url.isEmpty()) {
65
- val result = JSObject()
66
- result.put("fingerprintMatched", false)
67
- result.put("error", "Missing url")
68
- call.resolve(result)
151
+ if (url.isNullOrBlank()) {
152
+ call.reject("Missing url", "UNKNOWN_TYPE")
69
153
  return
70
154
  }
71
155
 
72
156
  execute {
73
- implementation.checkCertificates(url, fingerprints) { data ->
74
- val result = JSObject()
75
- for (entry in data.entries) {
76
- result.put(entry.key, entry.value)
157
+ try {
158
+ val result: Map<String, Any> =
159
+ implementation.checkCertificates(
160
+ urlString = url,
161
+ fingerprintsFromArgs = fingerprints,
162
+ )
163
+
164
+ val jsResult = JSObject()
165
+ for ((key, value) in result) {
166
+ jsResult.put(key, value)
77
167
  }
78
- call.resolve(result)
168
+
169
+ call.resolve(jsResult)
170
+ } catch (error: SSLPinningError) {
171
+ reject(call, error)
172
+ } catch (error: Exception) {
173
+ call.reject(
174
+ error.message ?: "SSL pinning failed",
175
+ "INIT_FAILED",
176
+ )
79
177
  }
80
178
  }
81
179
  }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Version
183
+ // ---------------------------------------------------------------------------
184
+
185
+ /**
186
+ * Returns the native plugin version.
187
+ *
188
+ * NOTE:
189
+ * - This method is guaranteed not to fail
190
+ * - Therefore it does NOT use TestError
191
+ * - Version is injected at build time from package.json
192
+ */
193
+ @PluginMethod
194
+ fun getPluginVersion(call: PluginCall) {
195
+ val ret = JSObject()
196
+ ret.put("version", BuildConfig.PLUGIN_VERSION)
197
+ call.resolve(ret)
198
+ }
82
199
  }