@tenxyte/core 0.9.0 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +362 -102
- package/dist/index.cjs +966 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1073 -15
- package/dist/index.d.ts +1073 -15
- package/dist/index.js +946 -35
- package/dist/index.js.map +1 -1
- package/package.json +15 -3
- package/patched-schema.json +0 -11388
- package/src/client.ts +0 -50
- package/src/config.ts +0 -0
- package/src/http/client.ts +0 -162
- package/src/http/index.ts +0 -1
- package/src/http/interceptors.ts +0 -117
- package/src/index.ts +0 -7
- package/src/modules/ai.ts +0 -178
- package/src/modules/auth.ts +0 -116
- package/src/modules/b2b.ts +0 -177
- package/src/modules/rbac.ts +0 -207
- package/src/modules/security.ts +0 -313
- package/src/modules/user.ts +0 -95
- package/src/storage/cookie.ts +0 -39
- package/src/storage/index.ts +0 -29
- package/src/storage/localStorage.ts +0 -75
- package/src/storage/memory.ts +0 -30
- package/src/types/api-schema.d.ts +0 -6590
- package/src/types/index.ts +0 -152
- package/src/utils/base64url.ts +0 -25
- package/src/utils/device_info.ts +0 -94
- package/src/utils/events.ts +0 -71
- package/src/utils/jwt.ts +0 -51
- package/tests/http.test.ts +0 -144
- package/tests/modules/auth.test.ts +0 -93
- package/tests/modules/rbac.test.ts +0 -95
- package/tests/modules/security.test.ts +0 -85
- package/tests/modules/user.test.ts +0 -76
- package/tests/storage.test.ts +0 -96
- package/tests/utils.test.ts +0 -71
- package/tsconfig.json +0 -26
- package/tsup.config.ts +0 -10
- package/vitest.config.ts +0 -7
package/src/modules/b2b.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { TenxyteHttpClient } from '../http/client';
|
|
2
|
-
import { Organization, PaginatedResponse } from '../types';
|
|
3
|
-
|
|
4
|
-
export interface OrgMembership {
|
|
5
|
-
id: number;
|
|
6
|
-
user_id: number;
|
|
7
|
-
email: string;
|
|
8
|
-
first_name: string;
|
|
9
|
-
last_name: string;
|
|
10
|
-
role: { code: string; name: string };
|
|
11
|
-
joined_at: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface OrgTreeNode {
|
|
15
|
-
id: number;
|
|
16
|
-
name: string;
|
|
17
|
-
slug: string;
|
|
18
|
-
children: OrgTreeNode[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class B2bModule {
|
|
22
|
-
private currentOrgSlug: string | null = null;
|
|
23
|
-
|
|
24
|
-
constructor(private client: TenxyteHttpClient) {
|
|
25
|
-
// Register an interceptor to auto-inject the X-Org-Slug header
|
|
26
|
-
this.client.addRequestInterceptor((config) => {
|
|
27
|
-
if (this.currentOrgSlug) {
|
|
28
|
-
config.headers = {
|
|
29
|
-
...config.headers,
|
|
30
|
-
'X-Org-Slug': this.currentOrgSlug,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
return config;
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ─── Context Management ───
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Set the active Organization context.
|
|
41
|
-
* Subsequent API requests will automatically include the `X-Org-Slug` header.
|
|
42
|
-
* @param slug - The unique string identifier of the organization.
|
|
43
|
-
*/
|
|
44
|
-
switchOrganization(slug: string): void {
|
|
45
|
-
this.currentOrgSlug = slug;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Clear the active Organization context, dropping the `X-Org-Slug` header for standard User operations.
|
|
50
|
-
*/
|
|
51
|
-
clearOrganization(): void {
|
|
52
|
-
this.currentOrgSlug = null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Get the currently active Organization slug context if set. */
|
|
56
|
-
getCurrentOrganizationSlug(): string | null {
|
|
57
|
-
return this.currentOrgSlug;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ─── Organizations CRUD ───
|
|
61
|
-
|
|
62
|
-
/** Create a new top-level or child Organization in the backend. */
|
|
63
|
-
async createOrganization(data: {
|
|
64
|
-
name: string;
|
|
65
|
-
slug?: string;
|
|
66
|
-
description?: string;
|
|
67
|
-
parent_id?: number;
|
|
68
|
-
metadata?: Record<string, unknown>;
|
|
69
|
-
max_members?: number;
|
|
70
|
-
}): Promise<Organization> {
|
|
71
|
-
return this.client.post('/api/v1/auth/organizations/', data);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** List organizations the currently authenticated user belongs to. */
|
|
75
|
-
async listMyOrganizations(params?: {
|
|
76
|
-
search?: string;
|
|
77
|
-
is_active?: boolean;
|
|
78
|
-
parent?: string;
|
|
79
|
-
ordering?: string;
|
|
80
|
-
page?: number;
|
|
81
|
-
page_size?: number;
|
|
82
|
-
}): Promise<PaginatedResponse<Organization>> {
|
|
83
|
-
return this.client.get('/api/v1/auth/organizations/', { params });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Retrieve details about a specific organization by slug. */
|
|
87
|
-
async getOrganization(slug: string): Promise<Organization> {
|
|
88
|
-
return this.client.get(`/api/v1/auth/organizations/${slug}/`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Update configuration and metadata of an Organization. */
|
|
92
|
-
async updateOrganization(slug: string, data: Partial<{
|
|
93
|
-
name: string;
|
|
94
|
-
slug: string;
|
|
95
|
-
description: string;
|
|
96
|
-
parent_id: number | null;
|
|
97
|
-
metadata: Record<string, unknown>;
|
|
98
|
-
max_members: number;
|
|
99
|
-
is_active: boolean;
|
|
100
|
-
}>): Promise<Organization> {
|
|
101
|
-
return this.client.patch(`/api/v1/auth/organizations/${slug}/`, data);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Permanently delete an Organization. */
|
|
105
|
-
async deleteOrganization(slug: string): Promise<{ message: string }> {
|
|
106
|
-
return this.client.delete(`/api/v1/auth/organizations/${slug}/`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Retrieve the topology subtree extending downward from this point. */
|
|
110
|
-
async getOrganizationTree(slug: string): Promise<OrgTreeNode> {
|
|
111
|
-
return this.client.get(`/api/v1/auth/organizations/${slug}/tree/`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ─── Member Management ───
|
|
115
|
-
|
|
116
|
-
/** List users bound to a specific Organization. */
|
|
117
|
-
async listMembers(slug: string, params?: {
|
|
118
|
-
search?: string;
|
|
119
|
-
role?: 'owner' | 'admin' | 'member';
|
|
120
|
-
status?: 'active' | 'inactive' | 'pending';
|
|
121
|
-
ordering?: string;
|
|
122
|
-
page?: number;
|
|
123
|
-
page_size?: number;
|
|
124
|
-
}): Promise<PaginatedResponse<OrgMembership>> {
|
|
125
|
-
return this.client.get(`/api/v1/auth/organizations/${slug}/members/`, { params });
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Add a user directly into an Organization with a designated role. */
|
|
129
|
-
async addMember(slug: string, data: {
|
|
130
|
-
user_id: number;
|
|
131
|
-
role_code: string;
|
|
132
|
-
}): Promise<OrgMembership> {
|
|
133
|
-
return this.client.post(`/api/v1/auth/organizations/${slug}/members/`, data);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Evolve or demote an existing member's role within the Organization. */
|
|
137
|
-
async updateMemberRole(slug: string, userId: number, roleCode: string): Promise<OrgMembership> {
|
|
138
|
-
return this.client.patch(`/api/v1/auth/organizations/${slug}/members/${userId}/`, { role_code: roleCode });
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Kick a user out of the Organization. */
|
|
142
|
-
async removeMember(slug: string, userId: number): Promise<{ message: string }> {
|
|
143
|
-
return this.client.delete(`/api/v1/auth/organizations/${slug}/members/${userId}/`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ─── Invitations ───
|
|
147
|
-
|
|
148
|
-
/** Send an onboarding email invitation to join an Organization. */
|
|
149
|
-
async inviteMember(slug: string, data: {
|
|
150
|
-
email: string;
|
|
151
|
-
role_code: string;
|
|
152
|
-
expires_in_days?: number;
|
|
153
|
-
}): Promise<{
|
|
154
|
-
id: number;
|
|
155
|
-
email: string;
|
|
156
|
-
role: string;
|
|
157
|
-
token: string;
|
|
158
|
-
expires_at: string;
|
|
159
|
-
invited_by: { id: number; email: string };
|
|
160
|
-
organization: { id: number; name: string; slug: string };
|
|
161
|
-
}> {
|
|
162
|
-
return this.client.post(`/api/v1/auth/organizations/${slug}/invitations/`, data);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Fetch a definition matrix of what Organization-level roles can be assigned. */
|
|
166
|
-
async listOrgRoles(): Promise<Array<{
|
|
167
|
-
code: string;
|
|
168
|
-
name: string;
|
|
169
|
-
description: string;
|
|
170
|
-
weight: number;
|
|
171
|
-
permissions: Array<{ code: string; name: string; description: string }>;
|
|
172
|
-
is_system_role: boolean;
|
|
173
|
-
created_at: string;
|
|
174
|
-
}>> {
|
|
175
|
-
return this.client.get('/api/v1/auth/organizations/roles/');
|
|
176
|
-
}
|
|
177
|
-
}
|
package/src/modules/rbac.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { TenxyteHttpClient } from '../http/client';
|
|
2
|
-
import { decodeJwt, DecodedTenxyteToken } from '../utils/jwt';
|
|
3
|
-
|
|
4
|
-
export interface Role {
|
|
5
|
-
id: string;
|
|
6
|
-
name: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
is_default?: boolean;
|
|
9
|
-
permissions?: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface Permission {
|
|
13
|
-
id: string;
|
|
14
|
-
code: string;
|
|
15
|
-
name: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class RbacModule {
|
|
20
|
-
private cachedToken: string | null = null;
|
|
21
|
-
|
|
22
|
-
constructor(private client: TenxyteHttpClient) { }
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Cache a decoded JWT payload locally to perform parameter-less synchronous permission checks.
|
|
26
|
-
* Usually invoked automatically by the system upon login or token refresh.
|
|
27
|
-
* @param token - The raw JWT access token encoded string.
|
|
28
|
-
*/
|
|
29
|
-
setToken(token: string | null) {
|
|
30
|
-
this.cachedToken = token;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private getDecodedToken(token?: string): DecodedTenxyteToken | null {
|
|
34
|
-
const t = token || this.cachedToken;
|
|
35
|
-
if (!t) return null;
|
|
36
|
-
return decodeJwt(t);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// --- Synchronous Checks --- //
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Synchronously deeply inspects the cached (or provided) JWT to determine if the user has a specific Role.
|
|
43
|
-
* @param role - The exact code name of the Role.
|
|
44
|
-
* @param token - (Optional) Provide a specific token overriding the cached one.
|
|
45
|
-
*/
|
|
46
|
-
hasRole(role: string, token?: string): boolean {
|
|
47
|
-
const decoded = this.getDecodedToken(token);
|
|
48
|
-
if (!decoded?.roles) return false;
|
|
49
|
-
return decoded.roles.includes(role);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Evaluates if the active session holds AT LEAST ONE of the listed Roles.
|
|
54
|
-
* @param roles - An array of Role codes.
|
|
55
|
-
*/
|
|
56
|
-
hasAnyRole(roles: string[], token?: string): boolean {
|
|
57
|
-
const decoded = this.getDecodedToken(token);
|
|
58
|
-
if (!decoded?.roles) return false;
|
|
59
|
-
return roles.some(r => decoded.roles!.includes(r));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Evaluates if the active session holds ALL of the listed Roles concurrently.
|
|
64
|
-
* @param roles - An array of Role codes.
|
|
65
|
-
*/
|
|
66
|
-
hasAllRoles(roles: string[], token?: string): boolean {
|
|
67
|
-
const decoded = this.getDecodedToken(token);
|
|
68
|
-
if (!decoded?.roles) return false;
|
|
69
|
-
return roles.every(r => decoded.roles!.includes(r));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Synchronously deeply inspects the cached (or provided) JWT to determine if the user has a specific granular Permission.
|
|
74
|
-
* @param permission - The exact code name of the Permission (e.g., 'invoices.read').
|
|
75
|
-
*/
|
|
76
|
-
hasPermission(permission: string, token?: string): boolean {
|
|
77
|
-
const decoded = this.getDecodedToken(token);
|
|
78
|
-
if (!decoded?.permissions) return false;
|
|
79
|
-
return decoded.permissions.includes(permission);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Evaluates if the active session holds AT LEAST ONE of the listed Permissions.
|
|
84
|
-
*/
|
|
85
|
-
hasAnyPermission(permissions: string[], token?: string): boolean {
|
|
86
|
-
const decoded = this.getDecodedToken(token);
|
|
87
|
-
if (!decoded?.permissions) return false;
|
|
88
|
-
return permissions.some(p => decoded.permissions!.includes(p));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Evaluates if the active session holds ALL of the listed Permissions concurrently.
|
|
93
|
-
*/
|
|
94
|
-
hasAllPermissions(permissions: string[], token?: string): boolean {
|
|
95
|
-
const decoded = this.getDecodedToken(token);
|
|
96
|
-
if (!decoded?.permissions) return false;
|
|
97
|
-
return permissions.every(p => decoded.permissions!.includes(p));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// --- Roles CRUD --- //
|
|
101
|
-
|
|
102
|
-
/** Fetch all application global Roles structure */
|
|
103
|
-
async listRoles(): Promise<Role[]> {
|
|
104
|
-
return this.client.get<Role[]>('/api/v1/auth/roles/');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Create a new architectural Role inside Tenxyte */
|
|
108
|
-
async createRole(data: { name: string; description?: string; permission_codes?: string[]; is_default?: boolean }): Promise<Role> {
|
|
109
|
-
return this.client.post<Role>('/api/v1/auth/roles/', data);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Get detailed metadata defining a single bounded Role */
|
|
113
|
-
async getRole(roleId: string): Promise<Role> {
|
|
114
|
-
return this.client.get<Role>(`/api/v1/auth/roles/${roleId}/`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Modify properties bounding a Role */
|
|
118
|
-
async updateRole(roleId: string, data: { name?: string; description?: string; permission_codes?: string[]; is_default?: boolean }): Promise<Role> {
|
|
119
|
-
return this.client.put<Role>(`/api/v1/auth/roles/${roleId}/`, data);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Unbind and destruct a Role from the global Tenant. (Dangerous, implies cascading permission unbindings) */
|
|
123
|
-
async deleteRole(roleId: string): Promise<void> {
|
|
124
|
-
return this.client.delete<void>(`/api/v1/auth/roles/${roleId}/`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// --- Role Permissions Management --- //
|
|
128
|
-
|
|
129
|
-
async getRolePermissions(roleId: string): Promise<Permission[]> {
|
|
130
|
-
return this.client.get<Permission[]>(`/api/v1/auth/roles/${roleId}/permissions/`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async addPermissionsToRole(roleId: string, permission_codes: string[]): Promise<void> {
|
|
134
|
-
return this.client.post<void>(`/api/v1/auth/roles/${roleId}/permissions/`, { permission_codes });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async removePermissionsFromRole(roleId: string, permission_codes: string[]): Promise<void> {
|
|
138
|
-
return this.client.delete<void>(`/api/v1/auth/roles/${roleId}/permissions/`, {
|
|
139
|
-
// Note: DELETE request with body is supported via our fetch wrapper if enabled,
|
|
140
|
-
// or we might need to rely on query strings. The schema specifies body or query.
|
|
141
|
-
// Let's pass it in body via a custom config or URL params.
|
|
142
|
-
body: { permission_codes }
|
|
143
|
-
} as any);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// --- Permissions CRUD --- //
|
|
147
|
-
|
|
148
|
-
/** Enumerates all available fine-grained Permissions inside this Tenant scope. */
|
|
149
|
-
async listPermissions(): Promise<Permission[]> {
|
|
150
|
-
return this.client.get<Permission[]>('/api/v1/auth/permissions/');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Bootstraps a new granular Permission flag (e.g. `billing.refund`). */
|
|
154
|
-
async createPermission(data: { code: string; name: string; description?: string; parent_code?: string }): Promise<Permission> {
|
|
155
|
-
return this.client.post<Permission>('/api/v1/auth/permissions/', data);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Retrieves an existing atomic Permission construct. */
|
|
159
|
-
async getPermission(permissionId: string): Promise<Permission> {
|
|
160
|
-
return this.client.get<Permission>(`/api/v1/auth/permissions/${permissionId}/`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Edits the human readable description or structural dependencies of a Permission. */
|
|
164
|
-
async updatePermission(permissionId: string, data: { name?: string; description?: string }): Promise<Permission> {
|
|
165
|
-
return this.client.put<Permission>(`/api/v1/auth/permissions/${permissionId}/`, data);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Destroys an atomic Permission permanently. Any Roles referencing it will be stripped of this grant automatically. */
|
|
169
|
-
async deletePermission(permissionId: string): Promise<void> {
|
|
170
|
-
return this.client.delete<void>(`/api/v1/auth/permissions/${permissionId}/`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// --- Direct Assignment (Users) --- //
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Attach a given Role globally to a user entity.
|
|
177
|
-
* Use sparingly if B2B multi-tenancy contexts are preferred.
|
|
178
|
-
*/
|
|
179
|
-
async assignRoleToUser(userId: string, roleCode: string): Promise<void> {
|
|
180
|
-
return this.client.post<void>(`/api/v1/auth/users/${userId}/roles/`, { role_code: roleCode });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Unbind a global Role from a user entity.
|
|
185
|
-
*/
|
|
186
|
-
async removeRoleFromUser(userId: string, roleCode: string): Promise<void> {
|
|
187
|
-
return this.client.delete<void>(`/api/v1/auth/users/${userId}/roles/`, {
|
|
188
|
-
params: { role_code: roleCode }
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Ad-Hoc directly attach specific granular Permissions to a single User, bypassing Role boundaries.
|
|
194
|
-
*/
|
|
195
|
-
async assignPermissionsToUser(userId: string, permissionCodes: string[]): Promise<void> {
|
|
196
|
-
return this.client.post<void>(`/api/v1/auth/users/${userId}/permissions/`, { permission_codes: permissionCodes });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Ad-Hoc strip direct granular Permissions bindings from a specific User.
|
|
201
|
-
*/
|
|
202
|
-
async removePermissionsFromUser(userId: string, permissionCodes: string[]): Promise<void> {
|
|
203
|
-
return this.client.delete<void>(`/api/v1/auth/users/${userId}/permissions/`, {
|
|
204
|
-
body: { permission_codes: permissionCodes }
|
|
205
|
-
} as any);
|
|
206
|
-
}
|
|
207
|
-
}
|
package/src/modules/security.ts
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import { TenxyteHttpClient } from '../http/client';
|
|
2
|
-
import { TenxyteUser, TokenPair } from '../types';
|
|
3
|
-
import { base64urlToBuffer, bufferToBase64url } from '../utils/base64url';
|
|
4
|
-
|
|
5
|
-
export class SecurityModule {
|
|
6
|
-
constructor(private client: TenxyteHttpClient) { }
|
|
7
|
-
|
|
8
|
-
// ─── 2FA (TOTP) Management ───
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get the current 2FA status for the authenticated user.
|
|
12
|
-
* @returns Information about whether 2FA is enabled and how many backup codes remain.
|
|
13
|
-
*/
|
|
14
|
-
async get2FAStatus(): Promise<{ is_enabled: boolean; backup_codes_remaining: number }> {
|
|
15
|
-
return this.client.get('/api/v1/auth/2fa/status/');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Start the 2FA enrollment process.
|
|
20
|
-
* @returns The secret key and QR code URL to be scanned by an Authenticator app.
|
|
21
|
-
*/
|
|
22
|
-
async setup2FA(): Promise<{
|
|
23
|
-
message: string;
|
|
24
|
-
secret: string;
|
|
25
|
-
manual_entry_key: string;
|
|
26
|
-
qr_code: string;
|
|
27
|
-
provisioning_uri: string;
|
|
28
|
-
backup_codes: string[];
|
|
29
|
-
warning: string;
|
|
30
|
-
}> {
|
|
31
|
-
return this.client.post('/api/v1/auth/2fa/setup/');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Confirm the 2FA setup by providing the first TOTP code generated by the Authenticator app.
|
|
36
|
-
* @param totpCode - The 6-digit code.
|
|
37
|
-
*/
|
|
38
|
-
async confirm2FA(totpCode: string): Promise<{
|
|
39
|
-
message: string;
|
|
40
|
-
is_enabled: boolean;
|
|
41
|
-
enabled_at: string;
|
|
42
|
-
}> {
|
|
43
|
-
return this.client.post('/api/v1/auth/2fa/confirm/', { totp_code: totpCode });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Disable 2FA for the current user.
|
|
48
|
-
* Usually requires re-authentication or providing the active password/totp code.
|
|
49
|
-
* @param totpCode - The current 6-digit code to verify intent.
|
|
50
|
-
* @param password - (Optional) The user's password if required by backend policy.
|
|
51
|
-
*/
|
|
52
|
-
async disable2FA(totpCode: string, password?: string): Promise<{
|
|
53
|
-
message: string;
|
|
54
|
-
is_enabled: boolean;
|
|
55
|
-
disabled_at: string;
|
|
56
|
-
backup_codes_invalidated: boolean;
|
|
57
|
-
}> {
|
|
58
|
-
return this.client.post('/api/v1/auth/2fa/disable/', { totp_code: totpCode, password });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Invalidate old backup codes and explicitly generate a new batch.
|
|
63
|
-
* @param totpCode - An active TOTP code to verify intent.
|
|
64
|
-
*/
|
|
65
|
-
async regenerateBackupCodes(totpCode: string): Promise<{
|
|
66
|
-
message: string;
|
|
67
|
-
backup_codes: string[];
|
|
68
|
-
codes_count: number;
|
|
69
|
-
generated_at?: string;
|
|
70
|
-
warning: string;
|
|
71
|
-
}> {
|
|
72
|
-
return this.client.post('/api/v1/auth/2fa/backup-codes/', { totp_code: totpCode });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ─── Verification OTP (Email / Phone) ───
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Request an OTP code to be dispatched to the user's primary contact method.
|
|
79
|
-
* @param type - The channel type ('email' or 'phone').
|
|
80
|
-
*/
|
|
81
|
-
async requestOtp(type: 'email' | 'phone'): Promise<{
|
|
82
|
-
message: string;
|
|
83
|
-
otp_id: number;
|
|
84
|
-
expires_at: string;
|
|
85
|
-
channel: 'email' | 'phone';
|
|
86
|
-
masked_recipient: string;
|
|
87
|
-
}> {
|
|
88
|
-
return this.client.post('/api/v1/auth/otp/request/', { type: type === 'email' ? 'email_verification' : 'phone_verification' });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Verify an email confirmation OTP.
|
|
93
|
-
* @param code - The numeric code received via email.
|
|
94
|
-
*/
|
|
95
|
-
async verifyOtpEmail(code: string): Promise<{
|
|
96
|
-
message: string;
|
|
97
|
-
email_verified: boolean;
|
|
98
|
-
verified_at: string;
|
|
99
|
-
}> {
|
|
100
|
-
return this.client.post('/api/v1/auth/otp/verify/email/', { code });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Verify a phone confirmation OTP (SMS dispatch).
|
|
105
|
-
* @param code - The numeric code received via SMS.
|
|
106
|
-
*/
|
|
107
|
-
async verifyOtpPhone(code: string): Promise<{
|
|
108
|
-
message: string;
|
|
109
|
-
phone_verified: boolean;
|
|
110
|
-
verified_at: string;
|
|
111
|
-
phone_number: string;
|
|
112
|
-
}> {
|
|
113
|
-
return this.client.post('/api/v1/auth/otp/verify/phone/', { code });
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ─── Password Sub-module ───
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Triggers a password reset flow, dispatching an OTP to the target.
|
|
120
|
-
* @param target - Either an email address or a phone configuration payload.
|
|
121
|
-
*/
|
|
122
|
-
async resetPasswordRequest(target: { email: string } | { phone_country_code: string; phone_number: string }): Promise<{ message: string }> {
|
|
123
|
-
return this.client.post('/api/v1/auth/password/reset/request/', target);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Confirm a password reset using the OTP dispatched by `resetPasswordRequest`.
|
|
128
|
-
* @param data - The OTP code and the new matching password fields.
|
|
129
|
-
*/
|
|
130
|
-
async resetPasswordConfirm(data: {
|
|
131
|
-
email?: string;
|
|
132
|
-
phone_country_code?: string;
|
|
133
|
-
phone_number?: string;
|
|
134
|
-
otp_code: string;
|
|
135
|
-
new_password: string;
|
|
136
|
-
confirm_password: string;
|
|
137
|
-
}): Promise<{ message: string }> {
|
|
138
|
-
return this.client.post('/api/v1/auth/password/reset/confirm/', data);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Change password for an already authenticated user.
|
|
143
|
-
* @param currentPassword - The existing password to verify intent.
|
|
144
|
-
* @param newPassword - The distinct new password.
|
|
145
|
-
*/
|
|
146
|
-
async changePassword(currentPassword: string, newPassword: string): Promise<{ message: string }> {
|
|
147
|
-
return this.client.post('/api/v1/auth/password/change/', {
|
|
148
|
-
current_password: currentPassword,
|
|
149
|
-
new_password: newPassword,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Evaluate the strength of a potential password against backend policies.
|
|
155
|
-
* @param password - The password string to test.
|
|
156
|
-
* @param email - (Optional) The user's email to ensure the password doesn't contain it.
|
|
157
|
-
*/
|
|
158
|
-
async checkPasswordStrength(password: string, email?: string): Promise<{
|
|
159
|
-
score: number;
|
|
160
|
-
strength: string;
|
|
161
|
-
is_valid: boolean;
|
|
162
|
-
errors: string[];
|
|
163
|
-
requirements: {
|
|
164
|
-
min_length: number;
|
|
165
|
-
require_lowercase: boolean;
|
|
166
|
-
require_uppercase: boolean;
|
|
167
|
-
require_numbers: boolean;
|
|
168
|
-
require_special: boolean;
|
|
169
|
-
};
|
|
170
|
-
}> {
|
|
171
|
-
return this.client.post('/api/v1/auth/password/strength/', { password, email });
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Fetch the password complexity requirements enforced by the Tenxyte backend.
|
|
176
|
-
*/
|
|
177
|
-
async getPasswordRequirements(): Promise<{
|
|
178
|
-
requirements: Record<string, boolean | number>;
|
|
179
|
-
min_length: number;
|
|
180
|
-
max_length: number;
|
|
181
|
-
}> {
|
|
182
|
-
return this.client.get('/api/v1/auth/password/requirements/');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ─── WebAuthn / Passkeys (FIDO2) ───
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Register a new WebAuthn device (Passkey/Biometrics/Security Key) for the authenticated user.
|
|
189
|
-
* Integrates transparently with the browser `navigator.credentials` API.
|
|
190
|
-
* @param deviceName - Optional human-readable name for the device being registered.
|
|
191
|
-
*/
|
|
192
|
-
async registerWebAuthn(deviceName?: string): Promise<{
|
|
193
|
-
message: string;
|
|
194
|
-
credential: {
|
|
195
|
-
id: number;
|
|
196
|
-
device_name: string;
|
|
197
|
-
created_at: string;
|
|
198
|
-
};
|
|
199
|
-
}> {
|
|
200
|
-
// 1. Begin registration
|
|
201
|
-
const optionsResponse = await this.client.post<any>('/api/v1/auth/webauthn/register/begin/');
|
|
202
|
-
|
|
203
|
-
// 2. Map options for the browser
|
|
204
|
-
const publicKeyOpts = optionsResponse.publicKey;
|
|
205
|
-
publicKeyOpts.challenge = base64urlToBuffer(publicKeyOpts.challenge);
|
|
206
|
-
publicKeyOpts.user.id = base64urlToBuffer(publicKeyOpts.user.id);
|
|
207
|
-
if (publicKeyOpts.excludeCredentials) {
|
|
208
|
-
publicKeyOpts.excludeCredentials.forEach((cred: any) => {
|
|
209
|
-
cred.id = base64urlToBuffer(cred.id);
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// 3. Request credential creation from the Authenticator
|
|
214
|
-
const cred = await navigator.credentials.create({ publicKey: publicKeyOpts }) as PublicKeyCredential;
|
|
215
|
-
if (!cred) {
|
|
216
|
-
throw new Error('WebAuthn registration was aborted or failed.');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const response = cred.response as AuthenticatorAttestationResponse;
|
|
220
|
-
|
|
221
|
-
// 4. Complete registration on the backend
|
|
222
|
-
const completionPayload = {
|
|
223
|
-
device_name: deviceName,
|
|
224
|
-
credential: {
|
|
225
|
-
id: cred.id,
|
|
226
|
-
type: cred.type,
|
|
227
|
-
rawId: bufferToBase64url(cred.rawId),
|
|
228
|
-
response: {
|
|
229
|
-
attestationObject: bufferToBase64url(response.attestationObject),
|
|
230
|
-
clientDataJSON: bufferToBase64url(response.clientDataJSON),
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
return this.client.post('/api/v1/auth/webauthn/register/complete/', completionPayload);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Authenticate via WebAuthn (Passkey) without requiring a password.
|
|
240
|
-
* Integrates transparently with the browser `navigator.credentials` API.
|
|
241
|
-
* @param email - The email address identifying the user account (optional if discoverable credentials are used).
|
|
242
|
-
* @returns A session token pair and the user context upon successful cryptographic challenge verification.
|
|
243
|
-
*/
|
|
244
|
-
async authenticateWebAuthn(email?: string): Promise<{
|
|
245
|
-
access: string;
|
|
246
|
-
refresh: string;
|
|
247
|
-
user: TenxyteUser;
|
|
248
|
-
message: string;
|
|
249
|
-
credential_used: string;
|
|
250
|
-
}> {
|
|
251
|
-
// 1. Begin authentication
|
|
252
|
-
const optionsResponse = await this.client.post<any>('/api/v1/auth/webauthn/authenticate/begin/', email ? { email } : {});
|
|
253
|
-
|
|
254
|
-
// 2. Map options for the browser
|
|
255
|
-
const publicKeyOpts = optionsResponse.publicKey;
|
|
256
|
-
publicKeyOpts.challenge = base64urlToBuffer(publicKeyOpts.challenge);
|
|
257
|
-
if (publicKeyOpts.allowCredentials) {
|
|
258
|
-
publicKeyOpts.allowCredentials.forEach((cred: any) => {
|
|
259
|
-
cred.id = base64urlToBuffer(cred.id);
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// 3. Request assertion from the Authenticator
|
|
264
|
-
const cred = await navigator.credentials.get({ publicKey: publicKeyOpts }) as PublicKeyCredential;
|
|
265
|
-
if (!cred) {
|
|
266
|
-
throw new Error('WebAuthn authentication was aborted or failed.');
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const response = cred.response as AuthenticatorAssertionResponse;
|
|
270
|
-
|
|
271
|
-
// 4. Complete authentication on the backend
|
|
272
|
-
const completionPayload = {
|
|
273
|
-
credential: {
|
|
274
|
-
id: cred.id,
|
|
275
|
-
type: cred.type,
|
|
276
|
-
rawId: bufferToBase64url(cred.rawId),
|
|
277
|
-
response: {
|
|
278
|
-
authenticatorData: bufferToBase64url(response.authenticatorData),
|
|
279
|
-
clientDataJSON: bufferToBase64url(response.clientDataJSON),
|
|
280
|
-
signature: bufferToBase64url(response.signature),
|
|
281
|
-
userHandle: response.userHandle ? bufferToBase64url(response.userHandle) : null,
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
return this.client.post('/api/v1/auth/webauthn/authenticate/complete/', completionPayload);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* List all registered WebAuthn credentials for the active user.
|
|
291
|
-
*/
|
|
292
|
-
async listWebAuthnCredentials(): Promise<{
|
|
293
|
-
credentials: Array<{
|
|
294
|
-
id: number;
|
|
295
|
-
device_name: string;
|
|
296
|
-
created_at: string;
|
|
297
|
-
last_used_at: string | null;
|
|
298
|
-
authenticator_type: string;
|
|
299
|
-
is_resident_key: boolean;
|
|
300
|
-
}>;
|
|
301
|
-
count: number;
|
|
302
|
-
}> {
|
|
303
|
-
return this.client.get('/api/v1/auth/webauthn/credentials/');
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Delete a specific WebAuthn credential, removing its capability to sign in.
|
|
308
|
-
* @param credentialId - The internal ID of the credential to delete.
|
|
309
|
-
*/
|
|
310
|
-
async deleteWebAuthnCredential(credentialId: number): Promise<void> {
|
|
311
|
-
return this.client.delete(`/api/v1/auth/webauthn/credentials/${credentialId}/`);
|
|
312
|
-
}
|
|
313
|
-
}
|