@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/ANALYSIS_REPORT.md +211 -0
- package/BLE_IMPLEMENTATION_PLAN.md +703 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +365 -0
- package/dist/index.d.mts +347 -0
- package/dist/index.d.ts +347 -0
- package/dist/index.js +1095 -0
- package/dist/index.mjs +1058 -0
- package/package.json +62 -0
- package/src/ble.ts +504 -0
- package/src/db.ts +57 -0
- package/src/identity.ts +91 -0
- package/src/index.ts +375 -0
- package/src/matcher.ts +224 -0
- package/src/storage.ts +110 -0
- package/src/transport.ts +20 -0
- package/src/types.ts +100 -0
- package/src/utils.ts +20 -0
- package/tsconfig.json +15 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { ExpoSQLiteDatabase } from 'drizzle-orm/expo-sqlite';
|
|
2
|
+
import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
|
|
3
|
+
import { EventEmitter } from 'eventemitter3';
|
|
4
|
+
|
|
5
|
+
interface ReportMetadata {
|
|
6
|
+
[key: string]: string | number | boolean | (string | number | boolean)[] | undefined;
|
|
7
|
+
}
|
|
8
|
+
interface Match {
|
|
9
|
+
rpi: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
metadata?: ReportMetadata;
|
|
12
|
+
reportedAt?: number;
|
|
13
|
+
}
|
|
14
|
+
interface KeyStorage {
|
|
15
|
+
getKey(): Promise<string | null>;
|
|
16
|
+
setKey(key: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
type MatchHandler = (matches: Match[]) => void;
|
|
19
|
+
type VailixDB = ExpoSQLiteDatabase<Record<string, never>>;
|
|
20
|
+
/**
|
|
21
|
+
* Represents a nearby user discovered via BLE.
|
|
22
|
+
*
|
|
23
|
+
* Design: Internal details (RPI, metadataKey) are hidden from the public API.
|
|
24
|
+
* The app only needs: something to display, something to pair with, and status flags.
|
|
25
|
+
* RPIs and keys are managed internally by the package for contact tracing.
|
|
26
|
+
*/
|
|
27
|
+
interface NearbyUser {
|
|
28
|
+
/** Opaque identifier for pairing (pass to pairWithUser) */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Generated emoji + number for UI display */
|
|
31
|
+
displayName: string;
|
|
32
|
+
/** Signal strength (for proximity filtering) */
|
|
33
|
+
rssi: number;
|
|
34
|
+
/** Timestamp of last advertisement received */
|
|
35
|
+
discoveredAt: number;
|
|
36
|
+
/** true if we have securely stored their data */
|
|
37
|
+
paired: boolean;
|
|
38
|
+
/** true if they sent us data but we haven't accepted yet (explicit mode) */
|
|
39
|
+
hasIncomingRequest: boolean;
|
|
40
|
+
/** Timestamp when pairing occurred (optional, populated after successful pair) */
|
|
41
|
+
pairedAt?: number;
|
|
42
|
+
}
|
|
43
|
+
/** Result of a pairing attempt */
|
|
44
|
+
interface PairResult {
|
|
45
|
+
success: boolean;
|
|
46
|
+
partnerRpi?: string;
|
|
47
|
+
partnerMetadataKey?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Unified SDK Configuration Interface
|
|
52
|
+
*/
|
|
53
|
+
interface VailixConfig {
|
|
54
|
+
/** The live API endpoint for submitting reports */
|
|
55
|
+
reportUrl: string;
|
|
56
|
+
/** The endpoint for downloading keys (can be the same as reportUrl or a CDN) */
|
|
57
|
+
downloadUrl: string;
|
|
58
|
+
/** Application secret for API authentication */
|
|
59
|
+
appSecret: string;
|
|
60
|
+
/** Custom key storage adapter (default: expo-secure-store) */
|
|
61
|
+
keyStorage?: KeyStorage;
|
|
62
|
+
/** Number of days of history to include in reports (default: 14) */
|
|
63
|
+
reportDays?: number;
|
|
64
|
+
/** How long RPI persists in ms (default: 15min, can be 24h for STD apps) */
|
|
65
|
+
rpiDurationMs?: number;
|
|
66
|
+
/** Minimum time between scans of same RPI in ms (default: 0 = no limit) */
|
|
67
|
+
rescanIntervalMs?: number;
|
|
68
|
+
/** Timeout before removing a user from nearby list in ms (default: 15000) */
|
|
69
|
+
bleDiscoveryTimeoutMs?: number;
|
|
70
|
+
/** Minimum RSSI to consider a user "nearby" (default: -70) */
|
|
71
|
+
proximityThreshold?: number;
|
|
72
|
+
/** If true, automatically pair when receiving a write (default: true) */
|
|
73
|
+
autoAcceptIncomingPairs?: boolean;
|
|
74
|
+
/** Custom BLE Service UUID (default: a1b2c3d4-e5f6-7890-abcd-ef1234567890) */
|
|
75
|
+
serviceUUID?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare class IdentityManager {
|
|
79
|
+
private masterKey;
|
|
80
|
+
private epochMs;
|
|
81
|
+
private keyStorage;
|
|
82
|
+
constructor(config?: {
|
|
83
|
+
rpiDurationMs?: number;
|
|
84
|
+
keyStorage?: KeyStorage;
|
|
85
|
+
});
|
|
86
|
+
initialize(): Promise<void>;
|
|
87
|
+
getCurrentRPI(): string;
|
|
88
|
+
getHistory(days: number): string[];
|
|
89
|
+
private _generateRPI;
|
|
90
|
+
getMetadataKey(rpi: string): string;
|
|
91
|
+
getMasterKey(): string;
|
|
92
|
+
/**
|
|
93
|
+
* Get anonymous display name derived from current RPI.
|
|
94
|
+
* Used for showing user's identity in UI without revealing master key.
|
|
95
|
+
* Format: "Emoji Number" (e.g. "🐼 42")
|
|
96
|
+
*/
|
|
97
|
+
getDisplayName(): string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
declare const scannedEvents: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
101
|
+
name: "scanned_events";
|
|
102
|
+
schema: undefined;
|
|
103
|
+
columns: {
|
|
104
|
+
id: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
105
|
+
name: "id";
|
|
106
|
+
tableName: "scanned_events";
|
|
107
|
+
dataType: "string";
|
|
108
|
+
columnType: "SQLiteText";
|
|
109
|
+
data: string;
|
|
110
|
+
driverParam: string;
|
|
111
|
+
notNull: true;
|
|
112
|
+
hasDefault: false;
|
|
113
|
+
isPrimaryKey: true;
|
|
114
|
+
isAutoincrement: false;
|
|
115
|
+
hasRuntimeDefault: false;
|
|
116
|
+
enumValues: [string, ...string[]];
|
|
117
|
+
baseColumn: never;
|
|
118
|
+
identity: undefined;
|
|
119
|
+
generated: undefined;
|
|
120
|
+
}, {}, {
|
|
121
|
+
length: number | undefined;
|
|
122
|
+
}>;
|
|
123
|
+
rpi: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
124
|
+
name: "rpi";
|
|
125
|
+
tableName: "scanned_events";
|
|
126
|
+
dataType: "string";
|
|
127
|
+
columnType: "SQLiteText";
|
|
128
|
+
data: string;
|
|
129
|
+
driverParam: string;
|
|
130
|
+
notNull: true;
|
|
131
|
+
hasDefault: false;
|
|
132
|
+
isPrimaryKey: false;
|
|
133
|
+
isAutoincrement: false;
|
|
134
|
+
hasRuntimeDefault: false;
|
|
135
|
+
enumValues: [string, ...string[]];
|
|
136
|
+
baseColumn: never;
|
|
137
|
+
identity: undefined;
|
|
138
|
+
generated: undefined;
|
|
139
|
+
}, {}, {
|
|
140
|
+
length: number | undefined;
|
|
141
|
+
}>;
|
|
142
|
+
metadataKey: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
143
|
+
name: "metadata_key";
|
|
144
|
+
tableName: "scanned_events";
|
|
145
|
+
dataType: "string";
|
|
146
|
+
columnType: "SQLiteText";
|
|
147
|
+
data: string;
|
|
148
|
+
driverParam: string;
|
|
149
|
+
notNull: true;
|
|
150
|
+
hasDefault: false;
|
|
151
|
+
isPrimaryKey: false;
|
|
152
|
+
isAutoincrement: false;
|
|
153
|
+
hasRuntimeDefault: false;
|
|
154
|
+
enumValues: [string, ...string[]];
|
|
155
|
+
baseColumn: never;
|
|
156
|
+
identity: undefined;
|
|
157
|
+
generated: undefined;
|
|
158
|
+
}, {}, {
|
|
159
|
+
length: number | undefined;
|
|
160
|
+
}>;
|
|
161
|
+
timestamp: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
162
|
+
name: "timestamp";
|
|
163
|
+
tableName: "scanned_events";
|
|
164
|
+
dataType: "number";
|
|
165
|
+
columnType: "SQLiteInteger";
|
|
166
|
+
data: number;
|
|
167
|
+
driverParam: number;
|
|
168
|
+
notNull: true;
|
|
169
|
+
hasDefault: false;
|
|
170
|
+
isPrimaryKey: false;
|
|
171
|
+
isAutoincrement: false;
|
|
172
|
+
hasRuntimeDefault: false;
|
|
173
|
+
enumValues: undefined;
|
|
174
|
+
baseColumn: never;
|
|
175
|
+
identity: undefined;
|
|
176
|
+
generated: undefined;
|
|
177
|
+
}, {}, {}>;
|
|
178
|
+
};
|
|
179
|
+
dialect: "sqlite";
|
|
180
|
+
}>;
|
|
181
|
+
/** Type for a scanned event row */
|
|
182
|
+
type ScannedEvent = typeof scannedEvents.$inferSelect;
|
|
183
|
+
declare class StorageService {
|
|
184
|
+
private db;
|
|
185
|
+
private rescanIntervalMs;
|
|
186
|
+
private lastScanByRpi;
|
|
187
|
+
private static readonly MAX_SCAN_HISTORY_SIZE;
|
|
188
|
+
constructor(db: VailixDB, config?: {
|
|
189
|
+
rescanIntervalMs?: number;
|
|
190
|
+
});
|
|
191
|
+
initialize(): Promise<void>;
|
|
192
|
+
canScan(rpi: string): boolean;
|
|
193
|
+
logScan(rpi: string, metadataKey: string, timestamp: number): Promise<void>;
|
|
194
|
+
cleanupOldScans(): Promise<void>;
|
|
195
|
+
getMatchingScans(rpiList: string[]): Promise<ScannedEvent[]>;
|
|
196
|
+
getRecentPairs(withinHours?: number): Promise<ScannedEvent[]>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
declare class MatcherService extends EventEmitter {
|
|
200
|
+
private storage;
|
|
201
|
+
private downloadUrl;
|
|
202
|
+
private appSecret;
|
|
203
|
+
constructor(storage: StorageService, downloadUrl: string, appSecret: string);
|
|
204
|
+
fetchAndMatch(): Promise<Match[]>;
|
|
205
|
+
private _downloadAndProcessKeys;
|
|
206
|
+
private _parseBinaryResponse;
|
|
207
|
+
private _decrypt;
|
|
208
|
+
/**
|
|
209
|
+
* Retrieve a specific match by RPI with on-demand decryption.
|
|
210
|
+
* Used when app needs to display exposure details to user.
|
|
211
|
+
*
|
|
212
|
+
* PRIVACY: Decrypted metadata exists ONLY in-memory (return value).
|
|
213
|
+
* Never persisted to storage.
|
|
214
|
+
*
|
|
215
|
+
* @param rpi - The RPI (match ID) to retrieve
|
|
216
|
+
* @returns Match with decrypted metadata, or null if not found
|
|
217
|
+
*/
|
|
218
|
+
getMatchById(rpi: string): Promise<Match | null>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
declare function formatQR(rpi: string, metadataKey: string): string;
|
|
222
|
+
declare function parseQR(data: string): ParsedQR | null;
|
|
223
|
+
interface ParsedQR {
|
|
224
|
+
rpi: string;
|
|
225
|
+
timestamp: number;
|
|
226
|
+
metadataKey: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
declare class VailixSDK {
|
|
230
|
+
identity: IdentityManager;
|
|
231
|
+
storage: StorageService;
|
|
232
|
+
matcher: MatcherService;
|
|
233
|
+
private ble;
|
|
234
|
+
private reportUrl;
|
|
235
|
+
private appSecret;
|
|
236
|
+
private reportDays;
|
|
237
|
+
private rpiDurationMs;
|
|
238
|
+
private constructor();
|
|
239
|
+
/**
|
|
240
|
+
* Create and initialize the VailixSDK.
|
|
241
|
+
*
|
|
242
|
+
* @param config - Unified configuration object
|
|
243
|
+
*/
|
|
244
|
+
static create(config: VailixConfig): Promise<VailixSDK>;
|
|
245
|
+
/** Get current QR code data */
|
|
246
|
+
getQRCode(): string;
|
|
247
|
+
/**
|
|
248
|
+
* Scan another user's QR code and log it.
|
|
249
|
+
* Returns false if: QR invalid, expired (>RPI duration), rescan blocked, or error
|
|
250
|
+
*/
|
|
251
|
+
scanQR(qrData: string): Promise<boolean>;
|
|
252
|
+
/**
|
|
253
|
+
* Check if BLE is supported on this device.
|
|
254
|
+
*/
|
|
255
|
+
static isBleSupported(): Promise<boolean>;
|
|
256
|
+
/**
|
|
257
|
+
* Start BLE discovery (call when pairing screen opens).
|
|
258
|
+
* Begins advertising our RPI and scanning for nearby users.
|
|
259
|
+
*/
|
|
260
|
+
startDiscovery(): Promise<void>;
|
|
261
|
+
/**
|
|
262
|
+
* Stop BLE discovery (call when leaving pairing screen).
|
|
263
|
+
*/
|
|
264
|
+
stopDiscovery(): Promise<void>;
|
|
265
|
+
/**
|
|
266
|
+
* Get current list of nearby users.
|
|
267
|
+
*/
|
|
268
|
+
getNearbyUsers(): NearbyUser[];
|
|
269
|
+
/**
|
|
270
|
+
* Subscribe to nearby user updates.
|
|
271
|
+
* Returns cleanup function for React useEffect compatibility.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* useEffect(() => {
|
|
275
|
+
* const cleanup = sdk.onNearbyUsersChanged(setNearbyUsers);
|
|
276
|
+
* return cleanup;
|
|
277
|
+
* }, []);
|
|
278
|
+
*/
|
|
279
|
+
onNearbyUsersChanged(callback: (users: NearbyUser[]) => void): () => void;
|
|
280
|
+
/**
|
|
281
|
+
* Pair with a specific user (one-tap action).
|
|
282
|
+
* In explicit consent mode, this also accepts pending incoming requests.
|
|
283
|
+
*
|
|
284
|
+
* @param userId - The user's ID from NearbyUser.id
|
|
285
|
+
*/
|
|
286
|
+
pairWithUser(userId: string): Promise<PairResult>;
|
|
287
|
+
/**
|
|
288
|
+
* Unpair with a user (removes from storage and resets status).
|
|
289
|
+
* Useful for "undo" functionality or rejecting requests.
|
|
290
|
+
*
|
|
291
|
+
* @param userId - The user's ID from NearbyUser.id
|
|
292
|
+
*/
|
|
293
|
+
unpairUser(userId: string): Promise<void>;
|
|
294
|
+
/**
|
|
295
|
+
* Get list of recent pairings from storage (for history/recap features).
|
|
296
|
+
* Returns pairs from the last N hours (default: 24h).
|
|
297
|
+
*
|
|
298
|
+
* @param withinHours - Look back window in hours (default: 24)
|
|
299
|
+
* @returns Array of NearbyUser objects representing recent pairs
|
|
300
|
+
*/
|
|
301
|
+
getRecentPairs(withinHours?: number): Promise<NearbyUser[]>;
|
|
302
|
+
/**
|
|
303
|
+
* Report positive (upload configured days of history).
|
|
304
|
+
*
|
|
305
|
+
* @param attestToken - Optional attestation token (e.g., Firebase App Check)
|
|
306
|
+
* @param metadata - App-specific data (e.g., STD type, test date). If null, a generic positive is reported.
|
|
307
|
+
* @param overrideReportDays - Optional: Override reportDays for this specific report
|
|
308
|
+
* (e.g., for apps with per-condition exposure windows)
|
|
309
|
+
*/
|
|
310
|
+
report(attestToken?: string, metadata?: ReportMetadata, overrideReportDays?: number): Promise<boolean>;
|
|
311
|
+
private static readonly MAX_METADATA_SIZE;
|
|
312
|
+
private _encrypt;
|
|
313
|
+
/**
|
|
314
|
+
* Subscribe to match events.
|
|
315
|
+
* Returns cleanup function for React useEffect compatibility.
|
|
316
|
+
*/
|
|
317
|
+
onMatch(handler: MatchHandler): () => void;
|
|
318
|
+
/**
|
|
319
|
+
* Subscribe to error events.
|
|
320
|
+
* Returns cleanup function for React useEffect compatibility.
|
|
321
|
+
*/
|
|
322
|
+
onError(handler: (error: Error) => void): () => void;
|
|
323
|
+
/** Explicit cleanup for match handler */
|
|
324
|
+
offMatch(handler: MatchHandler): void;
|
|
325
|
+
/** Explicit cleanup for error handler */
|
|
326
|
+
offError(handler: (error: Error) => void): void;
|
|
327
|
+
/**
|
|
328
|
+
* Get a specific match by ID with decrypted metadata.
|
|
329
|
+
* Used for on-demand decryption when user views exposure details.
|
|
330
|
+
*
|
|
331
|
+
* @param matchId - RPI of the match to retrieve
|
|
332
|
+
* @returns Match with decrypted metadata, or null if not found
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* // App calls this when user taps on notification
|
|
336
|
+
* const match = await sdk.getMatchById('abc123');
|
|
337
|
+
* console.log(match.metadata.conditions); // ["HIV", "Syphilis"]
|
|
338
|
+
*/
|
|
339
|
+
getMatchById(matchId: string): Promise<Match | null>;
|
|
340
|
+
/**
|
|
341
|
+
* Get own display name (emoji + number derived from current RPI).
|
|
342
|
+
* Used for showing user's anonymous identity in UI.
|
|
343
|
+
*/
|
|
344
|
+
getOwnDisplayName(): string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export { type KeyStorage, type Match, type MatchHandler, type NearbyUser, type PairResult, type ReportMetadata, type VailixConfig, VailixSDK, formatQR, parseQR };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { ExpoSQLiteDatabase } from 'drizzle-orm/expo-sqlite';
|
|
2
|
+
import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
|
|
3
|
+
import { EventEmitter } from 'eventemitter3';
|
|
4
|
+
|
|
5
|
+
interface ReportMetadata {
|
|
6
|
+
[key: string]: string | number | boolean | (string | number | boolean)[] | undefined;
|
|
7
|
+
}
|
|
8
|
+
interface Match {
|
|
9
|
+
rpi: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
metadata?: ReportMetadata;
|
|
12
|
+
reportedAt?: number;
|
|
13
|
+
}
|
|
14
|
+
interface KeyStorage {
|
|
15
|
+
getKey(): Promise<string | null>;
|
|
16
|
+
setKey(key: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
type MatchHandler = (matches: Match[]) => void;
|
|
19
|
+
type VailixDB = ExpoSQLiteDatabase<Record<string, never>>;
|
|
20
|
+
/**
|
|
21
|
+
* Represents a nearby user discovered via BLE.
|
|
22
|
+
*
|
|
23
|
+
* Design: Internal details (RPI, metadataKey) are hidden from the public API.
|
|
24
|
+
* The app only needs: something to display, something to pair with, and status flags.
|
|
25
|
+
* RPIs and keys are managed internally by the package for contact tracing.
|
|
26
|
+
*/
|
|
27
|
+
interface NearbyUser {
|
|
28
|
+
/** Opaque identifier for pairing (pass to pairWithUser) */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Generated emoji + number for UI display */
|
|
31
|
+
displayName: string;
|
|
32
|
+
/** Signal strength (for proximity filtering) */
|
|
33
|
+
rssi: number;
|
|
34
|
+
/** Timestamp of last advertisement received */
|
|
35
|
+
discoveredAt: number;
|
|
36
|
+
/** true if we have securely stored their data */
|
|
37
|
+
paired: boolean;
|
|
38
|
+
/** true if they sent us data but we haven't accepted yet (explicit mode) */
|
|
39
|
+
hasIncomingRequest: boolean;
|
|
40
|
+
/** Timestamp when pairing occurred (optional, populated after successful pair) */
|
|
41
|
+
pairedAt?: number;
|
|
42
|
+
}
|
|
43
|
+
/** Result of a pairing attempt */
|
|
44
|
+
interface PairResult {
|
|
45
|
+
success: boolean;
|
|
46
|
+
partnerRpi?: string;
|
|
47
|
+
partnerMetadataKey?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Unified SDK Configuration Interface
|
|
52
|
+
*/
|
|
53
|
+
interface VailixConfig {
|
|
54
|
+
/** The live API endpoint for submitting reports */
|
|
55
|
+
reportUrl: string;
|
|
56
|
+
/** The endpoint for downloading keys (can be the same as reportUrl or a CDN) */
|
|
57
|
+
downloadUrl: string;
|
|
58
|
+
/** Application secret for API authentication */
|
|
59
|
+
appSecret: string;
|
|
60
|
+
/** Custom key storage adapter (default: expo-secure-store) */
|
|
61
|
+
keyStorage?: KeyStorage;
|
|
62
|
+
/** Number of days of history to include in reports (default: 14) */
|
|
63
|
+
reportDays?: number;
|
|
64
|
+
/** How long RPI persists in ms (default: 15min, can be 24h for STD apps) */
|
|
65
|
+
rpiDurationMs?: number;
|
|
66
|
+
/** Minimum time between scans of same RPI in ms (default: 0 = no limit) */
|
|
67
|
+
rescanIntervalMs?: number;
|
|
68
|
+
/** Timeout before removing a user from nearby list in ms (default: 15000) */
|
|
69
|
+
bleDiscoveryTimeoutMs?: number;
|
|
70
|
+
/** Minimum RSSI to consider a user "nearby" (default: -70) */
|
|
71
|
+
proximityThreshold?: number;
|
|
72
|
+
/** If true, automatically pair when receiving a write (default: true) */
|
|
73
|
+
autoAcceptIncomingPairs?: boolean;
|
|
74
|
+
/** Custom BLE Service UUID (default: a1b2c3d4-e5f6-7890-abcd-ef1234567890) */
|
|
75
|
+
serviceUUID?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare class IdentityManager {
|
|
79
|
+
private masterKey;
|
|
80
|
+
private epochMs;
|
|
81
|
+
private keyStorage;
|
|
82
|
+
constructor(config?: {
|
|
83
|
+
rpiDurationMs?: number;
|
|
84
|
+
keyStorage?: KeyStorage;
|
|
85
|
+
});
|
|
86
|
+
initialize(): Promise<void>;
|
|
87
|
+
getCurrentRPI(): string;
|
|
88
|
+
getHistory(days: number): string[];
|
|
89
|
+
private _generateRPI;
|
|
90
|
+
getMetadataKey(rpi: string): string;
|
|
91
|
+
getMasterKey(): string;
|
|
92
|
+
/**
|
|
93
|
+
* Get anonymous display name derived from current RPI.
|
|
94
|
+
* Used for showing user's identity in UI without revealing master key.
|
|
95
|
+
* Format: "Emoji Number" (e.g. "🐼 42")
|
|
96
|
+
*/
|
|
97
|
+
getDisplayName(): string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
declare const scannedEvents: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
101
|
+
name: "scanned_events";
|
|
102
|
+
schema: undefined;
|
|
103
|
+
columns: {
|
|
104
|
+
id: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
105
|
+
name: "id";
|
|
106
|
+
tableName: "scanned_events";
|
|
107
|
+
dataType: "string";
|
|
108
|
+
columnType: "SQLiteText";
|
|
109
|
+
data: string;
|
|
110
|
+
driverParam: string;
|
|
111
|
+
notNull: true;
|
|
112
|
+
hasDefault: false;
|
|
113
|
+
isPrimaryKey: true;
|
|
114
|
+
isAutoincrement: false;
|
|
115
|
+
hasRuntimeDefault: false;
|
|
116
|
+
enumValues: [string, ...string[]];
|
|
117
|
+
baseColumn: never;
|
|
118
|
+
identity: undefined;
|
|
119
|
+
generated: undefined;
|
|
120
|
+
}, {}, {
|
|
121
|
+
length: number | undefined;
|
|
122
|
+
}>;
|
|
123
|
+
rpi: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
124
|
+
name: "rpi";
|
|
125
|
+
tableName: "scanned_events";
|
|
126
|
+
dataType: "string";
|
|
127
|
+
columnType: "SQLiteText";
|
|
128
|
+
data: string;
|
|
129
|
+
driverParam: string;
|
|
130
|
+
notNull: true;
|
|
131
|
+
hasDefault: false;
|
|
132
|
+
isPrimaryKey: false;
|
|
133
|
+
isAutoincrement: false;
|
|
134
|
+
hasRuntimeDefault: false;
|
|
135
|
+
enumValues: [string, ...string[]];
|
|
136
|
+
baseColumn: never;
|
|
137
|
+
identity: undefined;
|
|
138
|
+
generated: undefined;
|
|
139
|
+
}, {}, {
|
|
140
|
+
length: number | undefined;
|
|
141
|
+
}>;
|
|
142
|
+
metadataKey: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
143
|
+
name: "metadata_key";
|
|
144
|
+
tableName: "scanned_events";
|
|
145
|
+
dataType: "string";
|
|
146
|
+
columnType: "SQLiteText";
|
|
147
|
+
data: string;
|
|
148
|
+
driverParam: string;
|
|
149
|
+
notNull: true;
|
|
150
|
+
hasDefault: false;
|
|
151
|
+
isPrimaryKey: false;
|
|
152
|
+
isAutoincrement: false;
|
|
153
|
+
hasRuntimeDefault: false;
|
|
154
|
+
enumValues: [string, ...string[]];
|
|
155
|
+
baseColumn: never;
|
|
156
|
+
identity: undefined;
|
|
157
|
+
generated: undefined;
|
|
158
|
+
}, {}, {
|
|
159
|
+
length: number | undefined;
|
|
160
|
+
}>;
|
|
161
|
+
timestamp: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
162
|
+
name: "timestamp";
|
|
163
|
+
tableName: "scanned_events";
|
|
164
|
+
dataType: "number";
|
|
165
|
+
columnType: "SQLiteInteger";
|
|
166
|
+
data: number;
|
|
167
|
+
driverParam: number;
|
|
168
|
+
notNull: true;
|
|
169
|
+
hasDefault: false;
|
|
170
|
+
isPrimaryKey: false;
|
|
171
|
+
isAutoincrement: false;
|
|
172
|
+
hasRuntimeDefault: false;
|
|
173
|
+
enumValues: undefined;
|
|
174
|
+
baseColumn: never;
|
|
175
|
+
identity: undefined;
|
|
176
|
+
generated: undefined;
|
|
177
|
+
}, {}, {}>;
|
|
178
|
+
};
|
|
179
|
+
dialect: "sqlite";
|
|
180
|
+
}>;
|
|
181
|
+
/** Type for a scanned event row */
|
|
182
|
+
type ScannedEvent = typeof scannedEvents.$inferSelect;
|
|
183
|
+
declare class StorageService {
|
|
184
|
+
private db;
|
|
185
|
+
private rescanIntervalMs;
|
|
186
|
+
private lastScanByRpi;
|
|
187
|
+
private static readonly MAX_SCAN_HISTORY_SIZE;
|
|
188
|
+
constructor(db: VailixDB, config?: {
|
|
189
|
+
rescanIntervalMs?: number;
|
|
190
|
+
});
|
|
191
|
+
initialize(): Promise<void>;
|
|
192
|
+
canScan(rpi: string): boolean;
|
|
193
|
+
logScan(rpi: string, metadataKey: string, timestamp: number): Promise<void>;
|
|
194
|
+
cleanupOldScans(): Promise<void>;
|
|
195
|
+
getMatchingScans(rpiList: string[]): Promise<ScannedEvent[]>;
|
|
196
|
+
getRecentPairs(withinHours?: number): Promise<ScannedEvent[]>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
declare class MatcherService extends EventEmitter {
|
|
200
|
+
private storage;
|
|
201
|
+
private downloadUrl;
|
|
202
|
+
private appSecret;
|
|
203
|
+
constructor(storage: StorageService, downloadUrl: string, appSecret: string);
|
|
204
|
+
fetchAndMatch(): Promise<Match[]>;
|
|
205
|
+
private _downloadAndProcessKeys;
|
|
206
|
+
private _parseBinaryResponse;
|
|
207
|
+
private _decrypt;
|
|
208
|
+
/**
|
|
209
|
+
* Retrieve a specific match by RPI with on-demand decryption.
|
|
210
|
+
* Used when app needs to display exposure details to user.
|
|
211
|
+
*
|
|
212
|
+
* PRIVACY: Decrypted metadata exists ONLY in-memory (return value).
|
|
213
|
+
* Never persisted to storage.
|
|
214
|
+
*
|
|
215
|
+
* @param rpi - The RPI (match ID) to retrieve
|
|
216
|
+
* @returns Match with decrypted metadata, or null if not found
|
|
217
|
+
*/
|
|
218
|
+
getMatchById(rpi: string): Promise<Match | null>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
declare function formatQR(rpi: string, metadataKey: string): string;
|
|
222
|
+
declare function parseQR(data: string): ParsedQR | null;
|
|
223
|
+
interface ParsedQR {
|
|
224
|
+
rpi: string;
|
|
225
|
+
timestamp: number;
|
|
226
|
+
metadataKey: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
declare class VailixSDK {
|
|
230
|
+
identity: IdentityManager;
|
|
231
|
+
storage: StorageService;
|
|
232
|
+
matcher: MatcherService;
|
|
233
|
+
private ble;
|
|
234
|
+
private reportUrl;
|
|
235
|
+
private appSecret;
|
|
236
|
+
private reportDays;
|
|
237
|
+
private rpiDurationMs;
|
|
238
|
+
private constructor();
|
|
239
|
+
/**
|
|
240
|
+
* Create and initialize the VailixSDK.
|
|
241
|
+
*
|
|
242
|
+
* @param config - Unified configuration object
|
|
243
|
+
*/
|
|
244
|
+
static create(config: VailixConfig): Promise<VailixSDK>;
|
|
245
|
+
/** Get current QR code data */
|
|
246
|
+
getQRCode(): string;
|
|
247
|
+
/**
|
|
248
|
+
* Scan another user's QR code and log it.
|
|
249
|
+
* Returns false if: QR invalid, expired (>RPI duration), rescan blocked, or error
|
|
250
|
+
*/
|
|
251
|
+
scanQR(qrData: string): Promise<boolean>;
|
|
252
|
+
/**
|
|
253
|
+
* Check if BLE is supported on this device.
|
|
254
|
+
*/
|
|
255
|
+
static isBleSupported(): Promise<boolean>;
|
|
256
|
+
/**
|
|
257
|
+
* Start BLE discovery (call when pairing screen opens).
|
|
258
|
+
* Begins advertising our RPI and scanning for nearby users.
|
|
259
|
+
*/
|
|
260
|
+
startDiscovery(): Promise<void>;
|
|
261
|
+
/**
|
|
262
|
+
* Stop BLE discovery (call when leaving pairing screen).
|
|
263
|
+
*/
|
|
264
|
+
stopDiscovery(): Promise<void>;
|
|
265
|
+
/**
|
|
266
|
+
* Get current list of nearby users.
|
|
267
|
+
*/
|
|
268
|
+
getNearbyUsers(): NearbyUser[];
|
|
269
|
+
/**
|
|
270
|
+
* Subscribe to nearby user updates.
|
|
271
|
+
* Returns cleanup function for React useEffect compatibility.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* useEffect(() => {
|
|
275
|
+
* const cleanup = sdk.onNearbyUsersChanged(setNearbyUsers);
|
|
276
|
+
* return cleanup;
|
|
277
|
+
* }, []);
|
|
278
|
+
*/
|
|
279
|
+
onNearbyUsersChanged(callback: (users: NearbyUser[]) => void): () => void;
|
|
280
|
+
/**
|
|
281
|
+
* Pair with a specific user (one-tap action).
|
|
282
|
+
* In explicit consent mode, this also accepts pending incoming requests.
|
|
283
|
+
*
|
|
284
|
+
* @param userId - The user's ID from NearbyUser.id
|
|
285
|
+
*/
|
|
286
|
+
pairWithUser(userId: string): Promise<PairResult>;
|
|
287
|
+
/**
|
|
288
|
+
* Unpair with a user (removes from storage and resets status).
|
|
289
|
+
* Useful for "undo" functionality or rejecting requests.
|
|
290
|
+
*
|
|
291
|
+
* @param userId - The user's ID from NearbyUser.id
|
|
292
|
+
*/
|
|
293
|
+
unpairUser(userId: string): Promise<void>;
|
|
294
|
+
/**
|
|
295
|
+
* Get list of recent pairings from storage (for history/recap features).
|
|
296
|
+
* Returns pairs from the last N hours (default: 24h).
|
|
297
|
+
*
|
|
298
|
+
* @param withinHours - Look back window in hours (default: 24)
|
|
299
|
+
* @returns Array of NearbyUser objects representing recent pairs
|
|
300
|
+
*/
|
|
301
|
+
getRecentPairs(withinHours?: number): Promise<NearbyUser[]>;
|
|
302
|
+
/**
|
|
303
|
+
* Report positive (upload configured days of history).
|
|
304
|
+
*
|
|
305
|
+
* @param attestToken - Optional attestation token (e.g., Firebase App Check)
|
|
306
|
+
* @param metadata - App-specific data (e.g., STD type, test date). If null, a generic positive is reported.
|
|
307
|
+
* @param overrideReportDays - Optional: Override reportDays for this specific report
|
|
308
|
+
* (e.g., for apps with per-condition exposure windows)
|
|
309
|
+
*/
|
|
310
|
+
report(attestToken?: string, metadata?: ReportMetadata, overrideReportDays?: number): Promise<boolean>;
|
|
311
|
+
private static readonly MAX_METADATA_SIZE;
|
|
312
|
+
private _encrypt;
|
|
313
|
+
/**
|
|
314
|
+
* Subscribe to match events.
|
|
315
|
+
* Returns cleanup function for React useEffect compatibility.
|
|
316
|
+
*/
|
|
317
|
+
onMatch(handler: MatchHandler): () => void;
|
|
318
|
+
/**
|
|
319
|
+
* Subscribe to error events.
|
|
320
|
+
* Returns cleanup function for React useEffect compatibility.
|
|
321
|
+
*/
|
|
322
|
+
onError(handler: (error: Error) => void): () => void;
|
|
323
|
+
/** Explicit cleanup for match handler */
|
|
324
|
+
offMatch(handler: MatchHandler): void;
|
|
325
|
+
/** Explicit cleanup for error handler */
|
|
326
|
+
offError(handler: (error: Error) => void): void;
|
|
327
|
+
/**
|
|
328
|
+
* Get a specific match by ID with decrypted metadata.
|
|
329
|
+
* Used for on-demand decryption when user views exposure details.
|
|
330
|
+
*
|
|
331
|
+
* @param matchId - RPI of the match to retrieve
|
|
332
|
+
* @returns Match with decrypted metadata, or null if not found
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* // App calls this when user taps on notification
|
|
336
|
+
* const match = await sdk.getMatchById('abc123');
|
|
337
|
+
* console.log(match.metadata.conditions); // ["HIV", "Syphilis"]
|
|
338
|
+
*/
|
|
339
|
+
getMatchById(matchId: string): Promise<Match | null>;
|
|
340
|
+
/**
|
|
341
|
+
* Get own display name (emoji + number derived from current RPI).
|
|
342
|
+
* Used for showing user's anonymous identity in UI.
|
|
343
|
+
*/
|
|
344
|
+
getOwnDisplayName(): string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export { type KeyStorage, type Match, type MatchHandler, type NearbyUser, type PairResult, type ReportMetadata, type VailixConfig, VailixSDK, formatQR, parseQR };
|