@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 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://your-idaas-server.com',
32
- clientId: 'your-client-id',
33
- redirectUri: 'http://localhost:3000/callback',
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@example.com' });
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
- 带认证的 fetch 封装,自动附加 Authorization 头。
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.fetch('/api/protected-resource');
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://idaas.example.com',
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.fetch('https://api.example.com/data');
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
- phone_number?: string;
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
- phone_number?: string;
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
- sessionStorage.setItem(this.key("access_token"), tokens.access_token);
189
- sessionStorage.setItem(this.key("expires_at"), expiresAt.toString());
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
- sessionStorage.setItem(this.key("id_token"), tokens.id_token);
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
- sessionStorage.setItem(this.key("scope"), tokens.scope);
210
+ localStorage.setItem(this.key("scope"), tokens.scope);
211
+ sessionStorage.removeItem(this.key("scope"));
197
212
  }
198
213
  getAccessToken() {
199
- const token = sessionStorage.getItem(this.key("access_token"));
200
- const expiresAt = sessionStorage.getItem(this.key("expires_at"));
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 sessionStorage.getItem(this.key("id_token"));
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 = sessionStorage.getItem(this.key("expires_at"));
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
- sessionStorage.setItem(this.key("access_token"), tokens.access_token);
159
- sessionStorage.setItem(this.key("expires_at"), expiresAt.toString());
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
- sessionStorage.setItem(this.key("id_token"), tokens.id_token);
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
- sessionStorage.setItem(this.key("scope"), tokens.scope);
179
+ localStorage.setItem(this.key("scope"), tokens.scope);
180
+ sessionStorage.removeItem(this.key("scope"));
167
181
  }
168
182
  getAccessToken() {
169
- const token = sessionStorage.getItem(this.key("access_token"));
170
- const expiresAt = sessionStorage.getItem(this.key("expires_at"));
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 sessionStorage.getItem(this.key("id_token"));
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 = sessionStorage.getItem(this.key("expires_at"));
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": "1.0.3",
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",