@vailix/mask 0.2.1 → 0.2.3
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/CHANGELOG.md +16 -0
- package/README.md +10 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +88 -27
- package/dist/index.mjs +88 -27
- package/package.json +1 -1
- package/src/ble.ts +44 -0
- package/src/db.ts +75 -37
- package/src/index.ts +57 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @vailix/mask
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c1ca263: fix: prevent race condition in SDK initialization
|
|
8
|
+
- Implement singleton pattern for `VailixSDK.create()` to prevent concurrent database connections
|
|
9
|
+
- Ensure database is properly closed before deletion on key mismatch errors
|
|
10
|
+
- Add missing internal BLE types (`InternalNearbyUser`, `PendingPairRequest`, `BleServiceConfig`)
|
|
11
|
+
- Add `VailixSDK.destroy()` for cleanup and `VailixSDK.isInitialized()` for status checking
|
|
12
|
+
|
|
13
|
+
## 0.2.2
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 4b6a9d5: fix: ensure database is closed before deletion on key mismatch and add missing internal types
|
|
18
|
+
|
|
3
19
|
## 0.2.1
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -62,6 +62,16 @@ const unsubscribe = sdk.onNearbyUsersChanged((users) => {
|
|
|
62
62
|
});
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
> **Note:** `VailixSDK.create()` returns a singleton instance. Multiple calls return the same instance, and the config is only used on the first call. Use `VailixSDK.destroy()` to reset the SDK if needed.
|
|
66
|
+
|
|
67
|
+
### Static Methods
|
|
68
|
+
|
|
69
|
+
| Method | Description |
|
|
70
|
+
|--------|-------------|
|
|
71
|
+
| `VailixSDK.create(config)` | Create or return the singleton SDK instance |
|
|
72
|
+
| `VailixSDK.destroy()` | Release resources and reset the singleton |
|
|
73
|
+
| `VailixSDK.isInitialized()` | Check if the SDK has been initialized |
|
|
74
|
+
|
|
65
75
|
## Compatibility
|
|
66
76
|
|
|
67
77
|
- **Expo**: SDK 52+ (Development Build required, **Expo Go not supported**)
|
package/dist/index.d.mts
CHANGED
|
@@ -227,6 +227,8 @@ interface ParsedQR {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
declare class VailixSDK {
|
|
230
|
+
private static instance;
|
|
231
|
+
private static initPromise;
|
|
230
232
|
identity: IdentityManager;
|
|
231
233
|
storage: StorageService;
|
|
232
234
|
matcher: MatcherService;
|
|
@@ -237,11 +239,27 @@ declare class VailixSDK {
|
|
|
237
239
|
private rpiDurationMs;
|
|
238
240
|
private constructor();
|
|
239
241
|
/**
|
|
240
|
-
* Create
|
|
242
|
+
* Create or return the singleton SDK instance.
|
|
241
243
|
*
|
|
242
|
-
*
|
|
244
|
+
* Thread-safe: concurrent calls will wait for the first initialization to complete
|
|
245
|
+
* and return the same instance. Config is only used on first initialization.
|
|
246
|
+
*
|
|
247
|
+
* @param config - Unified configuration object (used only on first call)
|
|
243
248
|
*/
|
|
244
249
|
static create(config: VailixConfig): Promise<VailixSDK>;
|
|
250
|
+
/**
|
|
251
|
+
* Internal initialization logic (extracted for singleton pattern).
|
|
252
|
+
*/
|
|
253
|
+
private static doCreate;
|
|
254
|
+
/**
|
|
255
|
+
* Destroy the singleton instance and release all resources.
|
|
256
|
+
* Use for testing or when the app needs to fully reset the SDK.
|
|
257
|
+
*/
|
|
258
|
+
static destroy(): Promise<void>;
|
|
259
|
+
/**
|
|
260
|
+
* Check if the SDK has been initialized.
|
|
261
|
+
*/
|
|
262
|
+
static isInitialized(): boolean;
|
|
245
263
|
/** Get current QR code data */
|
|
246
264
|
getQRCode(): string;
|
|
247
265
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -227,6 +227,8 @@ interface ParsedQR {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
declare class VailixSDK {
|
|
230
|
+
private static instance;
|
|
231
|
+
private static initPromise;
|
|
230
232
|
identity: IdentityManager;
|
|
231
233
|
storage: StorageService;
|
|
232
234
|
matcher: MatcherService;
|
|
@@ -237,11 +239,27 @@ declare class VailixSDK {
|
|
|
237
239
|
private rpiDurationMs;
|
|
238
240
|
private constructor();
|
|
239
241
|
/**
|
|
240
|
-
* Create
|
|
242
|
+
* Create or return the singleton SDK instance.
|
|
241
243
|
*
|
|
242
|
-
*
|
|
244
|
+
* Thread-safe: concurrent calls will wait for the first initialization to complete
|
|
245
|
+
* and return the same instance. Config is only used on first initialization.
|
|
246
|
+
*
|
|
247
|
+
* @param config - Unified configuration object (used only on first call)
|
|
243
248
|
*/
|
|
244
249
|
static create(config: VailixConfig): Promise<VailixSDK>;
|
|
250
|
+
/**
|
|
251
|
+
* Internal initialization logic (extracted for singleton pattern).
|
|
252
|
+
*/
|
|
253
|
+
private static doCreate;
|
|
254
|
+
/**
|
|
255
|
+
* Destroy the singleton instance and release all resources.
|
|
256
|
+
* Use for testing or when the app needs to fully reset the SDK.
|
|
257
|
+
*/
|
|
258
|
+
static destroy(): Promise<void>;
|
|
259
|
+
/**
|
|
260
|
+
* Check if the SDK has been initialized.
|
|
261
|
+
*/
|
|
262
|
+
static isInitialized(): boolean;
|
|
245
263
|
/** Get current QR code data */
|
|
246
264
|
getQRCode(): string;
|
|
247
265
|
/**
|
package/dist/index.js
CHANGED
|
@@ -775,38 +775,60 @@ var import_expo_sqlite2 = require("expo-sqlite");
|
|
|
775
775
|
var import_drizzle_orm2 = require("drizzle-orm");
|
|
776
776
|
var DB_NAME = "vailix.db";
|
|
777
777
|
async function initializeDatabase(masterKey) {
|
|
778
|
+
const result = await tryOpenEncryptedDatabase(masterKey);
|
|
779
|
+
if (result.success) {
|
|
780
|
+
return result.db;
|
|
781
|
+
}
|
|
782
|
+
console.warn("Database key mismatch, recreating fresh database");
|
|
783
|
+
if (result.expo) {
|
|
784
|
+
result.expo.closeSync();
|
|
785
|
+
}
|
|
786
|
+
(0, import_expo_sqlite2.deleteDatabaseSync)(DB_NAME);
|
|
787
|
+
const retryResult = await tryOpenEncryptedDatabase(masterKey);
|
|
788
|
+
if (!retryResult.success) {
|
|
789
|
+
if (retryResult.expo) {
|
|
790
|
+
retryResult.expo.closeSync();
|
|
791
|
+
}
|
|
792
|
+
throw retryResult.error;
|
|
793
|
+
}
|
|
794
|
+
return retryResult.db;
|
|
795
|
+
}
|
|
796
|
+
async function tryOpenEncryptedDatabase(masterKey) {
|
|
797
|
+
let expo = null;
|
|
778
798
|
try {
|
|
779
|
-
|
|
799
|
+
expo = (0, import_expo_sqlite2.openDatabaseSync)(DB_NAME);
|
|
800
|
+
const db = (0, import_expo_sqlite.drizzle)(expo);
|
|
801
|
+
if (!/^[0-9a-f]+$/i.test(masterKey)) {
|
|
802
|
+
throw new Error("Invalid master key format");
|
|
803
|
+
}
|
|
804
|
+
await db.run(import_drizzle_orm2.sql.raw(`PRAGMA key = '${masterKey}'`));
|
|
805
|
+
await db.run(import_drizzle_orm2.sql`SELECT 1`);
|
|
806
|
+
await db.run(import_drizzle_orm2.sql`
|
|
807
|
+
CREATE TABLE IF NOT EXISTS scanned_events (
|
|
808
|
+
id TEXT PRIMARY KEY,
|
|
809
|
+
rpi TEXT NOT NULL,
|
|
810
|
+
metadata_key TEXT NOT NULL,
|
|
811
|
+
timestamp INTEGER NOT NULL
|
|
812
|
+
)
|
|
813
|
+
`);
|
|
814
|
+
await db.run(import_drizzle_orm2.sql`
|
|
815
|
+
CREATE INDEX IF NOT EXISTS rpi_idx ON scanned_events(rpi)
|
|
816
|
+
`);
|
|
817
|
+
return { success: true, db, expo };
|
|
780
818
|
} catch (error) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
819
|
+
return {
|
|
820
|
+
success: false,
|
|
821
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
822
|
+
expo
|
|
823
|
+
};
|
|
784
824
|
}
|
|
785
825
|
}
|
|
786
|
-
async function openEncryptedDatabase(masterKey) {
|
|
787
|
-
const expo = (0, import_expo_sqlite2.openDatabaseSync)(DB_NAME);
|
|
788
|
-
const db = (0, import_expo_sqlite.drizzle)(expo);
|
|
789
|
-
if (!/^[0-9a-f]+$/i.test(masterKey)) {
|
|
790
|
-
throw new Error("Invalid master key format");
|
|
791
|
-
}
|
|
792
|
-
await db.run(import_drizzle_orm2.sql.raw(`PRAGMA key = '${masterKey}'`));
|
|
793
|
-
await db.run(import_drizzle_orm2.sql`SELECT 1`);
|
|
794
|
-
await db.run(import_drizzle_orm2.sql`
|
|
795
|
-
CREATE TABLE IF NOT EXISTS scanned_events (
|
|
796
|
-
id TEXT PRIMARY KEY,
|
|
797
|
-
rpi TEXT NOT NULL,
|
|
798
|
-
metadata_key TEXT NOT NULL,
|
|
799
|
-
timestamp INTEGER NOT NULL
|
|
800
|
-
)
|
|
801
|
-
`);
|
|
802
|
-
await db.run(import_drizzle_orm2.sql`
|
|
803
|
-
CREATE INDEX IF NOT EXISTS rpi_idx ON scanned_events(rpi)
|
|
804
|
-
`);
|
|
805
|
-
return db;
|
|
806
|
-
}
|
|
807
826
|
|
|
808
827
|
// src/index.ts
|
|
809
828
|
var VailixSDK = class _VailixSDK {
|
|
829
|
+
// Singleton instance and initialization promise for thread-safety
|
|
830
|
+
static instance = null;
|
|
831
|
+
static initPromise = null;
|
|
810
832
|
identity;
|
|
811
833
|
storage;
|
|
812
834
|
matcher;
|
|
@@ -826,11 +848,33 @@ var VailixSDK = class _VailixSDK {
|
|
|
826
848
|
this.rpiDurationMs = rpiDurationMs;
|
|
827
849
|
}
|
|
828
850
|
/**
|
|
829
|
-
* Create
|
|
851
|
+
* Create or return the singleton SDK instance.
|
|
852
|
+
*
|
|
853
|
+
* Thread-safe: concurrent calls will wait for the first initialization to complete
|
|
854
|
+
* and return the same instance. Config is only used on first initialization.
|
|
830
855
|
*
|
|
831
|
-
* @param config - Unified configuration object
|
|
856
|
+
* @param config - Unified configuration object (used only on first call)
|
|
832
857
|
*/
|
|
833
858
|
static async create(config) {
|
|
859
|
+
if (_VailixSDK.instance) {
|
|
860
|
+
return _VailixSDK.instance;
|
|
861
|
+
}
|
|
862
|
+
if (_VailixSDK.initPromise) {
|
|
863
|
+
return _VailixSDK.initPromise;
|
|
864
|
+
}
|
|
865
|
+
_VailixSDK.initPromise = _VailixSDK.doCreate(config);
|
|
866
|
+
try {
|
|
867
|
+
_VailixSDK.instance = await _VailixSDK.initPromise;
|
|
868
|
+
return _VailixSDK.instance;
|
|
869
|
+
} catch (error) {
|
|
870
|
+
_VailixSDK.initPromise = null;
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Internal initialization logic (extracted for singleton pattern).
|
|
876
|
+
*/
|
|
877
|
+
static async doCreate(config) {
|
|
834
878
|
const rpiDuration = config.rpiDurationMs ?? 15 * 60 * 1e3;
|
|
835
879
|
if (config.rescanIntervalMs && config.rescanIntervalMs > rpiDuration) {
|
|
836
880
|
throw new Error(`rescanIntervalMs (${config.rescanIntervalMs}) cannot exceed rpiDurationMs (${rpiDuration})`);
|
|
@@ -866,6 +910,23 @@ var VailixSDK = class _VailixSDK {
|
|
|
866
910
|
rpiDuration
|
|
867
911
|
);
|
|
868
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* Destroy the singleton instance and release all resources.
|
|
915
|
+
* Use for testing or when the app needs to fully reset the SDK.
|
|
916
|
+
*/
|
|
917
|
+
static async destroy() {
|
|
918
|
+
if (_VailixSDK.instance) {
|
|
919
|
+
_VailixSDK.instance.ble.destroy();
|
|
920
|
+
_VailixSDK.instance = null;
|
|
921
|
+
}
|
|
922
|
+
_VailixSDK.initPromise = null;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Check if the SDK has been initialized.
|
|
926
|
+
*/
|
|
927
|
+
static isInitialized() {
|
|
928
|
+
return _VailixSDK.instance !== null;
|
|
929
|
+
}
|
|
869
930
|
// ========================================================================
|
|
870
931
|
// QR Code Methods
|
|
871
932
|
// ========================================================================
|
package/dist/index.mjs
CHANGED
|
@@ -739,38 +739,60 @@ import { openDatabaseSync, deleteDatabaseSync } from "expo-sqlite";
|
|
|
739
739
|
import { sql } from "drizzle-orm";
|
|
740
740
|
var DB_NAME = "vailix.db";
|
|
741
741
|
async function initializeDatabase(masterKey) {
|
|
742
|
+
const result = await tryOpenEncryptedDatabase(masterKey);
|
|
743
|
+
if (result.success) {
|
|
744
|
+
return result.db;
|
|
745
|
+
}
|
|
746
|
+
console.warn("Database key mismatch, recreating fresh database");
|
|
747
|
+
if (result.expo) {
|
|
748
|
+
result.expo.closeSync();
|
|
749
|
+
}
|
|
750
|
+
deleteDatabaseSync(DB_NAME);
|
|
751
|
+
const retryResult = await tryOpenEncryptedDatabase(masterKey);
|
|
752
|
+
if (!retryResult.success) {
|
|
753
|
+
if (retryResult.expo) {
|
|
754
|
+
retryResult.expo.closeSync();
|
|
755
|
+
}
|
|
756
|
+
throw retryResult.error;
|
|
757
|
+
}
|
|
758
|
+
return retryResult.db;
|
|
759
|
+
}
|
|
760
|
+
async function tryOpenEncryptedDatabase(masterKey) {
|
|
761
|
+
let expo = null;
|
|
742
762
|
try {
|
|
743
|
-
|
|
763
|
+
expo = openDatabaseSync(DB_NAME);
|
|
764
|
+
const db = drizzle(expo);
|
|
765
|
+
if (!/^[0-9a-f]+$/i.test(masterKey)) {
|
|
766
|
+
throw new Error("Invalid master key format");
|
|
767
|
+
}
|
|
768
|
+
await db.run(sql.raw(`PRAGMA key = '${masterKey}'`));
|
|
769
|
+
await db.run(sql`SELECT 1`);
|
|
770
|
+
await db.run(sql`
|
|
771
|
+
CREATE TABLE IF NOT EXISTS scanned_events (
|
|
772
|
+
id TEXT PRIMARY KEY,
|
|
773
|
+
rpi TEXT NOT NULL,
|
|
774
|
+
metadata_key TEXT NOT NULL,
|
|
775
|
+
timestamp INTEGER NOT NULL
|
|
776
|
+
)
|
|
777
|
+
`);
|
|
778
|
+
await db.run(sql`
|
|
779
|
+
CREATE INDEX IF NOT EXISTS rpi_idx ON scanned_events(rpi)
|
|
780
|
+
`);
|
|
781
|
+
return { success: true, db, expo };
|
|
744
782
|
} catch (error) {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
783
|
+
return {
|
|
784
|
+
success: false,
|
|
785
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
786
|
+
expo
|
|
787
|
+
};
|
|
748
788
|
}
|
|
749
789
|
}
|
|
750
|
-
async function openEncryptedDatabase(masterKey) {
|
|
751
|
-
const expo = openDatabaseSync(DB_NAME);
|
|
752
|
-
const db = drizzle(expo);
|
|
753
|
-
if (!/^[0-9a-f]+$/i.test(masterKey)) {
|
|
754
|
-
throw new Error("Invalid master key format");
|
|
755
|
-
}
|
|
756
|
-
await db.run(sql.raw(`PRAGMA key = '${masterKey}'`));
|
|
757
|
-
await db.run(sql`SELECT 1`);
|
|
758
|
-
await db.run(sql`
|
|
759
|
-
CREATE TABLE IF NOT EXISTS scanned_events (
|
|
760
|
-
id TEXT PRIMARY KEY,
|
|
761
|
-
rpi TEXT NOT NULL,
|
|
762
|
-
metadata_key TEXT NOT NULL,
|
|
763
|
-
timestamp INTEGER NOT NULL
|
|
764
|
-
)
|
|
765
|
-
`);
|
|
766
|
-
await db.run(sql`
|
|
767
|
-
CREATE INDEX IF NOT EXISTS rpi_idx ON scanned_events(rpi)
|
|
768
|
-
`);
|
|
769
|
-
return db;
|
|
770
|
-
}
|
|
771
790
|
|
|
772
791
|
// src/index.ts
|
|
773
792
|
var VailixSDK = class _VailixSDK {
|
|
793
|
+
// Singleton instance and initialization promise for thread-safety
|
|
794
|
+
static instance = null;
|
|
795
|
+
static initPromise = null;
|
|
774
796
|
identity;
|
|
775
797
|
storage;
|
|
776
798
|
matcher;
|
|
@@ -790,11 +812,33 @@ var VailixSDK = class _VailixSDK {
|
|
|
790
812
|
this.rpiDurationMs = rpiDurationMs;
|
|
791
813
|
}
|
|
792
814
|
/**
|
|
793
|
-
* Create
|
|
815
|
+
* Create or return the singleton SDK instance.
|
|
816
|
+
*
|
|
817
|
+
* Thread-safe: concurrent calls will wait for the first initialization to complete
|
|
818
|
+
* and return the same instance. Config is only used on first initialization.
|
|
794
819
|
*
|
|
795
|
-
* @param config - Unified configuration object
|
|
820
|
+
* @param config - Unified configuration object (used only on first call)
|
|
796
821
|
*/
|
|
797
822
|
static async create(config) {
|
|
823
|
+
if (_VailixSDK.instance) {
|
|
824
|
+
return _VailixSDK.instance;
|
|
825
|
+
}
|
|
826
|
+
if (_VailixSDK.initPromise) {
|
|
827
|
+
return _VailixSDK.initPromise;
|
|
828
|
+
}
|
|
829
|
+
_VailixSDK.initPromise = _VailixSDK.doCreate(config);
|
|
830
|
+
try {
|
|
831
|
+
_VailixSDK.instance = await _VailixSDK.initPromise;
|
|
832
|
+
return _VailixSDK.instance;
|
|
833
|
+
} catch (error) {
|
|
834
|
+
_VailixSDK.initPromise = null;
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Internal initialization logic (extracted for singleton pattern).
|
|
840
|
+
*/
|
|
841
|
+
static async doCreate(config) {
|
|
798
842
|
const rpiDuration = config.rpiDurationMs ?? 15 * 60 * 1e3;
|
|
799
843
|
if (config.rescanIntervalMs && config.rescanIntervalMs > rpiDuration) {
|
|
800
844
|
throw new Error(`rescanIntervalMs (${config.rescanIntervalMs}) cannot exceed rpiDurationMs (${rpiDuration})`);
|
|
@@ -830,6 +874,23 @@ var VailixSDK = class _VailixSDK {
|
|
|
830
874
|
rpiDuration
|
|
831
875
|
);
|
|
832
876
|
}
|
|
877
|
+
/**
|
|
878
|
+
* Destroy the singleton instance and release all resources.
|
|
879
|
+
* Use for testing or when the app needs to fully reset the SDK.
|
|
880
|
+
*/
|
|
881
|
+
static async destroy() {
|
|
882
|
+
if (_VailixSDK.instance) {
|
|
883
|
+
_VailixSDK.instance.ble.destroy();
|
|
884
|
+
_VailixSDK.instance = null;
|
|
885
|
+
}
|
|
886
|
+
_VailixSDK.initPromise = null;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Check if the SDK has been initialized.
|
|
890
|
+
*/
|
|
891
|
+
static isInitialized() {
|
|
892
|
+
return _VailixSDK.instance !== null;
|
|
893
|
+
}
|
|
833
894
|
// ========================================================================
|
|
834
895
|
// QR Code Methods
|
|
835
896
|
// ========================================================================
|
package/package.json
CHANGED
package/src/ble.ts
CHANGED
|
@@ -21,6 +21,50 @@ const DEFAULT_PROXIMITY_THRESHOLD = -70;
|
|
|
21
21
|
|
|
22
22
|
import { generateDisplayName } from './utils';
|
|
23
23
|
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Internal Types (not exported - implementation details)
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Internal representation of a nearby user with additional fields for BLE management.
|
|
30
|
+
* The public NearbyUser type hides these implementation details.
|
|
31
|
+
*/
|
|
32
|
+
interface InternalNearbyUser {
|
|
33
|
+
id: string;
|
|
34
|
+
displayName: string;
|
|
35
|
+
rssi: number;
|
|
36
|
+
discoveredAt: number;
|
|
37
|
+
paired: boolean;
|
|
38
|
+
hasIncomingRequest: boolean;
|
|
39
|
+
pairedAt?: number;
|
|
40
|
+
/** First 8 bytes (16 hex chars) of RPI from advertisement */
|
|
41
|
+
rpiPrefix: string;
|
|
42
|
+
/** Full RPI (received via GATT exchange) */
|
|
43
|
+
fullRpi?: string;
|
|
44
|
+
/** Metadata key (received via GATT exchange) */
|
|
45
|
+
metadataKey?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pending incoming pair request (explicit consent mode).
|
|
50
|
+
* Holds data in memory until user accepts.
|
|
51
|
+
*/
|
|
52
|
+
interface PendingPairRequest {
|
|
53
|
+
fullRpi: string;
|
|
54
|
+
metadataKey: string;
|
|
55
|
+
receivedAt: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Configuration options for BleService constructor.
|
|
60
|
+
*/
|
|
61
|
+
interface BleServiceConfig {
|
|
62
|
+
discoveryTimeoutMs?: number;
|
|
63
|
+
proximityThreshold?: number;
|
|
64
|
+
autoAccept?: boolean;
|
|
65
|
+
serviceUUID?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
24
68
|
/**
|
|
25
69
|
* Extract RPI prefix from advertisement manufacturer data or service data.
|
|
26
70
|
*/
|
package/src/db.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/expo-sqlite';
|
|
2
|
-
import { openDatabaseSync, deleteDatabaseSync } from 'expo-sqlite';
|
|
2
|
+
import { openDatabaseSync, deleteDatabaseSync, type SQLiteDatabase } from 'expo-sqlite';
|
|
3
3
|
import { sql } from 'drizzle-orm';
|
|
4
4
|
import type { VailixDB } from './types';
|
|
5
5
|
|
|
@@ -13,45 +13,83 @@ const DB_NAME = 'vailix.db';
|
|
|
13
13
|
* @param masterKey The user's master key, used to derive encryption password
|
|
14
14
|
*/
|
|
15
15
|
export async function initializeDatabase(masterKey: string): Promise<VailixDB> {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
const result = await tryOpenEncryptedDatabase(masterKey);
|
|
17
|
+
|
|
18
|
+
if (result.success) {
|
|
19
|
+
return result.db;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Key mismatch: "file is not a database" or similar SQLCipher error
|
|
23
|
+
// This happens when DB was restored from backup but key is different
|
|
24
|
+
console.warn('Database key mismatch, recreating fresh database');
|
|
25
|
+
|
|
26
|
+
// Close the connection before deletion (required by SQLite)
|
|
27
|
+
if (result.expo) {
|
|
28
|
+
result.expo.closeSync();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
deleteDatabaseSync(DB_NAME);
|
|
32
|
+
|
|
33
|
+
// Retry - if this fails, let it throw (unrecoverable error)
|
|
34
|
+
const retryResult = await tryOpenEncryptedDatabase(masterKey);
|
|
35
|
+
if (!retryResult.success) {
|
|
36
|
+
if (retryResult.expo) {
|
|
37
|
+
retryResult.expo.closeSync();
|
|
38
|
+
}
|
|
39
|
+
throw retryResult.error;
|
|
24
40
|
}
|
|
41
|
+
|
|
42
|
+
return retryResult.db;
|
|
25
43
|
}
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
type OpenResult =
|
|
46
|
+
| { success: true; db: VailixDB; expo: SQLiteDatabase }
|
|
47
|
+
| { success: false; error: Error; expo: SQLiteDatabase | null };
|
|
30
48
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Attempt to open and configure the encrypted database.
|
|
51
|
+
* Returns a result object that includes the raw expo connection for cleanup.
|
|
52
|
+
*/
|
|
53
|
+
async function tryOpenEncryptedDatabase(masterKey: string): Promise<OpenResult> {
|
|
54
|
+
let expo: SQLiteDatabase | null = null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
expo = openDatabaseSync(DB_NAME);
|
|
58
|
+
const db = drizzle(expo);
|
|
59
|
+
|
|
60
|
+
// Validate key is hex to prevent SQL injection
|
|
61
|
+
if (!/^[0-9a-f]+$/i.test(masterKey)) {
|
|
62
|
+
throw new Error('Invalid master key format');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Enable SQLCipher encryption using master key as password
|
|
66
|
+
// This encrypts the entire database at rest (AES-256)
|
|
67
|
+
await db.run(sql.raw(`PRAGMA key = '${masterKey}'`));
|
|
68
|
+
|
|
69
|
+
// Verify key works by attempting a read operation
|
|
70
|
+
// SQLCipher will throw if key is wrong
|
|
71
|
+
await db.run(sql`SELECT 1`);
|
|
72
|
+
|
|
73
|
+
// Create schema
|
|
74
|
+
await db.run(sql`
|
|
75
|
+
CREATE TABLE IF NOT EXISTS scanned_events (
|
|
76
|
+
id TEXT PRIMARY KEY,
|
|
77
|
+
rpi TEXT NOT NULL,
|
|
78
|
+
metadata_key TEXT NOT NULL,
|
|
79
|
+
timestamp INTEGER NOT NULL
|
|
80
|
+
)
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
await db.run(sql`
|
|
84
|
+
CREATE INDEX IF NOT EXISTS rpi_idx ON scanned_events(rpi)
|
|
85
|
+
`);
|
|
86
|
+
|
|
87
|
+
return { success: true, db, expo };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
92
|
+
expo
|
|
93
|
+
};
|
|
36
94
|
}
|
|
37
|
-
await db.run(sql.raw(`PRAGMA key = '${masterKey}'`));
|
|
38
|
-
|
|
39
|
-
// Verify key works by attempting a read operation
|
|
40
|
-
// SQLCipher will throw if key is wrong
|
|
41
|
-
await db.run(sql`SELECT 1`);
|
|
42
|
-
|
|
43
|
-
await db.run(sql`
|
|
44
|
-
CREATE TABLE IF NOT EXISTS scanned_events (
|
|
45
|
-
id TEXT PRIMARY KEY,
|
|
46
|
-
rpi TEXT NOT NULL,
|
|
47
|
-
metadata_key TEXT NOT NULL,
|
|
48
|
-
timestamp INTEGER NOT NULL
|
|
49
|
-
)
|
|
50
|
-
`);
|
|
51
|
-
|
|
52
|
-
await db.run(sql`
|
|
53
|
-
CREATE INDEX IF NOT EXISTS rpi_idx ON scanned_events(rpi)
|
|
54
|
-
`);
|
|
55
|
-
|
|
56
|
-
return db;
|
|
57
95
|
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,10 @@ import type {
|
|
|
16
16
|
} from './types';
|
|
17
17
|
|
|
18
18
|
export class VailixSDK {
|
|
19
|
+
// Singleton instance and initialization promise for thread-safety
|
|
20
|
+
private static instance: VailixSDK | null = null;
|
|
21
|
+
private static initPromise: Promise<VailixSDK> | null = null;
|
|
22
|
+
|
|
19
23
|
public identity: IdentityManager;
|
|
20
24
|
public storage: StorageService;
|
|
21
25
|
public matcher: MatcherService;
|
|
@@ -46,11 +50,41 @@ export class VailixSDK {
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
|
-
* Create
|
|
53
|
+
* Create or return the singleton SDK instance.
|
|
54
|
+
*
|
|
55
|
+
* Thread-safe: concurrent calls will wait for the first initialization to complete
|
|
56
|
+
* and return the same instance. Config is only used on first initialization.
|
|
50
57
|
*
|
|
51
|
-
* @param config - Unified configuration object
|
|
58
|
+
* @param config - Unified configuration object (used only on first call)
|
|
52
59
|
*/
|
|
53
60
|
static async create(config: VailixConfig): Promise<VailixSDK> {
|
|
61
|
+
// Already initialized - return existing instance
|
|
62
|
+
if (VailixSDK.instance) {
|
|
63
|
+
return VailixSDK.instance;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialization in progress - wait for it (prevents race condition)
|
|
67
|
+
if (VailixSDK.initPromise) {
|
|
68
|
+
return VailixSDK.initPromise;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// First call - start initialization
|
|
72
|
+
VailixSDK.initPromise = VailixSDK.doCreate(config);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
VailixSDK.instance = await VailixSDK.initPromise;
|
|
76
|
+
return VailixSDK.instance;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Clear promise on failure so retry is possible
|
|
79
|
+
VailixSDK.initPromise = null;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Internal initialization logic (extracted for singleton pattern).
|
|
86
|
+
*/
|
|
87
|
+
private static async doCreate(config: VailixConfig): Promise<VailixSDK> {
|
|
54
88
|
// Validate: rescanInterval cannot exceed rpiDuration
|
|
55
89
|
const rpiDuration = config.rpiDurationMs ?? 15 * 60 * 1000; // Default 15 min
|
|
56
90
|
if (config.rescanIntervalMs && config.rescanIntervalMs > rpiDuration) {
|
|
@@ -99,6 +133,27 @@ export class VailixSDK {
|
|
|
99
133
|
);
|
|
100
134
|
}
|
|
101
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Destroy the singleton instance and release all resources.
|
|
138
|
+
* Use for testing or when the app needs to fully reset the SDK.
|
|
139
|
+
*/
|
|
140
|
+
static async destroy(): Promise<void> {
|
|
141
|
+
if (VailixSDK.instance) {
|
|
142
|
+
// Cleanup BLE resources
|
|
143
|
+
VailixSDK.instance.ble.destroy();
|
|
144
|
+
// Note: Database connection cleanup is handled by expo-sqlite
|
|
145
|
+
VailixSDK.instance = null;
|
|
146
|
+
}
|
|
147
|
+
VailixSDK.initPromise = null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if the SDK has been initialized.
|
|
152
|
+
*/
|
|
153
|
+
static isInitialized(): boolean {
|
|
154
|
+
return VailixSDK.instance !== null;
|
|
155
|
+
}
|
|
156
|
+
|
|
102
157
|
// ========================================================================
|
|
103
158
|
// QR Code Methods
|
|
104
159
|
// ========================================================================
|