@witsauth/react-native-client 0.0.0-stage → 1.0.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.
package/README.md CHANGED
@@ -1,3 +1,195 @@
1
- # Temporary Holding Version
1
+ # @witsauth/react-native-client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Official React Native SDK for **WitsAuth** OAuth 2.0 + PKCE authentication, token lifecycle management, Axios interceptor integration, and hosted Account Management on **Android** and **iOS** (supporting both **Expo** and **Bare React Native CLI**).
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - **RFC 8252 System Browser OAuth 2.0 + PKCE (`S256`)**: Uses `ASWebAuthenticationSession` on iOS and Chrome Custom Tabs on Android.
10
+ - **Hermes-Ready Pure-TS SHA-256 PKCE**: Works out-of-the-box on React Native Hermes without requiring native Web Crypto polyfills.
11
+ - **Hardware-Backed Secure Token Storage**: Built-in adapters for `expo-secure-store` (Expo) and `react-native-keychain` (Bare React Native CLI), backed by iOS Keychain and Android Keystore.
12
+ - **Automatic Token Refresh**: Proactively refreshes expiring tokens and refreshes automatically when the app returns to the foreground (`AppState`).
13
+ - **Hosted Account Management (`/auth/account`)**: Allows users to change their password, update their profile, and verify their mobile phone number via OTP using a single-use handoff ticket (`navigateToAccountManagement()`).
14
+ - **Axios Interceptor**: Automatically attaches `Authorization: Bearer <token>` and queues/retries concurrent `401 Unauthorized` requests.
15
+
16
+ ---
17
+
18
+ ## 1. WitsAuth Console Configuration
19
+
20
+ 1. Open the **WitsAuth Console** and create or edit a **Regular Web / Mobile Application**.
21
+ 2. Set **Framework** to **React Native (Mobile)**.
22
+ 3. Add your mobile deep-link callback URI under **Allowed Callback URLs**, for example:
23
+ ```text
24
+ myapp://auth/callback
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 2. Installation & Setup
30
+
31
+ ### Option A: Expo (Managed or Prebuild Workflow)
32
+
33
+ ```bash
34
+ npx expo install @witsauth/react-native-client expo-web-browser expo-secure-store
35
+ ```
36
+
37
+ Configure your custom scheme in `app.json`:
38
+
39
+ ```json
40
+ {
41
+ "expo": {
42
+ "name": "My App",
43
+ "slug": "my-app",
44
+ "scheme": "myapp"
45
+ }
46
+ }
47
+ ```
48
+
49
+ Initialize `AuthProvider` in your app root:
50
+
51
+ ```tsx
52
+ import React from 'react';
53
+ import { AppState } from 'react-native';
54
+ import * as WebBrowser from 'expo-web-browser';
55
+ import * as SecureStore from 'expo-secure-store';
56
+ import {
57
+ AuthProvider,
58
+ createExpoSecureStoreAdapter,
59
+ createExpoWebBrowserLauncher,
60
+ OAuth2NativeConfig
61
+ } from '@witsauth/react-native-client';
62
+
63
+ WebBrowser.maybeCompleteAuthSession();
64
+
65
+ const authConfig: OAuth2NativeConfig = {
66
+ clientId: 'YOUR_CLIENT_ID',
67
+ authorizationEndpoint: 'https://your-tenant.witsauth.com/auth/authorize',
68
+ tokenEndpoint: 'https://your-tenant.witsauth.com/oauth/token',
69
+ revokeEndpoint: 'https://your-tenant.witsauth.com/oauth/revoke',
70
+ redirectUri: 'myapp://auth/callback',
71
+ scope: 'openid profile email offline_access',
72
+ storage: createExpoSecureStoreAdapter(SecureStore),
73
+ openAuthSession: createExpoWebBrowserLauncher(WebBrowser)
74
+ };
75
+
76
+ export default function App() {
77
+ return (
78
+ <AuthProvider
79
+ config={authConfig}
80
+ subscribeAppState={(listener) => {
81
+ const sub = AppState.addEventListener('change', listener);
82
+ return () => sub.remove();
83
+ }}
84
+ >
85
+ {/* Your App Navigation */}
86
+ </AuthProvider>
87
+ );
88
+ }
89
+ ```
90
+
91
+ ---
92
+
93
+ ### Option B: Bare React Native CLI (Android & iOS)
94
+
95
+ ```bash
96
+ npm install @witsauth/react-native-client react-native-inappbrowser-reborn react-native-keychain
97
+ cd ios && pod install
98
+ ```
99
+
100
+ #### Android Deep-Link Configuration (`android/app/src/main/AndroidManifest.xml`)
101
+
102
+ Add an `<intent-filter>` inside your main `<activity>`:
103
+
104
+ ```xml
105
+ <intent-filter>
106
+ <action android:name="android.intent.action.VIEW" />
107
+ <category android:name="android.intent.category.DEFAULT" />
108
+ <category android:name="android.intent.category.BROWSABLE" />
109
+ <data android:scheme="myapp" android:host="auth" android:pathPrefix="/callback" />
110
+ </intent-filter>
111
+ ```
112
+
113
+ #### iOS Deep-Link Configuration (`ios/<YourApp>/Info.plist`)
114
+
115
+ Register `CFBundleURLTypes`:
116
+
117
+ ```xml
118
+ <key>CFBundleURLTypes</key>
119
+ <array>
120
+ <dict>
121
+ <key>CFBundleTypeRole</key>
122
+ <string>Editor</string>
123
+ <key>CFBundleURLName</key>
124
+ <string>com.yourcompany.myapp</string>
125
+ <key>CFBundleURLSchemes</key>
126
+ <array>
127
+ <string>myapp</string>
128
+ </array>
129
+ </dict>
130
+ </array>
131
+ ```
132
+
133
+ Initialize `AuthProvider` with `react-native-keychain` and `react-native-inappbrowser-reborn`:
134
+
135
+ ```tsx
136
+ import React from 'react';
137
+ import { AppState } from 'react-native';
138
+ import InAppBrowser from 'react-native-inappbrowser-reborn';
139
+ import * as Keychain from 'react-native-keychain';
140
+ import {
141
+ AuthProvider,
142
+ createKeychainStorageAdapter,
143
+ createInAppBrowserLauncher,
144
+ OAuth2NativeConfig
145
+ } from '@witsauth/react-native-client';
146
+
147
+ const authConfig: OAuth2NativeConfig = {
148
+ clientId: 'YOUR_CLIENT_ID',
149
+ authorizationEndpoint: 'https://your-tenant.witsauth.com/auth/authorize',
150
+ tokenEndpoint: 'https://your-tenant.witsauth.com/oauth/token',
151
+ revokeEndpoint: 'https://your-tenant.witsauth.com/oauth/revoke',
152
+ redirectUri: 'myapp://auth/callback',
153
+ scope: 'openid profile email offline_access',
154
+ storage: createKeychainStorageAdapter(Keychain),
155
+ openAuthSession: createInAppBrowserLauncher(InAppBrowser)
156
+ };
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 3. Using `useAuth()` for Login, Logout & Account Management
162
+
163
+ ```tsx
164
+ import React from 'react';
165
+ import { View, Button, Text } from 'react-native';
166
+ import { useAuth } from '@witsauth/react-native-client';
167
+
168
+ export function ProfileScreen() {
169
+ const {
170
+ isAuthenticated,
171
+ isLoading,
172
+ login,
173
+ logout,
174
+ navigateToAccountManagement
175
+ } = useAuth();
176
+
177
+ if (isLoading) {
178
+ return <Text>Loading...</Text>;
179
+ }
180
+
181
+ if (!isAuthenticated) {
182
+ return <Button title="Sign In with WitsAuth" onPress={login} />;
183
+ }
184
+
185
+ return (
186
+ <View>
187
+ <Button
188
+ title="Manage Account (Password & Phone)"
189
+ onPress={navigateToAccountManagement}
190
+ />
191
+ <Button title="Sign Out" onPress={logout} />
192
+ </View>
193
+ );
194
+ }
195
+ ```
@@ -0,0 +1,13 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { AuthClient } from '../core/AuthClient';
3
+ export interface InterceptorOptions {
4
+ /**
5
+ * Optional callback triggered when token refresh fails completely (e.g., refresh token expired).
6
+ */
7
+ onUnauthorized?: () => void;
8
+ /**
9
+ * Specify URLs that should NOT have the Authorization header injected.
10
+ */
11
+ excludeUrls?: (string | RegExp)[];
12
+ }
13
+ export declare function setupAxiosInterceptors(axiosInstance: AxiosInstance, authClient: AuthClient, options?: InterceptorOptions): () => void;
@@ -0,0 +1,44 @@
1
+ import { OAuth2NativeConfig, TokenInfo } from '../models/types';
2
+ import { NativeTokenStorage } from './storage';
3
+ export declare class AuthClient {
4
+ private config;
5
+ private storage;
6
+ private refreshPromise;
7
+ constructor(config: OAuth2NativeConfig, storage?: NativeTokenStorage);
8
+ /**
9
+ * Hydrates the in-memory token cache from the underlying async secure storage.
10
+ * Called automatically by `AuthProvider` on mount.
11
+ */
12
+ initialize(): Promise<void>;
13
+ log(level: 'error' | 'warn' | 'info' | 'debug', ...args: unknown[]): void;
14
+ /**
15
+ * Builds the OAuth 2.0 + PKCE authorization URL and persists `code_verifier`, `state`, and `nonce`.
16
+ */
17
+ buildAuthorizationUrl(): Promise<string>;
18
+ /**
19
+ * Launches the system authentication browser (`ASWebAuthenticationSession` on iOS / Chrome Custom Tabs on Android),
20
+ * intercepts the deep-link callback (`redirectUri`), validates `state`, and exchanges `code` + `code_verifier` for tokens.
21
+ */
22
+ authorize(): Promise<TokenInfo>;
23
+ /**
24
+ * Parses a redirect callback URL (`myapp://auth/callback?code=...&state=...`) and exchanges the code for tokens.
25
+ */
26
+ handleCallbackUrl(callbackUrl: string): Promise<TokenInfo>;
27
+ exchangeCodeForTokens(code: string, state: string): Promise<TokenInfo>;
28
+ refreshAccessToken(): Promise<TokenInfo>;
29
+ private saveTokenInfo;
30
+ getAccessToken(): string | null;
31
+ getRefreshToken(): string | null;
32
+ getExpiresAt(): number | null;
33
+ isAuthenticated(): boolean;
34
+ /**
35
+ * Opens the hosted WitsAuth Account Management portal (`/auth/account`) in the native secure browser
36
+ * using a short-lived single-use ticket (`POST /auth/account/ticket`), allowing the user to change
37
+ * their password, update their profile, or verify their mobile phone number via OTP.
38
+ */
39
+ navigateToAccountManagement(): Promise<void>;
40
+ /**
41
+ * Clears tokens from secure storage and revokes both refresh_token and access_token at `revokeEndpoint`.
42
+ */
43
+ logout(): Promise<void>;
44
+ }
@@ -0,0 +1,29 @@
1
+ import { BrowserSessionLauncher } from '../models/types';
2
+ /**
3
+ * Creates a `BrowserSessionLauncher` using Expo's `expo-web-browser` module.
4
+ */
5
+ export declare function createExpoWebBrowserLauncher(webBrowserModule: {
6
+ openAuthSessionAsync: (url: string, redirectUrl?: string) => Promise<{
7
+ type: string;
8
+ url?: string;
9
+ }>;
10
+ }): BrowserSessionLauncher;
11
+ /**
12
+ * Creates a `BrowserSessionLauncher` using `react-native-inappbrowser-reborn` for Bare React Native CLI apps.
13
+ */
14
+ export declare function createInAppBrowserLauncher(inAppBrowserModule: {
15
+ isAvailable: () => Promise<boolean>;
16
+ openAuth: (url: string, redirectUrl: string, options?: Record<string, unknown>) => Promise<{
17
+ type: string;
18
+ url?: string;
19
+ }>;
20
+ }): BrowserSessionLauncher;
21
+ /**
22
+ * Parses `code`, `state`, `error`, and `error_description` from a custom-scheme or HTTPS callback URL.
23
+ */
24
+ export declare function parseCallbackUrl(callbackUrl: string): {
25
+ code: string | null;
26
+ state: string | null;
27
+ error: string | null;
28
+ errorDescription: string | null;
29
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Hermes-compatible PKCE (RFC 7636) utilities.
3
+ * Uses Web Crypto API when available, and falls back to a pure-TypeScript
4
+ * FIPS 180-4 SHA-256 implementation so S256 works out-of-the-box on React Native
5
+ * Hermes without requiring native crypto polyfills.
6
+ */
7
+ /**
8
+ * Pure-TypeScript FIPS 180-4 SHA-256 digest.
9
+ */
10
+ export declare function sha256Bytes(asciiOrUtf8: string): Uint8Array;
11
+ export declare function base64UrlEncode(bytes: Uint8Array): string;
12
+ export declare function generateRandomString(length?: number): string;
13
+ export declare function generateCodeVerifier(): string;
14
+ export declare function generateState(): string;
15
+ export declare function createCodeChallenge(verifier: string): Promise<string>;
@@ -0,0 +1,49 @@
1
+ import { OAuth2AsyncStorage } from '../models/types';
2
+ /**
3
+ * Wraps an asynchronous native secure store (e.g., Expo SecureStore or React Native Keychain)
4
+ * with a synchronous in-memory read cache. Once `hydrate()` resolves on startup,
5
+ * `getAccessToken()` and `isAuthenticated()` are synchronous while all mutations
6
+ * persist asynchronously to the native keychain/keystore.
7
+ */
8
+ export declare class NativeTokenStorage {
9
+ private backend;
10
+ private cache;
11
+ private hydrated;
12
+ private hydratePromise;
13
+ constructor(customStorage?: OAuth2AsyncStorage);
14
+ hydrate(): Promise<void>;
15
+ getItemSync(key: string): string | null;
16
+ getItem(key: string): Promise<string | null>;
17
+ setItem(key: string, value: string): Promise<void>;
18
+ removeItem(key: string): Promise<void>;
19
+ clearTemporary(): Promise<void>;
20
+ clearTokens(): Promise<void>;
21
+ }
22
+ export declare function createMemoryStorageAdapter(): OAuth2AsyncStorage;
23
+ /**
24
+ * Helper to create a storage adapter backed by `expo-secure-store`.
25
+ * Pass the imported `SecureStore` module from `expo-secure-store`.
26
+ */
27
+ export declare function createExpoSecureStoreAdapter(secureStoreModule: {
28
+ getItemAsync: (key: string) => Promise<string | null>;
29
+ setItemAsync: (key: string, value: string) => Promise<void>;
30
+ deleteItemAsync: (key: string) => Promise<void>;
31
+ }): OAuth2AsyncStorage;
32
+ /**
33
+ * Helper to create a storage adapter backed by `react-native-keychain` for Bare React Native CLI apps.
34
+ * Pass the imported `Keychain` module from `react-native-keychain`.
35
+ */
36
+ export declare function createKeychainStorageAdapter(keychainModule: {
37
+ getGenericPassword: (options?: {
38
+ service?: string;
39
+ }) => Promise<false | {
40
+ username: string;
41
+ password: string;
42
+ }>;
43
+ setGenericPassword: (username: string, password: string, options?: {
44
+ service?: string;
45
+ }) => Promise<unknown>;
46
+ resetGenericPassword: (options?: {
47
+ service?: string;
48
+ }) => Promise<unknown>;
49
+ }, servicePrefix?: string): OAuth2AsyncStorage;
@@ -0,0 +1,9 @@
1
+ export * from './models/types';
2
+ export { AuthClient } from './core/AuthClient';
3
+ export { NativeTokenStorage, createMemoryStorageAdapter, createExpoSecureStoreAdapter, createKeychainStorageAdapter } from './core/storage';
4
+ export { createExpoWebBrowserLauncher, createInAppBrowserLauncher, parseCallbackUrl } from './core/browser';
5
+ export * from './core/pkce';
6
+ export { AuthProvider, useAuth } from './react/AuthContext';
7
+ export type { AuthContextType, AuthProviderProps } from './react/AuthContext';
8
+ export { setupAxiosInterceptors } from './axios/interceptor';
9
+ export type { InterceptorOptions } from './axios/interceptor';
@@ -0,0 +1,619 @@
1
+ import { createContext as e, useCallback as t, useContext as n, useEffect as r, useState as i } from "react";
2
+ import { jsx as a } from "react/jsx-runtime";
3
+ // src/models/types.ts
4
+ var o = {
5
+ ACCESS_TOKEN: "witsauth_access_token",
6
+ REFRESH_TOKEN: "witsauth_refresh_token",
7
+ TOKEN_EXPIRES_AT: "witsauth_expires_at",
8
+ TOKEN_TYPE: "witsauth_token_type",
9
+ SCOPE: "witsauth_scope",
10
+ ID_TOKEN: "witsauth_id_token",
11
+ CODE_VERIFIER: "witsauth_code_verifier",
12
+ STATE: "witsauth_state",
13
+ NONCE: "witsauth_nonce"
14
+ }, s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", c = new Uint32Array([
15
+ 1116352408,
16
+ 1899447441,
17
+ 3049323471,
18
+ 3921009573,
19
+ 961987163,
20
+ 1508970993,
21
+ 2453635748,
22
+ 2870763221,
23
+ 3624381080,
24
+ 310598401,
25
+ 607225278,
26
+ 1426881987,
27
+ 1925078388,
28
+ 2162078206,
29
+ 2614888103,
30
+ 3248222580,
31
+ 3835390401,
32
+ 4022224774,
33
+ 264347078,
34
+ 604807628,
35
+ 770255983,
36
+ 1249150122,
37
+ 1555081692,
38
+ 1996064986,
39
+ 2554220882,
40
+ 2821834349,
41
+ 2952996808,
42
+ 3210313671,
43
+ 3336571891,
44
+ 3584528711,
45
+ 113926993,
46
+ 338241895,
47
+ 666307205,
48
+ 773529912,
49
+ 1294757372,
50
+ 1396182291,
51
+ 1695183700,
52
+ 1986661051,
53
+ 2177026350,
54
+ 2456956037,
55
+ 2730485921,
56
+ 2820302411,
57
+ 3259730800,
58
+ 3345764771,
59
+ 3516065817,
60
+ 3600352804,
61
+ 4094571909,
62
+ 275423344,
63
+ 430227734,
64
+ 506948616,
65
+ 659060556,
66
+ 883997877,
67
+ 958139571,
68
+ 1322822218,
69
+ 1537002063,
70
+ 1747873779,
71
+ 1955562222,
72
+ 2024104815,
73
+ 2227730452,
74
+ 2361852424,
75
+ 2428436474,
76
+ 2756734187,
77
+ 3204031479,
78
+ 3329325298
79
+ ]);
80
+ function l(e, t) {
81
+ return t >>> e | t << 32 - e;
82
+ }
83
+ function u(e) {
84
+ if (typeof TextEncoder < "u") return new TextEncoder().encode(e);
85
+ let t = [];
86
+ for (let n = 0; n < e.length; n++) {
87
+ let r = e.charCodeAt(n);
88
+ r < 128 ? t.push(r) : r < 2048 ? t.push(192 | r >> 6, 128 | r & 63) : r < 55296 || r >= 57344 ? t.push(224 | r >> 12, 128 | r >> 6 & 63, 128 | r & 63) : (n++, r = 65536 + ((r & 1023) << 10 | e.charCodeAt(n) & 1023), t.push(240 | r >> 18, 128 | r >> 12 & 63, 128 | r >> 6 & 63, 128 | r & 63));
89
+ }
90
+ return new Uint8Array(t);
91
+ }
92
+ function d(e) {
93
+ let t = u(e), n = t.length * 8, r = t.length + 9 + 63 >> 6 << 6, i = new Uint8Array(r);
94
+ i.set(t), i[t.length] = 128;
95
+ let a = new DataView(i.buffer);
96
+ a.setUint32(r - 8, Math.floor(n / 4294967296), !1), a.setUint32(r - 4, n >>> 0, !1);
97
+ let o = 1779033703, s = 3144134277, d = 1013904242, f = 2773480762, p = 1359893119, m = 2600822924, h = 528734635, g = 1541459225, _ = /* @__PURE__ */ new Uint32Array(64);
98
+ for (let e = 0; e < r; e += 64) {
99
+ for (let t = 0; t < 16; t++) _[t] = a.getUint32(e + t * 4, !1);
100
+ for (let e = 16; e < 64; e++) {
101
+ let t = l(7, _[e - 15]) ^ l(18, _[e - 15]) ^ _[e - 15] >>> 3, n = l(17, _[e - 2]) ^ l(19, _[e - 2]) ^ _[e - 2] >>> 10;
102
+ _[e] = _[e - 16] + t + _[e - 7] + n >>> 0;
103
+ }
104
+ let t = o, n = s, r = d, i = f, u = p, v = m, y = h, b = g;
105
+ for (let e = 0; e < 64; e++) {
106
+ let a = l(6, u) ^ l(11, u) ^ l(25, u), o = u & v ^ ~u & y, s = b + a + o + c[e] + _[e] >>> 0, d = (l(2, t) ^ l(13, t) ^ l(22, t)) + (t & n ^ t & r ^ n & r) >>> 0;
107
+ b = y, y = v, v = u, u = i + s >>> 0, i = r, r = n, n = t, t = s + d >>> 0;
108
+ }
109
+ o = o + t >>> 0, s = s + n >>> 0, d = d + r >>> 0, f = f + i >>> 0, p = p + u >>> 0, m = m + v >>> 0, h = h + y >>> 0, g = g + b >>> 0;
110
+ }
111
+ let v = /* @__PURE__ */ new Uint8Array(32), y = new DataView(v.buffer);
112
+ return y.setUint32(0, o, !1), y.setUint32(4, s, !1), y.setUint32(8, d, !1), y.setUint32(12, f, !1), y.setUint32(16, p, !1), y.setUint32(20, m, !1), y.setUint32(24, h, !1), y.setUint32(28, g, !1), v;
113
+ }
114
+ function f(e) {
115
+ let t = "", n = e.length;
116
+ for (let r = 0; r < n; r += 3) {
117
+ let i = e[r], a = r + 1 < n ? e[r + 1] : 0, o = r + 2 < n ? e[r + 2] : 0;
118
+ t += s[i >> 2], t += s[(i & 3) << 4 | a >> 4], r + 1 < n && (t += s[(a & 15) << 2 | o >> 6]), r + 2 < n && (t += s[o & 63]);
119
+ }
120
+ return t;
121
+ }
122
+ function p(e) {
123
+ let t = new Uint8Array(e), n = typeof globalThis < "u" ? globalThis : void 0;
124
+ if (n?.crypto && typeof n.crypto.getRandomValues == "function") return n.crypto.getRandomValues(t), t;
125
+ for (let n = 0; n < e; n++) t[n] = Math.floor(Math.random() * 256);
126
+ return t;
127
+ }
128
+ function m(e = 64) {
129
+ let t = p(e), n = "";
130
+ for (let r = 0; r < e; r++) n += "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"[t[r] % 66];
131
+ return n;
132
+ }
133
+ function h() {
134
+ return m(64);
135
+ }
136
+ function g() {
137
+ return m(32);
138
+ }
139
+ async function _(e) {
140
+ let t = typeof globalThis < "u" ? globalThis : void 0;
141
+ if (t?.crypto?.subtle && typeof t.crypto.subtle.digest == "function") try {
142
+ let n = u(e), r = await t.crypto.subtle.digest("SHA-256", n);
143
+ return f(new Uint8Array(r));
144
+ } catch {}
145
+ return f(d(e));
146
+ }
147
+ // src/core/storage.ts
148
+ var v = Object.values(o), y = class {
149
+ backend;
150
+ cache = /* @__PURE__ */ new Map();
151
+ hydrated = !1;
152
+ hydratePromise = null;
153
+ constructor(e) {
154
+ this.backend = e || b();
155
+ }
156
+ async hydrate() {
157
+ if (!this.hydrated) return this.hydratePromise ||= (async () => {
158
+ await Promise.all(v.map(async (e) => {
159
+ try {
160
+ let t = await this.backend.getItem(e);
161
+ t != null && this.cache.set(e, t);
162
+ } catch {}
163
+ })), this.hydrated = !0;
164
+ })().finally(() => {
165
+ this.hydratePromise = null;
166
+ }), this.hydratePromise;
167
+ }
168
+ getItemSync(e) {
169
+ return this.cache.get(e) ?? null;
170
+ }
171
+ async getItem(e) {
172
+ if (this.cache.has(e)) return this.cache.get(e);
173
+ let t = await this.backend.getItem(e);
174
+ return t == null ? null : (this.cache.set(e, t), t);
175
+ }
176
+ async setItem(e, t) {
177
+ this.cache.set(e, t), await this.backend.setItem(e, t);
178
+ }
179
+ async removeItem(e) {
180
+ this.cache.delete(e), await this.backend.removeItem(e);
181
+ }
182
+ async clearTemporary() {
183
+ await Promise.all([
184
+ this.removeItem(o.CODE_VERIFIER),
185
+ this.removeItem(o.STATE),
186
+ this.removeItem(o.NONCE)
187
+ ]);
188
+ }
189
+ async clearTokens() {
190
+ await Promise.all([
191
+ this.removeItem(o.ACCESS_TOKEN),
192
+ this.removeItem(o.REFRESH_TOKEN),
193
+ this.removeItem(o.TOKEN_EXPIRES_AT),
194
+ this.removeItem(o.TOKEN_TYPE),
195
+ this.removeItem(o.SCOPE),
196
+ this.removeItem(o.ID_TOKEN)
197
+ ]);
198
+ }
199
+ };
200
+ function b() {
201
+ let e = /* @__PURE__ */ new Map();
202
+ return {
203
+ getItem: async (t) => e.get(t) ?? null,
204
+ setItem: async (t, n) => {
205
+ e.set(t, n);
206
+ },
207
+ removeItem: async (t) => {
208
+ e.delete(t);
209
+ }
210
+ };
211
+ }
212
+ function x(e) {
213
+ return {
214
+ getItem: (t) => e.getItemAsync(t),
215
+ setItem: (t, n) => e.setItemAsync(t, n),
216
+ removeItem: (t) => e.deleteItemAsync(t)
217
+ };
218
+ }
219
+ function S(e, t = "com.witsauth.tokens") {
220
+ return {
221
+ getItem: async (n) => {
222
+ let r = await e.getGenericPassword({ service: `${t}.${n}` });
223
+ return r ? r.password : null;
224
+ },
225
+ setItem: async (n, r) => {
226
+ await e.setGenericPassword(n, r, { service: `${t}.${n}` });
227
+ },
228
+ removeItem: async (n) => {
229
+ await e.resetGenericPassword({ service: `${t}.${n}` });
230
+ }
231
+ };
232
+ }
233
+ // src/core/browser.ts
234
+ function C(e) {
235
+ return async (t, n) => {
236
+ let r = await e.openAuthSessionAsync(t, n);
237
+ return r.type === "success" && r.url ? {
238
+ type: "success",
239
+ url: r.url
240
+ } : r.type === "cancel" ? { type: "cancel" } : { type: "dismiss" };
241
+ };
242
+ }
243
+ function w(e) {
244
+ return async (t, n) => {
245
+ if (!await e.isAvailable()) throw Error("InAppBrowser is not available on this device.");
246
+ let r = await e.openAuth(t, n, {
247
+ ephemeralWebSession: !1,
248
+ showTitle: !1,
249
+ enableUrlBarHiding: !0,
250
+ enableDefaultShare: !1
251
+ });
252
+ return r.type === "success" && r.url ? {
253
+ type: "success",
254
+ url: r.url
255
+ } : r.type === "cancel" ? { type: "cancel" } : { type: "dismiss" };
256
+ };
257
+ }
258
+ function T(e) {
259
+ let t = e.indexOf("?");
260
+ if (t === -1) return {
261
+ code: null,
262
+ state: null,
263
+ error: null,
264
+ errorDescription: null
265
+ };
266
+ let n = e.indexOf("#", t), r = n === -1 ? e.slice(t + 1) : e.slice(t + 1, n), i = new URLSearchParams(r);
267
+ return {
268
+ code: i.get("code"),
269
+ state: i.get("state"),
270
+ error: i.get("error"),
271
+ errorDescription: i.get("error_description")
272
+ };
273
+ }
274
+ // src/core/AuthClient.ts
275
+ var E = class {
276
+ config;
277
+ storage;
278
+ refreshPromise = null;
279
+ constructor(e, t) {
280
+ this.config = e, this.storage = t || new y(e.storage);
281
+ }
282
+ async initialize() {
283
+ await this.storage.hydrate();
284
+ }
285
+ log(e, ...t) {
286
+ let n = {
287
+ none: 0,
288
+ error: 1,
289
+ warn: 2,
290
+ info: 3,
291
+ debug: 4
292
+ }, r = n[this.config.logLevel || "warn"] || 0, i = n[e] || 0;
293
+ i > 0 && r >= i && console[e](...t);
294
+ }
295
+ async buildAuthorizationUrl() {
296
+ await this.storage.hydrate();
297
+ let e = h(), t = await _(e), n = g(), r = this.config.nonce || g();
298
+ await this.storage.setItem(o.CODE_VERIFIER, e), await this.storage.setItem(o.STATE, n), await this.storage.setItem(o.NONCE, r);
299
+ let i = new URLSearchParams({
300
+ client_id: this.config.clientId,
301
+ redirect_uri: this.config.redirectUri,
302
+ response_type: this.config.responseType || "code",
303
+ code_challenge_method: this.config.codeChallengeMethod || "S256",
304
+ code_challenge: t,
305
+ state: n,
306
+ nonce: r
307
+ });
308
+ if (this.config.theme) {
309
+ let e = typeof this.config.theme == "function" ? this.config.theme() : this.config.theme;
310
+ e && i.append("theme", e);
311
+ }
312
+ return this.config.scope && i.append("scope", this.config.scope), this.config.audience && i.append("audience", this.config.audience), `${this.config.authorizationEndpoint}?${i.toString()}`;
313
+ }
314
+ async authorize() {
315
+ if (!this.config.openAuthSession) throw Error("No openAuthSession launcher configured. Pass createExpoWebBrowserLauncher(WebBrowser) or createInAppBrowserLauncher(InAppBrowser) in OAuth2NativeConfig.");
316
+ let e = await this.buildAuthorizationUrl(), t = await this.config.openAuthSession(e, this.config.redirectUri);
317
+ if (t.type !== "success" || !t.url) throw await this.storage.clearTemporary(), Error(`Authentication ${t.type}`);
318
+ return this.handleCallbackUrl(t.url);
319
+ }
320
+ async handleCallbackUrl(e) {
321
+ let { code: t, state: n, error: r, errorDescription: i } = T(e);
322
+ if (r) throw await this.storage.clearTemporary(), Error(i ? `${r}: ${i}` : r);
323
+ if (!t || !n) throw await this.storage.clearTemporary(), Error("Callback URL is missing required code or state parameters.");
324
+ return this.exchangeCodeForTokens(t, n);
325
+ }
326
+ async exchangeCodeForTokens(e, t) {
327
+ await this.storage.hydrate();
328
+ let n = await this.storage.getItem(o.STATE), r = await this.storage.getItem(o.CODE_VERIFIER);
329
+ if (!n || t !== n) throw await this.storage.clearTemporary(), Error("Invalid state parameter. Possible CSRF attack.");
330
+ if (!r) throw await this.storage.clearTemporary(), Error("Code verifier missing from storage.");
331
+ let i = new URLSearchParams({
332
+ grant_type: "authorization_code",
333
+ client_id: this.config.clientId,
334
+ redirect_uri: this.config.redirectUri,
335
+ code: e,
336
+ code_verifier: r
337
+ }), a = await fetch(this.config.tokenEndpoint, {
338
+ method: "POST",
339
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
340
+ body: i.toString()
341
+ });
342
+ if (!a.ok) throw Error("Failed to exchange code for tokens");
343
+ let s = await a.json(), c = {
344
+ accessToken: s.access_token,
345
+ refreshToken: s.refresh_token,
346
+ tokenType: s.token_type || "Bearer",
347
+ expiresAt: Date.now() + s.expires_in * 1e3,
348
+ scope: s.scope,
349
+ idToken: s.id_token
350
+ };
351
+ return await this.saveTokenInfo(c), await this.storage.clearTemporary(), c;
352
+ }
353
+ async refreshAccessToken() {
354
+ return this.refreshPromise ||= (async () => {
355
+ await this.storage.hydrate();
356
+ let e = await this.storage.getItem(o.REFRESH_TOKEN);
357
+ if (!e) throw Error("No refresh token available");
358
+ let t = new URLSearchParams({
359
+ grant_type: "refresh_token",
360
+ client_id: this.config.clientId,
361
+ refresh_token: e
362
+ }), n;
363
+ try {
364
+ n = await fetch(this.config.tokenEndpoint, {
365
+ method: "POST",
366
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
367
+ body: t.toString()
368
+ });
369
+ } catch (e) {
370
+ throw this.log("error", "Token refresh network request failed:", e), e;
371
+ }
372
+ if (!n.ok) {
373
+ let e = {};
374
+ try {
375
+ e = await n.json();
376
+ } catch {
377
+ try {
378
+ e = { error: await n.text() };
379
+ } catch {
380
+ e = {};
381
+ }
382
+ }
383
+ this.log("error", "Token refresh failed with status:", n.status, e), (n.status === 400 || n.status === 401) && await this.storage.clearTokens();
384
+ let t = e.error_description || e.error || n.statusText || "Failed to refresh token";
385
+ throw Error(`Failed to refresh token (${n.status}): ${t}`);
386
+ }
387
+ let r = await n.json(), i = {
388
+ accessToken: r.access_token,
389
+ refreshToken: r.refresh_token || e,
390
+ tokenType: r.token_type || "Bearer",
391
+ expiresAt: Date.now() + r.expires_in * 1e3,
392
+ scope: r.scope,
393
+ idToken: r.id_token
394
+ };
395
+ return await this.saveTokenInfo(i), i;
396
+ })().finally(() => {
397
+ this.refreshPromise = null;
398
+ }), this.refreshPromise;
399
+ }
400
+ async saveTokenInfo(e) {
401
+ await this.storage.setItem(o.ACCESS_TOKEN, e.accessToken), await this.storage.setItem(o.TOKEN_EXPIRES_AT, e.expiresAt.toString()), await this.storage.setItem(o.TOKEN_TYPE, e.tokenType), e.refreshToken && await this.storage.setItem(o.REFRESH_TOKEN, e.refreshToken), e.scope && await this.storage.setItem(o.SCOPE, e.scope), e.idToken && await this.storage.setItem(o.ID_TOKEN, e.idToken);
402
+ }
403
+ getAccessToken() {
404
+ return this.storage.getItemSync(o.ACCESS_TOKEN);
405
+ }
406
+ getRefreshToken() {
407
+ return this.storage.getItemSync(o.REFRESH_TOKEN);
408
+ }
409
+ getExpiresAt() {
410
+ let e = this.storage.getItemSync(o.TOKEN_EXPIRES_AT);
411
+ if (!e) return null;
412
+ let t = Number(e);
413
+ return Number.isFinite(t) ? t : null;
414
+ }
415
+ isAuthenticated() {
416
+ let e = this.getExpiresAt();
417
+ return e !== null && Date.now() < e - 3e4 && !!this.getAccessToken();
418
+ }
419
+ async navigateToAccountManagement() {
420
+ await this.storage.hydrate();
421
+ let e = await this.storage.getItem(o.REFRESH_TOKEN);
422
+ if (!e) throw Error("No refresh token available to access account management.");
423
+ if (!this.config.openAuthSession) throw Error("No openAuthSession launcher configured. Pass createExpoWebBrowserLauncher(WebBrowser) or createInAppBrowserLauncher(InAppBrowser) in OAuth2NativeConfig.");
424
+ let t = this.config.authorizationEndpoint.replace(/\/authorize\/?$/, ""), n = await fetch(`${t}/account/ticket`, {
425
+ method: "POST",
426
+ headers: { "Content-Type": "application/json" },
427
+ body: JSON.stringify({
428
+ refreshToken: e,
429
+ clientId: this.config.clientId,
430
+ redirectUri: this.config.redirectUri
431
+ })
432
+ });
433
+ if (!n.ok) throw Error(`Failed to request account management ticket (${n.status})`);
434
+ let { ticket: r } = await n.json();
435
+ if (!r) throw Error("Account management ticket missing from server response.");
436
+ let i = `${t}/account?ticket=${encodeURIComponent(r)}&return_to=${encodeURIComponent(this.config.redirectUri)}`;
437
+ await this.config.openAuthSession(i, this.config.redirectUri);
438
+ }
439
+ async logout() {
440
+ await this.storage.hydrate();
441
+ let e = this.getAccessToken(), t = await this.storage.getItem(o.REFRESH_TOKEN);
442
+ if (await this.storage.clearTokens(), await this.storage.clearTemporary(), this.config.revokeEndpoint) {
443
+ let n = [...t ? [{
444
+ token: t,
445
+ token_type_hint: "refresh_token"
446
+ }] : [], ...e ? [{
447
+ token: e,
448
+ token_type_hint: "access_token"
449
+ }] : []];
450
+ for (let e of n) try {
451
+ await fetch(this.config.revokeEndpoint, {
452
+ method: "POST",
453
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
454
+ body: new URLSearchParams({
455
+ token: e.token,
456
+ token_type_hint: e.token_type_hint,
457
+ client_id: this.config.clientId
458
+ }).toString()
459
+ });
460
+ } catch (e) {
461
+ this.log("error", "Failed to revoke token during logout", e);
462
+ }
463
+ }
464
+ }
465
+ }, D = e(null), O = ({ config: e, client: n, children: o, onLoginSuccess: s, onLoginError: c, subscribeAppState: l }) => {
466
+ let [u] = i(() => {
467
+ if (n) return n;
468
+ if (e) return new E(e);
469
+ throw Error("AuthProvider requires either a `client` or `config` prop.");
470
+ }), [d, f] = i(!1), [p, m] = i(null), [h, g] = i(!0), [_, v] = i(null), y = t(async () => {
471
+ if (await u.initialize(), u.isAuthenticated()) f(!0), m(u.getAccessToken());
472
+ else {
473
+ if (u.getRefreshToken()) try {
474
+ let e = await u.refreshAccessToken();
475
+ f(!0), m(e.accessToken);
476
+ return;
477
+ } catch {
478
+ f(!1), m(null);
479
+ return;
480
+ }
481
+ f(!1), m(null);
482
+ }
483
+ }, [u]);
484
+ r(() => {
485
+ let e = !0;
486
+ return (async () => {
487
+ try {
488
+ await y();
489
+ } finally {
490
+ e && g(!1);
491
+ }
492
+ })(), () => {
493
+ e = !1;
494
+ };
495
+ }, [y]), r(() => {
496
+ if (!d) return;
497
+ let e, t = () => {
498
+ let n = u.getExpiresAt();
499
+ if (!n) return;
500
+ let r = n - Date.now() - 6e4;
501
+ e = setTimeout(async () => {
502
+ try {
503
+ let e = await u.refreshAccessToken();
504
+ m(e.accessToken), f(!0), t();
505
+ } catch (n) {
506
+ u.log("error", "Background token refresh failed:", n), u.getAccessToken() ? e = setTimeout(t, 3e4) : (f(!1), m(null));
507
+ }
508
+ }, Math.max(0, r));
509
+ };
510
+ t();
511
+ let n = l ? l((e) => {
512
+ e === "active" && y().catch(() => {});
513
+ }) : void 0;
514
+ return () => {
515
+ e && clearTimeout(e), n && n();
516
+ };
517
+ }, [
518
+ d,
519
+ u,
520
+ l,
521
+ y
522
+ ]);
523
+ let b = t(async () => {
524
+ g(!0), v(null);
525
+ try {
526
+ let e = await u.authorize();
527
+ m(e.accessToken), f(!0), s?.();
528
+ } catch (e) {
529
+ let t = e instanceof Error ? e : /* @__PURE__ */ Error("Authentication failed");
530
+ v(t), c?.(t);
531
+ } finally {
532
+ g(!1);
533
+ }
534
+ }, [
535
+ u,
536
+ s,
537
+ c
538
+ ]), x = t(async () => {
539
+ g(!0);
540
+ try {
541
+ await u.logout(), f(!1), m(null);
542
+ } catch (e) {
543
+ v(e instanceof Error ? e : /* @__PURE__ */ Error("Logout failed"));
544
+ } finally {
545
+ g(!1);
546
+ }
547
+ }, [u]), S = t(async () => {
548
+ if (await u.initialize(), u.isAuthenticated()) return u.getAccessToken();
549
+ try {
550
+ let e = await u.refreshAccessToken();
551
+ return m(e.accessToken), f(!0), e.accessToken;
552
+ } catch {
553
+ return f(!1), m(null), null;
554
+ }
555
+ }, [u]), C = t(async () => {
556
+ await u.navigateToAccountManagement(), await y();
557
+ }, [u, y]);
558
+ return /* @__PURE__ */ a(D.Provider, {
559
+ value: {
560
+ isAuthenticated: d,
561
+ isLoading: h,
562
+ accessToken: p,
563
+ error: _,
564
+ login: b,
565
+ logout: x,
566
+ getToken: S,
567
+ navigateToAccountManagement: C,
568
+ authClient: u
569
+ },
570
+ children: o
571
+ });
572
+ }, k = () => {
573
+ let e = n(D);
574
+ if (!e) throw Error("useAuth must be used within an AuthProvider");
575
+ return e;
576
+ };
577
+ // src/axios/interceptor.ts
578
+ function A(e, t, n = {}) {
579
+ let r = !1, i = [], a = (e, t = null) => {
580
+ i.forEach((n) => {
581
+ e ? n.reject(e) : n.resolve(t);
582
+ }), i = [];
583
+ }, o = (e) => !e || !n.excludeUrls ? !1 : n.excludeUrls.some((t) => typeof t == "string" ? e.includes(t) : t.test(e)), s = e.interceptors.request.use(async (e) => {
584
+ if (o(e.url)) return e;
585
+ await t.initialize();
586
+ let n = t.getAccessToken();
587
+ if (!t.isAuthenticated() && n) try {
588
+ n = (await t.refreshAccessToken()).accessToken;
589
+ } catch {
590
+ n = null;
591
+ }
592
+ return n && e.headers.set("Authorization", `Bearer ${n}`), e;
593
+ }, (e) => Promise.reject(e)), c = e.interceptors.response.use((e) => e, async (s) => {
594
+ let c = s.config;
595
+ if (!c || o(c.url)) return Promise.reject(s);
596
+ if (s.response?.status === 401 && !c._retry) {
597
+ if (r) return new Promise((e, t) => {
598
+ i.push({
599
+ resolve: e,
600
+ reject: t
601
+ });
602
+ }).then((t) => (t && c.headers.set("Authorization", `Bearer ${t}`), e(c))).catch((e) => Promise.reject(e));
603
+ c._retry = !0, r = !0;
604
+ try {
605
+ let n = (await t.refreshAccessToken()).accessToken;
606
+ return a(null, n), c.headers.set("Authorization", `Bearer ${n}`), e(c);
607
+ } catch (e) {
608
+ return a(e, null), await t.logout(), n.onUnauthorized && n.onUnauthorized(), Promise.reject(e);
609
+ } finally {
610
+ r = !1;
611
+ }
612
+ }
613
+ return Promise.reject(s);
614
+ });
615
+ return () => {
616
+ e.interceptors.request.eject(s), e.interceptors.response.eject(c);
617
+ };
618
+ }
619
+ export { E as AuthClient, O as AuthProvider, y as NativeTokenStorage, o as STORAGE_KEYS, f as base64UrlEncode, _ as createCodeChallenge, x as createExpoSecureStoreAdapter, C as createExpoWebBrowserLauncher, w as createInAppBrowserLauncher, S as createKeychainStorageAdapter, b as createMemoryStorageAdapter, h as generateCodeVerifier, m as generateRandomString, g as generateState, T as parseCallbackUrl, A as setupAxiosInterceptors, d as sha256Bytes, k as useAuth };
@@ -0,0 +1 @@
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("react"),require("react/jsx-runtime")):typeof define==`function`&&define.amd?define([`exports`,`react`,`react/jsx-runtime`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.WitsAuthReactNativeClient={},e.React,e.jsxRuntime))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};t=((e,t,n)=>(n=e==null?{}:r(s(e)),l(i(n,`default`,{value:e,enumerable:!0}),e)))(t,1);var u={ACCESS_TOKEN:`witsauth_access_token`,REFRESH_TOKEN:`witsauth_refresh_token`,TOKEN_EXPIRES_AT:`witsauth_expires_at`,TOKEN_TYPE:`witsauth_token_type`,SCOPE:`witsauth_scope`,ID_TOKEN:`witsauth_id_token`,CODE_VERIFIER:`witsauth_code_verifier`,STATE:`witsauth_state`,NONCE:`witsauth_nonce`},d=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_`,f=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]);function p(e,t){return t>>>e|t<<32-e}function m(e){if(typeof TextEncoder<`u`)return new TextEncoder().encode(e);let t=[];for(let n=0;n<e.length;n++){let r=e.charCodeAt(n);r<128?t.push(r):r<2048?t.push(192|r>>6,128|r&63):r<55296||r>=57344?t.push(224|r>>12,128|r>>6&63,128|r&63):(n++,r=65536+((r&1023)<<10|e.charCodeAt(n)&1023),t.push(240|r>>18,128|r>>12&63,128|r>>6&63,128|r&63))}return new Uint8Array(t)}function h(e){let t=m(e),n=t.length*8,r=t.length+9+63>>6<<6,i=new Uint8Array(r);i.set(t),i[t.length]=128;let a=new DataView(i.buffer);a.setUint32(r-8,Math.floor(n/4294967296),!1),a.setUint32(r-4,n>>>0,!1);let o=1779033703,s=3144134277,c=1013904242,l=2773480762,u=1359893119,d=2600822924,h=528734635,g=1541459225,_=new Uint32Array(64);for(let e=0;e<r;e+=64){for(let t=0;t<16;t++)_[t]=a.getUint32(e+t*4,!1);for(let e=16;e<64;e++){let t=p(7,_[e-15])^p(18,_[e-15])^_[e-15]>>>3,n=p(17,_[e-2])^p(19,_[e-2])^_[e-2]>>>10;_[e]=_[e-16]+t+_[e-7]+n>>>0}let t=o,n=s,r=c,i=l,m=u,v=d,y=h,b=g;for(let e=0;e<64;e++){let a=p(6,m)^p(11,m)^p(25,m),o=m&v^~m&y,s=b+a+o+f[e]+_[e]>>>0,c=(p(2,t)^p(13,t)^p(22,t))+(t&n^t&r^n&r)>>>0;b=y,y=v,v=m,m=i+s>>>0,i=r,r=n,n=t,t=s+c>>>0}o=o+t>>>0,s=s+n>>>0,c=c+r>>>0,l=l+i>>>0,u=u+m>>>0,d=d+v>>>0,h=h+y>>>0,g=g+b>>>0}let v=new Uint8Array(32),y=new DataView(v.buffer);return y.setUint32(0,o,!1),y.setUint32(4,s,!1),y.setUint32(8,c,!1),y.setUint32(12,l,!1),y.setUint32(16,u,!1),y.setUint32(20,d,!1),y.setUint32(24,h,!1),y.setUint32(28,g,!1),v}function g(e){let t=``,n=e.length;for(let r=0;r<n;r+=3){let i=e[r],a=r+1<n?e[r+1]:0,o=r+2<n?e[r+2]:0;t+=d[i>>2],t+=d[(i&3)<<4|a>>4],r+1<n&&(t+=d[(a&15)<<2|o>>6]),r+2<n&&(t+=d[o&63])}return t}function _(e){let t=new Uint8Array(e),n=typeof globalThis<`u`?globalThis:void 0;if(n?.crypto&&typeof n.crypto.getRandomValues==`function`)return n.crypto.getRandomValues(t),t;for(let n=0;n<e;n++)t[n]=Math.floor(Math.random()*256);return t}function v(e=64){let t=_(e),n=``;for(let r=0;r<e;r++)n+=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~`[t[r]%66];return n}function y(){return v(64)}function b(){return v(32)}async function x(e){let t=typeof globalThis<`u`?globalThis:void 0;if(t?.crypto?.subtle&&typeof t.crypto.subtle.digest==`function`)try{let n=m(e),r=await t.crypto.subtle.digest(`SHA-256`,n);return g(new Uint8Array(r))}catch{}return g(h(e))}var S=Object.values(u),C=class{backend;cache=new Map;hydrated=!1;hydratePromise=null;constructor(e){this.backend=e||w()}async hydrate(){if(!this.hydrated)return this.hydratePromise||=(async()=>{await Promise.all(S.map(async e=>{try{let t=await this.backend.getItem(e);t!=null&&this.cache.set(e,t)}catch{}})),this.hydrated=!0})().finally(()=>{this.hydratePromise=null}),this.hydratePromise}getItemSync(e){return this.cache.get(e)??null}async getItem(e){if(this.cache.has(e))return this.cache.get(e);let t=await this.backend.getItem(e);return t==null?null:(this.cache.set(e,t),t)}async setItem(e,t){this.cache.set(e,t),await this.backend.setItem(e,t)}async removeItem(e){this.cache.delete(e),await this.backend.removeItem(e)}async clearTemporary(){await Promise.all([this.removeItem(u.CODE_VERIFIER),this.removeItem(u.STATE),this.removeItem(u.NONCE)])}async clearTokens(){await Promise.all([this.removeItem(u.ACCESS_TOKEN),this.removeItem(u.REFRESH_TOKEN),this.removeItem(u.TOKEN_EXPIRES_AT),this.removeItem(u.TOKEN_TYPE),this.removeItem(u.SCOPE),this.removeItem(u.ID_TOKEN)])}};function w(){let e=new Map;return{getItem:async t=>e.get(t)??null,setItem:async(t,n)=>{e.set(t,n)},removeItem:async t=>{e.delete(t)}}}function T(e){return{getItem:t=>e.getItemAsync(t),setItem:(t,n)=>e.setItemAsync(t,n),removeItem:t=>e.deleteItemAsync(t)}}function E(e,t=`com.witsauth.tokens`){return{getItem:async n=>{let r=await e.getGenericPassword({service:`${t}.${n}`});return r?r.password:null},setItem:async(n,r)=>{await e.setGenericPassword(n,r,{service:`${t}.${n}`})},removeItem:async n=>{await e.resetGenericPassword({service:`${t}.${n}`})}}}function D(e){return async(t,n)=>{let r=await e.openAuthSessionAsync(t,n);return r.type===`success`&&r.url?{type:`success`,url:r.url}:r.type===`cancel`?{type:`cancel`}:{type:`dismiss`}}}function O(e){return async(t,n)=>{if(!await e.isAvailable())throw Error(`InAppBrowser is not available on this device.`);let r=await e.openAuth(t,n,{ephemeralWebSession:!1,showTitle:!1,enableUrlBarHiding:!0,enableDefaultShare:!1});return r.type===`success`&&r.url?{type:`success`,url:r.url}:r.type===`cancel`?{type:`cancel`}:{type:`dismiss`}}}function k(e){let t=e.indexOf(`?`);if(t===-1)return{code:null,state:null,error:null,errorDescription:null};let n=e.indexOf(`#`,t),r=n===-1?e.slice(t+1):e.slice(t+1,n),i=new URLSearchParams(r);return{code:i.get(`code`),state:i.get(`state`),error:i.get(`error`),errorDescription:i.get(`error_description`)}}var A=class{config;storage;refreshPromise=null;constructor(e,t){this.config=e,this.storage=t||new C(e.storage)}async initialize(){await this.storage.hydrate()}log(e,...t){let n={none:0,error:1,warn:2,info:3,debug:4},r=n[this.config.logLevel||`warn`]||0,i=n[e]||0;i>0&&r>=i&&console[e](...t)}async buildAuthorizationUrl(){await this.storage.hydrate();let e=y(),t=await x(e),n=b(),r=this.config.nonce||b();await this.storage.setItem(u.CODE_VERIFIER,e),await this.storage.setItem(u.STATE,n),await this.storage.setItem(u.NONCE,r);let i=new URLSearchParams({client_id:this.config.clientId,redirect_uri:this.config.redirectUri,response_type:this.config.responseType||`code`,code_challenge_method:this.config.codeChallengeMethod||`S256`,code_challenge:t,state:n,nonce:r});if(this.config.theme){let e=typeof this.config.theme==`function`?this.config.theme():this.config.theme;e&&i.append(`theme`,e)}return this.config.scope&&i.append(`scope`,this.config.scope),this.config.audience&&i.append(`audience`,this.config.audience),`${this.config.authorizationEndpoint}?${i.toString()}`}async authorize(){if(!this.config.openAuthSession)throw Error(`No openAuthSession launcher configured. Pass createExpoWebBrowserLauncher(WebBrowser) or createInAppBrowserLauncher(InAppBrowser) in OAuth2NativeConfig.`);let e=await this.buildAuthorizationUrl(),t=await this.config.openAuthSession(e,this.config.redirectUri);if(t.type!==`success`||!t.url)throw await this.storage.clearTemporary(),Error(`Authentication ${t.type}`);return this.handleCallbackUrl(t.url)}async handleCallbackUrl(e){let{code:t,state:n,error:r,errorDescription:i}=k(e);if(r)throw await this.storage.clearTemporary(),Error(i?`${r}: ${i}`:r);if(!t||!n)throw await this.storage.clearTemporary(),Error(`Callback URL is missing required code or state parameters.`);return this.exchangeCodeForTokens(t,n)}async exchangeCodeForTokens(e,t){await this.storage.hydrate();let n=await this.storage.getItem(u.STATE),r=await this.storage.getItem(u.CODE_VERIFIER);if(!n||t!==n)throw await this.storage.clearTemporary(),Error(`Invalid state parameter. Possible CSRF attack.`);if(!r)throw await this.storage.clearTemporary(),Error(`Code verifier missing from storage.`);let i=new URLSearchParams({grant_type:`authorization_code`,client_id:this.config.clientId,redirect_uri:this.config.redirectUri,code:e,code_verifier:r}),a=await fetch(this.config.tokenEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:i.toString()});if(!a.ok)throw Error(`Failed to exchange code for tokens`);let o=await a.json(),s={accessToken:o.access_token,refreshToken:o.refresh_token,tokenType:o.token_type||`Bearer`,expiresAt:Date.now()+o.expires_in*1e3,scope:o.scope,idToken:o.id_token};return await this.saveTokenInfo(s),await this.storage.clearTemporary(),s}async refreshAccessToken(){return this.refreshPromise||=(async()=>{await this.storage.hydrate();let e=await this.storage.getItem(u.REFRESH_TOKEN);if(!e)throw Error(`No refresh token available`);let t=new URLSearchParams({grant_type:`refresh_token`,client_id:this.config.clientId,refresh_token:e}),n;try{n=await fetch(this.config.tokenEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:t.toString()})}catch(e){throw this.log(`error`,`Token refresh network request failed:`,e),e}if(!n.ok){let e={};try{e=await n.json()}catch{try{e={error:await n.text()}}catch{e={}}}this.log(`error`,`Token refresh failed with status:`,n.status,e),(n.status===400||n.status===401)&&await this.storage.clearTokens();let t=e.error_description||e.error||n.statusText||`Failed to refresh token`;throw Error(`Failed to refresh token (${n.status}): ${t}`)}let r=await n.json(),i={accessToken:r.access_token,refreshToken:r.refresh_token||e,tokenType:r.token_type||`Bearer`,expiresAt:Date.now()+r.expires_in*1e3,scope:r.scope,idToken:r.id_token};return await this.saveTokenInfo(i),i})().finally(()=>{this.refreshPromise=null}),this.refreshPromise}async saveTokenInfo(e){await this.storage.setItem(u.ACCESS_TOKEN,e.accessToken),await this.storage.setItem(u.TOKEN_EXPIRES_AT,e.expiresAt.toString()),await this.storage.setItem(u.TOKEN_TYPE,e.tokenType),e.refreshToken&&await this.storage.setItem(u.REFRESH_TOKEN,e.refreshToken),e.scope&&await this.storage.setItem(u.SCOPE,e.scope),e.idToken&&await this.storage.setItem(u.ID_TOKEN,e.idToken)}getAccessToken(){return this.storage.getItemSync(u.ACCESS_TOKEN)}getRefreshToken(){return this.storage.getItemSync(u.REFRESH_TOKEN)}getExpiresAt(){let e=this.storage.getItemSync(u.TOKEN_EXPIRES_AT);if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null}isAuthenticated(){let e=this.getExpiresAt();return e!==null&&Date.now()<e-3e4&&!!this.getAccessToken()}async navigateToAccountManagement(){await this.storage.hydrate();let e=await this.storage.getItem(u.REFRESH_TOKEN);if(!e)throw Error(`No refresh token available to access account management.`);if(!this.config.openAuthSession)throw Error(`No openAuthSession launcher configured. Pass createExpoWebBrowserLauncher(WebBrowser) or createInAppBrowserLauncher(InAppBrowser) in OAuth2NativeConfig.`);let t=this.config.authorizationEndpoint.replace(/\/authorize\/?$/,``),n=await fetch(`${t}/account/ticket`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({refreshToken:e,clientId:this.config.clientId,redirectUri:this.config.redirectUri})});if(!n.ok)throw Error(`Failed to request account management ticket (${n.status})`);let{ticket:r}=await n.json();if(!r)throw Error(`Account management ticket missing from server response.`);let i=`${t}/account?ticket=${encodeURIComponent(r)}&return_to=${encodeURIComponent(this.config.redirectUri)}`;await this.config.openAuthSession(i,this.config.redirectUri)}async logout(){await this.storage.hydrate();let e=this.getAccessToken(),t=await this.storage.getItem(u.REFRESH_TOKEN);if(await this.storage.clearTokens(),await this.storage.clearTemporary(),this.config.revokeEndpoint){let n=[...t?[{token:t,token_type_hint:`refresh_token`}]:[],...e?[{token:e,token_type_hint:`access_token`}]:[]];for(let e of n)try{await fetch(this.config.revokeEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:new URLSearchParams({token:e.token,token_type_hint:e.token_type_hint,client_id:this.config.clientId}).toString()})}catch(e){this.log(`error`,`Failed to revoke token during logout`,e)}}}},j=(0,t.createContext)(null),M=({config:e,client:r,children:i,onLoginSuccess:a,onLoginError:o,subscribeAppState:s})=>{let[c]=(0,t.useState)(()=>{if(r)return r;if(e)return new A(e);throw Error("AuthProvider requires either a `client` or `config` prop.")}),[l,u]=(0,t.useState)(!1),[d,f]=(0,t.useState)(null),[p,m]=(0,t.useState)(!0),[h,g]=(0,t.useState)(null),_=(0,t.useCallback)(async()=>{if(await c.initialize(),c.isAuthenticated())u(!0),f(c.getAccessToken());else{if(c.getRefreshToken())try{let e=await c.refreshAccessToken();u(!0),f(e.accessToken);return}catch{u(!1),f(null);return}u(!1),f(null)}},[c]);(0,t.useEffect)(()=>{let e=!0;return(async()=>{try{await _()}finally{e&&m(!1)}})(),()=>{e=!1}},[_]),(0,t.useEffect)(()=>{if(!l)return;let e,t=()=>{let n=c.getExpiresAt();if(!n)return;let r=n-Date.now()-6e4;e=setTimeout(async()=>{try{let e=await c.refreshAccessToken();f(e.accessToken),u(!0),t()}catch(n){c.log(`error`,`Background token refresh failed:`,n),c.getAccessToken()?e=setTimeout(t,3e4):(u(!1),f(null))}},Math.max(0,r))};t();let n=s?s(e=>{e===`active`&&_().catch(()=>{})}):void 0;return()=>{e&&clearTimeout(e),n&&n()}},[l,c,s,_]);let v=(0,t.useCallback)(async()=>{m(!0),g(null);try{let e=await c.authorize();f(e.accessToken),u(!0),a?.()}catch(e){let t=e instanceof Error?e:Error(`Authentication failed`);g(t),o?.(t)}finally{m(!1)}},[c,a,o]),y=(0,t.useCallback)(async()=>{m(!0);try{await c.logout(),u(!1),f(null)}catch(e){g(e instanceof Error?e:Error(`Logout failed`))}finally{m(!1)}},[c]),b=(0,t.useCallback)(async()=>{if(await c.initialize(),c.isAuthenticated())return c.getAccessToken();try{let e=await c.refreshAccessToken();return f(e.accessToken),u(!0),e.accessToken}catch{return u(!1),f(null),null}},[c]),x=(0,t.useCallback)(async()=>{await c.navigateToAccountManagement(),await _()},[c,_]);return(0,n.jsx)(j.Provider,{value:{isAuthenticated:l,isLoading:p,accessToken:d,error:h,login:v,logout:y,getToken:b,navigateToAccountManagement:x,authClient:c},children:i})},N=()=>{let e=(0,t.useContext)(j);if(!e)throw Error(`useAuth must be used within an AuthProvider`);return e};function P(e,t,n={}){let r=!1,i=[],a=(e,t=null)=>{i.forEach(n=>{e?n.reject(e):n.resolve(t)}),i=[]},o=e=>!e||!n.excludeUrls?!1:n.excludeUrls.some(t=>typeof t==`string`?e.includes(t):t.test(e)),s=e.interceptors.request.use(async e=>{if(o(e.url))return e;await t.initialize();let n=t.getAccessToken();if(!t.isAuthenticated()&&n)try{n=(await t.refreshAccessToken()).accessToken}catch{n=null}return n&&e.headers.set(`Authorization`,`Bearer ${n}`),e},e=>Promise.reject(e)),c=e.interceptors.response.use(e=>e,async s=>{let c=s.config;if(!c||o(c.url))return Promise.reject(s);if(s.response?.status===401&&!c._retry){if(r)return new Promise((e,t)=>{i.push({resolve:e,reject:t})}).then(t=>(t&&c.headers.set(`Authorization`,`Bearer ${t}`),e(c))).catch(e=>Promise.reject(e));c._retry=!0,r=!0;try{let n=(await t.refreshAccessToken()).accessToken;return a(null,n),c.headers.set(`Authorization`,`Bearer ${n}`),e(c)}catch(e){return a(e,null),await t.logout(),n.onUnauthorized&&n.onUnauthorized(),Promise.reject(e)}finally{r=!1}}return Promise.reject(s)});return()=>{e.interceptors.request.eject(s),e.interceptors.response.eject(c)}}e.AuthClient=A,e.AuthProvider=M,e.NativeTokenStorage=C,e.STORAGE_KEYS=u,e.base64UrlEncode=g,e.createCodeChallenge=x,e.createExpoSecureStoreAdapter=T,e.createExpoWebBrowserLauncher=D,e.createInAppBrowserLauncher=O,e.createKeychainStorageAdapter=E,e.createMemoryStorageAdapter=w,e.generateCodeVerifier=y,e.generateRandomString=v,e.generateState=b,e.parseCallbackUrl=k,e.setupAxiosInterceptors=P,e.sha256Bytes=h,e.useAuth=N});
@@ -0,0 +1,104 @@
1
+ export interface OAuth2AsyncStorage {
2
+ getItem(key: string): Promise<string | null> | string | null;
3
+ setItem(key: string, value: string): Promise<void> | void;
4
+ removeItem(key: string): Promise<void> | void;
5
+ }
6
+ export interface BrowserSessionResult {
7
+ type: 'success' | 'cancel' | 'dismiss';
8
+ url?: string;
9
+ }
10
+ export type BrowserSessionLauncher = (authUrl: string, redirectUri: string) => Promise<BrowserSessionResult>;
11
+ export interface OAuth2NativeConfig {
12
+ /**
13
+ * The Client ID obtained from the WitsAuth console.
14
+ */
15
+ clientId: string;
16
+ /**
17
+ * The authorization endpoint URL.
18
+ * E.g., https://auth.example.com/auth/authorize
19
+ */
20
+ authorizationEndpoint: string;
21
+ /**
22
+ * The token endpoint URL.
23
+ * E.g., https://auth.example.com/oauth/token
24
+ */
25
+ tokenEndpoint: string;
26
+ /**
27
+ * The callback URI configured for your mobile application.
28
+ * Supports custom schemes (e.g., myapp://auth/callback) and Universal/App Links.
29
+ */
30
+ redirectUri: string;
31
+ /**
32
+ * Optional userInfo endpoint URL.
33
+ * E.g., https://auth.example.com/oauth/userinfo
34
+ */
35
+ userInfoEndpoint?: string;
36
+ /**
37
+ * Optional token revocation endpoint URL.
38
+ * E.g., https://auth.example.com/oauth/revoke
39
+ */
40
+ revokeEndpoint?: string;
41
+ /**
42
+ * Optional logout endpoint URL.
43
+ * E.g., https://auth.example.com/auth/logout
44
+ */
45
+ logoutEndpoint?: string;
46
+ /**
47
+ * Scopes to request, separated by spaces.
48
+ * E.g., "openid profile email offline_access"
49
+ */
50
+ scope?: string;
51
+ /**
52
+ * Audience parameter to request (if required by WitsAuth).
53
+ */
54
+ audience?: string;
55
+ /**
56
+ * Optional static nonce value. If omitted, a random cryptographic nonce is generated per authorization request.
57
+ */
58
+ nonce?: string;
59
+ /**
60
+ * Optional theme hint ("light" | "dark") or a callback returning the current theme.
61
+ */
62
+ theme?: 'light' | 'dark' | (() => 'light' | 'dark' | undefined);
63
+ /**
64
+ * Response type. Defaults to "code".
65
+ */
66
+ responseType?: string;
67
+ /**
68
+ * PKCE code challenge method. Defaults to "S256".
69
+ */
70
+ codeChallengeMethod?: 'S256' | 'plain';
71
+ /**
72
+ * Secure async storage adapter (e.g., Expo SecureStore or React Native Keychain).
73
+ * If omitted, falls back to an in-memory store.
74
+ */
75
+ storage?: OAuth2AsyncStorage;
76
+ /**
77
+ * Custom system browser session launcher (ASWebAuthenticationSession on iOS / Chrome Custom Tabs on Android).
78
+ * If omitted, auto-detects `expo-web-browser` or `react-native-inappbrowser-reborn`.
79
+ */
80
+ openAuthSession?: BrowserSessionLauncher;
81
+ /**
82
+ * Log level for internal client messages. Defaults to 'warn'.
83
+ */
84
+ logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug';
85
+ }
86
+ export interface TokenInfo {
87
+ accessToken: string;
88
+ refreshToken?: string;
89
+ expiresAt: number;
90
+ tokenType: string;
91
+ scope?: string;
92
+ idToken?: string;
93
+ }
94
+ export declare const STORAGE_KEYS: {
95
+ readonly ACCESS_TOKEN: 'witsauth_access_token';
96
+ readonly REFRESH_TOKEN: 'witsauth_refresh_token';
97
+ readonly TOKEN_EXPIRES_AT: 'witsauth_expires_at';
98
+ readonly TOKEN_TYPE: 'witsauth_token_type';
99
+ readonly SCOPE: 'witsauth_scope';
100
+ readonly ID_TOKEN: 'witsauth_id_token';
101
+ readonly CODE_VERIFIER: 'witsauth_code_verifier';
102
+ readonly STATE: 'witsauth_state';
103
+ readonly NONCE: 'witsauth_nonce';
104
+ };
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { AuthClient } from '../core/AuthClient';
3
+ import { OAuth2NativeConfig } from '../models/types';
4
+ export interface AuthContextType {
5
+ isAuthenticated: boolean;
6
+ isLoading: boolean;
7
+ accessToken: string | null;
8
+ error: Error | null;
9
+ login: () => Promise<void>;
10
+ logout: () => Promise<void>;
11
+ getToken: () => Promise<string | null>;
12
+ navigateToAccountManagement: () => Promise<void>;
13
+ authClient: AuthClient;
14
+ }
15
+ export interface AuthProviderProps {
16
+ config?: OAuth2NativeConfig;
17
+ client?: AuthClient;
18
+ children: React.ReactNode;
19
+ onLoginSuccess?: () => void;
20
+ onLoginError?: (error: Error) => void;
21
+ /**
22
+ * Optional subscription to React Native's `AppState.addEventListener('change', listener)`
23
+ * so tokens automatically refresh when the app returns to the foreground (`active`).
24
+ */
25
+ subscribeAppState?: (listener: (state: string) => void) => () => void;
26
+ }
27
+ export declare const AuthProvider: React.FC<AuthProviderProps>;
28
+ export declare const useAuth: () => AuthContextType;
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@witsauth/react-native-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Official React Native client library (Expo and Bare React Native CLI) for integrating WitsAuth OAuth 2.0 + PKCE authentication on Android and iOS.",
5
+ "main": "./dist/index.umd.js",
6
+ "module": "./dist/index.es.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.es.js",
11
+ "require": "./dist/index.umd.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "dev": "vite",
20
+ "build": "tsc && vite build && tsc -p tsconfig.build.json",
21
+ "test": "vitest run",
22
+ "prepublishOnly": "pnpm test && pnpm build"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "keywords": [
28
+ "react-native",
29
+ "expo",
30
+ "android",
31
+ "ios",
32
+ "witsauth",
33
+ "auth",
34
+ "oauth2",
35
+ "pkce"
36
+ ],
37
+ "author": "",
38
+ "license": "ISC",
39
+ "type": "module",
40
+ "peerDependencies": {
41
+ "axios": "^1.0.0",
42
+ "react": "^18.0.0 || ^19.0.0",
43
+ "react-native": ">=0.72.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "axios": {
47
+ "optional": true
48
+ },
49
+ "react-native": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "devDependencies": {
54
+ "@testing-library/jest-dom": "^7.0.0",
55
+ "@testing-library/react": "^16.3.2",
56
+ "@types/node": "^24.13.2",
57
+ "@types/react": "^19.2.17",
58
+ "axios": "^1.18.1",
59
+ "jsdom": "^29.1.1",
60
+ "react": "^19.2.7",
61
+ "typescript": "^7.0.2",
62
+ "vite": "^8.1.5",
63
+ "vitest": "^4.1.10"
64
+ }
65
+ }