@gh-platform/auth-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/.gitlab-ci.yml ADDED
@@ -0,0 +1,22 @@
1
+ stages:
2
+ - build
3
+ - publish
4
+
5
+ build_package:
6
+ stage: build
7
+ image: node:20
8
+ script:
9
+ - npm ci --include=dev
10
+ - npm run build
11
+ artifacts:
12
+ paths:
13
+ - dist/
14
+
15
+ publish_to_registry:
16
+ stage: publish
17
+ image: node:20
18
+ script:
19
+ - echo "//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${NPM_TOKEN}" > .npmrc
20
+ - npm publish
21
+ only:
22
+ - tags
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # 🛡️ GH Platform – JavaScript Auth SDK (Multi‑Tenant Version)
2
+
3
+ SDK JavaScript hỗ trợ đăng nhập (**login**), làm mới token (**refresh**), kiểm tra token (**introspect**),
4
+ và tự động xác thực request HTTP trong kiến trúc **multi‑tenant** của GH Platform.
5
+
6
+ ---
7
+
8
+ ## 🚀 Tính năng chính
9
+
10
+ - ✅ Hỗ trợ **login**, **refresh**, **introspect**
11
+ - ✅ **Tenant‑aware client**: mọi API tự động gắn `{tenant}` vào URL
12
+ - ✅ Middleware **AuthFetch** tự động gắn Bearer token + auto refresh
13
+ - ✅ Hoạt động cả **browser** & **Node.js**
14
+ - ✅ Hỗ trợ nhúng trực tiếp (`auth-sdk.min.js`) hoặc cài qua **npm**
15
+
16
+ ---
17
+
18
+ ## 🧱 Kiến trúc Multi‑Tenant
19
+
20
+ Mọi API của GH Platform Authenticate đều sử dụng dạng:
21
+
22
+ ```
23
+ /api/v1/{tenant}/auth/login
24
+ /api/v1/{tenant}/auth/refresh
25
+ /api/v1/{tenant}/auth/introspect
26
+ ```
27
+
28
+ JavaScript SDK tự động truyền tenant trong mọi request.
29
+
30
+ ### ✳️ Khởi tạo client theo tenant
31
+
32
+ ```js
33
+ const tenant = "demo"; // hoặc trích xuất từ email: alice@example.com → example.com
34
+
35
+ const client = new AuthClient({
36
+ baseUrl: "https://auth.example.com",
37
+ tenant
38
+ });
39
+
40
+ const storage = new TokenStorage("auth", tenant);
41
+ const fetcher = new AuthFetch(client, storage);
42
+ ```
43
+
44
+ ### 📌 TokenStorage cũng tách token theo tenant
45
+
46
+ ```js
47
+ // Lưu token theo từng tenant
48
+ storage.accessToken = res.access_token;
49
+ storage.refreshToken = res.refresh_token;
50
+
51
+ // Key lưu trong localStorage sẽ giống:
52
+ // auth.demo.access_token
53
+ // auth.demo.refresh_token
54
+ ```
55
+
56
+ ---
57
+
58
+ ## 📦 Cài đặt
59
+
60
+ ### 🔹 Cách 1 – Cài qua NPM
61
+
62
+ ```bash
63
+ npm install @gh-platform/auth-sdk
64
+ ```
65
+
66
+ Import:
67
+
68
+ ```js
69
+ import { AuthClient, AuthFetch, TokenStorage } from "@gh-platform/auth-sdk";
70
+ ```
71
+
72
+ ---
73
+
74
+ ### 🔹 Cách 2 – Dùng trực tiếp trên Web (HTML thuần)
75
+
76
+ ```html
77
+ <script src="https://cdn.yourdomain.com/auth-sdk.min.js"></script>
78
+ <script>
79
+ const { AuthClient, AuthFetch, TokenStorage } = window.AuthSDK;
80
+ </script>
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 🔐 Ví dụ Multi‑Tenant Login
86
+
87
+ ```js
88
+ const tenant = "example";
89
+ const client = new AuthClient({
90
+ baseUrl: "https://auth.example.com",
91
+ tenant
92
+ });
93
+
94
+ async function login() {
95
+ const res = await client.login(
96
+ "alice@example.com", // identifier
97
+ "User@123", // password
98
+ "gh-platform-admin" // client_id
99
+ );
100
+
101
+ storage.accessToken = res.access_token;
102
+ storage.refreshToken = res.refresh_token;
103
+
104
+ console.log("Login success!", res);
105
+ }
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 🌐 Fetch API với AuthFetch (auto refresh + retry)
111
+
112
+ ```js
113
+ const fetcher = new AuthFetch(client, storage);
114
+
115
+ const resp = await fetcher.fetch(
116
+ client.baseUrl + "/api/v1/" + tenant + "/auth/me"
117
+ );
118
+
119
+ const user = await resp.json();
120
+ console.log("User:", user);
121
+ ```
122
+
123
+ Tự động:
124
+
125
+ - Gắn `Authorization: Bearer <access_token>`
126
+ - Nếu token hết hạn → tự refresh → retry request
127
+ - Lưu token đúng tenant
128
+
129
+ ---
130
+
131
+ ## 🔧 Build hướng dẫn
132
+
133
+ ```bash
134
+ npm install vite terser -D
135
+ npm run build
136
+ npx terser dist/auth-sdk.umd.js -o dist/auth-sdk.min.js --compress --mangle
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 🌐 Demo HTML
142
+
143
+ ```html
144
+ <!DOCTYPE html>
145
+ <html>
146
+ <head><title>Auth SDK Demo</title></head>
147
+ <body>
148
+ <button id="login">Login</button>
149
+ <button id="getUser">Get Profile</button>
150
+
151
+ <script src="./dist/auth-sdk.min.js"></script>
152
+ <script>
153
+ const tenant = "demo";
154
+
155
+ const { AuthClient, TokenStorage, AuthFetch } = window.AuthSDK;
156
+ const client = new AuthClient({ baseUrl: "https://auth.example.com", tenant });
157
+ const storage = new TokenStorage("auth", tenant);
158
+ const fetcher = new AuthFetch(client, storage);
159
+
160
+ document.getElementById("login").onclick = async () => {
161
+ const res = await client.login("alice@example.com", "User@123", "gh-platform-admin");
162
+ storage.accessToken = res.access_token;
163
+ storage.refreshToken = res.refresh_token;
164
+ alert("Login success!");
165
+ };
166
+
167
+ document.getElementById("getUser").onclick = async () => {
168
+ const r = await fetcher.fetch(client.baseUrl + "/api/v1/" + tenant + "/auth/me");
169
+ console.log("User:", await r.json());
170
+ };
171
+ </script>
172
+ </body>
173
+ </html>
174
+ ```
175
+
176
+ ---
177
+
178
+ ## 🧾 License
179
+
180
+ Bản quyền © 2025 **GH Platform** – Phát hành theo giấy phép **MIT**
@@ -0,0 +1,239 @@
1
+ class AuthClient {
2
+ /**
3
+ * options:
4
+ * - baseUrl (required) e.g. "https://auth.example.com"
5
+ * - tenant (optional) e.g. "demo" -> will target /api/v1/{tenant}/auth/*
6
+ * - loginPath / refreshPath / introspectPath (optional overrides)
7
+ * - headers (optional)
8
+ */
9
+ constructor({
10
+ baseUrl,
11
+ tenant = null,
12
+ loginPath = null,
13
+ refreshPath = null,
14
+ introspectPath = null,
15
+ headers = {},
16
+ storage = null
17
+ // <-- thêm storage vào constructor
18
+ }) {
19
+ if (!baseUrl) throw new Error("baseUrl is required");
20
+ this.baseUrl = baseUrl.replace(/\/$/, "");
21
+ const prefix = tenant ? `/api/v1/${tenant}/auth` : `/api/v1/auth`;
22
+ this.loginUrl = this.baseUrl + (loginPath || `${prefix}/login`);
23
+ this.refreshUrl = this.baseUrl + (refreshPath || `${prefix}/refresh`);
24
+ this.introspectUrl = this.baseUrl + (introspectPath || `${prefix}/me`);
25
+ this.tenant = tenant;
26
+ this.headers = { "Content-Type": "application/json", ...headers };
27
+ this.storage = storage;
28
+ }
29
+ /**
30
+ * Login payload uses identifier + password (+ optional totp)
31
+ * extra is optional object for backward-compat or extra fields
32
+ */
33
+ async login(identifier, password, totp = null, extra = {}) {
34
+ const body = {
35
+ identifier,
36
+ password,
37
+ ...extra
38
+ };
39
+ if (totp) body.totp = totp;
40
+ const res = await fetch(this.loginUrl, {
41
+ method: "POST",
42
+ headers: this.headers,
43
+ body: JSON.stringify(body)
44
+ });
45
+ if (!res.ok) {
46
+ const text = await res.text().catch(() => res.statusText);
47
+ throw new Error(`Login failed: ${res.status} ${text}`);
48
+ }
49
+ let json;
50
+ try {
51
+ json = await res.json();
52
+ } catch (e) {
53
+ console.error("❌ JSON parse error:", e);
54
+ throw new Error("Invalid JSON response from server");
55
+ }
56
+ const data = json.data || json;
57
+ if (this.storage) {
58
+ if (data.access_token) this.storage.accessToken = data.access_token;
59
+ if (data.refresh_token) this.storage.refreshToken = data.refresh_token;
60
+ }
61
+ return json;
62
+ }
63
+ 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}`);
72
+ }
73
+ let json;
74
+ 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;
84
+ }
85
+ return json;
86
+ }
87
+ async introspect(token = null) {
88
+ let finalToken = token;
89
+ if (this.storage && !finalToken) {
90
+ finalToken = this.storage.accessToken;
91
+ }
92
+ if (!finalToken) {
93
+ throw new Error("No access token available for introspection");
94
+ }
95
+ const headers = new Headers(this.headers);
96
+ headers.set("Authorization", `Bearer ${finalToken}`);
97
+ const res = await fetch(this.introspectUrl, {
98
+ method: "GET",
99
+ headers
100
+ });
101
+ if (!res.ok) {
102
+ const text = await res.text().catch(() => res.statusText);
103
+ throw new Error(`Introspect failed: ${res.status} ${text}`);
104
+ }
105
+ let json;
106
+ try {
107
+ json = await res.json();
108
+ } catch (e) {
109
+ console.error("❌ JSON parse error:", e);
110
+ throw new Error("Invalid JSON response from server");
111
+ }
112
+ return json;
113
+ }
114
+ }
115
+ class TokenStorage {
116
+ /**
117
+ * prefix: base prefix (default "auth")
118
+ * tenant: optional tenant string -> final key = `${prefix}:${tenant}_access_token`
119
+ */
120
+ constructor(prefix = "auth", tenant = null) {
121
+ this.prefix = prefix;
122
+ this.tenant = tenant;
123
+ }
124
+ _key(name) {
125
+ return this.tenant ? `${this.prefix}:${this.tenant}_${name}` : `${this.prefix}_${name}`;
126
+ }
127
+ get accessToken() {
128
+ return localStorage.getItem(this._key("access_token"));
129
+ }
130
+ set accessToken(val) {
131
+ if (val === null || val === void 0) {
132
+ localStorage.removeItem(this._key("access_token"));
133
+ } else {
134
+ localStorage.setItem(this._key("access_token"), val);
135
+ }
136
+ }
137
+ get refreshToken() {
138
+ return localStorage.getItem(this._key("refresh_token"));
139
+ }
140
+ set refreshToken(val) {
141
+ if (val === null || val === void 0) {
142
+ localStorage.removeItem(this._key("refresh_token"));
143
+ } else {
144
+ localStorage.setItem(this._key("refresh_token"), val);
145
+ }
146
+ }
147
+ clear() {
148
+ localStorage.removeItem(this._key("access_token"));
149
+ localStorage.removeItem(this._key("refresh_token"));
150
+ }
151
+ }
152
+ class AuthFetch {
153
+ constructor(authClient, storage = null) {
154
+ this.client = authClient;
155
+ this.storage = storage || new TokenStorage("auth", authClient.tenant || null);
156
+ }
157
+ /**
158
+ * fetchWithProgress:
159
+ * - input: URL
160
+ * - options: fetch options
161
+ * - onProgress: callback(percent, loaded, total)
162
+ */
163
+ async fetch(input, options = {}, onProgress = null) {
164
+ const token = this.storage.accessToken;
165
+ const headers = new Headers(options.headers || {});
166
+ if (token) headers.set("Authorization", `Bearer ${token}`);
167
+ if (options.method && options.method !== "GET" && options.body) {
168
+ return await this._xhrRequest(input, options, headers, onProgress);
169
+ }
170
+ return await this._fetchWithDownloadProgress(input, options, headers, onProgress);
171
+ }
172
+ // ------------------ DOWNLOAD PROGRESS ------------------
173
+ async _fetchWithDownloadProgress(input, options, headers, onProgress) {
174
+ let response = await fetch(input, { ...options, headers });
175
+ if (response.status === 401 && this.storage.refreshToken) {
176
+ try {
177
+ const newTokens = await this.client.refresh(this.storage.refreshToken);
178
+ if (newTokens.access_token) this.storage.accessToken = newTokens.access_token;
179
+ if (newTokens.refresh_token) this.storage.refreshToken = newTokens.refresh_token;
180
+ headers.set("Authorization", `Bearer ${this.storage.accessToken}`);
181
+ response = await fetch(input, { ...options, headers });
182
+ } catch {
183
+ this.storage.clear();
184
+ throw new Error("Unauthorized, please login again");
185
+ }
186
+ }
187
+ if (!onProgress || !response.body) return response;
188
+ const reader = response.body.getReader();
189
+ const contentLength = +response.headers.get("Content-Length") || 0;
190
+ let loaded = 0;
191
+ const chunks = [];
192
+ while (true) {
193
+ const { done, value } = await reader.read();
194
+ if (done) break;
195
+ chunks.push(value);
196
+ loaded += value.length;
197
+ if (contentLength) {
198
+ const percent = Math.round(loaded / contentLength * 100);
199
+ onProgress(percent, loaded, contentLength);
200
+ } else {
201
+ onProgress(null, loaded, null);
202
+ }
203
+ }
204
+ const blob = new Blob(chunks);
205
+ return new Response(blob, response);
206
+ }
207
+ // ------------------ UPLOAD PROGRESS ------------------
208
+ _xhrRequest(url, options, headers, onProgress) {
209
+ return new Promise((resolve, reject) => {
210
+ const xhr = new XMLHttpRequest();
211
+ xhr.open(options.method || "POST", url, true);
212
+ for (const [k, v] of headers.entries()) {
213
+ xhr.setRequestHeader(k, v);
214
+ }
215
+ if (xhr.upload && onProgress) {
216
+ xhr.upload.onprogress = (e) => {
217
+ if (e.lengthComputable) {
218
+ const percent = Math.round(e.loaded / e.total * 100);
219
+ onProgress(percent, e.loaded, e.total);
220
+ } else {
221
+ onProgress(null, e.loaded, null);
222
+ }
223
+ };
224
+ }
225
+ xhr.onload = () => {
226
+ resolve(new Response(xhr.response, { status: xhr.status }));
227
+ };
228
+ xhr.onerror = () => reject(new Error("Network error"));
229
+ xhr.send(options.body);
230
+ });
231
+ }
232
+ }
233
+ const index = { AuthClient, AuthFetch, TokenStorage };
234
+ export {
235
+ AuthClient,
236
+ AuthFetch,
237
+ TokenStorage,
238
+ index as default
239
+ };
@@ -0,0 +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"}})});
@@ -0,0 +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"}})});
@@ -0,0 +1,93 @@
1
+ export interface AuthClientOptions {
2
+ baseUrl: string;
3
+ tenant?: string | null;
4
+ loginPath?: string;
5
+ refreshPath?: string;
6
+ introspectPath?: string;
7
+ headers?: Record<string, string>;
8
+ storage?: TokenStorage | null;
9
+ }
10
+
11
+ /** Callback báo tiến trình (upload hoặc download) */
12
+ export type ProgressCallback = (
13
+ percent: number | null, // % (nếu biết), null nếu không xác định total
14
+ loaded: number, // số bytes đã tải
15
+ total: number | null // tổng bytes (nếu biết)
16
+ ) => void;
17
+
18
+ export interface TokenData {
19
+ access_token: string;
20
+ refresh_token: string;
21
+ token_type: string;
22
+ expires_in: number;
23
+ refresh_expires_at: string;
24
+ message?: string;
25
+ [key: string]: any;
26
+ }
27
+
28
+ export interface AuthResponse {
29
+ status: string; // "success" | "error"
30
+ code: number; // HTTP-like status code
31
+ message: string;
32
+ data: TokenData;
33
+ }
34
+
35
+ export class TokenStorage {
36
+ constructor(prefix?: string, tenant?: string | null);
37
+
38
+ readonly prefix: string;
39
+ readonly tenant: string | null;
40
+
41
+ get accessToken(): string | null;
42
+ set accessToken(value: string | null);
43
+
44
+ get refreshToken(): string | null;
45
+ set refreshToken(value: string | null);
46
+
47
+ clear(): void;
48
+ }
49
+
50
+ export class AuthClient {
51
+ constructor(options: AuthClientOptions);
52
+
53
+ baseUrl: string;
54
+ tenant: string | null;
55
+ headers: Record<string, string>;
56
+ storage: TokenStorage | null;
57
+
58
+ login(
59
+ identifier: string,
60
+ password: string,
61
+ clientId?: string | null,
62
+ extra?: Record<string, any>
63
+ ): Promise<AuthResponse>;
64
+
65
+ refresh(refreshToken: string): Promise<AuthResponse>;
66
+
67
+ introspect(token?: string | null): Promise<AuthResponse>;
68
+ }
69
+
70
+ export class AuthFetch {
71
+ constructor(authClient: AuthClient, storage?: TokenStorage);
72
+
73
+ /**
74
+ * fetch wrapper — hỗ trợ:
75
+ * - auto attach Bearer token
76
+ * - auto refresh token on 401
77
+ * - upload progress (XHR)
78
+ * - download progress (Fetch streaming)
79
+ */
80
+ fetch(
81
+ input: RequestInfo | URL,
82
+ init?: RequestInit,
83
+ onProgress?: ProgressCallback
84
+ ): Promise<Response>;
85
+ }
86
+
87
+ declare const _default: {
88
+ AuthClient: typeof AuthClient;
89
+ AuthFetch: typeof AuthFetch;
90
+ TokenStorage: typeof TokenStorage;
91
+ };
92
+
93
+ export default _default;
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@gh-platform/auth-sdk",
3
+ "version": "1.0.0",
4
+ "description": "VanillaJS Auth SDK for GH Platform",
5
+ "type": "module",
6
+ "main": "dist/auth-sdk.umd.js",
7
+ "module": "dist/auth-sdk.es.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/auth-sdk.es.js",
13
+ "require": "./dist/auth-sdk.umd.js"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "vite build && cp src/index.d.ts dist/index.d.ts && terser dist/auth-sdk.umd.js -o dist/auth-sdk.min.js --compress --mangle"
18
+ },
19
+ "devDependencies": {
20
+ "vite": "^5.0.0",
21
+ "terser": "^5.20.0"
22
+ }
23
+ }
package/src/client.js ADDED
@@ -0,0 +1,140 @@
1
+ // src/client.js
2
+ export default class AuthClient {
3
+ /**
4
+ * options:
5
+ * - baseUrl (required) e.g. "https://auth.example.com"
6
+ * - tenant (optional) e.g. "demo" -> will target /api/v1/{tenant}/auth/*
7
+ * - loginPath / refreshPath / introspectPath (optional overrides)
8
+ * - headers (optional)
9
+ */
10
+ constructor({
11
+ baseUrl,
12
+ tenant = null,
13
+ loginPath = null,
14
+ refreshPath = null,
15
+ introspectPath = null,
16
+ headers = {},
17
+ storage = null, // <-- thêm storage vào constructor
18
+ }) {
19
+ if (!baseUrl) throw new Error("baseUrl is required");
20
+ this.baseUrl = baseUrl.replace(/\/$/, "");
21
+
22
+ // default path builder: tenant-aware
23
+ const prefix = tenant ? `/api/v1/${tenant}/auth` : `/api/v1/auth`;
24
+
25
+ this.loginUrl = this.baseUrl + (loginPath || `${prefix}/login`);
26
+ this.refreshUrl = this.baseUrl + (refreshPath || `${prefix}/refresh`);
27
+ this.introspectUrl = this.baseUrl + (introspectPath || `${prefix}/me`);
28
+
29
+ this.tenant = tenant; // keep for storage prefix
30
+ this.headers = { "Content-Type": "application/json", ...headers };
31
+ this.storage = storage; // <-- store storage instance
32
+ }
33
+
34
+ /**
35
+ * Login payload uses identifier + password (+ optional totp)
36
+ * extra is optional object for backward-compat or extra fields
37
+ */
38
+ async login(identifier, password, totp = null, extra = {}) {
39
+ const body = {
40
+ identifier,
41
+ password,
42
+ ...extra,
43
+ };
44
+ if (totp) body.totp = totp;
45
+
46
+ const res = await fetch(this.loginUrl, {
47
+ method: "POST",
48
+ headers: this.headers,
49
+ body: JSON.stringify(body),
50
+ });
51
+
52
+ if (!res.ok) {
53
+ const text = await res.text().catch(() => res.statusText);
54
+ throw new Error(`Login failed: ${res.status} ${text}`);
55
+ }
56
+ let json; // 👈 MUST DECLARE
57
+ try {
58
+ json = await res.json();
59
+ } catch (e) {
60
+ console.error("❌ JSON parse error:", e);
61
+ throw new Error("Invalid JSON response from server");
62
+ }
63
+
64
+ // -----------------------------
65
+ // 🔥 Auto-save tokens
66
+ // -----------------------------
67
+ const data = json.data || json;
68
+ if (this.storage) {
69
+ if (data.access_token) this.storage.accessToken = data.access_token;
70
+ if (data.refresh_token) this.storage.refreshToken = data.refresh_token;
71
+ }
72
+
73
+ return json;
74
+ }
75
+
76
+ async refresh(refreshToken) {
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
+
87
+ let json; // 👈 MUST DECLARE
88
+ try {
89
+ json = await res.json();
90
+ } catch (e) {
91
+ console.error("❌ JSON parse error:", e);
92
+ throw new Error("Invalid JSON response from server");
93
+ }
94
+ const data = json.data || json;
95
+ // -----------------------------
96
+ // 🔥 Auto-save refreshed tokens
97
+ // -----------------------------
98
+ if (this.storage) {
99
+ if (data.access_token) this.storage.accessToken = data.access_token;
100
+ if (data.refresh_token) this.storage.refreshToken = data.refresh_token;
101
+ }
102
+
103
+ return json;
104
+ }
105
+
106
+ async introspect(token = null) {
107
+ // -----------------------------
108
+ // 🔥 Auto-get token from storage
109
+ // -----------------------------
110
+ let finalToken = token;
111
+
112
+ if (this.storage && !finalToken) {
113
+ finalToken = this.storage.accessToken;
114
+ }
115
+
116
+ if (!finalToken) {
117
+ throw new Error("No access token available for introspection");
118
+ }
119
+
120
+ const headers = new Headers(this.headers);
121
+ headers.set("Authorization", `Bearer ${finalToken}`);
122
+ const res = await fetch(this.introspectUrl, {
123
+ method: "GET",
124
+ headers: headers,
125
+ });
126
+
127
+ if (!res.ok) {
128
+ const text = await res.text().catch(() => res.statusText);
129
+ throw new Error(`Introspect failed: ${res.status} ${text}`);
130
+ }
131
+ let json; // 👈 MUST DECLARE
132
+ try {
133
+ json = await res.json();
134
+ } catch (e) {
135
+ console.error("❌ JSON parse error:", e);
136
+ throw new Error("Invalid JSON response from server");
137
+ }
138
+ return json;
139
+ }
140
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,93 @@
1
+ export interface AuthClientOptions {
2
+ baseUrl: string;
3
+ tenant?: string | null;
4
+ loginPath?: string;
5
+ refreshPath?: string;
6
+ introspectPath?: string;
7
+ headers?: Record<string, string>;
8
+ storage?: TokenStorage | null;
9
+ }
10
+
11
+ /** Callback báo tiến trình (upload hoặc download) */
12
+ export type ProgressCallback = (
13
+ percent: number | null, // % (nếu biết), null nếu không xác định total
14
+ loaded: number, // số bytes đã tải
15
+ total: number | null // tổng bytes (nếu biết)
16
+ ) => void;
17
+
18
+ export interface TokenData {
19
+ access_token: string;
20
+ refresh_token: string;
21
+ token_type: string;
22
+ expires_in: number;
23
+ refresh_expires_at: string;
24
+ message?: string;
25
+ [key: string]: any;
26
+ }
27
+
28
+ export interface AuthResponse {
29
+ status: string; // "success" | "error"
30
+ code: number; // HTTP-like status code
31
+ message: string;
32
+ data: TokenData;
33
+ }
34
+
35
+ export class TokenStorage {
36
+ constructor(prefix?: string, tenant?: string | null);
37
+
38
+ readonly prefix: string;
39
+ readonly tenant: string | null;
40
+
41
+ get accessToken(): string | null;
42
+ set accessToken(value: string | null);
43
+
44
+ get refreshToken(): string | null;
45
+ set refreshToken(value: string | null);
46
+
47
+ clear(): void;
48
+ }
49
+
50
+ export class AuthClient {
51
+ constructor(options: AuthClientOptions);
52
+
53
+ baseUrl: string;
54
+ tenant: string | null;
55
+ headers: Record<string, string>;
56
+ storage: TokenStorage | null;
57
+
58
+ login(
59
+ identifier: string,
60
+ password: string,
61
+ clientId?: string | null,
62
+ extra?: Record<string, any>
63
+ ): Promise<AuthResponse>;
64
+
65
+ refresh(refreshToken: string): Promise<AuthResponse>;
66
+
67
+ introspect(token?: string | null): Promise<AuthResponse>;
68
+ }
69
+
70
+ export class AuthFetch {
71
+ constructor(authClient: AuthClient, storage?: TokenStorage);
72
+
73
+ /**
74
+ * fetch wrapper — hỗ trợ:
75
+ * - auto attach Bearer token
76
+ * - auto refresh token on 401
77
+ * - upload progress (XHR)
78
+ * - download progress (Fetch streaming)
79
+ */
80
+ fetch(
81
+ input: RequestInfo | URL,
82
+ init?: RequestInit,
83
+ onProgress?: ProgressCallback
84
+ ): Promise<Response>;
85
+ }
86
+
87
+ declare const _default: {
88
+ AuthClient: typeof AuthClient;
89
+ AuthFetch: typeof AuthFetch;
90
+ TokenStorage: typeof TokenStorage;
91
+ };
92
+
93
+ export default _default;
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // src/index.js
2
+ import AuthClient from "./client.js";
3
+ import { AuthFetch } from "./middleware.js";
4
+ import { TokenStorage } from "./storage.js";
5
+
6
+ export { AuthClient, AuthFetch, TokenStorage };
7
+ export default { AuthClient, AuthFetch, TokenStorage };
@@ -0,0 +1,110 @@
1
+ // src/middleware.js
2
+ import { TokenStorage } from "./storage.js";
3
+
4
+ export class AuthFetch {
5
+ constructor(authClient, storage = null) {
6
+ this.client = authClient;
7
+ this.storage = storage || new TokenStorage("auth", authClient.tenant || null);
8
+ }
9
+
10
+ /**
11
+ * fetchWithProgress:
12
+ * - input: URL
13
+ * - options: fetch options
14
+ * - onProgress: callback(percent, loaded, total)
15
+ */
16
+ async fetch(input, options = {}, onProgress = null) {
17
+ const token = this.storage.accessToken;
18
+ const headers = new Headers(options.headers || {});
19
+ if (token) headers.set("Authorization", `Bearer ${token}`);
20
+
21
+ // Nếu có body → dùng XHR để GET progress upload
22
+ if (options.method && options.method !== "GET" && options.body) {
23
+ return await this._xhrRequest(input, options, headers, onProgress);
24
+ }
25
+
26
+ // GET/STREAM → dùng fetch
27
+ return await this._fetchWithDownloadProgress(input, options, headers, onProgress);
28
+ }
29
+
30
+ // ------------------ DOWNLOAD PROGRESS ------------------
31
+ async _fetchWithDownloadProgress(input, options, headers, onProgress) {
32
+ let response = await fetch(input, { ...options, headers });
33
+
34
+ // Handle 401 refresh
35
+ if (response.status === 401 && this.storage.refreshToken) {
36
+ try {
37
+ const newTokens = await this.client.refresh(this.storage.refreshToken);
38
+ if (newTokens.access_token) this.storage.accessToken = newTokens.access_token;
39
+ if (newTokens.refresh_token) this.storage.refreshToken = newTokens.refresh_token;
40
+
41
+ headers.set("Authorization", `Bearer ${this.storage.accessToken}`);
42
+ response = await fetch(input, { ...options, headers });
43
+ } catch {
44
+ this.storage.clear();
45
+ throw new Error("Unauthorized, please login again");
46
+ }
47
+ }
48
+
49
+ // Nếu không cần track progress
50
+ if (!onProgress || !response.body) return response;
51
+
52
+ // Stream reader
53
+ const reader = response.body.getReader();
54
+ const contentLength = +response.headers.get("Content-Length") || 0;
55
+
56
+ let loaded = 0;
57
+ const chunks = [];
58
+
59
+ while (true) {
60
+ const { done, value } = await reader.read();
61
+ if (done) break;
62
+
63
+ chunks.push(value);
64
+ loaded += value.length;
65
+
66
+ if (contentLength) {
67
+ const percent = Math.round((loaded / contentLength) * 100);
68
+ onProgress(percent, loaded, contentLength);
69
+ } else {
70
+ onProgress(null, loaded, null); // không biết tổng → chỉ báo loaded
71
+ }
72
+ }
73
+
74
+ const blob = new Blob(chunks);
75
+ return new Response(blob, response);
76
+ }
77
+
78
+ // ------------------ UPLOAD PROGRESS ------------------
79
+ _xhrRequest(url, options, headers, onProgress) {
80
+ return new Promise((resolve, reject) => {
81
+ const xhr = new XMLHttpRequest();
82
+ xhr.open(options.method || "POST", url, true);
83
+
84
+ // set headers
85
+ for (const [k, v] of headers.entries()) {
86
+ xhr.setRequestHeader(k, v);
87
+ }
88
+
89
+ // Upload progress
90
+ if (xhr.upload && onProgress) {
91
+ xhr.upload.onprogress = (e) => {
92
+ if (e.lengthComputable) {
93
+ const percent = Math.round((e.loaded / e.total) * 100);
94
+ onProgress(percent, e.loaded, e.total);
95
+ } else {
96
+ onProgress(null, e.loaded, null);
97
+ }
98
+ };
99
+ }
100
+
101
+ xhr.onload = () => {
102
+ resolve(new Response(xhr.response, { status: xhr.status }));
103
+ };
104
+
105
+ xhr.onerror = () => reject(new Error("Network error"));
106
+ xhr.send(options.body);
107
+ });
108
+ }
109
+ }
110
+ export default AuthFetch;
package/src/storage.js ADDED
@@ -0,0 +1,43 @@
1
+ // src/storage.js
2
+ export class TokenStorage {
3
+ /**
4
+ * prefix: base prefix (default "auth")
5
+ * tenant: optional tenant string -> final key = `${prefix}:${tenant}_access_token`
6
+ */
7
+ constructor(prefix = "auth", tenant = null) {
8
+ this.prefix = prefix;
9
+ this.tenant = tenant;
10
+ }
11
+
12
+ _key(name) {
13
+ // include tenant to isolate tokens between tenants if provided
14
+ return this.tenant ? `${this.prefix}:${this.tenant}_${name}` : `${this.prefix}_${name}`;
15
+ }
16
+
17
+ get accessToken() {
18
+ return localStorage.getItem(this._key("access_token"));
19
+ }
20
+ set accessToken(val) {
21
+ if (val === null || val === undefined) {
22
+ localStorage.removeItem(this._key("access_token"));
23
+ } else {
24
+ localStorage.setItem(this._key("access_token"), val);
25
+ }
26
+ }
27
+
28
+ get refreshToken() {
29
+ return localStorage.getItem(this._key("refresh_token"));
30
+ }
31
+ set refreshToken(val) {
32
+ if (val === null || val === undefined) {
33
+ localStorage.removeItem(this._key("refresh_token"));
34
+ } else {
35
+ localStorage.setItem(this._key("refresh_token"), val);
36
+ }
37
+ }
38
+
39
+ clear() {
40
+ localStorage.removeItem(this._key("access_token"));
41
+ localStorage.removeItem(this._key("refresh_token"));
42
+ }
43
+ }
package/vite.config.js ADDED
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from "vite";
2
+
3
+ export default defineConfig({
4
+ build: {
5
+ lib: {
6
+ entry: "src/index.js",
7
+ name: "AuthSDK", // window.AuthSDK.*
8
+ fileName: (format) => `auth-sdk.${format}.js`,
9
+ formats: ["es", "umd"],
10
+ },
11
+ rollupOptions: {
12
+ output: {
13
+ exports: "named",
14
+ },
15
+ },
16
+ minify: "terser",
17
+ outDir: "dist",
18
+ },
19
+ });