@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.
Files changed (81) hide show
  1. package/LICENSE +661 -0
  2. package/package.json +47 -0
  3. package/src/api/__tests__/entry-plane.test.ts +119 -0
  4. package/src/api/bff-client.ts +94 -0
  5. package/src/api/client.ts +174 -0
  6. package/src/api/entry-plane.ts +61 -0
  7. package/src/api/trace.ts +21 -0
  8. package/src/auth/__tests__/ac007-role-source.test.ts +124 -0
  9. package/src/auth/__tests__/auth-machine.test.ts +157 -0
  10. package/src/auth/__tests__/build-login-url.test.ts +219 -0
  11. package/src/auth/__tests__/get-current-role.test.ts +61 -0
  12. package/src/auth/__tests__/oauth-client-from-slug.test.ts +132 -0
  13. package/src/auth/__tests__/pkce-uniformity.test.ts +48 -0
  14. package/src/auth/__tests__/session-client-id.test.ts +124 -0
  15. package/src/auth/__tests__/slug-from-url.test.ts +112 -0
  16. package/src/auth/auth-machine.ts +151 -0
  17. package/src/auth/auth-queries.ts +118 -0
  18. package/src/auth/build-tenant-urls.ts +48 -0
  19. package/src/auth/oauth-client-from-slug.ts +119 -0
  20. package/src/auth/oauth-client-id-store.ts +41 -0
  21. package/src/auth/oauth-login.ts +291 -0
  22. package/src/auth/pkce.ts +141 -0
  23. package/src/auth/portal-config.ts +68 -0
  24. package/src/auth/roles.ts +102 -0
  25. package/src/auth/service.ts +368 -0
  26. package/src/auth/slug-from-url.ts +92 -0
  27. package/src/auth/store.ts +226 -0
  28. package/src/auth/tenant-route-middleware.ts +113 -0
  29. package/src/auth/tenant-slug-context.tsx +9 -0
  30. package/src/branding/BrandingInitializer.tsx +60 -0
  31. package/src/branding/brand-color.ts +155 -0
  32. package/src/branding/branding-cache.ts +71 -0
  33. package/src/branding/extract-branding.ts +27 -0
  34. package/src/branding/index.ts +15 -0
  35. package/src/branding/tenant-store.ts +13 -0
  36. package/src/branding/types.ts +14 -0
  37. package/src/branding/useBranding.ts +85 -0
  38. package/src/components/AuditStatsOnly.tsx +65 -0
  39. package/src/components/OAuthCallbackPage.tsx +56 -0
  40. package/src/components/PortalGuard.tsx +105 -0
  41. package/src/components/RequireAuth.tsx +142 -0
  42. package/src/components/TenantIndexGuard.tsx +131 -0
  43. package/src/components/__tests__/RequireAuth.test.tsx +244 -0
  44. package/src/components/__tests__/TenantIndexGuard.test.tsx +218 -0
  45. package/src/config/__tests__/portal-resolver.test.ts +247 -0
  46. package/src/config/__tests__/valid-redirect.test.ts +47 -0
  47. package/src/config/constants.ts +27 -0
  48. package/src/config/index.ts +411 -0
  49. package/src/config/resolve-basename.ts +57 -0
  50. package/src/constants/api-paths.ts +450 -0
  51. package/src/generated/api.ts +12566 -0
  52. package/src/generated/queries.ts +12623 -0
  53. package/src/generated/types.ts +13888 -0
  54. package/src/hooks/__tests__/useBootstrap.test.tsx +91 -0
  55. package/src/hooks/__tests__/useCurrentRole.test.tsx +122 -0
  56. package/src/hooks/useAuth.ts +239 -0
  57. package/src/hooks/useBootstrap.ts +74 -0
  58. package/src/hooks/useCurrentRole.ts +18 -0
  59. package/src/hooks/useLogout.ts +33 -0
  60. package/src/hooks/usePermission.ts +116 -0
  61. package/src/hooks/useSodMode.ts +73 -0
  62. package/src/index.ts +209 -0
  63. package/src/lib/push.ts +41 -0
  64. package/src/seo/dom-utils.ts +94 -0
  65. package/src/seo/index.ts +6 -0
  66. package/src/seo/jsonld.ts +29 -0
  67. package/src/seo/types.ts +29 -0
  68. package/src/seo/usePageMeta.ts +17 -0
  69. package/src/seo/usePageTitle.ts +14 -0
  70. package/src/seo/useSEO.ts +80 -0
  71. package/src/types/index.ts +185 -0
  72. package/src/utils/__tests__/mock-test.ts +15 -0
  73. package/src/utils/browser.ts +6 -0
  74. package/src/utils/case.ts +44 -0
  75. package/src/utils/error.ts +26 -0
  76. package/src/utils/format.ts +63 -0
  77. package/src/utils/password-transmission.test.ts +145 -0
  78. package/src/utils/password-transmission.ts +253 -0
  79. package/src/utils/response.ts +62 -0
  80. package/src/utils/vite.ts +4 -0
  81. package/src/vite-env.d.ts +18 -0
