
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
@rabstack/rab-react-sdk
Advanced tools
React SDK for building applications with React Query and other utilities
A generic React SDK library for building type-safe React Query hooks with any API SDK. This library provides reusable components and hooks that wrap your API SDK with React Query, giving you automatic loading states, caching, and error handling.
ApiDomainError class with helper methods for common error scenariosnpm install @rabstack/rab-react-sdk @tanstack/react-query
# or
yarn add @rabstack/rab-react-sdk @tanstack/react-query
# or
pnpm add @rabstack/rab-react-sdk @tanstack/react-query
The SDK uses a spec-based approach where you define your API endpoints as specifications and the library automatically generates type-safe SDK methods and React Query hooks.
This approach automatically generates your SDK from endpoint specifications. It's simpler and requires less boilerplate.
// api-specs.ts
import { GetApi, PostApi, PutApi, DeleteApi } from '@rabstack/rab-react-sdk';
// Define your types
export interface Product {
id: string;
name: string;
price: number;
}
export interface CreateProductDto {
name: string;
price: number;
}
// Define endpoint specifications using helper functions
export const donationApiSpecs = {
// Query endpoints (GET)
getProfile: GetApi<{ id: string; name: string }>('/profile'),
listProducts: GetApi<Product[], { page?: number; pageSize?: number }>('/products'),
getProduct: GetApi<Product, never, { productId: string }>('/products/:productId'),
// Mutation endpoints (POST/PUT/DELETE)
createProduct: PostApi<Product, CreateProductDto>('/products'),
updateProduct: PutApi<Product, Partial<CreateProductDto>, { productId: string }>('/products/:productId'),
deleteProduct: DeleteApi<void, { productId: string }>('/products/:productId'),
} as const;
// donation-sdk.tsx
'use client';
import { createSDKProvider } from '@rabstack/rab-react-sdk';
import { donationApiSpecs } from './api-specs';
export const {
SDKProvider: DonationSDKProvider,
useSDK: useDonationSDK,
useQuery: useDonationQuery,
useMutation: useDonationMutation,
} = createSDKProvider({
apiSpecs: donationApiSpecs,
// Optional: Define how to get authorization headers
// getAuthorization: () => {
// const token = localStorage.getItem('token');
// return token ? { Authorization: `Bearer ${token}` } : undefined;
// },
});
// app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { DonationSDKProvider } from './donation-sdk';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<DonationSDKProvider baseUrl="https://api.example.com">
<YourApp />
</DonationSDKProvider>
</QueryClientProvider>
);
}
// components/ProductList.tsx
import { useDonationQuery, useDonationMutation } from './donation-sdk';
function ProductList() {
// Query with parameters
const { data: products, isLoading } = useDonationQuery('listProducts', {
query: { page: 1, pageSize: 20 },
enabled: true,
});
// Mutation with callbacks and automatic query invalidation
const createProduct = useDonationMutation('createProduct', {
onSuccess: (data) => {
console.log('Product created:', data);
},
onError: (error) => {
console.error('Failed to create product:', error);
},
// Automatically invalidate these queries when mutation completes
invalidateQueries: ['listProducts'],
});
const handleCreate = () => {
createProduct.mutate({
body: {
name: 'New Product',
price: 100,
},
});
};
if (isLoading) return <div>Loading...</div>;
return (
<div>
<button onClick={handleCreate}>Create Product</button>
{products?.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}
These helper functions provide a clean API for defining endpoint specifications:
GetApi<RESPONSE, QUERY?, PARAMS?>(url)
Create a GET endpoint specification.
import { GetApi } from '@rabstack/rab-react-sdk';
// Simple GET
const getProfile = GetApi<User>('/profile');
// GET with query parameters
const listUsers = GetApi<User[], { page?: number; limit?: number }>('/users');
// GET with path parameters
const getUser = GetApi<User, never, { userId: string }>('/users/:userId');
PostApi<RESPONSE, BODY, PARAMS?>(url)
Create a POST endpoint specification.
import { PostApi } from '@rabstack/rab-react-sdk';
// POST with body
const createUser = PostApi<User, CreateUserDto>('/users');
// POST with body and path parameters
const addComment = PostApi<Comment, CommentDto, { postId: string }>('/posts/:postId/comments');
PutApi<RESPONSE, BODY, PARAMS?>(url)
Create a PUT endpoint specification.
import { PutApi } from '@rabstack/rab-react-sdk';
const updateUser = PutApi<User, UpdateUserDto, { userId: string }>('/users/:userId');
PatchApi<RESPONSE, BODY, PARAMS?>(url)
Create a PATCH endpoint specification.
import { PatchApi } from '@rabstack/rab-react-sdk';
const partialUpdate = PatchApi<User, Partial<UserDto>, { userId: string }>('/users/:userId');
DeleteApi<RESPONSE?, PARAMS?>(url)
Create a DELETE endpoint specification.
import { DeleteApi } from '@rabstack/rab-react-sdk';
// DELETE with no response (void)
const deleteUser = DeleteApi<void, { userId: string }>('/users/:userId');
// DELETE with response
const softDelete = DeleteApi<User, { userId: string }>('/users/:userId');
Type Parameters Order:
RESPONSE - The response type (required)BODY - Request body type (for POST/PUT/PATCH only, required)QUERY - Query parameters type (for GET only, optional, use never to skip)PARAMS - URL path parameters type (optional)EndpointSpec<RESPONSE, BODY, QUERY, PARAMS>The underlying type for endpoint specifications. Usually you'll use the helper functions above instead of this type directly.
Type Parameters:
RESPONSE - The response type returned by the endpointBODY - The request body type (use unknown if not applicable)QUERY - The query parameters type (use unknown if not applicable)PARAMS - The URL path parameters type (use unknown if not applicable)Properties:
url: string - The endpoint URL path (supports :paramName placeholders)method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' - The HTTP methodcreateSDKProvider<TApiSpecs>(config)Creates a complete SDK setup with provider, hooks, and type-safe query/mutation hooks from API specifications.
Parameters:
config.apiSpecs - Object mapping endpoint names to EndpointSpec definitionsconfig.getAuthorization?: () => Record<string, string> | undefined - Optional function to provide authorization headersconfig.extractApiError?: (status, statusText, url, method, data) => ApiErrorDetails - Optional custom error extractorconfig.parseQuery?: (query: any) => string - Optional custom query string parserconfig.transformResponse?: <T>(data: any, response: Response) => T - Optional response transformer (defaults to unwrapping { data: ... })config.onError?: (error: ApiDomainError) => void - Optional global error handler called for all API errors (useful for logging, analytics, or notifications)Returns:
SDKProvider - Provider component that accepts baseUrl, optional getAuthorization, and optional onError propsuseSDK - Hook to access the raw SDK instanceuseQuery - Type-safe query hook for GET requestsuseMutation - Type-safe mutation hook for POST/PUT/DELETE requestsSDKContext - The React context (rarely needed directly)Example:
import { createSDKProvider, GetApi, PostApi } from '@rabstack/rab-react-sdk';
const apiSpecs = {
getUser: GetApi<User, never, { userId: string }>('/users/:userId'),
createUser: PostApi<User, CreateUserDto>('/users'),
};
const { SDKProvider, useQuery, useMutation } = createSDKProvider({
apiSpecs,
// Optional: Provide authorization headers
getAuthorization: () => {
const token = getYourToken(); // Your token retrieval logic
return token ? { Authorization: token } : undefined;
},
});
// In your app
<SDKProvider baseUrl="https://api.example.com">
<App />
</SDKProvider>
// In components
const { data } = useQuery('getUser', { params: { userId: '123' } });
const createUser = useMutation('createUser');
createSDK<TApiSpecs>(config)Low-level function to create an SDK instance from API specifications. Usually you'll use createSDKProvider instead.
Parameters:
config.apiSpecs - Object mapping endpoint names to EndpointSpec definitionsconfig.baseUrl - The base URL for all API requestsconfig.getAuthorization?: () => Record<string, string> | undefined - Optional function to provide authorization headersconfig.extractApiError? - Optional custom error extractorconfig.parseQuery? - Optional custom query string parserReturns: SDK object with type-safe methods
Example:
import { createSDK, GetApi } from '@rabstack/rab-react-sdk';
const sdk = createSDK({
apiSpecs: {
getUser: GetApi<User, never, { userId: string }>('/users/:userId'),
},
baseUrl: 'https://api.example.com',
});
const user = await sdk.getUser({ params: { userId: '123' } });
ApiDomainErrorCustom error class for API errors with helper methods.
Properties:
status: number - HTTP status codeurl: string - Request URLmethod: string - HTTP methodmessage: string - Error messageerrorCode?: string | number - Optional error code from APIerrors?: string[] - Optional array of validation errorsdata?: any - Raw error data from APIMethods:
isNetworkError(): boolean - Returns true if status is 0 (network/connection error)isClientError(): boolean - Returns true if status is 4xxisServerError(): boolean - Returns true if status is 5xxisUnauthorized(): boolean - Returns true if status is 401isForbidden(): boolean - Returns true if status is 403isNotFound(): boolean - Returns true if status is 404isValidationError(): boolean - Returns true if status is 400 or 422isConflict(): boolean - Returns true if status is 409getAllMessages(): string[] - Returns all error messages including validation errorsgetDisplayMessage(): string - Returns formatted message for displayExample:
import { ApiDomainError } from '@rabstack/rab-react-sdk';
try {
await sdk.createUser({ body: userData });
} catch (error) {
// All errors from the SDK are ApiDomainError instances
if (error.isValidationError()) {
console.error('Validation errors:', error.getAllMessages());
} else if (error.isUnauthorized()) {
// Redirect to login
} else if (error.isNetworkError()) {
console.error('Network error - check your connection');
} else {
console.error(error.getDisplayMessage());
}
}
buildUrlPath(...segments)Utility function to build URL paths with parameter replacement.
Parameters:
...segments - Path segments, optionally ending with a replacements objectReturns: Constructed URL path string
Example:
import { buildUrlPath } from '@rabstack/rab-react-sdk';
buildUrlPath('/users/:userId', { userId: '123' }); // '/users/123'
buildUrlPath('/api', 'users', ':userId', { userId: '456' }); // '/api/users/456'
buildUrlPath('/products'); // '/products'
This library automatically infers types from your SDK methods. No need to manually define request/response types!
// SDK method signature
getProduct: (options: { params: { productId: string } }) => Promise<Product>
// Automatically inferred types
const { data } = useQuery('getProduct', {
params: { productId: '123' }, // ✅ Type-safe params - REQUIRED
});
// data is typed as Product | undefined
The library uses advanced TypeScript to enforce required fields based on your SDK definition:
// SDK with required body
createProduct: (options: { body: CreateProductDto }) => Promise<Product>
// ✅ Correct - body is REQUIRED
const create = useMutation('createProduct');
create.mutate({ body: { name: 'Product' } });
// ❌ Error - body is missing
create.mutate({}); // TypeScript error!
// SDK with optional params
listProducts: (options?: { query?: { page?: number } }) => Promise<Product[]>
// ✅ Correct - everything is optional
const { data } = useQuery('listProducts');
const { data } = useQuery('listProducts', { query: { page: 1 } });
// SDK with required params
getProduct: (options: { params: { productId: string } }) => Promise<Product>
// ✅ Correct - params is REQUIRED
const { data } = useQuery('getProduct', {
params: { productId: '123' }
});
// ❌ Error - params is missing
const { data } = useQuery('getProduct'); // TypeScript error!
How it works:
body or params in a required position, the hook will enforce them?:), the hook makes them optional tooquery and headers are always optionalencodeQuery is always optional (used for special query encoding)The getAuthorization function allows you to define custom authorization headers. Here are common patterns:
Bearer Token:
getAuthorization: () => {
const token = localStorage.getItem('token');
return token ? { Authorization: `Bearer ${token}` } : undefined;
}
API Key:
getAuthorization: () => ({
'X-API-Key': process.env.REACT_APP_API_KEY || '',
})
Basic Auth:
getAuthorization: () => {
const credentials = btoa(`${username}:${password}`);
return { Authorization: `Basic ${credentials}` };
}
Custom Headers:
getAuthorization: () => {
const session = getSession();
return session ? {
'X-Session-ID': session.id,
'X-User-ID': session.userId,
} : undefined;
}
Dynamic Provider-Level Authorization:
export const { SDKProvider, useQuery, useMutation } = createSDKProvider({
apiSpecs,
// Optional default authorization
getAuthorization: () => {
const token = localStorage.getItem('token');
return token ? { Authorization: `Bearer ${token}` } : undefined;
},
});
// You can also override getAuthorization at the provider level
function App() {
return (
<SDKProvider
baseUrl="https://api.example.com"
getAuthorization={() => {
// Override with different auth logic
const apiKey = getApiKey();
return apiKey ? { 'X-API-Key': apiKey } : undefined;
}}
>
<YourApp />
</SDKProvider>
);
}
The SDK uses ApiDomainError for all API errors, providing rich error information and helper methods. All errors thrown by the SDK are guaranteed to be ApiDomainError instances, so you can use the helper methods directly without type checking:
import { useDonationQuery, useDonationMutation } from './donation-sdk';
function UserProfile() {
const { data, error } = useDonationQuery('getProfile');
// All errors from the SDK are ApiDomainError instances
if (error) {
if (error.isUnauthorized()) {
return <div>Please log in to view your profile</div>;
}
if (error.isNotFound()) {
return <div>Profile not found</div>;
}
if (error.isValidationError()) {
return (
<div>
<h3>Validation Errors:</h3>
<ul>
{error.getAllMessages().map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
</div>
);
}
if (error.isNetworkError()) {
return <div>Network error - check your connection</div>;
}
// Generic error display
return <div>Error: {error.getDisplayMessage()}</div>;
}
return <div>{data?.name}</div>;
}
function CreateProduct() {
const createProduct = useDonationMutation('createProduct', {
onError: (error) => {
// All errors from the SDK are ApiDomainError instances
if (error.isConflict()) {
alert('A product with this name already exists');
} else if (error.isValidationError()) {
alert(`Validation failed: ${error.getAllMessages().join(', ')}`);
} else if (error.isServerError()) {
alert('Server error. Please try again later.');
} else if (error.isNetworkError()) {
alert('Network error. Please check your connection.');
} else {
alert(error.getDisplayMessage());
}
},
});
// ...
}
Custom Error Extraction
You can customize how errors are extracted from API responses:
import { createSDKProvider, type ApiErrorDetails } from '@rabstack/rab-react-sdk';
function extractApiError(
status: number,
statusText: string,
url: string,
method: string,
data?: any
): ApiErrorDetails {
// Custom error extraction logic for your API format
return {
status,
url,
method,
message: data?.error?.message || data?.message || statusText,
errorCode: data?.error?.code || data?.code,
errors: data?.error?.details || data?.errors || [],
data,
};
}
const { SDKProvider } = createSDKProvider({
apiSpecs,
extractApiError, // Use custom error extractor
});
Global Error Handler
The onError callback provides a centralized place to handle all API errors globally. This is useful for logging, analytics, displaying notifications, or redirecting to login on authentication errors. The callback is called for every API error before the error is thrown to React Query.
import { createSDKProvider, type ApiDomainError } from '@rabstack/rab-react-sdk';
import { toast } from 'your-toast-library';
const { SDKProvider, useQuery, useMutation } = createSDKProvider({
apiSpecs,
// Global error handler - called for ALL API errors
onError: (error: ApiDomainError) => {
// Log to error tracking service
console.error('API Error:', {
status: error.status,
url: error.url,
method: error.method,
message: error.message,
errorCode: error.errorCode,
});
// Show toast notification for certain errors
if (error.isServerError()) {
toast.error('Server error. Please try again later.');
} else if (error.isNetworkError()) {
toast.error('Network error. Check your connection.');
}
// Redirect to login on unauthorized errors
if (error.isUnauthorized()) {
window.location.href = '/login';
}
// Send to analytics
analytics.track('api_error', {
status: error.status,
endpoint: error.url,
errorCode: error.errorCode,
});
},
});
You can also provide or override the error handler at the provider level:
function App() {
return (
<SDKProvider
baseUrl="https://api.example.com"
onError={(error) => {
// Provider-level error handler - overrides config-level handler
if (error.isValidationError()) {
toast.error(error.getAllMessages().join(', '));
}
}}
>
<YourApp />
</SDKProvider>
);
}
Note: The onError callback is called in addition to React Query's onError handlers on individual queries/mutations. Use the global handler for cross-cutting concerns (logging, analytics) and local handlers for component-specific error handling (displaying error messages, resetting forms, etc.).
The mutation hook supports automatic query invalidation via the invalidateQueries option. When a mutation settles (succeeds or fails), it will automatically invalidate the specified query keys, triggering a refetch.
const createProduct = useDonationMutation('createProduct', {
// These queries will be invalidated when the mutation completes
invalidateQueries: ['listProducts', 'getCategories', 'getDashboard'],
onSuccess: (data) => {
console.log('Product created and queries invalidated!');
},
});
// When you call this mutation:
createProduct.mutate({ body: { name: 'New Product' } });
// After completion, 'listProducts', 'getCategories', and 'getDashboard' queries
// will be automatically invalidated and refetched
Benefits:
invalidateQueriesqueryClient.invalidateQueries()onSettled callback is still calledExample: Complete CRUD workflow
function ProductManager() {
// Query
const { data: products } = useDonationQuery('listProducts');
// Create - invalidates list
const createProduct = useDonationMutation('createProduct', {
invalidateQueries: ['listProducts'],
});
// Update - invalidates list and detail
const updateProduct = useDonationMutation('updateProduct', {
invalidateQueries: ['listProducts', 'getProduct'],
});
// Delete - invalidates list
const deleteProduct = useDonationMutation('deleteProduct', {
invalidateQueries: ['listProducts'],
});
return (
<div>
<button onClick={() => createProduct.mutate({ body: { name: 'New' } })}>
Create
</button>
{products?.map((product) => (
<div key={product.id}>
<span>{product.name}</span>
<button onClick={() => updateProduct.mutate({
params: { productId: product.id },
body: { name: 'Updated' }
})}>
Update
</button>
<button onClick={() => deleteProduct.mutate({
params: { productId: product.id }
})}>
Delete
</button>
</div>
))}
</div>
);
}
You can pass any React Query options to customize behavior:
const { data, error } = useDonationQuery('getProfile', {
retry: (failureCount, error) => {
// Don't retry on client errors (4xx) - all errors are ApiDomainError
if (error.isClientError()) {
return false;
}
// Retry server errors up to 3 times
return failureCount < 3;
},
staleTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false,
onError: (err) => {
// All errors from the SDK are ApiDomainError instances
if (err.isUnauthorized()) {
// Redirect to login
window.location.href = '/login';
}
},
});
By default, query parameters are encoded as standard URL query strings. You can customize this behavior:
// Custom query encoder (e.g., for nested objects, arrays, etc.)
function customParseQuery(query: any): string {
// Example: Custom encoding for complex filters
const params = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (Array.isArray(value)) {
// Encode arrays as comma-separated values
params.append(key, value.join(','));
} else if (typeof value === 'object' && value !== null) {
// Encode objects as JSON
params.append(key, JSON.stringify(value));
} else if (value !== undefined && value !== null) {
params.append(key, String(value));
}
});
return params.toString();
}
const { SDKProvider, useQuery } = createSDKProvider({
apiSpecs,
parseQuery: customParseQuery, // Use custom query encoder
});
// Or disable custom encoding for a specific request
const { data } = useQuery('listProducts', {
query: { tags: ['sale', 'featured'], category: 'electronics' },
encodeQuery: false, // Skip custom encoder, use default encoding
});
Default Behavior:
The SDK automatically unwraps responses in the { data: ... } format:
// If your API returns: { data: { id: 1, name: "Product" } }
// The SDK automatically extracts and returns: { id: 1, name: "Product" }
const { data } = useDonationQuery('getProduct', { params: { productId: '1' } });
// data is typed as Product, not { data: Product }
// If your API returns data directly without wrapping, that's fine too:
// API returns: { id: 1, name: "Product" }
// SDK returns: { id: 1, name: "Product" }
This means you can define your endpoint response types as the actual data shape, not the wrapper:
// ✅ Correct - define the actual data type
const getProduct = GetApi<Product>('/products/:productId');
// ❌ Not needed - don't include the wrapper
const getProduct = GetApi<{ data: Product }>('/products/:productId');
Custom Response Transformation:
If your API uses a different response format, you can customize the transformation:
import { createSDKProvider, defaultTransformResponse } from '@rabstack/rab-react-sdk';
const { SDKProvider, useQuery, useMutation } = createSDKProvider({
apiSpecs,
// Custom response transformer
transformResponse: (data, response) => {
// Example 1: Extract from nested structure
// API returns: { result: { success: true, payload: {...} } }
if (data.result?.payload) {
return data.result.payload;
}
// Example 2: Transform dates
if (data.createdAt) {
return {
...data,
createdAt: new Date(data.createdAt),
};
}
// Fallback to default behavior
return defaultTransformResponse(data);
},
});
Common Transformation Examples:
// Stripe-style: { object: 'charge', data: {...} }
transformResponse: (data) => data.data || data
// Nested payload: { success: true, payload: {...} }
transformResponse: (data) => data.payload
// Array wrapper: { results: [...] }
transformResponse: (data) => data.results || data
// No transformation
transformResponse: (data) => data
// Access response headers
transformResponse: (data, response) => ({
...data,
rateLimit: response.headers.get('X-RateLimit-Remaining'),
})
The transformResponse function receives:
data - The parsed JSON responseresponse - The original Response object (for accessing headers, status, etc.)const updateProduct = useDonationMutation('updateProduct', {
onMutate: async (variables) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['listProducts'] });
// Snapshot previous value
const previous = queryClient.getQueryData(['listProducts']);
// Optimistically update
queryClient.setQueryData(['listProducts'], (old) => {
// Update logic
});
return { previous };
},
onError: (err, variables, context) => {
// Rollback on error
queryClient.setQueryData(['listProducts'], context?.previous);
},
});
Spec-Based SDK Creation
createSDKProvider function that generates SDK, provider, and hooks from endpoint specificationsGetApi(), PostApi(), PutApi(), PatchApi(), DeleteApi() for defining endpointsEnhanced Error Handling
ApiDomainError instances (no need for instanceof checks)ApiDomainError class with helper methods:
isNetworkError() - Detects network/connection errorsisUnauthorized(), isForbidden(), isNotFound()isValidationError(), isConflict()isClientError(), isServerError()getAllMessages(), getDisplayMessage()extractApiError optionUtility Functions
buildUrlPath() - Build URLs with parameter replacementcreateSDK() - Low-level SDK creation from specscreateRequestFunction() - Customizable request handlerImproved Developer Experience
{ data: ... } formattransformResponse optionparseQuery optionWe're considering adding support for a factory-based approach that allows you to wrap existing SDKs or create custom SDK implementations. This would be useful for:
Potential API (Subject to Change):
// Create SDK with custom factory
const { SDKProvider, useSDK } = createSDKProviderWithFactory<
MySDK,
{ baseUrl: string; apiKey: string }
>((props) => createMyCustomSDK(props.baseUrl, props.apiKey));
// Create typed hooks from existing SDK
export const useMyQuery = createSDKQueryHook<MySDK>(useMySDK);
export const useMyMutation = createSDKMutationHook<MySDK>(useMySDK);
Status: Under consideration. If you need this feature, please upvote or comment on the GitHub issue.
MIT
Contributions are welcome! Please feel free to submit a Pull Request to the rabstack repository.
For issues and feature requests, please visit the issue tracker.
FAQs
React SDK for building applications with React Query and other utilities
We found that @rabstack/rab-react-sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.