@zskj-idaas/sdk 1.0.3 → 2.3.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.
- package/README.md +99 -12
- package/dist/index.d.mts +58 -3
- package/dist/index.d.ts +58 -3
- package/dist/index.js +63 -15
- package/dist/index.mjs +62 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
IDaaS OIDC Client SDK — 适用于浏览器应用的 OpenID Connect 客户端,支持 Authorization Code Flow with PKCE。
|
|
4
4
|
|
|
5
|
+
当前生产环境的 OIDC Issuer 是 `https://account.genisomai.com`。本文中的 IDaaS 端点均填写为完整生产地址。
|
|
6
|
+
|
|
5
7
|
## 特性
|
|
6
8
|
|
|
7
9
|
- **零运行时依赖** — 轻量、无外部依赖
|
|
@@ -28,9 +30,10 @@ yarn add @zskj-idaas/sdk
|
|
|
28
30
|
import { IdaasClient } from '@zskj-idaas/sdk';
|
|
29
31
|
|
|
30
32
|
const client = new IdaasClient({
|
|
31
|
-
issuer: 'https://
|
|
32
|
-
clientId: '
|
|
33
|
-
redirectUri:
|
|
33
|
+
issuer: 'https://account.genisomai.com',
|
|
34
|
+
clientId: 'my-app',
|
|
35
|
+
redirectUri: `${window.location.origin}/callback`,
|
|
36
|
+
postLogoutRedirectUri: window.location.origin,
|
|
34
37
|
scope: 'openid profile email',
|
|
35
38
|
});
|
|
36
39
|
|
|
@@ -41,6 +44,30 @@ await client.init();
|
|
|
41
44
|
await client.login();
|
|
42
45
|
```
|
|
43
46
|
|
|
47
|
+
`clientId` 必须替换为 IDaaS 管理员分配的 Public Client ID。当前 Origin、`${window.location.origin}/callback` 和 `window.location.origin` 必须分别登记到管理后台的 CORS Origins、Redirect URIs 和 Post Logout Redirect URIs。
|
|
48
|
+
|
|
49
|
+
## 生产端点
|
|
50
|
+
|
|
51
|
+
SDK 默认从 Discovery 自动读取标准 OIDC 端点。当前生产环境的完整地址如下:
|
|
52
|
+
|
|
53
|
+
| 用途 | 方法 | 完整地址 |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| OIDC Discovery | GET | `https://account.genisomai.com/.well-known/openid-configuration` |
|
|
56
|
+
| JWKS 公钥 | GET | `https://account.genisomai.com/.well-known/jwks.json` |
|
|
57
|
+
| Authorization | GET | `https://account.genisomai.com/oauth2/authorize` |
|
|
58
|
+
| Token | POST | `https://account.genisomai.com/oauth2/token` |
|
|
59
|
+
| UserInfo | GET / POST | `https://account.genisomai.com/oauth2/userinfo` |
|
|
60
|
+
| 企业资料(当前用户) | GET | `https://account.genisomai.com/oauth2/enterprise-profile` |
|
|
61
|
+
| Business Token | POST | `https://account.genisomai.com/oauth2/business-token` |
|
|
62
|
+
| Logout | GET | `https://account.genisomai.com/oauth2/logout` |
|
|
63
|
+
| Revoke | POST | `https://account.genisomai.com/oauth2/revoke` |
|
|
64
|
+
| Introspect | POST | `https://account.genisomai.com/oauth2/introspect` |
|
|
65
|
+
| 清理 IDaaS Session | POST | `https://account.genisomai.com/auth/logout` |
|
|
66
|
+
| Service Token Verify(仅业务后端) | POST | `https://account.genisomai.com/api/service-auth/verify` |
|
|
67
|
+
| Authorization Evaluate(仅业务后端,以 `my-app-service` 为例) | POST | `https://account.genisomai.com/api/service-auth/my-app-service/evaluate` |
|
|
68
|
+
|
|
69
|
+
浏览器不能持有 ServiceCredential,因此不能直接调用最后两个服务端授权端点。`my-app-service` 是 Service Key 示例,实际接入时使用管理员分配的值。
|
|
70
|
+
|
|
44
71
|
## API 参考
|
|
45
72
|
|
|
46
73
|
### `new IdaasClient(config)`
|
|
@@ -51,7 +78,6 @@ await client.login();
|
|
|
51
78
|
interface IdaasConfig {
|
|
52
79
|
issuer: string; // OIDC 提供者地址
|
|
53
80
|
clientId: string; // 客户端 ID
|
|
54
|
-
clientSecret?: string; // 客户端密钥(机密客户端)
|
|
55
81
|
redirectUri: string; // 登录回调地址
|
|
56
82
|
postLogoutRedirectUri?: string; // 登出后跳转地址
|
|
57
83
|
scope?: string; // 请求的 scope(默认: 'openid profile email')
|
|
@@ -60,6 +86,8 @@ interface IdaasConfig {
|
|
|
60
86
|
}
|
|
61
87
|
```
|
|
62
88
|
|
|
89
|
+
该 SDK 仅用于浏览器 Public Client,始终使用 PKCE,不能配置或传输 Client Secret。
|
|
90
|
+
|
|
63
91
|
### `client.init(): Promise<AuthState>`
|
|
64
92
|
|
|
65
93
|
初始化客户端,执行 OIDC Discovery 并返回当前认证状态。
|
|
@@ -70,7 +98,7 @@ interface IdaasConfig {
|
|
|
70
98
|
|
|
71
99
|
```typescript
|
|
72
100
|
// 附加额外参数
|
|
73
|
-
await client.login({ prompt: 'login', login_hint: 'user@
|
|
101
|
+
await client.login({ prompt: 'login', login_hint: 'user@company.test' });
|
|
74
102
|
```
|
|
75
103
|
|
|
76
104
|
### `client.handleCallback(callbackUrl?): Promise<TokenSet>`
|
|
@@ -86,10 +114,56 @@ const tokens = await client.handleCallback();
|
|
|
86
114
|
|
|
87
115
|
获取当前用户信息。
|
|
88
116
|
|
|
117
|
+
管理员为 Client 开放 `feishu_identity`,并在登录时申请且经用户同意后,可返回以下可选字段:
|
|
118
|
+
|
|
119
|
+
- `feishu_union_id` 与 `feishu_tenant_key`:必须成对完整,供同企业、同应用开发商范围内的跨飞书应用身份关联。
|
|
120
|
+
- `feishu_open_id`:同一身份元数据中合法的 Open ID,供 IDaaS 所用飞书应用内关联,可与 `primary_department_leaders[].feishu_open_id` 对应。
|
|
121
|
+
|
|
122
|
+
未授权、身份歧义或 UnionID/企业标识缺失时不返回上述字段;仅 Open ID 缺失或非法时省略 `feishu_open_id`,保留原有完整关联键。不得从用户名推导 Open ID,也不得用它跨飞书应用匹配。身份字段不授予业务权限,不写入 ID Token 或业务 Token。
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const user = await client.getUserInfo(true); // 强制重新获取,避免沿用先前缓存
|
|
126
|
+
const feishuOpenId = user?.feishu_open_id;
|
|
127
|
+
// 使用 feishuOpenId 进行同一飞书应用内关联;缺失时跳过关联,不记录身份值到日志。
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `client.getEnterpriseProfile(): Promise<EnterpriseProfile | null>`
|
|
131
|
+
|
|
132
|
+
按当前应用获准且用户已同意的 Scope 获取当前用户的敏感企业资料。支持的独立 Scope 为 `phone`、`employee_number` 和 `primary_department`;对应字段不会进入 ID Token。
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { EnterpriseProfileError } from '@zskj-idaas/sdk';
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const profile = await client.getEnterpriseProfile();
|
|
139
|
+
console.log(profile?.employee_number);
|
|
140
|
+
console.log(profile?.primary_department_path_name);
|
|
141
|
+
console.log(profile?.primary_department_leaders);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error instanceof EnterpriseProfileError && error.retryable) {
|
|
144
|
+
// 提示用户稍后重试,不要把查询失败当成字段为空。
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`primary_department_path` 按一级部门到叶子部门排序,每项包含 `id` 和 `name`,不包含公司根节点;`primary_department_path_name` 是使用 ` / ` 连接的可展示路径。`primary_department_leaders` 返回主部门的主负责人和副负责人,每项包含 `feishu_open_id`、`name`、`avatar_url` 和 `leader_type`;`leader_type` 取值为 `primary` 或 `deputy`。负责人仍只需 `primary_department` scope,读取本人 `feishu_open_id` 则需 `feishu_identity`。兼容旧服务时,调用方可回退到 `primary_department_name`。
|
|
150
|
+
|
|
151
|
+
### 2.3.0 兼容升级
|
|
152
|
+
|
|
153
|
+
本版本为 `EnterpriseDepartmentLeader` 新增 `feishu_open_id`,并保留 `open_id` 等值兼容别名(标记为废弃);新服务端同时返回两个字段。为兼容旧服务和 2.x 调用方创建的对象,SDK 将新增字段声明为可选,可使用 `leader.feishu_open_id ?? leader.open_id`。`UserInfo` 新增可选的 `feishu_open_id`、`feishu_union_id` 和 `feishu_tenant_key` 类型。SDK 版本更新不等于服务端已部署或 npm 已发布。
|
|
154
|
+
|
|
89
155
|
### `client.getAccessToken(): Promise<string | null>`
|
|
90
156
|
|
|
91
157
|
获取有效的 Access Token(过期时自动刷新)。
|
|
92
158
|
|
|
159
|
+
### `client.refreshToken(): Promise<TokenSet | null>`
|
|
160
|
+
|
|
161
|
+
使用 Refresh Token 主动刷新 Token;刷新失败时清理本地登录态并返回 `null`。
|
|
162
|
+
|
|
163
|
+
### `client.getServiceToken(serviceKey): Promise<string | null>`
|
|
164
|
+
|
|
165
|
+
调用 `https://account.genisomai.com/oauth2/business-token` 获取指定 Service 的业务 Token,并按 `serviceKey` 隔离缓存。
|
|
166
|
+
|
|
93
167
|
### `client.getIdToken(): string | null`
|
|
94
168
|
|
|
95
169
|
获取 ID Token。
|
|
@@ -114,7 +188,11 @@ interface AuthState {
|
|
|
114
188
|
|
|
115
189
|
### `client.logout(): Promise<void>`
|
|
116
190
|
|
|
117
|
-
执行 OIDC
|
|
191
|
+
清理本地 Token,并跳转到 `https://account.genisomai.com/oauth2/logout` 执行 OIDC 登出。
|
|
192
|
+
|
|
193
|
+
### `client.logoutSession(options?): Promise<void>`
|
|
194
|
+
|
|
195
|
+
清理本地 Token,并调用 `https://account.genisomai.com/auth/logout` 销毁 IDaaS Session;可通过 `redirectTo` 指定完成后的前端地址。
|
|
118
196
|
|
|
119
197
|
### `client.revokeToken(): Promise<void>`
|
|
120
198
|
|
|
@@ -122,10 +200,19 @@ interface AuthState {
|
|
|
122
200
|
|
|
123
201
|
### `client.fetch(url, init?): Promise<Response>`
|
|
124
202
|
|
|
125
|
-
|
|
203
|
+
带基础 IDaaS Access Token 的 fetch 封装,自动附加 Authorization 头。基础 Token 只允许用于 IDaaS UserInfo 和 Business Token 端点,不能用于业务 API;读取用户信息时优先使用 `getUserInfo()`。
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const res = await client.fetch('https://account.genisomai.com/oauth2/userinfo');
|
|
207
|
+
const data = await res.json();
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### `client.fetchService(serviceKey, url, init?): Promise<Response>`
|
|
211
|
+
|
|
212
|
+
获取指定 Service 的业务 Token,并以 Bearer Token 调用业务 API。
|
|
126
213
|
|
|
127
214
|
```typescript
|
|
128
|
-
const res = await client.
|
|
215
|
+
const res = await client.fetchService('my-app-service', '/api/customers');
|
|
129
216
|
const data = await res.json();
|
|
130
217
|
```
|
|
131
218
|
|
|
@@ -136,7 +223,7 @@ import { IdaasClient } from '@zskj-idaas/sdk';
|
|
|
136
223
|
import type { AuthState, UserInfo } from '@zskj-idaas/sdk';
|
|
137
224
|
|
|
138
225
|
const client = new IdaasClient({
|
|
139
|
-
issuer: 'https://
|
|
226
|
+
issuer: 'https://account.genisomai.com',
|
|
140
227
|
clientId: 'my-app',
|
|
141
228
|
redirectUri: `${window.location.origin}/callback`,
|
|
142
229
|
postLogoutRedirectUri: window.location.origin,
|
|
@@ -164,7 +251,7 @@ async function handleCallback() {
|
|
|
164
251
|
|
|
165
252
|
// 调用受保护的 API
|
|
166
253
|
async function fetchData() {
|
|
167
|
-
const res = await client.
|
|
254
|
+
const res = await client.fetchService('my-app-service', '/api/data');
|
|
168
255
|
return res.json();
|
|
169
256
|
}
|
|
170
257
|
```
|
|
@@ -173,14 +260,14 @@ async function fetchData() {
|
|
|
173
260
|
|
|
174
261
|
```typescript
|
|
175
262
|
// 类
|
|
176
|
-
export { IdaasClient } from './client';
|
|
263
|
+
export { EnterpriseProfileError, IdaasClient } from './client';
|
|
177
264
|
export { TokenStorage } from './storage';
|
|
178
265
|
|
|
179
266
|
// 工具函数
|
|
180
267
|
export { createPKCE, generateState, generateNonce } from './crypto';
|
|
181
268
|
|
|
182
269
|
// 类型
|
|
183
|
-
export type { IdaasConfig, TokenSet, UserInfo, AuthState } from './types';
|
|
270
|
+
export type { IdaasConfig, TokenSet, UserInfo, EnterpriseDepartmentPathItem, EnterpriseProfile, AuthState } from './types';
|
|
184
271
|
```
|
|
185
272
|
|
|
186
273
|
## 浏览器兼容性
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
interface IdaasConfig {
|
|
2
2
|
issuer: string;
|
|
3
3
|
clientId: string;
|
|
4
|
-
clientSecret?: string;
|
|
5
4
|
redirectUri: string;
|
|
6
5
|
postLogoutRedirectUri?: string;
|
|
7
6
|
scope?: string;
|
|
@@ -28,11 +27,51 @@ interface UserInfo {
|
|
|
28
27
|
preferred_username?: string;
|
|
29
28
|
name?: string;
|
|
30
29
|
email?: string;
|
|
31
|
-
|
|
30
|
+
email_verified?: boolean;
|
|
32
31
|
picture?: string;
|
|
32
|
+
/** Requires feishu_identity. Scoped to the Feishu app used by IDaaS; never use across Feishu apps. */
|
|
33
|
+
feishu_open_id?: string;
|
|
34
|
+
/** Requires feishu_identity. Returned together with feishu_tenant_key for cross-app identity linking. */
|
|
35
|
+
feishu_union_id?: string;
|
|
36
|
+
/** Enterprise identity namespace; only returned together with feishu_union_id. */
|
|
37
|
+
feishu_tenant_key?: string;
|
|
33
38
|
roles?: string[];
|
|
34
39
|
[key: string]: unknown;
|
|
35
40
|
}
|
|
41
|
+
/** Sensitive enterprise attributes returned only when the current client and user grant the matching scopes. */
|
|
42
|
+
interface EnterpriseDepartmentPathItem {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
}
|
|
46
|
+
type EnterpriseDepartmentLeaderType = 'primary' | 'deputy';
|
|
47
|
+
interface EnterpriseDepartmentLeader {
|
|
48
|
+
/** Scoped to the same Feishu app as UserInfo.feishu_open_id. */
|
|
49
|
+
feishu_open_id?: string;
|
|
50
|
+
/** @deprecated Use feishu_open_id. Kept as an equal-valued alias for 2.x compatibility. */
|
|
51
|
+
open_id: string;
|
|
52
|
+
name: string | null;
|
|
53
|
+
avatar_url: string | null;
|
|
54
|
+
/** primary = 主负责人;deputy = 副负责人。 */
|
|
55
|
+
leader_type: EnterpriseDepartmentLeaderType;
|
|
56
|
+
}
|
|
57
|
+
interface EnterpriseProfile {
|
|
58
|
+
phone_number?: string | null;
|
|
59
|
+
employee_number?: string | null;
|
|
60
|
+
primary_department_id?: string | null;
|
|
61
|
+
primary_department_name?: string | null;
|
|
62
|
+
/** Ordered from the top-level department to the user's primary (leaf) department. */
|
|
63
|
+
primary_department_path?: EnterpriseDepartmentPathItem[] | null;
|
|
64
|
+
/** Human-readable path such as "一级部门 / 二级部门 / 叶子部门". */
|
|
65
|
+
primary_department_path_name?: string | null;
|
|
66
|
+
/** Main and deputy leaders of the primary department. */
|
|
67
|
+
primary_department_leaders?: EnterpriseDepartmentLeader[] | null;
|
|
68
|
+
}
|
|
69
|
+
interface EnterpriseProfileErrorBody {
|
|
70
|
+
statusCode?: number;
|
|
71
|
+
code?: string;
|
|
72
|
+
message?: string | string[];
|
|
73
|
+
retryable?: boolean;
|
|
74
|
+
}
|
|
36
75
|
interface AuthState {
|
|
37
76
|
isAuthenticated: boolean;
|
|
38
77
|
user: UserInfo | null;
|
|
@@ -49,6 +88,16 @@ interface PKCEPair {
|
|
|
49
88
|
codeChallenge: string;
|
|
50
89
|
}
|
|
51
90
|
|
|
91
|
+
declare class EnterpriseProfileError extends Error {
|
|
92
|
+
readonly status: number;
|
|
93
|
+
readonly code?: string;
|
|
94
|
+
readonly retryable: boolean;
|
|
95
|
+
constructor(message: string, options: {
|
|
96
|
+
status: number;
|
|
97
|
+
code?: string;
|
|
98
|
+
retryable?: boolean;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
52
101
|
declare class IdaasClient {
|
|
53
102
|
private config;
|
|
54
103
|
private endpoints;
|
|
@@ -62,6 +111,11 @@ declare class IdaasClient {
|
|
|
62
111
|
handleCallback(callbackUrl?: string): Promise<TokenSet>;
|
|
63
112
|
refreshToken(): Promise<TokenSet | null>;
|
|
64
113
|
getUserInfo(forceRefresh?: boolean): Promise<UserInfo | null>;
|
|
114
|
+
/**
|
|
115
|
+
* Fetch sensitive attributes for the current user. The server enforces both
|
|
116
|
+
* the client's scope allowlist and the user's consent before returning fields.
|
|
117
|
+
*/
|
|
118
|
+
getEnterpriseProfile(): Promise<EnterpriseProfile | null>;
|
|
65
119
|
getAccessToken(): Promise<string | null>;
|
|
66
120
|
getServiceToken(serviceKey: string): Promise<string | null>;
|
|
67
121
|
getIdToken(): string | null;
|
|
@@ -83,6 +137,7 @@ declare class TokenStorage {
|
|
|
83
137
|
private prefix;
|
|
84
138
|
constructor(prefix?: string);
|
|
85
139
|
private key;
|
|
140
|
+
private migrateTokenValue;
|
|
86
141
|
saveTokens(tokens: TokenSet): void;
|
|
87
142
|
getAccessToken(): string | null;
|
|
88
143
|
getIdToken(): string | null;
|
|
@@ -99,4 +154,4 @@ declare class TokenStorage {
|
|
|
99
154
|
clearAuthState(): void;
|
|
100
155
|
}
|
|
101
156
|
|
|
102
|
-
export { type AuthState, IdaasClient, type IdaasConfig, type ServiceTokenState, type SessionLogoutOptions, type TokenSet, TokenStorage, type UserInfo, createPKCE, generateNonce, generateState };
|
|
157
|
+
export { type AuthState, type EnterpriseDepartmentLeader, type EnterpriseDepartmentLeaderType, type EnterpriseDepartmentPathItem, type EnterpriseProfile, EnterpriseProfileError, type EnterpriseProfileErrorBody, IdaasClient, type IdaasConfig, type ServiceTokenState, type SessionLogoutOptions, type TokenSet, TokenStorage, type UserInfo, createPKCE, generateNonce, generateState };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
interface IdaasConfig {
|
|
2
2
|
issuer: string;
|
|
3
3
|
clientId: string;
|
|
4
|
-
clientSecret?: string;
|
|
5
4
|
redirectUri: string;
|
|
6
5
|
postLogoutRedirectUri?: string;
|
|
7
6
|
scope?: string;
|
|
@@ -28,11 +27,51 @@ interface UserInfo {
|
|
|
28
27
|
preferred_username?: string;
|
|
29
28
|
name?: string;
|
|
30
29
|
email?: string;
|
|
31
|
-
|
|
30
|
+
email_verified?: boolean;
|
|
32
31
|
picture?: string;
|
|
32
|
+
/** Requires feishu_identity. Scoped to the Feishu app used by IDaaS; never use across Feishu apps. */
|
|
33
|
+
feishu_open_id?: string;
|
|
34
|
+
/** Requires feishu_identity. Returned together with feishu_tenant_key for cross-app identity linking. */
|
|
35
|
+
feishu_union_id?: string;
|
|
36
|
+
/** Enterprise identity namespace; only returned together with feishu_union_id. */
|
|
37
|
+
feishu_tenant_key?: string;
|
|
33
38
|
roles?: string[];
|
|
34
39
|
[key: string]: unknown;
|
|
35
40
|
}
|
|
41
|
+
/** Sensitive enterprise attributes returned only when the current client and user grant the matching scopes. */
|
|
42
|
+
interface EnterpriseDepartmentPathItem {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
}
|
|
46
|
+
type EnterpriseDepartmentLeaderType = 'primary' | 'deputy';
|
|
47
|
+
interface EnterpriseDepartmentLeader {
|
|
48
|
+
/** Scoped to the same Feishu app as UserInfo.feishu_open_id. */
|
|
49
|
+
feishu_open_id?: string;
|
|
50
|
+
/** @deprecated Use feishu_open_id. Kept as an equal-valued alias for 2.x compatibility. */
|
|
51
|
+
open_id: string;
|
|
52
|
+
name: string | null;
|
|
53
|
+
avatar_url: string | null;
|
|
54
|
+
/** primary = 主负责人;deputy = 副负责人。 */
|
|
55
|
+
leader_type: EnterpriseDepartmentLeaderType;
|
|
56
|
+
}
|
|
57
|
+
interface EnterpriseProfile {
|
|
58
|
+
phone_number?: string | null;
|
|
59
|
+
employee_number?: string | null;
|
|
60
|
+
primary_department_id?: string | null;
|
|
61
|
+
primary_department_name?: string | null;
|
|
62
|
+
/** Ordered from the top-level department to the user's primary (leaf) department. */
|
|
63
|
+
primary_department_path?: EnterpriseDepartmentPathItem[] | null;
|
|
64
|
+
/** Human-readable path such as "一级部门 / 二级部门 / 叶子部门". */
|
|
65
|
+
primary_department_path_name?: string | null;
|
|
66
|
+
/** Main and deputy leaders of the primary department. */
|
|
67
|
+
primary_department_leaders?: EnterpriseDepartmentLeader[] | null;
|
|
68
|
+
}
|
|
69
|
+
interface EnterpriseProfileErrorBody {
|
|
70
|
+
statusCode?: number;
|
|
71
|
+
code?: string;
|
|
72
|
+
message?: string | string[];
|
|
73
|
+
retryable?: boolean;
|
|
74
|
+
}
|
|
36
75
|
interface AuthState {
|
|
37
76
|
isAuthenticated: boolean;
|
|
38
77
|
user: UserInfo | null;
|
|
@@ -49,6 +88,16 @@ interface PKCEPair {
|
|
|
49
88
|
codeChallenge: string;
|
|
50
89
|
}
|
|
51
90
|
|
|
91
|
+
declare class EnterpriseProfileError extends Error {
|
|
92
|
+
readonly status: number;
|
|
93
|
+
readonly code?: string;
|
|
94
|
+
readonly retryable: boolean;
|
|
95
|
+
constructor(message: string, options: {
|
|
96
|
+
status: number;
|
|
97
|
+
code?: string;
|
|
98
|
+
retryable?: boolean;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
52
101
|
declare class IdaasClient {
|
|
53
102
|
private config;
|
|
54
103
|
private endpoints;
|
|
@@ -62,6 +111,11 @@ declare class IdaasClient {
|
|
|
62
111
|
handleCallback(callbackUrl?: string): Promise<TokenSet>;
|
|
63
112
|
refreshToken(): Promise<TokenSet | null>;
|
|
64
113
|
getUserInfo(forceRefresh?: boolean): Promise<UserInfo | null>;
|
|
114
|
+
/**
|
|
115
|
+
* Fetch sensitive attributes for the current user. The server enforces both
|
|
116
|
+
* the client's scope allowlist and the user's consent before returning fields.
|
|
117
|
+
*/
|
|
118
|
+
getEnterpriseProfile(): Promise<EnterpriseProfile | null>;
|
|
65
119
|
getAccessToken(): Promise<string | null>;
|
|
66
120
|
getServiceToken(serviceKey: string): Promise<string | null>;
|
|
67
121
|
getIdToken(): string | null;
|
|
@@ -83,6 +137,7 @@ declare class TokenStorage {
|
|
|
83
137
|
private prefix;
|
|
84
138
|
constructor(prefix?: string);
|
|
85
139
|
private key;
|
|
140
|
+
private migrateTokenValue;
|
|
86
141
|
saveTokens(tokens: TokenSet): void;
|
|
87
142
|
getAccessToken(): string | null;
|
|
88
143
|
getIdToken(): string | null;
|
|
@@ -99,4 +154,4 @@ declare class TokenStorage {
|
|
|
99
154
|
clearAuthState(): void;
|
|
100
155
|
}
|
|
101
156
|
|
|
102
|
-
export { type AuthState, IdaasClient, type IdaasConfig, type ServiceTokenState, type SessionLogoutOptions, type TokenSet, TokenStorage, type UserInfo, createPKCE, generateNonce, generateState };
|
|
157
|
+
export { type AuthState, type EnterpriseDepartmentLeader, type EnterpriseDepartmentLeaderType, type EnterpriseDepartmentPathItem, type EnterpriseProfile, EnterpriseProfileError, type EnterpriseProfileErrorBody, IdaasClient, type IdaasConfig, type ServiceTokenState, type SessionLogoutOptions, type TokenSet, TokenStorage, type UserInfo, createPKCE, generateNonce, generateState };
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
EnterpriseProfileError: () => EnterpriseProfileError,
|
|
23
24
|
IdaasClient: () => IdaasClient,
|
|
24
25
|
TokenStorage: () => TokenStorage,
|
|
25
26
|
createPKCE: () => createPKCE,
|
|
@@ -183,33 +184,48 @@ var TokenStorage = class {
|
|
|
183
184
|
key(name) {
|
|
184
185
|
return `${this.prefix}:${name}`;
|
|
185
186
|
}
|
|
187
|
+
migrateTokenValue(name) {
|
|
188
|
+
const key = this.key(name);
|
|
189
|
+
const current = localStorage.getItem(key);
|
|
190
|
+
if (current) return current;
|
|
191
|
+
const legacy = sessionStorage.getItem(key);
|
|
192
|
+
if (!legacy) return null;
|
|
193
|
+
localStorage.setItem(key, legacy);
|
|
194
|
+
sessionStorage.removeItem(key);
|
|
195
|
+
return legacy;
|
|
196
|
+
}
|
|
186
197
|
saveTokens(tokens) {
|
|
187
198
|
const expiresAt = Date.now() + tokens.expires_in * 1e3;
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
localStorage.setItem(this.key("access_token"), tokens.access_token);
|
|
200
|
+
localStorage.setItem(this.key("expires_at"), expiresAt.toString());
|
|
201
|
+
sessionStorage.removeItem(this.key("access_token"));
|
|
202
|
+
sessionStorage.removeItem(this.key("expires_at"));
|
|
190
203
|
if (tokens.id_token) {
|
|
191
|
-
|
|
204
|
+
localStorage.setItem(this.key("id_token"), tokens.id_token);
|
|
205
|
+
sessionStorage.removeItem(this.key("id_token"));
|
|
192
206
|
}
|
|
193
207
|
if (tokens.refresh_token) {
|
|
194
208
|
localStorage.setItem(this.key("refresh_token"), tokens.refresh_token);
|
|
195
209
|
}
|
|
196
|
-
|
|
210
|
+
localStorage.setItem(this.key("scope"), tokens.scope);
|
|
211
|
+
sessionStorage.removeItem(this.key("scope"));
|
|
197
212
|
}
|
|
198
213
|
getAccessToken() {
|
|
199
|
-
const token =
|
|
200
|
-
const expiresAt =
|
|
214
|
+
const token = this.migrateTokenValue("access_token");
|
|
215
|
+
const expiresAt = this.migrateTokenValue("expires_at");
|
|
216
|
+
this.migrateTokenValue("scope");
|
|
201
217
|
if (!token || !expiresAt) return null;
|
|
202
218
|
if (Date.now() > parseInt(expiresAt, 10)) return null;
|
|
203
219
|
return token;
|
|
204
220
|
}
|
|
205
221
|
getIdToken() {
|
|
206
|
-
return
|
|
222
|
+
return this.migrateTokenValue("id_token");
|
|
207
223
|
}
|
|
208
224
|
getRefreshToken() {
|
|
209
225
|
return localStorage.getItem(this.key("refresh_token"));
|
|
210
226
|
}
|
|
211
227
|
getExpiresAt() {
|
|
212
|
-
const val =
|
|
228
|
+
const val = this.migrateTokenValue("expires_at");
|
|
213
229
|
return val ? parseInt(val, 10) : null;
|
|
214
230
|
}
|
|
215
231
|
isTokenExpired() {
|
|
@@ -218,11 +234,15 @@ var TokenStorage = class {
|
|
|
218
234
|
return Date.now() > expiresAt - 6e4;
|
|
219
235
|
}
|
|
220
236
|
clear() {
|
|
237
|
+
localStorage.removeItem(this.key("access_token"));
|
|
238
|
+
localStorage.removeItem(this.key("id_token"));
|
|
239
|
+
localStorage.removeItem(this.key("expires_at"));
|
|
240
|
+
localStorage.removeItem(this.key("scope"));
|
|
241
|
+
localStorage.removeItem(this.key("refresh_token"));
|
|
221
242
|
sessionStorage.removeItem(this.key("access_token"));
|
|
222
243
|
sessionStorage.removeItem(this.key("id_token"));
|
|
223
244
|
sessionStorage.removeItem(this.key("expires_at"));
|
|
224
245
|
sessionStorage.removeItem(this.key("scope"));
|
|
225
|
-
localStorage.removeItem(this.key("refresh_token"));
|
|
226
246
|
}
|
|
227
247
|
saveState(state) {
|
|
228
248
|
sessionStorage.setItem(this.key("state"), state);
|
|
@@ -250,6 +270,15 @@ var TokenStorage = class {
|
|
|
250
270
|
};
|
|
251
271
|
|
|
252
272
|
// src/client.ts
|
|
273
|
+
var EnterpriseProfileError = class extends Error {
|
|
274
|
+
constructor(message, options) {
|
|
275
|
+
super(message);
|
|
276
|
+
this.name = "EnterpriseProfileError";
|
|
277
|
+
this.status = options.status;
|
|
278
|
+
this.code = options.code;
|
|
279
|
+
this.retryable = options.retryable ?? options.status >= 500;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
253
282
|
var IdaasClient = class {
|
|
254
283
|
constructor(config) {
|
|
255
284
|
this.endpoints = null;
|
|
@@ -271,6 +300,7 @@ var IdaasClient = class {
|
|
|
271
300
|
authorization_endpoint: `${this.config.issuer}/oauth2/authorize`,
|
|
272
301
|
token_endpoint: `${this.config.issuer}/oauth2/token`,
|
|
273
302
|
userinfo_endpoint: `${this.config.issuer}/oauth2/userinfo`,
|
|
303
|
+
enterprise_profile_endpoint: `${this.config.issuer}/oauth2/enterprise-profile`,
|
|
274
304
|
end_session_endpoint: `${this.config.issuer}/oauth2/logout`,
|
|
275
305
|
jwks_uri: `${this.config.issuer}/.well-known/jwks.json`,
|
|
276
306
|
revocation_endpoint: `${this.config.issuer}/oauth2/revoke`,
|
|
@@ -288,6 +318,7 @@ var IdaasClient = class {
|
|
|
288
318
|
authorization_endpoint: doc.authorization_endpoint,
|
|
289
319
|
token_endpoint: doc.token_endpoint,
|
|
290
320
|
userinfo_endpoint: doc.userinfo_endpoint,
|
|
321
|
+
enterprise_profile_endpoint: doc.enterprise_profile_endpoint || `${this.config.issuer}/oauth2/enterprise-profile`,
|
|
291
322
|
end_session_endpoint: doc.end_session_endpoint,
|
|
292
323
|
jwks_uri: doc.jwks_uri,
|
|
293
324
|
revocation_endpoint: doc.revocation_endpoint,
|
|
@@ -339,9 +370,6 @@ var IdaasClient = class {
|
|
|
339
370
|
client_id: this.config.clientId,
|
|
340
371
|
code_verifier: codeVerifier
|
|
341
372
|
});
|
|
342
|
-
if (this.config.clientSecret) {
|
|
343
|
-
body.set("client_secret", this.config.clientSecret);
|
|
344
|
-
}
|
|
345
373
|
const res = await fetch(this.endpoints.token_endpoint, {
|
|
346
374
|
method: "POST",
|
|
347
375
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -365,9 +393,6 @@ var IdaasClient = class {
|
|
|
365
393
|
refresh_token: refreshToken,
|
|
366
394
|
client_id: this.config.clientId
|
|
367
395
|
});
|
|
368
|
-
if (this.config.clientSecret) {
|
|
369
|
-
body.set("client_secret", this.config.clientSecret);
|
|
370
|
-
}
|
|
371
396
|
const res = await fetch(this.endpoints.token_endpoint, {
|
|
372
397
|
method: "POST",
|
|
373
398
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -393,6 +418,28 @@ var IdaasClient = class {
|
|
|
393
418
|
this.userInfo = await res.json();
|
|
394
419
|
return this.userInfo;
|
|
395
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Fetch sensitive attributes for the current user. The server enforces both
|
|
423
|
+
* the client's scope allowlist and the user's consent before returning fields.
|
|
424
|
+
*/
|
|
425
|
+
async getEnterpriseProfile() {
|
|
426
|
+
if (!this.endpoints) await this.init();
|
|
427
|
+
const accessToken = await this.getAccessToken();
|
|
428
|
+
if (!accessToken) return null;
|
|
429
|
+
const res = await fetch(this.endpoints.enterprise_profile_endpoint, {
|
|
430
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
431
|
+
});
|
|
432
|
+
if (!res.ok) {
|
|
433
|
+
const body = await res.json().catch(() => ({}));
|
|
434
|
+
const message = Array.isArray(body.message) ? body.message.join(", ") : body.message || "Enterprise profile is temporarily unavailable";
|
|
435
|
+
throw new EnterpriseProfileError(message, {
|
|
436
|
+
status: res.status,
|
|
437
|
+
code: body.code,
|
|
438
|
+
retryable: body.retryable
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return await res.json();
|
|
442
|
+
}
|
|
396
443
|
async getAccessToken() {
|
|
397
444
|
const token = this.storage.getAccessToken();
|
|
398
445
|
if (token && !this.storage.isTokenExpired()) return token;
|
|
@@ -500,6 +547,7 @@ var IdaasClient = class {
|
|
|
500
547
|
};
|
|
501
548
|
// Annotate the CommonJS export names for ESM import in node:
|
|
502
549
|
0 && (module.exports = {
|
|
550
|
+
EnterpriseProfileError,
|
|
503
551
|
IdaasClient,
|
|
504
552
|
TokenStorage,
|
|
505
553
|
createPKCE,
|
package/dist/index.mjs
CHANGED
|
@@ -153,33 +153,48 @@ var TokenStorage = class {
|
|
|
153
153
|
key(name) {
|
|
154
154
|
return `${this.prefix}:${name}`;
|
|
155
155
|
}
|
|
156
|
+
migrateTokenValue(name) {
|
|
157
|
+
const key = this.key(name);
|
|
158
|
+
const current = localStorage.getItem(key);
|
|
159
|
+
if (current) return current;
|
|
160
|
+
const legacy = sessionStorage.getItem(key);
|
|
161
|
+
if (!legacy) return null;
|
|
162
|
+
localStorage.setItem(key, legacy);
|
|
163
|
+
sessionStorage.removeItem(key);
|
|
164
|
+
return legacy;
|
|
165
|
+
}
|
|
156
166
|
saveTokens(tokens) {
|
|
157
167
|
const expiresAt = Date.now() + tokens.expires_in * 1e3;
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
localStorage.setItem(this.key("access_token"), tokens.access_token);
|
|
169
|
+
localStorage.setItem(this.key("expires_at"), expiresAt.toString());
|
|
170
|
+
sessionStorage.removeItem(this.key("access_token"));
|
|
171
|
+
sessionStorage.removeItem(this.key("expires_at"));
|
|
160
172
|
if (tokens.id_token) {
|
|
161
|
-
|
|
173
|
+
localStorage.setItem(this.key("id_token"), tokens.id_token);
|
|
174
|
+
sessionStorage.removeItem(this.key("id_token"));
|
|
162
175
|
}
|
|
163
176
|
if (tokens.refresh_token) {
|
|
164
177
|
localStorage.setItem(this.key("refresh_token"), tokens.refresh_token);
|
|
165
178
|
}
|
|
166
|
-
|
|
179
|
+
localStorage.setItem(this.key("scope"), tokens.scope);
|
|
180
|
+
sessionStorage.removeItem(this.key("scope"));
|
|
167
181
|
}
|
|
168
182
|
getAccessToken() {
|
|
169
|
-
const token =
|
|
170
|
-
const expiresAt =
|
|
183
|
+
const token = this.migrateTokenValue("access_token");
|
|
184
|
+
const expiresAt = this.migrateTokenValue("expires_at");
|
|
185
|
+
this.migrateTokenValue("scope");
|
|
171
186
|
if (!token || !expiresAt) return null;
|
|
172
187
|
if (Date.now() > parseInt(expiresAt, 10)) return null;
|
|
173
188
|
return token;
|
|
174
189
|
}
|
|
175
190
|
getIdToken() {
|
|
176
|
-
return
|
|
191
|
+
return this.migrateTokenValue("id_token");
|
|
177
192
|
}
|
|
178
193
|
getRefreshToken() {
|
|
179
194
|
return localStorage.getItem(this.key("refresh_token"));
|
|
180
195
|
}
|
|
181
196
|
getExpiresAt() {
|
|
182
|
-
const val =
|
|
197
|
+
const val = this.migrateTokenValue("expires_at");
|
|
183
198
|
return val ? parseInt(val, 10) : null;
|
|
184
199
|
}
|
|
185
200
|
isTokenExpired() {
|
|
@@ -188,11 +203,15 @@ var TokenStorage = class {
|
|
|
188
203
|
return Date.now() > expiresAt - 6e4;
|
|
189
204
|
}
|
|
190
205
|
clear() {
|
|
206
|
+
localStorage.removeItem(this.key("access_token"));
|
|
207
|
+
localStorage.removeItem(this.key("id_token"));
|
|
208
|
+
localStorage.removeItem(this.key("expires_at"));
|
|
209
|
+
localStorage.removeItem(this.key("scope"));
|
|
210
|
+
localStorage.removeItem(this.key("refresh_token"));
|
|
191
211
|
sessionStorage.removeItem(this.key("access_token"));
|
|
192
212
|
sessionStorage.removeItem(this.key("id_token"));
|
|
193
213
|
sessionStorage.removeItem(this.key("expires_at"));
|
|
194
214
|
sessionStorage.removeItem(this.key("scope"));
|
|
195
|
-
localStorage.removeItem(this.key("refresh_token"));
|
|
196
215
|
}
|
|
197
216
|
saveState(state) {
|
|
198
217
|
sessionStorage.setItem(this.key("state"), state);
|
|
@@ -220,6 +239,15 @@ var TokenStorage = class {
|
|
|
220
239
|
};
|
|
221
240
|
|
|
222
241
|
// src/client.ts
|
|
242
|
+
var EnterpriseProfileError = class extends Error {
|
|
243
|
+
constructor(message, options) {
|
|
244
|
+
super(message);
|
|
245
|
+
this.name = "EnterpriseProfileError";
|
|
246
|
+
this.status = options.status;
|
|
247
|
+
this.code = options.code;
|
|
248
|
+
this.retryable = options.retryable ?? options.status >= 500;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
223
251
|
var IdaasClient = class {
|
|
224
252
|
constructor(config) {
|
|
225
253
|
this.endpoints = null;
|
|
@@ -241,6 +269,7 @@ var IdaasClient = class {
|
|
|
241
269
|
authorization_endpoint: `${this.config.issuer}/oauth2/authorize`,
|
|
242
270
|
token_endpoint: `${this.config.issuer}/oauth2/token`,
|
|
243
271
|
userinfo_endpoint: `${this.config.issuer}/oauth2/userinfo`,
|
|
272
|
+
enterprise_profile_endpoint: `${this.config.issuer}/oauth2/enterprise-profile`,
|
|
244
273
|
end_session_endpoint: `${this.config.issuer}/oauth2/logout`,
|
|
245
274
|
jwks_uri: `${this.config.issuer}/.well-known/jwks.json`,
|
|
246
275
|
revocation_endpoint: `${this.config.issuer}/oauth2/revoke`,
|
|
@@ -258,6 +287,7 @@ var IdaasClient = class {
|
|
|
258
287
|
authorization_endpoint: doc.authorization_endpoint,
|
|
259
288
|
token_endpoint: doc.token_endpoint,
|
|
260
289
|
userinfo_endpoint: doc.userinfo_endpoint,
|
|
290
|
+
enterprise_profile_endpoint: doc.enterprise_profile_endpoint || `${this.config.issuer}/oauth2/enterprise-profile`,
|
|
261
291
|
end_session_endpoint: doc.end_session_endpoint,
|
|
262
292
|
jwks_uri: doc.jwks_uri,
|
|
263
293
|
revocation_endpoint: doc.revocation_endpoint,
|
|
@@ -309,9 +339,6 @@ var IdaasClient = class {
|
|
|
309
339
|
client_id: this.config.clientId,
|
|
310
340
|
code_verifier: codeVerifier
|
|
311
341
|
});
|
|
312
|
-
if (this.config.clientSecret) {
|
|
313
|
-
body.set("client_secret", this.config.clientSecret);
|
|
314
|
-
}
|
|
315
342
|
const res = await fetch(this.endpoints.token_endpoint, {
|
|
316
343
|
method: "POST",
|
|
317
344
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -335,9 +362,6 @@ var IdaasClient = class {
|
|
|
335
362
|
refresh_token: refreshToken,
|
|
336
363
|
client_id: this.config.clientId
|
|
337
364
|
});
|
|
338
|
-
if (this.config.clientSecret) {
|
|
339
|
-
body.set("client_secret", this.config.clientSecret);
|
|
340
|
-
}
|
|
341
365
|
const res = await fetch(this.endpoints.token_endpoint, {
|
|
342
366
|
method: "POST",
|
|
343
367
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -363,6 +387,28 @@ var IdaasClient = class {
|
|
|
363
387
|
this.userInfo = await res.json();
|
|
364
388
|
return this.userInfo;
|
|
365
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Fetch sensitive attributes for the current user. The server enforces both
|
|
392
|
+
* the client's scope allowlist and the user's consent before returning fields.
|
|
393
|
+
*/
|
|
394
|
+
async getEnterpriseProfile() {
|
|
395
|
+
if (!this.endpoints) await this.init();
|
|
396
|
+
const accessToken = await this.getAccessToken();
|
|
397
|
+
if (!accessToken) return null;
|
|
398
|
+
const res = await fetch(this.endpoints.enterprise_profile_endpoint, {
|
|
399
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
400
|
+
});
|
|
401
|
+
if (!res.ok) {
|
|
402
|
+
const body = await res.json().catch(() => ({}));
|
|
403
|
+
const message = Array.isArray(body.message) ? body.message.join(", ") : body.message || "Enterprise profile is temporarily unavailable";
|
|
404
|
+
throw new EnterpriseProfileError(message, {
|
|
405
|
+
status: res.status,
|
|
406
|
+
code: body.code,
|
|
407
|
+
retryable: body.retryable
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return await res.json();
|
|
411
|
+
}
|
|
366
412
|
async getAccessToken() {
|
|
367
413
|
const token = this.storage.getAccessToken();
|
|
368
414
|
if (token && !this.storage.isTokenExpired()) return token;
|
|
@@ -469,6 +515,7 @@ var IdaasClient = class {
|
|
|
469
515
|
}
|
|
470
516
|
};
|
|
471
517
|
export {
|
|
518
|
+
EnterpriseProfileError,
|
|
472
519
|
IdaasClient,
|
|
473
520
|
TokenStorage,
|
|
474
521
|
createPKCE,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zskj-idaas/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "IDaaS OIDC Client SDK for browser applications — supports Authorization Code Flow with PKCE, token management, and authenticated fetch.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|