@@ -0,0 +1,253 @@
1
+ /**
2
+ * 密码传输安全模块
3
+ *
4
+ * 根据租户 password_transmission 策略在前端对密码进行预处理:
5
+ * plain — 不做处理,原始密码传输
6
+ * hash — SHA-256(password + tenantId + clientNonce + serverNonce)
7
+ * symmetric — ECDH 密钥交换 + AES-256-GCM (Phase 2)
8
+ * asymmetric — RSA-OAEP 公钥加密 (Phase 2)
9
+ */
10
+
11
+ import { apiClient } from '../api/client';
12
+
13
+ // @generated-api-exempt: PublicKeyExchange tree-shook by Vite; inline required
14
+ async function identityPublicKeyExchange() {
15
+ const res = await apiClient.get('/identity/api/v1/public/key-exchange'); // @generated-api-exempt
16
+ return res.data;
17
+ }
18
+
19
+ const ENCODER = new TextEncoder();
20
+
21
+ function base64ToBytes(base64: string): Uint8Array {
22
+ const binary = atob(base64);
23
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
24
+ }
25
+
26
+ function bytesToHex(bytes: Uint8Array): string {
27
+ return Array.from(bytes)
28
+ .map((b) => b.toString(16).padStart(2, '0'))
29
+ .join('');
30
+ }
31
+
32
+ export interface TransmissionResult {
33
+ password: string;
34
+ passwordTransmission: string;
35
+ clientNonce?: string;
36
+ keyExchangeId?: string;
37
+ clientPubKey?: string;
38
+ keyId?: string;
39
+ }
40
+
41
+ /**
42
+ * 根据传输模式预处理密码。
43
+ * @param rawPassword 用户输入的原始密码
44
+ * @param mode 租户配置的传输模式
45
+ * @param tenantId 租户 ID
46
+ * @param serverNonce 服务端返回的 transmission_nonce (hash 模式必需)
47
+ */
48
+ export async function processPasswordForTransmission(
49
+ rawPassword: string,
50
+ mode: string | undefined,
51
+ tenantId: string,
52
+ serverNonce: string | undefined,
53
+ ): Promise<TransmissionResult> {
54
+ // 2026-08-17 安全修复:mode 必须显式提供。
55
+ // 后端 PasswordPolicyResponse.password_transmission 恒返回(无 omitempty 缺失,
56
+ // GetPasswordPolicy 有全局默认兜底),因此 undefined/空串只意味着"契约错误或未知模式"。
57
+ // 静默回退 plain 会把 hash/symmetric 租户降级为明文传输(后端 61000104 拒绝),
58
+ // 且掩盖真实错误。只有明确 mode==='plain' 才走明文;未知模式一律抛错暴露。
59
+ if (mode === undefined || mode === '') {
60
+ throw new Error('password transmission mode is missing from tenant auth-config');
61
+ }
62
+
63
+ const result: TransmissionResult = {
64
+ password: rawPassword,
65
+ passwordTransmission: mode,
66
+ };
67
+
68
+ switch (mode) {
69
+ case 'plain':
70
+ // 显式 plain:明文传输(合法)
71
+ break;
72
+ case 'hash': {
73
+ const input = rawPassword + '|' + tenantId;
74
+ try {
75
+ const hashBuffer = await crypto.subtle.digest('SHA-256', ENCODER.encode(input));
76
+ result.password = bytesToHex(new Uint8Array(hashBuffer));
77
+ } catch {
78
+ result.password = sha256HexPureJS(input);
79
+ }
80
+ result.passwordTransmission = 'hash';
81
+ break;
82
+ }
83
+ case 'symmetric': {
84
+ const keData = await identityPublicKeyExchange();
85
+ const serverPubBytes = base64ToBytes(keData.serverPubKey) as BufferSource;
86
+
87
+ const clientKeyPair = await crypto.subtle.generateKey(
88
+ { name: 'ECDH', namedCurve: 'P-256' },
89
+ false,
90
+ ['deriveBits'],
91
+ );
92
+
93
+ const serverPubKey = await crypto.subtle.importKey(
94
+ 'raw',
95
+ serverPubBytes,
96
+ { name: 'ECDH', namedCurve: 'P-256' },
97
+ false,
98
+ [],
99
+ );
100
+
101
+ const sharedBits = await crypto.subtle.deriveBits(
102
+ { name: 'ECDH', public: serverPubKey },
103
+ clientKeyPair.privateKey,
104
+ 256,
105
+ );
106
+ const aesKey = await crypto.subtle.importKey('raw', sharedBits, 'AES-GCM', false, [
107
+ 'encrypt',
108
+ ]);
109
+
110
+ const iv = crypto.getRandomValues(new Uint8Array(12));
111
+ const encoded = ENCODER.encode(rawPassword);
112
+ const ciphertext = await crypto.subtle.encrypt(
113
+ { name: 'AES-GCM', iv: iv },
114
+ aesKey,
115
+ encoded,
116
+ );
117
+
118
+ const clientPubRaw = await crypto.subtle.exportKey('raw', clientKeyPair.publicKey);
119
+
120
+ const combined = new Uint8Array(12 + ciphertext.byteLength + clientPubRaw.byteLength);
121
+ combined.set(iv, 0);
122
+ combined.set(new Uint8Array(ciphertext), 12);
123
+ combined.set(new Uint8Array(clientPubRaw), 12 + ciphertext.byteLength);
124
+
125
+ result.password = btoa(String.fromCharCode(...combined));
126
+ result.keyExchangeId = keData.keyExchangeId;
127
+ result.clientPubKey = btoa(String.fromCharCode(...new Uint8Array(clientPubRaw)));
128
+ result.passwordTransmission = 'symmetric';
129
+ break;
130
+ }
131
+ case 'asymmetric': {
132
+ const publicKeyPem = serverNonce;
133
+ if (!publicKeyPem || publicKeyPem.length < 100) {
134
+ throw new Error('public_key is required for asymmetric mode');
135
+ }
136
+ const pemContents = publicKeyPem
137
+ .replace('-----BEGIN PUBLIC KEY-----', '')
138
+ .replace('-----END PUBLIC KEY-----', '')
139
+ .replace(/\s/g, '');
140
+ const derBytes = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
141
+ const publicKey = await crypto.subtle.importKey(
142
+ 'spki',
143
+ derBytes,
144
+ { name: 'RSA-OAEP', hash: 'SHA-256' },
145
+ false,
146
+ ['encrypt'],
147
+ );
148
+ const encoded = ENCODER.encode(rawPassword);
149
+ const ciphertext = await crypto.subtle.encrypt(
150
+ { name: 'RSA-OAEP' },
151
+ publicKey,
152
+ encoded,
153
+ );
154
+ result.password = btoa(String.fromCharCode(...new Uint8Array(ciphertext)));
155
+ result.passwordTransmission = 'asymmetric';
156
+ break;
157
+ }
158
+ default:
159
+ // 未知模式(非 plain/hash/symmetric/asymmetric):抛错而非静默降级
160
+ throw new Error(`unknown password transmission mode: ${mode}`);
161
+ }
162
+
163
+ return result;
164
+ }
165
+
166
+ /**
167
+ * 哈希密码用于传输 (简化版本,用于 registration 和 change-password)。
168
+ */
169
+ export async function hashPasswordForTransmission(
170
+ password: string,
171
+ tenantId: string,
172
+ _serverNonce: string,
173
+ ): Promise<{ hash: string; clientNonce: string }> {
174
+ const combined = ENCODER.encode(password + '|' + tenantId);
175
+ const hashBuffer = await crypto.subtle.digest('SHA-256', combined);
176
+ return {
177
+ hash: bytesToHex(new Uint8Array(hashBuffer)),
178
+ clientNonce: '',
179
+ };
180
+ }
181
+
182
+ function ror(x: number, n: number): number {
183
+ return (x >>> n) | (x << (32 - n));
184
+ }
185
+
186
+ function sha256HexPureJS(input: string): string {
187
+ const msg = new TextEncoder().encode(input);
188
+ const msgBits = msg.length * 8;
189
+ const buf = new Uint8Array(((msg.length + 9 + 63) >>> 6) << 6);
190
+ buf.set(msg);
191
+ buf[msg.length] = 0x80;
192
+ const view = new DataView(buf.buffer);
193
+ view.setUint32(buf.length - 4, msgBits);
194
+
195
+ const K = new Uint32Array([
196
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
197
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
198
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
199
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
200
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
201
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
202
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
203
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
204
+ ]);
205
+
206
+ const H = new Uint32Array([
207
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
208
+ ]);
209
+ const W = new Uint32Array(64);
210
+
211
+ for (let off = 0; off < buf.length; off += 64) {
212
+ for (let i = 0; i < 16; i++) {
213
+ W[i] = view.getUint32(off + i * 4);
214
+ }
215
+ for (let i = 16; i < 64; i++) {
216
+ const s0 = ror(W[i - 15], 7) ^ ror(W[i - 15], 18) ^ (W[i - 15] >>> 3);
217
+ const s1 = ror(W[i - 2], 17) ^ ror(W[i - 2], 19) ^ (W[i - 2] >>> 10);
218
+ W[i] = (W[i - 16] + s0 + W[i - 7] + s1) | 0;
219
+ }
220
+ let [a, b, c, d, e, f, g, h] = H;
221
+ for (let i = 0; i < 64; i++) {
222
+ const S1 = ror(e, 6) ^ ror(e, 11) ^ ror(e, 25);
223
+ const ch = (e & f) ^ (~e & g);
224
+ const t1 = (h + S1 + ch + K[i] + W[i]) | 0;
225
+ const S0 = ror(a, 2) ^ ror(a, 13) ^ ror(a, 22);
226
+ const maj = (a & b) ^ (a & c) ^ (b & c);
227
+ const t2 = (S0 + maj) | 0;
228
+ h = g;
229
+ g = f;
230
+ f = e;
231
+ e = (d + t1) | 0;
232
+ d = c;
233
+ c = b;
234
+ b = a;
235
+ a = (t1 + t2) | 0;
236
+ }
237
+ H[0] = (H[0] + a) | 0;
238
+ H[1] = (H[1] + b) | 0;
239
+ H[2] = (H[2] + c) | 0;
240
+ H[3] = (H[3] + d) | 0;
241
+ H[4] = (H[4] + e) | 0;
242
+ H[5] = (H[5] + f) | 0;
243
+ H[6] = (H[6] + g) | 0;
244
+ H[7] = (H[7] + h) | 0;
245
+ }
246
+
247
+ const digest = new Uint8Array(32);
248
+ const dv = new DataView(digest.buffer);
249
+ for (let i = 0; i < 8; i++) dv.setUint32(i * 4, H[i]);
250
+ return Array.from(digest)
251
+ .map((b) => b.toString(16).padStart(2, '0'))
252
+ .join('');
253
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Standardized API response unwrapping utilities.
3
+ * Replaces ad-hoc `res?.data?.items ?? res?.data ?? []` patterns across all apps.
4
+ */
5
+
6
+ export interface PaginationInfo {
7
+ page: number;
8
+ pageSize: number;
9
+ total: number;
10
+ }
11
+
12
+ export interface ListResult<T> {
13
+ items: T[];
14
+ pagination: PaginationInfo;
15
+ }
16
+
17
+ export function extractList<T = Record<string, any>>(res: unknown): T[] {
18
+ if (!res) return [];
19
+ if (Array.isArray(res)) return res as T[];
20
+ const r = res as Record<string, unknown>;
21
+ const dataObj = r.data as Record<string, unknown> | undefined;
22
+ const items = r.items ?? dataObj?.items ?? r.data ?? [];
23
+ return Array.isArray(items) ? (items as T[]) : [];
24
+ }
25
+
26
+ export function extractItem<T = Record<string, any>>(res: unknown): T | null {
27
+ if (!res) return null;
28
+ const r = res as Record<string, unknown>;
29
+ return (r.data ?? r ?? null) as T | null;
30
+ }
31
+
32
+ export function extractPagination(res: unknown): PaginationInfo {
33
+ const r = res as Record<string, unknown>;
34
+ const pagination = r.pagination as Record<string, unknown> | undefined;
35
+ return {
36
+ page: (pagination?.page ?? r.page ?? 1) as number,
37
+ pageSize: (pagination?.pageSize ?? r.pageSize ?? 10) as number,
38
+ total: (r.total ?? 0) as number,
39
+ };
40
+ }
41
+
42
+ export function extractListResult<T>(res: unknown): ListResult<T> {
43
+ return {
44
+ items: extractList<T>(res),
45
+ pagination: extractPagination(res),
46
+ };
47
+ }
48
+
49
+ export async function fetchList<T = any>(apiCall: Promise<unknown>): Promise<T[]> {
50
+ const res = await apiCall;
51
+ return extractList<T>(res);
52
+ }
53
+
54
+ export async function fetchItem<T = any>(apiCall: Promise<unknown>): Promise<T | null> {
55
+ const res = await apiCall;
56
+ return extractItem<T>(res);
57
+ }
58
+
59
+ export async function fetchListResult<T = any>(apiCall: Promise<unknown>): Promise<ListResult<T>> {
60
+ const res = await apiCall;
61
+ return extractListResult<T>(res);
62
+ }
@@ -0,0 +1,4 @@
1
+ export function normalizeViteBase(p: string | undefined): string {
2
+ if (!p || p === '/') return '/';
3
+ return p.replace(/\/$/, '') + '/';
4
+ }
@@ -0,0 +1,18 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly PROD: boolean;
5
+ readonly DEV: boolean;
6
+ readonly MODE: string;
7
+ readonly BASE_URL: string;
8
+ readonly SSR: boolean;
9
+ readonly VITE_OAUTH_CLIENT_ID: string;
10
+ readonly VITE_AUTH_PAGES_URL: string;
11
+ readonly VITE_API_BASE_URL: string;
12
+ readonly VITE_AUDIT_BASE_URL: string;
13
+ [key: string]: string | boolean | undefined;
14
+ }
15
+
16
+ interface ImportMeta {
17
+ readonly env: ImportMetaEnv;
18
+ }