@nebulr-group/bridge-svelte 0.1.0 → 0.2.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 thebridgedev
3
+ Copyright (c) 2025 Nebulr AB
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -19,5 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
23
-
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
 
2
2
  ## @nebulr-group/bridge-svelte
3
3
 
4
+ [![MadeWithSvelte.com shield](https://madewithsvelte.com/storage/repo-shields/5996-shield.svg)](https://madewithsvelte.com/p/the-bridge/shield-link)
5
+
4
6
 
5
7
  Bridge Svelte library. Add Bridge auth, feature flags, and payments to your SvelteKit 2 + Svelte 5 apps.
6
8
 
@@ -57,7 +57,15 @@ export function createRouteGuard() {
57
57
  }
58
58
  function shouldRedirectToLogin(pathname) {
59
59
  const isProtected = isProtectedRoute(pathname);
60
+ const tokens = auth.getToken();
60
61
  const authenticated = get(isAuthenticated);
62
+ logger.debug(`[route-guard] shouldRedirectToLogin check`, {
63
+ pathname,
64
+ isProtected,
65
+ hasTokens: !!tokens?.accessToken,
66
+ authenticated,
67
+ tokenStoreValue: tokens
68
+ });
61
69
  if (isProtectedRoute(pathname) && !get(isAuthenticated)) {
62
70
  logger.debug(`[route-guard] path ${pathname} is protected and user is not authenticated`);
63
71
  return true;
@@ -1,3 +1,5 @@
1
1
  import type { RouteGuardConfig } from '../auth/route-guard.js';
2
2
  import type { BridgeConfig } from '../shared/types/config.js';
3
3
  export declare function bridgeBootstrap(url: URL, config: BridgeConfig | string, routeConfig?: RouteGuardConfig): Promise<void>;
4
+ export declare const bridgeReady: import("svelte/store").Writable<boolean>;
5
+ export declare function waitForBridge(): Promise<void>;
@@ -1,11 +1,27 @@
1
1
  // src/lib/bridge/bootstrap.ts
2
- import { redirect } from '@sveltejs/kit';
2
+ import { isRedirect, redirect } from '@sveltejs/kit';
3
+ import { get, writable } from 'svelte/store';
3
4
  import { createRouteGuard } from '../auth/route-guard.js';
4
5
  import { featureFlags } from '../shared/feature-flag.js';
5
6
  import { logger } from '../shared/logger.js';
6
7
  import { auth, maybeRefreshNow } from '../shared/services/auth.service.js';
7
8
  import { bridgeConfig } from './stores/config.store.js';
9
+ const bridgeReadyStore = writable(false);
10
+ let resolveReady = null;
11
+ const bridgeReadyPromise = new Promise((resolve) => {
12
+ resolveReady = resolve;
13
+ });
14
+ function markBridgeReady() {
15
+ if (get(bridgeReadyStore))
16
+ return;
17
+ bridgeReadyStore.set(true);
18
+ resolveReady?.();
19
+ }
8
20
  export async function bridgeBootstrap(url, config, routeConfig = { rules: [], defaultAccess: 'protected' }) {
21
+ // If we've already completed bootstrap once, short-circuit
22
+ if (get(bridgeReadyStore)) {
23
+ return;
24
+ }
9
25
  const finalConfig = typeof config === 'string' ? { appId: config } : config;
10
26
  // 1. Initialize configuration (synchronously)
11
27
  bridgeConfig.initConfig(finalConfig, routeConfig);
@@ -16,19 +32,41 @@ export async function bridgeBootstrap(url, config, routeConfig = { rules: [], de
16
32
  logger.debug('[bridgeBootstrap] callback route detected, skipping bootstrap flow');
17
33
  const { handleCallback } = auth;
18
34
  const code = url.searchParams.get('code');
35
+ logger.debug('[bridgeBootstrap] callback code check', { hasCode: !!code, pathname: url.pathname });
19
36
  if (code) {
20
37
  try {
38
+ logger.debug('[bridgeBootstrap] calling handleCallback');
21
39
  await handleCallback(code);
40
+ // Verify tokens before redirect
41
+ const tokensAfterCallback = auth.getToken();
42
+ const isAuthAfterCallback = !!tokensAfterCallback?.accessToken;
43
+ logger.debug('[bridgeBootstrap] after handleCallback', {
44
+ hasTokens: !!tokensAfterCallback?.accessToken,
45
+ isAuthenticated: isAuthAfterCallback
46
+ });
22
47
  // Redirect bridge user to bridge home page after bridge callback is handled
23
- redirect(303, '/');
48
+ // Preserve payment status if present (from post-payment redirect)
49
+ const payment = url.searchParams.get('payment');
50
+ const redirectUrl = payment ? `/?payment=${payment}` : '/';
51
+ logger.debug('[bridgeBootstrap] redirecting to', { redirectUrl, payment });
52
+ redirect(303, redirectUrl);
24
53
  }
25
54
  catch (err) {
26
- logger.error('Auth callback error:', err);
55
+ if (isRedirect(err)) {
56
+ throw err; // Re-throw redirect so SvelteKit can handle it
57
+ }
58
+ logger.error('[bridgeBootstrap] Auth callback error:', err);
27
59
  }
28
60
  }
61
+ else {
62
+ logger.warn('[bridgeBootstrap] callback route detected but no code parameter');
63
+ }
29
64
  }
30
65
  }
31
66
  catch (e) {
67
+ if (isRedirect(e)) {
68
+ throw e; // Re-throw redirect so SvelteKit can handle it
69
+ }
32
70
  logger.warn('[bridgeBootstrap] failed parsing callbackUrl', e);
33
71
  }
34
72
  // 2. Ensure tokens are fresh if needed
@@ -37,6 +75,14 @@ export async function bridgeBootstrap(url, config, routeConfig = { rules: [], de
37
75
  await featureFlags.refresh();
38
76
  // 4. Handle route guarding and redirects
39
77
  const guard = createRouteGuard();
78
+ // Check auth state before route guard decision
79
+ const currentTokens = auth.getToken();
80
+ const currentAuth = !!currentTokens?.accessToken;
81
+ logger.debug('[bridgeBootstrap] before route guard check', {
82
+ pathname: url.pathname,
83
+ hasTokens: !!currentTokens?.accessToken,
84
+ isAuthenticated: currentAuth
85
+ });
40
86
  const decision = await guard.getNavigationDecision(url.pathname);
41
87
  logger.debug('[bridgeBootstrap] navigation decision', decision);
42
88
  if (decision.type === 'login') {
@@ -46,4 +92,9 @@ export async function bridgeBootstrap(url, config, routeConfig = { rules: [], de
46
92
  redirect(303, decision.to);
47
93
  }
48
94
  logger.debug('[bridgeBootstrap] in bridge end');
95
+ markBridgeReady();
96
+ }
97
+ export const bridgeReady = bridgeReadyStore;
98
+ export function waitForBridge() {
99
+ return bridgeReadyPromise;
49
100
  }
@@ -3,19 +3,35 @@
3
3
  import { onMount } from 'svelte';
4
4
  import { isFeatureEnabled } from '../../shared/feature-flag.js';
5
5
 
6
- let { flagName, forceLive = false, negate = false, children }: { flagName: string; forceLive?: boolean; negate?: boolean; children?: Snippet } = $props();
6
+ type FlagRenderArgs = { enabled: boolean; rawEnabled: boolean };
7
7
 
8
- let enabled = $state(false);
8
+ let {
9
+ flagName,
10
+ forceLive = false,
11
+ negate = false,
12
+ renderWhenDisabled = false,
13
+ children
14
+ }: {
15
+ flagName: string;
16
+ forceLive?: boolean;
17
+ negate?: boolean;
18
+ renderWhenDisabled?: boolean;
19
+ children?: Snippet<[FlagRenderArgs]>;
20
+ } = $props();
9
21
 
10
- let shouldRender = $derived(() => negate ? !enabled : enabled);
22
+ let enabled = $state(false);
23
+ let rawEnabled = $derived(enabled);
24
+ let effectiveEnabled = $derived(negate ? !enabled : enabled);
11
25
 
12
26
  onMount(async () => {
13
27
  enabled = await isFeatureEnabled(flagName, forceLive);
14
28
  });
15
29
  </script>
16
30
 
17
- {#if shouldRender}
18
- {#if children}
19
- {@render children()}
31
+ {#if children}
32
+ {#if renderWhenDisabled}
33
+ {@render children({ enabled: effectiveEnabled, rawEnabled })}
34
+ {:else if effectiveEnabled}
35
+ {@render children({ enabled: true, rawEnabled })}
20
36
  {/if}
21
37
  {/if}
@@ -1,9 +1,14 @@
1
1
  import type { Snippet } from 'svelte';
2
+ type FlagRenderArgs = {
3
+ enabled: boolean;
4
+ rawEnabled: boolean;
5
+ };
2
6
  type $$ComponentProps = {
3
7
  flagName: string;
4
8
  forceLive?: boolean;
5
9
  negate?: boolean;
6
- children?: Snippet;
10
+ renderWhenDisabled?: boolean;
11
+ children?: Snippet<[FlagRenderArgs]>;
7
12
  };
8
13
  declare const FeatureFlag: import("svelte").Component<$$ComponentProps, {}, "">;
9
14
  type FeatureFlag = ReturnType<typeof FeatureFlag>;
@@ -4,67 +4,75 @@
4
4
  import { auth } from '../../../shared/services/auth.service.js';
5
5
  import { getConfig } from '../../stores/config.store.js';
6
6
 
7
- let iframeUrl: string | null = null;
8
- let error: string | null = null;
9
- let isLoading = true;
7
+ let iframeUrl = $state<string | null>(null);
8
+ let error = $state<string | null>(null);
9
+ let isLoading = $state(true);
10
10
 
11
11
  async function getHandoverCode(accessToken: string) {
12
12
  const config = getConfig();
13
13
  const authBaseUrl = config.authBaseUrl;
14
14
  const appId = config.appId;
15
15
 
16
- try {
17
- const response = await fetch(
18
- `${authBaseUrl}/handover/code/${appId}`,
19
- {
20
- method: 'POST',
21
- headers: {
22
- 'Content-Type': 'application/json',
23
- },
24
- body: JSON.stringify({ accessToken }),
25
- }
26
- );
27
-
28
- if (!response.ok) {
29
- const errorText = await response.text();
30
- logger.error(`Failed to get handover code: ${response.status} ${response.statusText}`, errorText);
31
- throw new Error(`Failed to get handover code: ${response.statusText}`);
32
- }
16
+ logger.debug('[TeamManagement] getHandoverCode called', { authBaseUrl, appId });
33
17
 
34
- const data = await response.json();
35
- if (!data.code) {
36
- logger.error('No handover code in response:', data);
37
- throw new Error('Failed to get handover code: No code in response');
18
+ const response = await fetch(
19
+ `${authBaseUrl}/handover/code/${appId}`,
20
+ {
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ },
25
+ body: JSON.stringify({
26
+ accessToken,
27
+ redirectUri: config.callbackUrl || `${window.location.origin}/auth/oauth-callback`
28
+ }),
38
29
  }
30
+ );
39
31
 
40
- // Create bridge team management URL with bridge handover code
41
- const baseUrl = config.teamManagementUrl;
42
- return `${baseUrl}?code=${data.code}`;
43
- } catch (err) {
44
- logger.error('Error getting handover code:', err);
45
- throw err;
32
+ logger.debug('[TeamManagement] handover response status:', response.status);
33
+
34
+ if (!response.ok) {
35
+ const errorText = await response.text();
36
+ logger.error(`[TeamManagement] Failed to get handover code: ${response.status} ${response.statusText}`, errorText);
37
+ throw new Error(`Failed to get handover code: ${response.status} - ${errorText}`);
46
38
  }
39
+
40
+ const data = await response.json();
41
+ logger.debug('[TeamManagement] handover response data:', data);
42
+
43
+ if (!data.code) {
44
+ logger.error('[TeamManagement] No handover code in response:', data);
45
+ throw new Error('Failed to get handover code: No code in response');
46
+ }
47
+
48
+ // Use the teamManagementUrl which points to bridge-api's user-management-portal endpoint
49
+ // bridge-api handles the redirect to cloud-views with proper handover
50
+ const baseUrl = config.teamManagementUrl;
51
+ const url = `${baseUrl}?code=${data.code}`;
52
+ logger.debug('[TeamManagement] constructed iframe URL:', url);
53
+ return url;
47
54
  }
48
55
 
49
56
  onMount(async () => {
50
- logger.debug('TeamManagement onMount');
57
+ logger.debug('[TeamManagement] onMount started');
51
58
  try {
52
- if (!auth.isAuthenticated) {
53
- logger.debug('TeamManagement onMount: User is not authenticated');
54
- throw new Error('User must be authenticated to access team management');
55
- }
59
+ // Get token directly - auth.getToken() returns the current token synchronously
56
60
  const token = auth.getToken();
57
- // Get bridge access token from your auth store
61
+ logger.debug('[TeamManagement] token check:', { hasToken: !!token, hasAccessToken: !!token?.accessToken });
62
+
58
63
  const accessToken = token?.accessToken;
59
64
  if (!accessToken) {
60
- throw new Error('No access token available');
65
+ throw new Error('No access token available. Please log in first.');
61
66
  }
62
67
 
63
68
  iframeUrl = await getHandoverCode(accessToken);
69
+ logger.debug('[TeamManagement] iframe URL set:', iframeUrl);
64
70
  } catch (err) {
71
+ logger.error('[TeamManagement] Error:', err);
65
72
  error = err instanceof Error ? err.message : 'Failed to load team management';
66
73
  } finally {
67
74
  isLoading = false;
75
+ logger.debug('[TeamManagement] loading complete, isLoading:', isLoading);
68
76
  }
69
77
  });
70
78
  </script>
@@ -1,18 +1,3 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const TeamManagement: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type TeamManagement = InstanceType<typeof TeamManagement>;
1
+ declare const TeamManagement: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type TeamManagement = ReturnType<typeof TeamManagement>;
18
3
  export default TeamManagement;
@@ -2,9 +2,9 @@
2
2
  import { derived, writable } from 'svelte/store';
3
3
  import { logger } from '../../shared/logger.js';
4
4
  const DEFAULT_CONFIG = {
5
- authBaseUrl: 'https://auth.nblocks.cloud',
6
- backendlessBaseUrl: 'https://backendless.nblocks.cloud',
7
- teamManagementUrl: 'https://backendless.nblocks.cloud/user-management-portal/users',
5
+ authBaseUrl: 'https://api.thebridge.dev/auth',
6
+ teamManagementUrl: 'https://api.thebridge.dev/cloud-views/user-management-portal/users',
7
+ cloudViewsUrl: 'https://api.thebridge.dev/cloud-views',
8
8
  defaultRedirectRoute: '/',
9
9
  loginRoute: '/login',
10
10
  debug: false
@@ -20,10 +20,23 @@ export const bridgeConfig = {
20
20
  if (!config?.appId) {
21
21
  throw new Error('Bridge appId is required but was not provided in bridge configuration.');
22
22
  }
23
+ // Default to /auth/oauth-callback if not provided
24
+ const DEFAULT_CALLBACK_PATH = '/auth/oauth-callback';
25
+ const defaultCallback = typeof window !== 'undefined'
26
+ ? `${window.location.origin}${DEFAULT_CALLBACK_PATH}`
27
+ : undefined;
23
28
  const merged = {
24
29
  ...DEFAULT_CONFIG,
30
+ callbackUrl: defaultCallback,
25
31
  ...config
26
32
  };
33
+ // Use user provided callbackUrl if present, otherwise use default
34
+ if (config.callbackUrl) {
35
+ merged.callbackUrl = config.callbackUrl;
36
+ }
37
+ else if (!merged.callbackUrl && defaultCallback) {
38
+ merged.callbackUrl = defaultCallback;
39
+ }
27
40
  // Set bridge full config and mark it as loaded
28
41
  set({
29
42
  config: merged,
package/dist/index.d.ts CHANGED
@@ -9,5 +9,6 @@ export * from './shared/feature-flag.js';
9
9
  export * from './auth/route-guard.js';
10
10
  export * from './shared/profile.js';
11
11
  export * from './shared/services/auth.service.js';
12
+ export * from './shared/services/plan.service.js';
12
13
  export * from './shared/types/config.js';
13
14
  export { logger } from './shared/logger.js';
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ export * from './auth/route-guard.js';
14
14
  // Types
15
15
  export * from './shared/profile.js'; // If this exists
16
16
  export * from './shared/services/auth.service.js';
17
+ export * from './shared/services/plan.service.js';
17
18
  export * from './shared/types/config.js';
18
19
  // Logger
19
20
  export { logger } from './shared/logger.js';
@@ -8,11 +8,14 @@ let lastFetchTime = 0;
8
8
  export async function loadFeatureFlags() {
9
9
  const tokens = get(auth.token);
10
10
  const appId = tokens?.appId ?? getConfig().appId;
11
+ // We should prefer the token from the store as it is more likely to be up to date
11
12
  const accessToken = tokens?.accessToken;
12
- const backendlessBaseUrl = getConfig().backendlessBaseUrl;
13
+ const cloudViewsUrl = getConfig().cloudViewsUrl;
13
14
  if (!appId)
14
15
  return;
15
- const url = `${backendlessBaseUrl}/flags/bulkEvaluate/${appId}`;
16
+ // Log the token we are using to fetch flags (partial for security)
17
+ logger.debug(`[feature-flag] fetching flags with token: ${accessToken ? accessToken.substring(0, 10) + '...' : 'none'}`);
18
+ const url = `${cloudViewsUrl}/flags/bulkEvaluate/${appId}`;
16
19
  const body = accessToken ? { accessToken } : {};
17
20
  const res = await fetch(url, {
18
21
  method: 'POST',
@@ -33,7 +36,7 @@ export async function isFeatureEnabled(flag, forceLive = false) {
33
36
  const tokens = get(auth.token);
34
37
  const appId = tokens?.appId ?? getConfig().appId;
35
38
  const accessToken = tokens?.accessToken;
36
- const backendlessBaseUrl = getConfig().backendlessBaseUrl;
39
+ const cloudViewsUrl = getConfig().cloudViewsUrl;
37
40
  if (!appId)
38
41
  return false;
39
42
  logger.debug(`[feature-flag] is flag:${flag}: enabled: ${get(cachedFlags)[flag]}`);
@@ -43,7 +46,7 @@ export async function isFeatureEnabled(flag, forceLive = false) {
43
46
  if (!forceLive)
44
47
  await loadFeatureFlags();
45
48
  if (forceLive) {
46
- const url = `${backendlessBaseUrl}/flags/evaluate/${appId}/${flag}`;
49
+ const url = `${cloudViewsUrl}/flags/evaluate/${appId}/${flag}`;
47
50
  const body = accessToken ? { accessToken } : {};
48
51
  const res = await fetch(url, {
49
52
  method: 'POST',
@@ -1,6 +1,7 @@
1
1
  // src/lib/auth/profile.ts
2
2
  import { createRemoteJWKSet, errors as joseErrors, jwtVerify } from 'jose';
3
3
  import { derived, get, writable } from 'svelte/store';
4
+ import { waitForBridge } from '../client/BridgeBootstrap.js';
4
5
  import { getConfig } from '../client/stores/config.store.js';
5
6
  import { auth } from './services/auth.service.js';
6
7
  const profile = writable(undefined);
@@ -35,8 +36,8 @@ function ensureVerifier() {
35
36
  const config = getConfig();
36
37
  if (!jwks || expectedIssuer !== config.authBaseUrl || expectedAudience !== config.appId) {
37
38
  jwks = createRemoteJWKSet(new URL(`${config.authBaseUrl}/.well-known/jwks.json`));
38
- expectedIssuer = config.authBaseUrl;
39
- expectedAudience = config.appId;
39
+ expectedIssuer = config.authBaseUrl ?? null;
40
+ expectedAudience = config.appId ?? null;
40
41
  }
41
42
  }
42
43
  async function verifyToken(idToken) {
@@ -77,10 +78,12 @@ async function updateProfile(idToken) {
77
78
  if (result)
78
79
  error.set(null);
79
80
  }
80
- // 🔁 Auto-sync profile with token
81
- auth.token.subscribe(($token) => {
82
- const idToken = $token?.idToken || null;
83
- updateProfile(idToken);
81
+ // 🔁 Auto-sync profile with token after bridge is ready
82
+ waitForBridge().then(() => {
83
+ auth.token.subscribe(($token) => {
84
+ const idToken = $token?.idToken || null;
85
+ updateProfile(idToken);
86
+ });
84
87
  });
85
88
  const isOnboarded = derived(profile, ($profile) => $profile?.onboarded ?? false);
86
89
  const hasMultiTenantAccess = derived(profile, ($profile) => $profile?.multiTenantAccess ?? false);
@@ -25,9 +25,20 @@ if (browser) {
25
25
  }
26
26
  // --- API ---
27
27
  function setTokens(tokens) {
28
+ logger.debug('[auth] setTokens called', {
29
+ hasAccessToken: !!tokens?.accessToken,
30
+ hasRefreshToken: !!tokens?.refreshToken,
31
+ hasIdToken: !!tokens?.idToken
32
+ });
28
33
  tokenStore.set(tokens);
29
- if (browser)
34
+ if (browser) {
30
35
  localStorage.setItem(TOKEN_KEY, JSON.stringify(tokens));
36
+ logger.debug('[auth] tokens stored in localStorage');
37
+ }
38
+ // Verify tokens are set
39
+ const current = get(tokenStore);
40
+ const isAuth = !!current?.accessToken;
41
+ logger.debug('[auth] tokens set, isAuthenticated check:', isAuth);
31
42
  scheduleTokenRefresh();
32
43
  }
33
44
  function clearTokens() {
@@ -36,6 +47,16 @@ function clearTokens() {
36
47
  localStorage.removeItem(TOKEN_KEY);
37
48
  stopAutoRefresh();
38
49
  }
50
+ async function logout() {
51
+ // Clear local storage tokens first
52
+ clearTokens();
53
+ // Redirect to backend logout endpoint to clear all authentication cookies
54
+ if (browser) {
55
+ const config = getConfig();
56
+ const logoutUrl = `${config.authBaseUrl}/url/logout/${config.appId}`;
57
+ window.location.href = logoutUrl;
58
+ }
59
+ }
39
60
  async function login(options = {}) {
40
61
  const loginUrl = createLoginUrl(options);
41
62
  if (browser) {
@@ -52,8 +73,10 @@ function createLoginUrl(options = {}) {
52
73
  return redirectUri ? `${base}?cv_env=bridge&redirect_uri=${encodeURIComponent(redirectUri)}` : base;
53
74
  }
54
75
  async function handleCallback(code) {
76
+ logger.debug('[auth] handleCallback called with code:', code ? 'present' : 'missing');
55
77
  const config = getConfig();
56
78
  const url = `${config.authBaseUrl}/token/code/${config.appId}`;
79
+ logger.debug('[auth] exchanging code for tokens', { url, appId: config.appId, callbackUrl: config.callbackUrl });
57
80
  const response = await fetch(url, {
58
81
  method: 'POST',
59
82
  headers: { 'Content-Type': 'application/json' },
@@ -77,14 +100,23 @@ async function handleCallback(code) {
77
100
  // If parsing JSON fails, use bridge generic message or response status text
78
101
  errorMessage = `Failed to exchange code for tokens: ${response.statusText || 'Unknown error'}`;
79
102
  }
103
+ logger.error('[auth] handleCallback failed', errorMessage);
80
104
  throw new Error(errorMessage);
81
105
  }
82
106
  const data = await response.json();
107
+ logger.debug('[auth] token exchange successful, setting tokens');
83
108
  setTokens({
84
109
  accessToken: data.access_token,
85
110
  refreshToken: data.refresh_token,
86
111
  idToken: data.id_token
87
112
  });
113
+ // Double-check after setting
114
+ const finalCheck = get(tokenStore);
115
+ const finalAuth = get(isAuthenticated);
116
+ logger.debug('[auth] handleCallback completed', {
117
+ tokensSet: !!finalCheck?.accessToken,
118
+ isAuthenticated: finalAuth
119
+ });
88
120
  }
89
121
  async function refreshToken(refreshToken) {
90
122
  const config = getConfig();
@@ -200,7 +232,7 @@ export const auth = {
200
232
  isLoading,
201
233
  error,
202
234
  login,
203
- logout: clearTokens,
235
+ logout,
204
236
  handleCallback,
205
237
  refreshToken,
206
238
  createLoginUrl,
@@ -0,0 +1,7 @@
1
+ declare function setSecurityCookie(): Promise<void>;
2
+ declare function redirectToPlanSelection(): Promise<void>;
3
+ export declare const planService: {
4
+ redirectToPlanSelection: typeof redirectToPlanSelection;
5
+ setSecurityCookie: typeof setSecurityCookie;
6
+ };
7
+ export { };
@@ -0,0 +1,90 @@
1
+ // src/lib/shared/services/plan.service.ts
2
+ import { browser } from '$app/environment';
3
+ import { getConfig } from '../../client/stores/config.store.js';
4
+ import { logger } from '../logger.js';
5
+ import { auth } from './auth.service.js';
6
+ /**
7
+ * Sets the security cookie required for Bridge redirects
8
+ */
9
+ async function setSecurityCookie() {
10
+ if (!browser) {
11
+ throw new Error('Plan redirects are only available in the browser');
12
+ }
13
+ const config = getConfig();
14
+ const cloudViewsUrl = config.cloudViewsUrl;
15
+ if (!cloudViewsUrl) {
16
+ throw new Error('cloudViewsUrl must be configured');
17
+ }
18
+ const tokenSet = auth.getToken();
19
+ const token = tokenSet?.accessToken;
20
+ if (!token) {
21
+ throw new Error('No access token available. Please log in first.');
22
+ }
23
+ try {
24
+ const response = await fetch(`${cloudViewsUrl}/security/setCookie`, {
25
+ method: 'POST',
26
+ credentials: 'include',
27
+ headers: {
28
+ 'Authorization': `Bearer ${token}`,
29
+ 'Content-Type': 'application/json'
30
+ }
31
+ });
32
+ if (!response.ok) {
33
+ throw new Error(`Failed to set security cookie: ${response.statusText}`);
34
+ }
35
+ logger.debug('[plan] Security cookie set successfully');
36
+ }
37
+ catch (error) {
38
+ logger.error('[plan] Failed to set security cookie', error);
39
+ throw error;
40
+ }
41
+ }
42
+ /**
43
+ * Redirects to Bridge's tenant plan selection page
44
+ * This allows users to view available plans and upgrade/downgrade
45
+ * USES the handover protocol for secure cross-domain authentication
46
+ */
47
+ async function redirectToPlanSelection() {
48
+ if (!browser) {
49
+ throw new Error('Plan redirects are only available in the browser');
50
+ }
51
+ const config = getConfig();
52
+ const authBaseUrl = config.authBaseUrl;
53
+ const cloudViewsUrl = config.cloudViewsUrl;
54
+ try {
55
+ const tokenSet = auth.getToken();
56
+ const accessToken = tokenSet?.accessToken;
57
+ if (!accessToken) {
58
+ throw new Error('No access token available. Please log in first.');
59
+ }
60
+ // 1. Get the handover code from the Auth API
61
+ // We call the auth-api to exchange our access token for a short-lived handover code
62
+ const handoverResponse = await fetch(`${authBaseUrl}/handover/code/${config.appId}`, {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ },
67
+ body: JSON.stringify({ accessToken }),
68
+ });
69
+ if (!handoverResponse.ok) {
70
+ throw new Error(`Failed to get handover code: ${handoverResponse.statusText}`);
71
+ }
72
+ const { code } = await handoverResponse.json();
73
+ if (!code) {
74
+ throw new Error('Handover response did not contain a code');
75
+ }
76
+ // 2. Redirect to the Bridge API entry point which will land us on the plan selection page in cloud-views
77
+ // This entry point will handle the final redirect to the correct cloud-views domain and flow
78
+ const redirectUrl = `${cloudViewsUrl}/subscription-portal/selectPlan?code=${code}`;
79
+ logger.debug('[plan] Redirecting to plan selection via handover', redirectUrl);
80
+ window.location.href = redirectUrl;
81
+ }
82
+ catch (error) {
83
+ logger.error('[plan] Failed to redirect to plan selection', error);
84
+ throw error;
85
+ }
86
+ }
87
+ export const planService = {
88
+ redirectToPlanSelection,
89
+ setSecurityCookie
90
+ };
@@ -11,14 +11,9 @@ export interface BridgeConfig {
11
11
  callbackUrl?: string;
12
12
  /**
13
13
  * The base URL for Bridge auth services
14
- * @default 'https://auth.nblocks.cloud'
14
+ * @default 'https://api.thebridge.dev/auth'
15
15
  */
16
16
  authBaseUrl?: string;
17
- /**
18
- * The base URL for Bridge backendless services
19
- * @default 'https://backendless.nblocks.cloud'
20
- */
21
- backendlessBaseUrl?: string;
22
17
  /**
23
18
  * Route to redirect to after login
24
19
  * @default '/'
@@ -31,9 +26,14 @@ export interface BridgeConfig {
31
26
  loginRoute?: string;
32
27
  /**
33
28
  * URL for bridge team management portal
34
- * @default 'https://backendless.nblocks.cloud'
29
+ * @default 'https://api.thebridge.dev/cloud-views/user-management-portal/users'
35
30
  */
36
31
  teamManagementUrl?: string;
32
+ /**
33
+ * Base URL for bridge cloud-views service (for plan selection, payments, feature flags, etc.)
34
+ * @default 'https://api.thebridge.dev/cloud-views'
35
+ */
36
+ cloudViewsUrl?: string;
37
37
  /**
38
38
  * Debug mode
39
39
  * @default false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebulr-group/bridge-svelte",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
 
5
5
  "description": "Bridge Svelte library, This library helps you to add bridge authentication and feature flags, and payments to your svelte application.",
6
6
  "author": "Iman Pouya",
@@ -12,7 +12,7 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/thebridgedev/bridge-svelte/issues"
14
14
  },
15
- "homepage": "https://github.com/thebridgedev/bridge-svelte#readme",
15
+ "homepage": "https://thebridge.dev",
16
16
  "scripts": {
17
17
  "dev": "vite dev",
18
18
  "build": "bun run prepack",