@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 +1 -3
- package/README.md +2 -0
- package/dist/auth/route-guard.js +8 -0
- package/dist/client/BridgeBootstrap.d.ts +2 -0
- package/dist/client/BridgeBootstrap.js +54 -3
- package/dist/client/components/FeatureFlag.svelte +22 -6
- package/dist/client/components/FeatureFlag.svelte.d.ts +6 -1
- package/dist/client/components/team/TeamManagement.svelte +45 -37
- package/dist/client/components/team/TeamManagement.svelte.d.ts +2 -17
- package/dist/client/stores/config.store.js +16 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/shared/feature-flag.js +7 -4
- package/dist/shared/profile.js +9 -6
- package/dist/shared/services/auth.service.js +34 -2
- package/dist/shared/services/plan.service.d.ts +7 -0
- package/dist/shared/services/plan.service.js +90 -0
- package/dist/shared/types/config.d.ts +7 -7
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2025
|
|
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
|
+
[](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
|
|
package/dist/auth/route-guard.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
+
type FlagRenderArgs = { enabled: boolean; rawEnabled: boolean };
|
|
7
7
|
|
|
8
|
-
let
|
|
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
|
|
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
|
|
18
|
-
{#if
|
|
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
|
-
|
|
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
|
|
8
|
-
let error
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
logger.error(
|
|
45
|
-
throw
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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://
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
13
|
+
const cloudViewsUrl = getConfig().cloudViewsUrl;
|
|
13
14
|
if (!appId)
|
|
14
15
|
return;
|
|
15
|
-
|
|
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
|
|
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 = `${
|
|
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',
|
package/dist/shared/profile.js
CHANGED
|
@@ -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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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://
|
|
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://
|
|
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.
|
|
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://
|
|
15
|
+
"homepage": "https://thebridge.dev",
|
|
16
16
|
"scripts": {
|
|
17
17
|
"dev": "vite dev",
|
|
18
18
|
"build": "bun run prepack",
|