@vailix/mask 0.1.2

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 ADDED
@@ -0,0 +1,13 @@
1
+ # @vailix/mask
2
+
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Initial public publish
8
+
9
+ ## 0.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Initial public release
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gil Eyni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,365 @@
1
+ # @vailix/mask
2
+
3
+ Privacy-preserving proximity tracing SDK for React Native + Expo.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @vailix/mask
9
+ ```
10
+
11
+ > **Note:** This package requires Expo Development Builds (not Expo Go) due to native dependencies.
12
+
13
+ ```bash
14
+ npx expo prebuild
15
+ npx expo run:android # or run:ios
16
+ ```
17
+
18
+ **Required:** Add SQLCipher to your `app.json`:
19
+
20
+ ```json
21
+ {
22
+ "expo": {
23
+ "plugins": [
24
+ ["expo-sqlite", { "useSQLCipher": true }]
25
+ ]
26
+ }
27
+ }
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```typescript
33
+ import { VailixSDK } from '@vailix/mask';
34
+
35
+ // Initialize SDK
36
+ const sdk = await VailixSDK.create({
37
+ reportUrl: process.env.VAILIX_REPORT_URL!,
38
+ downloadUrl: process.env.VAILIX_DOWNLOAD_URL!,
39
+ appSecret: process.env.VAILIX_APP_SECRET!,
40
+ });
41
+
42
+ // Set up event handlers
43
+ const cleanup = sdk.onMatch((matches) => {
44
+ console.log('Exposure detected:', matches);
45
+ });
46
+
47
+ // Start background sync
48
+ await sdk.matcher.fetchAndMatch();
49
+ ```
50
+
51
+ ## Configuration
52
+
53
+ | Option | Type | Default | Description |
54
+ |--------|------|---------|-------------|
55
+ | `reportUrl` | string | required | API endpoint for submitting reports |
56
+ | `downloadUrl` | string | required | API endpoint for downloading infected keys |
57
+ | `appSecret` | string | required | Shared secret for API authentication |
58
+ | `rpiDurationMs` | number | 900000 (15min) | How long each RPI persists. Use 86400000 (24h) for STD apps |
59
+ | `rescanIntervalMs` | number | 0 | Minimum time between scans of same RPI. 0 = no limit |
60
+ | `reportDays` | number | 14 | Days of history to upload when reporting |
61
+ | `keyStorage` | KeyStorage | SecureStore | Custom key storage adapter (for cloud backup) |
62
+ | `bleDiscoveryTimeoutMs` | number | 15000 | Timeout before removing user from nearby list |
63
+ | `proximityThreshold` | number | -70 | Minimum RSSI to consider a user "nearby" |
64
+ | `autoAcceptIncomingPairs` | boolean | true | Auto-accept incoming pair requests |
65
+
66
+ ### Example Configuration
67
+
68
+ ```typescript
69
+ const sdk = await VailixSDK.create({
70
+ reportUrl: 'https://api.yourapp.com',
71
+ downloadUrl: 'https://api.yourapp.com',
72
+ appSecret: 'your-secret-key',
73
+ rpiDurationMs: 24 * 60 * 60 * 1000, // 24 hours
74
+ rescanIntervalMs: 24 * 60 * 60 * 1000, // Block rescan for 24h
75
+ reportDays: 14,
76
+ bleDiscoveryTimeoutMs: 15000, // 15 seconds
77
+ proximityThreshold: -70, // ~1-2 meters
78
+ autoAcceptIncomingPairs: true, // Instant mutual exchange
79
+ });
80
+ ```
81
+
82
+ ## Pairing Methods
83
+
84
+ Both methods result in **mutual notification** — if either user reports positive, the other is notified.
85
+
86
+ ### BLE Pairing (Recommended)
87
+
88
+ One-tap proximity-based pairing via Bluetooth Low Energy:
89
+
90
+ ```typescript
91
+ import { useState, useEffect } from 'react';
92
+ import { NearbyUser } from '@vailix/mask';
93
+
94
+ function PairingScreen({ sdk }) {
95
+ const [nearbyUsers, setNearbyUsers] = useState<NearbyUser[]>([]);
96
+
97
+ useEffect(() => {
98
+ // Start discovery when screen opens
99
+ sdk.startDiscovery();
100
+
101
+ // Subscribe to nearby user updates
102
+ const cleanup = sdk.onNearbyUsersChanged(setNearbyUsers);
103
+
104
+ return () => {
105
+ cleanup();
106
+ sdk.stopDiscovery();
107
+ };
108
+ }, []);
109
+
110
+ const handlePair = async (user: NearbyUser) => {
111
+ const result = await sdk.pairWithUser(user.id);
112
+ if (result.success) {
113
+ console.log('Paired successfully with', user.displayName);
114
+ }
115
+ };
116
+
117
+ return (
118
+ <FlatList
119
+ data={nearbyUsers}
120
+ renderItem={({ item }) => (
121
+ <View>
122
+ <Text>{item.displayName}</Text>
123
+ {item.paired ? (
124
+ <Text>✓ Paired</Text>
125
+ ) : (
126
+ <Button title="Pair" onPress={() => handlePair(item)} />
127
+ )}
128
+ </View>
129
+ )}
130
+ />
131
+ );
132
+ }
133
+ ```
134
+
135
+ **User Flow:**
136
+ 1. Both users open the pairing screen
137
+ 2. They see each other in the nearby users list (e.g., "🐼 42", "🦊 17")
138
+ 3. Either user taps to pair
139
+ 4. Both phones exchange data automatically
140
+
141
+ ### QR Code Pairing (Fallback)
142
+
143
+ Both users must scan each other's QR code:
144
+
145
+ ```typescript
146
+ // User A shows QR
147
+ const qrData = sdk.getQRCode();
148
+ // Display qrData as QR code
149
+
150
+ // User B scans
151
+ const success = await sdk.scanQR(scannedData);
152
+
153
+ // Then swap: B shows, A scans
154
+ ```
155
+
156
+ ## BLE Configuration
157
+
158
+ ### iOS Configuration
159
+
160
+ Add to `ios/[AppName]/Info.plist`:
161
+
162
+ ```xml
163
+ <key>NSBluetoothAlwaysUsageDescription</key>
164
+ <string>Used to discover and pair with nearby users</string>
165
+ <key>NSBluetoothPeripheralUsageDescription</key>
166
+ <string>Used to allow other users to discover you</string>
167
+ ```
168
+
169
+ ### Android Configuration
170
+
171
+ Add to `android/app/src/main/AndroidManifest.xml`:
172
+
173
+ ```xml
174
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
175
+ <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
176
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
177
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
178
+ ```
179
+
180
+ ## Explicit Consent Mode
181
+
182
+ By default, pair requests are auto-accepted. For apps requiring explicit consent:
183
+
184
+ ```typescript
185
+ const sdk = await VailixSDK.create({
186
+ ...config,
187
+ autoAcceptIncomingPairs: false, // Require explicit acceptance
188
+ });
189
+
190
+ // In your pairing screen:
191
+ {item.hasIncomingRequest ? (
192
+ <Button title="Accept" onPress={() => sdk.pairWithUser(item.id)} />
193
+ ) : item.paired ? (
194
+ <Text>✓ Paired</Text>
195
+ ) : (
196
+ <Button title="Pair" onPress={() => handlePair(item)} />
197
+ )}
198
+ ```
199
+
200
+ ## Reporting Positive
201
+
202
+ When a user tests positive:
203
+
204
+ ```typescript
205
+ const success = await sdk.report(
206
+ attestToken, // Optional: Firebase App Check token
207
+ { // Optional: metadata (encrypted per-contact)
208
+ condition: 'chlamydia',
209
+ testDate: '2025-01-05',
210
+ }
211
+ );
212
+ ```
213
+
214
+ This uploads the user's last 14 days of RPIs (configurable via `reportDays`).
215
+
216
+ ## Receiving Notifications
217
+
218
+ ```typescript
219
+ // Subscribe to matches
220
+ const cleanup = sdk.onMatch((matches) => {
221
+ for (const match of matches) {
222
+ console.log('Exposure on:', new Date(match.timestamp));
223
+ console.log('Reported at:', new Date(match.reportedAt));
224
+ console.log('Details:', match.metadata); // Decrypted per-contact
225
+ }
226
+ });
227
+
228
+ // Trigger sync (call this in background fetch)
229
+ await sdk.matcher.fetchAndMatch();
230
+
231
+ // Cleanup when component unmounts (React)
232
+ useEffect(() => {
233
+ return sdk.onMatch(handler); // Returns cleanup function
234
+ }, []);
235
+ ```
236
+
237
+ ## Error Handling
238
+
239
+ ```typescript
240
+ const cleanup = sdk.onError((error) => {
241
+ console.error('SDK error:', error);
242
+ });
243
+ ```
244
+
245
+ ## Data Recovery
246
+
247
+ The SDK uses SQLCipher AES-256 encryption for all stored data. Recovery behavior depends on whether you implement `keyStorage`.
248
+
249
+ ### Without keyStorage (Default)
250
+
251
+ Master key stored locally only. On reinstall:
252
+ - New master key generated → new identity
253
+ - Old scan history unreadable (wrong key)
254
+ - SDK automatically deletes corrupted database
255
+
256
+ ### With keyStorage (Recommended)
257
+
258
+ Enable cross-device recovery by syncing the master key:
259
+
260
+ ```typescript
261
+ // Custom storage for iCloud/Google Drive sync
262
+ // Recommended library: react-native-cloud-storage
263
+ import { CloudStorage } from 'react-native-cloud-storage';
264
+
265
+ const sdk = await VailixSDK.create({
266
+ ...config,
267
+ keyStorage: {
268
+ getKey: () => CloudStorage.getItem('vailix_key'),
269
+ setKey: (k) => CloudStorage.setItem('vailix_key', k),
270
+ },
271
+ });
272
+ ```
273
+
274
+ ### User Experience Flow
275
+
276
+ 1. **First install:** SDK generates key → `setKey()` saves to cloud
277
+ 2. **Normal use:** Scans saved locally, OS backs up database
278
+ 3. **Phone lost:** Key in cloud, database in iCloud/Google backup
279
+ 4. **New phone:** User signs into same Apple ID/Google Account
280
+ 5. **Reinstall:** SDK calls `getKey()` → returns existing key → opens database
281
+
282
+ **No user action required** except signing into the same cloud account.
283
+
284
+ ### Recovery Scenarios
285
+
286
+ | Scenario | Master Key | Database | Result |
287
+ |----------|------------|----------|--------|
288
+ | Both recovered | ✅ From cloud | ✅ From backup | Full restore |
289
+ | Key only | ✅ From cloud | ❌ Empty | Fresh DB, can still report |
290
+ | New key, old DB | ❌ New | ⚠️ Wrong key | SDK deletes old DB, starts fresh |
291
+ | Neither | ❌ New | ❌ Empty | New identity |
292
+
293
+ ### KeyStorage Interface
294
+
295
+ ```typescript
296
+ interface KeyStorage {
297
+ getKey(): Promise<string | null>;
298
+ setKey(key: string): Promise<void>;
299
+ }
300
+ ```
301
+
302
+ > [!IMPORTANT]
303
+ > The SDK does NOT require app-level login. Cloud sync uses device-level Apple ID / Google Account.
304
+
305
+ ## API Reference
306
+
307
+ ### VailixSDK
308
+
309
+ | Method | Description |
310
+ |--------|-------------|
311
+ | `VailixSDK.create(config)` | Initialize SDK with VailixConfig |
312
+ | `VailixSDK.isBleSupported()` | Check if device supports BLE |
313
+ | `sdk.getQRCode()` | Get QR code data for display |
314
+ | `sdk.scanQR(data)` | Scan and store another user's QR |
315
+ | `sdk.startDiscovery()` | Start BLE discovery (call when pairing screen opens) |
316
+ | `sdk.stopDiscovery()` | Stop BLE discovery (call when leaving pairing screen) |
317
+ | `sdk.getNearbyUsers()` | Get current list of nearby users |
318
+ | `sdk.onNearbyUsersChanged(handler)` | Subscribe to nearby user updates |
319
+ | `sdk.pairWithUser(userId)` | Pair with a specific user |
320
+ | `sdk.unpairUser(userId)` | Unpair/reject a user |
321
+ | `sdk.report(token?, metadata?)` | Report positive |
322
+ | `sdk.onMatch(handler)` | Subscribe to exposure matches |
323
+ | `sdk.offMatch(handler)` | Unsubscribe from matches |
324
+ | `sdk.onError(handler)` | Subscribe to errors |
325
+ | `sdk.offError(handler)` | Unsubscribe from errors |
326
+ | `sdk.matcher.fetchAndMatch()` | Sync and check for matches |
327
+
328
+ ### NearbyUser Object
329
+
330
+ ```typescript
331
+ interface NearbyUser {
332
+ id: string; // Opaque identifier for pairing
333
+ displayName: string; // Generated emoji + number (e.g., "🐼 42")
334
+ rssi: number; // Signal strength
335
+ discoveredAt: number; // Timestamp of last advertisement
336
+ paired: boolean; // true if already paired
337
+ hasIncomingRequest: boolean; // true if they sent a pair request (explicit mode)
338
+ }
339
+ ```
340
+
341
+ ### PairResult Object
342
+
343
+ ```typescript
344
+ interface PairResult {
345
+ success: boolean;
346
+ partnerRpi?: string;
347
+ partnerMetadataKey?: string;
348
+ error?: string;
349
+ }
350
+ ```
351
+
352
+ ### Match Object
353
+
354
+ ```typescript
355
+ interface Match {
356
+ rpi: string; // The matched identifier
357
+ timestamp: number; // When the contact occurred
358
+ metadata?: object; // Decrypted reporter metadata
359
+ reportedAt?: number; // When the report was submitted
360
+ }
361
+ ```
362
+
363
+ ## License
364
+
365
+ MIT