@bagelink/auth 1.4.178 → 1.4.182

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
@@ -28,18 +28,14 @@ bun add @bagelink/auth
28
28
  ### 1. Initialize Auth
29
29
 
30
30
  ```typescript
31
- import { initAuth } from '@bagelink/auth'
32
- import axios from 'axios'
31
+ import { initAuth, AuthState } from '@bagelink/auth'
33
32
 
34
33
  const auth = initAuth({
35
- axios: axios,
36
34
  baseURL: 'https://api.example.com'
37
35
  })
38
36
 
39
37
  // Listen to auth events
40
- auth.on(AuthState.LOGIN, () => {
41
- console.log('User logged in!')
42
- })
38
+ auth.on(AuthState.LOGIN, () => console.log('User logged in!'))
43
39
  ```
44
40
 
45
41
  ### 2. Use in Vue Components
@@ -50,17 +46,23 @@ import { useAuth } from '@bagelink/auth'
50
46
 
51
47
  const {
52
48
  user, // Primary state - use this!
49
+ sso, // SSO providers
53
50
  getIsLoggedIn,
54
51
  login,
55
52
  logout
56
53
  } = useAuth()
57
54
 
58
- const handleLogin = async () => {
55
+ const handlePasswordLogin = async () => {
59
56
  await login({
60
57
  email: 'user@example.com',
61
58
  password: 'password'
62
59
  })
63
60
  }
61
+
62
+ const handleSSOLogin = async () => {
63
+ // SSO is just this simple!
64
+ await sso.google.redirect()
65
+ }
64
66
  </script>
65
67
 
66
68
  <template>
@@ -70,7 +72,8 @@ const handleLogin = async () => {
70
72
  <button @click="logout">Logout</button>
71
73
  </div>
72
74
  <div v-else>
73
- <button @click="handleLogin">Login</button>
75
+ <button @click="handlePasswordLogin">Login with Password</button>
76
+ <button @click="handleSSOLogin">Login with Google</button>
74
77
  </div>
75
78
  </template>
76
79
  ```
package/dist/index.cjs CHANGED
@@ -7,12 +7,12 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
7
7
 
8
8
  const axios__default = /*#__PURE__*/_interopDefaultCompat(axios);
9
9
 
10
- function createAxiosInstance(baseURL = "") {
10
+ function createAxiosInstance(baseURL) {
11
11
  return axios__default.create({
12
- baseURL: baseURL || "",
12
+ baseURL,
13
+ withCredentials: true,
13
14
  headers: {
14
- "Content-Type": "application/json",
15
- "withCredentials": true
15
+ "Content-Type": "application/json"
16
16
  }
17
17
  });
18
18
  }
@@ -49,16 +49,12 @@ class EventEmitter {
49
49
 
50
50
  class AuthApi {
51
51
  api;
52
- constructor(axiosInstance, baseURL = "") {
53
- this.api = axiosInstance || createAxiosInstance(baseURL);
52
+ constructor(baseURL = "") {
53
+ this.api = createAxiosInstance(baseURL);
54
54
  this.setupInterceptors();
55
55
  }
56
56
  setupInterceptors() {
57
57
  this.api.interceptors.request.use((config) => {
58
- const sessionToken = localStorage.getItem("session_token");
59
- if (sessionToken !== null && config.headers) {
60
- config.headers.Authorization = `Bearer ${sessionToken}`;
61
- }
62
58
  const urlParams = new URLSearchParams(window.location.search);
63
59
  const resetToken = urlParams.get("token");
64
60
  if (resetToken !== null && config.headers) {
@@ -80,45 +76,67 @@ class AuthApi {
80
76
  * Register a new account
81
77
  */
82
78
  async register(data) {
83
- const response = await this.api.post("/authentication/register", {
79
+ return this.api.post("/authentication/register", {
84
80
  ...data,
85
81
  email: data.email.toLowerCase()
86
82
  });
87
- if (response.data.session_token) {
88
- localStorage.setItem("session_token", response.data.session_token);
89
- }
90
- return response;
91
83
  }
92
84
  /**
93
85
  * Login with password
94
86
  */
95
87
  async login(email, password) {
96
- const response = await this.api.post("/authentication/login/password", {
88
+ return this.api.post("/authentication/login/password", {
97
89
  email: email.toLowerCase(),
98
90
  password
99
91
  });
100
- if (response.data.session_token) {
101
- localStorage.setItem("session_token", response.data.session_token);
102
- }
103
- return response;
104
92
  }
105
93
  /**
106
94
  * Logout and clear session
107
95
  */
108
96
  async logout() {
109
- const response = await this.api.post("/authentication/logout", {});
110
- localStorage.removeItem("session_token");
111
- return response;
97
+ return this.api.post("/authentication/logout", {});
112
98
  }
113
99
  /**
114
100
  * Refresh current session
115
101
  */
116
102
  async refreshSession() {
117
- const response = await this.api.post("/authentication/refresh", {});
118
- if (response.data.session_token) {
119
- localStorage.setItem("session_token", response.data.session_token);
120
- }
121
- return response;
103
+ return this.api.post("/authentication/refresh", {});
104
+ }
105
+ // ============================================
106
+ // SSO Authentication Methods
107
+ // ============================================
108
+ /**
109
+ * Initiate SSO login flow
110
+ * Returns authorization URL to redirect user to
111
+ */
112
+ async initiateSSO(data) {
113
+ return this.api.post(`/authentication/sso/${data.provider}/initiate`, {
114
+ redirect_uri: data.redirect_uri,
115
+ state: data.state
116
+ });
117
+ }
118
+ /**
119
+ * Complete SSO login after callback from provider
120
+ */
121
+ async ssoCallback(data) {
122
+ return this.api.post(`/authentication/sso/${data.provider}/callback`, {
123
+ code: data.code,
124
+ state: data.state
125
+ });
126
+ }
127
+ /**
128
+ * Link an SSO provider to existing account
129
+ */
130
+ async linkSSOProvider(data) {
131
+ return this.api.post(`/authentication/sso/${data.provider}/link`, {
132
+ code: data.code
133
+ });
134
+ }
135
+ /**
136
+ * Unlink an SSO provider from account
137
+ */
138
+ async unlinkSSOProvider(provider) {
139
+ return this.api.delete(`/authentication/sso/${provider}/unlink`);
122
140
  }
123
141
  // ============================================
124
142
  // Current User (Me) Methods
@@ -139,9 +157,7 @@ class AuthApi {
139
157
  * Delete current user account
140
158
  */
141
159
  async deleteCurrentUser() {
142
- const response = await this.api.delete("/authentication/me");
143
- localStorage.removeItem("session_token");
144
- return response;
160
+ return this.api.delete("/authentication/me");
145
161
  }
146
162
  // ============================================
147
163
  // Account Management (Admin)
@@ -251,6 +267,356 @@ class AuthApi {
251
267
  }
252
268
  }
253
269
 
270
+ let authApiRef = null;
271
+ function setAuthContext(authApi) {
272
+ authApiRef = authApi;
273
+ }
274
+ function getAuthApi() {
275
+ if (!authApiRef) {
276
+ throw new Error("SSO auth context not initialized. Make sure to call useAuth() before using SSO methods.");
277
+ }
278
+ return authApiRef;
279
+ }
280
+ class SSOError extends Error {
281
+ constructor(message, code) {
282
+ super(message);
283
+ this.code = code;
284
+ this.name = "SSOError";
285
+ }
286
+ }
287
+ class PopupBlockedError extends SSOError {
288
+ constructor() {
289
+ super("Popup was blocked. Please allow popups for this site.", "POPUP_BLOCKED");
290
+ this.name = "PopupBlockedError";
291
+ }
292
+ }
293
+ class PopupClosedError extends SSOError {
294
+ constructor() {
295
+ super("Popup was closed by user", "POPUP_CLOSED");
296
+ this.name = "PopupClosedError";
297
+ }
298
+ }
299
+ class PopupTimeoutError extends SSOError {
300
+ constructor() {
301
+ super("Popup authentication timed out", "POPUP_TIMEOUT");
302
+ this.name = "PopupTimeoutError";
303
+ }
304
+ }
305
+ class StateMismatchError extends SSOError {
306
+ constructor() {
307
+ super("State mismatch - possible CSRF attack", "STATE_MISMATCH");
308
+ this.name = "StateMismatchError";
309
+ }
310
+ }
311
+ function generateState() {
312
+ const array = new Uint8Array(32);
313
+ crypto.getRandomValues(array);
314
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
315
+ }
316
+ function openPopup(url, width = 500, height = 600) {
317
+ const left = window.screenX + (window.outerWidth - width) / 2;
318
+ const top = window.screenY + (window.outerHeight - height) / 2;
319
+ return window.open(
320
+ url,
321
+ "oauth-popup",
322
+ `width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,location=no,status=no`
323
+ );
324
+ }
325
+ function waitForPopupCallback(popup, provider, timeoutMs = 9e4) {
326
+ return new Promise((resolve, reject) => {
327
+ let done = false;
328
+ const finish = (fn) => {
329
+ if (!done) {
330
+ done = true;
331
+ fn();
332
+ }
333
+ };
334
+ const timer = setTimeout(() => {
335
+ finish(() => {
336
+ reject(new PopupTimeoutError());
337
+ });
338
+ }, timeoutMs);
339
+ function onMessage(ev) {
340
+ try {
341
+ if (ev.origin !== window.location.origin) return;
342
+ const data = ev.data || {};
343
+ if (data.type !== "auth:complete" || data.provider !== provider) return;
344
+ cleanup();
345
+ if (data.error) {
346
+ reject(new SSOError(data.error, "OAUTH_ERROR"));
347
+ } else if (data.code) {
348
+ resolve({ code: data.code, state: data.state });
349
+ }
350
+ } catch {
351
+ }
352
+ }
353
+ const pollInterval = setInterval(() => {
354
+ try {
355
+ if (popup.closed) {
356
+ cleanup();
357
+ reject(new PopupClosedError());
358
+ return;
359
+ }
360
+ const url = new URL(popup.location.href);
361
+ if (url.origin === window.location.origin) {
362
+ const code = url.searchParams.get("code");
363
+ const state = url.searchParams.get("state") ?? void 0;
364
+ const error = url.searchParams.get("error");
365
+ if (code || error) {
366
+ cleanup();
367
+ try {
368
+ popup.close();
369
+ } catch {
370
+ }
371
+ if (error) {
372
+ reject(new SSOError(error, "OAUTH_ERROR"));
373
+ } else if (code) {
374
+ resolve({ code, state });
375
+ }
376
+ }
377
+ }
378
+ } catch {
379
+ }
380
+ }, 150);
381
+ function cleanup() {
382
+ finish(() => {
383
+ clearInterval(pollInterval);
384
+ clearTimeout(timer);
385
+ window.removeEventListener("message", onMessage);
386
+ try {
387
+ popup.close();
388
+ } catch {
389
+ }
390
+ });
391
+ }
392
+ window.addEventListener("message", onMessage);
393
+ });
394
+ }
395
+ function createSSOProvider(config) {
396
+ const getDefaultRedirectUri = () => {
397
+ if (typeof window !== "undefined") {
398
+ return `${window.location.origin}/auth/callback`;
399
+ }
400
+ return `/auth/callback`;
401
+ };
402
+ const getStateKey = () => `oauth_state:${config.id}`;
403
+ return {
404
+ ...config,
405
+ async redirect(options = {}) {
406
+ const auth = getAuthApi();
407
+ const redirectUri = options.redirectUri ?? getDefaultRedirectUri();
408
+ const state = options.state ?? generateState();
409
+ if (typeof sessionStorage !== "undefined") {
410
+ sessionStorage.setItem(getStateKey(), state);
411
+ sessionStorage.setItem(`oauth_provider:${state}`, config.id);
412
+ }
413
+ const authUrl = await auth.initiateSSO({
414
+ provider: config.id,
415
+ redirect_uri: redirectUri,
416
+ state,
417
+ scopes: options.scopes ?? config.defaultScopes,
418
+ params: options.params
419
+ });
420
+ window.location.href = authUrl;
421
+ },
422
+ async popup(options = {}) {
423
+ const auth = getAuthApi();
424
+ const redirectUri = options.redirectUri ?? getDefaultRedirectUri();
425
+ const state = options.state ?? generateState();
426
+ const timeout = options.popupTimeout ?? 9e4;
427
+ if (typeof sessionStorage !== "undefined") {
428
+ sessionStorage.setItem(getStateKey(), state);
429
+ sessionStorage.setItem(`oauth_provider:${state}`, config.id);
430
+ }
431
+ const authUrl = await auth.initiateSSO({
432
+ provider: config.id,
433
+ redirect_uri: redirectUri,
434
+ state,
435
+ scopes: options.scopes ?? config.defaultScopes,
436
+ params: options.params
437
+ });
438
+ const { width = 500, height = 600 } = options.popupDimensions ?? {};
439
+ const popupWindow = openPopup(authUrl, width, height);
440
+ if (!popupWindow) {
441
+ throw new PopupBlockedError();
442
+ }
443
+ const result = await waitForPopupCallback(popupWindow, config.id, timeout);
444
+ return auth.loginWithSSO({
445
+ provider: config.id,
446
+ code: result.code,
447
+ state: result.state
448
+ });
449
+ },
450
+ async callback(code, state) {
451
+ const auth = getAuthApi();
452
+ if (typeof sessionStorage !== "undefined" && state) {
453
+ const storedState = sessionStorage.getItem(getStateKey());
454
+ sessionStorage.removeItem(getStateKey());
455
+ sessionStorage.removeItem(`oauth_provider:${state}`);
456
+ if (storedState && storedState !== state) {
457
+ throw new StateMismatchError();
458
+ }
459
+ }
460
+ return auth.loginWithSSO({
461
+ provider: config.id,
462
+ code,
463
+ state
464
+ });
465
+ },
466
+ async link(code) {
467
+ const auth = getAuthApi();
468
+ await auth.linkSSOProvider({
469
+ provider: config.id,
470
+ code
471
+ });
472
+ },
473
+ async unlink() {
474
+ const auth = getAuthApi();
475
+ await auth.unlinkSSOProvider(config.id);
476
+ },
477
+ async getAuthUrl(options = {}) {
478
+ const auth = getAuthApi();
479
+ const redirectUri = options.redirectUri ?? getDefaultRedirectUri();
480
+ const state = options.state ?? generateState();
481
+ return auth.initiateSSO({
482
+ provider: config.id,
483
+ redirect_uri: redirectUri,
484
+ state,
485
+ scopes: options.scopes ?? config.defaultScopes,
486
+ params: options.params
487
+ });
488
+ },
489
+ supportsPopup: true
490
+ // Default, can be overridden per provider
491
+ };
492
+ }
493
+ const sso = {
494
+ /**
495
+ * Google OAuth Provider
496
+ * https://developers.google.com/identity/protocols/oauth2
497
+ */
498
+ google: createSSOProvider({
499
+ id: "google",
500
+ name: "Google",
501
+ color: "#4285F4",
502
+ icon: "google",
503
+ defaultScopes: ["openid", "email", "profile"],
504
+ metadata: {
505
+ authDomain: "accounts.google.com",
506
+ buttonText: "Continue with Google"
507
+ }
508
+ }),
509
+ /**
510
+ * Microsoft OAuth Provider (Azure AD / Microsoft Entra ID)
511
+ * https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
512
+ */
513
+ microsoft: createSSOProvider({
514
+ id: "microsoft",
515
+ name: "Microsoft",
516
+ color: "#00A4EF",
517
+ icon: "microsoft",
518
+ defaultScopes: ["openid", "email", "profile", "User.Read"],
519
+ metadata: {
520
+ authDomain: "login.microsoftonline.com",
521
+ buttonText: "Continue with Microsoft"
522
+ }
523
+ }),
524
+ /**
525
+ * GitHub OAuth Provider
526
+ * https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
527
+ */
528
+ github: createSSOProvider({
529
+ id: "github",
530
+ name: "GitHub",
531
+ color: "#24292E",
532
+ icon: "github",
533
+ defaultScopes: ["read:user", "user:email"],
534
+ metadata: {
535
+ authDomain: "github.com",
536
+ buttonText: "Continue with GitHub"
537
+ }
538
+ }),
539
+ /**
540
+ * Okta OAuth Provider
541
+ * https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/
542
+ */
543
+ okta: createSSOProvider({
544
+ id: "okta",
545
+ name: "Okta",
546
+ color: "#007DC1",
547
+ icon: "okta",
548
+ defaultScopes: ["openid", "email", "profile"],
549
+ metadata: {
550
+ buttonText: "Continue with Okta"
551
+ }
552
+ }),
553
+ /**
554
+ * Apple Sign In Provider
555
+ * https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api
556
+ * Note: Apple works best with redirect flow on web
557
+ */
558
+ apple: {
559
+ ...createSSOProvider({
560
+ id: "apple",
561
+ name: "Apple",
562
+ color: "#000000",
563
+ icon: "apple",
564
+ defaultScopes: ["name", "email"],
565
+ metadata: {
566
+ authDomain: "appleid.apple.com",
567
+ buttonText: "Continue with Apple"
568
+ }
569
+ }),
570
+ supportsPopup: false,
571
+ // Apple prefers redirect on web
572
+ // Override popup to use redirect for better UX
573
+ async popup(options) {
574
+ return this.redirect(options);
575
+ }
576
+ },
577
+ /**
578
+ * Facebook OAuth Provider
579
+ * https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow
580
+ */
581
+ facebook: createSSOProvider({
582
+ id: "facebook",
583
+ name: "Facebook",
584
+ color: "#1877F2",
585
+ icon: "facebook",
586
+ defaultScopes: ["email", "public_profile"],
587
+ metadata: {
588
+ authDomain: "www.facebook.com",
589
+ buttonText: "Continue with Facebook"
590
+ }
591
+ })
592
+ };
593
+ const ssoProviders = Object.values(sso);
594
+ function getSSOProvider(provider) {
595
+ return sso[provider];
596
+ }
597
+ function getAllSSOProviders() {
598
+ return ssoProviders;
599
+ }
600
+ function isSupportedProvider(provider) {
601
+ return provider in sso;
602
+ }
603
+ async function handleOAuthCallback() {
604
+ if (typeof window === "undefined") {
605
+ return null;
606
+ }
607
+ const urlParams = new URLSearchParams(window.location.search);
608
+ const code = urlParams.get("code");
609
+ const state = urlParams.get("state");
610
+ if (!code || !state) {
611
+ return null;
612
+ }
613
+ const provider = sessionStorage.getItem(`oauth_provider:${state}`);
614
+ if (!provider || !isSupportedProvider(provider)) {
615
+ throw new Error("Unable to determine OAuth provider. State may have expired.");
616
+ }
617
+ return sso[provider].callback(code, state);
618
+ }
619
+
254
620
  var AuthState = /* @__PURE__ */ ((AuthState2) => {
255
621
  AuthState2["LOGIN"] = "login";
256
622
  AuthState2["LOGOUT"] = "logout";
@@ -291,7 +657,7 @@ function accountToUser(account) {
291
657
  metadata: account.entity.metadata
292
658
  };
293
659
  }
294
- const emailMethod = account.authentication_methods?.find(
660
+ const emailMethod = account.authentication_methods.find(
295
661
  (m) => m.type === "password" || m.type === "email_token"
296
662
  );
297
663
  return {
@@ -310,11 +676,10 @@ let authApi = null;
310
676
  let eventEmitter = null;
311
677
  const accountInfo = vue.ref(null);
312
678
  function initAuth({
313
- axios,
314
679
  baseURL
315
680
  }) {
316
681
  if (authApi === null) {
317
- authApi = new AuthApi(axios, baseURL);
682
+ authApi = new AuthApi(baseURL);
318
683
  }
319
684
  if (eventEmitter === null) {
320
685
  eventEmitter = new EventEmitter();
@@ -350,6 +715,29 @@ function useAuth() {
350
715
  }
351
716
  const api = authApi;
352
717
  const emitter = eventEmitter;
718
+ const authMethods = {
719
+ initiateSSO: async (params) => {
720
+ const { data } = await api.initiateSSO(params);
721
+ return data.authorization_url;
722
+ },
723
+ loginWithSSO: async (params) => {
724
+ const { data } = await api.ssoCallback(params);
725
+ if (data.success === true && data.requires_verification !== true) {
726
+ await checkAuth();
727
+ }
728
+ emitter.emit(AuthState.LOGIN);
729
+ return data;
730
+ },
731
+ linkSSOProvider: async (params) => {
732
+ await api.linkSSOProvider(params);
733
+ await checkAuth();
734
+ },
735
+ unlinkSSOProvider: async (provider) => {
736
+ await api.unlinkSSOProvider(provider);
737
+ await checkAuth();
738
+ }
739
+ };
740
+ setAuthContext(authMethods);
353
741
  const user = vue.computed(() => accountToUser(accountInfo.value));
354
742
  const getFullName = () => {
355
743
  return user.value?.name ?? "";
@@ -488,11 +876,33 @@ function useAuth() {
488
876
  }
489
877
  await api.revokeAllSessions(id);
490
878
  }
879
+ async function initiateSSO(params) {
880
+ const { data } = await api.initiateSSO(params);
881
+ return data.authorization_url;
882
+ }
883
+ async function loginWithSSO(params) {
884
+ const { data } = await api.ssoCallback(params);
885
+ if (data.success === true && data.requires_verification !== true) {
886
+ await checkAuth();
887
+ }
888
+ emitter.emit(AuthState.LOGIN);
889
+ return data;
890
+ }
891
+ async function linkSSOProvider(params) {
892
+ await api.linkSSOProvider(params);
893
+ await checkAuth();
894
+ }
895
+ async function unlinkSSOProvider(provider) {
896
+ await api.unlinkSSOProvider(provider);
897
+ await checkAuth();
898
+ }
491
899
  return {
492
900
  // Primary State (use this!)
493
901
  user,
494
902
  // Full account info (for advanced use cases)
495
903
  accountInfo,
904
+ // SSO Providers (ready to use!)
905
+ sso,
496
906
  // Getters
497
907
  getFullName,
498
908
  getIsLoggedIn,
@@ -507,6 +917,11 @@ function useAuth() {
507
917
  signup,
508
918
  checkAuth,
509
919
  refreshSession,
920
+ // SSO Authentication (lower-level - prefer using sso.google.redirect())
921
+ initiateSSO,
922
+ loginWithSSO,
923
+ linkSSOProvider,
924
+ unlinkSSOProvider,
510
925
  // Profile Actions
511
926
  updateProfile,
512
927
  deleteCurrentUser,
@@ -531,6 +946,18 @@ function useAuth() {
531
946
 
532
947
  exports.AuthApi = AuthApi;
533
948
  exports.AuthState = AuthState;
949
+ exports.PopupBlockedError = PopupBlockedError;
950
+ exports.PopupClosedError = PopupClosedError;
951
+ exports.PopupTimeoutError = PopupTimeoutError;
952
+ exports.SSOError = SSOError;
953
+ exports.StateMismatchError = StateMismatchError;
534
954
  exports.accountToUser = accountToUser;
955
+ exports.getAllSSOProviders = getAllSSOProviders;
956
+ exports.getSSOProvider = getSSOProvider;
957
+ exports.handleOAuthCallback = handleOAuthCallback;
535
958
  exports.initAuth = initAuth;
959
+ exports.isSupportedProvider = isSupportedProvider;
960
+ exports.setAuthContext = setAuthContext;
961
+ exports.sso = sso;
962
+ exports.ssoProviders = ssoProviders;
536
963
  exports.useAuth = useAuth;