@niid/sdk 1.0.2 → 1.0.3
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/dist/core/OAuthFlow.d.ts +8 -0
- package/dist/core/OAuthFlow.d.ts.map +1 -1
- package/dist/core/TokenManager.d.ts +11 -0
- package/dist/core/TokenManager.d.ts.map +1 -1
- package/dist/index.js +125 -17
- package/dist/index.js.map +1 -1
- package/dist/utils/validation.d.ts +8 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/core/OAuthFlow.d.ts
CHANGED
|
@@ -4,12 +4,20 @@ export declare class OAuthFlow {
|
|
|
4
4
|
private config;
|
|
5
5
|
private apiClient;
|
|
6
6
|
private stateStorageKey;
|
|
7
|
+
private pkceStorageKey;
|
|
7
8
|
constructor(config: NIIDConfig);
|
|
8
9
|
initiateLogin(): void;
|
|
9
10
|
exchangeCodeForToken(code: string, state?: string): Promise<TokenResponse>;
|
|
10
11
|
refreshAccessToken(refreshToken: string): Promise<TokenResponse>;
|
|
12
|
+
/**
|
|
13
|
+
* POST request with automatic retry on network/server errors
|
|
14
|
+
*/
|
|
15
|
+
private postWithRetry;
|
|
11
16
|
private saveState;
|
|
12
17
|
private getState;
|
|
13
18
|
private clearState;
|
|
19
|
+
private savePKCE;
|
|
20
|
+
private getPKCE;
|
|
21
|
+
private clearPKCE;
|
|
14
22
|
}
|
|
15
23
|
//# sourceMappingURL=OAuthFlow.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OAuthFlow.d.ts","sourceRoot":"","sources":["../../src/core/OAuthFlow.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"OAuthFlow.d.ts","sourceRoot":"","sources":["../../src/core/OAuthFlow.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AA0BrD,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,cAAc,CAAS;gBAEnB,MAAM,EAAE,UAAU;IAuB9B,aAAa,IAAI,IAAI;IAoBf,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAgC1E,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IActE;;OAEG;YACW,aAAa;IA2B3B,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,SAAS;CASlB"}
|
|
@@ -5,6 +5,8 @@ export declare class TokenManager {
|
|
|
5
5
|
private accessTokenKey;
|
|
6
6
|
private refreshTokenKey;
|
|
7
7
|
private refreshTimer;
|
|
8
|
+
private isRefreshing;
|
|
9
|
+
private refreshPromise;
|
|
8
10
|
constructor(storageKey?: string);
|
|
9
11
|
setTokens(tokenResponse: TokenResponse): void;
|
|
10
12
|
getAccessToken(): string | null;
|
|
@@ -12,6 +14,15 @@ export declare class TokenManager {
|
|
|
12
14
|
isTokenValid(token: string | null): boolean;
|
|
13
15
|
isAccessTokenValid(): boolean;
|
|
14
16
|
getTokenExpiration(token: string): number | null;
|
|
17
|
+
/**
|
|
18
|
+
* Execute a refresh with lock to prevent duplicate requests.
|
|
19
|
+
* If refresh is already in progress, wait for it to complete.
|
|
20
|
+
*/
|
|
21
|
+
executeWithRefreshLock(refreshFn: () => Promise<void>): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Check if a refresh is currently in progress
|
|
24
|
+
*/
|
|
25
|
+
isRefreshInProgress(): boolean;
|
|
15
26
|
scheduleTokenRefresh(accessToken: string, onRefresh?: () => void): void;
|
|
16
27
|
clearRefreshTimer(): void;
|
|
17
28
|
clearTokens(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TokenManager.d.ts","sourceRoot":"","sources":["../../src/core/TokenManager.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,aAAa,EAAE,MAAM,UAAU,CAAC;AAErD,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAA8C;
|
|
1
|
+
{"version":3,"file":"TokenManager.d.ts","sourceRoot":"","sources":["../../src/core/TokenManager.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,aAAa,EAAE,MAAM,UAAU,CAAC;AAErD,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAA8C;IAGlE,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,cAAc,CAA8B;gBAExC,UAAU,GAAE,MAAe;IAMvC,SAAS,CAAC,aAAa,EAAE,aAAa,GAAG,IAAI;IAQ7C,cAAc,IAAI,MAAM,GAAG,IAAI;IAI/B,eAAe,IAAI,MAAM,GAAG,IAAI;IAIhC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;IAY3C,kBAAkB,IAAI,OAAO;IAK7B,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAShD;;;OAGG;IACG,sBAAsB,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB3E;;OAEG;IACH,mBAAmB,IAAI,OAAO;IAI9B,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;IAoBvE,iBAAiB,IAAI,IAAI;IAOzB,WAAW,IAAI,IAAI;CAKpB"}
|
package/dist/index.js
CHANGED
|
@@ -2512,7 +2512,27 @@ function validateConfig(config) {
|
|
|
2512
2512
|
function generateState() {
|
|
2513
2513
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
2514
2514
|
}
|
|
2515
|
-
function
|
|
2515
|
+
function generatePKCE() {
|
|
2516
|
+
const array = new Uint8Array(32);
|
|
2517
|
+
if (typeof window !== "undefined" && window.crypto) {
|
|
2518
|
+
window.crypto.getRandomValues(array);
|
|
2519
|
+
} else {
|
|
2520
|
+
for (let i = 0; i < array.length; i++) {
|
|
2521
|
+
array[i] = Math.floor(Math.random() * 256);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
const verifier = base64UrlEncode(array);
|
|
2525
|
+
const challenge = verifier;
|
|
2526
|
+
return { verifier, challenge };
|
|
2527
|
+
}
|
|
2528
|
+
function base64UrlEncode(buffer) {
|
|
2529
|
+
let binary = "";
|
|
2530
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
2531
|
+
binary += String.fromCharCode(buffer[i]);
|
|
2532
|
+
}
|
|
2533
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2534
|
+
}
|
|
2535
|
+
function buildAuthorizationUrl(ssoUrl, clientId, redirectUri, scope = "profile email", state, codeChallenge) {
|
|
2516
2536
|
const params = new URLSearchParams({
|
|
2517
2537
|
response_type: "code",
|
|
2518
2538
|
client_id: clientId,
|
|
@@ -2522,6 +2542,10 @@ function buildAuthorizationUrl(ssoUrl, clientId, redirectUri, scope = "profile e
|
|
|
2522
2542
|
if (state) {
|
|
2523
2543
|
params.append("state", state);
|
|
2524
2544
|
}
|
|
2545
|
+
if (codeChallenge) {
|
|
2546
|
+
params.append("code_challenge", codeChallenge);
|
|
2547
|
+
params.append("code_challenge_method", "plain");
|
|
2548
|
+
}
|
|
2525
2549
|
return `${ssoUrl}/?${params.toString()}`;
|
|
2526
2550
|
}
|
|
2527
2551
|
class InvalidTokenError extends Error {
|
|
@@ -2624,6 +2648,8 @@ class StorageManager {
|
|
|
2624
2648
|
class TokenManager {
|
|
2625
2649
|
constructor(storageKey = "niid") {
|
|
2626
2650
|
this.refreshTimer = null;
|
|
2651
|
+
this.isRefreshing = false;
|
|
2652
|
+
this.refreshPromise = null;
|
|
2627
2653
|
this.storage = new StorageManager(storageKey);
|
|
2628
2654
|
this.accessTokenKey = "access_token";
|
|
2629
2655
|
this.refreshTokenKey = "refresh_token";
|
|
@@ -2663,6 +2689,27 @@ class TokenManager {
|
|
|
2663
2689
|
return null;
|
|
2664
2690
|
}
|
|
2665
2691
|
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Execute a refresh with lock to prevent duplicate requests.
|
|
2694
|
+
* If refresh is already in progress, wait for it to complete.
|
|
2695
|
+
*/
|
|
2696
|
+
async executeWithRefreshLock(refreshFn) {
|
|
2697
|
+
if (this.isRefreshing && this.refreshPromise) {
|
|
2698
|
+
return this.refreshPromise;
|
|
2699
|
+
}
|
|
2700
|
+
this.isRefreshing = true;
|
|
2701
|
+
this.refreshPromise = refreshFn().finally(() => {
|
|
2702
|
+
this.isRefreshing = false;
|
|
2703
|
+
this.refreshPromise = null;
|
|
2704
|
+
});
|
|
2705
|
+
return this.refreshPromise;
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Check if a refresh is currently in progress
|
|
2709
|
+
*/
|
|
2710
|
+
isRefreshInProgress() {
|
|
2711
|
+
return this.isRefreshing;
|
|
2712
|
+
}
|
|
2666
2713
|
scheduleTokenRefresh(accessToken, onRefresh) {
|
|
2667
2714
|
this.clearRefreshTimer();
|
|
2668
2715
|
const expiration = this.getTokenExpiration(accessToken);
|
|
@@ -2690,6 +2737,18 @@ class TokenManager {
|
|
|
2690
2737
|
this.clearRefreshTimer();
|
|
2691
2738
|
}
|
|
2692
2739
|
}
|
|
2740
|
+
const MAX_RETRIES = 3;
|
|
2741
|
+
const RETRY_DELAY_MS = 1e3;
|
|
2742
|
+
function delay(ms) {
|
|
2743
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2744
|
+
}
|
|
2745
|
+
function isRetryableError(error) {
|
|
2746
|
+
if (!error.response) {
|
|
2747
|
+
return true;
|
|
2748
|
+
}
|
|
2749
|
+
const status = error.response.status;
|
|
2750
|
+
return status >= 500 && status < 600;
|
|
2751
|
+
}
|
|
2693
2752
|
class OAuthFlow {
|
|
2694
2753
|
constructor(config) {
|
|
2695
2754
|
this.config = {
|
|
@@ -2702,27 +2761,32 @@ class OAuthFlow {
|
|
|
2702
2761
|
storageKey: config.storageKey || "niid"
|
|
2703
2762
|
};
|
|
2704
2763
|
this.stateStorageKey = `${this.config.storageKey}_oauth_state_${this.config.clientId}`;
|
|
2764
|
+
this.pkceStorageKey = `${this.config.storageKey}_pkce_verifier_${this.config.clientId}`;
|
|
2705
2765
|
this.apiClient = axios.create({
|
|
2706
2766
|
baseURL: this.config.apiUrl,
|
|
2707
2767
|
headers: {
|
|
2708
2768
|
"Content-Type": "application/json"
|
|
2709
|
-
}
|
|
2769
|
+
},
|
|
2770
|
+
timeout: 3e4
|
|
2771
|
+
// 30 second timeout
|
|
2710
2772
|
});
|
|
2711
2773
|
}
|
|
2712
2774
|
initiateLogin() {
|
|
2713
2775
|
const state = generateState();
|
|
2714
2776
|
this.saveState(state);
|
|
2777
|
+
const { verifier, challenge } = generatePKCE();
|
|
2778
|
+
this.savePKCE(verifier);
|
|
2715
2779
|
const authUrl = buildAuthorizationUrl(
|
|
2716
2780
|
this.config.ssoUrl,
|
|
2717
2781
|
this.config.clientId,
|
|
2718
2782
|
this.config.redirectUri,
|
|
2719
2783
|
this.config.scope,
|
|
2720
|
-
state
|
|
2784
|
+
state,
|
|
2785
|
+
challenge
|
|
2721
2786
|
);
|
|
2722
2787
|
window.location.href = authUrl;
|
|
2723
2788
|
}
|
|
2724
2789
|
async exchangeCodeForToken(code, state) {
|
|
2725
|
-
var _a, _b;
|
|
2726
2790
|
if (state) {
|
|
2727
2791
|
const savedState = this.getState();
|
|
2728
2792
|
if (savedState) {
|
|
@@ -2741,16 +2805,14 @@ class OAuthFlow {
|
|
|
2741
2805
|
if (this.config.clientSecret) {
|
|
2742
2806
|
tokenData.client_secret = this.config.clientSecret;
|
|
2743
2807
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
const errorMessage = ((_b = (_a = error.response) == null ? void 0 : _a.data) == null ? void 0 : _b.detail) || error.message || "Failed to exchange authorization code";
|
|
2749
|
-
throw new Error(errorMessage);
|
|
2808
|
+
const pkceVerifier = this.getPKCE();
|
|
2809
|
+
if (pkceVerifier) {
|
|
2810
|
+
tokenData.code_verifier = pkceVerifier;
|
|
2811
|
+
this.clearPKCE();
|
|
2750
2812
|
}
|
|
2813
|
+
return this.postWithRetry("/oauth/token", tokenData);
|
|
2751
2814
|
}
|
|
2752
2815
|
async refreshAccessToken(refreshToken) {
|
|
2753
|
-
var _a, _b;
|
|
2754
2816
|
const tokenData = {
|
|
2755
2817
|
grant_type: "refresh_token",
|
|
2756
2818
|
refresh_token: refreshToken,
|
|
@@ -2759,13 +2821,32 @@ class OAuthFlow {
|
|
|
2759
2821
|
if (this.config.clientSecret) {
|
|
2760
2822
|
tokenData.client_secret = this.config.clientSecret;
|
|
2761
2823
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2824
|
+
return this.postWithRetry("/oauth/token", tokenData);
|
|
2825
|
+
}
|
|
2826
|
+
/**
|
|
2827
|
+
* POST request with automatic retry on network/server errors
|
|
2828
|
+
*/
|
|
2829
|
+
async postWithRetry(url, data) {
|
|
2830
|
+
var _a, _b;
|
|
2831
|
+
let lastError = null;
|
|
2832
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
2833
|
+
try {
|
|
2834
|
+
const response = await this.apiClient.post(url, data);
|
|
2835
|
+
return response.data;
|
|
2836
|
+
} catch (error) {
|
|
2837
|
+
lastError = error;
|
|
2838
|
+
if (error.isAxiosError && isRetryableError(error)) {
|
|
2839
|
+
if (attempt < MAX_RETRIES) {
|
|
2840
|
+
console.warn(`[NIID SDK] Request failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`);
|
|
2841
|
+
await delay(RETRY_DELAY_MS * attempt);
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
const errorMessage = ((_b = (_a = error.response) == null ? void 0 : _a.data) == null ? void 0 : _b.detail) || error.message || "Request failed";
|
|
2846
|
+
throw new Error(errorMessage);
|
|
2847
|
+
}
|
|
2768
2848
|
}
|
|
2849
|
+
throw lastError || new Error("Request failed after retries");
|
|
2769
2850
|
}
|
|
2770
2851
|
saveState(state) {
|
|
2771
2852
|
if (typeof window !== "undefined") {
|
|
@@ -2796,6 +2877,33 @@ class OAuthFlow {
|
|
|
2796
2877
|
}
|
|
2797
2878
|
}
|
|
2798
2879
|
}
|
|
2880
|
+
savePKCE(verifier) {
|
|
2881
|
+
if (typeof window !== "undefined") {
|
|
2882
|
+
try {
|
|
2883
|
+
localStorage.setItem(this.pkceStorageKey, verifier);
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
console.warn("[NIID SDK] Failed to save PKCE verifier:", error);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
getPKCE() {
|
|
2890
|
+
if (typeof window !== "undefined") {
|
|
2891
|
+
try {
|
|
2892
|
+
return localStorage.getItem(this.pkceStorageKey);
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
return null;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
return null;
|
|
2898
|
+
}
|
|
2899
|
+
clearPKCE() {
|
|
2900
|
+
if (typeof window !== "undefined") {
|
|
2901
|
+
try {
|
|
2902
|
+
localStorage.removeItem(this.pkceStorageKey);
|
|
2903
|
+
} catch (error) {
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2799
2907
|
}
|
|
2800
2908
|
class NIIDClient {
|
|
2801
2909
|
constructor(config) {
|