@switchlabs/verify-ai-react-native 1.1.0 → 1.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/README.md +17 -4
- package/lib/client/index.d.ts +22 -2
- package/lib/client/index.js +132 -19
- package/lib/components/VerifyAIScanner.d.ts +7 -4
- package/lib/components/VerifyAIScanner.js +235 -18
- package/lib/hooks/useVerifyAI.d.ts +32 -10
- package/lib/hooks/useVerifyAI.js +246 -14
- package/lib/index.d.ts +5 -2
- package/lib/index.js +3 -0
- package/lib/ml/featureExtractor.d.ts +16 -0
- package/lib/ml/featureExtractor.js +123 -0
- package/lib/ml/imagePreprocessor.d.ts +2 -0
- package/lib/ml/imagePreprocessor.js +48 -0
- package/lib/ml/index.d.ts +5 -0
- package/lib/ml/index.js +4 -0
- package/lib/ml/inferenceEngine.d.ts +24 -0
- package/lib/ml/inferenceEngine.js +156 -0
- package/lib/ml/modelManager.d.ts +26 -0
- package/lib/ml/modelManager.js +207 -0
- package/lib/ml/policyEngine.d.ts +14 -0
- package/lib/ml/policyEngine.js +161 -0
- package/lib/ml/types.d.ts +84 -0
- package/lib/ml/types.js +4 -0
- package/lib/storage/offlineQueue.js +1 -1
- package/lib/telemetry/TelemetryContext.d.ts +4 -0
- package/lib/telemetry/TelemetryContext.js +5 -0
- package/lib/telemetry/TelemetryReporter.d.ts +24 -0
- package/lib/telemetry/TelemetryReporter.js +141 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/version.d.ts +1 -0
- package/lib/version.js +1 -0
- package/package.json +23 -2
- package/src/client/index.ts +176 -25
- package/src/components/VerifyAIScanner.tsx +282 -21
- package/src/hooks/useVerifyAI.ts +332 -18
- package/src/index.ts +20 -1
- package/src/ml/featureExtractor.ts +160 -0
- package/src/ml/imagePreprocessor.ts +72 -0
- package/src/ml/index.ts +14 -0
- package/src/ml/inferenceEngine.ts +200 -0
- package/src/ml/modelManager.ts +265 -0
- package/src/ml/policyEngine.ts +201 -0
- package/src/ml/types.ts +104 -0
- package/src/storage/offlineQueue.ts +1 -1
- package/src/telemetry/TelemetryContext.tsx +8 -0
- package/src/telemetry/TelemetryReporter.ts +184 -0
- package/src/types/index.ts +20 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare class TelemetryReporter {
|
|
2
|
+
private buffer;
|
|
3
|
+
private flushTimer;
|
|
4
|
+
private sessionId;
|
|
5
|
+
private baseUrl;
|
|
6
|
+
private apiKey;
|
|
7
|
+
private disposed;
|
|
8
|
+
private flushing;
|
|
9
|
+
constructor(apiKey: string, baseUrl: string);
|
|
10
|
+
/** Track an error event. Fire-and-forget — never throws. */
|
|
11
|
+
track(eventType: string, opts?: {
|
|
12
|
+
component?: string;
|
|
13
|
+
error?: unknown;
|
|
14
|
+
errorCode?: string;
|
|
15
|
+
metadata?: Record<string, string | number>;
|
|
16
|
+
}): void;
|
|
17
|
+
/** Flush all buffered events immediately. Returns a promise but never rejects. */
|
|
18
|
+
flush(): Promise<void>;
|
|
19
|
+
/** Dispose — flush remaining events and stop timers. */
|
|
20
|
+
dispose(): void;
|
|
21
|
+
private scheduleFlush;
|
|
22
|
+
private flushNow;
|
|
23
|
+
private clearFlushTimer;
|
|
24
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import { SDK_VERSION } from '../version';
|
|
3
|
+
/** Event types that flush immediately (critical init failures). */
|
|
4
|
+
const CRITICAL_EVENTS = new Set([
|
|
5
|
+
'camera_init_failure',
|
|
6
|
+
'camera_preview_timeout',
|
|
7
|
+
'camera_permission_denied',
|
|
8
|
+
]);
|
|
9
|
+
export class TelemetryReporter {
|
|
10
|
+
constructor(apiKey, baseUrl) {
|
|
11
|
+
this.buffer = new Map();
|
|
12
|
+
this.flushTimer = null;
|
|
13
|
+
this.disposed = false;
|
|
14
|
+
this.flushing = false;
|
|
15
|
+
this.apiKey = apiKey;
|
|
16
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
17
|
+
this.sessionId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
18
|
+
}
|
|
19
|
+
/** Track an error event. Fire-and-forget — never throws. */
|
|
20
|
+
track(eventType, opts = {}) {
|
|
21
|
+
if (this.disposed)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
const errorObj = opts.error instanceof Error ? opts.error : null;
|
|
26
|
+
const errorMessage = errorObj?.message
|
|
27
|
+
?? (typeof opts.error === 'string' ? opts.error : undefined);
|
|
28
|
+
const dedupKey = `${eventType}|${errorMessage ?? ''}|${opts.component ?? ''}`;
|
|
29
|
+
const existing = this.buffer.get(dedupKey);
|
|
30
|
+
if (existing) {
|
|
31
|
+
existing.event_count++;
|
|
32
|
+
existing.last_occurred_at = now;
|
|
33
|
+
// If the new occurrence is critical, flush immediately
|
|
34
|
+
if (CRITICAL_EVENTS.has(eventType)) {
|
|
35
|
+
this.flushNow();
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const event = {
|
|
40
|
+
event_type: eventType,
|
|
41
|
+
component: opts.component,
|
|
42
|
+
error_message: errorMessage?.slice(0, 1000),
|
|
43
|
+
error_stack: errorObj?.stack?.slice(0, 2000),
|
|
44
|
+
error_code: opts.errorCode,
|
|
45
|
+
metadata: opts.metadata,
|
|
46
|
+
sdk_platform: Platform.OS,
|
|
47
|
+
sdk_version: SDK_VERSION,
|
|
48
|
+
os_name: Platform.OS,
|
|
49
|
+
os_version: String(Platform.Version),
|
|
50
|
+
session_id: this.sessionId,
|
|
51
|
+
event_count: 1,
|
|
52
|
+
first_occurred_at: now,
|
|
53
|
+
last_occurred_at: now,
|
|
54
|
+
};
|
|
55
|
+
this.buffer.set(dedupKey, event);
|
|
56
|
+
if (CRITICAL_EVENTS.has(eventType)) {
|
|
57
|
+
this.flushNow();
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.scheduleFlush();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Never throw from telemetry
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Flush all buffered events immediately. Returns a promise but never rejects. */
|
|
68
|
+
async flush() {
|
|
69
|
+
if (this.buffer.size === 0 || this.flushing)
|
|
70
|
+
return;
|
|
71
|
+
this.flushing = true;
|
|
72
|
+
const bufferedEntries = Array.from(this.buffer.entries());
|
|
73
|
+
const events = bufferedEntries.map(([, event]) => event);
|
|
74
|
+
this.buffer.clear();
|
|
75
|
+
this.clearFlushTimer();
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(`${this.baseUrl}/telemetry`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
'X-API-Key': this.apiKey,
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({ events }),
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(`Telemetry request failed with status ${response.status}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
for (const [dedupKey, event] of bufferedEntries) {
|
|
94
|
+
const existing = this.buffer.get(dedupKey);
|
|
95
|
+
if (existing) {
|
|
96
|
+
existing.event_count += event.event_count;
|
|
97
|
+
existing.first_occurred_at = existing.first_occurred_at < event.first_occurred_at
|
|
98
|
+
? existing.first_occurred_at
|
|
99
|
+
: event.first_occurred_at;
|
|
100
|
+
existing.last_occurred_at = existing.last_occurred_at > event.last_occurred_at
|
|
101
|
+
? existing.last_occurred_at
|
|
102
|
+
: event.last_occurred_at;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.buffer.set(dedupKey, event);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
this.flushing = false;
|
|
112
|
+
if (this.buffer.size > 0 && !this.disposed) {
|
|
113
|
+
this.scheduleFlush();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Dispose — flush remaining events and stop timers. */
|
|
118
|
+
dispose() {
|
|
119
|
+
this.disposed = true;
|
|
120
|
+
this.clearFlushTimer();
|
|
121
|
+
this.flush();
|
|
122
|
+
}
|
|
123
|
+
scheduleFlush() {
|
|
124
|
+
if (this.flushTimer)
|
|
125
|
+
return;
|
|
126
|
+
this.flushTimer = setTimeout(() => {
|
|
127
|
+
this.flushTimer = null;
|
|
128
|
+
this.flush();
|
|
129
|
+
}, 5000);
|
|
130
|
+
}
|
|
131
|
+
flushNow() {
|
|
132
|
+
this.clearFlushTimer();
|
|
133
|
+
this.flush();
|
|
134
|
+
}
|
|
135
|
+
clearFlushTimer() {
|
|
136
|
+
if (this.flushTimer) {
|
|
137
|
+
clearTimeout(this.flushTimer);
|
|
138
|
+
this.flushTimer = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface VerifyAIConfig {
|
|
|
4
4
|
baseUrl?: string;
|
|
5
5
|
timeout?: number;
|
|
6
6
|
offlineMode?: boolean;
|
|
7
|
+
/** Enable SDK telemetry reporting for client-side errors. Default: true. */
|
|
8
|
+
telemetry?: boolean;
|
|
7
9
|
}
|
|
8
10
|
export interface VerificationRequest {
|
|
9
11
|
image: string;
|
|
@@ -11,6 +13,12 @@ export interface VerificationRequest {
|
|
|
11
13
|
metadata?: Record<string, unknown>;
|
|
12
14
|
provider?: 'openai' | 'anthropic' | 'gemini';
|
|
13
15
|
}
|
|
16
|
+
export interface MultipartVerificationRequest {
|
|
17
|
+
imageUri: string;
|
|
18
|
+
policy: string;
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
provider?: 'openai' | 'anthropic' | 'gemini';
|
|
21
|
+
}
|
|
14
22
|
export interface VerificationResult {
|
|
15
23
|
id: string;
|
|
16
24
|
created_at: string;
|
|
@@ -37,6 +45,10 @@ export interface VerificationListParams {
|
|
|
37
45
|
start_date?: string;
|
|
38
46
|
end_date?: string;
|
|
39
47
|
}
|
|
48
|
+
export interface VerifyOptions {
|
|
49
|
+
/** Idempotency key to prevent duplicate verifications on retry. */
|
|
50
|
+
idempotencyKey?: string;
|
|
51
|
+
}
|
|
40
52
|
export interface QueueItem {
|
|
41
53
|
id: string;
|
|
42
54
|
request: VerificationRequest;
|
|
@@ -49,6 +61,11 @@ export interface VerifyAIError {
|
|
|
49
61
|
current_usage?: number;
|
|
50
62
|
limit?: number;
|
|
51
63
|
upgrade_url?: string;
|
|
64
|
+
request_id?: string;
|
|
65
|
+
code?: string;
|
|
66
|
+
path?: string;
|
|
67
|
+
url?: string;
|
|
68
|
+
method?: string;
|
|
52
69
|
}
|
|
53
70
|
export type ScannerStatus = 'idle' | 'capturing' | 'processing' | 'success' | 'error';
|
|
54
71
|
export interface ScannerOverlayConfig {
|
|
@@ -77,6 +94,7 @@ export interface ScannerOverlayConfig {
|
|
|
77
94
|
exhaustedMessage?: string;
|
|
78
95
|
maxAttempts?: number;
|
|
79
96
|
autoApproveOnExhaust?: boolean;
|
|
97
|
+
showTechnicalErrorDetails?: boolean;
|
|
80
98
|
}
|
|
81
99
|
export interface PolicyConfigResponse {
|
|
82
100
|
maxAttempts: number;
|
package/lib/version.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SDK_VERSION = "1.1.1";
|
package/lib/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SDK_VERSION = '1.1.1';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchlabs/verify-ai-react-native",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "React Native SDK for Verify AI - photo verification with AI vision processing",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -23,26 +23,47 @@
|
|
|
23
23
|
],
|
|
24
24
|
"author": "Switch Labs",
|
|
25
25
|
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@noble/hashes": "^1.8.0",
|
|
28
|
+
"buffer": "^6.0.3",
|
|
29
|
+
"jpeg-js": "^0.4.4"
|
|
30
|
+
},
|
|
26
31
|
"peerDependencies": {
|
|
27
32
|
"react": ">=18.0.0",
|
|
28
33
|
"react-native": ">=0.72.0",
|
|
29
34
|
"expo-camera": ">=15.0.0",
|
|
30
|
-
"
|
|
35
|
+
"expo-image-manipulator": ">=12.0.0",
|
|
36
|
+
"@react-native-async-storage/async-storage": ">=1.19.0",
|
|
37
|
+
"react-native-fast-tflite": ">=1.0.0",
|
|
38
|
+
"expo-file-system": ">=17.0.0"
|
|
31
39
|
},
|
|
32
40
|
"peerDependenciesMeta": {
|
|
33
41
|
"expo-camera": {
|
|
34
42
|
"optional": true
|
|
35
43
|
},
|
|
44
|
+
"expo-image-manipulator": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
36
47
|
"@react-native-async-storage/async-storage": {
|
|
37
48
|
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"react-native-fast-tflite": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"expo-file-system": {
|
|
54
|
+
"optional": true
|
|
38
55
|
}
|
|
39
56
|
},
|
|
40
57
|
"devDependencies": {
|
|
58
|
+
"@types/jpeg-js": "^0.3.7",
|
|
41
59
|
"@react-native-async-storage/async-storage": "^2.1.0",
|
|
42
60
|
"@types/react": "^18.2.0",
|
|
43
61
|
"expo-camera": "^16.0.0",
|
|
62
|
+
"expo-file-system": "^18.0.12",
|
|
63
|
+
"expo-image-manipulator": "^13.0.0",
|
|
44
64
|
"react": "^18.2.0",
|
|
45
65
|
"react-native": "^0.76.0",
|
|
66
|
+
"react-native-fast-tflite": "^1.6.1",
|
|
46
67
|
"typescript": "^5.5.0"
|
|
47
68
|
}
|
|
48
69
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
VerifyAIConfig,
|
|
3
3
|
VerificationRequest,
|
|
4
|
+
MultipartVerificationRequest,
|
|
4
5
|
VerificationResult,
|
|
5
6
|
VerificationListResponse,
|
|
6
7
|
VerificationListParams,
|
|
7
8
|
VerifyAIError,
|
|
9
|
+
VerifyOptions,
|
|
8
10
|
PolicyConfigResponse,
|
|
9
11
|
} from '../types';
|
|
12
|
+
import { TelemetryReporter } from '../telemetry/TelemetryReporter';
|
|
10
13
|
|
|
11
14
|
const DEFAULT_BASE_URL = 'https://verify.switchlabs.dev/api/v1';
|
|
12
15
|
const DEFAULT_TIMEOUT = 30000;
|
|
13
16
|
|
|
17
|
+
interface RequestContext {
|
|
18
|
+
path: string;
|
|
19
|
+
url: string;
|
|
20
|
+
method: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
export class VerifyAIClient {
|
|
15
24
|
private apiKey: string;
|
|
16
25
|
private baseUrl: string;
|
|
17
26
|
private timeout: number;
|
|
27
|
+
readonly telemetry: TelemetryReporter | null;
|
|
18
28
|
|
|
19
29
|
constructor(config: VerifyAIConfig) {
|
|
20
30
|
if (!config.apiKey) {
|
|
@@ -23,17 +33,71 @@ export class VerifyAIClient {
|
|
|
23
33
|
this.apiKey = config.apiKey;
|
|
24
34
|
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
25
35
|
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
36
|
+
this.telemetry = config.telemetry !== false
|
|
37
|
+
? new TelemetryReporter(this.apiKey, this.baseUrl)
|
|
38
|
+
: null;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
|
-
private
|
|
41
|
+
private parseResponseBody(rawBody: string): unknown {
|
|
42
|
+
if (!rawBody) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(rawBody);
|
|
48
|
+
} catch {
|
|
49
|
+
return { error: rawBody };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private buildRequestError(
|
|
54
|
+
message: string,
|
|
55
|
+
status: number,
|
|
56
|
+
context: RequestContext,
|
|
57
|
+
body: Partial<VerifyAIError> = {}
|
|
58
|
+
): VerifyAIRequestError {
|
|
59
|
+
const error: VerifyAIError = {
|
|
60
|
+
error: message,
|
|
61
|
+
status,
|
|
62
|
+
code: status === 408 ? 'timeout' : status === 0 ? 'network_error' : 'request_error',
|
|
63
|
+
path: context.path,
|
|
64
|
+
url: context.url,
|
|
65
|
+
method: context.method,
|
|
66
|
+
...body,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return new VerifyAIRequestError(message, status, error);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private normalizeRequestError(error: unknown, context: RequestContext): VerifyAIRequestError {
|
|
73
|
+
if (error instanceof VerifyAIRequestError) {
|
|
74
|
+
return error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
78
|
+
return this.buildRequestError('Verification request timed out', 408, context, { code: 'timeout' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error instanceof TypeError) {
|
|
82
|
+
return this.buildRequestError('Network request failed', 0, context, { code: 'network_error' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const message = error instanceof Error ? error.message : 'VerifyAI request failed';
|
|
86
|
+
return this.buildRequestError(message, 0, context);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async executeRequest<T>(
|
|
29
90
|
path: string,
|
|
30
91
|
options: RequestInit = {}
|
|
31
92
|
): Promise<T> {
|
|
32
93
|
const controller = new AbortController();
|
|
33
94
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
95
|
+
const method = (options.method || 'GET').toUpperCase();
|
|
96
|
+
const url = `${this.baseUrl}${path}`;
|
|
97
|
+
const context: RequestContext = { path, url, method };
|
|
34
98
|
|
|
35
99
|
try {
|
|
36
|
-
const response = await fetch(
|
|
100
|
+
const response = await fetch(url, {
|
|
37
101
|
...options,
|
|
38
102
|
signal: controller.signal,
|
|
39
103
|
headers: {
|
|
@@ -43,43 +107,76 @@ export class VerifyAIClient {
|
|
|
43
107
|
});
|
|
44
108
|
|
|
45
109
|
const rawBody = await response.text();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (rawBody) {
|
|
49
|
-
try {
|
|
50
|
-
body = JSON.parse(rawBody);
|
|
51
|
-
} catch {
|
|
52
|
-
body = { error: rawBody };
|
|
53
|
-
}
|
|
54
|
-
}
|
|
110
|
+
const body = this.parseResponseBody(rawBody);
|
|
55
111
|
|
|
56
112
|
if (!response.ok) {
|
|
57
113
|
const errorBody = (body && typeof body === 'object' ? body : null) as VerifyAIError | null;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
status: response.status,
|
|
61
|
-
current_usage: errorBody?.current_usage,
|
|
62
|
-
limit: errorBody?.limit,
|
|
63
|
-
upgrade_url: errorBody?.upgrade_url,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
throw new VerifyAIRequestError(
|
|
67
|
-
error.error,
|
|
114
|
+
throw this.buildRequestError(
|
|
115
|
+
errorBody?.error || `Request failed with status ${response.status}`,
|
|
68
116
|
response.status,
|
|
69
|
-
|
|
117
|
+
context,
|
|
118
|
+
{
|
|
119
|
+
current_usage: errorBody?.current_usage,
|
|
120
|
+
limit: errorBody?.limit,
|
|
121
|
+
upgrade_url: errorBody?.upgrade_url,
|
|
122
|
+
request_id: errorBody?.request_id || response.headers.get('X-Request-Id') || undefined,
|
|
123
|
+
code: errorBody?.code || 'http_error',
|
|
124
|
+
}
|
|
70
125
|
);
|
|
71
126
|
}
|
|
72
127
|
|
|
73
128
|
if (!body || typeof body !== 'object') {
|
|
74
|
-
throw
|
|
129
|
+
throw this.buildRequestError(
|
|
130
|
+
'VerifyAI: Invalid response payload',
|
|
131
|
+
0,
|
|
132
|
+
context,
|
|
133
|
+
{
|
|
134
|
+
code: 'invalid_response',
|
|
135
|
+
request_id: response.headers.get('X-Request-Id') || undefined,
|
|
136
|
+
}
|
|
137
|
+
);
|
|
75
138
|
}
|
|
76
139
|
|
|
77
140
|
return body as T;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const normalized = this.normalizeRequestError(error, context);
|
|
143
|
+
// Track network/timeout/server errors
|
|
144
|
+
if (this.telemetry) {
|
|
145
|
+
const errorCode = normalized.code || (normalized.status >= 500 ? 'server_error' : undefined);
|
|
146
|
+
const eventType =
|
|
147
|
+
normalized.status === 408 || normalized.code === 'timeout'
|
|
148
|
+
? 'request_timeout'
|
|
149
|
+
: normalized.status === 0 || normalized.code === 'network_error'
|
|
150
|
+
? 'network_error'
|
|
151
|
+
: normalized.status === 401 || normalized.status === 403
|
|
152
|
+
? 'auth_error'
|
|
153
|
+
: normalized.status === 429
|
|
154
|
+
? 'rate_limited'
|
|
155
|
+
: normalized.status >= 400 && normalized.status < 500
|
|
156
|
+
? 'request_error'
|
|
157
|
+
: 'server_error';
|
|
158
|
+
this.telemetry.track(
|
|
159
|
+
eventType,
|
|
160
|
+
{
|
|
161
|
+
component: 'client',
|
|
162
|
+
error: normalized,
|
|
163
|
+
errorCode,
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
throw normalized;
|
|
78
168
|
} finally {
|
|
79
169
|
clearTimeout(timer);
|
|
80
170
|
}
|
|
81
171
|
}
|
|
82
172
|
|
|
173
|
+
private async request<T>(
|
|
174
|
+
path: string,
|
|
175
|
+
options: RequestInit = {}
|
|
176
|
+
): Promise<T> {
|
|
177
|
+
return this.executeRequest<T>(path, options);
|
|
178
|
+
}
|
|
179
|
+
|
|
83
180
|
/**
|
|
84
181
|
* Submit a photo for AI verification.
|
|
85
182
|
*
|
|
@@ -102,14 +199,60 @@ export class VerifyAIClient {
|
|
|
102
199
|
* }
|
|
103
200
|
* ```
|
|
104
201
|
*/
|
|
105
|
-
async verify(request: VerificationRequest): Promise<VerificationResult> {
|
|
202
|
+
async verify(request: VerificationRequest, options?: VerifyOptions): Promise<VerificationResult> {
|
|
203
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
204
|
+
if (options?.idempotencyKey) {
|
|
205
|
+
headers['Idempotency-Key'] = options.idempotencyKey;
|
|
206
|
+
}
|
|
106
207
|
return this.request<VerificationResult>('/verify', {
|
|
107
208
|
method: 'POST',
|
|
108
|
-
headers
|
|
209
|
+
headers,
|
|
109
210
|
body: JSON.stringify(request),
|
|
110
211
|
});
|
|
111
212
|
}
|
|
112
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Submit a photo for AI verification using multipart/form-data.
|
|
216
|
+
* Streams the image directly from disk — avoids base64 encoding overhead.
|
|
217
|
+
*
|
|
218
|
+
* **Breaking change:** `onCapture` prop on `VerifyAIScanner` now receives
|
|
219
|
+
* an image URI instead of a base64 string.
|
|
220
|
+
*
|
|
221
|
+
* @param request - Multipart request with file URI and policy
|
|
222
|
+
* @param options - Optional verify options (e.g. idempotency key)
|
|
223
|
+
* @returns The verification result with compliance status and feedback
|
|
224
|
+
*/
|
|
225
|
+
async verifyMultipart(request: MultipartVerificationRequest, options?: VerifyOptions): Promise<VerificationResult> {
|
|
226
|
+
const formData = new FormData();
|
|
227
|
+
// React Native FormData accepts { uri, type, name } objects for file fields
|
|
228
|
+
formData.append('image', {
|
|
229
|
+
uri: request.imageUri,
|
|
230
|
+
type: 'image/jpeg',
|
|
231
|
+
name: 'photo.jpg',
|
|
232
|
+
} as unknown as Blob);
|
|
233
|
+
formData.append('policy', request.policy);
|
|
234
|
+
if (request.metadata) {
|
|
235
|
+
formData.append('metadata', JSON.stringify(request.metadata));
|
|
236
|
+
}
|
|
237
|
+
if (request.provider) {
|
|
238
|
+
formData.append('provider', request.provider);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const headers: Record<string, string> = {
|
|
242
|
+
'X-API-Key': this.apiKey,
|
|
243
|
+
// Do NOT set Content-Type — fetch auto-sets multipart boundary
|
|
244
|
+
};
|
|
245
|
+
if (options?.idempotencyKey) {
|
|
246
|
+
headers['Idempotency-Key'] = options.idempotencyKey;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return this.executeRequest<VerificationResult>('/verify', {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers,
|
|
252
|
+
body: formData,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
113
256
|
/**
|
|
114
257
|
* List past verifications with optional filters.
|
|
115
258
|
*
|
|
@@ -211,4 +354,12 @@ export class VerifyAIRequestError extends Error {
|
|
|
211
354
|
get upgradeUrl(): string | undefined {
|
|
212
355
|
return this.body.upgrade_url;
|
|
213
356
|
}
|
|
357
|
+
|
|
358
|
+
get requestId(): string | undefined {
|
|
359
|
+
return this.body.request_id;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
get code(): string | undefined {
|
|
363
|
+
return this.body.code;
|
|
364
|
+
}
|
|
214
365
|
}
|