@autional-cn/shared 0.1.0-rc
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 +661 -0
- package/package.json +47 -0
- package/src/api/__tests__/entry-plane.test.ts +119 -0
- package/src/api/bff-client.ts +94 -0
- package/src/api/client.ts +174 -0
- package/src/api/entry-plane.ts +61 -0
- package/src/api/trace.ts +21 -0
- package/src/auth/__tests__/ac007-role-source.test.ts +124 -0
- package/src/auth/__tests__/auth-machine.test.ts +157 -0
- package/src/auth/__tests__/build-login-url.test.ts +219 -0
- package/src/auth/__tests__/get-current-role.test.ts +61 -0
- package/src/auth/__tests__/oauth-client-from-slug.test.ts +132 -0
- package/src/auth/__tests__/pkce-uniformity.test.ts +48 -0
- package/src/auth/__tests__/session-client-id.test.ts +124 -0
- package/src/auth/__tests__/slug-from-url.test.ts +112 -0
- package/src/auth/auth-machine.ts +151 -0
- package/src/auth/auth-queries.ts +118 -0
- package/src/auth/build-tenant-urls.ts +48 -0
- package/src/auth/oauth-client-from-slug.ts +119 -0
- package/src/auth/oauth-client-id-store.ts +41 -0
- package/src/auth/oauth-login.ts +291 -0
- package/src/auth/pkce.ts +141 -0
- package/src/auth/portal-config.ts +68 -0
- package/src/auth/roles.ts +102 -0
- package/src/auth/service.ts +368 -0
- package/src/auth/slug-from-url.ts +92 -0
- package/src/auth/store.ts +226 -0
- package/src/auth/tenant-route-middleware.ts +113 -0
- package/src/auth/tenant-slug-context.tsx +9 -0
- package/src/branding/BrandingInitializer.tsx +60 -0
- package/src/branding/brand-color.ts +155 -0
- package/src/branding/branding-cache.ts +71 -0
- package/src/branding/extract-branding.ts +27 -0
- package/src/branding/index.ts +15 -0
- package/src/branding/tenant-store.ts +13 -0
- package/src/branding/types.ts +14 -0
- package/src/branding/useBranding.ts +85 -0
- package/src/components/AuditStatsOnly.tsx +65 -0
- package/src/components/OAuthCallbackPage.tsx +56 -0
- package/src/components/PortalGuard.tsx +105 -0
- package/src/components/RequireAuth.tsx +142 -0
- package/src/components/TenantIndexGuard.tsx +131 -0
- package/src/components/__tests__/RequireAuth.test.tsx +244 -0
- package/src/components/__tests__/TenantIndexGuard.test.tsx +218 -0
- package/src/config/__tests__/portal-resolver.test.ts +247 -0
- package/src/config/__tests__/valid-redirect.test.ts +47 -0
- package/src/config/constants.ts +27 -0
- package/src/config/index.ts +411 -0
- package/src/config/resolve-basename.ts +57 -0
- package/src/constants/api-paths.ts +450 -0
- package/src/generated/api.ts +12566 -0
- package/src/generated/queries.ts +12623 -0
- package/src/generated/types.ts +13888 -0
- package/src/hooks/__tests__/useBootstrap.test.tsx +91 -0
- package/src/hooks/__tests__/useCurrentRole.test.tsx +122 -0
- package/src/hooks/useAuth.ts +239 -0
- package/src/hooks/useBootstrap.ts +74 -0
- package/src/hooks/useCurrentRole.ts +18 -0
- package/src/hooks/useLogout.ts +33 -0
- package/src/hooks/usePermission.ts +116 -0
- package/src/hooks/useSodMode.ts +73 -0
- package/src/index.ts +209 -0
- package/src/lib/push.ts +41 -0
- package/src/seo/dom-utils.ts +94 -0
- package/src/seo/index.ts +6 -0
- package/src/seo/jsonld.ts +29 -0
- package/src/seo/types.ts +29 -0
- package/src/seo/usePageMeta.ts +17 -0
- package/src/seo/usePageTitle.ts +14 -0
- package/src/seo/useSEO.ts +80 -0
- package/src/types/index.ts +185 -0
- package/src/utils/__tests__/mock-test.ts +15 -0
- package/src/utils/browser.ts +6 -0
- package/src/utils/case.ts +44 -0
- package/src/utils/error.ts +26 -0
- package/src/utils/format.ts +63 -0
- package/src/utils/password-transmission.test.ts +145 -0
- package/src/utils/password-transmission.ts +253 -0
- package/src/utils/response.ts +62 -0
- package/src/utils/vite.ts +4 -0
- package/src/vite-env.d.ts +18 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autional 共享 TypeScript 类型
|
|
3
|
+
* 所有前端应用共用
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============ 用户 ============
|
|
7
|
+
|
|
8
|
+
export interface User {
|
|
9
|
+
id: string;
|
|
10
|
+
username: string;
|
|
11
|
+
email: string;
|
|
12
|
+
status: 'active' | 'inactive' | 'locked' | 'pending';
|
|
13
|
+
createdAt?: string;
|
|
14
|
+
updatedAt?: string;
|
|
15
|
+
lastLoginAt?: string;
|
|
16
|
+
avatarUrl?: string;
|
|
17
|
+
phone?: string;
|
|
18
|
+
roles?: RoleSummary[];
|
|
19
|
+
department?: DepartmentSummary;
|
|
20
|
+
mfaEnabled?: boolean;
|
|
21
|
+
/** 后端 /auth/me 返回的扩展字段(可选,向后兼容) */
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
tenant_type?: string;
|
|
24
|
+
tenant_id?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RoleSummary {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DepartmentSummary {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============ 认证 ============
|
|
38
|
+
|
|
39
|
+
export interface LoginRequest {
|
|
40
|
+
identity: string;
|
|
41
|
+
password: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LoginResponse {
|
|
45
|
+
accessToken: string;
|
|
46
|
+
refreshToken: string;
|
|
47
|
+
expiresIn?: number;
|
|
48
|
+
tokenType?: string;
|
|
49
|
+
user: User;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RegisterRequest {
|
|
53
|
+
username: string;
|
|
54
|
+
email: string;
|
|
55
|
+
password: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============ 租户 ============
|
|
59
|
+
|
|
60
|
+
export interface Tenant {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
domain?: string;
|
|
64
|
+
status: 'active' | 'suspended' | 'pending';
|
|
65
|
+
createdAt?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TenantMember {
|
|
69
|
+
userId: string;
|
|
70
|
+
username: string;
|
|
71
|
+
email: string;
|
|
72
|
+
role: 'owner' | 'admin' | 'member';
|
|
73
|
+
status: string;
|
|
74
|
+
joinedAt?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============ RBAC ============
|
|
78
|
+
|
|
79
|
+
export interface Role {
|
|
80
|
+
id: string;
|
|
81
|
+
code: string;
|
|
82
|
+
name: string;
|
|
83
|
+
description: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface Permission {
|
|
87
|
+
id: string;
|
|
88
|
+
code: string;
|
|
89
|
+
name: string;
|
|
90
|
+
description: string;
|
|
91
|
+
category: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============ 应用 ============
|
|
95
|
+
|
|
96
|
+
export interface Application {
|
|
97
|
+
id: string;
|
|
98
|
+
name: string;
|
|
99
|
+
type: 'oidc' | 'saml' | 'custom';
|
|
100
|
+
clientId: string;
|
|
101
|
+
status: string;
|
|
102
|
+
redirectUris: string[];
|
|
103
|
+
createdAt?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============ 审计 ============
|
|
107
|
+
|
|
108
|
+
export interface AuditLog {
|
|
109
|
+
id: string;
|
|
110
|
+
createdAt: string;
|
|
111
|
+
actorId: string;
|
|
112
|
+
actorName: string;
|
|
113
|
+
action: string;
|
|
114
|
+
resourceType: string;
|
|
115
|
+
resourceId: string;
|
|
116
|
+
description: string;
|
|
117
|
+
result: 'success' | 'failure';
|
|
118
|
+
ipAddress: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============ 会话 ============
|
|
122
|
+
|
|
123
|
+
export interface Session {
|
|
124
|
+
id: string;
|
|
125
|
+
userId: string;
|
|
126
|
+
username: string;
|
|
127
|
+
ipAddress: string;
|
|
128
|
+
device: string;
|
|
129
|
+
browser: string;
|
|
130
|
+
location: string;
|
|
131
|
+
riskScore: number;
|
|
132
|
+
createdAt: string;
|
|
133
|
+
lastActiveAt: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============ 部门 ============
|
|
137
|
+
|
|
138
|
+
export interface Department {
|
|
139
|
+
id: string;
|
|
140
|
+
name: string;
|
|
141
|
+
parentId?: string;
|
|
142
|
+
memberCount: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============ 列表响应 ============
|
|
146
|
+
|
|
147
|
+
export interface ListResponse<T> {
|
|
148
|
+
items: T[];
|
|
149
|
+
total: number;
|
|
150
|
+
pagination?: {
|
|
151
|
+
page: number;
|
|
152
|
+
pageSize: number;
|
|
153
|
+
totalPages: number;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ============ 分页请求 ============
|
|
158
|
+
|
|
159
|
+
export interface PageRequest {
|
|
160
|
+
page?: number;
|
|
161
|
+
pageSize?: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============ API 错误 ============
|
|
165
|
+
|
|
166
|
+
export interface ApiError {
|
|
167
|
+
code: string;
|
|
168
|
+
message: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============ 后端原始响应 ============
|
|
172
|
+
|
|
173
|
+
export interface ApiResponse<T> {
|
|
174
|
+
code: number;
|
|
175
|
+
message: string;
|
|
176
|
+
data?: T;
|
|
177
|
+
items?: T[];
|
|
178
|
+
total?: number;
|
|
179
|
+
pagination?: {
|
|
180
|
+
page: number;
|
|
181
|
+
pageSize: number;
|
|
182
|
+
totalPages: number;
|
|
183
|
+
};
|
|
184
|
+
timestamp?: number;
|
|
185
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock from test's relative path
|
|
4
|
+
vi.mock('../../generated/api', () => ({
|
|
5
|
+
PublicKeyExchange: vi.fn(() => Promise.resolve({ server_pub_key: 'x', key_exchange_id: 'y' })),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import { PublicKeyExchange as pk } from '../../generated/api';
|
|
9
|
+
|
|
10
|
+
describe('mock test', () => {
|
|
11
|
+
it('should return mock value', async () => {
|
|
12
|
+
const result = await pk();
|
|
13
|
+
expect(result).toEqual({ server_pub_key: 'x', key_exchange_id: 'y' });
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
2
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
3
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
4
|
+
const rawData = window.atob(base64);
|
|
5
|
+
return Uint8Array.from(rawData.split('').map((c) => c.charCodeAt(0)));
|
|
6
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字段名大小写转换工具
|
|
3
|
+
* 后端 Go 返回 PascalCase,前端统一使用 camelCase
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { camelCase, isPlainObject, isArray } from 'lodash-es';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 递归将对象的所有键从 PascalCase/snake_case 转换为 camelCase
|
|
10
|
+
*/
|
|
11
|
+
export function camelCaseKeys<T>(obj: T): T {
|
|
12
|
+
if (isArray(obj)) {
|
|
13
|
+
return obj.map(camelCaseKeys) as unknown as T;
|
|
14
|
+
}
|
|
15
|
+
if (!isPlainObject(obj) || obj === null) {
|
|
16
|
+
return obj;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result: Record<string, unknown> = {};
|
|
20
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
21
|
+
result[camelCase(key)] = camelCaseKeys(value);
|
|
22
|
+
}
|
|
23
|
+
return result as T;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 递归将对象的所有键从 camelCase 转换为 snake_case
|
|
28
|
+
* 用于发送请求到后端
|
|
29
|
+
*/
|
|
30
|
+
export function snakeCaseKeys<T>(obj: T): T {
|
|
31
|
+
if (isArray(obj)) {
|
|
32
|
+
return obj.map(snakeCaseKeys) as unknown as T;
|
|
33
|
+
}
|
|
34
|
+
if (!isPlainObject(obj) || obj === null) {
|
|
35
|
+
return obj;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const result: Record<string, unknown> = {};
|
|
39
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
40
|
+
const snakeKey = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
41
|
+
result[snakeKey] = snakeCaseKeys(value);
|
|
42
|
+
}
|
|
43
|
+
return result as T;
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error handling utilities
|
|
3
|
+
* Framework-agnostic: works with any UI notification system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ExtractedApiError {
|
|
7
|
+
code: number | string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function extractApiErrorMessage(err: unknown, defaultMsg: string): string {
|
|
12
|
+
return extractApiError(err, defaultMsg).message;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function extractApiError(err: unknown, defaultMsg: string): ExtractedApiError {
|
|
16
|
+
return {
|
|
17
|
+
code: (err as any)?.response?.data?.code || 'UNKNOWN',
|
|
18
|
+
message: (err as any)?.response?.data?.message || (err as any)?.message || defaultMsg,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createHandleApiError(notify: (msg: string) => void) {
|
|
23
|
+
return (err: unknown, defaultMsg: string): void => {
|
|
24
|
+
notify(extractApiErrorMessage(err, defaultMsg));
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
let localeGetter: (() => string) | null = null;
|
|
2
|
+
|
|
3
|
+
export function setLocaleGetter(fn: () => string) {
|
|
4
|
+
localeGetter = fn;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function getLocale(): string {
|
|
8
|
+
if (localeGetter) return localeGetter();
|
|
9
|
+
return 'zh-CN';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatTime(iso: string | undefined | null, locale?: string): string {
|
|
13
|
+
if (!iso) return '--';
|
|
14
|
+
try {
|
|
15
|
+
const d = new Date(iso);
|
|
16
|
+
if (isNaN(d.getTime())) return iso;
|
|
17
|
+
return d.toLocaleString(locale || getLocale(), {
|
|
18
|
+
year: 'numeric',
|
|
19
|
+
month: '2-digit',
|
|
20
|
+
day: '2-digit',
|
|
21
|
+
hour: '2-digit',
|
|
22
|
+
minute: '2-digit',
|
|
23
|
+
});
|
|
24
|
+
} catch {
|
|
25
|
+
return iso;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatDate(iso: string | undefined | null, locale?: string): string {
|
|
30
|
+
if (!iso) return '--';
|
|
31
|
+
try {
|
|
32
|
+
const d = new Date(iso);
|
|
33
|
+
if (isNaN(d.getTime())) return iso;
|
|
34
|
+
return d.toLocaleDateString(locale || getLocale(), {
|
|
35
|
+
year: 'numeric',
|
|
36
|
+
month: '2-digit',
|
|
37
|
+
day: '2-digit',
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
return iso;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function formatRelativeTime(iso: string | undefined | null, locale?: string): string {
|
|
45
|
+
if (!iso) return '--';
|
|
46
|
+
try {
|
|
47
|
+
const d = new Date(iso);
|
|
48
|
+
if (isNaN(d.getTime())) return iso;
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const diff = now - d.getTime();
|
|
51
|
+
const mins = Math.floor(diff / 60000);
|
|
52
|
+
const hours = Math.floor(diff / 3600000);
|
|
53
|
+
const days = Math.floor(diff / 86400000);
|
|
54
|
+
|
|
55
|
+
if (mins < 1) return 'just now';
|
|
56
|
+
if (mins < 60) return `${mins}m ago`;
|
|
57
|
+
if (hours < 24) return `${hours}h ago`;
|
|
58
|
+
if (days < 7) return `${days}d ago`;
|
|
59
|
+
return formatDate(iso, locale);
|
|
60
|
+
} catch {
|
|
61
|
+
return iso;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
|
|
4
|
+
// SAME specifier string as source file's import ('../generated/api')
|
|
5
|
+
vi.mock('../generated/api', () => ({
|
|
6
|
+
PublicKeyExchange: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { processPasswordForTransmission } from './password-transmission';
|
|
10
|
+
import { PublicKeyExchange as identityPublicKeyExchange } from '../generated/api';
|
|
11
|
+
|
|
12
|
+
function sha256hex(input: string): string {
|
|
13
|
+
return createHash('sha256').update(input).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TEST_PEM = `-----BEGIN PUBLIC KEY-----
|
|
17
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt4R4KXzLBZqHyQNqkLZA
|
|
18
|
+
6h9YThQq4j+XdKmVF8vPq3HmLcDJ5FfTqM2kVxXCVmNJKHDbGzqP1WyvF5nTbgLV
|
|
19
|
+
w8LkGd2pGHp3RKjCmJxLFHZVZyhWHt7aBPqMqMZGqkJFxD2cHqmNHyDCpHGkNs4b
|
|
20
|
+
GqvBKPxcJnMkFEyFqFf7bSMCj1ARPbHHCq0vT9TmVwLPxkTVNpRjEGLWQzDqJGmF
|
|
21
|
+
n9yIHxR5ckRLHkWKNvUPWyCkNQWHKtKvJWqKjqPpVtLRLHwNmyEYNYgWHPMfmjXZ
|
|
22
|
+
dCqGNpLTHqJBYfCBGHJRFHYZQkFYi6LkUVhFqGxLkNLVNFPEKcZSMcKJqHmPQEtJ
|
|
23
|
+
cQIDAQAB
|
|
24
|
+
-----END PUBLIC KEY-----`;
|
|
25
|
+
|
|
26
|
+
describe('processPasswordForTransmission', () => {
|
|
27
|
+
describe('plain mode', () => {
|
|
28
|
+
it('UT-TX-01: returns password unchanged with transmission=plain', async () => {
|
|
29
|
+
const result = await processPasswordForTransmission(
|
|
30
|
+
'Test@123',
|
|
31
|
+
'plain',
|
|
32
|
+
'acme-corp',
|
|
33
|
+
undefined,
|
|
34
|
+
);
|
|
35
|
+
expect(result.password).toBe('Test@123');
|
|
36
|
+
expect(result.passwordTransmission).toBe('plain');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('hash mode', () => {
|
|
41
|
+
it('UT-TX-02: returns SHA-256 hex of password|tenantId', async () => {
|
|
42
|
+
const result = await processPasswordForTransmission(
|
|
43
|
+
'Test@123',
|
|
44
|
+
'hash',
|
|
45
|
+
'acme-corp',
|
|
46
|
+
undefined,
|
|
47
|
+
);
|
|
48
|
+
const expected = sha256hex('Test@123|acme-corp');
|
|
49
|
+
expect(result.password).toBe(expected);
|
|
50
|
+
expect(result.passwordTransmission).toBe('hash');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('UT-TX-03: handles empty tenantId', async () => {
|
|
54
|
+
const result = await processPasswordForTransmission('Test@123', 'hash', '', undefined);
|
|
55
|
+
const expected = sha256hex('Test@123|');
|
|
56
|
+
expect(result.password).toBe(expected);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('UT-TX-04: handles chinese characters', async () => {
|
|
60
|
+
const result = await processPasswordForTransmission(
|
|
61
|
+
'测试@123',
|
|
62
|
+
'hash',
|
|
63
|
+
'acme-corp',
|
|
64
|
+
undefined,
|
|
65
|
+
);
|
|
66
|
+
const expected = sha256hex('测试@123|acme-corp');
|
|
67
|
+
expect(result.password).toBe(expected);
|
|
68
|
+
expect(result.passwordTransmission).toBe('hash');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('undefined / default mode', () => {
|
|
73
|
+
it('UT-TX-05: throws when mode is undefined (no silent plain fallback)', async () => {
|
|
74
|
+
await expect(
|
|
75
|
+
processPasswordForTransmission('Test@123', undefined, 'acme-corp', undefined),
|
|
76
|
+
).rejects.toThrow('password transmission mode is missing from tenant auth-config');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('UT-TX-05b: throws when mode is empty string', async () => {
|
|
80
|
+
await expect(
|
|
81
|
+
processPasswordForTransmission('Test@123', '', 'acme-corp', undefined),
|
|
82
|
+
).rejects.toThrow('password transmission mode is missing from tenant auth-config');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('symmetric mode', () => {
|
|
87
|
+
const mockServerPubKey =
|
|
88
|
+
'BDmXBCDz0nrP5sHAh7ahMBjiIPIqg1lEHfXzw434ChYa/scxqUzjV4vFzw0jFzpKVhvhgIW62xdHKppccUa7sic=';
|
|
89
|
+
|
|
90
|
+
// jsdom limitation: crypto.subtle ECDH P-256 raw key import not supported.
|
|
91
|
+
// The symmetric encryption logic is verified at the Go level:
|
|
92
|
+
// seed/tool/crypto_test.go — TestSymmetricEncrypt_RoundTrip PASS
|
|
93
|
+
// The mock interception is verified by UT-TX-07 (rejection case).
|
|
94
|
+
it.skip('UT-TX-06: returns ciphertext with keyExchangeId (skipped — jsdom crypto.subtle lacks ECDH raw import)', async () => {
|
|
95
|
+
(identityPublicKeyExchange as unknown as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
|
96
|
+
Promise.resolve({ serverPubKey: mockServerPubKey, keyExchangeId: 'test-exchange-1' }),
|
|
97
|
+
);
|
|
98
|
+
try {
|
|
99
|
+
const result = await processPasswordForTransmission(
|
|
100
|
+
'Test@123',
|
|
101
|
+
'symmetric',
|
|
102
|
+
'acme-corp',
|
|
103
|
+
undefined,
|
|
104
|
+
);
|
|
105
|
+
expect(result.passwordTransmission).toBe('symmetric');
|
|
106
|
+
expect(result.password).toBeTruthy();
|
|
107
|
+
expect(typeof result.password).toBe('string');
|
|
108
|
+
expect(result.keyExchangeId).toBe('test-exchange-1');
|
|
109
|
+
expect(result.clientPubKey).toBeTruthy();
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.error('SYMMETRIC FAILED:', e);
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('UT-TX-07: throws when keyExchange fails', async () => {
|
|
117
|
+
vi.mocked(identityPublicKeyExchange).mockRejectedValueOnce(new Error('Network error'));
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
processPasswordForTransmission('Test@123', 'symmetric', 'acme-corp', undefined),
|
|
121
|
+
).rejects.toThrow();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('asymmetric mode', () => {
|
|
126
|
+
it('UT-TX-08: returns RSA-OAEP ciphertext', async () => {
|
|
127
|
+
const result = await processPasswordForTransmission(
|
|
128
|
+
'Test@123',
|
|
129
|
+
'asymmetric',
|
|
130
|
+
'acme-corp',
|
|
131
|
+
TEST_PEM,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(result.passwordTransmission).toBe('asymmetric');
|
|
135
|
+
expect(result.password).toBeTruthy();
|
|
136
|
+
expect(typeof result.password).toBe('string');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('UT-TX-09: throws when publicKey is missing', async () => {
|
|
140
|
+
await expect(
|
|
141
|
+
processPasswordForTransmission('Test@123', 'asymmetric', 'acme-corp', undefined),
|
|
142
|
+
).rejects.toThrow();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|