more package declarations

This commit is contained in:
2026-05-17 21:52:38 -04:00
parent a8a5930ced
commit f118d3a4f3
44 changed files with 14019 additions and 1918 deletions

View File

@@ -0,0 +1,249 @@
/**
* API Client for ShieldAI services
* Handles authentication, request/response interception, and error handling
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { tokenStorage } from '../storage/token-storage';
import { requestQueue } from '../utils/request-queue';
import type { AuthTokens, AuthResponse, RefreshTokenRequest } from '../types';
export interface ApiClientConfig {
baseURL: string;
timeout?: number;
debug?: boolean;
}
export class ApiClient {
private client: AxiosInstance;
private config: ApiClientConfig;
private isRefreshing = false;
private refreshSubscribers: Set<(token: string) => void> = new Set();
constructor(config: ApiClientConfig) {
this.config = {
baseURL: config.baseURL,
timeout: config.timeout ?? 30000,
debug: config.debug ?? false,
};
this.client = axios.create({
baseURL: this.config.baseURL,
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors(): void {
// Request interceptor - add auth token
this.client.interceptors.request.use(
async (config) => {
if (this.config.debug) {
console.log('[API] Request:', config.method?.toUpperCase(), config.url);
}
// Add auth token if available
const token = await tokenStorage.getAccessToken();
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
// Queue request if offline
if (!requestQueue.isOnline() && this.requiresNetwork(config)) {
await requestQueue.enqueue(config);
throw new Error('OFFLINE');
}
return config;
},
(error) => {
if (error.message === 'OFFLINE') {
return Promise.reject({ offline: true, config: error.config });
}
return Promise.reject(error);
}
);
// Response interceptor - handle errors and token refresh
this.client.interceptors.response.use(
(response) => {
if (this.config.debug) {
console.log('[API] Response:', response.status, response.config.url);
}
return response;
},
async (error: AxiosError) => {
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
// Handle 401 - unauthorized
if (error.response?.status === 401 && !originalRequest._retry) {
if (this.isRefreshing) {
// Wait for refresh to complete
return new Promise((resolve) => {
this.refreshSubscribers.add((token) => {
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(this.client(originalRequest));
});
});
}
originalRequest._retry = true;
this.isRefreshing = true;
try {
const refreshToken = await tokenStorage.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token');
}
const newTokens = await this.refreshAccessToken(refreshToken);
await tokenStorage.saveTokens(newTokens.accessToken, newTokens.refreshToken);
// Retry failed requests
this.refreshSubscribers.forEach((callback) => callback(newTokens.accessToken));
this.refreshSubscribers.clear();
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;
return this.client(originalRequest);
} catch (refreshError) {
// Refresh failed - clear tokens and redirect to login
await tokenStorage.clearTokens();
this.refreshSubscribers.clear();
return Promise.reject(refreshError);
} finally {
this.isRefreshing = false;
}
}
return Promise.reject(error);
}
);
}
private requiresNetwork(config: AxiosRequestConfig): boolean {
// Don't queue GET requests that can be cached
const method = (config.method || 'get').toLowerCase();
return method !== 'get';
}
private async refreshAccessToken(refreshToken: string): Promise<AuthTokens> {
const response = await this.client.post<AuthResponse>('/auth/refresh', {
refreshToken,
});
return {
accessToken: response.data.tokens.accessToken,
refreshToken: response.data.tokens.refreshToken,
expiresIn: response.data.tokens.expiresIn,
tokenType: response.data.tokens.tokenType,
};
}
// Subscribe to token refresh
onTokenRefresh(callback: (token: string) => void): () => void {
this.refreshSubscribers.add(callback);
return () => this.refreshSubscribers.delete(callback);
}
// Public API methods
async get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.get<T>(url, config);
}
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.post<T>(url, data, config);
}
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.put<T>(url, data, config);
}
async patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.patch<T>(url, data, config);
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.delete<T>(url, config);
}
// Auth methods
async login(email: string, password: string): Promise<AuthResponse> {
const response = await this.post<AuthResponse>('/auth/login', {
email,
password,
});
if (response.data.tokens) {
await tokenStorage.saveTokens(
response.data.tokens.accessToken,
response.data.tokens.refreshToken
);
}
return response.data;
}
async register(data: {
email: string;
password: string;
firstName?: string;
lastName?: string;
}): Promise<AuthResponse> {
const response = await this.post<AuthResponse>('/auth/register', data);
if (response.data.tokens) {
await tokenStorage.saveTokens(
response.data.tokens.accessToken,
response.data.tokens.refreshToken
);
}
return response.data;
}
async logout(): Promise<void> {
try {
await this.post('/auth/logout');
} finally {
await tokenStorage.clearTokens();
}
}
async isAuthenticated(): Promise<boolean> {
const token = await tokenStorage.getAccessToken();
return !!token;
}
// Health check
async healthCheck(): Promise<{ status: string; version: string }> {
const response = await this.get<{ status: string; version: string }>('/health');
return response.data;
}
// Get the underlying axios instance for advanced usage
getClient(): AxiosInstance {
return this.client;
}
}
// Default client instance
let defaultClient: ApiClient | null = null;
export const createApiClient = (config: ApiClientConfig): ApiClient => {
defaultClient = new ApiClient(config);
return defaultClient;
};
export const getApiClient = (): ApiClient => {
if (!defaultClient) {
throw new Error('API Client not initialized. Call createApiClient first.');
}
return defaultClient;
};

View File

@@ -0,0 +1,46 @@
/**
* Authentication API service
*/
import { getApiClient } from './api-client';
import type {
User,
AuthResponse,
LoginCredentials,
RegisterData,
AuthTokens
} from '../types';
export class AuthService {
private api = getApiClient();
async login(credentials: LoginCredentials): Promise<AuthResponse> {
const response = await this.api.login(credentials.email, credentials.password);
return response;
}
async register(data: RegisterData): Promise<AuthResponse> {
const response = await this.api.register(data);
return response;
}
async logout(): Promise<void> {
await this.api.logout();
}
async getCurrentUser(): Promise<User> {
const response = await this.api.get<{ user: User; authType: string }>('/auth/user/me');
return response.data.user;
}
async refreshToken(): Promise<AuthTokens> {
const response = await this.api.get<AuthTokens>('/auth/refresh-token');
return response.data;
}
async isAuthenticated(): Promise<boolean> {
return await this.api.isAuthenticated();
}
}
export const authService = new AuthService();

