@capgo/capacitor-fast-sql 8.0.18 → 8.0.19

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.
package/README.md CHANGED
@@ -55,7 +55,7 @@ This plugin provides direct native SQLite database access with a custom communic
55
55
 
56
56
  ## iOS Configuration
57
57
 
58
- Add to your `Info.plist` if you encounter any issues:
58
+ This plugin runs a local HTTP server on `localhost`. iOS App Transport Security (ATS) blocks cleartext HTTP by default, so you **must** allow local networking in your `Info.plist`:
59
59
 
60
60
  ```xml
61
61
  <key>NSAppTransportSecurity</key>
@@ -65,9 +65,36 @@ Add to your `Info.plist` if you encounter any issues:
65
65
  </dict>
66
66
  ```
67
67
 
68
+ This only permits cleartext to loopback addresses (`localhost` / `127.0.0.1`) — it does not weaken ATS for external connections.
69
+
68
70
  ## Android Configuration
69
71
 
70
- Add to your `AndroidManifest.xml` if needed:
72
+ This plugin runs a local HTTP server on `localhost` to bypass Capacitor's bridge for performance. Android 9+ blocks cleartext (non-HTTPS) traffic by default, so you **must** allow it for `localhost`.
73
+
74
+ **Option A — Scoped to localhost only (recommended):**
75
+
76
+ Create `android/app/src/main/res/xml/network_security_config.xml`:
77
+
78
+ ```xml
79
+ <?xml version="1.0" encoding="utf-8"?>
80
+ <network-security-config>
81
+ <domain-config cleartextTrafficPermitted="true">
82
+ <domain includeSubdomains="false">localhost</domain>
83
+ <domain includeSubdomains="false">127.0.0.1</domain>
84
+ </domain-config>
85
+ </network-security-config>
86
+ ```
87
+
88
+ Then reference it in your `AndroidManifest.xml`:
89
+
90
+ ```xml
91
+ <application
92
+ android:networkSecurityConfig="@xml/network_security_config">
93
+ ...
94
+ </application>
95
+ ```
96
+
97
+ **Option B — Allow all cleartext (simpler but less secure):**
71
98
 
72
99
  ```xml
73
100
  <application
@@ -76,9 +103,27 @@ Add to your `AndroidManifest.xml` if needed:
76
103
  </application>
77
104
  ```
78
105
 
