@tenxyte/core 0.1.5 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,150 +0,0 @@
1
- import type { components, paths } from './api-schema';
2
-
3
- export type GeneratedSchema = components['schemas'];
4
-
5
- /**
6
- * Core User Interface exposed by the SDK.
7
- */
8
- export interface TenxyteUser {
9
- id: string; // UUID
10
- email: string | null;
11
- phone_country_code: string | null;
12
- phone_number: string | null;
13
- first_name: string;
14
- last_name: string;
15
- is_email_verified: boolean;
16
- is_phone_verified: boolean;
17
- is_2fa_enabled: boolean;
18
- roles: string[]; // Role codes e.g., ['admin', 'viewer']
19
- permissions: string[]; // Permission codes (direct + inherited)
20
- created_at: string; // ISO 8601
21
- last_login: string | null;
22
- }
23
-
24
- /**
25
- * Standard SDK Token Pair (internal structure normalized by interceptors)
26
- */
27
- export interface TokenPair {
28
- access_token: string; // JWT Bearer
29
- refresh_token: string;
30
- token_type: 'Bearer';
31
- expires_in: number; // Current access_token lifetime in seconds
32
- device_summary: string | null; // e.g., "desktop — windows 11 — chrome 122" (null if device_info absent)
33
- }
34
-
35
- /**
36
- * Standardized API Error Response wrapper
37
- */
38
- export interface TenxyteError {
39
- error: string; // Human message
40
- code: TenxyteErrorCode; // Machine identifier
41
- details?: Record<string, string[]> | string; // Per-field errors or free message
42
- retry_after?: number; // Present on 429 and 423
43
- }
44
-
45
- export type TenxyteErrorCode =
46
- // Auth
47
- | 'LOGIN_FAILED'
48
- | 'INVALID_CREDENTIALS'
49
- | 'ACCOUNT_LOCKED'
50
- | 'ACCOUNT_BANNED'
51
- | '2FA_REQUIRED'
52
- | 'ADMIN_2FA_SETUP_REQUIRED'
53
- | 'TOKEN_EXPIRED'
54
- | 'TOKEN_BLACKLISTED'
55
- | 'REFRESH_FAILED'
56
- | 'PERMISSION_DENIED'
57
- | 'SESSION_LIMIT_EXCEEDED'
58
- | 'DEVICE_LIMIT_EXCEEDED'
59
- | 'RATE_LIMITED'
60
- | 'INVALID_OTP'
61
- | 'OTP_EXPIRED'
62
- | 'INVALID_PROVIDER'
63
- | 'SOCIAL_AUTH_FAILED'
64
- | 'VALIDATION_URL_REQUIRED'
65
- | 'INVALID_TOKEN'
66
- // User / Account
67
- | 'CONFIRMATION_REQUIRED'
68
- | 'PASSWORD_REQUIRED'
69
- | 'INVALID_PASSWORD'
70
- | 'INVALID_DEVICE_INFO'
71
- // B2B / Organizations
72
- | 'ORG_NOT_FOUND'
73
- | 'NOT_ORG_MEMBER'
74
- | 'NOT_OWNER'
75
- | 'ALREADY_MEMBER'
76
- | 'MEMBER_LIMIT_EXCEEDED'
77
- | 'HAS_CHILDREN'
78
- | 'CIRCULAR_HIERARCHY'
79
- | 'LAST_OWNER_REQUIRED'
80
- | 'INVITATION_EXISTS'
81
- | 'INVALID_ROLE'
82
- // AIRS — Agent errors
83
- | 'AGENT_NOT_FOUND'
84
- | 'AGENT_SUSPENDED'
85
- | 'AGENT_REVOKED'
86
- | 'AGENT_EXPIRED'
87
- | 'BUDGET_EXCEEDED'
88
- | 'RATE_LIMIT_EXCEEDED'
89
- | 'HEARTBEAT_MISSING'
90
- | 'AIRS_DISABLED';
91
-
92
- /**
93
- * Organization Structure
94
- */
95
- export interface Organization {
96
- id: number;
97
- name: string;
98
- slug: string;
99
- description: string | null;
100
- metadata: Record<string, unknown> | null;
101
- is_active: boolean;
102
- max_members: number; // 0 = unlimited
103
- member_count: number;
104
- created_at: string;
105
- updated_at: string;
106
- parent: { id: number; name: string; slug: string } | null;
107
- children: Array<{ id: number; name: string; slug: string }>;
108
- user_role: string | null; // Current user's role in this org
109
- user_permissions: string[]; // Effective permissions in this org
110
- }
111
-
112
- /**
113
- * Base Pagination Response wrapper
114
- */
115
- export interface PaginatedResponse<T> {
116
- count: number;
117
- page: number;
118
- page_size: number;
119
- total_pages: number;
120
- next: string | null;
121
- previous: string | null;
122
- results: T[];
123
- }
124
-
125
- /**
126
- * AIRS Agent Token metadata
127
- */
128
- export interface AgentTokenSummary {
129
- id: number;
130
- agent_id: string;
131
- status: 'ACTIVE' | 'SUSPENDED' | 'REVOKED' | 'EXPIRED';
132
- expires_at: string;
133
- created_at: string;
134
- organization: string | null; // Org slug, or null
135
- current_request_count: number;
136
- }
137
-
138
- /**
139
- * Request awaiting Human-In-The-Loop approval
140
- */
141
- export interface AgentPendingAction {
142
- id: number;
143
- agent_id: string;
144
- permission: string; // e.g., "users.delete"
145
- endpoint: string;
146
- payload: unknown;
147
- confirmation_token: string;
148
- expires_at: string;
149
- created_at: string;
150
- }
@@ -1,94 +0,0 @@
1
- /**
2
- * Helper utility to build the device fingerprint required by Tenxyte security features.
3
- * Format: `v=1|os=windows;osv=11|device=desktop|arch=x64|app=tenxyte;appv=1.0.0|runtime=chrome;rtv=122|tz=Europe/Paris`
4
- */
5
- export interface CustomDeviceInfo {
6
- os?: string;
7
- osVersion?: string;
8
- device?: string;
9
- arch?: string;
10
- app?: string;
11
- appVersion?: string;
12
- runtime?: string;
13
- runtimeVersion?: string;
14
- timezone?: string;
15
- }
16
-
17
- export function buildDeviceInfo(customInfo: CustomDeviceInfo = {}): string {
18
- // Try to determine automatically from navigator
19
- const autoInfo = getAutoInfo();
20
-
21
- const v = '1';
22
- const os = customInfo.os || autoInfo.os;
23
- const osv = customInfo.osVersion || autoInfo.osVersion;
24
- const device = customInfo.device || autoInfo.device;
25
- const arch = customInfo.arch || autoInfo.arch;
26
- const app = customInfo.app || autoInfo.app;
27
- const appv = customInfo.appVersion || autoInfo.appVersion;
28
- const runtime = customInfo.runtime || autoInfo.runtime;
29
- const rtv = customInfo.runtimeVersion || autoInfo.runtimeVersion;
30
- const tz = customInfo.timezone || autoInfo.timezone;
31
-
32
- const parts = [
33
- `v=${v}`,
34
- `os=${os}` + (osv ? `;osv=${osv}` : ''),
35
- `device=${device}`,
36
- arch ? `arch=${arch}` : '',
37
- app ? `app=${app}${appv ? `;appv=${appv}` : ''}` : '',
38
- `runtime=${runtime}` + (rtv ? `;rtv=${rtv}` : ''),
39
- tz ? `tz=${tz}` : ''
40
- ];
41
-
42
- return parts.filter(Boolean).join('|');
43
- }
44
-
45
- function getAutoInfo() {
46
- const info = {
47
- os: 'unknown',
48
- osVersion: '',
49
- device: 'desktop', // default
50
- arch: '',
51
- app: 'sdk',
52
- appVersion: '0.1.0',
53
- runtime: 'unknown',
54
- runtimeVersion: '',
55
- timezone: ''
56
- };
57
-
58
- try {
59
- if (typeof Intl !== 'undefined') {
60
- info.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
61
- }
62
-
63
- if (typeof process !== 'undefined' && process.version) {
64
- info.runtime = 'node';
65
- info.runtimeVersion = process.version;
66
- info.os = process.platform;
67
- info.arch = process.arch;
68
- info.device = 'server';
69
- } else if (typeof window !== 'undefined' && window.navigator) {
70
- const ua = window.navigator.userAgent.toLowerCase();
71
-
72
- // Basic OS detection
73
- if (ua.includes('windows')) info.os = 'windows';
74
- else if (ua.includes('mac')) info.os = 'macos';
75
- else if (ua.includes('linux')) info.os = 'linux';
76
- else if (ua.includes('android')) info.os = 'android';
77
- else if (ua.includes('ios') || ua.includes('iphone') || ua.includes('ipad')) info.os = 'ios';
78
-
79
- // Basic Device Type
80
- if (/mobi|android|touch|mini/i.test(ua)) info.device = 'mobile';
81
- if (/tablet|ipad/i.test(ua)) info.device = 'tablet';
82
-
83
- // Basic Runtime (Browser)
84
- if (ua.includes('firefox')) info.runtime = 'firefox';
85
- else if (ua.includes('edg/')) info.runtime = 'edge';
86
- else if (ua.includes('chrome')) info.runtime = 'chrome';
87
- else if (ua.includes('safari')) info.runtime = 'safari';
88
- }
89
- } catch (e) {
90
- // Ignore context extraction errors
91
- }
92
-
93
- return info;
94
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * Lightweight EventEmitter for TenxyteClient.
3
- * Provides `.on`, `.once`, `.off`, and `.emit`.
4
- */
5
- export class EventEmitter<Events extends Record<string, any>> {
6
- private events: Map<keyof Events, Array<Function>>;
7
-
8
- constructor() {
9
- this.events = new Map();
10
- }
11
-
12
- /**
13
- * Subscribe to an event.
14
- * @param event The event name
15
- * @param callback The callback function
16
- * @returns Unsubscribe function
17
- */
18
- on<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): () => void {
19
- if (!this.events.has(event)) {
20
- this.events.set(event, []);
21
- }
22
- this.events.get(event)!.push(callback);
23
- return () => this.off(event, callback);
24
- }
25
-
26
- /**
27
- * Unsubscribe from an event.
28
- * @param event The event name
29
- * @param callback The exact callback function that was passed to .on()
30
- */
31
- off<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): void {
32
- const callbacks = this.events.get(event);
33
- if (!callbacks) return;
34
- const index = callbacks.indexOf(callback);
35
- if (index !== -1) {
36
- callbacks.splice(index, 1);
37
- }
38
- }
39
-
40
- /**
41
- * Subscribe to an event exactly once.
42
- */
43
- once<K extends keyof Events>(event: K, callback: (payload: Events[K]) => void): () => void {
44
- const wrapped = (payload: Events[K]) => {
45
- this.off(event, wrapped);
46
- callback(payload);
47
- };
48
- return this.on(event, wrapped);
49
- }
50
-
51
- /**
52
- * Emit an event internally.
53
- */
54
- emit<K extends keyof Events>(event: K, payload: Events[K]): void {
55
- const callbacks = this.events.get(event);
56
- if (!callbacks) return;
57
- // Copy array to prevent mutation issues during emission
58
- const copy = [...callbacks];
59
- for (const callback of copy) {
60
- try {
61
- callback(payload);
62
- } catch (err) {
63
- console.error(`[Tenxyte EventEmitter] Error executing callback for event ${String(event)}`, err);
64
- }
65
- }
66
- }
67
-
68
- removeAllListeners(): void {
69
- this.events.clear();
70
- }
71
- }
package/src/utils/jwt.ts DELETED
@@ -1,51 +0,0 @@
1
- export interface DecodedTenxyteToken {
2
- exp?: number;
3
- iat?: number;
4
- sub?: string;
5
- roles?: string[];
6
- permissions?: string[];
7
- [key: string]: any;
8
- }
9
-
10
- /**
11
- * Decodes the payload of a JWT without verifying the signature.
12
- * Suitable for client-side routing and UI state.
13
- */
14
- export function decodeJwt(token: string): DecodedTenxyteToken | null {
15
- try {
16
- const parts = token.split('.');
17
- if (parts.length !== 3) {
18
- return null;
19
- }
20
-
21
- let base64Url = parts[1];
22
- if (!base64Url) return null;
23
-
24
- let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
25
-
26
- // Pad with standard base64 padding
27
- while (base64.length % 4) {
28
- base64 += '=';
29
- }
30
-
31
- const isBrowser = typeof window !== 'undefined' && typeof window.atob === 'function';
32
- let jsonPayload: string;
33
-
34
- if (isBrowser) {
35
- // Browser decode
36
- jsonPayload = decodeURIComponent(
37
- window.atob(base64)
38
- .split('')
39
- .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
40
- .join('')
41
- );
42
- } else {
43
- // Node.js decode
44
- jsonPayload = Buffer.from(base64, 'base64').toString('utf8');
45
- }
46
-
47
- return JSON.parse(jsonPayload);
48
- } catch (e) {
49
- return null;
50
- }
51
- }
@@ -1,144 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { TenxyteHttpClient } from '../src/http/client';
3
- import { MemoryStorage } from '../src/storage';
4
- import { createAuthInterceptor, createRefreshInterceptor } from '../src/http/interceptors';
5
-
6
- // Mock global fetch
7
- const mockFetch = vi.fn();
8
- global.fetch = mockFetch;
9
-
10
- describe('TenxyteHttpClient', () => {
11
- let client: TenxyteHttpClient;
12
- let storage: MemoryStorage;
13
-
14
- beforeEach(() => {
15
- mockFetch.mockReset();
16
- storage = new MemoryStorage();
17
- client = new TenxyteHttpClient({ baseUrl: 'https://api.tenxyte.com/v1' });
18
- });
19
-
20
- describe('Core HTTP', () => {
21
- it('should format URL correctly and apply default headers', async () => {
22
- mockFetch.mockResolvedValueOnce({
23
- ok: true,
24
- status: 200,
25
- headers: new Headers({ 'content-type': 'application/json' }),
26
- json: () => Promise.resolve({ data: 'ok' })
27
- } as any);
28
-
29
- const res = await client.get<{ data: string }>('/users', { params: { limit: 10 } });
30
-
31
- expect(res.data).toBe('ok');
32
- expect(mockFetch).toHaveBeenCalledWith('https://api.tenxyte.com/v1/users?limit=10', expect.objectContaining({
33
- method: 'GET',
34
- headers: expect.objectContaining({
35
- 'Content-Type': 'application/json',
36
- 'Accept': 'application/json'
37
- })
38
- }));
39
- });
40
-
41
- it('should throw normalized TenxyteError on failure', async () => {
42
- mockFetch.mockResolvedValueOnce({
43
- ok: false,
44
- status: 400,
45
- headers: new Headers({ 'content-type': 'application/json' }),
46
- json: () => Promise.resolve({ error: 'Bad data', code: 'INVALID_CREDENTIALS', details: { email: ['invalid'] } })
47
- } as any);
48
-
49
- await expect(client.post('/auth/login', { bad: true })).rejects.toMatchObject({
50
- error: 'Bad data',
51
- code: 'INVALID_CREDENTIALS',
52
- details: { email: ['invalid'] }
53
- });
54
- });
55
-
56
- it('should handle 204 No Content correctly without parsing JSON', async () => {
57
- mockFetch.mockResolvedValueOnce({
58
- ok: true,
59
- status: 204,
60
- headers: new Headers()
61
- } as any);
62
-
63
- const res = await client.delete('/users/1');
64
- expect(res).toEqual({});
65
- });
66
- });
67
-
68
- describe('Interceptors', () => {
69
- it('should inject Authorization and Context headers', async () => {
70
- storage.setItem('tx_access', 'jwt.token.123');
71
- const authInterceptor = createAuthInterceptor(storage, { activeOrgSlug: 'tenxyte-labs', agentTraceId: 'trc_999' });
72
- client.addRequestInterceptor(authInterceptor);
73
-
74
- mockFetch.mockResolvedValueOnce({
75
- ok: true,
76
- status: 200,
77
- headers: new Headers({ 'content-type': 'application/json' }),
78
- json: () => Promise.resolve({ ok: true })
79
- } as any);
80
-
81
- await client.get('/me');
82
- expect(mockFetch).toHaveBeenCalledWith('https://api.tenxyte.com/v1/me', expect.objectContaining({
83
- headers: expect.objectContaining({
84
- Authorization: 'Bearer jwt.token.123',
85
- 'X-Org-Slug': 'tenxyte-labs',
86
- 'X-Prompt-Trace-ID': 'trc_999'
87
- })
88
- }));
89
- });
90
-
91
- it('should seamlessly refresh token on 401', async () => {
92
- storage.setItem('tx_access', 'expired.token');
93
- storage.setItem('tx_refresh', 'valid.refresh.token');
94
-
95
- const onSessionExpired = vi.fn();
96
- const authInterceptor = createAuthInterceptor(storage, { activeOrgSlug: null, agentTraceId: null });
97
- const refreshInterceptor = createRefreshInterceptor(client, storage, onSessionExpired);
98
-
99
- client.addRequestInterceptor(authInterceptor);
100
- client.addResponseInterceptor(refreshInterceptor);
101
-
102
- // 1. Initial request gets 401
103
- // 2. Interceptor triggers refresh (/auth/refresh/) returning 200 with new token
104
- // 3. Interceptor retries initial request with new token, returning 200
105
-
106
- mockFetch
107
- // First fail call '/me' with 401
108
- .mockResolvedValueOnce({
109
- ok: false,
110
- status: 401,
111
- headers: new Headers(),
112
- json: () => Promise.resolve({ error: 'Token expired', code: 'TOKEN_EXPIRED' })
113
- } as any)
114
- // Refresh call '/auth/refresh/' succeeds
115
- .mockResolvedValueOnce({
116
- ok: true,
117
- status: 200,
118
- headers: new Headers({ 'content-type': 'application/json' }),
119
- json: () => Promise.resolve({ access: 'new.access', refresh: 'new.refresh' })
120
- } as any)
121
- // Retry call '/me' succeeds
122
- .mockResolvedValueOnce({
123
- ok: true,
124
- status: 200,
125
- headers: new Headers({ 'content-type': 'application/json' }),
126
- json: () => Promise.resolve({ id: 1, name: 'Bob' })
127
- } as any);
128
-
129
- const res = await client.get('/me');
130
-
131
- expect(res).toEqual({ id: 1, name: 'Bob' });
132
- expect(storage.getItem('tx_access')).toBe('new.access');
133
- expect(storage.getItem('tx_refresh')).toBe('new.refresh');
134
- expect(mockFetch).toHaveBeenCalledTimes(3);
135
-
136
- // Verify the retry request had the NEW token!
137
- expect(mockFetch).toHaveBeenNthCalledWith(3, 'https://api.tenxyte.com/v1/me', expect.objectContaining({
138
- headers: expect.objectContaining({
139
- Authorization: 'Bearer new.access'
140
- })
141
- }));
142
- });
143
- });
144
- });
@@ -1,93 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { AuthModule } from '../../src/modules/auth';
3
- import { TenxyteHttpClient } from '../../src/http/client';
4
-
5
- describe('AuthModule', () => {
6
- let client: TenxyteHttpClient;
7
- let auth: AuthModule;
8
-
9
- beforeEach(() => {
10
- client = new TenxyteHttpClient({ baseUrl: 'http://localhost:8000' });
11
- auth = new AuthModule(client);
12
-
13
- // Mock the underlying request method
14
- vi.spyOn(client, 'request').mockImplementation(async () => {
15
- return {};
16
- });
17
- });
18
-
19
- it('loginWithEmail should POST to /api/v1/auth/login/email/', async () => {
20
- const mockResponse = { access_token: 'acc', refresh_token: 'ref', token_type: 'Bearer', expires_in: 3600, device_summary: null };
21
- vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
22
-
23
- const data = { email: 'test@example.com', password: 'password123' };
24
- const result = await auth.loginWithEmail(data);
25
-
26
- expect(client.request).toHaveBeenCalledWith('/api/v1/auth/login/email/', {
27
- method: 'POST',
28
- body: data,
29
- });
30
- expect(result).toEqual(mockResponse);
31
- });
32
-
33
- it('logout should POST to /api/v1/auth/logout/ with refresh_token', async () => {
34
- vi.mocked(client.request).mockResolvedValueOnce(undefined);
35
-
36
- await auth.logout('some_refresh_token');
37
-
38
- expect(client.request).toHaveBeenCalledWith('/api/v1/auth/logout/', {
39
- method: 'POST',
40
- body: { refresh_token: 'some_refresh_token' },
41
- });
42
- });
43
-
44
- it('requestMagicLink should POST to /api/v1/auth/magic-link/request/', async () => {
45
- vi.mocked(client.request).mockResolvedValueOnce(undefined);
46
-
47
- const data = { email: 'magic@example.com' };
48
- await auth.requestMagicLink(data);
49
-
50
- expect(client.request).toHaveBeenCalledWith('/api/v1/auth/magic-link/request/', {
51
- method: 'POST',
52
- body: data,
53
- });
54
- });
55
-
56
- it('verifyMagicLink should GET from /api/v1/auth/magic-link/verify/ with token in query', async () => {
57
- const mockResponse = { access_token: 'acc' };
58
- vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
59
-
60
- const result = await auth.verifyMagicLink('magic_token_123');
61
-
62
- expect(client.request).toHaveBeenCalledWith('/api/v1/auth/magic-link/verify/', {
63
- method: 'GET',
64
- params: { token: 'magic_token_123' },
65
- });
66
- expect(result).toEqual(mockResponse);
67
- });
68
-
69
- it('loginWithSocial should POST to /api/v1/auth/social/:provider/', async () => {
70
- const mockResponse = { access_token: 'acc' };
71
- vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
72
-
73
- const data = { access_token: 'google_token' };
74
- const result = await auth.loginWithSocial('google', data);
75
-
76
- expect(client.request).toHaveBeenCalledWith('/api/v1/auth/social/google/', {
77
- method: 'POST',
78
- body: data,
79
- });
80
- expect(result).toEqual(mockResponse);
81
- });
82
-
83
- it('handleSocialCallback should GET from callback endpoint with correct params', async () => {
84
- vi.mocked(client.request).mockResolvedValueOnce({ access_token: 'acc' });
85
-
86
- await auth.handleSocialCallback('github', 'auth_code', 'http://localhost/callback');
87
-
88
- expect(client.request).toHaveBeenCalledWith('/api/v1/auth/social/github/callback/', {
89
- method: 'GET',
90
- params: { code: 'auth_code', redirect_uri: 'http://localhost/callback' },
91
- });
92
- });
93
- });