View File

@@ -0,0 +1,47 @@
/**
* Device API service
*/
import { getApiClient } from './api-client';
import type { Device, DeviceRegistration, DeviceListResponse } from '../types';
export class DeviceService {
private api = getApiClient();
async registerDevice(data: DeviceRegistration): Promise<Device> {
const response = await this.api.post<Device>('/api/v1/devices/register', data);
return response.data;
}
async updatePushToken(pushToken: string): Promise<Device> {
const response = await this.api.patch<Device>('/api/v1/devices/push-token', {
pushToken,
});
return response.data;
}
async getDevices(): Promise<DeviceListResponse> {
const response = await this.api.get<DeviceListResponse>('/api/v1/devices');
return response.data;
}
async getDevice(deviceId: string): Promise<Device> {
const response = await this.api.get<Device>(`/api/v1/devices/${deviceId}`);
return response.data;
}
async deleteDevice(deviceId: string): Promise<void> {
await this.api.delete(`/api/v1/devices/${deviceId}`);
}
async getCurrentDevice(): Promise<Device | null> {
try {
const response = await this.api.get<Device>('/api/v1/devices/current');
return response.data;
} catch {
return null;
}
}
}
export const deviceService = new DeviceService();

View File

@@ -0,0 +1,53 @@
/**
* Notification API service
*/
import { getApiClient } from './api-client';
import type { Notification, NotificationListResponse, NotificationPreferences } from '../types';
export class NotificationService {
private api = getApiClient();
async getNotifications(params?: {
page?: number;
limit?: number;
unreadOnly?: boolean;
}): Promise<NotificationListResponse> {
const response = await this.api.get<NotificationListResponse>('/notifications', { params });
return response.data;
}
async getNotification(notificationId: string): Promise<Notification> {
const response = await this.api.get<Notification>(`/notifications/${notificationId}`);
return response.data;
}
async markAsRead(notificationId: string): Promise<void> {
await this.api.patch(`/notifications/${notificationId}/read`);
}
async markAllAsRead(): Promise<void> {
await this.api.post('/notifications/read-all');
}
async deleteNotification(notificationId: string): Promise<void> {
await this.api.delete(`/notifications/${notificationId}`);
}
async getPreferences(): Promise<NotificationPreferences> {
const response = await this.api.get<NotificationPreferences>('/notifications/preferences');
return response.data;
}
async updatePreferences(preferences: NotificationPreferences): Promise<NotificationPreferences> {
const response = await this.api.put<NotificationPreferences>('/notifications/preferences', preferences);
return response.data;
}
async getUnreadCount(): Promise<number> {
const response = await this.api.get<{ count: number }>('/notifications/unread-count');
return response.data.count;
}
}
export const notificationService = new NotificationService();

