@csntgao/uni-base 0.1.0

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 (90) hide show
  1. package/.prettierignore +4 -0
  2. package/.prettierrc.json +6 -0
  3. package/AGENTS.md +519 -0
  4. package/CLAUDE.md +512 -0
  5. package/README.md +734 -0
  6. package/docs//346/225/260/345/255/227/345/221/230/345/267/245/345/256/214/346/225/264/346/212/275/345/261/211/346/216/245/345/205/245/350/257/264/346/230/216.md +110 -0
  7. package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/215/225/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +79 -0
  8. package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/244/232/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +93 -0
  9. package/docs//350/264/246/346/210/267/344/270/216/347/231/273/345/275/225/347/273/204/344/273/266/345/212/250/346/200/201/346/216/245/345/205/245/350/257/264/346/230/216.md +201 -0
  10. package/eslint.config.js +30 -0
  11. package/examples/vanilla/index.html +33 -0
  12. package/examples/vanilla/main.js +247 -0
  13. package/examples/vanilla/package.json +15 -0
  14. package/examples/vanilla/public/runtime-config.js +10 -0
  15. package/examples/vanilla/style.css +99 -0
  16. package/package.json +31 -0
  17. package/packages/core/package.json +30 -0
  18. package/packages/core/src/client.ts +1132 -0
  19. package/packages/core/src/customer-support-client.ts +825 -0
  20. package/packages/core/src/customer-support-types.ts +123 -0
  21. package/packages/core/src/index.ts +79 -0
  22. package/packages/core/src/notification-center-client.ts +462 -0
  23. package/packages/core/src/notification-center-types.ts +154 -0
  24. package/packages/core/src/protocol.ts +237 -0
  25. package/packages/core/src/types.ts +170 -0
  26. package/packages/core/tests/client.test.ts +760 -0
  27. package/packages/core/tests/customer-support-client.test.ts +576 -0
  28. package/packages/core/tests/fake-websocket.ts +55 -0
  29. package/packages/core/tests/notification-center-client.test.ts +467 -0
  30. package/packages/core/tests/protocol.test.ts +124 -0
  31. package/packages/core/tsconfig.json +7 -0
  32. package/packages/core/tsup.config.ts +11 -0
  33. package/packages/web-components/package.json +35 -0
  34. package/packages/web-components/scripts/verify-runtime-build.mjs +111 -0
  35. package/packages/web-components/src/account-context.ts +1540 -0
  36. package/packages/web-components/src/application-switcher-transport.ts +243 -0
  37. package/packages/web-components/src/application-switcher.ts +1103 -0
  38. package/packages/web-components/src/chat.ts +1754 -0
  39. package/packages/web-components/src/customer-support-chat.ts +862 -0
  40. package/packages/web-components/src/customer-support-transport.ts +269 -0
  41. package/packages/web-components/src/index.ts +256 -0
  42. package/packages/web-components/src/notification-center.ts +1066 -0
  43. package/packages/web-components/src/notification-popover.ts +615 -0
  44. package/packages/web-components/src/organization-multi-employee-selector.ts +1564 -0
  45. package/packages/web-components/src/organization-onboarding-transport.ts +54 -0
  46. package/packages/web-components/src/organization-onboarding.ts +1216 -0
  47. package/packages/web-components/src/organization-selector.ts +534 -0
  48. package/packages/web-components/src/organization-single-employee-selector.ts +114 -0
  49. package/packages/web-components/src/prototype-icons.ts +242 -0
  50. package/packages/web-components/src/safe-image-url.ts +29 -0
  51. package/packages/web-components/src/safe-markdown.ts +192 -0
  52. package/packages/web-components/src/uniplat-base-account-context-transport.ts +625 -0
  53. package/packages/web-components/src/uniplat-base-application-switcher-transport.ts +418 -0
  54. package/packages/web-components/src/uniplat-base-application-ticket.ts +254 -0
  55. package/packages/web-components/src/uniplat-base-identity-ticket.ts +202 -0
  56. package/packages/web-components/src/uniplat-base-notification-center-transport.ts +378 -0
  57. package/packages/web-components/src/uniplat-base-organization-multi-employee-selector-transport.ts +394 -0
  58. package/packages/web-components/src/uniplat-base-organization-onboarding-transport.ts +183 -0
  59. package/packages/web-components/src/uniplat-base-organization-single-employee-selector-transport.ts +14 -0
  60. package/packages/web-components/src/user-login-transport.ts +563 -0
  61. package/packages/web-components/src/user-login.ts +2027 -0
  62. package/packages/web-components/src/user-menu.ts +880 -0
  63. package/packages/web-components/tests/account-context.test.ts +798 -0
  64. package/packages/web-components/tests/application-switcher.test.ts +562 -0
  65. package/packages/web-components/tests/chat.test.ts +1130 -0
  66. package/packages/web-components/tests/customer-support-chat.test.ts +428 -0
  67. package/packages/web-components/tests/customer-support-transport.test.ts +79 -0
  68. package/packages/web-components/tests/notification-center-transport.test.ts +271 -0
  69. package/packages/web-components/tests/notification-center.test.ts +429 -0
  70. package/packages/web-components/tests/notification-popover.test.ts +283 -0
  71. package/packages/web-components/tests/organization-multi-employee-selector-transport.test.ts +332 -0
  72. package/packages/web-components/tests/organization-multi-employee-selector.test.ts +225 -0
  73. package/packages/web-components/tests/organization-onboarding.test.ts +260 -0
  74. package/packages/web-components/tests/organization-selector.test.ts +127 -0
  75. package/packages/web-components/tests/organization-single-employee-selector.test.ts +145 -0
  76. package/packages/web-components/tests/uniplat-base-account-context-transport.test.ts +496 -0
  77. package/packages/web-components/tests/uniplat-base-application-switcher-transport.test.ts +598 -0
  78. package/packages/web-components/tests/uniplat-base-application-ticket.test.ts +233 -0
  79. package/packages/web-components/tests/uniplat-base-identity-ticket.test.ts +226 -0
  80. package/packages/web-components/tests/uniplat-base-organization-onboarding-transport.test.ts +227 -0
  81. package/packages/web-components/tests/user-login-transport.test.ts +443 -0
  82. package/packages/web-components/tests/user-login.test.ts +626 -0
  83. package/packages/web-components/tests/user-menu.test.ts +271 -0
  84. package/packages/web-components/tsconfig.json +8 -0
  85. package/packages/web-components/tsup.config.ts +12 -0
  86. package/packages/web-components/tsup.runtime.config.ts +37 -0
  87. package/pnpm-workspace.yaml +6 -0
  88. package/tsconfig.base.json +21 -0
  89. package/vitest.config.ts +20 -0
  90. package//344/272/244/346/216/245/346/226/207/346/241/243.md +254 -0
