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 CHANGED
@@ -1,32 +1,23 @@
1
- import { NocalClientConfig, OAuthLoginReq, User as NocalUser, Session as NocalSession } from "./types";
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/Đăng ký tập trung của Nocal
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
- import { createClient, } from "@supabase/supabase-js";
2
- const NOCAL_AUTH_SERVER_URL = "https://hldymiruucsqqzicnwdv.supabase.co";
3
- const NOCAL_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhsZHltaXJ1dWNzcXF6aWNud2R2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODk2MTQ2NzAsImV4cCI6MjEwNTE5MDY3MH0.dLaPWdgTV8mRVToNT1XLg1EUjHvyvRRK2qc0DQAu8YI";
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.supabase = createClient(NOCAL_AUTH_SERVER_URL, NOCAL_ANON_KEY, {
9
- auth: {
10
- autoRefreshToken: true,
11
- persistSession: true,
12
- detectSessionInUrl: true,
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/Đăng ký tập trung của Nocal
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 targetRedirect = req?.redirectTo || this.redirectUri;
21
- const provider = (req?.provider || "google");
22
- // 1. Gọi Supabase SDK để sinh URL ủy quyền chuẩn OAuth 2.0 / PKCE
23
- const { data, error } = await this.supabase.auth.signInWithOAuth({
24
- provider,
25
- options: {
26
- redirectTo: targetRedirect,
27
- skipBrowserRedirect: true, // Lấy URL trả về, không nhảy trang tab chính
28
- queryParams: {
29
- client_id: this.clientId, // Gắn Client ID để Auth Server nhận diện App
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
- ? window.screenX + (window.outerWidth - width) / 2
42
- : 0;
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 isPopupClosed = () => {
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
- return popup.closed;
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 (e) {
58
- return false;
108
+ catch (err) {
109
+ reject(err);
59
110
  }
60
111
  };
61
- const timer = setInterval(async () => {
62
- if (isPopupClosed()) {
63
- clearInterval(timer);
64
- // Sau khi popup đóng, lấy session đã đăng nhập
65
- const { data: sessionData } = await this.supabase.auth.getSession();
66
- if (sessionData?.session?.user) {
67
- resolve(sessionData.session.user);
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
- reject(new Error("LOGIN_CANCELLED_OR_FAILED"));
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
- * Lấy thông tin User hiện tại
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
- const { data: { user }, error, } = await this.supabase.auth.getUser();
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
- const { data: { session }, error, } = await this.supabase.auth.getSession();
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
- const { error } = await this.supabase.auth.signOut();
99
- if (error)
100
- throw error;
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
- provider?: NocalOAuthProvider;
9
- redirectTo?: string;
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nocal-auth-sdk",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "Nocal Auth - client-side SDK for Login with Nocal (popup flow)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",