View File

@@ -0,0 +1,53 @@
/**
* Subscription API service
*/
import { getApiClient } from './api-client';
import type {
Subscription,
SubscriptionTier,
SubscriptionStatusResponse,
CreateSubscriptionRequest,
UpdateSubscriptionRequest,
} from '../types';
export class SubscriptionService {
private api = getApiClient();
async getSubscription(): Promise<SubscriptionStatusResponse> {
const response = await this.api.get<SubscriptionStatusResponse>('/billing/subscription');
return response.data;
}
async createSubscription(data: CreateSubscriptionRequest): Promise<Subscription> {
const response = await this.api.post<Subscription>('/billing/subscription', data);
return response.data;
}
async updateSubscription(data: UpdateSubscriptionRequest): Promise<Subscription> {
const response = await this.api.patch<Subscription>('/billing/subscription', data);
return response.data;
}
async cancelSubscription(): Promise<Subscription> {
const response = await this.api.delete<Subscription>('/billing/subscription');
return response.data;
}
async getTiers(): Promise<SubscriptionTier[]> {
const response = await this.api.get<SubscriptionTier[]>('/billing/tiers');
return response.data;
}
async createCheckoutSession(tier: string): Promise<{ url: string }> {
const response = await this.api.post<{ url: string }>('/billing/checkout', { tier });
return response.data;
}
async createCustomerPortalSession(): Promise<{ url: string }> {
const response = await this.api.post<{ url: string }>('/billing/customer-portal');
return response.data;
}
}
export const subscriptionService = new SubscriptionService();

View File

@@ -0,0 +1,53 @@
/**
* ShieldAI Mobile API Client
*
* A comprehensive TypeScript API client library for React Native apps
* to interact with ShieldAI backend services.
*
* @example
* ```typescript
* import { createApiClient, authService, deviceService } from '@shieldai/mobile-api-client';
*
* // Initialize the client
* createApiClient({
* baseURL: 'https://api.shieldai.freno.me/api/v1',
* timeout: 30000,
* debug: __DEV__,
* });
*
* // Login
* const { user, tokens } = await authService.login({
* email: 'user@example.com',
* password: 'password123',
* });
*
* // Register device for push notifications
* await deviceService.registerDevice({
* platform: 'ios',
* pushToken: '...',
* });
* ```
*/
// Core API client
export {
createApiClient,
getApiClient,
ApiClient,
ApiClientConfig,
} from './api/api-client';
// Services
export { authService, AuthService } from './api/auth.service';
export { deviceService, DeviceService } from './api/device.service';
export { subscriptionService, SubscriptionService } from './api/subscription.service';
export { notificationService, NotificationService } from './api/notification.service';
// Types
export * from './types';
// Storage
export { storage, tokenStorage, StorageAdapter } from './storage/token-storage';
// Utils
export { requestQueue, RequestQueue } from './utils/request-queue';

View File

@@ -0,0 +1,21 @@
// Type declarations for React Native and Expo packages
declare module 'expo-secure-store' {
export function getItemAsync(key: string): Promise<string | null>;
export function setItemAsync(key: string, value: string): Promise<void>;
export function deleteItemAsync(key: string): Promise<void>;
}
declare module '@react-native-async-storage/async-storage' {
export function getItem(key: string): Promise<string | null>;
export function setItem(key: string, value: string): Promise<void>;
export function removeItem(key: string): Promise<void>;
export function clear(): Promise<void>;
}
declare module 'react-native' {
export import Platform = require('react-native/Libraries/Utilities/Platform');
export const Platform: {
OS: 'ios' | 'android' | 'web' | 'windows' | 'macos';
Version: number;
};
}

View File

