@zskj-idaas/sdk 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 IDaaS Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # @zskj-idaas/sdk
2
+
3
+ IDaaS OIDC Client SDK — 适用于浏览器应用的 OpenID Connect 客户端,支持 Authorization Code Flow with PKCE。
4
+
5
+ ## 特性
6
+
7
+ - **零运行时依赖** — 轻量、无外部依赖
8
+ - **PKCE 支持** — 自动生成 code_verifier / code_challenge
9
+ - **双模块输出** — 同时支持 ESM 和 CommonJS
10
+ - **TypeScript** — 完整的类型定义
11
+ - **OIDC Discovery** — 自动发现 OpenID 配置端点
12
+ - **Token 管理** — 自动存储、过期检测、静默刷新
13
+ - **认证 Fetch** — 内置带 Bearer Token 的 fetch 封装
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ npm install @zskj-idaas/sdk
19
+ # 或
20
+ pnpm add @zskj-idaas/sdk
21
+ # 或
22
+ yarn add @zskj-idaas/sdk
23
+ ```
24
+
25
+ ## 快速开始
26
+
27
+ ```typescript
28
+ import { IdaasClient } from '@zskj-idaas/sdk';
29
+
30
+ const client = new IdaasClient({
31
+ issuer: 'https://your-idaas-server.com',
32
+ clientId: 'your-client-id',
33
+ redirectUri: 'http://localhost:3000/callback',
34
+ scope: 'openid profile email',
35
+ });
36
+
37
+ // 初始化(自动发现 OIDC 端点)
38
+ await client.init();
39
+
40
+ // 发起登录
41
+ await client.login();
42
+ ```
43
+
44
+ ## API 参考
45
+
46
+ ### `new IdaasClient(config)`
47
+
48
+ 创建 SDK 客户端实例。
49
+
50
+ ```typescript
51
+ interface IdaasConfig {
52
+ issuer: string; // OIDC 提供者地址
53
+ clientId: string; // 客户端 ID
54
+ clientSecret?: string; // 客户端密钥(机密客户端)
55
+ redirectUri: string; // 登录回调地址
56
+ postLogoutRedirectUri?: string; // 登出后跳转地址
57
+ scope?: string; // 请求的 scope(默认: 'openid profile email')
58
+ autoDiscovery?: boolean; // 是否自动发现端点(默认: true)
59
+ storagePrefix?: string; // 存储 key 前缀(默认: 'idaas')
60
+ }
61
+ ```
62
+
63
+ ### `client.init(): Promise<AuthState>`
64
+
65
+ 初始化客户端,执行 OIDC Discovery 并返回当前认证状态。
66
+
67
+ ### `client.login(extraParams?): Promise<void>`
68
+
69
+ 发起 OIDC 授权码登录流程,跳转到授权端点。
70
+
71
+ ```typescript
72
+ // 附加额外参数
73
+ await client.login({ prompt: 'login', login_hint: 'user@example.com' });
74
+ ```
75
+
76
+ ### `client.handleCallback(callbackUrl?): Promise<TokenSet>`
77
+
78
+ 处理授权回调,用授权码交换 Token。
79
+
80
+ ```typescript
81
+ // 在回调页面调用
82
+ const tokens = await client.handleCallback();
83
+ ```
84
+
85
+ ### `client.getUserInfo(forceRefresh?): Promise<UserInfo | null>`
86
+
87
+ 获取当前用户信息。
88
+
89
+ ### `client.getAccessToken(): Promise<string | null>`
90
+
91
+ 获取有效的 Access Token(过期时自动刷新)。
92
+
93
+ ### `client.getIdToken(): string | null`
94
+
95
+ 获取 ID Token。
96
+
97
+ ### `client.getAuthState(): AuthState`
98
+
99
+ 获取当前认证状态。
100
+
101
+ ```typescript
102
+ interface AuthState {
103
+ isAuthenticated: boolean;
104
+ user: UserInfo | null;
105
+ accessToken: string | null;
106
+ idToken: string | null;
107
+ expiresAt: number | null;
108
+ }
109
+ ```
110
+
111
+ ### `client.isAuthenticated(): boolean`
112
+
113
+ 检查是否已认证。
114
+
115
+ ### `client.logout(): Promise<void>`
116
+
117
+ 执行 OIDC 登出流程。
118
+
119
+ ### `client.revokeToken(): Promise<void>`
120
+
121
+ 撤销 Refresh Token。
122
+
123
+ ### `client.fetch(url, init?): Promise<Response>`
124
+
125
+ 带认证的 fetch 封装,自动附加 Authorization 头。
126
+
127
+ ```typescript
128
+ const res = await client.fetch('/api/protected-resource');
129
+ const data = await res.json();
130
+ ```
131
+
132
+ ## 完整示例
133
+
134
+ ```typescript
135
+ import { IdaasClient } from '@zskj-idaas/sdk';
136
+ import type { AuthState, UserInfo } from '@zskj-idaas/sdk';
137
+
138
+ const client = new IdaasClient({
139
+ issuer: 'https://idaas.example.com',
140
+ clientId: 'my-app',
141
+ redirectUri: `${window.location.origin}/callback`,
142
+ postLogoutRedirectUri: window.location.origin,
143
+ });
144
+
145
+ // 应用启动时初始化
146
+ async function bootstrap() {
147
+ const state: AuthState = await client.init();
148
+
149
+ if (state.isAuthenticated) {
150
+ const user: UserInfo | null = await client.getUserInfo();
151
+ console.log('已登录:', user?.name);
152
+ }
153
+ }
154
+
155
+ // 回调页面处理
156
+ async function handleCallback() {
157
+ try {
158
+ await client.handleCallback();
159
+ window.location.href = '/';
160
+ } catch (err) {
161
+ console.error('登录失败:', err);
162
+ }
163
+ }
164
+
165
+ // 调用受保护的 API
166
+ async function fetchData() {
167
+ const res = await client.fetch('https://api.example.com/data');
168
+ return res.json();
169
+ }
170
+ ```
171
+
172
+ ## 导出内容
173
+
174
+ ```typescript
175
+ // 类
176
+ export { IdaasClient } from './client';
177
+ export { TokenStorage } from './storage';
178
+
179
+ // 工具函数
180
+ export { createPKCE, generateState, generateNonce } from './crypto';
181
+
182
+ // 类型
183
+ export type { IdaasConfig, TokenSet, UserInfo, AuthState } from './types';
184
+ ```
185
+
186
+ ## 浏览器兼容性
187
+
188
+ 需要支持 Web Crypto API 的现代浏览器:
189
+
190
+ - Chrome 63+
191
+ - Firefox 57+
192
+ - Safari 11.1+
193
+ - Edge 79+
194
+
195
+ ## License
196
+
197
+ MIT
@@ -0,0 +1,89 @@
1
+ interface IdaasConfig {
2
+ issuer: string;
3
+ clientId: string;
4
+ clientSecret?: string;
5
+ redirectUri: string;
6
+ postLogoutRedirectUri?: string;
7
+ scope?: string;
8
+ /** Auto-discover OIDC endpoints from issuer */
9
+ autoDiscovery?: boolean;
10
+ /** Storage key prefix */
11
+ storagePrefix?: string;
12
+ }
13
+ interface TokenSet {
14
+ access_token: string;
15
+ token_type: string;
16
+ expires_in: number;
17
+ id_token?: string;
18
+ refresh_token?: string;
19
+ scope: string;
20
+ }
21
+ interface UserInfo {
22
+ sub: string;
23
+ preferred_username?: string;
24
+ name?: string;
25
+ email?: string;
26
+ phone_number?: string;
27
+ picture?: string;
28
+ roles?: string[];
29
+ [key: string]: unknown;
30
+ }
31
+ interface AuthState {
32
+ isAuthenticated: boolean;
33
+ user: UserInfo | null;
34
+ accessToken: string | null;
35
+ idToken: string | null;
36
+ expiresAt: number | null;
37
+ }
38
+ interface PKCEPair {
39
+ codeVerifier: string;
40
+ codeChallenge: string;
41
+ }
42
+
43
+ declare class IdaasClient {
44
+ private config;
45
+ private endpoints;
46
+ private storage;
47
+ private userInfo;
48
+ constructor(config: IdaasConfig);
49
+ init(): Promise<AuthState>;
50
+ private discover;
51
+ login(extraParams?: Record<string, string>): Promise<void>;
52
+ handleCallback(callbackUrl?: string): Promise<TokenSet>;
53
+ refreshToken(): Promise<TokenSet | null>;
54
+ getUserInfo(forceRefresh?: boolean): Promise<UserInfo | null>;
55
+ getAccessToken(): Promise<string | null>;
56
+ getIdToken(): string | null;
57
+ getAuthState(): AuthState;
58
+ isAuthenticated(): boolean;
59
+ logout(): Promise<void>;
60
+ revokeToken(): Promise<void>;
61
+ /** Create an authenticated fetch wrapper */
62
+ fetch(url: string, init?: RequestInit): Promise<Response>;
63
+ }
64
+
65
+ declare function createPKCE(): Promise<PKCEPair>;
66
+ declare function generateState(): string;
67
+ declare function generateNonce(): string;
68
+
69
+ declare class TokenStorage {
70
+ private prefix;
71
+ constructor(prefix?: string);
72
+ private key;
73
+ saveTokens(tokens: TokenSet): void;
74
+ getAccessToken(): string | null;
75
+ getIdToken(): string | null;
76
+ getRefreshToken(): string | null;
77
+ getExpiresAt(): number | null;
78
+ isTokenExpired(): boolean;
79
+ clear(): void;
80
+ saveState(state: string): void;
81
+ getState(): string | null;
82
+ saveCodeVerifier(verifier: string): void;
83
+ getCodeVerifier(): string | null;
84
+ saveNonce(nonce: string): void;
85
+ getNonce(): string | null;
86
+ clearAuthState(): void;
87
+ }
88
+
89
+ export { type AuthState, IdaasClient, type IdaasConfig, type TokenSet, TokenStorage, type UserInfo, createPKCE, generateNonce, generateState };
@@ -0,0 +1,89 @@
1
+ interface IdaasConfig {
2
+ issuer: string;
3
+ clientId: string;
4
+ clientSecret?: string;
5
+ redirectUri: string;
6
+ postLogoutRedirectUri?: string;
7
+ scope?: string;
8
+ /** Auto-discover OIDC endpoints from issuer */
9
+ autoDiscovery?: boolean;
10
+ /** Storage key prefix */
11
+ storagePrefix?: string;
12
+ }
13
+ interface TokenSet {
14
+ access_token: string;
15
+ token_type: string;
16
+ expires_in: number;
17
+ id_token?: string;
18
+ refresh_token?: string;
19
+ scope: string;
20
+ }
21
+ interface UserInfo {
22
+ sub: string;
23
+ preferred_username?: string;
24
+ name?: string;
25
+ email?: string;
26
+ phone_number?: string;
27
+ picture?: string;
28
+ roles?: string[];
29
+ [key: string]: unknown;
30
+ }
31
+ interface AuthState {
32
+ isAuthenticated: boolean;
33
+ user: UserInfo | null;
34
+ accessToken: string | null;
35
+ idToken: string | null;
36
+ expiresAt: number | null;
37
+ }
38
+ interface PKCEPair {
39
+ codeVerifier: string;
40
+ codeChallenge: string;
41
+ }
42
+
43
+ declare class IdaasClient {
44
+ private config;
45
+ private endpoints;
46
+ private storage;
47
+ private userInfo;
48
+ constructor(config: IdaasConfig);
49
+ init(): Promise<AuthState>;
50
+ private discover;
51
+ login(extraParams?: Record<string, string>): Promise<void>;
52
+ handleCallback(callbackUrl?: string): Promise<TokenSet>;
53
+ refreshToken(): Promise<TokenSet | null>;
54
+ getUserInfo(forceRefresh?: boolean): Promise<UserInfo | null>;
55
+ getAccessToken(): Promise<string | null>;
56
+ getIdToken(): string | null;
57
+ getAuthState(): AuthState;
58
+ isAuthenticated(): boolean;
59
+ logout(): Promise<void>;
60
+ revokeToken(): Promise<void>;
61
+ /** Create an authenticated fetch wrapper */
62
+ fetch(url: string, init?: RequestInit): Promise<Response>;
63
+ }
64
+
65
+ declare function createPKCE(): Promise<PKCEPair>;
66
+ declare function generateState(): string;
67
+ declare function generateNonce(): string;
68
+
69
+ declare class TokenStorage {
70
+ private prefix;
71
+ constructor(prefix?: string);
72
+ private key;
73
+ saveTokens(tokens: TokenSet): void;
74
+ getAccessToken(): string | null;
75
+ getIdToken(): string | null;
76
+ getRefreshToken(): string | null;
77
+ getExpiresAt(): number | null;
78
+ isTokenExpired(): boolean;
79
+ clear(): void;
80
+ saveState(state: string): void;
81
+ getState(): string | null;
82
+ saveCodeVerifier(verifier: string): void;
83
+ getCodeVerifier(): string | null;
84
+ saveNonce(nonce: string): void;
85
+ getNonce(): string | null;
86
+ clearAuthState(): void;
87
+ }
88
+
89
+ export { type AuthState, IdaasClient, type IdaasConfig, type TokenSet, TokenStorage, type UserInfo, createPKCE, generateNonce, generateState };
package/dist/index.js ADDED
@@ -0,0 +1,344 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ IdaasClient: () => IdaasClient,
24
+ TokenStorage: () => TokenStorage,
25
+ createPKCE: () => createPKCE,
26
+ generateNonce: () => generateNonce,
27
+ generateState: () => generateState
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/crypto.ts
32
+ function generateRandomString(length) {
33
+ const array = new Uint8Array(length);
34
+ crypto.getRandomValues(array);
35
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("").substring(0, length);
36
+ }
37
+ function base64UrlEncode(buffer) {
38
+ const bytes = new Uint8Array(buffer);
39
+ let binary = "";
40
+ bytes.forEach((b) => {
41
+ binary += String.fromCharCode(b);
42
+ });
43
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
44
+ }
45
+ async function createPKCE() {
46
+ const codeVerifier = generateRandomString(64);
47
+ const encoder = new TextEncoder();
48
+ const data = encoder.encode(codeVerifier);
49
+ const digest = await crypto.subtle.digest("SHA-256", data);
50
+ const codeChallenge = base64UrlEncode(digest);
51
+ return { codeVerifier, codeChallenge };
52
+ }
53
+ function generateState() {
54
+ return generateRandomString(32);
55
+ }
56
+ function generateNonce() {
57
+ return generateRandomString(32);
58
+ }
59
+
60
+ // src/storage.ts
61
+ var TokenStorage = class {
62
+ constructor(prefix = "idaas") {
63
+ this.prefix = prefix;
64
+ }
65
+ key(name) {
66
+ return `${this.prefix}:${name}`;
67
+ }
68
+ saveTokens(tokens) {
69
+ const expiresAt = Date.now() + tokens.expires_in * 1e3;
70
+ sessionStorage.setItem(this.key("access_token"), tokens.access_token);
71
+ sessionStorage.setItem(this.key("expires_at"), expiresAt.toString());
72
+ if (tokens.id_token) {
73
+ sessionStorage.setItem(this.key("id_token"), tokens.id_token);
74
+ }
75
+ if (tokens.refresh_token) {
76
+ localStorage.setItem(this.key("refresh_token"), tokens.refresh_token);
77
+ }
78
+ sessionStorage.setItem(this.key("scope"), tokens.scope);
79
+ }
80
+ getAccessToken() {
81
+ const token = sessionStorage.getItem(this.key("access_token"));
82
+ const expiresAt = sessionStorage.getItem(this.key("expires_at"));
83
+ if (!token || !expiresAt) return null;
84
+ if (Date.now() > parseInt(expiresAt, 10)) return null;
85
+ return token;
86
+ }
87
+ getIdToken() {
88
+ return sessionStorage.getItem(this.key("id_token"));
89
+ }
90
+ getRefreshToken() {
91
+ return localStorage.getItem(this.key("refresh_token"));
92
+ }
93
+ getExpiresAt() {
94
+ const val = sessionStorage.getItem(this.key("expires_at"));
95
+ return val ? parseInt(val, 10) : null;
96
+ }
97
+ isTokenExpired() {
98
+ const expiresAt = this.getExpiresAt();
99
+ if (!expiresAt) return true;
100
+ return Date.now() > expiresAt - 6e4;
101
+ }
102
+ clear() {
103
+ sessionStorage.removeItem(this.key("access_token"));
104
+ sessionStorage.removeItem(this.key("id_token"));
105
+ sessionStorage.removeItem(this.key("expires_at"));
106
+ sessionStorage.removeItem(this.key("scope"));
107
+ localStorage.removeItem(this.key("refresh_token"));
108
+ }
109
+ saveState(state) {
110
+ sessionStorage.setItem(this.key("state"), state);
111
+ }
112
+ getState() {
113
+ return sessionStorage.getItem(this.key("state"));
114
+ }
115
+ saveCodeVerifier(verifier) {
116
+ sessionStorage.setItem(this.key("code_verifier"), verifier);
117
+ }
118
+ getCodeVerifier() {
119
+ return sessionStorage.getItem(this.key("code_verifier"));
120
+ }
121
+ saveNonce(nonce) {
122
+ sessionStorage.setItem(this.key("nonce"), nonce);
123
+ }
124
+ getNonce() {
125
+ return sessionStorage.getItem(this.key("nonce"));
126
+ }
127
+ clearAuthState() {
128
+ sessionStorage.removeItem(this.key("state"));
129
+ sessionStorage.removeItem(this.key("code_verifier"));
130
+ sessionStorage.removeItem(this.key("nonce"));
131
+ }
132
+ };
133
+
134
+ // src/client.ts
135
+ var IdaasClient = class {
136
+ constructor(config) {
137
+ this.endpoints = null;
138
+ this.userInfo = null;
139
+ this.config = {
140
+ scope: "openid profile email",
141
+ autoDiscovery: true,
142
+ storagePrefix: "idaas",
143
+ ...config
144
+ };
145
+ this.storage = new TokenStorage(this.config.storagePrefix);
146
+ }
147
+ async init() {
148
+ if (this.config.autoDiscovery) {
149
+ await this.discover();
150
+ } else {
151
+ this.endpoints = {
152
+ authorization_endpoint: `${this.config.issuer}/oauth2/authorize`,
153
+ token_endpoint: `${this.config.issuer}/oauth2/token`,
154
+ userinfo_endpoint: `${this.config.issuer}/oauth2/userinfo`,
155
+ end_session_endpoint: `${this.config.issuer}/oauth2/logout`,
156
+ jwks_uri: `${this.config.issuer}/.well-known/jwks.json`,
157
+ revocation_endpoint: `${this.config.issuer}/oauth2/revoke`,
158
+ introspection_endpoint: `${this.config.issuer}/oauth2/introspect`
159
+ };
160
+ }
161
+ return this.getAuthState();
162
+ }
163
+ async discover() {
164
+ const res = await fetch(`${this.config.issuer}/.well-known/openid-configuration`);
165
+ if (!res.ok) throw new Error("Failed to fetch OIDC discovery document");
166
+ const doc = await res.json();
167
+ this.endpoints = {
168
+ authorization_endpoint: doc.authorization_endpoint,
169
+ token_endpoint: doc.token_endpoint,
170
+ userinfo_endpoint: doc.userinfo_endpoint,
171
+ end_session_endpoint: doc.end_session_endpoint,
172
+ jwks_uri: doc.jwks_uri,
173
+ revocation_endpoint: doc.revocation_endpoint,
174
+ introspection_endpoint: doc.introspection_endpoint
175
+ };
176
+ }
177
+ async login(extraParams) {
178
+ if (!this.endpoints) await this.init();
179
+ const { codeVerifier, codeChallenge } = await createPKCE();
180
+ const state = generateState();
181
+ const nonce = generateNonce();
182
+ this.storage.saveCodeVerifier(codeVerifier);
183
+ this.storage.saveState(state);
184
+ this.storage.saveNonce(nonce);
185
+ const params = new URLSearchParams({
186
+ response_type: "code",
187
+ client_id: this.config.clientId,
188
+ redirect_uri: this.config.redirectUri,
189
+ scope: this.config.scope,
190
+ state,
191
+ nonce,
192
+ code_challenge: codeChallenge,
193
+ code_challenge_method: "S256",
194
+ ...extraParams
195
+ });
196
+ window.location.href = `${this.endpoints.authorization_endpoint}?${params.toString()}`;
197
+ }
198
+ async handleCallback(callbackUrl) {
199
+ if (!this.endpoints) await this.init();
200
+ const url = new URL(callbackUrl || window.location.href);
201
+ const code = url.searchParams.get("code");
202
+ const state = url.searchParams.get("state");
203
+ const error = url.searchParams.get("error");
204
+ if (error) {
205
+ const desc = url.searchParams.get("error_description") || error;
206
+ throw new Error(desc);
207
+ }
208
+ if (!code) throw new Error("No authorization code in callback URL");
209
+ const savedState = this.storage.getState();
210
+ if (state !== savedState) {
211
+ throw new Error("State mismatch - possible CSRF attack");
212
+ }
213
+ const codeVerifier = this.storage.getCodeVerifier();
214
+ if (!codeVerifier) throw new Error("No code verifier found");
215
+ const body = new URLSearchParams({
216
+ grant_type: "authorization_code",
217
+ code,
218
+ redirect_uri: this.config.redirectUri,
219
+ client_id: this.config.clientId,
220
+ code_verifier: codeVerifier
221
+ });
222
+ if (this.config.clientSecret) {
223
+ body.set("client_secret", this.config.clientSecret);
224
+ }
225
+ const res = await fetch(this.endpoints.token_endpoint, {
226
+ method: "POST",
227
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
228
+ body: body.toString()
229
+ });
230
+ if (!res.ok) {
231
+ const err = await res.json().catch(() => ({ error: "token_exchange_failed" }));
232
+ throw new Error(err.error_description || err.error || "Token exchange failed");
233
+ }
234
+ const tokens = await res.json();
235
+ this.storage.saveTokens(tokens);
236
+ this.storage.clearAuthState();
237
+ return tokens;
238
+ }
239
+ async refreshToken() {
240
+ if (!this.endpoints) await this.init();
241
+ const refreshToken = this.storage.getRefreshToken();
242
+ if (!refreshToken) return null;
243
+ const body = new URLSearchParams({
244
+ grant_type: "refresh_token",
245
+ refresh_token: refreshToken,
246
+ client_id: this.config.clientId
247
+ });
248
+ if (this.config.clientSecret) {
249
+ body.set("client_secret", this.config.clientSecret);
250
+ }
251
+ const res = await fetch(this.endpoints.token_endpoint, {
252
+ method: "POST",
253
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
254
+ body: body.toString()
255
+ });
256
+ if (!res.ok) {
257
+ this.storage.clear();
258
+ return null;
259
+ }
260
+ const tokens = await res.json();
261
+ this.storage.saveTokens(tokens);
262
+ return tokens;
263
+ }
264
+ async getUserInfo(forceRefresh = false) {
265
+ if (!this.endpoints) await this.init();
266
+ if (this.userInfo && !forceRefresh) return this.userInfo;
267
+ const accessToken = await this.getAccessToken();
268
+ if (!accessToken) return null;
269
+ const res = await fetch(this.endpoints.userinfo_endpoint, {
270
+ headers: { Authorization: `Bearer ${accessToken}` }
271
+ });
272
+ if (!res.ok) return null;
273
+ this.userInfo = await res.json();
274
+ return this.userInfo;
275
+ }
276
+ async getAccessToken() {
277
+ let token = this.storage.getAccessToken();
278
+ if (token) return token;
279
+ if (this.storage.isTokenExpired()) {
280
+ const tokens = await this.refreshToken();
281
+ if (tokens) return tokens.access_token;
282
+ }
283
+ return null;
284
+ }
285
+ getIdToken() {
286
+ return this.storage.getIdToken();
287
+ }
288
+ getAuthState() {
289
+ const accessToken = this.storage.getAccessToken();
290
+ return {
291
+ isAuthenticated: !!accessToken,
292
+ user: this.userInfo,
293
+ accessToken,
294
+ idToken: this.storage.getIdToken(),
295
+ expiresAt: this.storage.getExpiresAt()
296
+ };
297
+ }
298
+ isAuthenticated() {
299
+ return !!this.storage.getAccessToken();
300
+ }
301
+ async logout() {
302
+ if (!this.endpoints) await this.init();
303
+ const idToken = this.storage.getIdToken();
304
+ this.storage.clear();
305
+ this.userInfo = null;
306
+ const params = new URLSearchParams();
307
+ if (idToken) params.set("id_token_hint", idToken);
308
+ if (this.config.postLogoutRedirectUri) {
309
+ params.set("post_logout_redirect_uri", this.config.postLogoutRedirectUri);
310
+ }
311
+ window.location.href = `${this.endpoints.end_session_endpoint}?${params.toString()}`;
312
+ }
313
+ async revokeToken() {
314
+ if (!this.endpoints) await this.init();
315
+ const refreshToken = this.storage.getRefreshToken();
316
+ if (refreshToken) {
317
+ await fetch(this.endpoints.revocation_endpoint, {
318
+ method: "POST",
319
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
320
+ body: new URLSearchParams({
321
+ token: refreshToken,
322
+ token_type_hint: "refresh_token"
323
+ }).toString()
324
+ });
325
+ }
326
+ }
327
+ /** Create an authenticated fetch wrapper */
328
+ async fetch(url, init) {
329
+ const token = await this.getAccessToken();
330
+ const headers = new Headers(init?.headers);
331
+ if (token) {
332
+ headers.set("Authorization", `Bearer ${token}`);
333
+ }
334
+ return fetch(url, { ...init, headers });
335
+ }
336
+ };
337
+ // Annotate the CommonJS export names for ESM import in node:
338
+ 0 && (module.exports = {
339
+ IdaasClient,
340
+ TokenStorage,
341
+ createPKCE,
342
+ generateNonce,
343
+ generateState
344
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,313 @@
1
+ // src/crypto.ts
2
+ function generateRandomString(length) {
3
+ const array = new Uint8Array(length);
4
+ crypto.getRandomValues(array);
5
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("").substring(0, length);
6
+ }
7
+ function base64UrlEncode(buffer) {
8
+ const bytes = new Uint8Array(buffer);
9
+ let binary = "";
10
+ bytes.forEach((b) => {
11
+ binary += String.fromCharCode(b);
12
+ });
13
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
14
+ }
15
+ async function createPKCE() {
16
+ const codeVerifier = generateRandomString(64);
17
+ const encoder = new TextEncoder();
18
+ const data = encoder.encode(codeVerifier);
19
+ const digest = await crypto.subtle.digest("SHA-256", data);
20
+ const codeChallenge = base64UrlEncode(digest);
21
+ return { codeVerifier, codeChallenge };
22
+ }
23
+ function generateState() {
24
+ return generateRandomString(32);
25
+ }
26
+ function generateNonce() {
27
+ return generateRandomString(32);
28
+ }
29
+
30
+ // src/storage.ts
31
+ var TokenStorage = class {
32
+ constructor(prefix = "idaas") {
33
+ this.prefix = prefix;
34
+ }
35
+ key(name) {
36
+ return `${this.prefix}:${name}`;
37
+ }
38
+ saveTokens(tokens) {
39
+ const expiresAt = Date.now() + tokens.expires_in * 1e3;
40
+ sessionStorage.setItem(this.key("access_token"), tokens.access_token);
41
+ sessionStorage.setItem(this.key("expires_at"), expiresAt.toString());
42
+ if (tokens.id_token) {
43
+ sessionStorage.setItem(this.key("id_token"), tokens.id_token);
44
+ }
45
+ if (tokens.refresh_token) {
46
+ localStorage.setItem(this.key("refresh_token"), tokens.refresh_token);
47
+ }
48
+ sessionStorage.setItem(this.key("scope"), tokens.scope);
49
+ }
50
+ getAccessToken() {
51
+ const token = sessionStorage.getItem(this.key("access_token"));
52
+ const expiresAt = sessionStorage.getItem(this.key("expires_at"));
53
+ if (!token || !expiresAt) return null;
54
+ if (Date.now() > parseInt(expiresAt, 10)) return null;
55
+ return token;
56
+ }
57
+ getIdToken() {
58
+ return sessionStorage.getItem(this.key("id_token"));
59
+ }
60
+ getRefreshToken() {
61
+ return localStorage.getItem(this.key("refresh_token"));
62
+ }
63
+ getExpiresAt() {
64
+ const val = sessionStorage.getItem(this.key("expires_at"));
65
+ return val ? parseInt(val, 10) : null;
66
+ }
67
+ isTokenExpired() {
68
+ const expiresAt = this.getExpiresAt();
69
+ if (!expiresAt) return true;
70
+ return Date.now() > expiresAt - 6e4;
71
+ }
72
+ clear() {
73
+ sessionStorage.removeItem(this.key("access_token"));
74
+ sessionStorage.removeItem(this.key("id_token"));
75
+ sessionStorage.removeItem(this.key("expires_at"));
76
+ sessionStorage.removeItem(this.key("scope"));
77
+ localStorage.removeItem(this.key("refresh_token"));
78
+ }
79
+ saveState(state) {
80
+ sessionStorage.setItem(this.key("state"), state);
81
+ }
82
+ getState() {
83
+ return sessionStorage.getItem(this.key("state"));
84
+ }
85
+ saveCodeVerifier(verifier) {
86
+ sessionStorage.setItem(this.key("code_verifier"), verifier);
87
+ }
88
+ getCodeVerifier() {
89
+ return sessionStorage.getItem(this.key("code_verifier"));
90
+ }
91
+ saveNonce(nonce) {
92
+ sessionStorage.setItem(this.key("nonce"), nonce);
93
+ }
94
+ getNonce() {
95
+ return sessionStorage.getItem(this.key("nonce"));
96
+ }
97
+ clearAuthState() {
98
+ sessionStorage.removeItem(this.key("state"));
99
+ sessionStorage.removeItem(this.key("code_verifier"));
100
+ sessionStorage.removeItem(this.key("nonce"));
101
+ }
102
+ };
103
+
104
+ // src/client.ts
105
+ var IdaasClient = class {
106
+ constructor(config) {
107
+ this.endpoints = null;
108
+ this.userInfo = null;
109
+ this.config = {
110
+ scope: "openid profile email",
111
+ autoDiscovery: true,
112
+ storagePrefix: "idaas",
113
+ ...config
114
+ };
115
+ this.storage = new TokenStorage(this.config.storagePrefix);
116
+ }
117
+ async init() {
118
+ if (this.config.autoDiscovery) {
119
+ await this.discover();
120
+ } else {
121
+ this.endpoints = {
122
+ authorization_endpoint: `${this.config.issuer}/oauth2/authorize`,
123
+ token_endpoint: `${this.config.issuer}/oauth2/token`,
124
+ userinfo_endpoint: `${this.config.issuer}/oauth2/userinfo`,
125
+ end_session_endpoint: `${this.config.issuer}/oauth2/logout`,
126
+ jwks_uri: `${this.config.issuer}/.well-known/jwks.json`,
127
+ revocation_endpoint: `${this.config.issuer}/oauth2/revoke`,
128
+ introspection_endpoint: `${this.config.issuer}/oauth2/introspect`
129
+ };
130
+ }
131
+ return this.getAuthState();
132
+ }
133
+ async discover() {
134
+ const res = await fetch(`${this.config.issuer}/.well-known/openid-configuration`);
135
+ if (!res.ok) throw new Error("Failed to fetch OIDC discovery document");
136
+ const doc = await res.json();
137
+ this.endpoints = {
138
+ authorization_endpoint: doc.authorization_endpoint,
139
+ token_endpoint: doc.token_endpoint,
140
+ userinfo_endpoint: doc.userinfo_endpoint,
141
+ end_session_endpoint: doc.end_session_endpoint,
142
+ jwks_uri: doc.jwks_uri,
143
+ revocation_endpoint: doc.revocation_endpoint,
144
+ introspection_endpoint: doc.introspection_endpoint
145
+ };
146
+ }
147
+ async login(extraParams) {
148
+ if (!this.endpoints) await this.init();
149
+ const { codeVerifier, codeChallenge } = await createPKCE();
150
+ const state = generateState();
151
+ const nonce = generateNonce();
152
+ this.storage.saveCodeVerifier(codeVerifier);
153
+ this.storage.saveState(state);
154
+ this.storage.saveNonce(nonce);
155
+ const params = new URLSearchParams({
156
+ response_type: "code",
157
+ client_id: this.config.clientId,
158
+ redirect_uri: this.config.redirectUri,
159
+ scope: this.config.scope,
160
+ state,
161
+ nonce,
162
+ code_challenge: codeChallenge,
163
+ code_challenge_method: "S256",
164
+ ...extraParams
165
+ });
166
+ window.location.href = `${this.endpoints.authorization_endpoint}?${params.toString()}`;
167
+ }
168
+ async handleCallback(callbackUrl) {
169
+ if (!this.endpoints) await this.init();
170
+ const url = new URL(callbackUrl || window.location.href);
171
+ const code = url.searchParams.get("code");
172
+ const state = url.searchParams.get("state");
173
+ const error = url.searchParams.get("error");
174
+ if (error) {
175
+ const desc = url.searchParams.get("error_description") || error;
176
+ throw new Error(desc);
177
+ }
178
+ if (!code) throw new Error("No authorization code in callback URL");
179
+ const savedState = this.storage.getState();
180
+ if (state !== savedState) {
181
+ throw new Error("State mismatch - possible CSRF attack");
182
+ }
183
+ const codeVerifier = this.storage.getCodeVerifier();
184
+ if (!codeVerifier) throw new Error("No code verifier found");
185
+ const body = new URLSearchParams({
186
+ grant_type: "authorization_code",
187
+ code,
188
+ redirect_uri: this.config.redirectUri,
189
+ client_id: this.config.clientId,
190
+ code_verifier: codeVerifier
191
+ });
192
+ if (this.config.clientSecret) {
193
+ body.set("client_secret", this.config.clientSecret);
194
+ }
195
+ const res = await fetch(this.endpoints.token_endpoint, {
196
+ method: "POST",
197
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
198
+ body: body.toString()
199
+ });
200
+ if (!res.ok) {
201
+ const err = await res.json().catch(() => ({ error: "token_exchange_failed" }));
202
+ throw new Error(err.error_description || err.error || "Token exchange failed");
203
+ }
204
+ const tokens = await res.json();
205
+ this.storage.saveTokens(tokens);
206
+ this.storage.clearAuthState();
207
+ return tokens;
208
+ }
209
+ async refreshToken() {
210
+ if (!this.endpoints) await this.init();
211
+ const refreshToken = this.storage.getRefreshToken();
212
+ if (!refreshToken) return null;
213
+ const body = new URLSearchParams({
214
+ grant_type: "refresh_token",
215
+ refresh_token: refreshToken,
216
+ client_id: this.config.clientId
217
+ });
218
+ if (this.config.clientSecret) {
219
+ body.set("client_secret", this.config.clientSecret);
220
+ }
221
+ const res = await fetch(this.endpoints.token_endpoint, {
222
+ method: "POST",
223
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
224
+ body: body.toString()
225
+ });
226
+ if (!res.ok) {
227
+ this.storage.clear();
228
+ return null;
229
+ }
230
+ const tokens = await res.json();
231
+ this.storage.saveTokens(tokens);
232
+ return tokens;
233
+ }
234
+ async getUserInfo(forceRefresh = false) {
235
+ if (!this.endpoints) await this.init();
236
+ if (this.userInfo && !forceRefresh) return this.userInfo;
237
+ const accessToken = await this.getAccessToken();
238
+ if (!accessToken) return null;
239
+ const res = await fetch(this.endpoints.userinfo_endpoint, {
240
+ headers: { Authorization: `Bearer ${accessToken}` }
241
+ });
242
+ if (!res.ok) return null;
243
+ this.userInfo = await res.json();
244
+ return this.userInfo;
245
+ }
246
+ async getAccessToken() {
247
+ let token = this.storage.getAccessToken();
248
+ if (token) return token;
249
+ if (this.storage.isTokenExpired()) {
250
+ const tokens = await this.refreshToken();
251
+ if (tokens) return tokens.access_token;
252
+ }
253
+ return null;
254
+ }
255
+ getIdToken() {
256
+ return this.storage.getIdToken();
257
+ }
258
+ getAuthState() {
259
+ const accessToken = this.storage.getAccessToken();
260
+ return {
261
+ isAuthenticated: !!accessToken,
262
+ user: this.userInfo,
263
+ accessToken,
264
+ idToken: this.storage.getIdToken(),
265
+ expiresAt: this.storage.getExpiresAt()
266
+ };
267
+ }
268
+ isAuthenticated() {
269
+ return !!this.storage.getAccessToken();
270
+ }
271
+ async logout() {
272
+ if (!this.endpoints) await this.init();
273
+ const idToken = this.storage.getIdToken();
274
+ this.storage.clear();
275
+ this.userInfo = null;
276
+ const params = new URLSearchParams();
277
+ if (idToken) params.set("id_token_hint", idToken);
278
+ if (this.config.postLogoutRedirectUri) {
279
+ params.set("post_logout_redirect_uri", this.config.postLogoutRedirectUri);
280
+ }
281
+ window.location.href = `${this.endpoints.end_session_endpoint}?${params.toString()}`;
282
+ }
283
+ async revokeToken() {
284
+ if (!this.endpoints) await this.init();
285
+ const refreshToken = this.storage.getRefreshToken();
286
+ if (refreshToken) {
287
+ await fetch(this.endpoints.revocation_endpoint, {
288
+ method: "POST",
289
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
290
+ body: new URLSearchParams({
291
+ token: refreshToken,
292
+ token_type_hint: "refresh_token"
293
+ }).toString()
294
+ });
295
+ }
296
+ }
297
+ /** Create an authenticated fetch wrapper */
298
+ async fetch(url, init) {
299
+ const token = await this.getAccessToken();
300
+ const headers = new Headers(init?.headers);
301
+ if (token) {
302
+ headers.set("Authorization", `Bearer ${token}`);
303
+ }
304
+ return fetch(url, { ...init, headers });
305
+ }
306
+ };
307
+ export {
308
+ IdaasClient,
309
+ TokenStorage,
310
+ createPKCE,
311
+ generateNonce,
312
+ generateState
313
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@zskj-idaas/sdk",
3
+ "version": "1.0.0",
4
+ "description": "IDaaS OIDC Client SDK for browser applications — supports Authorization Code Flow with PKCE, token management, and authenticated fetch.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.mts",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "sideEffects": false,
26
+ "scripts": {
27
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
28
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "keywords": [
32
+ "idaas",
33
+ "oidc",
34
+ "oauth2",
35
+ "pkce",
36
+ "sso",
37
+ "authentication",
38
+ "identity",
39
+ "openid-connect",
40
+ "browser-sdk"
41
+ ],
42
+ "license": "MIT",
43
+ "author": "IDaaS Team",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": ""
47
+ },
48
+ "homepage": "",
49
+ "bugs": {
50
+ "url": ""
51
+ },
52
+ "engines": {
53
+ "node": ">=16.0.0"
54
+ },
55
+ "browserslist": [
56
+ "> 0.5%",
57
+ "not dead",
58
+ "not ie 11"
59
+ ],
60
+ "dependencies": {},
61
+ "devDependencies": {
62
+ "tsup": "^8.2.0",
63
+ "typescript": "^5.6.0"
64
+ }
65
+ }