🚀. Socket Launch Week Day 2:Introducing Manifest Alerts.Learn more
Sign In

@tenxyte/core

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tenxyte/core - npm Package Compare versions

Comparing version
0.9.0
to
0.9.2
+15
-3
package.json
{
"name": "@tenxyte/core",
"version": "0.9.0",
"version": "0.9.2",
"description": "Core JavaScript SDK for Tenxyte",

@@ -18,2 +18,8 @@ "main": "./dist/index.cjs",

},
"files": [
"dist",
"README.md",
"LICENSE"
],
"sideEffects": false,
"publishConfig": {

@@ -30,2 +36,4 @@ "access": "public",

"format": "prettier --write \"src/**/*.ts\"",
"typecheck": "tsc --noEmit",
"check": "npm run lint && npm run typecheck && npm run test",
"generate:schema": "openapi-typescript http://localhost:8000/api/v1/docs/schema/ --output src/types/api-schema.d.ts"

@@ -65,3 +73,6 @@ },

"devDependencies": {
"@eslint/js": "^10.0.1",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"happy-dom": "^20.8.3",

@@ -72,4 +83,5 @@ "openapi-typescript": "^7.13.0",

"typescript": "^5.9.3",
"vitest": "^1.6.1"
"typescript-eslint": "^8.57.2",
"vitest": "^4.1.1"
}
}
}
+361
-101
# @tenxyte/core
The official core JavaScript/TypeScript SDK for the Tenxyte API.
This SDK is the foundation for interacting securely with Tenxyte's robust authentication, multi-tenant organization management, and Advanced AI Security (AIRS) capabilities.
The official core JavaScript/TypeScript SDK for the **Tenxyte** API — a unified platform for authentication, multi-tenant organizations, RBAC, GDPR compliance, and AI agent security.
## Features
- **Authentication** — Email/password, phone, magic link, social OAuth2, registration
- **Security** — 2FA/TOTP, OTP verification, WebAuthn/Passkeys (FIDO2), password management
- **RBAC** — Role & permission management, synchronous JWT checks, user role assignment
- **User Management** — Profile CRUD, avatar upload, admin user operations
- **B2B Multi-Tenancy** — Organization CRUD, member management, invitations, context switching
- **AI Agent Security (AIRS)** — Agent tokens, circuit breakers, Human-in-the-Loop, usage reporting
- **Applications** — API client management, credential regeneration
- **Admin** — Audit logs, login attempts, blacklisted/refresh token management
- **GDPR** — Account deletion flows, data export, admin deletion request processing
- **Dashboard** — Global, auth, security, GDPR, and per-org statistics
## Installation
You can install the package using `npm`, `yarn`, or `pnpm`:
```bash

@@ -18,8 +28,4 @@ npm install @tenxyte/core

## Initialization
## Quick Start
The single entry point for all operations is the `TenxyteClient`. You must initialize it with your API's base URL and (if you're using App-Centric auth) the `appKey` in headers.
> **Important**: Never expose an `appSecret` in frontend environments like React or Vue client bundles. Use it exclusively in server-side processes.
```typescript

@@ -29,46 +35,60 @@ import { TenxyteClient } from '@tenxyte/core';