@@ -0,0 +1,93 @@
/**
* Secure storage for authentication tokens
* Uses expo-secure-store for production, AsyncStorage for fallback
*/
import * as SecureStore from 'expo-secure-store';
import AsyncStorage from '@react-native-async-storage/async-storage';
const ACCESS_TOKEN_KEY = '@shieldai:access_token';
const REFRESH_TOKEN_KEY = '@shieldai:refresh_token';
export interface StorageAdapter {
getItem: (key: string) => Promise<string | null>;
setItem: (key: string, value: string) => Promise<void>;
removeItem: (key: string) => Promise<void>;
}
class SecureStorageAdapter implements StorageAdapter {
async getItem(key: string): Promise<string | null> {
try {
return await SecureStore.getItemAsync(key);
} catch {
// Fallback to AsyncStorage if SecureStore fails
return await AsyncStorage.getItem(key);
}
}
async setItem(key: string, value: string): Promise<void> {
try {
await SecureStore.setItemAsync(key, value);
} catch {
await AsyncStorage.setItem(key, value);
}
}
async removeItem(key: string): Promise<void> {
try {
await SecureStore.deleteItemAsync(key);
} catch {
await AsyncStorage.removeItem(key);
}
}
}
class InMemoryStorageAdapter implements StorageAdapter {
private store: Map<string, string> = new Map();
async getItem(key: string): Promise<string | null> {
return this.store.get(key) || null;
}
async setItem(key: string, value: string): Promise<void> {
this.store.set(key, value);
}
async removeItem(key: string): Promise<void> {
this.store.delete(key);
}
}
// Detect environment and choose appropriate storage
const getStorageAdapter = (): StorageAdapter => {
if (process.env.NODE_ENV === 'test') {
return new InMemoryStorageAdapter();
}
return new SecureStorageAdapter();
};
export const storage = getStorageAdapter();
export const tokenStorage = {
async getAccessToken(): Promise<string | null> {
return await storage.getItem(ACCESS_TOKEN_KEY);
},
async getRefreshToken(): Promise<string | null> {
return await storage.getItem(REFRESH_TOKEN_KEY);
},
async saveTokens(accessToken: string, refreshToken: string): Promise<void> {
await Promise.all([
storage.setItem(ACCESS_TOKEN_KEY, accessToken),
storage.setItem(REFRESH_TOKEN_KEY, refreshToken),
]);
},
async clearTokens(): Promise<void> {
await Promise.all([
storage.removeItem(ACCESS_TOKEN_KEY),
storage.removeItem(REFRESH_TOKEN_KEY),
]);
},
};

View File

@@ -0,0 +1,46 @@
/**
* Authentication types for ShieldAI API
*/
export interface User {
id: string;
email: string;
firstName?: string;
lastName?: string;
createdAt: string;
updatedAt: string;
}
export interface AuthTokens {
accessToken: string;
refreshToken: string;
expiresIn: number;
tokenType: 'Bearer';
}
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterData {
email: string;
password: string;
firstName?: string;
lastName?: string;
}
export interface AuthResponse {
user: User;
tokens: AuthTokens;
}
export interface RefreshTokenRequest {
refreshToken: string;
}
export interface AuthError {
code: string;
message: string;
statusCode: number;
}

View File

@@ -0,0 +1,37 @@
/**
* Shared API types
*/
export interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export interface ErrorResponse {
code: string;
message: string;
details?: Record<string, string[]>;
statusCode: number;
}
export interface HealthStatus {
status: 'healthy' | 'degraded' | 'unhealthy';
timestamp: string;
version: string;
services?: Record<string, { status: 'healthy' | 'degraded' | 'unhealthy' }>;
}
export interface VersionInfo {
version: string;
environment: string;
build: string;
}

View File

@@ -0,0 +1,30 @@
/**
* Device types for push notification and device management
*/
export interface Device {
id: string;
userId: string;
platform: 'ios' | 'android';
pushToken?: string;
modelName?: string;
osVersion?: string;
appVersion?: string;
isActive: boolean;
lastActiveAt: string;
createdAt: string;
updatedAt: string;
}
export interface DeviceRegistration {
platform: 'ios' | 'android';
pushToken: string;
modelName?: string;
osVersion?: string;
appVersion?: string;
}
export interface DeviceListResponse {
devices: Device[];
total: number;
}

View File

@@ -0,0 +1,5 @@
export * from './auth.types';
export * from './device.types';
export * from './subscription.types';
export * from './notification.types';
export * from './common.types';

View File

@@ -0,0 +1,27 @@
/**
* Notification types
*/
export interface Notification {
id: string;
userId: string;
type: 'darkwatch_alert' | 'spam_blocked' | 'voiceprint_analysis' | 'subscription' | 'system';
title: string;
message: string;
data?: Record<string, unknown>;
isRead: boolean;
createdAt: string;
readAt?: string;
}
export interface NotificationListResponse {
notifications: Notification[];
total: number;
unreadCount: number;
}
export interface NotificationPreferences {
emailNotifications: boolean;
pushNotifications: boolean;
notificationTypes: Record<string, boolean>;
}