79
- ## Encryption (iOS/Android)
106
+ ## Encryption (Android)
107
+
108
+ Encryption uses [SQLCipher](https://www.zetetic.net/sqlcipher/) and is opt-in. Add the SQLCipher dependency to your **app-level** `build.gradle`:
109
+
110
+ ```gradle
111
+ dependencies {
112
+ implementation 'net.zetetic:sqlcipher-android:4.13.0'
113
+ }
114
+ ```
115
+
116
+ Then connect with encryption enabled:
117
+
118
+ ```typescript
119
+ const db = await FastSQL.connect({
120
+ database: 'secure',
121
+ encrypted: true,
122
+ encryptionKey: 'my-secret-key',
123
+ });
124
+ ```
80
125
 
81
- Set `encrypted: true` and provide an `encryptionKey` when connecting. This uses SQLCipher on mobile platforms (ensure SQLCipher is linked on iOS if you use SwiftPM).
126
+ If SQLCipher is not installed and `encrypted: true` is passed, the plugin returns a clear error message instead of crashing.
82
127
 
83
128
  ## Usage
84
129
 
@@ -54,7 +54,7 @@ dependencies {
54
54
  implementation 'androidx.sqlite:sqlite:2.6.2'
55
55
  implementation 'com.google.code.gson:gson:2.13.2'
56
56
  implementation 'org.nanohttpd:nanohttpd:2.3.1'
57
- implementation 'net.zetetic:sqlcipher-android:4.13.0'
57
+ compileOnly 'net.zetetic:sqlcipher-android:4.13.0'
58
58
  testImplementation "junit:junit:$junitVersion"
59
59
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
60
60
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -19,7 +19,7 @@ import java.util.Map;
19
19
  @CapacitorPlugin(name = "CapgoCapacitorFastSql")
20
20
  public class CapgoCapacitorFastSqlPlugin extends Plugin {
21
21
 
22
- private final String pluginVersion = "8.0.18";
22
+ private final String pluginVersion = "8.0.19";
23
23
 
24
24
  private Map<String, DatabaseConnection> databases = new HashMap<>();
25
25
  private SQLHTTPServer server;
@@ -57,6 +57,16 @@ public class CapgoCapacitorFastSqlPlugin extends Plugin {
57
57
  // Open database
58
58
  DatabaseConnection db;
59
59
  if (encrypted) {
60
+ try {
61
+ Class.forName("net.zetetic.database.sqlcipher.SQLiteDatabase");
62
+ } catch (ClassNotFoundException e) {
63
+ call.reject(
64
+ "Encryption is not available. " +
65
+ "Add 'implementation \"net.zetetic:sqlcipher-android:4.13.0\"' " +
66
+ "to your app's build.gradle to enable encryption."
67
+ );
68
+ return;
69
+ }
60
70
  db = new EncryptedSQLDatabase(dbFile.getAbsolutePath(), encryptionKey);
61
71
  } else {
62
72
  db = new SQLDatabase(dbFile.getAbsolutePath());
@@ -23,6 +23,16 @@ public class EncryptedSQLDatabase implements DatabaseConnection {
23
23
  if (encryptionKey == null || encryptionKey.isEmpty()) {
24
24
  throw new Exception("Encryption key is required when encrypted is true");
25
25
  }
26
+ try {
27
+ System.loadLibrary("sqlcipher");
28
+ } catch (UnsatisfiedLinkError e) {
29
+ throw new Exception(
30
+ "SQLCipher native library not found. " +
31
+ "Add 'implementation \"net.zetetic:sqlcipher-android:4.13.0\"' " +
32
+ "to your app's build.gradle to enable encryption.",
33
+ e
34
+ );
35
+ }
26
36
  byte[] keyBytes = encryptionKey.getBytes(StandardCharsets.UTF_8);
27
37
  this.db = SQLiteDatabase.openOrCreateDatabase(path, keyBytes, null, null);
28
38
  // Enable foreign keys
@@ -40,24 +40,33 @@ public class SQLHTTPServer extends NanoHTTPD {
40
40
  return getListeningPort();
41
41
  }
42
42
 
43
+ private Response addCorsHeaders(Response response) {
44
+ response.addHeader("Access-Control-Allow-Origin", "*");
45
+ response.addHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
46
+ response.addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Database");
47
+ response.addHeader("Access-Control-Max-Age", "86400");
48
+ return response;
49
+ }
50
+
43
51
  @Override
44
52
  public Response serve(IHTTPSession session) {
45
- // Check authentication
53
+ if (Method.OPTIONS.equals(session.getMethod())) {
54
+ return addCorsHeaders(newFixedLengthResponse(Response.Status.OK, "text/plain", ""));
55
+ }
56
+
46
57
  String authHeader = session.getHeaders().get("authorization");
47
58
  if (authHeader == null || !authHeader.equals("Bearer " + token)) {
48
- return newFixedLengthResponse(Response.Status.UNAUTHORIZED, "text/plain", "Unauthorized");
59
+ return addCorsHeaders(newFixedLengthResponse(Response.Status.UNAUTHORIZED, "text/plain", "Unauthorized"));
49
60
  }
50
61
 
51
- // Get database name
52
62
  String database = session.getHeaders().get("x-database");
53
63
  if (database == null) {
54
- return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Database header required");
64
+ return addCorsHeaders(newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Database header required"));
55
65
  }
56
66
 
57
- // Check if database exists
58
67
  DatabaseConnection db = databases.get(database);
59
68
  if (db == null) {
60
- return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Database not found");
69
+ return addCorsHeaders(newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Database not found"));
61
70
  }
62
71
 
63
72
  String uri = session.getUri();
@@ -65,20 +74,20 @@ public class SQLHTTPServer extends NanoHTTPD {
65
74
 
66
75
  try {
67
76
  if (method == Method.POST && uri.equals("/execute")) {
68
- return handleExecute(session, db);
77
+ return addCorsHeaders(handleExecute(session, db));
69
78
  } else if (method == Method.POST && uri.equals("/batch")) {
70
- return handleBatch(session, db);
79
+ return addCorsHeaders(handleBatch(session, db));
71
80
  } else if (method == Method.POST && uri.equals("/transaction/begin")) {
72
- return handleBeginTransaction(db);
81
+ return addCorsHeaders(handleBeginTransaction(db));
73
82
  } else if (method == Method.POST && uri.equals("/transaction/commit")) {
74
- return handleCommitTransaction(db);
83
+ return addCorsHeaders(handleCommitTransaction(db));
75
84
  } else if (method == Method.POST && uri.equals("/transaction/rollback")) {
76
- return handleRollbackTransaction(db);
85
+ return addCorsHeaders(handleRollbackTransaction(db));
77
86
  } else {
78
- return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Endpoint not found");
87
+ return addCorsHeaders(newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Endpoint not found"));
79
88
  }
80
89
  } catch (Exception e) {
81
- return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Error: " + e.getMessage());
90
+ return addCorsHeaders(newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Error: " + e.getMessage()));
82
91
  }
83
92
  }
84
93
 
@@ -10,7 +10,7 @@ import SQLite3
10
10
  */
11
11
  @objc(CapgoCapacitorFastSqlPlugin)
12
12
  public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
13
- private let pluginVersion: String = "8.0.18"
13
+ private let pluginVersion: String = "8.0.19"
14
14
  public let identifier = "CapgoCapacitorFastSqlPlugin"
15
15
  public let jsName = "CapgoCapacitorFastSql"
16
16
  public let pluginMethods: [CAPPluginMethod] = [
@@ -45,17 +45,48 @@ class SQLHTTPServer {
45
45
  isRunning = false
46
46
  }
47
47
 
48
+ // MARK: - CORS Headers
49
+
50
+ private let corsHeaders: HTTPHeaders = [
51
+ .accessControlAllowOrigin: "*",
52
+ .accessControlAllowMethods: "POST, OPTIONS",
53
+ .accessControlAllowHeaders: "Content-Type, Authorization, X-Database",
54
+ .accessControlMaxAge: "86400"
55
+ ]
56
+
57
+ private func corsPreflightResponse() -> HTTPResponse {
58
+ return HTTPResponse(.ok, headers: corsHeaders)
59
+ }
60
+
61
+ private func addCorsHeaders(_ response: HTTPResponse) -> HTTPResponse {
62
+ for (key, value) in corsHeaders {
63
+ response.headers[key] = value
64
+ }
65
+ return response
66
+ }
67
+
48
68
  // MARK: - Route Setup
49
69
 
50
70
  private func setupRoutes() {
51
71
  guard let server = server else { return }
52
72
 
73
+ // CORS preflight for all endpoints
74
+ let preflightPaths = ["/execute", "/batch", "/transaction/begin", "/transaction/commit", "/transaction/rollback"]
75
+ for path in preflightPaths {
76
+ server.route(.OPTIONS, path) { [weak self] _ in
77
+ guard let self = self else {
78
+ return HTTPResponse(.internalServerError)
79
+ }
80
+ return self.corsPreflightResponse()
81
+ }
82
+ }
83
+
53
84
  // Execute endpoint
54
85
  server.route(.POST, "/execute") { [weak self] request in
55
86
  guard let self = self else {
56
87
  return HTTPResponse(.internalServerError)
57
88
  }
58
- return self.handleExecuteRequest(request: request)
89
+ return self.addCorsHeaders(self.handleExecuteRequest(request: request))
59
90
  }
60
91
 
61
92
  // Batch endpoint
@@ -63,7 +94,7 @@ class SQLHTTPServer {
63
94
  guard let self = self else {
64
95
  return HTTPResponse(.internalServerError)
65
96
  }
66
- return self.handleBatchRequest(request: request)
97
+ return self.addCorsHeaders(self.handleBatchRequest(request: request))
67
98
  }
68
99
 
69
100
  // Transaction endpoints
@@ -71,21 +102,21 @@ class SQLHTTPServer {
71
102
  guard let self = self else {
72
103
  return HTTPResponse(.internalServerError)
73
104
  }
74
- return self.handleBeginTransactionRequest(request: request)
105
+ return self.addCorsHeaders(self.handleBeginTransactionRequest(request: request))
75
106
  }
76
107
 
77
108
  server.route(.POST, "/transaction/commit") { [weak self] request in
78
109
  guard let self = self else {
79
110
  return HTTPResponse(.internalServerError)
80
111
  }
81
- return self.handleCommitTransactionRequest(request: request)
112
+ return self.addCorsHeaders(self.handleCommitTransactionRequest(request: request))
82
113
  }
83
114
 
84
115
  server.route(.POST, "/transaction/rollback") { [weak self] request in
85
116
  guard let self = self else {
86
117
  return HTTPResponse(.internalServerError)
87
118
  }
88
- return self.handleRollbackTransactionRequest(request: request)
119
+ return self.addCorsHeaders(self.handleRollbackTransactionRequest(request: request))
89
120
  }
90
121
  }
91
122
 
@@ -167,7 +198,7 @@ class SQLHTTPServer {
167
198
  do {
168
199
  let result = try db.execute(statement: statement, params: params)
169
200
  let resultData = try JSONSerialization.data(withJSONObject: result)
170
- return HTTPResponse(.ok, headers: ["Content-Type": "application/json"], body: resultData)
201
+ return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
171
202
  } catch {
172
203
  let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
173
204
  return HTTPResponse(.internalServerError, body: errorData)
@@ -215,7 +246,7 @@ class SQLHTTPServer {
215
246
  }
216
247
 
217
248
  let resultData = try JSONSerialization.data(withJSONObject: results)
218
- return HTTPResponse(.ok, headers: ["Content-Type": "application/json"], body: resultData)
249
+ return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
219
250
  } catch {
220
251
  let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
221
252
  return HTTPResponse(.internalServerError, body: errorData)
@@ -244,7 +275,7 @@ class SQLHTTPServer {
244
275
  do {
245
276
  try db.beginTransaction()
246
277
  let resultData = "{}".data(using: .utf8)!
247
- return HTTPResponse(.ok, headers: ["Content-Type": "application/json"], body: resultData)
278
+ return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
248
279
  } catch {
249
280
  let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
250
281
  return HTTPResponse(.internalServerError, body: errorData)
@@ -273,7 +304,7 @@ class SQLHTTPServer {
273
304
  do {
274
305
  try db.commitTransaction()
275
306
  let resultData = "{}".data(using: .utf8)!
276
- return HTTPResponse(.ok, headers: ["Content-Type": "application/json"], body: resultData)
307
+ return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
277
308
  } catch {
278
309
  let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
279
310
  return HTTPResponse(.internalServerError, body: errorData)
@@ -302,7 +333,7 @@ class SQLHTTPServer {
302
333
  do {
303
334
  try db.rollbackTransaction()
304
335
  let resultData = "{}".data(using: .utf8)!
305
- return HTTPResponse(.ok, headers: ["Content-Type": "application/json"], body: resultData)
336
+ return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
306
337
  } catch {
307
338
  let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
308
339
  return HTTPResponse(.internalServerError, body: errorData)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-fast-sql",
3
- "version": "8.0.18",
3
+ "version": "8.0.19",
4
4
  "description": "High-performance native SQLite plugin with custom protocol for efficient sync operations and IndexedDB replacement",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",