nocal-auth-sdk 0.1.9 → 0.2.1
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/index.d.ts +12 -21
- package/dist/index.js +176 -76
- package/dist/types.d.ts +17 -5
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
|
-
import { NocalClientConfig, OAuthLoginReq,
|
|
1
|
+
import { NocalClientConfig, OAuthLoginReq, NocalUser, NocalSession } from "./types";
|
|
2
2
|
export declare class NocalAuthSDK {
|
|
3
|
-
private supabase;
|
|
4
3
|
private clientId;
|
|
5
4
|
private redirectUri;
|
|
5
|
+
private authServerUrl;
|
|
6
|
+
private currentSession;
|
|
7
|
+
private currentUser;
|
|
6
8
|
constructor(config: NocalClientConfig);
|
|
7
9
|
/**
|
|
8
|
-
* Mở Popup UI Đăng nhập
|
|
9
|
-
*/
|
|
10
|
-
signInWithNocal(req?: OAuthLoginReq): Promise<NocalUser>;
|
|
11
|
-
/**
|
|
12
|
-
* Lấy thông tin User hiện tại
|
|
10
|
+
* Mở Popup UI Đăng nhập & Ủy quyền chuẩn Supabase Native OAuth 2.0 với PKCE
|
|
13
11
|
*/
|
|
12
|
+
signInWithNocal(req?: OAuthLoginReq): Promise<{
|
|
13
|
+
user: NocalUser;
|
|
14
|
+
session: NocalSession;
|
|
15
|
+
}>;
|
|
16
|
+
private exchangeCodeForToken;
|
|
17
|
+
private fetchUserInfo;
|
|
14
18
|
getUser(): Promise<NocalUser | null>;
|
|
15
|
-
/**
|
|
16
|
-
* Lấy Session / Access Token hiện tại
|
|
17
|
-
*/
|
|
18
19
|
getSession(): Promise<NocalSession | null>;
|
|
19
|
-
/**
|
|
20
|
-
* Đăng xuất khỏi hệ thống
|
|
21
|
-
*/
|
|
22
20
|
signOut(): Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* Lắng nghe sự kiện đăng nhập / đăng xuất
|
|
25
|
-
*/
|
|
26
|
-
onAuthStateChange(callback: (event: string, session: NocalSession | null) => void): {
|
|
27
|
-
data: {
|
|
28
|
-
subscription: import("@supabase/auth-js").Subscription;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
21
|
}
|
|
22
|
+
export { NocalAuthSDK as NocalAuth };
|
|
32
23
|
export * from "./types";
|
package/dist/index.js
CHANGED
|
@@ -1,109 +1,209 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const DEFAULT_AUTH_SERVER_URL = "https://hldymiruucsqqzicnwdv.supabase.co";
|
|
2
|
+
// Helper for PKCE crypto
|
|
3
|
+
function base64URLEncode(buffer) {
|
|
4
|
+
let binary = "";
|
|
5
|
+
const bytes = new Uint8Array(buffer);
|
|
6
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
7
|
+
binary += String.fromCharCode(bytes[i]);
|
|
8
|
+
}
|
|
9
|
+
return btoa(binary)
|
|
10
|
+
.replace(/\+/g, "-")
|
|
11
|
+
.replace(/\//g, "_")
|
|
12
|
+
.replace(/=+$/, "");
|
|
13
|
+
}
|
|
14
|
+
async function generateCodeChallenge(verifier) {
|
|
15
|
+
const encoder = new TextEncoder();
|
|
16
|
+
const data = encoder.encode(verifier);
|
|
17
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
18
|
+
return base64URLEncode(new Uint8Array(digest));
|
|
19
|
+
}
|
|
20
|
+
function generateRandomString(length = 32) {
|
|
21
|
+
const array = new Uint8Array(length);
|
|
22
|
+
crypto.getRandomValues(array);
|
|
23
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
24
|
+
}
|
|
4
25
|
export class NocalAuthSDK {
|
|
5
26
|
constructor(config) {
|
|
27
|
+
this.currentSession = null;
|
|
28
|
+
this.currentUser = null;
|
|
6
29
|
this.clientId = config.clientId;
|
|
7
30
|
this.redirectUri = config.redirectUri;
|
|
8
|
-
this.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
this.authServerUrl = config.authServerUrl || DEFAULT_AUTH_SERVER_URL;
|
|
32
|
+
// Load session from localStorage if available
|
|
33
|
+
if (typeof window !== "undefined") {
|
|
34
|
+
try {
|
|
35
|
+
const stored = localStorage.getItem("nocal_auth_session");
|
|
36
|
+
if (stored) {
|
|
37
|
+
const parsed = JSON.parse(stored);
|
|
38
|
+
this.currentSession = parsed.session;
|
|
39
|
+
this.currentUser = parsed.user;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
// ignore
|
|
44
|
+
}
|
|
45
|
+
}
|
|
15
46
|
}
|
|
16
47
|
/**
|
|
17
|
-
* Mở Popup UI Đăng nhập
|
|
48
|
+
* Mở Popup UI Đăng nhập & Ủy quyền chuẩn Supabase Native OAuth 2.0 với PKCE
|
|
18
49
|
*/
|
|
19
50
|
async signInWithNocal(req) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
if (error)
|
|
34
|
-
throw error;
|
|
35
|
-
if (!data?.url)
|
|
36
|
-
throw new Error("CANNOT_GENERATE_AUTH_URL");
|
|
37
|
-
// 2. Tính toán kích thước và căn giữa màn hình Popup
|
|
51
|
+
const state = req?.state || generateRandomString(16);
|
|
52
|
+
const codeVerifier = generateRandomString(32);
|
|
53
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
54
|
+
const authUrl = new URL(`${this.authServerUrl}/auth/v1/oauth/authorize`);
|
|
55
|
+
authUrl.searchParams.set("client_id", this.clientId);
|
|
56
|
+
authUrl.searchParams.set("redirect_uri", this.redirectUri);
|
|
57
|
+
authUrl.searchParams.set("response_type", "code");
|
|
58
|
+
authUrl.searchParams.set("state", state);
|
|
59
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
60
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
38
61
|
const width = 480;
|
|
39
62
|
const height = 680;
|
|
40
|
-
const left = typeof window !== "undefined"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const top = typeof window !== "undefined"
|
|
44
|
-
? window.screenY + (window.outerHeight - height) / 2
|
|
45
|
-
: 0;
|
|
46
|
-
// 3. Mở Cửa sổ Popup chứa UI đăng nhập Nocal
|
|
47
|
-
const popup = window.open(data.url, "NocalAuthPopup", `width=${width},height=${height},left=${left},top=${top},status=yes,scrollbars=yes`);
|
|
63
|
+
const left = typeof window !== "undefined" ? window.screenX + (window.outerWidth - width) / 2 : 0;
|
|
64
|
+
const top = typeof window !== "undefined" ? window.screenY + (window.outerHeight - height) / 2 : 0;
|
|
65
|
+
const popup = window.open(authUrl.toString(), "NocalAuthPopup", `width=${width},height=${height},left=${left},top=${top},status=yes,scrollbars=yes`);
|
|
48
66
|
if (!popup) {
|
|
49
67
|
throw new Error("POPUP_BLOCKED");
|
|
50
68
|
}
|
|
51
|
-
// 4. Lắng nghe và kiểm tra khi Popup đóng
|
|
52
69
|
return new Promise((resolve, reject) => {
|
|
53
|
-
const
|
|
70
|
+
const allowedOrigin = new URL(this.authServerUrl).origin;
|
|
71
|
+
const messageListener = async (event) => {
|
|
72
|
+
if (event.origin !== allowedOrigin)
|
|
73
|
+
return;
|
|
74
|
+
const data = event.data;
|
|
75
|
+
if (!data || (data.type !== "NOCAL_OAUTH_CODE" && data.type !== "NOCAL_OAUTH_ERROR")) {
|
|
76
|
+
// Hoặc kiểm tra URL redirect của popup nếu Supabase native redirect trực tiếp
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
window.removeEventListener("message", messageListener);
|
|
80
|
+
clearInterval(checkClosedTimer);
|
|
81
|
+
if (data.type === "NOCAL_OAUTH_ERROR" || data.error) {
|
|
82
|
+
popup.close();
|
|
83
|
+
reject(new Error(data.error || "OAUTH_ERROR"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (data.state && data.state !== state) {
|
|
87
|
+
popup.close();
|
|
88
|
+
reject(new Error("OAUTH_STATE_MISMATCH"));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const code = data.code;
|
|
92
|
+
if (!code) {
|
|
93
|
+
popup.close();
|
|
94
|
+
reject(new Error("MISSING_AUTHORIZATION_CODE"));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
54
97
|
try {
|
|
55
|
-
|
|
98
|
+
popup.close();
|
|
99
|
+
const session = await this.exchangeCodeForToken(code, codeVerifier);
|
|
100
|
+
const user = await this.fetchUserInfo(session.access_token);
|
|
101
|
+
this.currentSession = session;
|
|
102
|
+
this.currentUser = user;
|
|
103
|
+
if (typeof window !== "undefined") {
|
|
104
|
+
localStorage.setItem("nocal_auth_session", JSON.stringify({ session, user }));
|
|
105
|
+
}
|
|
106
|
+
resolve({ user, session });
|
|
56
107
|
}
|
|
57
|
-
catch (
|
|
58
|
-
|
|
108
|
+
catch (err) {
|
|
109
|
+
reject(err);
|
|
59
110
|
}
|
|
60
111
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
112
|
+
window.addEventListener("message", messageListener);
|
|
113
|
+
const checkClosedTimer = setInterval(() => {
|
|
114
|
+
try {
|
|
115
|
+
if (popup.closed) {
|
|
116
|
+
clearInterval(checkClosedTimer);
|
|
117
|
+
window.removeEventListener("message", messageListener);
|
|
118
|
+
// Kiểm tra nếu popup đóng và đã có code trong redirect URL (nếu dùng redirect thuần)
|
|
119
|
+
reject(new Error("POPUP_CLOSED_BY_USER"));
|
|
68
120
|
}
|
|
69
121
|
else {
|
|
70
|
-
|
|
122
|
+
// Lắng nghe URL thay đổi nếu Supabase redirect trực tiếp về redirectUri
|
|
123
|
+
try {
|
|
124
|
+
const popupUrl = new URL(popup.location.href);
|
|
125
|
+
if (popupUrl.origin === new URL(this.redirectUri).origin) {
|
|
126
|
+
const code = popupUrl.searchParams.get("code");
|
|
127
|
+
const error = popupUrl.searchParams.get("error");
|
|
128
|
+
const returnedState = popupUrl.searchParams.get("state");
|
|
129
|
+
if (code || error) {
|
|
130
|
+
clearInterval(checkClosedTimer);
|
|
131
|
+
window.removeEventListener("message", messageListener);
|
|
132
|
+
popup.close();
|
|
133
|
+
if (error) {
|
|
134
|
+
reject(new Error(error));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (code) {
|
|
138
|
+
this.exchangeCodeForToken(code, codeVerifier)
|
|
139
|
+
.then(async (session) => {
|
|
140
|
+
const user = await this.fetchUserInfo(session.access_token);
|
|
141
|
+
this.currentSession = session;
|
|
142
|
+
this.currentUser = user;
|
|
143
|
+
if (typeof window !== "undefined") {
|
|
144
|
+
localStorage.setItem("nocal_auth_session", JSON.stringify({ session, user }));
|
|
145
|
+
}
|
|
146
|
+
resolve({ user, session });
|
|
147
|
+
})
|
|
148
|
+
.catch(reject);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
// Cross-origin check while on Supabase auth domain
|
|
155
|
+
}
|
|
71
156
|
}
|
|
72
157
|
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
// ignore
|
|
160
|
+
}
|
|
73
161
|
}, 500);
|
|
74
162
|
});
|
|
75
163
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
164
|
+
async exchangeCodeForToken(code, codeVerifier) {
|
|
165
|
+
const res = await fetch(`${this.authServerUrl}/auth/v1/oauth/token`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: { "Content-Type": "application/json" },
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
grant_type: "authorization_code",
|
|
170
|
+
code,
|
|
171
|
+
client_id: this.clientId,
|
|
172
|
+
redirect_uri: this.redirectUri,
|
|
173
|
+
code_verifier: codeVerifier,
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
const data = await res.json();
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
throw new Error(data.error_description || data.error || "FAILED_TO_EXCHANGE_TOKEN");
|
|
179
|
+
}
|
|
180
|
+
return data;
|
|
181
|
+
}
|
|
182
|
+
async fetchUserInfo(accessToken) {
|
|
183
|
+
const res = await fetch(`${this.authServerUrl}/auth/v1/oauth/userinfo`, {
|
|
184
|
+
headers: {
|
|
185
|
+
Authorization: `Bearer ${accessToken}`,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const data = await res.json();
|
|
189
|
+
if (!res.ok) {
|
|
190
|
+
throw new Error(data.error_description || data.error || "FAILED_TO_FETCH_USERINFO");
|
|
191
|
+
}
|
|
192
|
+
return data;
|
|
193
|
+
}
|
|
79
194
|
async getUser() {
|
|
80
|
-
|
|
81
|
-
if (error)
|
|
82
|
-
throw error;
|
|
83
|
-
return user;
|
|
195
|
+
return this.currentUser;
|
|
84
196
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Lấy Session / Access Token hiện tại
|
|
87
|
-
*/
|
|
88
197
|
async getSession() {
|
|
89
|
-
|
|
90
|
-
if (error)
|
|
91
|
-
throw error;
|
|
92
|
-
return session;
|
|
198
|
+
return this.currentSession;
|
|
93
199
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Đăng xuất khỏi hệ thống
|
|
96
|
-
*/
|
|
97
200
|
async signOut() {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* Lắng nghe sự kiện đăng nhập / đăng xuất
|
|
104
|
-
*/
|
|
105
|
-
onAuthStateChange(callback) {
|
|
106
|
-
return this.supabase.auth.onAuthStateChange(callback);
|
|
201
|
+
this.currentSession = null;
|
|
202
|
+
this.currentUser = null;
|
|
203
|
+
if (typeof window !== "undefined") {
|
|
204
|
+
localStorage.removeItem("nocal_auth_session");
|
|
205
|
+
}
|
|
107
206
|
}
|
|
108
207
|
}
|
|
208
|
+
export { NocalAuthSDK as NocalAuth };
|
|
109
209
|
export * from "./types";
|
package/dist/types.d.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import { User, Session } from "@supabase/supabase-js";
|
|
2
1
|
export interface NocalClientConfig {
|
|
3
2
|
clientId: string;
|
|
4
3
|
redirectUri: string;
|
|
4
|
+
authServerUrl?: string;
|
|
5
5
|
}
|
|
6
|
-
export type NocalOAuthProvider = "google" | "facebook" | "github" | "apple";
|
|
7
6
|
export interface OAuthLoginReq {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
state?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface NocalUser {
|
|
10
|
+
sub: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
phone?: string;
|
|
13
|
+
email_confirmed?: boolean;
|
|
14
|
+
phone_confirmed?: boolean;
|
|
15
|
+
user_metadata?: Record<string, any>;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
export interface NocalSession {
|
|
19
|
+
access_token: string;
|
|
20
|
+
refresh_token: string;
|
|
21
|
+
token_type: string;
|
|
22
|
+
expires_in: number;
|
|
10
23
|
}
|
|
11
|
-
export type { User, Session };
|