@gh-platform/auth-sdk 1.0.1 → 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/README.md CHANGED
@@ -22,7 +22,7 @@ Mọi API của GH Platform Authenticate đều sử dụng dạng:
22
22
  ```
23
23
  /api/v1/{tenant}/auth/login
24
24
  /api/v1/{tenant}/auth/refresh
25
- /api/v1/{tenant}/auth/introspect
25
+ {service_url}/introspect
26
26
  ```
27
27
 
28
28
  JavaScript SDK tự động truyền tenant trong mọi request.
@@ -1,3 +1,10 @@
1
+ let _refreshPromise = null;
2
+ function getRefreshPromise() {
3
+ return _refreshPromise;
4
+ }
5
+ function setRefreshPromise(promise) {
6
+ _refreshPromise = promise;
7
+ }
1
8
  class AuthClient {
2
9
  /**
3
10
  * options:
@@ -8,20 +15,22 @@ class AuthClient {
8
15
  */
9
16
  constructor({
10
17
  baseUrl,
18
+ introspectPath,
11
19
  tenant = null,
12
20
  loginPath = null,
13
21
  refreshPath = null,
14
- introspectPath = null,
15
22
  headers = {},
16
23
  storage = null
17
- // <-- thêm storage vào constructor
18
24
  }) {
19
25
  if (!baseUrl) throw new Error("baseUrl is required");
20
26
  this.baseUrl = baseUrl.replace(/\/$/, "");
27
+ if (!introspectUrl) {
28
+ throw new Error("introspectUrl is required");
29
+ }
21
30
  const prefix = tenant ? `/api/v1/${tenant}/auth` : `/api/v1/auth`;
22
31
  this.loginUrl = this.baseUrl + (loginPath || `${prefix}/login`);
23
32
  this.refreshUrl = this.baseUrl + (refreshPath || `${prefix}/refresh`);
24
- this.introspectUrl = this.baseUrl + (introspectPath || `${prefix}/me`);
33
+ this.introspectUrl = introspectPath;
25
34
  this.tenant = tenant;
26
35
  this.headers = { "Content-Type": "application/json", ...headers };
27
36
  this.storage = storage;
@@ -61,28 +70,39 @@ class AuthClient {
61
70
  return json;
62
71
  }
63
72
  async refresh(refreshToken) {
64
- const res = await fetch(this.refreshUrl, {
65
- method: "POST",
66
- headers: this.headers,
67
- body: JSON.stringify({ refresh_token: refreshToken })
68
- });
69
- if (!res.ok) {
70
- const text = await res.text().catch(() => res.statusText);
71
- throw new Error(`Refresh failed: ${res.status} ${text}`);
73
+ if (getRefreshPromise()) {
74
+ return getRefreshPromise();
72
75
  }
73
- let json;
76
+ const promise = (async () => {
77
+ const res = await fetch(this.refreshUrl, {
78
+ method: "POST",
79
+ headers: this.headers,
80
+ body: JSON.stringify({ refresh_token: refreshToken })
81
+ });
82
+ if (!res.ok) {
83
+ const text = await res.text().catch(() => res.statusText);
84
+ throw new Error(`Refresh failed: ${res.status} ${text}`);
85
+ }
86
+ let json;
87
+ try {
88
+ json = await res.json();
89
+ } catch (e) {
90
+ console.error("❌ JSON parse error:", e);
91
+ throw new Error("Invalid JSON response from server");
92
+ }
93
+ const data = json.data || json;
94
+ if (this.storage) {
95
+ if (data.access_token) this.storage.accessToken = data.access_token;
96
+ if (data.refresh_token) this.storage.refreshToken = data.refresh_token;
97
+ }
98
+ return json;
99
+ })();
100
+ setRefreshPromise(promise);
74
101
  try {
75
- json = await res.json();
76
- } catch (e) {
77
- console.error("❌ JSON parse error:", e);
78
- throw new Error("Invalid JSON response from server");
79
- }
80
- const data = json.data || json;
81
- if (this.storage) {
82
- if (data.access_token) this.storage.accessToken = data.access_token;
83
- if (data.refresh_token) this.storage.refreshToken = data.refresh_token;
102
+ return await promise;
103
+ } finally {
104
+ setRefreshPromise(null);
84
105
  }
85
- return json;
86
106
  }
87
107
  async introspect(token = null) {
88
108
  let finalToken = token;
@@ -92,6 +112,9 @@ class AuthClient {
92
112
  if (!finalToken) {
93
113
  throw new Error("No access token available for introspection");
94
114
  }
115
+ if (!this.introspectUrl) {
116
+ throw new Error("No introspect url config");
117
+ }
95
118
  const headers = new Headers(this.headers);
96
119
  headers.set("Authorization", `Bearer ${finalToken}`);
97
120
  const res = await fetch(this.introspectUrl, {
@@ -1 +1 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).AuthSDK={})}(this,function(e){"use strict";class t{constructor({baseUrl:e,tenant:t=null,loginPath:s=null,refreshPath:r=null,introspectPath:o=null,headers:a={},storage:n=null}){if(!e)throw new Error("baseUrl is required");this.baseUrl=e.replace(/\/$/,"");const h=t?`/api/v1/${t}/auth`:"/api/v1/auth";this.loginUrl=this.baseUrl+(s||`${h}/login`),this.refreshUrl=this.baseUrl+(r||`${h}/refresh`),this.introspectUrl=this.baseUrl+(o||`${h}/me`),this.tenant=t,this.headers={"Content-Type":"application/json",...a},this.storage=n}async login(e,t,s=null,r={}){const o={identifier:e,password:t,...r};s&&(o.totp=s);const a=await fetch(this.loginUrl,{method:"POST",headers:this.headers,body:JSON.stringify(o)});if(!a.ok){const e=await a.text().catch(()=>a.statusText);throw new Error(`Login failed: ${a.status} ${e}`)}let n;try{n=await a.json()}catch(e){throw console.error("❌ JSON parse error:",e),new Error("Invalid JSON response from server")}const h=n.data||n;return this.storage&&(h.access_token&&(this.storage.accessToken=h.access_token),h.refresh_token&&(this.storage.refreshToken=h.refresh_token)),n}async refresh(e){const t=await fetch(this.refreshUrl,{method:"POST",headers:this.headers,body:JSON.stringify({refresh_token:e})});if(!t.ok){const e=await t.text().catch(()=>t.statusText);throw new Error(`Refresh failed: ${t.status} ${e}`)}let s;try{s=await t.json()}catch(e){throw console.error("❌ JSON parse error:",e),new Error("Invalid JSON response from server")}const r=s.data||s;return this.storage&&(r.access_token&&(this.storage.accessToken=r.access_token),r.refresh_token&&(this.storage.refreshToken=r.refresh_token)),s}async introspect(e=null){let t=e;if(this.storage&&!t&&(t=this.storage.accessToken),!t)throw new Error("No access token available for introspection");const s=new Headers(this.headers);s.set("Authorization",`Bearer ${t}`);const r=await fetch(this.introspectUrl,{method:"GET",headers:s});if(!r.ok){const e=await r.text().catch(()=>r.statusText);throw new Error(`Introspect failed: ${r.status} ${e}`)}let o;try{o=await r.json()}catch(e){throw console.error("❌ JSON parse error:",e),new Error("Invalid JSON response from server")}return o}}class s{constructor(e="auth",t=null){this.prefix=e,this.tenant=t}_key(e){return this.tenant?`${this.prefix}:${this.tenant}_${e}`:`${this.prefix}_${e}`}get accessToken(){return localStorage.getItem(this._key("access_token"))}set accessToken(e){null==e?localStorage.removeItem(this._key("access_token")):localStorage.setItem(this._key("access_token"),e)}get refreshToken(){return localStorage.getItem(this._key("refresh_token"))}set refreshToken(e){null==e?localStorage.removeItem(this._key("refresh_token")):localStorage.setItem(this._key("refresh_token"),e)}clear(){localStorage.removeItem(this._key("access_token")),localStorage.removeItem(this._key("refresh_token"))}}class r{constructor(e,t=null){this.client=e,this.storage=t||new s("auth",e.tenant||null)}async fetch(e,t={},s=null){const r=this.storage.accessToken,o=new Headers(t.headers||{});return r&&o.set("Authorization",`Bearer ${r}`),t.method&&"GET"!==t.method&&t.body?await this._xhrRequest(e,t,o,s):await this._fetchWithDownloadProgress(e,t,o,s)}async _fetchWithDownloadProgress(e,t,s,r){let o=await fetch(e,{...t,headers:s});if(401===o.status&&this.storage.refreshToken)try{const r=await this.client.refresh(this.storage.refreshToken);r.access_token&&(this.storage.accessToken=r.access_token),r.refresh_token&&(this.storage.refreshToken=r.refresh_token),s.set("Authorization",`Bearer ${this.storage.accessToken}`),o=await fetch(e,{...t,headers:s})}catch{throw this.storage.clear(),new Error("Unauthorized, please login again")}if(!r||!o.body)return o;const a=o.body.getReader(),n=+o.headers.get("Content-Length")||0;let h=0;const i=[];for(;;){const{done:e,value:t}=await a.read();if(e)break;i.push(t),h+=t.length,n?r(Math.round(h/n*100),h,n):r(null,h,null)}const c=new Blob(i);return new Response(c,o)}_xhrRequest(e,t,s,r){return new Promise((o,a)=>{const n=new XMLHttpRequest;n.open(t.method||"POST",e,!0);for(const[e,t]of s.entries())n.setRequestHeader(e,t);n.upload&&r&&(n.upload.onprogress=e=>{if(e.lengthComputable){const t=Math.round(e.loaded/e.total*100);r(t,e.loaded,e.total)}else r(null,e.loaded,null)}),n.onload=()=>{o(new Response(n.response,{status:n.status}))},n.onerror=()=>a(new Error("Network error")),n.send(t.body)})}}const o={AuthClient:t,AuthFetch:r,TokenStorage:s};e.AuthClient=t,e.AuthFetch=r,e.TokenStorage=s,e.default=o,Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).AuthSDK={})}(this,function(e){"use strict";let t=null;function r(){return t}function s(e){t=e}class o{constructor({baseUrl:e,introspectPath:t,tenant:r=null,loginPath:s=null,refreshPath:o=null,headers:n={},storage:a=null}){if(!e)throw new Error("baseUrl is required");if(this.baseUrl=e.replace(/\/$/,""),!introspectUrl)throw new Error("introspectUrl is required");const i=r?`/api/v1/${r}/auth`:"/api/v1/auth";this.loginUrl=this.baseUrl+(s||`${i}/login`),this.refreshUrl=this.baseUrl+(o||`${i}/refresh`),this.introspectUrl=t,this.tenant=r,this.headers={"Content-Type":"application/json",...n},this.storage=a}async login(e,t,r=null,s={}){const o={identifier:e,password:t,...s};r&&(o.totp=r);const n=await fetch(this.loginUrl,{method:"POST",headers:this.headers,body:JSON.stringify(o)});if(!n.ok){const e=await n.text().catch(()=>n.statusText);throw new Error(`Login failed: ${n.status} ${e}`)}let a;try{a=await n.json()}catch(e){throw console.error("❌ JSON parse error:",e),new Error("Invalid JSON response from server")}const i=a.data||a;return this.storage&&(i.access_token&&(this.storage.accessToken=i.access_token),i.refresh_token&&(this.storage.refreshToken=i.refresh_token)),a}async refresh(e){if(r())return r();const t=(async()=>{const t=await fetch(this.refreshUrl,{method:"POST",headers:this.headers,body:JSON.stringify({refresh_token:e})});if(!t.ok){const e=await t.text().catch(()=>t.statusText);throw new Error(`Refresh failed: ${t.status} ${e}`)}let r;try{r=await t.json()}catch(e){throw console.error("❌ JSON parse error:",e),new Error("Invalid JSON response from server")}const s=r.data||r;return this.storage&&(s.access_token&&(this.storage.accessToken=s.access_token),s.refresh_token&&(this.storage.refreshToken=s.refresh_token)),r})();s(t);try{return await t}finally{s(null)}}async introspect(e=null){let t=e;if(this.storage&&!t&&(t=this.storage.accessToken),!t)throw new Error("No access token available for introspection");if(!this.introspectUrl)throw new Error("No introspect url config");const r=new Headers(this.headers);r.set("Authorization",`Bearer ${t}`);const s=await fetch(this.introspectUrl,{method:"GET",headers:r});if(!s.ok){const e=await s.text().catch(()=>s.statusText);throw new Error(`Introspect failed: ${s.status} ${e}`)}let o;try{o=await s.json()}catch(e){throw console.error("❌ JSON parse error:",e),new Error("Invalid JSON response from server")}return o}}class n{constructor(e="auth",t=null){this.prefix=e,this.tenant=t}_key(e){return this.tenant?`${this.prefix}:${this.tenant}_${e}`:`${this.prefix}_${e}`}get accessToken(){return localStorage.getItem(this._key("access_token"))}set accessToken(e){null==e?localStorage.removeItem(this._key("access_token")):localStorage.setItem(this._key("access_token"),e)}get refreshToken(){return localStorage.getItem(this._key("refresh_token"))}set refreshToken(e){null==e?localStorage.removeItem(this._key("refresh_token")):localStorage.setItem(this._key("refresh_token"),e)}clear(){localStorage.removeItem(this._key("access_token")),localStorage.removeItem(this._key("refresh_token"))}}class a{constructor(e,t=null){this.client=e,this.storage=t||new n("auth",e.tenant||null)}async fetch(e,t={},r=null){const s=this.storage.accessToken,o=new Headers(t.headers||{});return s&&o.set("Authorization",`Bearer ${s}`),t.method&&"GET"!==t.method&&t.body?await this._xhrRequest(e,t,o,r):await this._fetchWithDownloadProgress(e,t,o,r)}async _fetchWithDownloadProgress(e,t,r,s){let o=await fetch(e,{...t,headers:r});if(401===o.status&&this.storage.refreshToken)try{const s=await this.client.refresh(this.storage.refreshToken);s.access_token&&(this.storage.accessToken=s.access_token),s.refresh_token&&(this.storage.refreshToken=s.refresh_token),r.set("Authorization",`Bearer ${this.storage.accessToken}`),o=await fetch(e,{...t,headers:r})}catch{throw this.storage.clear(),new Error("Unauthorized, please login again")}if(!s||!o.body)return o;const n=o.body.getReader(),a=+o.headers.get("Content-Length")||0;let i=0;const h=[];for(;;){const{done:e,value:t}=await n.read();if(e)break;h.push(t),i+=t.length,a?s(Math.round(i/a*100),i,a):s(null,i,null)}const c=new Blob(h);return new Response(c,o)}_xhrRequest(e,t,r,s){return new Promise((o,n)=>{const a=new XMLHttpRequest;a.open(t.method||"POST",e,!0);for(const[e,t]of r.entries())a.setRequestHeader(e,t);a.upload&&s&&(a.upload.onprogress=e=>{if(e.lengthComputable){const t=Math.round(e.loaded/e.total*100);s(t,e.loaded,e.total)}else s(null,e.loaded,null)}),a.onload=()=>{o(new Response(a.response,{status:a.status}))},a.onerror=()=>n(new Error("Network error")),a.send(t.body)})}}const i={AuthClient:o,AuthFetch:a,TokenStorage:n};e.AuthClient=o,e.AuthFetch=a,e.TokenStorage=n,e.default=i,Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
@@ -1 +1 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).AuthSDK={})}(this,function(e){"use strict";class t{constructor({baseUrl:e,tenant:t=null,loginPath:s=null,refreshPath:r=null,introspectPath:o=null,headers:a={},storage:n=null}){if(!e)throw new Error("baseUrl is required");this.baseUrl=e.replace(/\/$/,"");const h=t?`/api/v1/${t}/auth`:"/api/v1/auth";this.loginUrl=this.baseUrl+(s||`${h}/login`),this.refreshUrl=this.baseUrl+(r||`${h}/refresh`),this.introspectUrl=this.baseUrl+(o||`${h}/me`),this.tenant=t,this.headers={"Content-Type":"application/json",...a},this.storage=n}async login(e,t,s=null,r={}){const o={identifier:e,password:t,...r};s&&(o.totp=s);const a=await fetch(this.loginUrl,{method:"POST",headers:this.headers,body:JSON.stringify(o)});if(!a.ok){const e=await a.text().catch(()=>a.statusText);throw new Error(`Login failed: ${a.status} ${e}`)}let n;try{n=await a.json()}catch(i){throw console.error("❌ JSON parse error:",i),new Error("Invalid JSON response from server")}const h=n.data||n;return this.storage&&(h.access_token&&(this.storage.accessToken=h.access_token),h.refresh_token&&(this.storage.refreshToken=h.refresh_token)),n}async refresh(e){const t=await fetch(this.refreshUrl,{method:"POST",headers:this.headers,body:JSON.stringify({refresh_token:e})});if(!t.ok){const e=await t.text().catch(()=>t.statusText);throw new Error(`Refresh failed: ${t.status} ${e}`)}let s;try{s=await t.json()}catch(o){throw console.error("❌ JSON parse error:",o),new Error("Invalid JSON response from server")}const r=s.data||s;return this.storage&&(r.access_token&&(this.storage.accessToken=r.access_token),r.refresh_token&&(this.storage.refreshToken=r.refresh_token)),s}async introspect(e=null){let t=e;if(this.storage&&!t&&(t=this.storage.accessToken),!t)throw new Error("No access token available for introspection");const s=new Headers(this.headers);s.set("Authorization",`Bearer ${t}`);const r=await fetch(this.introspectUrl,{method:"GET",headers:s});if(!r.ok){const e=await r.text().catch(()=>r.statusText);throw new Error(`Introspect failed: ${r.status} ${e}`)}let o;try{o=await r.json()}catch(a){throw console.error("❌ JSON parse error:",a),new Error("Invalid JSON response from server")}return o}}class s{constructor(e="auth",t=null){this.prefix=e,this.tenant=t}_key(e){return this.tenant?`${this.prefix}:${this.tenant}_${e}`:`${this.prefix}_${e}`}get accessToken(){return localStorage.getItem(this._key("access_token"))}set accessToken(e){null==e?localStorage.removeItem(this._key("access_token")):localStorage.setItem(this._key("access_token"),e)}get refreshToken(){return localStorage.getItem(this._key("refresh_token"))}set refreshToken(e){null==e?localStorage.removeItem(this._key("refresh_token")):localStorage.setItem(this._key("refresh_token"),e)}clear(){localStorage.removeItem(this._key("access_token")),localStorage.removeItem(this._key("refresh_token"))}}class r{constructor(e,t=null){this.client=e,this.storage=t||new s("auth",e.tenant||null)}async fetch(e,t={},s=null){const r=this.storage.accessToken,o=new Headers(t.headers||{});return r&&o.set("Authorization",`Bearer ${r}`),t.method&&"GET"!==t.method&&t.body?await this._xhrRequest(e,t,o,s):await this._fetchWithDownloadProgress(e,t,o,s)}async _fetchWithDownloadProgress(e,t,s,r){let o=await fetch(e,{...t,headers:s});if(401===o.status&&this.storage.refreshToken)try{const r=await this.client.refresh(this.storage.refreshToken);r.access_token&&(this.storage.accessToken=r.access_token),r.refresh_token&&(this.storage.refreshToken=r.refresh_token),s.set("Authorization",`Bearer ${this.storage.accessToken}`),o=await fetch(e,{...t,headers:s})}catch{throw this.storage.clear(),new Error("Unauthorized, please login again")}if(!r||!o.body)return o;const a=o.body.getReader(),n=+o.headers.get("Content-Length")||0;let h=0;const i=[];for(;;){const{done:e,value:t}=await a.read();if(e)break;if(i.push(t),h+=t.length,n){r(Math.round(h/n*100),h,n)}else r(null,h,null)}const c=new Blob(i);return new Response(c,o)}_xhrRequest(e,t,s,r){return new Promise((o,a)=>{const n=new XMLHttpRequest;n.open(t.method||"POST",e,!0);for(const[e,t]of s.entries())n.setRequestHeader(e,t);n.upload&&r&&(n.upload.onprogress=e=>{if(e.lengthComputable){const t=Math.round(e.loaded/e.total*100);r(t,e.loaded,e.total)}else r(null,e.loaded,null)}),n.onload=()=>{o(new Response(n.response,{status:n.status}))},n.onerror=()=>a(new Error("Network error")),n.send(t.body)})}}const o={AuthClient:t,AuthFetch:r,TokenStorage:s};e.AuthClient=t,e.AuthFetch=r,e.TokenStorage=s,e.default=o,Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).AuthSDK={})}(this,function(e){"use strict";let t=null;function r(){return t}function s(e){t=e}class o{constructor({baseUrl:e,introspectPath:t,tenant:r=null,loginPath:s=null,refreshPath:o=null,headers:n={},storage:a=null}){if(!e)throw new Error("baseUrl is required");if(this.baseUrl=e.replace(/\/$/,""),!introspectUrl)throw new Error("introspectUrl is required");const i=r?`/api/v1/${r}/auth`:"/api/v1/auth";this.loginUrl=this.baseUrl+(s||`${i}/login`),this.refreshUrl=this.baseUrl+(o||`${i}/refresh`),this.introspectUrl=t,this.tenant=r,this.headers={"Content-Type":"application/json",...n},this.storage=a}async login(e,t,r=null,s={}){const o={identifier:e,password:t,...s};r&&(o.totp=r);const n=await fetch(this.loginUrl,{method:"POST",headers:this.headers,body:JSON.stringify(o)});if(!n.ok){const e=await n.text().catch(()=>n.statusText);throw new Error(`Login failed: ${n.status} ${e}`)}let a;try{a=await n.json()}catch(h){throw console.error("❌ JSON parse error:",h),new Error("Invalid JSON response from server")}const i=a.data||a;return this.storage&&(i.access_token&&(this.storage.accessToken=i.access_token),i.refresh_token&&(this.storage.refreshToken=i.refresh_token)),a}async refresh(e){if(r())return r();const t=(async()=>{const t=await fetch(this.refreshUrl,{method:"POST",headers:this.headers,body:JSON.stringify({refresh_token:e})});if(!t.ok){const e=await t.text().catch(()=>t.statusText);throw new Error(`Refresh failed: ${t.status} ${e}`)}let r;try{r=await t.json()}catch(o){throw console.error("❌ JSON parse error:",o),new Error("Invalid JSON response from server")}const s=r.data||r;return this.storage&&(s.access_token&&(this.storage.accessToken=s.access_token),s.refresh_token&&(this.storage.refreshToken=s.refresh_token)),r})();s(t);try{return await t}finally{s(null)}}async introspect(e=null){let t=e;if(this.storage&&!t&&(t=this.storage.accessToken),!t)throw new Error("No access token available for introspection");if(!this.introspectUrl)throw new Error("No introspect url config");const r=new Headers(this.headers);r.set("Authorization",`Bearer ${t}`);const s=await fetch(this.introspectUrl,{method:"GET",headers:r});if(!s.ok){const e=await s.text().catch(()=>s.statusText);throw new Error(`Introspect failed: ${s.status} ${e}`)}let o;try{o=await s.json()}catch(n){throw console.error("❌ JSON parse error:",n),new Error("Invalid JSON response from server")}return o}}class n{constructor(e="auth",t=null){this.prefix=e,this.tenant=t}_key(e){return this.tenant?`${this.prefix}:${this.tenant}_${e}`:`${this.prefix}_${e}`}get accessToken(){return localStorage.getItem(this._key("access_token"))}set accessToken(e){null==e?localStorage.removeItem(this._key("access_token")):localStorage.setItem(this._key("access_token"),e)}get refreshToken(){return localStorage.getItem(this._key("refresh_token"))}set refreshToken(e){null==e?localStorage.removeItem(this._key("refresh_token")):localStorage.setItem(this._key("refresh_token"),e)}clear(){localStorage.removeItem(this._key("access_token")),localStorage.removeItem(this._key("refresh_token"))}}class a{constructor(e,t=null){this.client=e,this.storage=t||new n("auth",e.tenant||null)}async fetch(e,t={},r=null){const s=this.storage.accessToken,o=new Headers(t.headers||{});return s&&o.set("Authorization",`Bearer ${s}`),t.method&&"GET"!==t.method&&t.body?await this._xhrRequest(e,t,o,r):await this._fetchWithDownloadProgress(e,t,o,r)}async _fetchWithDownloadProgress(e,t,r,s){let o=await fetch(e,{...t,headers:r});if(401===o.status&&this.storage.refreshToken)try{const s=await this.client.refresh(this.storage.refreshToken);s.access_token&&(this.storage.accessToken=s.access_token),s.refresh_token&&(this.storage.refreshToken=s.refresh_token),r.set("Authorization",`Bearer ${this.storage.accessToken}`),o=await fetch(e,{...t,headers:r})}catch{throw this.storage.clear(),new Error("Unauthorized, please login again")}if(!s||!o.body)return o;const n=o.body.getReader(),a=+o.headers.get("Content-Length")||0;let i=0;const h=[];for(;;){const{done:e,value:t}=await n.read();if(e)break;if(h.push(t),i+=t.length,a){s(Math.round(i/a*100),i,a)}else s(null,i,null)}const c=new Blob(h);return new Response(c,o)}_xhrRequest(e,t,r,s){return new Promise((o,n)=>{const a=new XMLHttpRequest;a.open(t.method||"POST",e,!0);for(const[e,t]of r.entries())a.setRequestHeader(e,t);a.upload&&s&&(a.upload.onprogress=e=>{if(e.lengthComputable){const t=Math.round(e.loaded/e.total*100);s(t,e.loaded,e.total)}else s(null,e.loaded,null)}),a.onload=()=>{o(new Response(a.response,{status:a.status}))},a.onerror=()=>n(new Error("Network error")),a.send(t.body)})}}const i={AuthClient:o,AuthFetch:a,TokenStorage:n};e.AuthClient=o,e.AuthFetch=a,e.TokenStorage=n,e.default=i,Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export interface AuthClientOptions {
2
2
  baseUrl: string;
3
+ introspectPath: string;
3
4
  tenant?: string | null;
4
5
  loginPath?: string;
5
6
  refreshPath?: string;
6
- introspectPath?: string;
7
7
  headers?: Record<string, string>;
8
8
  storage?: TokenStorage | null;
9
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gh-platform/auth-sdk",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "VanillaJS Auth SDK for GH Platform",
5
5
  "type": "module",
6
6
  "main": "dist/auth-sdk.umd.js",
package/src/client.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // src/client.js
2
+ import { getRefreshPromise, setRefreshPromise } from "./refresh-lock.js";
3
+
2
4
  export default class AuthClient {
3
5
  /**
4
6
  * options:
@@ -7,29 +9,30 @@ export default class AuthClient {
7
9
  * - loginPath / refreshPath / introspectPath (optional overrides)
8
10
  * - headers (optional)
9
11
  */
10
- _refreshPromise = null;
11
12
  constructor({
12
13
  baseUrl,
14
+ introspectPath,
13
15
  tenant = null,
14
16
  loginPath = null,
15
17
  refreshPath = null,
16
- introspectPath = null,
17
18
  headers = {},
18
- storage = null, // <-- thêm storage vào constructor
19
+ storage = null,
19
20
  }) {
20
21
  if (!baseUrl) throw new Error("baseUrl is required");
21
22
  this.baseUrl = baseUrl.replace(/\/$/, "");
22
-
23
+ if (!introspectUrl) {
24
+ throw new Error("introspectUrl is required");
25
+ }
23
26
  // default path builder: tenant-aware
24
27
  const prefix = tenant ? `/api/v1/${tenant}/auth` : `/api/v1/auth`;
25
28
 
26
29
  this.loginUrl = this.baseUrl + (loginPath || `${prefix}/login`);
27
30
  this.refreshUrl = this.baseUrl + (refreshPath || `${prefix}/refresh`);
28
- this.introspectUrl = this.baseUrl + (introspectPath || `${prefix}/me`);
31
+ this.introspectUrl = introspectPath;
29
32
 
30
- this.tenant = tenant; // keep for storage prefix
33
+ this.tenant = tenant;
31
34
  this.headers = { "Content-Type": "application/json", ...headers };
32
- this.storage = storage; // <-- store storage instance
35
+ this.storage = storage;
33
36
  }
34
37
 
35
38
  /**
@@ -75,10 +78,10 @@ export default class AuthClient {
75
78
  }
76
79
 
77
80
  async refresh(refreshToken) {
78
- if (this._refreshPromise) {
79
- return this._refreshPromise;
81
+ if (getRefreshPromise()) {
82
+ return getRefreshPromise();
80
83
  }
81
- this._refreshPromise = (async () => {
84
+ const promise = (async () => {
82
85
  const res = await fetch(this.refreshUrl, {
83
86
  method: "POST",
84
87
  headers: this.headers,
@@ -107,11 +110,12 @@ export default class AuthClient {
107
110
 
108
111
  return json;
109
112
  })();
113
+ setRefreshPromise(promise);
110
114
  try {
111
- return await this._refreshPromise;
115
+ return await promise;
112
116
  } finally {
113
117
  // 🔓 Luôn reset lock (kể cả khi lỗi)
114
- this._refreshPromise = null;
118
+ setRefreshPromise(null);
115
119
  }
116
120
  }
117
121
 
@@ -128,7 +132,9 @@ export default class AuthClient {
128
132
  if (!finalToken) {
129
133
  throw new Error("No access token available for introspection");
130
134
  }
131
-
135
+ if (!this.introspectUrl) {
136
+ throw new Error("No introspect url config");
137
+ }
132
138
  const headers = new Headers(this.headers);
133
139
  headers.set("Authorization", `Bearer ${finalToken}`);
134
140
  const res = await fetch(this.introspectUrl, {
package/src/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export interface AuthClientOptions {
2
2
  baseUrl: string;
3
+ introspectPath: string;
3
4
  tenant?: string | null;
4
5
  loginPath?: string;
5
6
  refreshPath?: string;
6
- introspectPath?: string;
7
7
  headers?: Record<string, string>;
8
8
  storage?: TokenStorage | null;
9
9
  }
@@ -0,0 +1,9 @@
1
+ let _refreshPromise = null;
2
+
3
+ export function getRefreshPromise() {
4
+ return _refreshPromise;
5
+ }
6
+
7
+ export function setRefreshPromise(promise) {
8
+ _refreshPromise = promise;
9
+ }