@@ -0,0 +1,625 @@
1
+ import type {
2
+ AccountContextOrganization,
3
+ AccountContextEmployeeInfo,
4
+ AccountContextProjection,
5
+ AccountContextUser,
6
+ } from './account-context'
7
+ import { normalizeSafeImageUrl } from './safe-image-url'
8
+ import {
9
+ createApplicationTicketLaunchResult,
10
+ resolveUniplatBaseApplicationEntryUrl,
11
+ } from './uniplat-base-application-ticket'
12
+ import type { ApplicationLaunchResult } from './application-switcher-transport'
13
+
14
+ export type AccountContextApplicationScope = 'open' | 'organization'
15
+
16
+ export type UniplatBaseAccountContextTransportErrorCode =
17
+ | 'AUTH_REQUIRED'
18
+ | 'INVALID_RESPONSE'
19
+ | 'ORGANIZATION_UNAVAILABLE'
20
+ | 'ORGANIZATION_SWITCH_UNSUPPORTED'
21
+ | 'REQUEST_FAILED'
22
+ | 'TOKEN_PERSIST_FAILED'
23
+
24
+ export interface UniplatBaseAccountContextIdentity {
25
+ xid: string
26
+ oid: string
27
+ eid: string
28
+ mid: string
29
+ applicationName: string
30
+ applicationScope: AccountContextApplicationScope
31
+ }
32
+
33
+ export interface UniplatBaseAccountContextProjection extends AccountContextProjection {
34
+ applicationName: string
35
+ applicationScope: AccountContextApplicationScope
36
+ user: AccountContextUser
37
+ currentIdentity: UniplatBaseAccountContextIdentity
38
+ currentOrganization: AccountContextOrganization | null
39
+ currentOrganizationId: string | null
40
+ organizations: readonly AccountContextOrganization[]
41
+ }
42
+
43
+ export interface UniplatBaseAccountContextTransport {
44
+ load(applicationName: string): Promise<UniplatBaseAccountContextProjection>
45
+ loadCurrentEmployeeInfo(): Promise<AccountContextEmployeeInfo>
46
+ switchOrganization(
47
+ applicationName: string,
48
+ organizationId: string,
49
+ ): Promise<UniplatBaseAccountContextProjection>
50
+ launchAccountSettings(applicationName?: string): Promise<ApplicationLaunchResult>
51
+ }
52
+
53
+ export interface UniplatBaseAccountContextTransportOptions {
54
+ serviceBaseUrl: string
55
+ getAccessToken(): string | Promise<string>
56
+ setAccessToken(
57
+ accessToken: string,
58
+ metadata: UniplatBaseAccountContextProjection,
59
+ ): void | Promise<void>
60
+ fetch?: typeof fetch
61
+ }
62
+
63
+ interface ApplicationTarget {
64
+ xid: string
65
+ oid: string
66
+ applicationName: string
67
+ applicationScope: AccountContextApplicationScope
68
+ }
69
+
70
+ interface LoadedContext {
71
+ projection: UniplatBaseAccountContextProjection
72
+ targets: readonly ApplicationTarget[]
73
+ }
74
+
75
+ const LIST_PATH = 'application.identity/list_available_applications'
76
+ const ORGANIZATIONS_PATH = 'application.identity/list_available_organizations'
77
+ const SWITCH_PATH = 'application.identity/switch_current_application'
78
+ const CURRENT_EMPLOYEE_INFO_PATH = 'system.employee_profile/get_current_employee_info'
79
+ const PERSONAL_CENTER_ENTRY_URL = '/personal'
80
+
81
+ export class UniplatBaseAccountContextTransportError extends Error {
82
+ readonly code: UniplatBaseAccountContextTransportErrorCode
83
+
84
+ constructor(code: UniplatBaseAccountContextTransportErrorCode, message: string) {
85
+ super(message)
86
+ this.name = 'UniplatBaseAccountContextTransportError'
87
+ this.code = code
88
+ }
89
+ }
90
+
91
+ export function createUniplatBaseAccountContextTransport(
92
+ options: UniplatBaseAccountContextTransportOptions,
93
+ ): UniplatBaseAccountContextTransport {
94
+ if (!options || typeof options.getAccessToken !== 'function') {
95
+ throw new TypeError('UniplatBase account context transport requires getAccessToken()')
96
+ }
97
+ if (typeof options.setAccessToken !== 'function') {
98
+ throw new TypeError('UniplatBase account context transport requires setAccessToken()')
99
+ }
100
+ const serviceBaseUrl = validateServiceBaseUrl(options.serviceBaseUrl)
101
+ const listEndpoint = `${serviceBaseUrl}/${LIST_PATH}`
102
+ const organizationsEndpoint = `${serviceBaseUrl}/${ORGANIZATIONS_PATH}`
103
+ const switchEndpoint = `${serviceBaseUrl}/${SWITCH_PATH}`
104
+ const currentEmployeeInfoEndpoint = `${serviceBaseUrl}/${CURRENT_EMPLOYEE_INFO_PATH}`
105
+ const fetchImplementation = options.fetch ?? globalThis.fetch
106
+
107
+ const loadContext = async (
108
+ applicationName: string,
109
+ accessToken: string,
110
+ ): Promise<LoadedContext> => {
111
+ const [applicationsData, organizationsData] = await Promise.all([
112
+ requestData(fetchImplementation, listEndpoint, accessToken, {}, false),
113
+ requestData(
114
+ fetchImplementation,
115
+ organizationsEndpoint,
116
+ accessToken,
117
+ { application_name: applicationName },
118
+ false,
119
+ ),
120
+ ])
121
+ return parseLoadedContext(applicationsData, organizationsData, applicationName, serviceBaseUrl)
122
+ }
123
+
124
+ return {
125
+ async load(applicationName) {
126
+ const normalizedApplicationName = requiredText(applicationName, 'applicationName', 255)
127
+ const accessToken = await resolveAccessToken(options.getAccessToken)
128
+ return (await loadContext(normalizedApplicationName, accessToken)).projection
129
+ },
130
+
131
+ async loadCurrentEmployeeInfo() {
132
+ const accessToken = await resolveAccessToken(options.getAccessToken)
133
+ const data = await requestData(
134
+ fetchImplementation,
135
+ currentEmployeeInfoEndpoint,
136
+ accessToken,
137
+ {},
138
+ false,
139
+ )
140
+ return parseCurrentEmployeeInfo(data)
141
+ },
142
+
143
+ async switchOrganization(applicationName, organizationId) {
144
+ const normalizedApplicationName = requiredText(applicationName, 'applicationName', 255)
145
+ const normalizedOrganizationId = positiveIdentifier(organizationId)
146
+ if (!normalizedOrganizationId) {
147
+ throw new UniplatBaseAccountContextTransportError(
148
+ 'ORGANIZATION_UNAVAILABLE',
149
+ 'Organization is unavailable',
150
+ )
151
+ }
152
+
153
+ const accessToken = await resolveAccessToken(options.getAccessToken)
154
+ const current = await loadContext(normalizedApplicationName, accessToken)
155
+ if (
156
+ !current.projection.organizations.some(
157
+ (organization) => organization.id === normalizedOrganizationId,
158
+ )
159
+ ) {
160
+ throw new UniplatBaseAccountContextTransportError(
161
+ 'ORGANIZATION_UNAVAILABLE',
162
+ 'Organization is unavailable',
163
+ )
164
+ }
165
+
166
+ const body = switchRequestBody(current, normalizedOrganizationId)
167
+ const switchedData = await requestData(
168
+ fetchImplementation,
169
+ switchEndpoint,
170
+ accessToken,
171
+ body,
172
+ true,
173
+ )
174
+ const nextAccessToken = accessTokenFromResponse(switchedData)
175
+ const projection = (await loadContext(normalizedApplicationName, nextAccessToken)).projection
176
+ if (projection.currentOrganizationId !== normalizedOrganizationId) {
177
+ throw invalidResponse()
178
+ }
179
+ try {
180
+ await options.setAccessToken(nextAccessToken, projection)
181
+ } catch {
182
+ throw new UniplatBaseAccountContextTransportError(
183
+ 'TOKEN_PERSIST_FAILED',
184
+ 'Account context token could not be saved',
185
+ )
186
+ }
187
+ return projection
188
+ },
189
+
190
+ async launchAccountSettings(applicationName) {
191
+ const accessToken = await resolveAccessToken(options.getAccessToken)
192
+ const targetApplicationName = requiredText(applicationName, 'applicationName', 255)
193
+ const entryUrl = resolveUniplatBaseApplicationEntryUrl(PERSONAL_CENTER_ENTRY_URL)
194
+ try {
195
+ const data = await requestData(
196
+ fetchImplementation,
197
+ switchEndpoint,
198
+ accessToken,
199
+ {
200
+ application_name: targetApplicationName,
201
+ response_type: 'ticket',
202
+ entry_url: entryUrl,
203
+ },
204
+ false,
205
+ )
206
+ return createApplicationTicketLaunchResult(entryUrl, data.ticket, data.expires_in)
207
+ } catch (error) {
208
+ if (error instanceof UniplatBaseAccountContextTransportError) throw error
209
+ throw new UniplatBaseAccountContextTransportError(
210
+ 'REQUEST_FAILED',
211
+ 'Account settings request failed',
212
+ )
213
+ }
214
+ },
215
+ }
216
+ }
217
+
218
+ function switchRequestBody(
219
+ context: LoadedContext,
220
+ organizationId: string,
221
+ ): Record<string, unknown> {
222
+ const { applicationName, applicationScope } = context.projection
223
+ if (applicationScope === 'open') {
224
+ const target = context.targets.find(
225
+ (candidate) =>
226
+ candidate.applicationName === applicationName && candidate.applicationScope === 'open',
227
+ )
228
+ if (!target) throw invalidResponse()
229
+ return {
230
+ application_name: applicationName,
231
+ xid: target.xid,
232
+ organization_oid: organizationId,
233
+ }
234
+ }
235
+
236
+ const target = context.targets.find(
237
+ (candidate) =>
238
+ candidate.applicationName === applicationName &&
239
+ candidate.applicationScope === 'organization' &&
240
+ candidate.oid === organizationId,
241
+ )
242
+ if (!target) {
243
+ throw new UniplatBaseAccountContextTransportError(
244
+ 'ORGANIZATION_SWITCH_UNSUPPORTED',
245
+ 'Organization switch is unavailable for this application',
246
+ )
247
+ }
248
+ return { application_name: applicationName, xid: target.xid }
249
+ }
250
+
251
+ async function resolveAccessToken(
252
+ getAccessToken: UniplatBaseAccountContextTransportOptions['getAccessToken'],
253
+ ): Promise<string> {
254
+ let accessToken: string
255
+ try {
256
+ accessToken = await getAccessToken()
257
+ } catch {
258
+ throw new UniplatBaseAccountContextTransportError(
259
+ 'AUTH_REQUIRED',
260
+ 'Account context authentication is required',
261
+ )
262
+ }
263
+ const normalized = typeof accessToken === 'string' ? accessToken.trim() : ''
264
+ if (!normalized) {
265
+ throw new UniplatBaseAccountContextTransportError(
266
+ 'AUTH_REQUIRED',
267
+ 'Account context authentication is required',
268
+ )
269
+ }
270
+ return normalized
271
+ }
272
+
273
+ async function requestData(
274
+ fetchImplementation: typeof fetch | undefined,
275
+ endpoint: string,
276
+ accessToken: string,
277
+ body: Record<string, unknown>,
278
+ containsToken: boolean,
279
+ ): Promise<Record<string, unknown>> {
280
+ if (!fetchImplementation) {
281
+ throw new UniplatBaseAccountContextTransportError(
282
+ 'REQUEST_FAILED',
283
+ 'Account context request failed',
284
+ )
285
+ }
286
+ let response: Response
287
+ let payload: unknown
288
+ try {
289
+ response = await fetchImplementation(endpoint, {
290
+ method: 'POST',
291
+ credentials: 'include',
292
+ headers: {
293
+ accept: 'application/json',
294
+ authorization: `Bearer ${accessToken}`,
295
+ 'content-type': 'application/json',
296
+ },
297
+ body: JSON.stringify(body),
298
+ })
299
+ payload = await response.json()
300
+ } catch {
301
+ throw new UniplatBaseAccountContextTransportError(
302
+ 'REQUEST_FAILED',
303
+ 'Account context request failed',
304
+ )
305
+ }
306
+ const envelope = record(payload)
307
+ if (!response.ok || integer(envelope.rescode) !== 0) {
308
+ throw new UniplatBaseAccountContextTransportError(
309
+ 'REQUEST_FAILED',
310
+ 'Account context request failed',
311
+ )
312
+ }
313
+ const data = record(envelope.data)
314
+ if (Object.keys(data).length === 0 || (containsToken && !text(data.token).trim())) {
315
+ throw invalidResponse()
316
+ }
317
+ return data
318
+ }
319
+
320
+ function parseLoadedContext(
321
+ applicationsData: Record<string, unknown>,
322
+ organizationsData: Record<string, unknown>,
323
+ applicationName: string,
324
+ serviceBaseUrl: string,
325
+ ): LoadedContext {
326
+ try {
327
+ const projection = parseProjection(
328
+ applicationsData,
329
+ organizationsData,
330
+ applicationName,
331
+ serviceBaseUrl,
332
+ )
333
+ if (!Array.isArray(applicationsData.available_applications)) throw invalidResponse()
334
+ const targets = applicationsData.available_applications.map(parseApplicationTarget)
335
+ if (targets.length === 0) throw invalidResponse()
336
+ return { projection, targets: Object.freeze(targets) }
337
+ } catch (error) {
338
+ if (error instanceof UniplatBaseAccountContextTransportError) throw error
339
+ throw invalidResponse()
340
+ }
341
+ }
342
+
343
+ function parseProjection(
344
+ applicationsData: Record<string, unknown>,
345
+ organizationsData: Record<string, unknown>,
346
+ expectedApplicationName: string,
347
+ serviceBaseUrl: string,
348
+ ): UniplatBaseAccountContextProjection {
349
+ try {
350
+ const applicationName = requiredText(
351
+ organizationsData.application_name,
352
+ 'application_name',
353
+ 255,
354
+ )
355
+ const applicationScope = parseApplicationScope(organizationsData.application_scope)
356
+ if (applicationName !== expectedApplicationName) throw invalidResponse()
357
+ const currentIdentity = parseCurrentIdentity(applicationsData.current_identity)
358
+ if (
359
+ currentIdentity.applicationName !== applicationName ||
360
+ currentIdentity.applicationScope !== applicationScope
361
+ ) {
362
+ throw invalidResponse()
363
+ }
364
+ if (
365
+ !Array.isArray(organizationsData.organizations) ||
366
+ !Array.isArray(applicationsData.available_applications)
367
+ ) {
368
+ throw invalidResponse()
369
+ }
370
+ applicationsData.available_applications.forEach(parseApplicationTarget)
371
+ const organizations = Object.freeze(
372
+ organizationsData.organizations.map((organization) =>
373
+ parseOrganization(organization, serviceBaseUrl),
374
+ ),
375
+ )
376
+ const organizationIds = new Set(organizations.map((organization) => organization.id))
377
+ if (organizationIds.size !== organizations.length) throw invalidResponse()
378
+ const currentOrganizationId = currentIdentity.oid === '0' ? null : currentIdentity.oid
379
+ const currentOrganization = currentOrganizationId
380
+ ? (organizations.find((organization) => organization.id === currentOrganizationId) ?? null)
381
+ : null
382
+ if (currentOrganizationId && !currentOrganization) throw invalidResponse()
383
+ const user = parseUser(organizationsData, currentOrganization)
384
+ return Object.freeze({
385
+ applicationName,
386
+ applicationScope,
387
+ user,
388
+ currentIdentity,
389
+ currentOrganization,
390
+ currentOrganizationId,
391
+ organizations,
392
+ })
393
+ } catch (error) {
394
+ if (error instanceof UniplatBaseAccountContextTransportError) throw error
395
+ throw invalidResponse()
396
+ }
397
+ }
398
+
399
+ function parseUser(
400
+ data: Record<string, unknown>,
401
+ currentOrganization: AccountContextOrganization | null,
402
+ ): AccountContextUser {
403
+ const displayName =
404
+ text(data.display_name).trim().slice(0, 80) ||
405
+ currentOrganization?.employeeDisplayName ||
406
+ '用户'
407
+ const maskedMobile = text(data.mobile_masked).trim()
408
+ const result: AccountContextUser = { displayName }
409
+ if (maskedMobile && !/^1[3-9]\d{9}$/.test(maskedMobile)) result.maskedMobile = maskedMobile
410
+ try {
411
+ const avatarUrl = normalizeSafeImageUrl(data.avatar_url, 'account-context avatar URL')
412
+ if (avatarUrl) result.avatarUrl = avatarUrl
413
+ } catch {
414
+ // User avatars are cosmetic. A legacy file locator or a malformed URL
415
+ // must not prevent account and organization context from loading.
416
+ }
417
+ return Object.freeze(result)
418
+ }
419
+
420
+ function parseCurrentIdentity(value: unknown): UniplatBaseAccountContextIdentity {
421
+ const identity = record(value)
422
+ const applicationScope = parseApplicationScope(identity.application_scope)
423
+ const xid = nonNegativeIdentifier(identity.xid)
424
+ const oid = nonNegativeIdentifier(identity.oid)
425
+ const eid = nonNegativeIdentifier(identity.eid)
426
+ const mid = nonNegativeIdentifier(identity.mid)
427
+ if (
428
+ xid === '' ||
429
+ (applicationScope === 'organization' && xid === '0') ||
430
+ oid === '' ||
431
+ eid === '' ||
432
+ mid === ''
433
+ ) {
434
+ throw invalidResponse()
435
+ }
436
+ return Object.freeze({
437
+ xid,
438
+ oid,
439
+ eid,
440
+ mid,
441
+ applicationName: requiredText(identity.application_name, 'application_name', 255),
442
+ applicationScope,
443
+ })
444
+ }
445
+
446
+ function parseApplicationTarget(value: unknown): ApplicationTarget {
447
+ const target = record(value)
448
+ const applicationScope = parseApplicationScope(target.application_scope)
449
+ const xid = nonNegativeIdentifier(target.xid)
450
+ const oid = nonNegativeIdentifier(target.oid)
451
+ if (xid === '' || (applicationScope === 'organization' && xid === '0') || oid === '') {
452
+ throw invalidResponse()
453
+ }
454
+ return Object.freeze({
455
+ xid,
456
+ oid,
457
+ applicationName: requiredText(target.application_name, 'application_name', 255),
458
+ applicationScope,
459
+ })
460
+ }
461
+
462
+ function parseOrganization(value: unknown, serviceBaseUrl: string): AccountContextOrganization {
463
+ const organization = record(value)
464
+ const id = positiveIdentifier(organization.oid)
465
+ const employeeId = positiveIdentifier(organization.eid)
466
+ if (!id || !employeeId) throw invalidResponse()
467
+ const type = text(organization.organization_type).trim()
468
+ if (type !== 'enterprise' && type !== 'team') throw invalidResponse()
469
+ const name = text(organization.team_name).trim() || `组织 ${id}`
470
+ const companyName = text(organization.customer_name).trim()
471
+ const employeeDisplayName = text(organization.employee_display_name).trim()
472
+ const employeeAvatarUrl = employeeDisplayName
473
+ ? normalizeEmployeeAvatarUrl(organization.employee_avatar_url, employeeId, serviceBaseUrl)
474
+ : ''
475
+ const role = roleLabel(text(organization.role).trim())
476
+ const memberCount = nonNegativeInteger(organization.member_count)
477
+ const result: AccountContextOrganization = {
478
+ id,
479
+ name,
480
+ type,
481
+ role,
482
+ memberCount,
483
+ }
484
+ if (employeeDisplayName) result.employeeDisplayName = employeeDisplayName
485
+ if (employeeAvatarUrl) result.employeeAvatarUrl = employeeAvatarUrl
486
+ if (companyName) result.companyName = companyName
487
+ if (type === 'enterprise') {
488
+ result.verificationStatus = parseVerificationStatus(organization.verification_status)
489
+ }
490
+ return Object.freeze(result)
491
+ }
492
+
493
+ function parseCurrentEmployeeInfo(value: Record<string, unknown>): AccountContextEmployeeInfo {
494
+ const primaryDepartment = record(value.primary_department)
495
+ const departmentName = text(primaryDepartment.name).trim()
496
+ return Object.freeze({ departmentName: departmentName || null })
497
+ }
498
+
499
+ function normalizeEmployeeAvatarUrl(
500
+ value: unknown,
501
+ employeeId: string,
502
+ serviceBaseUrl: string,
503
+ ): string {
504
+ const rawValue = text(value).trim()
505
+ if (!rawValue) return ''
506
+ const managedPath = rawValue.match(
507
+ /^(\d+)\/\.root\/\.avatars\/(\d+)\/avatar-([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.(?:jpg|png|webp)$/,
508
+ )
509
+ if (!managedPath) {
510
+ return normalizeSafeImageUrl(rawValue, 'account-context employee avatar URL')
511
+ }
512
+ const xid = positiveIdentifier(managedPath[1])
513
+ const pathEmployeeId = positiveIdentifier(managedPath[2])
514
+ const fileId = managedPath[3]
515
+ if (!xid || pathEmployeeId !== employeeId || !fileId) throw invalidResponse()
516
+ const avatarUrl = new URL(`${serviceBaseUrl}/anonymous/system.employee_profile/get_avatar`)
517
+ avatarUrl.searchParams.set('xid', xid)
518
+ avatarUrl.searchParams.set('eid', employeeId)
519
+ avatarUrl.searchParams.set('file_id', fileId)
520
+ return normalizeSafeImageUrl(avatarUrl.href, 'account-context employee avatar URL')
521
+ }
522
+
523
+ function parseApplicationScope(value: unknown): AccountContextApplicationScope {
524
+ if (value === 'open' || value === 'organization') return value
525
+ throw invalidResponse()
526
+ }
527
+
528
+ function parseVerificationStatus(
529
+ value: unknown,
530
+ ): 'unverified' | 'pending' | 'rejected' | 'verified' | 'expired' {
531
+ if (['unverified', 'pending', 'rejected', 'verified', 'expired'].includes(String(value))) {
532
+ return value as 'unverified' | 'pending' | 'rejected' | 'verified' | 'expired'
533
+ }
534
+ throw invalidResponse()
535
+ }
536
+
537
+ function roleLabel(value: string): string {
538
+ if (value === 'owner') return '所有者'
539
+ if (value === 'admin') return '管理员'
540
+ if (value === 'member') return '成员'
541
+ return ''
542
+ }
543
+
544
+ function accessTokenFromResponse(data: Record<string, unknown>): string {
545
+ const accessToken = text(data.token).trim()
546
+ if (!accessToken) throw invalidResponse()
547
+ return accessToken
548
+ }
549
+
550
+ function validateServiceBaseUrl(rawValue: string): string {
551
+ const value = typeof rawValue === 'string' ? rawValue.trim() : ''
552
+ if (!value) throw new TypeError('UniplatBase serviceBaseUrl is required')
553
+ const documentBase = typeof document === 'undefined' ? 'https://localhost/' : document.baseURI
554
+ let url: URL
555
+ try {
556
+ url = new URL(value, documentBase)
557
+ } catch {
558
+ throw new TypeError('UniplatBase serviceBaseUrl is invalid')
559
+ }
560
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
561
+ throw new TypeError('UniplatBase serviceBaseUrl must use HTTP(S) without user credentials')
562
+ }
563
+ if (url.search || url.hash) {
564
+ throw new TypeError('UniplatBase serviceBaseUrl must not contain query or hash')
565
+ }
566
+ return url.href.replace(/\/+$/, '')
567
+ }
568
+
569
+ function requiredText(value: unknown, label: string, maxLength: number): string {
570
+ const normalized = text(value).trim()
571
+ if (!normalized || normalized.length > maxLength) {
572
+ throw new TypeError(`${label} is invalid`)
573
+ }
574
+ return normalized
575
+ }
576
+
577
+ function positiveIdentifier(value: unknown): string {
578
+ const normalized = nonNegativeIdentifier(value)
579
+ return normalized && normalized !== '0' ? normalized : ''
580
+ }
581
+
582
+ function nonNegativeIdentifier(value: unknown): string {
583
+ if (typeof value === 'number') {
584
+ return Number.isSafeInteger(value) && value >= 0 ? String(value) : ''
585
+ }
586
+ if (typeof value !== 'string') return ''
587
+ const normalized = value.trim()
588
+ return /^\d+$/.test(normalized) ? normalized.replace(/^0+(?=\d)/, '') : ''
589
+ }
590
+
591
+ function nonNegativeInteger(value: unknown): number {
592
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) return value
593
+ if (typeof value === 'string' && /^\d+$/.test(value.trim())) {
594
+ const parsed = Number(value)
595
+ if (Number.isSafeInteger(parsed)) return parsed
596
+ }
597
+ throw invalidResponse()
598
+ }
599
+
600
+ function record(value: unknown): Record<string, unknown> {
601
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
602
+ ? (value as Record<string, unknown>)
603
+ : {}
604
+ }
605
+
606
+ function text(value: unknown): string {
607
+ return typeof value === 'string'
608
+ ? value
609
+ : typeof value === 'number' && Number.isFinite(value)
610
+ ? String(value)
611
+ : ''
612
+ }
613
+
614
+ function integer(value: unknown): number | null {
615
+ if (typeof value === 'number' && Number.isSafeInteger(value)) return value
616
+ if (typeof value === 'string' && /^-?\d+$/.test(value.trim())) return Number(value)
617
+ return null
618
+ }
619
+
620
+ function invalidResponse(): UniplatBaseAccountContextTransportError {
621
+ return new UniplatBaseAccountContextTransportError(
622
+ 'INVALID_RESPONSE',
623
+ 'Account context response is invalid',
624
+ )
625
+ }