const tx = new TenxyteClient({
baseUrl: 'https://api.my-backend.com',
headers: {
'X-Access-Key': 'your-public-app-key' // Optional, based on your backend configs.
}
baseUrl: 'https://api.my-backend.com',
headers: { 'X-Access-Key': 'your-public-app-key' },
});
// Login
const tokens = await tx.auth.loginWithEmail({
email: 'user@example.com',
password: 'secure_password!',
device_info: '',
});
// Check authentication state
const isLoggedIn = await tx.isAuthenticated();
const user = await tx.getCurrentUser();
```
The SDK is composed of separate functional modules: `auth`, `security`, `rbac`, `user`, `b2b`, and `ai`.
> **Important**: Never expose `X-Access-Secret` in frontend bundles. Use it exclusively server-side.
---
## Authentication Flows
## Configuration
### Standard Email / Password
```typescript
try {
const { user, tokens } = await tx.auth.loginWithEmail('user@example.com', 'secure_password!');
console.log(`Welcome back, ${user.first_name}!`);
} catch (error) {
if (error.code === '2FA_REQUIRED') {
// Collect TOTP code from user...
await tx.auth.loginWithEmail('user@example.com', 'secure_password!', { totpCode: '123456' });
}
}
```
The `TenxyteClient` accepts a single configuration object. Only `baseUrl` is required.
### Social Login (OAuth2)
```typescript
// Direct token exchange
const response = await tx.auth.loginWithSocial('google', {
id_token: 'google_id_token_jwt'
});
const tx = new TenxyteClient({
// Required
baseUrl: 'https://api.my-service.com',
// Or using OAuth code exchange
const callback = await tx.auth.handleSocialCallback('github', 'auth_code', 'https://myapp.com/callback');
```
// Optional — extra headers for every request
headers: { 'X-Access-Key': 'pkg_abc123' },
### Passwordless (Magic Link)
```typescript
// 1. Request the link
await tx.auth.requestMagicLink('user@example.com', 'https://myapp.com/verify-magic');
// Optional — token storage backend (default: MemoryStorage)
// Use LocalStorageAdapter for browser persistence
storage: new LocalStorageAdapter(),
// 2. On the callback page, verify the token returned in the URL
const { user, tokens } = await tx.auth.verifyMagicLink(urlParams.get('token'));
// Optional — auto-refresh 401s silently (default: true)
autoRefresh: true,
// Optional — auto-inject device fingerprint into auth requests (default: true)
autoDeviceInfo: true,
// Optional — global request timeout in ms (default: undefined)
timeoutMs: 10_000,
// Optional — retry config for 429/5xx with exponential backoff
retryConfig: { maxRetries: 3, baseDelayMs: 500 },
// Optional — callback when session cannot be recovered
onSessionExpired: () => router.push('/login'),
// Optional — pluggable logger (default: silent no-op)
logger: console,
logLevel: 'debug', // 'silent' | 'error' | 'warn' | 'debug'
// Optional — override auto-detected device info
deviceInfoOverride: { app_name: 'MyApp', app_version: '2.0.0' },
});
```

@@ -78,110 +98,350 @@

## Authorization & RBAC
## Modules
The SDK automatically intercepts your requests to attach `Authorization: Bearer <token>` when available.
By utilizing the embedded `EventEmitter`, you can listen to rotation and expiration changes.
### Authentication (`tx.auth`)
```typescript
tx.http.addResponseInterceptor(async (response) => {
// You can intercept logic, or use tx.on(...) to be built later over the SDK Event layer.
return response;
// Email/password login
const tokens = await tx.auth.loginWithEmail({
email: 'user@example.com',
password: 'password123',
device_info: '',
totp_code: '123456', // optional, for 2FA
});
```
### Verifying Roles and Permissions
```typescript
// Fetch user roles & direct permissions across their active scope
const myRoles = await tx.user.getMyRoles();
// Phone login
const tokens = await tx.auth.loginWithPhone({
phone_country_code: '+1',
phone_number: '5551234567',
password: 'password123',
device_info: '',
});
// List backend global roles
const roles = await tx.rbac.listRoles();
```
// Registration
const result = await tx.auth.register({
email: 'new@example.com',
password: 'StrongP@ss1',
first_name: 'Jane',
last_name: 'Doe',
});
---
// Magic Link (passwordless)
await tx.auth.requestMagicLink({ email: 'user@example.com', validation_url: 'https://myapp.com/verify' });
const tokens = await tx.auth.verifyMagicLink(urlToken);
## Advanced Security
// Social OAuth2
const tokens = await tx.auth.loginWithSocial('google', { id_token: 'jwt...' });
const tokens = await tx.auth.handleSocialCallback('github', 'auth_code', 'https://myapp.com/cb');
### WebAuthn / Passkeys
The `security` module natively wraps browser credentials APIs to seamlessly interact with Tenxyte's FIDO2 bindings.
// Session management
await tx.auth.logout('refresh_token_value');
await tx.auth.logoutAll();
await tx.auth.refreshToken('refresh_token_value');
```
### Security (`tx.security`)
```typescript
// Register a new device/Passkey for the authenticated user
await tx.security.registerWebAuthn('My MacBook Chrome');
// 2FA (TOTP)
const status = await tx.security.get2FAStatus();
const { secret, qr_code_url, backup_codes } = await tx.security.setup2FA();
await tx.security.confirm2FA('123456');
await tx.security.disable2FA('123456');
// Authenticate securely (Without needing a password)
// OTP
await tx.security.requestOtp({ delivery_method: 'email', purpose: 'login' });
const result = await tx.security.verifyOtp({ otp: '123456', purpose: 'login' });
// Password management
await tx.security.resetPasswordRequest({ email: 'user@example.com' });
await tx.security.resetPasswordConfirm({ token: '...', new_password: 'NewP@ss1' });
await tx.security.changePassword({ old_password: 'old', new_password: 'new' });
// WebAuthn / Passkeys
await tx.security.registerWebAuthn('My Laptop');
const session = await tx.security.authenticateWebAuthn('user@example.com');
const creds = await tx.security.listWebAuthnCredentials();
await tx.security.deleteWebAuthnCredential(credentialId);
```
### 2FA (TOTP) Enrollment
### RBAC (`tx.rbac`)
```typescript
const { secret, qr_code_url, backup_codes } = await tx.security.setup2FA();
// Show the QR code to the user, then confirm their first valid code
await tx.security.confirm2FA(userProvidedCode);
// Synchronous JWT checks (no network call)
tx.rbac.setToken(accessToken);
const isAdmin = tx.rbac.hasRole('admin');
const canEdit = tx.rbac.hasPermission('users.edit');
const hasAny = tx.rbac.hasAnyRole(['admin', 'manager']);
const hasAll = tx.rbac.hasAllRoles(['admin', 'superadmin']);
// CRUD operations (network calls)
const roles = await tx.rbac.listRoles();
await tx.rbac.createRole({ code: 'editor', name: 'Editor' });
await tx.rbac.assignRoleToUser('user-id', 'editor');
await tx.rbac.removeRoleFromUser('user-id', 'editor');
const permissions = await tx.rbac.listPermissions();
await tx.rbac.assignPermissionsToUser('user-id', ['posts.create', 'posts.edit']);
await tx.rbac.removePermissionsFromUser('user-id', ['posts.create']);
// Fetch user's roles/permissions from backend
const userRoles = await tx.rbac.getUserRoles('user-id');
const userPerms = await tx.rbac.getUserPermissions('user-id');
```
---
### User Management (`tx.user`)
## B2B Organizations (Multi-Tenancy)
```typescript
const profile = await tx.user.getProfile();
await tx.user.updateProfile({ first_name: 'Updated' });
await tx.user.uploadAvatar(fileFormData);
await tx.user.deleteAccount('my-password');
const myRoles = await tx.user.getMyRoles();
Tenxyte natively supports complex multi-tenant B2B topologies. Using `switchOrganization` instructs the SDK to pass the context `X-Org-Slug` downstream transparently.
// Admin operations
const users = await tx.user.listUsers({ page: 1, page_size: 20 });
const user = await tx.user.getUser('user-id');
await tx.user.adminUpdateUser('user-id', { is_active: false });
await tx.user.adminDeleteUser('user-id');
await tx.user.banUser('user-id', 'spam');
```
### B2B Organizations (`tx.b2b`)
```typescript
// Activate context
// Context switching — auto-injects X-Org-Slug header
tx.b2b.switchOrganization('acme-corp');
const slug = tx.b2b.getCurrentOrganizationSlug(); // 'acme-corp'
tx.b2b.clearOrganization();
// All subsequent calls inject `X-Org-Slug: acme-corp`.
// Organization CRUD
const orgs = await tx.b2b.listOrganizations();
const org = await tx.b2b.createOrganization({ name: 'Acme Corp', slug: 'acme-corp' });
await tx.b2b.updateOrganization('acme-corp', { name: 'Acme Corp Inc.' });
await tx.b2b.deleteOrganization('acme-corp');
// Members
const members = await tx.b2b.listMembers('acme-corp');
await tx.b2b.addMember('acme-corp', { user_id: 'uid', role_code: 'member' });
await tx.b2b.updateMember('acme-corp', 'uid', { role_code: 'admin' });
await tx.b2b.removeMember('acme-corp', 'uid');
// Invite a collaborator into this organization
// Invitations
await tx.b2b.inviteMember('acme-corp', { email: 'dev@example.com', role_code: 'admin' });
// Clear context
tx.b2b.clearOrganization();
const roles = await tx.b2b.listOrgRoles('acme-corp');
```
---
### AI Agent Security (`tx.ai`)
## AIRS (AI Responsibility & Security)
If your architecture includes orchestrating authenticated LLM agents that take action via Tenxyte endpoints, you must use **AgentTokens**.
```typescript
// 1. Authenticated User delegates secure permissions to an Agent
const agentTokenData = await tx.ai.createAgentToken({
// Agent token lifecycle
const agentData = await tx.ai.createAgentToken({
agent_id: 'Invoice-Parser-Bot',
permissions: ['invoices.read', 'invoices.create'],
budget_limit_usd: 5.00, // strict budget enforcing
circuit_breaker: { max_requests: 100, window_seconds: 60 }
budget_limit_usd: 5.00,
circuit_breaker: { max_requests: 100, window_seconds: 60 },
});
// 2. Instruct the SDK to flip into Agent Mode
tx.ai.setAgentToken(agentTokenData.token);
tx.ai.setAgentToken(agentData.token); // SDK switches to AgentBearer auth
tx.ai.isAgentMode(); // true
tx.ai.clearAgentToken(); // back to standard Bearer
// The SDK will now authorize using `AgentBearer <token>`.
// 3. Keep the agent alive
await tx.ai.sendHeartbeat(agentTokenData.id);
// Token management
const tokens = await tx.ai.listAgentTokens();
const token = await tx.ai.getAgentToken('token-id');
await tx.ai.revokeAgentToken('token-id');
await tx.ai.suspendAgentToken('token-id');
await tx.ai.revokeAllAgentTokens();
// 4. Report LLM consumption cost transparently back to backend
await tx.ai.reportUsage(agentTokenData.id, {
// Human-in-the-Loop
const pending = await tx.ai.listPendingActions();
await tx.ai.confirmPendingAction('confirmation-token');
await tx.ai.denyPendingAction('confirmation-token');
// Monitoring
await tx.ai.sendHeartbeat('token-id');
await tx.ai.reportUsage('token-id', {
cost_usd: 0.015,
prompt_tokens: 1540,
completion_tokens: 420
completion_tokens: 420,
});
// Disable agent mode and return to standard User flow
tx.ai.clearAgentToken();
// Traceability
tx.ai.setTraceId('trace-1234'); // adds X-Prompt-Trace-ID header
tx.ai.clearTraceId();
```
### Human In The Loop (HITL) & Auditing
### Applications (`tx.applications`)
```typescript
// Linking operations to prompt identifiers for debugging
tx.ai.setTraceId('trace-1234abcd-prompt');
// Request will now include X-Prompt-Trace-ID
const apps = await tx.applications.listApplications();
const app = await tx.applications.createApplication({
name: 'My API Client',
description: 'Backend service',
});
const detail = await tx.applications.getApplication('app-id');
await tx.applications.updateApplication('app-id', { name: 'Renamed' });
await tx.applications.patchApplication('app-id', { description: 'Updated desc' });
await tx.applications.deleteApplication('app-id');
const newCreds = await tx.applications.regenerateCredentials('app-id');
```
// Any requests generating a `HTTP 202 Accepted` indicate HITL.
const pendingActions = await tx.ai.listPendingActions();
await tx.ai.confirmPendingAction(pendingActions[0].confirmation_token);
### Admin (`tx.admin`)
```typescript
// Audit logs
const logs = await tx.admin.listAuditLogs({ page: 1 });
const log = await tx.admin.getAuditLog('log-id');
// Login attempts
const attempts = await tx.admin.listLoginAttempts({ user_id: 'uid' });
// Blacklisted tokens
const blacklisted = await tx.admin.listBlacklistedTokens();
await tx.admin.cleanupBlacklistedTokens();
// Refresh tokens
const refreshTokens = await tx.admin.listRefreshTokens({ user_id: 'uid' });
await tx.admin.revokeRefreshToken('token-id');
```
### GDPR (`tx.gdpr`)
```typescript
// User-facing
await tx.gdpr.requestAccountDeletion({ reason: 'No longer needed' });
await tx.gdpr.confirmAccountDeletion('confirmation-code');
await tx.gdpr.cancelAccountDeletion();
const status = await tx.gdpr.getDeletionStatus();
const data = await tx.gdpr.exportUserData();
// Admin-facing
const requests = await tx.gdpr.listDeletionRequests({ status: 'pending' });
const request = await tx.gdpr.getDeletionRequest('request-id');
await tx.gdpr.processDeletionRequest('request-id', { action: 'approve' });
await tx.gdpr.processExpiredDeletions();
```
### Dashboard (`tx.dashboard`)
```typescript
const global = await tx.dashboard.getStats({ period: 'last_30_days' });
const auth = await tx.dashboard.getAuthStats();
const security = await tx.dashboard.getSecurityStats();
const gdpr = await tx.dashboard.getGdprStats();
const orgStats = await tx.dashboard.getOrganizationStats('acme-corp');
```
---
## SDK Events
The SDK emits events via a built-in `EventEmitter`. Use `tx.on()`, `tx.once()`, and `tx.off()` to subscribe.
| Event | Payload | When |
|---|---|---|
| `session:expired` | `void` | Refresh token expired/revoked, session unrecoverable |
| `token:refreshed` | `{ accessToken: string }` | Access token silently rotated via auto-refresh |
| `token:stored` | `{ accessToken: string; refreshToken?: string }` | Tokens persisted after login, register, or refresh |
| `agent:awaiting_approval` | `{ action: unknown }` | AI agent action requires human confirmation (HTTP 202) |
| `error` | `{ error: unknown }` | Unrecoverable SDK error not tied to a specific call |
```typescript
// React to session expiry
tx.on('session:expired', () => {
router.push('/login');
});
// Track token refreshes
tx.on('token:refreshed', ({ accessToken }) => {
console.log('Token refreshed silently');
});
// HITL notification
tx.on('agent:awaiting_approval', ({ action }) => {
showApprovalDialog(action);
});
```
---
## High-Level Helpers
```typescript
// Check if user is authenticated (synchronous JWT expiry check)
const isLoggedIn = await tx.isAuthenticated();
// Get the raw access token
const token = await tx.getAccessToken();
// Get decoded JWT payload (no network call)
const user = await tx.getCurrentUser();
// Check token expiry
const expired = await tx.isTokenExpired();
// Get full SDK state snapshot (for framework wrappers)
const state = await tx.getState();
// { isAuthenticated, user, accessToken, activeOrg, isAgentMode }
```
---
## Migration Guide: v0.9 → v1.0
### Breaking Changes
1. **Constructor signature changed** — The client now accepts a `TenxyteClientConfig` object:
```typescript
// Before (v0.9)
const tx = new TenxyteClient({ baseUrl: '...', headers: { ... } });
// After (v1.0) — same, but new options available
const tx = new TenxyteClient({
baseUrl: '...',
headers: { ... },
autoRefresh: true, // NEW
autoDeviceInfo: true, // NEW
retryConfig: { ... }, // NEW
});
```
2. **`loginWithEmail` now requires `device_info`**:
```typescript
// Before (v0.9)
await tx.auth.loginWithEmail({ email, password });
// After (v1.0)
await tx.auth.loginWithEmail({ email, password, device_info: '' });
```
3. **`requestMagicLink` now requires `validation_url`**:
```typescript
// Before
await tx.auth.requestMagicLink({ email });
// After
await tx.auth.requestMagicLink({ email, validation_url: 'https://...' });
```
4. **Auto-session management** — Tokens are now automatically stored and the `Authorization` header is automatically injected. You no longer need to manage this manually.
5. **New modules added** — `tx.applications`, `tx.admin`, `tx.gdpr`, `tx.dashboard` are now available.
6. **`register()` return type changed** — Now returns `RegisterResponse` (may include tokens if auto-login is enabled).
### New Features in v1.0
- Auto-refresh interceptor (silent 401 → refresh → retry)
- Configurable retry with exponential backoff (429/5xx)
- Device info auto-injection
- Pluggable logger with log levels
- High-level helpers (`isAuthenticated`, `getCurrentUser`, `isTokenExpired`)
- `getState()` for framework wrapper integration
- EventEmitter for reactive state (`session:expired`, `token:refreshed`, etc.)
---
## License
MIT

Sorry, the diff of this file is too big to display

import { TenxyteHttpClient, HttpClientOptions } from './http/client';
import { AuthModule } from './modules/auth';
import { SecurityModule } from './modules/security';
import { RbacModule } from './modules/rbac';
import { UserModule } from './modules/user';
import { B2bModule } from './modules/b2b';
import { AiModule } from './modules/ai';
/**
* The primary entry point for the Tenxyte SDK.
* Groups together logic for authentication, security, organization switching, and AI control.
*/
export class TenxyteClient {
/** The core HTTP wrapper handling network interception and parsing */
public http: TenxyteHttpClient;
/** Authentication module (Login, Signup, Magic link, session handling) */
public auth: AuthModule;
/** Security module (2FA, WebAuthn, Passwords, OTPs) */
public security: SecurityModule;
/** Role-Based Access Control and permission checking module */
public rbac: RbacModule;
/** Connected user's profile and management module */
public user: UserModule;
/** Business-to-Business organizations module (multi-tenant environments) */
public b2b: B2bModule;
/** AIRS - AI Responsibility & Security module (Agent tokens, Circuit breakers, HITL) */
public ai: AiModule;
/**
* Initializes the SDK with connection details for your Tenxyte-powered API.
* @param options Configuration options including `baseUrl` and custom headers like `X-Access-Key`
*
* @example
* ```typescript
* const tx = new TenxyteClient({
* baseUrl: 'https://api.my-service.com',
* headers: { 'X-Access-Key': 'pkg_abc123' }
* });
* ```
*/
constructor(options: HttpClientOptions) {
this.http = new TenxyteHttpClient(options);
this.auth = new AuthModule(this.http);
this.security = new SecurityModule(this.http);
this.rbac = new RbacModule(this.http);
this.user = new UserModule(this.http);
this.b2b = new B2bModule(this.http);
this.ai = new AiModule(this.http);
}
}
import type { TenxyteError } from '../types';
export interface HttpClientOptions {
baseUrl: string;
timeoutMs?: number;
headers?: Record<string, string>;
}
export type RequestConfig = Omit<RequestInit, 'body' | 'headers'> & {
body?: unknown;
headers?: Record<string, string>;
params?: Record<string, string | number | boolean>;
};
/**
* Core HTTP Client underlying the SDK.
* Handles JSON parsing, standard headers, simple request processing,
* and normalizing errors into TenxyteError format.
*/
export class TenxyteHttpClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
// Interceptors
private requestInterceptors: Array<(config: RequestConfig & { url: string }) => Promise<RequestConfig & { url: string }> | (RequestConfig & { url: string })> = [];
private responseInterceptors: Array<(response: Response, request: { url: string; config: RequestConfig }) => Promise<Response> | Response> = [];
constructor(options: HttpClientOptions) {
this.baseUrl = options.baseUrl.replace(/\/$/, ''); // Remove trailing slash
this.defaultHeaders = {
'Content-Type': 'application/json',
Accept: 'application/json',
...options.headers,
};
}
// Interceptor Registration
addRequestInterceptor(interceptor: typeof this.requestInterceptors[0]) {
this.requestInterceptors.push(interceptor);
}
addResponseInterceptor(interceptor: typeof this.responseInterceptors[0]) {
this.responseInterceptors.push(interceptor);
}
/**
* Main request method wrapping fetch
*/
async request<T>(endpoint: string, config: RequestConfig = {}): Promise<T> {
const urlStr = endpoint.startsWith('http')
? endpoint
: `${this.baseUrl}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
let urlObj = new URL(urlStr);
if (config.params) {
Object.entries(config.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
urlObj.searchParams.append(key, String(value));
}
});
}
let requestContext: any = {
url: urlObj.toString(),
...config,
headers: { ...this.defaultHeaders, ...(config.headers || {}) } as Record<string, string>,
};
// Handle FormData implicitly for multipart requests
if (typeof FormData !== 'undefined' && requestContext.body instanceof FormData) {
const headers = requestContext.headers as Record<string, string>;
// Explicitly remove Content-Type so fetch can auto-assign the multipart boundary
delete headers['Content-Type'];
delete headers['content-type'];
} else if (requestContext.body && typeof requestContext.body === 'object') {
const contentType = (requestContext.headers as Record<string, string>)['Content-Type'] || '';
if (contentType.toLowerCase().includes('application/json')) {
requestContext.body = JSON.stringify(requestContext.body);
}
}
// Run Request Interceptors
for (const interceptor of this.requestInterceptors) {
requestContext = await interceptor(requestContext);
}
const { url, ...fetchConfig } = requestContext as any;
try {
let response = await fetch(url, fetchConfig as RequestInit);
// Run Response Interceptors (e.g., token refresh logic)
for (const interceptor of this.responseInterceptors) {
response = await interceptor(response, { url, config: fetchConfig as RequestConfig });
}
if (!response.ok) {
throw await this.normalizeError(response);
}
// Handle NoContent
if (response.status === 204) {
return {} as T;
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return (await response.json()) as T;
}
return (await response.text()) as unknown as T;
} catch (error: any) {
if (error && error.code) {
throw error; // Already normalized
}
throw {
error: error.message || 'Network request failed',
code: 'NETWORK_ERROR' as unknown as import('../types').TenxyteErrorCode,
details: String(error)
} as TenxyteError;
}
}
private async normalizeError(response: Response): Promise<TenxyteError> {
try {
const body = await response.json();
return {
error: body.error || body.detail || 'API request failed',
code: body.code || `HTTP_${response.status}`,
details: body.details || body,
retry_after: response.headers.has('Retry-After') ? parseInt(response.headers.get('Retry-After')!, 10) : undefined,
} as TenxyteError;
} catch (e) {
return {
error: `HTTP Error ${response.status}: ${response.statusText}`,
code: `HTTP_${response.status}` as unknown as import('../types').TenxyteErrorCode,
} as TenxyteError;
}
}
// Convenience methods
get<T>(endpoint: string, config?: Omit<RequestConfig, 'method' | 'body'>) {
return this.request<T>(endpoint, { ...config, method: 'GET' });
}
post<T>(endpoint: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'body'>) {
return this.request<T>(endpoint, { ...config, method: 'POST', body: data });
}
put<T>(endpoint: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'body'>) {
return this.request<T>(endpoint, { ...config, method: 'PUT', body: data });
}
patch<T>(endpoint: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'body'>) {
return this.request<T>(endpoint, { ...config, method: 'PATCH', body: data });
}
delete<T>(endpoint: string, config?: Omit<RequestConfig, 'method' | 'body'>) {
return this.request<T>(endpoint, { ...config, method: 'DELETE' });
}
}
export * from './client';
import type { TenxyteStorage } from '../storage';
import type { RequestConfig, TenxyteHttpClient } from './client';
export interface TenxyteContext {
activeOrgSlug: string | null;
agentTraceId: string | null;
}
export function createAuthInterceptor(storage: TenxyteStorage, context: TenxyteContext) {
return async (request: RequestConfig & { url: string }) => {
// Inject Authorization if present
const token = await storage.getItem('tx_access');
const headers = { ...(request.headers as Record<string, string>) || {} };
if (token && !headers['Authorization']) {
headers['Authorization'] = `Bearer ${token}`;
}
// Inject Contextual Headers based on SDK state
if (context.activeOrgSlug && !headers['X-Org-Slug']) {
headers['X-Org-Slug'] = context.activeOrgSlug;
}
if (context.agentTraceId && !headers['X-Prompt-Trace-ID']) {
headers['X-Prompt-Trace-ID'] = context.agentTraceId;
}
return { ...request, headers };
};
}
export function createRefreshInterceptor(
client: TenxyteHttpClient,
storage: TenxyteStorage,
onSessionExpired: () => void
) {
let isRefreshing = false;
let refreshQueue: Array<(token: string | null) => void> = [];
const processQueue = (error: Error | null, token: string | null = null) => {
refreshQueue.forEach(prom => prom(token));
refreshQueue = [];
};
return async (response: Response, request: { url: string; config: RequestConfig }): Promise<Response> => {
// Only intercept 401s when not attempting to login/refresh itself
if (response.status === 401 && !request.url.includes('/auth/refresh') && !request.url.includes('/auth/login')) {
const refreshToken = await storage.getItem('tx_refresh');
if (!refreshToken) {
onSessionExpired();
return response; // Pass through 401 if we cannot refresh
}
if (isRefreshing) {
// Wait in queue for the refresh to complete
return new Promise<Response>((resolve) => {
refreshQueue.push((newToken: string | null) => {
if (newToken) {
const retryHeaders = { ...(request.config.headers as Record<string, string>), Authorization: `Bearer ${newToken}` };
resolve(fetch(request.url, { ...request.config, headers: retryHeaders } as RequestInit));
} else {
resolve(response);
}
});
});
}
// We are the first one, initiate refresh
isRefreshing = true;
try {
const refreshResponse = await fetch(`${client['baseUrl']}/auth/refresh/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
});
if (!refreshResponse.ok) {
throw new Error('Refresh failed');
}
const data = await refreshResponse.json();
await storage.setItem('tx_access', data.access);
if (data.refresh) {
await storage.setItem('tx_refresh', data.refresh);
}
isRefreshing = false;
processQueue(null, data.access);
// Retry original request seamlessly for the caller that initiated this
const retryHeaders = { ...(request.config.headers as Record<string, string>), Authorization: `Bearer ${data.access}` };
// We use fetch directly to return a true Response object back to the chain,
// rather than using client.request which resolves the JSON.
// Wait, the interceptor must return a Promise<Response>!
const r = await fetch(request.url, { ...request.config, headers: retryHeaders } as RequestInit);
return r;
} catch (err) {
// Refresh failed (invalid token, expired, network error)
isRefreshing = false;
await storage.removeItem('tx_access');
await storage.removeItem('tx_refresh');
processQueue(err as Error, null);
onSessionExpired();
// Pass original 401 back
return response;
}
}
return response;
};
}
export * from './client';
export * from './http/client';
export * from './modules/auth';
export * from './modules/security';
export * from './modules/rbac';
export * from './modules/user';
export * from './types';
import { TenxyteHttpClient } from '../http/client';
import { AgentTokenSummary, AgentPendingAction } from '../types';
export class AiModule {
private agentToken: string | null = null;
private traceId: string | null = null;
constructor(private client: TenxyteHttpClient) {
// Register an interceptor to auto-inject AgentBearer and Trace ID
this.client.addRequestInterceptor((config) => {
const headers: Record<string, string> = { ...config.headers };
if (this.agentToken) {
// Determine if we should replace the standard Authorization
// By Tenxyte specification, AgentToken uses "AgentBearer"
headers['Authorization'] = `AgentBearer ${this.agentToken}`;
}
if (this.traceId) {
headers['X-Prompt-Trace-ID'] = this.traceId;
}
return { ...config, headers };
});
// Intercept 202 Accepted and specific 403 errors (Circuit Breaker)
// Usually, these should be emitted via the main TenxyteClient EventEmitter.
// For now, we add a response interceptor to handle the HTTP side.
this.client.addResponseInterceptor(async (response, request) => {
// Note: Since response streams can only be read once, full integration
// with EventEmitter for deep inspection (like 202s body) requires cloning.
if (response.status === 202) {
// HTTP 202 Accepted indicates HITL awaiting confirmation
const cloned = response.clone();
try {
const data = await cloned.json();
// Assuming TenxyteClient will fire 'agent:awaiting_approval' based on this later
console.debug('[Tenxyte AI] Received 202 Awaiting Approval:', data);
} catch {
// Ignore parsing errors
}
} else if (response.status === 403) {
const cloned = response.clone();
try {
const data = await cloned.json();
if (data.code === 'BUDGET_EXCEEDED') {
console.warn('[Tenxyte AI] Network responded with Budget Exceeded for Agent.');
} else if (data.status === 'suspended') {
console.warn('[Tenxyte AI] Circuit breaker open for Agent.');
}
} catch {
// Ignore parsing errors
}
}
return response;
});
}
// ─── AgentToken Lifecycle ───
/**
* Create an AgentToken granting specific deterministic limits to an AI Agent.
*/
async createAgentToken(data: {
agent_id: string;
permissions?: string[];
expires_in?: number;
organization?: string;
budget_limit_usd?: number;
circuit_breaker?: {
max_requests?: number;
window_seconds?: number;
};
dead_mans_switch?: {
heartbeat_required_every?: number;
};
}): Promise<{
id: number;
token: string;
agent_id: string;
status: string;
expires_at: string;
}> {
return this.client.post('/api/v1/auth/ai/tokens/', data);
}
/**
* Set the SDK to operate on behalf of an Agent using the generated Agent Token payload.
* Overrides standard `Authorization` headers with `AgentBearer`.
*/
setAgentToken(token: string): void {
this.agentToken = token;
}
/** Disables the active Agent override and reverts to standard User session requests. */
clearAgentToken(): void {
this.agentToken = null;
}
/** Check if the SDK is currently mocking requests as an AI Agent. */
isAgentMode(): boolean {
return this.agentToken !== null;
}
/** List previously provisioned active Agent tokens. */
async listAgentTokens(): Promise<AgentTokenSummary[]> {
return this.client.get('/api/v1/auth/ai/tokens/');
}
/** Fetch the status and configuration of a specific AgentToken. */
async getAgentToken(tokenId: number): Promise<AgentTokenSummary> {
return this.client.get(`/api/v1/auth/ai/tokens/${tokenId}/`);
}
/** Irreversibly revoke a targeted AgentToken from acting upon the Tenant. */
async revokeAgentToken(tokenId: number): Promise<{ status: 'revoked' }> {
return this.client.post(`/api/v1/auth/ai/tokens/${tokenId}/revoke/`);
}
/** Temporarily freeze an AgentToken by forcibly closing its Circuit Breaker. */
async suspendAgentToken(tokenId: number): Promise<{ status: 'suspended' }> {
return this.client.post(`/api/v1/auth/ai/tokens/${tokenId}/suspend/`);
}
/** Emergency kill-switch to wipe all operational Agent Tokens. */
async revokeAllAgentTokens(): Promise<{ status: 'revoked'; count: number }> {
return this.client.post('/api/v1/auth/ai/tokens/revoke-all/');
}
// ─── Circuit Breaker ───
/** Satisfy an Agent's Dead-Man's switch heartbeat requirement to prevent suspension. */
async sendHeartbeat(tokenId: number): Promise<{ status: 'ok' }> {
return this.client.post(`/api/v1/auth/ai/tokens/${tokenId}/heartbeat/`);
}
// ─── Human in the Loop (HITL) ───
/** List intercepted HTTP 202 actions waiting for Human interaction / approval. */
async listPendingActions(): Promise<AgentPendingAction[]> {
return this.client.get('/api/v1/auth/ai/pending-actions/');
}
/** Complete a pending HITL authorization to finally flush the Agent action to backend systems. */
async confirmPendingAction(confirmationToken: string): Promise<{ status: 'confirmed' }> {
return this.client.post('/api/v1/auth/ai/pending-actions/confirm/', { token: confirmationToken });
}
/** Block an Agent action permanently. */
async denyPendingAction(confirmationToken: string): Promise<{ status: 'denied' }> {
return this.client.post('/api/v1/auth/ai/pending-actions/deny/', { token: confirmationToken });
}
// ─── Traceability and Budget ───
/** Start piping the `X-Prompt-Trace-ID` custom header outwards for tracing logs against LLM inputs. */
setTraceId(traceId: string): void {
this.traceId = traceId;
}
/** Disable trace forwarding context. */
clearTraceId(): void {
this.traceId = null;
}
/**
* Report consumption costs associated with a backend invocation back to Tenxyte for strict circuit budgeting.
* @param tokenId - AgentToken evaluating ID.
* @param usage - Sunk token costs or explicit USD derivations.
*/
async reportUsage(tokenId: number, usage: {
cost_usd: number;
prompt_tokens: number;
completion_tokens: number;
}): Promise<{ status: 'ok' } | { error: 'Budget exceeded'; status: 'suspended' }> {
return this.client.post(`/api/v1/auth/ai/tokens/${tokenId}/report-usage/`, usage);
}
}
import { TenxyteHttpClient } from '../http/client';
import { TokenPair, GeneratedSchema } from '../types';
export interface LoginEmailOptions {
totp_code?: string;
}
export interface LoginPhoneOptions {
totp_code?: string;
}
export type RegisterRequest = any;
export interface MagicLinkRequest {
email: string;
}
export interface SocialLoginRequest {
access_token?: string;
authorization_code?: string;
id_token?: string;
}
export class AuthModule {
constructor(private client: TenxyteHttpClient) { }
/**
* Authenticate a user with their email and password.
* @param data - The login credentials and optional TOTP code if 2FA is required.
* @returns A pair of Access and Refresh tokens upon successful authentication.
* @throws {TenxyteError} If credentials are invalid, or if `2FA_REQUIRED` without a valid `totp_code`.
*/
async loginWithEmail(
data: GeneratedSchema['LoginEmail'],
): Promise<TokenPair> {
return this.client.post<TokenPair>('/api/v1/auth/login/email/', data);
}
/**
* Authenticate a user with an international phone number and password.
* @param data - The login credentials and optional TOTP code if 2FA is required.
* @returns A pair of Access and Refresh tokens.
*/
async loginWithPhone(
data: GeneratedSchema['LoginPhone'],
): Promise<TokenPair> {
return this.client.post<TokenPair>('/api/v1/auth/login/phone/', data);
}
/**
* Registers a new user account.
* @param data - The registration details (email, password, etc.).
* @returns The registered user data or a confirmation message.
*/
async register(data: RegisterRequest): Promise<any> {
return this.client.post<any>('/api/v1/auth/register/', data);
}
/**
* Logout from the current session.
* Informs the backend to immediately revoke the specified refresh token.
* @param refreshToken - The refresh token to revoke.
*/
async logout(refreshToken: string): Promise<void> {
return this.client.post<void>('/api/v1/auth/logout/', { refresh_token: refreshToken });
}
/**
* Logout from all sessions across all devices.
* Revokes all refresh tokens currently assigned to the user.
*/
async logoutAll(): Promise<void> {
return this.client.post<void>('/api/v1/auth/logout/all/');
}
/**
* Request a Magic Link for passwordless sign-in.
* @param data - The email to send the logic link to.
*/
async requestMagicLink(data: MagicLinkRequest): Promise<void> {
return this.client.post<void>('/api/v1/auth/magic-link/request/', data);
}
/**
* Verifies a magic link token extracted from the URL.
* @param token - The cryptographic token received via email.
* @returns A session token pair if the token is valid and unexpired.
*/
async verifyMagicLink(token: string): Promise<TokenPair> {
return this.client.get<TokenPair>(`/api/v1/auth/magic-link/verify/`, { params: { token } });
}
/**
* Submits OAuth2 Social Authentication payloads to the backend.
* Can be used with native mobile SDK tokens (like Apple Sign-In JWTs).
* @param provider - The OAuth provider ('google', 'github', etc.)
* @param data - The OAuth tokens (access_token, id_token, etc.)
* @returns An active session token pair.
*/
async loginWithSocial(provider: 'google' | 'github' | 'microsoft' | 'facebook', data: SocialLoginRequest): Promise<TokenPair> {
return this.client.post<TokenPair>(`/api/v1/auth/social/${provider}/`, data);
}
/**
* Handle Social Auth Callbacks (Authorization Code flow).
* @param provider - The OAuth provider ('google', 'github', etc.)
* @param code - The authorization code retrieved from the query string parameters.
* @param redirectUri - The original redirect URI that was requested.
* @returns An active session token pair after successful code exchange.
*/
async handleSocialCallback(provider: 'google' | 'github' | 'microsoft' | 'facebook', code: string, redirectUri: string): Promise<TokenPair> {
return this.client.get<TokenPair>(`/api/v1/auth/social/${provider}/callback/`, {
params: { code, redirect_uri: redirectUri },
});
}
}
import { TenxyteHttpClient } from '../http/client';
import { Organization, PaginatedResponse } from '../types';
export interface OrgMembership {
id: number;
user_id: number;
email: string;
first_name: string;
last_name: string;
role: { code: string; name: string };
joined_at: string;
}
export interface OrgTreeNode {
id: number;
name: string;
slug: string;
children: OrgTreeNode[];
}
export class B2bModule {
private currentOrgSlug: string | null = null;
constructor(private client: TenxyteHttpClient) {
// Register an interceptor to auto-inject the X-Org-Slug header
this.client.addRequestInterceptor((config) => {
if (this.currentOrgSlug) {
config.headers = {
...config.headers,
'X-Org-Slug': this.currentOrgSlug,
};
}
return config;
});
}
// ─── Context Management ───
/**
* Set the active Organization context.
* Subsequent API requests will automatically include the `X-Org-Slug` header.
* @param slug - The unique string identifier of the organization.
*/
switchOrganization(slug: string): void {
this.currentOrgSlug = slug;
}
/**
* Clear the active Organization context, dropping the `X-Org-Slug` header for standard User operations.
*/
clearOrganization(): void {
this.currentOrgSlug = null;
}
/** Get the currently active Organization slug context if set. */
getCurrentOrganizationSlug(): string | null {
return this.currentOrgSlug;
}
// ─── Organizations CRUD ───
/** Create a new top-level or child Organization in the backend. */
async createOrganization(data: {
name: string;
slug?: string;
description?: string;
parent_id?: number;
metadata?: Record<string, unknown>;
max_members?: number;
}): Promise<Organization> {
return this.client.post('/api/v1/auth/organizations/', data);
}
/** List organizations the currently authenticated user belongs to. */
async listMyOrganizations(params?: {
search?: string;
is_active?: boolean;
parent?: string;
ordering?: string;
page?: number;
page_size?: number;
}): Promise<PaginatedResponse<Organization>> {
return this.client.get('/api/v1/auth/organizations/', { params });
}
/** Retrieve details about a specific organization by slug. */
async getOrganization(slug: string): Promise<Organization> {
return this.client.get(`/api/v1/auth/organizations/${slug}/`);
}
/** Update configuration and metadata of an Organization. */
async updateOrganization(slug: string, data: Partial<{
name: string;
slug: string;
description: string;
parent_id: number | null;
metadata: Record<string, unknown>;
max_members: number;
is_active: boolean;
}>): Promise<Organization> {
return this.client.patch(`/api/v1/auth/organizations/${slug}/`, data);
}
/** Permanently delete an Organization. */
async deleteOrganization(slug: string): Promise<{ message: string }> {
return this.client.delete(`/api/v1/auth/organizations/${slug}/`);
}
/** Retrieve the topology subtree extending downward from this point. */
async getOrganizationTree(slug: string): Promise<OrgTreeNode> {
return this.client.get(`/api/v1/auth/organizations/${slug}/tree/`);
}
// ─── Member Management ───
/** List users bound to a specific Organization. */
async listMembers(slug: string, params?: {
search?: string;
role?: 'owner' | 'admin' | 'member';
status?: 'active' | 'inactive' | 'pending';
ordering?: string;
page?: number;
page_size?: number;
}): Promise<PaginatedResponse<OrgMembership>> {
return this.client.get(`/api/v1/auth/organizations/${slug}/members/`, { params });
}
/** Add a user directly into an Organization with a designated role. */
async addMember(slug: string, data: {
user_id: number;
role_code: string;
}): Promise<OrgMembership> {
return this.client.post(`/api/v1/auth/organizations/${slug}/members/`, data);
}
/** Evolve or demote an existing member's role within the Organization. */
async updateMemberRole(slug: string, userId: number, roleCode: string): Promise<OrgMembership> {
return this.client.patch(`/api/v1/auth/organizations/${slug}/members/${userId}/`, { role_code: roleCode });
}
/** Kick a user out of the Organization. */
async removeMember(slug: string, userId: number): Promise<{ message: string }> {
return this.client.delete(`/api/v1/auth/organizations/${slug}/members/${userId}/`);
}
// ─── Invitations ───
/** Send an onboarding email invitation to join an Organization. */
async inviteMember(slug: string, data: {
email: string;
role_code: string;
expires_in_days?: number;
}): Promise<{
id: number;
email: string;
role: string;
token: string;
expires_at: string;
invited_by: { id: number; email: string };
organization: { id: number; name: string; slug: string };
}> {
return this.client.post(`/api/v1/auth/organizations/${slug}/invitations/`, data);
}
/** Fetch a definition matrix of what Organization-level roles can be assigned. */
async listOrgRoles(): Promise<Array<{
code: string;
name: string;
description: string;
weight: number;
permissions: Array<{ code: string; name: string; description: string }>;
is_system_role: boolean;
created_at: string;
}>> {
return this.client.get('/api/v1/auth/organizations/roles/');
}
}
import { TenxyteHttpClient } from '../http/client';
import { decodeJwt, DecodedTenxyteToken } from '../utils/jwt';
export interface Role {
id: string;
name: string;
description?: string;
is_default?: boolean;
permissions?: string[];
}
export interface Permission {
id: string;
code: string;
name: string;
description?: string;
}
export class RbacModule {
private cachedToken: string | null = null;
constructor(private client: TenxyteHttpClient) { }
/**
* Cache a decoded JWT payload locally to perform parameter-less synchronous permission checks.
* Usually invoked automatically by the system upon login or token refresh.
* @param token - The raw JWT access token encoded string.
*/
setToken(token: string | null) {
this.cachedToken = token;
}
private getDecodedToken(token?: string): DecodedTenxyteToken | null {
const t = token || this.cachedToken;
if (!t) return null;
return decodeJwt(t);
}
// --- Synchronous Checks --- //
/**
* Synchronously deeply inspects the cached (or provided) JWT to determine if the user has a specific Role.
* @param role - The exact code name of the Role.
* @param token - (Optional) Provide a specific token overriding the cached one.
*/
hasRole(role: string, token?: string): boolean {
const decoded = this.getDecodedToken(token);
if (!decoded?.roles) return false;
return decoded.roles.includes(role);
}
/**
* Evaluates if the active session holds AT LEAST ONE of the listed Roles.
* @param roles - An array of Role codes.
*/
hasAnyRole(roles: string[], token?: string): boolean {
const decoded = this.getDecodedToken(token);
if (!decoded?.roles) return false;
return roles.some(r => decoded.roles!.includes(r));
}
/**
* Evaluates if the active session holds ALL of the listed Roles concurrently.
* @param roles - An array of Role codes.
*/
hasAllRoles(roles: string[], token?: string): boolean {
const decoded = this.getDecodedToken(token);
if (!decoded?.roles) return false;
return roles.every(r => decoded.roles!.includes(r));
}
/**
* Synchronously deeply inspects the cached (or provided) JWT to determine if the user has a specific granular Permission.
* @param permission - The exact code name of the Permission (e.g., 'invoices.read').
*/
hasPermission(permission: string, token?: string): boolean {
const decoded = this.getDecodedToken(token);
if (!decoded?.permissions) return false;
return decoded.permissions.includes(permission);
}
/**
* Evaluates if the active session holds AT LEAST ONE of the listed Permissions.
*/
hasAnyPermission(permissions: string[], token?: string): boolean {
const decoded = this.getDecodedToken(token);
if (!decoded?.permissions) return false;
return permissions.some(p => decoded.permissions!.includes(p));
}
/**
* Evaluates if the active session holds ALL of the listed Permissions concurrently.
*/
hasAllPermissions(permissions: string[], token?: string): boolean {
const decoded = this.getDecodedToken(token);
if (!decoded?.permissions) return false;
return permissions.every(p => decoded.permissions!.includes(p));
}
// --- Roles CRUD --- //
/** Fetch all application global Roles structure */
async listRoles(): Promise<Role[]> {
return this.client.get<Role[]>('/api/v1/auth/roles/');
}
/** Create a new architectural Role inside Tenxyte */
async createRole(data: { name: string; description?: string; permission_codes?: string[]; is_default?: boolean }): Promise<Role> {
return this.client.post<Role>('/api/v1/auth/roles/', data);
}
/** Get detailed metadata defining a single bounded Role */
async getRole(roleId: string): Promise<Role> {
return this.client.get<Role>(`/api/v1/auth/roles/${roleId}/`);
}
/** Modify properties bounding a Role */
async updateRole(roleId: string, data: { name?: string; description?: string; permission_codes?: string[]; is_default?: boolean }): Promise<Role> {
return this.client.put<Role>(`/api/v1/auth/roles/${roleId}/`, data);
}
/** Unbind and destruct a Role from the global Tenant. (Dangerous, implies cascading permission unbindings) */
async deleteRole(roleId: string): Promise<void> {
return this.client.delete<void>(`/api/v1/auth/roles/${roleId}/`);
}
// --- Role Permissions Management --- //
async getRolePermissions(roleId: string): Promise<Permission[]> {
return this.client.get<Permission[]>(`/api/v1/auth/roles/${roleId}/permissions/`);
}
async addPermissionsToRole(roleId: string, permission_codes: string[]): Promise<void> {
return this.client.post<void>(`/api/v1/auth/roles/${roleId}/permissions/`, { permission_codes });
}
async removePermissionsFromRole(roleId: string, permission_codes: string[]): Promise<void> {
return this.client.delete<void>(`/api/v1/auth/roles/${roleId}/permissions/`, {
// Note: DELETE request with body is supported via our fetch wrapper if enabled,
// or we might need to rely on query strings. The schema specifies body or query.
// Let's pass it in body via a custom config or URL params.
body: { permission_codes }
} as any);
}
// --- Permissions CRUD --- //
/** Enumerates all available fine-grained Permissions inside this Tenant scope. */
async listPermissions(): Promise<Permission[]> {
return this.client.get<Permission[]>('/api/v1/auth/permissions/');
}
/** Bootstraps a new granular Permission flag (e.g. `billing.refund`). */
async createPermission(data: { code: string; name: string; description?: string; parent_code?: string }): Promise<Permission> {
return this.client.post<Permission>('/api/v1/auth/permissions/', data);
}
/** Retrieves an existing atomic Permission construct. */
async getPermission(permissionId: string): Promise<Permission> {
return this.client.get<Permission>(`/api/v1/auth/permissions/${permissionId}/`);
}
/** Edits the human readable description or structural dependencies of a Permission. */
async updatePermission(permissionId: string, data: { name?: string; description?: string }): Promise<Permission> {
return this.client.put<Permission>(`/api/v1/auth/permissions/${permissionId}/`, data);
}
/** Destroys an atomic Permission permanently. Any Roles referencing it will be stripped of this grant automatically. */
async deletePermission(permissionId: string): Promise<void> {
return this.client.delete<void>(`/api/v1/auth/permissions/${permissionId}/`);
}
// --- Direct Assignment (Users) --- //
/**
* Attach a given Role globally to a user entity.
* Use sparingly if B2B multi-tenancy contexts are preferred.
*/
async assignRoleToUser(userId: string, roleCode: string): Promise<void> {
return this.client.post<void>(`/api/v1/auth/users/${userId}/roles/`, { role_code: roleCode });
}
/**
* Unbind a global Role from a user entity.
*/
async removeRoleFromUser(userId: string, roleCode: string): Promise<void> {
return this.client.delete<void>(`/api/v1/auth/users/${userId}/roles/`, {
params: { role_code: roleCode }
});
}
/**
* Ad-Hoc directly attach specific granular Permissions to a single User, bypassing Role boundaries.
*/
async assignPermissionsToUser(userId: string, permissionCodes: string[]): Promise<void> {
return this.client.post<void>(`/api/v1/auth/users/${userId}/permissions/`, { permission_codes: permissionCodes });
}
/**
* Ad-Hoc strip direct granular Permissions bindings from a specific User.
*/
async removePermissionsFromUser(userId: string, permissionCodes: string[]): Promise<void> {
return this.client.delete<void>(`/api/v1/auth/users/${userId}/permissions/`, {
body: { permission_codes: permissionCodes }
} as any);
}
}
import { TenxyteHttpClient } from '../http/client';
import { TenxyteUser, TokenPair } from '../types';
import { base64urlToBuffer, bufferToBase64url } from '../utils/base64url';
export class SecurityModule {
constructor(private client: TenxyteHttpClient) { }
// ─── 2FA (TOTP) Management ───
/**
* Get the current 2FA status for the authenticated user.
* @returns Information about whether 2FA is enabled and how many backup codes remain.
*/
async get2FAStatus(): Promise<{ is_enabled: boolean; backup_codes_remaining: number }> {
return this.client.get('/api/v1/auth/2fa/status/');
}
/**
* Start the 2FA enrollment process.
* @returns The secret key and QR code URL to be scanned by an Authenticator app.
*/
async setup2FA(): Promise<{
message: string;
secret: string;
manual_entry_key: string;
qr_code: string;
provisioning_uri: string;
backup_codes: string[];
warning: string;
}> {
return this.client.post('/api/v1/auth/2fa/setup/');
}
/**
* Confirm the 2FA setup by providing the first TOTP code generated by the Authenticator app.
* @param totpCode - The 6-digit code.
*/
async confirm2FA(totpCode: string): Promise<{
message: string;
is_enabled: boolean;
enabled_at: string;
}> {
return this.client.post('/api/v1/auth/2fa/confirm/', { totp_code: totpCode });
}
/**
* Disable 2FA for the current user.
* Usually requires re-authentication or providing the active password/totp code.
* @param totpCode - The current 6-digit code to verify intent.
* @param password - (Optional) The user's password if required by backend policy.
*/
async disable2FA(totpCode: string, password?: string): Promise<{
message: string;
is_enabled: boolean;
disabled_at: string;
backup_codes_invalidated: boolean;
}> {
return this.client.post('/api/v1/auth/2fa/disable/', { totp_code: totpCode, password });
}
/**
* Invalidate old backup codes and explicitly generate a new batch.
* @param totpCode - An active TOTP code to verify intent.
*/
async regenerateBackupCodes(totpCode: string): Promise<{
message: string;
backup_codes: string[];
codes_count: number;
generated_at?: string;
warning: string;
}> {
return this.client.post('/api/v1/auth/2fa/backup-codes/', { totp_code: totpCode });
}
// ─── Verification OTP (Email / Phone) ───
/**
* Request an OTP code to be dispatched to the user's primary contact method.
* @param type - The channel type ('email' or 'phone').
*/
async requestOtp(type: 'email' | 'phone'): Promise<{
message: string;
otp_id: number;
expires_at: string;
channel: 'email' | 'phone';
masked_recipient: string;
}> {
return this.client.post('/api/v1/auth/otp/request/', { type: type === 'email' ? 'email_verification' : 'phone_verification' });
}
/**
* Verify an email confirmation OTP.
* @param code - The numeric code received via email.
*/
async verifyOtpEmail(code: string): Promise<{
message: string;
email_verified: boolean;
verified_at: string;
}> {
return this.client.post('/api/v1/auth/otp/verify/email/', { code });
}
/**
* Verify a phone confirmation OTP (SMS dispatch).
* @param code - The numeric code received via SMS.
*/
async verifyOtpPhone(code: string): Promise<{
message: string;
phone_verified: boolean;
verified_at: string;
phone_number: string;
}> {
return this.client.post('/api/v1/auth/otp/verify/phone/', { code });
}
// ─── Password Sub-module ───
/**
* Triggers a password reset flow, dispatching an OTP to the target.
* @param target - Either an email address or a phone configuration payload.
*/
async resetPasswordRequest(target: { email: string } | { phone_country_code: string; phone_number: string }): Promise<{ message: string }> {
return this.client.post('/api/v1/auth/password/reset/request/', target);
}
/**
* Confirm a password reset using the OTP dispatched by `resetPasswordRequest`.
* @param data - The OTP code and the new matching password fields.
*/
async resetPasswordConfirm(data: {
email?: string;
phone_country_code?: string;
phone_number?: string;
otp_code: string;
new_password: string;
confirm_password: string;
}): Promise<{ message: string }> {
return this.client.post('/api/v1/auth/password/reset/confirm/', data);
}
/**
* Change password for an already authenticated user.
* @param currentPassword - The existing password to verify intent.
* @param newPassword - The distinct new password.
*/
async changePassword(currentPassword: string, newPassword: string): Promise<{ message: string }> {
return this.client.post('/api/v1/auth/password/change/', {
current_password: currentPassword,
new_password: newPassword,
});
}
/**
* Evaluate the strength of a potential password against backend policies.
* @param password - The password string to test.
* @param email - (Optional) The user's email to ensure the password doesn't contain it.
*/
async checkPasswordStrength(password: string, email?: string): Promise<{
score: number;
strength: string;
is_valid: boolean;
errors: string[];
requirements: {
min_length: number;
require_lowercase: boolean;
require_uppercase: boolean;
require_numbers: boolean;
require_special: boolean;
};
}> {
return this.client.post('/api/v1/auth/password/strength/', { password, email });
}
/**
* Fetch the password complexity requirements enforced by the Tenxyte backend.
*/
async getPasswordRequirements(): Promise<{
requirements: Record<string, boolean | number>;
min_length: number;
max_length: number;
}> {
return this.client.get('/api/v1/auth/password/requirements/');
}
// ─── WebAuthn / Passkeys (FIDO2) ───
/**
* Register a new WebAuthn device (Passkey/Biometrics/Security Key) for the authenticated user.
* Integrates transparently with the browser `navigator.credentials` API.
* @param deviceName - Optional human-readable name for the device being registered.
*/
async registerWebAuthn(deviceName?: string): Promise<{
message: string;
credential: {
id: number;
device_name: string;
created_at: string;
};
}> {
// 1. Begin registration
const optionsResponse = await this.client.post<any>('/api/v1/auth/webauthn/register/begin/');
// 2. Map options for the browser
const publicKeyOpts = optionsResponse.publicKey;
publicKeyOpts.challenge = base64urlToBuffer(publicKeyOpts.challenge);
publicKeyOpts.user.id = base64urlToBuffer(publicKeyOpts.user.id);
if (publicKeyOpts.excludeCredentials) {
publicKeyOpts.excludeCredentials.forEach((cred: any) => {
cred.id = base64urlToBuffer(cred.id);
});
}
// 3. Request credential creation from the Authenticator
const cred = await navigator.credentials.create({ publicKey: publicKeyOpts }) as PublicKeyCredential;
if (!cred) {
throw new Error('WebAuthn registration was aborted or failed.');
}
const response = cred.response as AuthenticatorAttestationResponse;
// 4. Complete registration on the backend
const completionPayload = {
device_name: deviceName,
credential: {
id: cred.id,
type: cred.type,
rawId: bufferToBase64url(cred.rawId),
response: {
attestationObject: bufferToBase64url(response.attestationObject),
clientDataJSON: bufferToBase64url(response.clientDataJSON),
},
},
};
return this.client.post('/api/v1/auth/webauthn/register/complete/', completionPayload);
}
/**
* Authenticate via WebAuthn (Passkey) without requiring a password.
* Integrates transparently with the browser `navigator.credentials` API.
* @param email - The email address identifying the user account (optional if discoverable credentials are used).
* @returns A session token pair and the user context upon successful cryptographic challenge verification.
*/
async authenticateWebAuthn(email?: string): Promise<{
access: string;
refresh: string;
user: TenxyteUser;
message: string;
credential_used: string;
}> {
// 1. Begin authentication
const optionsResponse = await this.client.post<any>('/api/v1/auth/webauthn/authenticate/begin/', email ? { email } : {});
// 2. Map options for the browser
const publicKeyOpts = optionsResponse.publicKey;
publicKeyOpts.challenge = base64urlToBuffer(publicKeyOpts.challenge);
if (publicKeyOpts.allowCredentials) {
publicKeyOpts.allowCredentials.forEach((cred: any) => {
cred.id = base64urlToBuffer(cred.id);
});
}
// 3. Request assertion from the Authenticator
const cred = await navigator.credentials.get({ publicKey: publicKeyOpts }) as PublicKeyCredential;
if (!cred) {
throw new Error('WebAuthn authentication was aborted or failed.');
}
const response = cred.response as AuthenticatorAssertionResponse;
// 4. Complete authentication on the backend
const completionPayload = {
credential: {
id: cred.id,
type: cred.type,
rawId: bufferToBase64url(cred.rawId),
response: {
authenticatorData: bufferToBase64url(response.authenticatorData),
clientDataJSON: bufferToBase64url(response.clientDataJSON),
signature: bufferToBase64url(response.signature),
userHandle: response.userHandle ? bufferToBase64url(response.userHandle) : null,
},
},
};
return this.client.post('/api/v1/auth/webauthn/authenticate/complete/', completionPayload);
}
/**
* List all registered WebAuthn credentials for the active user.
*/
async listWebAuthnCredentials(): Promise<{
credentials: Array<{
id: number;
device_name: string;
created_at: string;
last_used_at: string | null;
authenticator_type: string;
is_resident_key: boolean;
}>;
count: number;
}> {
return this.client.get('/api/v1/auth/webauthn/credentials/');
}
/**
* Delete a specific WebAuthn credential, removing its capability to sign in.
* @param credentialId - The internal ID of the credential to delete.
*/
async deleteWebAuthnCredential(credentialId: number): Promise<void> {
return this.client.delete(`/api/v1/auth/webauthn/credentials/${credentialId}/`);
}
}
import { TenxyteHttpClient } from '../http/client';
export interface UpdateProfileParams {
first_name?: string;
last_name?: string;
[key: string]: any; // Allow custom metadata updates
}
export interface AdminUpdateUserParams {
first_name?: string;
last_name?: string;
is_active?: boolean;
is_locked?: boolean;
max_sessions?: number;
max_devices?: number;
}
export class UserModule {
constructor(private client: TenxyteHttpClient) { }
// --- Standard Profile Actions --- //
/** Retrieve your current comprehensive Profile metadata matching the active network bearer token. */
async getProfile(): Promise<any> {
return this.client.get('/api/v1/auth/me/');
}
/** Modify your active profile core details or injected application metadata. */
async updateProfile(data: UpdateProfileParams): Promise<any> {
return this.client.patch('/api/v1/auth/me/', data);
}
/**
* Upload an avatar using FormData.
* Ensure the environment supports FormData (browser or Node.js v18+).
* @param formData The FormData object containing the 'avatar' field.
*/
async uploadAvatar(formData: FormData): Promise<any> {
return this.client.patch('/api/v1/auth/me/', formData);
}
/**
* Trigger self-deletion of an entire account data boundary.
* @param password - Requires the active system password as destructive proof of intent.
* @param otpCode - (Optional) If an OTP was queried prior to attempting account deletion.
*/
async deleteAccount(password: string, otpCode?: string): Promise<void> {
return this.client.post<void>('/api/v1/auth/request-account-deletion/', {
password,
otp_code: otpCode
});
}
// --- Admin Actions Mapping --- //
/** (Admin only) Lists users paginated matching criteria. */
async listUsers(params?: Record<string, any>): Promise<any[]> {
return this.client.get<any[]>('/api/v1/auth/admin/users/', { params });
}
/** (Admin only) Gets deterministic data related to a remote unassociated user. */
async getUser(userId: string): Promise<any> {
return this.client.get(`/api/v1/auth/admin/users/${userId}/`);
}
/** (Admin only) Modifies configuration/details or capacity bounds related to a remote unassociated user. */
async adminUpdateUser(userId: string, data: AdminUpdateUserParams): Promise<any> {
return this.client.patch(`/api/v1/auth/admin/users/${userId}/`, data);
}
/** (Admin only) Force obliterate a User boundary. Can affect relational database stability if not bound carefully. */
async adminDeleteUser(userId: string): Promise<void> {
return this.client.delete<void>(`/api/v1/auth/admin/users/${userId}/`);
}
/** (Admin only) Apply a permanent suspension / ban state globally on a user token footprint. */
async banUser(userId: string, reason: string = ''): Promise<void> {
return this.client.post<void>(`/api/v1/auth/admin/users/${userId}/ban/`, { reason });
}
/** (Admin only) Recover a user footprint from a global ban state. */
async unbanUser(userId: string): Promise<void> {
return this.client.post<void>(`/api/v1/auth/admin/users/${userId}/unban/`);
}
/** (Admin only) Apply a temporary lock bounding block on a user interaction footprint. */
async lockUser(userId: string, durationMinutes: number = 30, reason: string = ''): Promise<void> {
return this.client.post<void>(`/api/v1/auth/admin/users/${userId}/lock/`, { duration_minutes: durationMinutes, reason });
}
/** (Admin only) Releases an arbitrary temporary system lock placed on a user bounds. */
async unlockUser(userId: string): Promise<void> {
return this.client.post<void>(`/api/v1/auth/admin/users/${userId}/unlock/`);
}
}
import type { TenxyteStorage } from './index';
/**
* CookieStorage implementation
* Note: To be secure, tokens should be HttpOnly where possible.
* This class handles client-side cookies if necessary.
*/
export class CookieStorage implements TenxyteStorage {
private defaultOptions: string;
constructor(options: { secure?: boolean; sameSite?: 'Strict' | 'Lax' | 'None' } = {}) {
const secure = options.secure ?? true;
const sameSite = options.sameSite ?? 'Lax';
this.defaultOptions = `path=/; SameSite=${sameSite}${secure ? '; Secure' : ''}`;
}
getItem(key: string): string | null {
if (typeof document === 'undefined') return null;
const match = document.cookie.match(new RegExp(`(^| )${key}=([^;]+)`));
return match ? decodeURIComponent(match[2]) : null;
}
setItem(key: string, value: string): void {
if (typeof document === 'undefined') return;
document.cookie = `${key}=${encodeURIComponent(value)}; ${this.defaultOptions}`;
}
removeItem(key: string): void {
if (typeof document === 'undefined') return;
document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
}
clear(): void {
// Cannot easily clear all cookies securely because we don't know them all
// Usually auth keys are known, e.g., tx_access, tx_refresh
this.removeItem('tx_access');
this.removeItem('tx_refresh');
}
}
export interface TenxyteStorage {
/**
* Retrieves a value from storage.
* @param key The key to retrieve
*/
getItem(key: string): string | null | Promise<string | null>;
/**
* Saves a value to storage.
* @param key The key to store
* @param value The string value
*/
setItem(key: string, value: string): void | Promise<void>;
/**
* Removes a specific key from storage.
* @param key The key to remove
*/
removeItem(key: string): void | Promise<void>;
/**
* Clears all storage keys managed by the SDK.
*/
clear(): void | Promise<void>;
}
export * from './memory';
export * from './localStorage';
export * from './cookie';
import type { TenxyteStorage } from './index';
import { MemoryStorage } from './memory';
/**
* LocalStorage wrapper for the browser.
* Degrades gracefully to MemoryStorage if localStorage is unavailable
* (e.g., SSR, Private Browsing mode strictness).
*/
export class LocalStorage implements TenxyteStorage {
private fallbackMemoryStore: MemoryStorage | null = null;
private isAvailable: boolean;
constructor() {
this.isAvailable = this.checkAvailability();
if (!this.isAvailable) {
this.fallbackMemoryStore = new MemoryStorage();
}
}
private checkAvailability(): boolean {
try {
if (typeof window === 'undefined' || !window.localStorage) {
return false;
}
const testKey = '__tenxyte_test__';
window.localStorage.setItem(testKey, '1');
window.localStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
}
getItem(key: string): string | null {
if (!this.isAvailable && this.fallbackMemoryStore) {
return this.fallbackMemoryStore.getItem(key);
}
return window.localStorage.getItem(key);
}
setItem(key: string, value: string): void {
if (!this.isAvailable && this.fallbackMemoryStore) {
this.fallbackMemoryStore.setItem(key, value);
return;
}
try {
window.localStorage.setItem(key, value);
} catch (e) {
// Storage quota exceeded or similar error
console.warn(`[Tenxyte SDK] Warning: failed to write to localStorage for key ${key}`);
}
}
removeItem(key: string): void {
if (!this.isAvailable && this.fallbackMemoryStore) {
this.fallbackMemoryStore.removeItem(key);
return;
}
window.localStorage.removeItem(key);
}
clear(): void {
if (!this.isAvailable && this.fallbackMemoryStore) {
this.fallbackMemoryStore.clear();
return;
}
// We ideally only clear tenxyte specific keys if needed,
// but standard clear() removes everything.
// If the library only ever writes specific keys,
// we could keep track of them and iterate, but for now clear() is standard.
// For safer implementation we could just let the caller do removeItems() individually
// but let's conform to the clear API.
window.localStorage.clear();
}
}
import type { TenxyteStorage } from './index';
/**
* MemoryStorage implementation primarily used in Node.js (SSR)
* environments or as a fallback when browser storage is unavailable.
*/
export class MemoryStorage implements TenxyteStorage {
private store: Map<string, string>;
constructor() {
this.store = new Map<string, string>();
}
getItem(key: string): string | null {
const value = this.store.get(key);
return value !== undefined ? value : null;
}
setItem(key: string, value: string): void {
this.store.set(key, value);
}
removeItem(key: string): void {
this.store.delete(key);
}
clear(): void {
this.store.clear();
}
}

Sorry, the diff of this file is too big to display

import type { components, paths } from './api-schema';
export type GeneratedSchema = components['schemas'];
/**
* Core User Interface exposed by the SDK.
* Represents the authenticated entity bound to the active session.
*/
export interface TenxyteUser {
id: string; // UUID
email: string | null;
phone_country_code: string | null;
phone_number: string | null;
first_name: string;
last_name: string;
is_email_verified: boolean;
is_phone_verified: boolean;
is_2fa_enabled: boolean;
roles: string[]; // Role codes e.g., ['admin', 'viewer']
permissions: string[]; // Permission codes (direct + inherited)
created_at: string; // ISO 8601
last_login: string | null;
}
/**
* Standard SDK Token Pair (internal structure normalized by interceptors).
* These are managed automatically if auto-refresh is enabled.
*/
export interface TokenPair {
access_token: string; // JWT Bearer
refresh_token: string;
token_type: 'Bearer';
expires_in: number; // Current access_token lifetime in seconds
device_summary: string | null; // e.g., "desktop — windows 11 — chrome 122" (null if device_info absent)
}
/**
* Standardized API Error Response wrapper thrown by network interceptors.
*/
export interface TenxyteError {
error: string; // Human message
code: TenxyteErrorCode; // Machine identifier
details?: Record<string, string[]> | string; // Per-field errors or free message
retry_after?: number; // Present on HTTP 429 and 423
}
export type TenxyteErrorCode =
// Auth
| 'LOGIN_FAILED'
| 'INVALID_CREDENTIALS'
| 'ACCOUNT_LOCKED'
| 'ACCOUNT_BANNED'
| '2FA_REQUIRED'
| 'ADMIN_2FA_SETUP_REQUIRED'
| 'TOKEN_EXPIRED'
| 'TOKEN_BLACKLISTED'
| 'REFRESH_FAILED'
| 'PERMISSION_DENIED'
| 'SESSION_LIMIT_EXCEEDED'
| 'DEVICE_LIMIT_EXCEEDED'
| 'RATE_LIMITED'
| 'INVALID_OTP'
| 'OTP_EXPIRED'
| 'INVALID_PROVIDER'
| 'SOCIAL_AUTH_FAILED'
| 'VALIDATION_URL_REQUIRED'
| 'INVALID_TOKEN'
// User / Account
| 'CONFIRMATION_REQUIRED'
| 'PASSWORD_REQUIRED'
| 'INVALID_PASSWORD'
| 'INVALID_DEVICE_INFO'
// B2B / Organizations
| 'ORG_NOT_FOUND'
| 'NOT_ORG_MEMBER'
| 'NOT_OWNER'
| 'ALREADY_MEMBER'
| 'MEMBER_LIMIT_EXCEEDED'
| 'HAS_CHILDREN'
| 'CIRCULAR_HIERARCHY'
| 'LAST_OWNER_REQUIRED'
| 'INVITATION_EXISTS'
| 'INVALID_ROLE'
// AIRS — Agent errors
| 'AGENT_NOT_FOUND'
| 'AGENT_SUSPENDED'
| 'AGENT_REVOKED'
| 'AGENT_EXPIRED'
| 'BUDGET_EXCEEDED'
| 'RATE_LIMIT_EXCEEDED'
| 'HEARTBEAT_MISSING'
| 'AIRS_DISABLED';
/**
* Organization Structure defining a B2B tenant or hierarchical unit.
*/
export interface Organization {
id: number;
name: string;
slug: string;
description: string | null;
metadata: Record<string, unknown> | null;
is_active: boolean;
max_members: number; // 0 = unlimited
member_count: number;
created_at: string;
updated_at: string;
parent: { id: number; name: string; slug: string } | null;
children: Array<{ id: number; name: string; slug: string }>;
user_role: string | null; // Current user's contextual role inside this exact org
user_permissions: string[]; // Effective permissions resolving downward in this org
}
/**
* Base Pagination Response wrapper
*/
export interface PaginatedResponse<T> {
count: number;
page: number;
page_size: number;
total_pages: number;
next: string | null;
previous: string | null;
results: T[];
}
/**
* AIRS Agent Token metadata
*/
export interface AgentTokenSummary {
id: number;
agent_id: string;
status: 'ACTIVE' | 'SUSPENDED' | 'REVOKED' | 'EXPIRED';
expires_at: string;
created_at: string;
organization: string | null; // Org slug, or null
current_request_count: number;
}
/**
* Request awaiting Human-In-The-Loop approval
*/
export interface AgentPendingAction {
id: number;
agent_id: string;
permission: string; // e.g., "users.delete"
endpoint: string;
payload: unknown;
confirmation_token: string;
expires_at: string;
created_at: string;
}
export function bufferToBase64url(buffer: ArrayBuffer | Uint8Array): string {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
export function base64urlToBuffer(base64url: string): Uint8Array {
const base64 = base64url
.replace(/-/g, '+')
.replace(/_/g, '/');
const padLen = (4 - (base64.length % 4)) % 4;
const padded = base64 + '='.repeat(padLen);
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
/**
* Helper utility to build the device fingerprint required by Tenxyte security features.
* Format: `v=1|os=windows;osv=11|device=desktop|arch=x64|app=tenxyte;appv=1.0.0|runtime=chrome;rtv=122|tz=Europe/Paris`
*/
export interface CustomDeviceInfo {
os?: string;
osVersion?: string;
device?: string;
arch?: string;
app?: string;
appVersion?: string;
runtime?: string;
runtimeVersion?: string;
timezone?: string;
}
export function buildDeviceInfo(customInfo: CustomDeviceInfo = {}): string {
// Try to determine automatically from navigator
const autoInfo = getAutoInfo();
const v = '1';
const os = customInfo.os || autoInfo.os;
const osv = customInfo.osVersion || autoInfo.osVersion;
const device = customInfo.device || autoInfo.device;
const arch = customInfo.arch || autoInfo.arch;
const app = customInfo.app || autoInfo.app;
const appv = customInfo.appVersion || autoInfo.appVersion;
const runtime = customInfo.runtime || autoInfo.runtime;
const rtv = customInfo.runtimeVersion || autoInfo.runtimeVersion;
const tz = customInfo.timezone || autoInfo.timezone;
const parts = [
`v=${v}`,
`os=${os}` + (osv ? `;osv=${osv}` : ''),
`device=${device}`,
arch ? `arch=${arch}` : '',
app ? `app=${app}${appv ? `;appv=${appv}` : ''}` : '',
`runtime=${runtime}` + (rtv ? `;rtv=${rtv}` : ''),
tz ? `tz=${tz}` : ''
];
return parts.filter(Boolean).join('|');
}
function getAutoInfo() {
const info = {
os: 'unknown',
osVersion: '',
device: 'desktop', // default
arch: '',
app: 'sdk',
appVersion: '0.1.0',
runtime: 'unknown',
runtimeVersion: '',
timezone: ''
};
try {
if (typeof Intl !== 'undefined') {
info.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
}
if (typeof process !== 'undefined' && process.version) {
info.runtime = 'node';
info.runtimeVersion = process.version;
info.os = process.platform;
info.arch = process.arch;
info.device = 'server';
} else if (typeof window !== 'undefined' && window.navigator) {
const ua = window.navigator.userAgent.toLowerCase();
// Basic OS detection
if (ua.includes('windows')) info.os = 'windows';
else if (ua.includes('mac')) info.os = 'macos';
else if (ua.includes('linux')) info.os = 'linux';
else if (ua.includes('android')) info.os = 'android';
else if (ua.includes('ios') || ua.includes('iphone') || ua.includes('ipad')) info.os = 'ios';
// Basic Device Type
if (/mobi|android|touch|mini/i.test(ua)) info.device = 'mobile';
if (/tablet|ipad/i.test(ua)) info.device = 'tablet';
// Basic Runtime (Browser)
if (ua.includes('firefox')) info.runtime = 'firefox';
else if (ua.includes('edg/')) info.runtime = 'edge';
else if (ua.includes('chrome')) info.runtime = 'chrome';
else if (ua.includes('safari')) info.runtime = 'safari';
}
} catch (e) {
// Ignore context extraction errors
}
return info;
}
/**
* Lightweight EventEmitter for TenxyteClient.
* Provides `.on`, `.once`, `.off`, and `.emit`.
*/
export class EventEmitter<Events extends Record<string, any>> {
private events: Map<keyof Events, Array<Function>>;
constructor() {
this.events = new Map();
}
/**
* Subscribe to an event.
* @param event The event name
* @param callback The callback function
* @returns Unsubscribe function
*/
on<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): () => void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(callback);
return () => this.off(event, callback);
}
/**
* Unsubscribe from an event.
* @param event The event name
* @param callback The exact callback function that was passed to .on()
*/
off<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): void {
const callbacks = this.events.get(event);
if (!callbacks) return;
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
/**
* Subscribe to an event exactly once.
*/
once<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): () => void {
const wrapped = (payload: Events[K]) => {
this.off(event, wrapped);
callback(payload);
};
return this.on(event, wrapped);
}
/**
* Emit an event internally.
*/
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
const callbacks = this.events.get(event);
if (!callbacks) return;
// Copy array to prevent mutation issues during emission
const copy = [...callbacks];
for (const callback of copy) {
try {
callback(payload);
} catch (err) {
console.error(`[Tenxyte EventEmitter] Error executing callback for event ${String(event)}`, err);
}
}
}
removeAllListeners(): void {
this.events.clear();
}
}
export interface DecodedTenxyteToken {
exp?: number;
iat?: number;
sub?: string;
roles?: string[];
permissions?: string[];
[key: string]: any;
}
/**
* Decodes the payload of a JWT without verifying the signature.
* Suitable for client-side routing and UI state.
*/
export function decodeJwt(token: string): DecodedTenxyteToken | null {
try {
const parts = token.split('.');
if (parts.length !== 3) {
return null;
}
let base64Url = parts[1];
if (!base64Url) return null;
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
// Pad with standard base64 padding
while (base64.length % 4) {
base64 += '=';
}
const isBrowser = typeof window !== 'undefined' && typeof window.atob === 'function';
let jsonPayload: string;
if (isBrowser) {
// Browser decode
jsonPayload = decodeURIComponent(
window.atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
} else {
// Node.js decode
jsonPayload = Buffer.from(base64, 'base64').toString('utf8');
}
return JSON.parse(jsonPayload);
} catch (e) {
return null;
}
}
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TenxyteHttpClient } from '../src/http/client';
import { MemoryStorage } from '../src/storage';
import { createAuthInterceptor, createRefreshInterceptor } from '../src/http/interceptors';
// Mock global fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe('TenxyteHttpClient', () => {
let client: TenxyteHttpClient;
let storage: MemoryStorage;
beforeEach(() => {
mockFetch.mockReset();
storage = new MemoryStorage();
client = new TenxyteHttpClient({ baseUrl: 'https://api.tenxyte.com/v1' });
});
describe('Core HTTP', () => {
it('should format URL correctly and apply default headers', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'application/json' }),
json: () => Promise.resolve({ data: 'ok' })
} as any);
const res = await client.get<{ data: string }>('/users', { params: { limit: 10 } });
expect(res.data).toBe('ok');
expect(mockFetch).toHaveBeenCalledWith('https://api.tenxyte.com/v1/users?limit=10', expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
}));
});
it('should throw normalized TenxyteError on failure', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
headers: new Headers({ 'content-type': 'application/json' }),
json: () => Promise.resolve({ error: 'Bad data', code: 'INVALID_CREDENTIALS', details: { email: ['invalid'] } })
} as any);
await expect(client.post('/auth/login', { bad: true })).rejects.toMatchObject({
error: 'Bad data',
code: 'INVALID_CREDENTIALS',
details: { email: ['invalid'] }
});
});
it('should handle 204 No Content correctly without parsing JSON', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 204,
headers: new Headers()
} as any);
const res = await client.delete('/users/1');
expect(res).toEqual({});
});
});
describe('Interceptors', () => {
it('should inject Authorization and Context headers', async () => {
storage.setItem('tx_access', 'jwt.token.123');
const authInterceptor = createAuthInterceptor(storage, { activeOrgSlug: 'tenxyte-labs', agentTraceId: 'trc_999' });
client.addRequestInterceptor(authInterceptor);
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'application/json' }),
json: () => Promise.resolve({ ok: true })
} as any);
await client.get('/me');
expect(mockFetch).toHaveBeenCalledWith('https://api.tenxyte.com/v1/me', expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer jwt.token.123',
'X-Org-Slug': 'tenxyte-labs',
'X-Prompt-Trace-ID': 'trc_999'
})
}));
});
it('should seamlessly refresh token on 401', async () => {
storage.setItem('tx_access', 'expired.token');
storage.setItem('tx_refresh', 'valid.refresh.token');
const onSessionExpired = vi.fn();
const authInterceptor = createAuthInterceptor(storage, { activeOrgSlug: null, agentTraceId: null });
const refreshInterceptor = createRefreshInterceptor(client, storage, onSessionExpired);
client.addRequestInterceptor(authInterceptor);
client.addResponseInterceptor(refreshInterceptor);
// 1. Initial request gets 401
// 2. Interceptor triggers refresh (/auth/refresh/) returning 200 with new token
// 3. Interceptor retries initial request with new token, returning 200
mockFetch
// First fail call '/me' with 401
.mockResolvedValueOnce({
ok: false,
status: 401,
headers: new Headers(),
json: () => Promise.resolve({ error: 'Token expired', code: 'TOKEN_EXPIRED' })
} as any)
// Refresh call '/auth/refresh/' succeeds
.mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'application/json' }),
json: () => Promise.resolve({ access: 'new.access', refresh: 'new.refresh' })
} as any)
// Retry call '/me' succeeds
.mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'application/json' }),
json: () => Promise.resolve({ id: 1, name: 'Bob' })
} as any);
const res = await client.get('/me');
expect(res).toEqual({ id: 1, name: 'Bob' });
expect(storage.getItem('tx_access')).toBe('new.access');
expect(storage.getItem('tx_refresh')).toBe('new.refresh');
expect(mockFetch).toHaveBeenCalledTimes(3);
// Verify the retry request had the NEW token!
expect(mockFetch).toHaveBeenNthCalledWith(3, 'https://api.tenxyte.com/v1/me', expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer new.access'
})
}));
});
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AuthModule } from '../../src/modules/auth';
import { TenxyteHttpClient } from '../../src/http/client';
describe('AuthModule', () => {
let client: TenxyteHttpClient;
let auth: AuthModule;
beforeEach(() => {
client = new TenxyteHttpClient({ baseUrl: 'http://localhost:8000' });
auth = new AuthModule(client);
// Mock the underlying request method
vi.spyOn(client, 'request').mockImplementation(async () => {
return {};
});
});
it('loginWithEmail should POST to /api/v1/auth/login/email/', async () => {
const mockResponse = { access_token: 'acc', refresh_token: 'ref', token_type: 'Bearer', expires_in: 3600, device_summary: null };
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
const data = { email: 'test@example.com', password: 'password123' };
const result = await auth.loginWithEmail(data);
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/login/email/', {
method: 'POST',
body: data,
});
expect(result).toEqual(mockResponse);
});
it('logout should POST to /api/v1/auth/logout/ with refresh_token', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
await auth.logout('some_refresh_token');
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/logout/', {
method: 'POST',
body: { refresh_token: 'some_refresh_token' },
});
});
it('requestMagicLink should POST to /api/v1/auth/magic-link/request/', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
const data = { email: 'magic@example.com' };
await auth.requestMagicLink(data);
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/magic-link/request/', {
method: 'POST',
body: data,
});
});
it('verifyMagicLink should GET from /api/v1/auth/magic-link/verify/ with token in query', async () => {
const mockResponse = { access_token: 'acc' };
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
const result = await auth.verifyMagicLink('magic_token_123');
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/magic-link/verify/', {
method: 'GET',
params: { token: 'magic_token_123' },
});
expect(result).toEqual(mockResponse);
});
it('loginWithSocial should POST to /api/v1/auth/social/:provider/', async () => {
const mockResponse = { access_token: 'acc' };
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
const data = { access_token: 'google_token' };
const result = await auth.loginWithSocial('google', data);
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/social/google/', {
method: 'POST',
body: data,
});
expect(result).toEqual(mockResponse);
});
it('handleSocialCallback should GET from callback endpoint with correct params', async () => {
vi.mocked(client.request).mockResolvedValueOnce({ access_token: 'acc' });
await auth.handleSocialCallback('github', 'auth_code', 'http://localhost/callback');
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/social/github/callback/', {
method: 'GET',
params: { code: 'auth_code', redirect_uri: 'http://localhost/callback' },
});
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { RbacModule } from '../../src/modules/rbac';
import { TenxyteHttpClient } from '../../src/http/client';
// Helper to create a dummy JWT for testing
function createDummyJwt(payload: any) {
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64');
return `header.${encodedPayload}.signature`;
}
describe('RbacModule', () => {
let client: TenxyteHttpClient;
let rbac: RbacModule;
beforeEach(() => {
client = new TenxyteHttpClient({ baseUrl: 'http://localhost:8000' });
rbac = new RbacModule(client);
vi.spyOn(client, 'request').mockImplementation(async () => {
return {};
});
});
describe('Synchronous Decoding & Checks', () => {
const token = createDummyJwt({
roles: ['admin', 'manager'],
permissions: ['users.view', 'users.edit']
});
it('hasRole should return true if role exists', () => {
expect(rbac.hasRole('admin', token)).toBe(true);
expect(rbac.hasRole('user', token)).toBe(false);
});
it('hasAnyRole should return true if any role exists', () => {
expect(rbac.hasAnyRole(['admin', 'user'], token)).toBe(true);
expect(rbac.hasAnyRole(['user', 'guest'], token)).toBe(false);
});
it('hasAllRoles should return true if all roles exist', () => {
expect(rbac.hasAllRoles(['admin', 'manager'], token)).toBe(true);
expect(rbac.hasAllRoles(['admin', 'user'], token)).toBe(false);
});
it('hasPermission should return true if permission exists', () => {
expect(rbac.hasPermission('users.view', token)).toBe(true);
expect(rbac.hasPermission('users.delete', token)).toBe(false);
});
it('setToken should cache the token for parameter-less calls', () => {
rbac.setToken(token);
expect(rbac.hasRole('admin')).toBe(true);
expect(rbac.hasPermission('users.edit')).toBe(true);
rbac.setToken(null);
expect(rbac.hasRole('admin')).toBe(false);
});
});
describe('Roles CRUD & Permissions', () => {
it('listRoles should GET /api/v1/auth/roles/', async () => {
vi.mocked(client.request).mockResolvedValueOnce([{ id: '1', name: 'admin' }]);
const result = await rbac.listRoles();
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/roles/', { method: 'GET' });
expect(result.length).toBe(1);
});
it('createRole should POST /api/v1/auth/roles/', async () => {
vi.mocked(client.request).mockResolvedValueOnce({ id: '2', name: 'newRole' });
const data = { name: 'newRole', permission_codes: ['a.b'] };
await rbac.createRole(data);
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/roles/', {
method: 'POST',
body: data,
});
});
it('assignRoleToUser should POST to users/{id}/roles/', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
await rbac.assignRoleToUser('user123', 'admin');
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/users/user123/roles/', {
method: 'POST',
body: { role_code: 'admin' },
});
});
it('removePermissionsFromRole should DELETE with body payload if config allows', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
await rbac.removePermissionsFromRole('role123', ['users.view']);
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/roles/role123/permissions/', {
method: 'DELETE',
body: { permission_codes: ['users.view'] },
} as any);
});
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SecurityModule } from '../../src/modules/security';
import { TenxyteHttpClient } from '../../src/http/client';
describe('SecurityModule', () => {
let client: TenxyteHttpClient;
let security: SecurityModule;
beforeEach(() => {
client = new TenxyteHttpClient({ baseUrl: 'http://localhost:8000' });
security = new SecurityModule(client);
vi.spyOn(client, 'request').mockImplementation(async () => {
return {};
});
});
it('requestOtp should POST to /api/v1/auth/otp/request/', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
await security.requestOtp('email');
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/otp/request/', {
method: 'POST',
body: { type: 'email_verification' },
});
});
it('setup2FA should POST to /api/v1/auth/2fa/setup/', async () => {
const mockResponse = { secret: 'secret_key', qr_code_url: 'url', backup_codes: ['123'] };
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
const result = await security.setup2FA();
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/2fa/setup/', {
method: 'POST',
body: undefined
});
expect(result).toEqual(mockResponse);
});
it('confirm2FA should POST to /api/v1/auth/2fa/confirm/', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
await security.confirm2FA('123456');
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/2fa/confirm/', {
method: 'POST',
body: { totp_code: '123456' },
});
});
it('resetPasswordRequest should POST to /api/v1/auth/password/reset/request/', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
await security.resetPasswordRequest({ email: 'hello@example.com' });
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/password/reset/request/', {
method: 'POST',
body: { email: 'hello@example.com' },
});
});
it('authenticateWebAuthn should POST to /api/v1/auth/webauthn/authenticate/begin/', async () => {
const mockResponse = { publicKey: {} };
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
// We mock navigator to stop the promise from throwing regarding missing credentials API
vi.stubGlobal('navigator', {
credentials: {
get: vi.fn(),
}
});
try {
await security.authenticateWebAuthn('user@example.com');
} catch (e) { /* expected to throw because navigator mock returns undefined */ }
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/webauthn/authenticate/begin/', {
method: 'POST',
body: { email: 'user@example.com' },
});
vi.unstubAllGlobals();
});
it('deleteWebAuthnCredential should DELETE /api/v1/auth/webauthn/credentials/{credentialId}/', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
await security.deleteWebAuthnCredential(123);
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/webauthn/credentials/123/', {
method: 'DELETE',
});
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserModule } from '../../src/modules/user';
import { TenxyteHttpClient } from '../../src/http/client';
describe('UserModule', () => {
let client: TenxyteHttpClient;
let user: UserModule;
beforeEach(() => {
client = new TenxyteHttpClient({ baseUrl: 'http://localhost:8000' });
user = new UserModule(client);
vi.spyOn(client, 'request').mockImplementation(async () => {
return {};
});
});
// --- Profile ---
it('getProfile should GET /api/v1/auth/me/', async () => {
vi.mocked(client.request).mockResolvedValueOnce({ id: 'me' });
const result = await user.getProfile();
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/me/', { method: 'GET' });
expect(result.id).toBe('me');
});
it('updateProfile should PATCH /api/v1/auth/me/', async () => {
vi.mocked(client.request).mockResolvedValueOnce({ id: 'me' });
const data = { first_name: 'John' };
await user.updateProfile(data);
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/me/', {
method: 'PATCH',
body: data,
});
});
it('uploadAvatar should PATCH /api/v1/auth/me/ with FormData', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
// Mock global FormData if not in purely browser environment
const FormDataMock = class { append() { } };
const formData = new FormDataMock() as any;
await user.uploadAvatar(formData);
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/me/', {
method: 'PATCH',
body: formData,
});
});
it('deleteAccount should POST to /api/v1/auth/request-account-deletion/', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
await user.deleteAccount('mypassword', '123456');
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/request-account-deletion/', {
method: 'POST',
body: { password: 'mypassword', otp_code: '123456' },
});
});
// --- Admin Actions ---
it('listUsers should GET /api/v1/auth/admin/users/', async () => {
vi.mocked(client.request).mockResolvedValueOnce([]);
await user.listUsers({ page: 1 });
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/admin/users/', {
method: 'GET',
params: { page: 1 },
});
});
it('banUser should POST /api/v1/auth/admin/users/{id}/ban/', async () => {
vi.mocked(client.request).mockResolvedValueOnce(undefined);
await user.banUser('user-1', 'spam');
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/admin/users/user-1/ban/', {
method: 'POST',
body: { reason: 'spam' },
});
});
});
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { MemoryStorage, LocalStorage } from '../src/storage';
describe('Storage Abstractions', () => {
describe('MemoryStorage', () => {
let storage: MemoryStorage;
beforeEach(() => {
storage = new MemoryStorage();
});
it('should set and get items', () => {
storage.setItem('key1', 'value1');
expect(storage.getItem('key1')).toBe('value1');
});
it('should return null for non-existent keys', () => {
expect(storage.getItem('key2')).toBeNull();
});
it('should remove items', () => {
storage.setItem('key1', 'value1');
storage.removeItem('key1');
expect(storage.getItem('key1')).toBeNull();
});
it('should clear all items', () => {
storage.setItem('key1', 'value1');
storage.setItem('key2', 'value2');
storage.clear();
expect(storage.getItem('key1')).toBeNull();
expect(storage.getItem('key2')).toBeNull();
});
});
describe('LocalStorage', () => {
let storage: LocalStorage;
beforeEach(() => {
// Mock window.localStorage for node environment
const localStorageMock = (() => {
let store: Record<string, string> = {};
return {
getItem: vi.fn((key: string) => store[key] || null),
setItem: vi.fn((key: string, value: string) => {
if (key === 'fallback_key') throw new Error('Quota'); // For fallback test
store[key] = value.toString();
}),
removeItem: vi.fn((key: string) => {
delete store[key];
}),
clear: vi.fn(() => {
store = {};
}),
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
writable: true,
});
storage = new LocalStorage();
});
it('should set and get items in browser localStorage', () => {
storage.setItem('tx_test', 'value123');
expect(storage.getItem('tx_test')).toBe('value123');
expect(localStorage.getItem('tx_test')).toBe('value123');
});
it('should return null for non-existent keys', () => {
expect(storage.getItem('missing')).toBeNull();
});
it('should degrade to MemoryStorage if window.localStorage throws error', () => {
// The mock throws an error for 'fallback_key' specifically, but 'new LocalStorage()' checking availability passes.
// Wait, checkAvailability() tries to set '__tenxyte_test__'.
// To test constructor degradation, we can mock localStorage globally to throw on everything during setup:
Object.defineProperty(window, 'localStorage', {
value: {
setItem: () => { throw new Error('QuotaExceededError'); },
getItem: () => null,
removeItem: () => { },
clear: () => { }
},
writable: true,
});
const strictStorage = new LocalStorage();
strictStorage.setItem('fallback_key', 'test');
expect(strictStorage.getItem('fallback_key')).toBe('test');
});
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { buildDeviceInfo } from '../src/utils/device_info';
import { EventEmitter } from '../src/utils/events';
describe('Utilities', () => {
describe('buildDeviceInfo', () => {
it('should respect custom properties', () => {
const info = buildDeviceInfo({
os: 'macos',
osVersion: '14.0',
device: 'desktop',
arch: 'arm64',
app: 'tenxyte-test',
appVersion: '2.0.0',
runtime: 'node',
runtimeVersion: 'v20.0.0',
timezone: 'America/New_York'
});
// v=1|os=macos;osv=14.0|device=desktop|arch=arm64|app=tenxyte-test;appv=2.0.0|runtime=node;rtv=v20.0.0|tz=America/New_York
expect(info).toContain('os=macos;osv=14.0');
expect(info).toContain('app=tenxyte-test;appv=2.0.0');
expect(info).toContain('runtime=node;rtv=v20.0.0');
expect(info).toContain('tz=America/New_York');
expect(info).toContain('v=1');
expect(info).toContain('device=desktop');
expect(info).toContain('arch=arm64');
});
it('should generate sensible defaults in absence of custom info', () => {
const info = buildDeviceInfo();
expect(info).toContain('v=1');
// Should have parsed process or window depending on env
});
});
describe('EventEmitter', () => {
type TestEvents = {
'test:event': { data: string };
};
let emitter: EventEmitter<TestEvents>;
beforeEach(() => {
emitter = new EventEmitter();
});
it('should allow emitting and receiving events', () => {
const mockCb = vi.fn();
emitter.on('test:event', mockCb);
emitter.emit('test:event', { data: 'hello' });
expect(mockCb).toHaveBeenCalledWith({ data: 'hello' });
});
it('should allow unsubscribing', () => {
const mockCb = vi.fn();
const unsub = emitter.on('test:event', mockCb);
unsub();
emitter.emit('test:event', { data: 'hello' });
expect(mockCb).not.toHaveBeenCalled();
});
it('should execute "once" subscriptions exactly once', () => {
const mockCb = vi.fn();
emitter.once('test:event', mockCb);
emitter.emit('test:event', { data: 'one' });
emitter.emit('test:event', { data: 'two' });
expect(mockCb).toHaveBeenCalledTimes(1);
expect(mockCb).toHaveBeenCalledWith({ data: 'one' });
});
});
});
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts"
]
}
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
});
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'happy-dom',
},
});

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display