View File

@@ -0,0 +1,49 @@
/**
* Subscription and billing types
*/
export interface Subscription {
id: string;
userId: string;
tier: 'free' | 'basic' | 'premium' | 'enterprise';
status: 'active' | 'canceled' | 'past_due' | 'trialing';
stripeCustomerId: string;
stripeSubscriptionId?: string;
currentPeriodStart: string;
currentPeriodEnd: string;
cancelAtPeriodEnd: boolean;
createdAt: string;
updatedAt: string;
}
export interface SubscriptionTier {
id: string;
name: string;
description: string;
price: number;
currency: string;
interval: 'month' | 'year';
features: string[];
}
export interface CreateSubscriptionRequest {
tier: 'free' | 'basic' | 'premium' | 'enterprise';
paymentMethodId?: string;
}
export interface UpdateSubscriptionRequest {
tier?: 'free' | 'basic' | 'premium' | 'enterprise';
cancelAtPeriodEnd?: boolean;
}
export interface SubscriptionStatusResponse {
subscription: Subscription;
tier: SubscriptionTier;
usage: {
currentPeriod: {
start: string;
end: string;
};
features: Record<string, { used: number; limit: number | null }>;
};
}

View File

@@ -0,0 +1,141 @@
/**
* Request queue for offline support
* Queues API requests when offline and replays when online
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
const QUEUE_KEY = '@shieldai:api_queue';
const MAX_QUEUE_SIZE = 100;
export interface QueuedRequest {
id: string;
config: AxiosRequestConfig;
timestamp: number;
retryCount: number;
maxRetries: number;
}
export interface QueueStatus {
size: number;
oldestRequest: number | null;
newestRequest: number | null;
}
export class RequestQueue {
private queue: QueuedRequest[] = [];
private isProcessing = false;
private listeners: Set<() => void> = new Set();
constructor() {
this.loadFromStorage();
}
private notifyListeners(): void {
this.listeners.forEach((listener) => listener());
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
async loadFromStorage(): Promise<void> {
try {
const data = await AsyncStorage.getItem(QUEUE_KEY);
if (data) {
this.queue = JSON.parse(data);
}
} catch (error) {
console.error('Failed to load request queue:', error);
this.queue = [];
}
}
private async saveToStorage(): Promise<void> {
try {
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(this.queue));
} catch (error) {
console.error('Failed to save request queue:', error);
}
}
async enqueue(config: AxiosRequestConfig): Promise<string> {
const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const queuedRequest: QueuedRequest = {
id,
config,
timestamp: Date.now(),
retryCount: 0,
maxRetries: 3,
};
// Limit queue size
if (this.queue.length >= MAX_QUEUE_SIZE) {
this.queue.shift(); // Remove oldest
}
this.queue.push(queuedRequest);
await this.saveToStorage();
this.notifyListeners();
return id;
}
async dequeue(): Promise<QueuedRequest | null> {
if (this.queue.length === 0) return null;
const request = this.queue.shift();
if (request) {
await this.saveToStorage();
this.notifyListeners();
}
return request ?? null;
}
async remove(id: string): Promise<void> {
const index = this.queue.findIndex((r) => r.id === id);
if (index !== -1) {
this.queue.splice(index, 1);
await this.saveToStorage();
this.notifyListeners();
}
}
async retry(id: string): Promise<void> {
const index = this.queue.findIndex((r) => r.id === id);
if (index !== -1) {
this.queue[index].retryCount += 1;
await this.saveToStorage();
this.notifyListeners();
}
}
async clear(): Promise<void> {
this.queue = [];
await AsyncStorage.removeItem(QUEUE_KEY);
this.notifyListeners();
}
getStatus(): QueueStatus {
if (this.queue.length === 0) {
return { size: 0, oldestRequest: null, newestRequest: null };
}
return {
size: this.queue.length,
oldestRequest: this.queue[0]?.timestamp ?? null,
newestRequest: this.queue[this.queue.length - 1]?.timestamp ?? null,
};
}
isOnline(): boolean {
// In React Native, you'd use NetInfo here
// For now, return true (assume online)
return true;
}
}
export const requestQueue = new RequestQueue();