react-native-app-attestation 0.1.0

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.
@@ -0,0 +1,123 @@
1
+ // src/AttestationService.ts
2
+
3
+ import { Platform, NativeModules } from 'react-native';
4
+ import { AttestationConfig, AttestationResult } from './types';
5
+
6
+ export class AttestationService {
7
+ private config: AttestationConfig;
8
+ private cachedToken: string | null = null;
9
+ private tokenExpiry: number | null = null;
10
+ private cacheDuration: number;
11
+
12
+ constructor(config: AttestationConfig) {
13
+ this.config = config;
14
+ this.cacheDuration = config.tokenCacheDurationMs ?? 10 * 60 * 1000;
15
+ }
16
+
17
+ private log(msg: string) {
18
+ if (this.config.debug) console.log(`[Attestation] ${msg}`);
19
+ }
20
+
21
+ // Backend se nonce fetch karo
22
+ private async fetchNonce(): Promise<string> {
23
+ const response = await fetch(this.config.nonceEndpoint);
24
+ const data = await response.json();
25
+ if (!data.nonce) throw new Error('Nonce nahi mila backend se');
26
+ return data.nonce;
27
+ }
28
+
29
+ // Android — Play Integrity
30
+ private async getAndroidToken(): Promise<string | null> {
31
+ try {
32
+ const { PlayIntegrityModule } = NativeModules;
33
+
34
+ if (!PlayIntegrityModule) {
35
+ this.log('PlayIntegrityModule nahi mila');
36
+ return null;
37
+ }
38
+
39
+ const nonce = await this.fetchNonce();
40
+ const token = await PlayIntegrityModule
41
+ .getAttestationToken(nonce);
42
+ this.log('Android token mila!');
43
+ return token;
44
+
45
+ } catch (error) {
46
+ this.log(`Android error: ${error}`);
47
+ return null;
48
+ }
49
+ }
50
+
51
+ // iOS — App Attest
52
+ private async getIOSToken(): Promise<string | null> {
53
+ try {
54
+ const { AppAttestModule } = NativeModules;
55
+
56
+ if (!AppAttestModule) {
57
+ this.log('AppAttestModule nahi mila');
58
+ return null;
59
+ }
60
+
61
+ const challenge = await this.fetchNonce();
62
+ const token = await AppAttestModule
63
+ .getAttestationToken(challenge);
64
+ this.log('iOS token mila!');
65
+ return token;
66
+
67
+ } catch (error) {
68
+ this.log(`iOS error: ${error}`);
69
+ return null;
70
+ }
71
+ }
72
+
73
+ // Main function
74
+ async getToken(forceRefresh = false): Promise<AttestationResult> {
75
+ const now = Date.now();
76
+ const platform = Platform.OS as 'android' | 'ios';
77
+
78
+ // Cache valid hai?
79
+ if (
80
+ !forceRefresh &&
81
+ this.cachedToken &&
82
+ this.tokenExpiry &&
83
+ now < this.tokenExpiry
84
+ ) {
85
+ this.log('Cached token use ho raha hai');
86
+ return {
87
+ token: this.cachedToken,
88
+ fromCache: true,
89
+ platform,
90
+ };
91
+ }
92
+
93
+ // Fresh token lo
94
+ let token: string | null = null;
95
+
96
+ if (Platform.OS === 'android') {
97
+ token = await this.getAndroidToken();
98
+ } else if (Platform.OS === 'ios') {
99
+ token = await this.getIOSToken();
100
+ }
101
+
102
+ // Cache karo
103
+ if (token) {
104
+ this.cachedToken = token;
105
+ this.tokenExpiry = now + this.cacheDuration;
106
+ this.log(`Token cached — ${this.cacheDuration / 60000} min valid`);
107
+ }
108
+
109
+ return { token, fromCache: false, platform };
110
+ }
111
+
112
+ // Sensitive operations ke liye
113
+ async getFreshToken(): Promise<AttestationResult> {
114
+ return this.getToken(true);
115
+ }
116
+
117
+ // Logout pe call karo
118
+ clearCache(): void {
119
+ this.cachedToken = null;
120
+ this.tokenExpiry = null;
121
+ this.log('Cache cleared');
122
+ }
123
+ }
@@ -0,0 +1,62 @@
1
+ // src/DeviceIDService.ts
2
+
3
+ import { StorageAdapter } from './types';
4
+
5
+ const DEVICE_ID_KEY = 'rn_attestation_device_id';
6
+
7
+ // Khud ka UUID generator — koi library nahi!
8
+ const generateUUID = (): string => {
9
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
10
+ const r = (Math.random() * 16) | 0;
11
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
12
+ return v.toString(16);
13
+ });
14
+ };
15
+
16
+ export class DeviceIDService {
17
+ private storage: StorageAdapter;
18
+ private debug: boolean;
19
+
20
+ constructor(storage: StorageAdapter, debug = false) {
21
+ this.storage = storage;
22
+ this.debug = debug;
23
+ }
24
+
25
+ private log(msg: string) {
26
+ if (this.debug) console.log(`[DeviceID] ${msg}`);
27
+ }
28
+
29
+ async getDeviceID(): Promise<string> {
30
+ try {
31
+ // Storage se check karo
32
+ let deviceId = await Promise.resolve(
33
+ this.storage.get(DEVICE_ID_KEY)
34
+ );
35
+
36
+ if (!deviceId) {
37
+ // Pehli baar — naya UUID banao
38
+ deviceId = generateUUID();
39
+ await Promise.resolve(
40
+ this.storage.set(DEVICE_ID_KEY, deviceId)
41
+ );
42
+ this.log(`Naya Device ID banaya: ${deviceId}`);
43
+ } else {
44
+ this.log(`Existing Device ID mila: ${deviceId}`);
45
+ }
46
+
47
+ return deviceId;
48
+
49
+ } catch (error) {
50
+ this.log(`Error: ${error}`);
51
+ // Fallback — storage fail ho toh bhi app crash na ho
52
+ return generateUUID();
53
+ }
54
+ }
55
+
56
+ async resetDeviceID(): Promise<void> {
57
+ await Promise.resolve(
58
+ this.storage.delete(DEVICE_ID_KEY)
59
+ );
60
+ this.log('Device ID reset ho gaya');
61
+ }
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,174 @@
1
+ // src/index.ts
2
+
3
+ import { Platform } from 'react-native';
4
+ import { DeviceIDService } from './DeviceIDService';
5
+ import { AttestationService } from './AttestationService';
6
+ import { axiosInterceptor, secureFetch } from './interceptors';
7
+ import {
8
+ AttestationConfig,
9
+ SecurityHeaders,
10
+ StorageAdapter,
11
+ } from './types';
12
+
13
+ // Re-export — user directly import kar sake
14
+ export * from './types';
15
+ export { DeviceIDService } from './DeviceIDService';
16
+ export { AttestationService } from './AttestationService';
17
+ export { axiosInterceptor, secureFetch } from './interceptors';
18
+
19
+ // ========================================
20
+ // SINGLETON — ek baar init, har jagah use
21
+ // ========================================
22
+ let deviceIDService: DeviceIDService | null = null;
23
+ let attestationService: AttestationService | null = null;
24
+ let globalConfig: AttestationConfig | null = null;
25
+
26
+ /**
27
+ * App start pe ek baar call karo
28
+ *
29
+ * MMKV example:
30
+ * initAttestation({
31
+ * storage: {
32
+ * get: (key) => mmkv.getString(key) ?? null,
33
+ * set: (key, val) => mmkv.set(key, val),
34
+ * delete: (key) => mmkv.delete(key),
35
+ * },
36
+ * nonceEndpoint: 'https://api.yourapp.com/auth/nonce',
37
+ * appVersion: '1.4',
38
+ * debug: __DEV__,
39
+ * });
40
+ */
41
+ export const initAttestation = (config: AttestationConfig): void => {
42
+ globalConfig = config;
43
+ deviceIDService = new DeviceIDService(config.storage, config.debug);
44
+ attestationService = new AttestationService(config);
45
+ };
46
+
47
+ // Init check
48
+ const checkInit = (fnName: string) => {
49
+ if (!deviceIDService || !attestationService || !globalConfig) {
50
+ throw new Error(
51
+ `[react-native-app-attestation] ${fnName}() call karne se pehle initAttestation() call karo!`
52
+ );
53
+ }
54
+ };
55
+
56
+ /**
57
+ * Device ID lo
58
+ */
59
+ export const getDeviceID = async (): Promise<string> => {
60
+ checkInit('getDeviceID');
61
+ return deviceIDService!.getDeviceID();
62
+ };
63
+
64
+ /**
65
+ * Attestation token lo (cached ya fresh)
66
+ */
67
+ export const getAttestationToken = async (
68
+ forceRefresh = false
69
+ ): Promise<string | null> => {
70
+ checkInit('getAttestationToken');
71
+ const result = await attestationService!.getToken(forceRefresh);
72
+ return result.token;
73
+ };
74
+
75
+ /**
76
+ * Sensitive operations ke liye fresh token
77
+ * Payment, Login, OTP pe ye use karo
78
+ */
79
+ export const getFreshAttestationToken = async (): Promise<string | null> => {
80
+ checkInit('getFreshAttestationToken');
81
+ const result = await attestationService!.getFreshToken();
82
+ return result.token;
83
+ };
84
+
85
+ /**
86
+ * Saare security headers ek saath lo
87
+ * Interceptor mein ye use karo
88
+ */
89
+ export const getSecurityHeaders = async (): Promise<SecurityHeaders> => {
90
+ checkInit('getSecurityHeaders');
91
+
92
+ const [deviceId, attestationResult] = await Promise.all([
93
+ deviceIDService!.getDeviceID(),
94
+ attestationService!.getToken(),
95
+ ]);
96
+
97
+ const timestamp = Math.floor(Date.now() / 1000).toString();
98
+ const platform = Platform.OS === 'ios' ? 'iOS' : 'Android';
99
+
100
+ const headers: SecurityHeaders = {
101
+ 'User-Agent': `App/${globalConfig!.appVersion} (${platform})`,
102
+ 'X-App-Platform': Platform.OS,
103
+ 'X-App-Version': globalConfig!.appVersion,
104
+ 'X-Device-ID': deviceId,
105
+ 'X-Timestamp': timestamp,
106
+ };
107
+
108
+ if (attestationResult.token) {
109
+ headers['X-Attestation'] = attestationResult.token;
110
+ }
111
+
112
+ return headers;
113
+ };
114
+
115
+ /**
116
+ * Axios interceptor setup
117
+ *
118
+ * import axios from 'axios';
119
+ * import { initAttestation, setupAxios } from 'react-native-app-attestation';
120
+ *
121
+ * const api = axios.create({ baseURL: '...' });
122
+ * setupAxios(api);
123
+ */
124
+ export const setupAxios = (axiosInstance: any): void => {
125
+ checkInit('setupAxios');
126
+ axiosInterceptor(axiosInstance, {
127
+ appVersion: globalConfig!.appVersion,
128
+ getHeaders: getSecurityHeaders,
129
+ });
130
+ };
131
+
132
+ /**
133
+ * Secure fetch — fetch ki jagah use karo
134
+ *
135
+ * const res = await secureGet('https://api.com/user');
136
+ */
137
+ export const secureGet = (
138
+ url: string,
139
+ options?: RequestInit
140
+ ): Promise<Response> => {
141
+ checkInit('secureGet');
142
+ return secureFetch(url, options ?? {}, getSecurityHeaders);
143
+ };
144
+
145
+ export const securePost = (
146
+ url: string,
147
+ body: unknown,
148
+ options?: RequestInit
149
+ ): Promise<Response> => {
150
+ checkInit('securePost');
151
+ return secureFetch(
152
+ url,
153
+ {
154
+ ...options,
155
+ method: 'POST',
156
+ body: JSON.stringify(body),
157
+ },
158
+ getSecurityHeaders
159
+ );
160
+ };
161
+
162
+ /**
163
+ * Logout pe call karo
164
+ */
165
+ export const clearAttestationCache = (): void => {
166
+ attestationService?.clearCache();
167
+ };
168
+
169
+ /**
170
+ * Device ID reset karo
171
+ */
172
+ export const resetDeviceID = async (): Promise<void> => {
173
+ await deviceIDService?.resetDeviceID();
174
+ };
@@ -0,0 +1,72 @@
1
+ // src/interceptors.ts
2
+
3
+ import { SecurityHeaders } from './types';
4
+
5
+ /**
6
+ * AXIOS ke liye
7
+ *
8
+ * Use karo:
9
+ * axiosInterceptor(api, { appVersion: '1.4', getHeaders })
10
+ */
11
+ export const axiosInterceptor = (
12
+ axiosInstance: any,
13
+ options: {
14
+ appVersion: string;
15
+ getHeaders: () => Promise<SecurityHeaders>;
16
+ }
17
+ ) => {
18
+ axiosInstance.interceptors.request.use(
19
+ async (config: any) => {
20
+ try {
21
+ // Security headers lo
22
+ const securityHeaders = await options.getHeaders();
23
+
24
+ // Existing headers ke saath merge karo
25
+ config.headers = {
26
+ ...config.headers,
27
+ ...securityHeaders,
28
+ };
29
+
30
+ } catch (error) {
31
+ console.warn('[Attestation] Headers add nahi ho sake:', error);
32
+ // App crash nahi hona chahiye — silently fail
33
+ }
34
+
35
+ return config;
36
+ },
37
+ (error: any) => Promise.reject(error)
38
+ );
39
+ };
40
+
41
+ /**
42
+ * FETCH ke liye
43
+ *
44
+ * Use karo:
45
+ * const res = await secureFetch(url, options, getHeaders)
46
+ */
47
+ export const secureFetch = async (
48
+ url: string,
49
+ options: RequestInit = {},
50
+ getHeaders: () => Promise<SecurityHeaders>
51
+ ): Promise<Response> => {
52
+ try {
53
+ // Security headers lo
54
+ const securityHeaders = await getHeaders();
55
+
56
+ // Existing headers ke saath merge karo
57
+ const mergedHeaders = {
58
+ ...options.headers,
59
+ ...securityHeaders,
60
+ };
61
+
62
+ return fetch(url, {
63
+ ...options,
64
+ headers: mergedHeaders,
65
+ });
66
+
67
+ } catch (error) {
68
+ console.warn('[Attestation] secureFetch headers error:', error);
69
+ // Headers add nahi hue — normal fetch karo
70
+ return fetch(url, options);
71
+ }
72
+ };
package/src/types.ts ADDED
@@ -0,0 +1,52 @@
1
+ // src/types.ts
2
+
3
+ /**
4
+ * Storage interface — user apna storage dega
5
+ * MMKV, AsyncStorage, ya koi bhi
6
+ */
7
+ export interface StorageAdapter {
8
+ get: (key: string) => string | null | Promise<string | null>;
9
+ set: (key: string, value: string) => void | Promise<void>;
10
+ delete: (key: string) => void | Promise<void>;
11
+ }
12
+
13
+ /**
14
+ * Package initialize karne ke liye config
15
+ */
16
+ export interface AttestationConfig {
17
+ // Tumhara storage (MMKV ya AsyncStorage)
18
+ storage: StorageAdapter;
19
+
20
+ // Nonce fetch karne ke liye URL
21
+ nonceEndpoint: string;
22
+
23
+ // App version — header mein jayega
24
+ appVersion: string;
25
+
26
+ // Token cache kitni der valid rahe (default: 10 min)
27
+ tokenCacheDurationMs?: number;
28
+
29
+ // Debug logs on/off (default: false)
30
+ debug?: boolean;
31
+ }
32
+
33
+ /**
34
+ * Attestation token result
35
+ */
36
+ export interface AttestationResult {
37
+ token: string | null;
38
+ fromCache: boolean;
39
+ platform: 'android' | 'ios';
40
+ }
41
+
42
+ /**
43
+ * Security headers jo API call mein jayenge
44
+ */
45
+ export interface SecurityHeaders {
46
+ 'User-Agent': string;
47
+ 'X-App-Platform': string;
48
+ 'X-App-Version': string;
49
+ 'X-Device-ID': string;
50
+ 'X-Timestamp': string;
51
+ 'X-Attestation'?: string;
52
+ }