@nebulr-group/bridge-svelte 0.4.0-beta.2 → 0.4.0-beta.4

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.
@@ -5,7 +5,6 @@ import { createRouteGuard } from '../auth/route-guard.js';
5
5
  import { getBridgeAuth, bridgeReadyStore, markReady, waitForBridge as _waitForBridge, } from '../core/bridge-instance.js';
6
6
  import { installBridgeAuthFetch } from '../core/bridge-runtime.js';
7
7
  import { useBridge } from '@nebulr-group/bridge-auth-core';
8
- import { featureFlags } from '../shared/feature-flag.js';
9
8
  import { logger } from '../shared/logger.js';
10
9
  import { bridgeConfig, getConfig } from './stores/config.store.js';
11
10
  export async function bridgeBootstrap(url, config, routeConfig = { rules: [], defaultAccess: 'protected' }, kitFetch) {
@@ -158,11 +157,17 @@ export async function bridgeBootstrap(url, config, routeConfig = { rules: [], de
158
157
  catch {
159
158
  // Non-fatal — gate fails open; the notice hydrates lazily on first render.
160
159
  }
161
- // 3. Fire flag fetch without awaiting so GQL queries can start in parallel.
162
- // The flagsReady promise is passed to the guard and returned to callers
163
- // so routes that need flags can still await before rendering.
164
- // Swallow errors feature flag failures should never crash the bootstrap.
165
- const flagsReady = featureFlags.refresh().catch((err) => {
160
+ // 3. Warm the route-guard's flag cache without awaiting so GQL queries can
161
+ // start in parallel. `loadFeatureFlags()` populates the same auth-core
162
+ // FeatureFlagService the route guard evaluates `featureFlag` rules
163
+ // against; the flagsReady promise is passed to the guard and returned to
164
+ // callers so routes that gate on flags can await before rendering.
165
+ // (Component/programmatic flag reads use FF 2.0 `useFlag` / <FeatureFlag>.)
166
+ // Errors are logged, never thrown — flag failures must not crash bootstrap.
167
+ const flagsReady = getBridgeAuth()
168
+ .loadFeatureFlags()
169
+ .then(() => { })
170
+ .catch((err) => {
166
171
  logger.warn('[bridgeBootstrap] Feature flags failed to load:', err);
167
172
  });
168
173
  // 4. Handle route guarding and redirects
@@ -39,8 +39,6 @@ export declare const isOnboarded: Readable<boolean>;
39
39
  export declare const hasMultiTenantAccess: Readable<boolean>;
40
40
  /** Tenant users for multi-tenant selection */
41
41
  export declare const tenantUsersStore: Readable<TenantUser[]>;
42
- /** Feature flags map */
43
- export declare const flagsStore: Readable<Record<string, boolean>>;
44
42
  /** Bridge ready state */
45
43
  export declare const bridgeReadyStore: Readable<boolean>;
46
44
  /** App-level config (SSO providers, feature flags, etc.) — loaded anonymously on init */
@@ -56,6 +54,5 @@ export declare const subscriptionStore: Readable<SubscriptionState>;
56
54
  export declare function loadSubscription(): Promise<void>;
57
55
  /** Lazy proxy to the BridgeAuth singleton — call methods directly: `auth.getToken()`, `auth.logout()`, etc. */
58
56
  export declare const auth: BridgeAuth;
59
- export declare const _flagsWritable: Writable<Record<string, boolean>>;
60
57
  export declare const _profileWritable: Writable<Profile | null | undefined>;
61
58
  export declare const _errorWritable: Writable<string | null>;
@@ -14,7 +14,6 @@ let _instance = null;
14
14
  const _tokens = writable(null);
15
15
  const _appConfig = writable(null);
16
16
  const _profile = writable(undefined);
17
- const _flags = writable({});
18
17
  const _authState = writable('unauthenticated');
19
18
  const _isLoading = writable(true);
20
19
  const _error = writable(null);
@@ -46,19 +45,18 @@ export function initBridge(config) {
46
45
  if (existingTokens) {
47
46
  _tokens.set(existingTokens);
48
47
  // Fetch profile for existing tokens
49
- _instance.getProfile().then((p) => _profile.set(p ?? null)).catch(() => { });
48
+ _instance.getProfile().then((p) => _profile.set(p ?? null)).catch((err) => logger.warn('[bridge-instance] profile fetch failed:', err));
50
49
  }
51
50
  _authState.set(_instance.getAuthState());
52
51
  _isLoading.set(false);
53
52
  // Wire auth-core events → Svelte stores
54
53
  _instance.on('auth:login', (tokens) => {
55
54
  _tokens.set(tokens);
56
- _instance.getProfile().then((p) => _profile.set(p ?? null)).catch(() => { });
55
+ _instance.getProfile().then((p) => _profile.set(p ?? null)).catch((err) => logger.warn('[bridge-instance] profile fetch failed:', err));
57
56
  });
58
57
  _instance.on('auth:logout', () => {
59
58
  _tokens.set(null);
60
59
  _profile.set(null);
61
- _flags.set({});
62
60
  });
63
61
  _instance.on('auth:token-refreshed', (tokens) => {
64
62
  _tokens.set(tokens);
@@ -77,9 +75,8 @@ export function initBridge(config) {
77
75
  });
78
76
  _instance.on('auth:workspace-changed', (tokens) => {
79
77
  _tokens.set(tokens);
80
- _flags.set({});
81
78
  _subscriptionWritable.set({ status: null, plans: null, loading: false, error: null });
82
- _instance.getProfile().then((p) => _profile.set(p ?? null)).catch(() => { });
79
+ _instance.getProfile().then((p) => _profile.set(p ?? null)).catch((err) => logger.warn('[bridge-instance] profile fetch failed:', err));
83
80
  });
84
81
  _instance.on('auth:error', (err) => {
85
82
  _error.set(err.message);
@@ -150,8 +147,6 @@ export const isOnboarded = _isOnboarded;
150
147
  export const hasMultiTenantAccess = _hasMultiTenantAccess;
151
148
  /** Tenant users for multi-tenant selection */
152
149
  export const tenantUsersStore = _tenantUsers;
153
- /** Feature flags map */
154
- export const flagsStore = _flags;
155
150
  /** Bridge ready state */
156
151
  export const bridgeReadyStore = _ready;
157
152
  /** App-level config (SSO providers, feature flags, etc.) — loaded anonymously on init */
@@ -183,11 +178,10 @@ export async function loadSubscription() {
183
178
  export const auth = new Proxy({}, {
184
179
  get(_, prop) {
185
180
  const instance = getBridgeAuth();
186
- const value = instance[prop];
181
+ const value = Reflect.get(instance, prop);
187
182
  return typeof value === 'function' ? value.bind(instance) : value;
188
183
  },
189
184
  });
190
185
  // ── Internal-only store writers (for use by wrapper modules) ───────────────────
191
- export const _flagsWritable = _flags;
192
186
  export const _profileWritable = _profile;
193
187
  export const _errorWritable = _error;
@@ -104,6 +104,10 @@ export function startBridgeRuntime(options = {}) {
104
104
  getAuthToken: () => _currentAuthToken,
105
105
  });
106
106
  let _connectedOnce = false;
107
+ // Set just before we call _realtime.reauthorize() so the resulting
108
+ // reconnect's setOnOpen handler knows the token is already fresh and skips
109
+ // its proactive refresh — see the loop note in setOnOpen below.
110
+ let _reauthInFlight = false;
107
111
  _realtime.setOnOpen(() => {
108
112
  _setRealtimeStatus('open');
109
113
  // On reconnect (not initial connect), proactively refresh tokens.
@@ -111,7 +115,18 @@ export function startBridgeRuntime(options = {}) {
111
115
  // client missed the user.state_changed broadcast. Refreshing here
112
116
  // syncs tokens before the first post-reconnect request can fail with
113
117
  // TOKEN_VERSION_STALE.
114
- if (_connectedOnce) {
118
+ //
119
+ // EXCEPT when this reconnect was caused by our OWN reauthorize() below
120
+ // (a token-only refresh). In that case the token is already current, so
121
+ // refreshing again would mint yet another JWT, which the tokenStore
122
+ // subscription would see as a change and reauthorize() again → reconnect
123
+ // → setOnOpen → refresh → … an unbounded loop that hammers /auth/token
124
+ // (observed ~32 cycles/sec, jamming the page's main thread and stalling
125
+ // every downstream wait). Only genuine, externally-triggered reconnects
126
+ // (network blips, server restarts) should trigger the catch-up refresh.
127
+ const causedByReauthorize = _reauthInFlight;
128
+ _reauthInFlight = false;
129
+ if (_connectedOnce && !causedByReauthorize) {
115
130
  getBridgeAuth().refreshTokens().catch(() => { });
116
131
  }
117
132
  _connectedOnce = true;
@@ -224,6 +239,10 @@ export function startBridgeRuntime(options = {}) {
224
239
  // Centrifugo's connection-token TTL drops it. Force a reauthorize so the
225
240
  // server re-validates against the new token immediately.
226
241
  if (prevAuthToken && _currentAuthToken && prevAuthToken !== _currentAuthToken) {
242
+ // Flag this as a self-induced reconnect so setOnOpen does NOT fire its
243
+ // proactive refresh (which would mint a new token → land back here →
244
+ // loop forever). The token we're reauthorizing with is already current.
245
+ _reauthInFlight = true;
227
246
  void _realtime.reauthorize();
228
247
  }
229
248
  });
@@ -8,10 +8,9 @@
8
8
  * follow-up sub-tickets (TBP-322 `.load()` semantics + TBP-323 reactive
9
9
  * binding for loaded slices).
10
10
  *
11
- * Backward compat: this is purely additive. The legacy module-level stores
12
- * (`subscriptionStore`, `flagsStore`, `appConfigStore`, etc.) continue to
13
- * exist and are populated by the same internal state TBP-324 wraps them
14
- * in deprecation warnings; for now both surfaces coexist.
11
+ * Backward compat: this is purely additive. The module-level stores
12
+ * (`subscriptionStore`, `appConfigStore`, etc.) continue to exist and are
13
+ * populated by the same internal state; both surfaces coexist.
15
14
  *
16
15
  * Note: `useBridge()` (Svelte context hook) lands in TBP-320. This module
17
16
  * only exposes the singleton aggregate `bridge`; consumers can import it
@@ -8,10 +8,9 @@
8
8
  * follow-up sub-tickets (TBP-322 `.load()` semantics + TBP-323 reactive
9
9
  * binding for loaded slices).
10
10
  *
11
- * Backward compat: this is purely additive. The legacy module-level stores
12
- * (`subscriptionStore`, `flagsStore`, `appConfigStore`, etc.) continue to
13
- * exist and are populated by the same internal state TBP-324 wraps them
14
- * in deprecation warnings; for now both surfaces coexist.
11
+ * Backward compat: this is purely additive. The module-level stores
12
+ * (`subscriptionStore`, `appConfigStore`, etc.) continue to exist and are
13
+ * populated by the same internal state; both surfaces coexist.
15
14
  *
16
15
  * Note: `useBridge()` (Svelte context hook) lands in TBP-320. This module
17
16
  * only exposes the singleton aggregate `bridge`; consumers can import it
@@ -218,7 +218,7 @@ describe('bridge.app.plans lazy slice (Phase 4, TBP-321/322)', () => {
218
218
  });
219
219
  it('apply() updates a loaded slice (TBP-323 reactive binding primitive)', async () => {
220
220
  await bridge.app.plans.load();
221
- bridge.app.plans.apply([{ key: 'enterprise', name: 'Enterprise' }]);
222
- expect(get(bridge.app.plans)).toEqual([{ key: 'enterprise', name: 'Enterprise' }]);
221
+ bridge.app.plans.apply([{ key: 'enterprise', name: 'Enterprise', prices: [] }]);
222
+ expect(get(bridge.app.plans)).toEqual([{ key: 'enterprise', name: 'Enterprise', prices: [] }]);
223
223
  });
224
224
  });
@@ -27,11 +27,10 @@ export class BrowserIdentityStorage {
27
27
  constructor(mode, key = 'bridge.anon_id') {
28
28
  this.mode = mode;
29
29
  this.key = key;
30
- if (typeof globalThis === 'undefined' || !globalThis.window) {
30
+ if (typeof window === 'undefined') {
31
31
  throw new Error('BrowserIdentityStorage requires a window — use MemoryIdentityStorage on the server');
32
32
  }
33
- const w = globalThis.window;
34
- this.storage = mode === 'persistent' ? w.localStorage : w.sessionStorage;
33
+ this.storage = mode === 'persistent' ? window.localStorage : window.sessionStorage;
35
34
  }
36
35
  read() {
37
36
  try {
package/dist/index.d.ts CHANGED
@@ -5,10 +5,10 @@ export type { SubscriptionState } from './core/bridge-instance.js';
5
5
  export { bridge } from './core/bridge.js';
6
6
  export type { BridgeSurface, BridgeAppSurface, BridgeTenantSurface, } from './core/bridge.js';
7
7
  export type { BrandingSnapshot, SubscriptionSnapshot, UserSnapshot, SessionSnapshotData, } from './core/snapshot-stores.js';
8
- export type { BridgeEventHandlers } from './core/events.js';
8
+ export type { BridgeEventHandlers, BridgeEventsDispatcher } from './core/events.js';
9
9
  export { default as BridgeBootstrap, default as BridgeProvider } from './client/BridgeBootstrap.svelte';
10
10
  export { default as ApiTokenManagement } from './client/components/developer/ApiTokenManagement.svelte';
11
- export { default as FeatureFlag } from './client/components/FeatureFlag.svelte';
11
+ export { default as FeatureFlag } from './flags/FeatureFlag.svelte';
12
12
  export { default as ProfileName } from './client/components/ProfileName.svelte';
13
13
  export { default as TeamManagementPanel } from './client/components/team/TeamManagementPanel.svelte';
14
14
  export { default as TeamUserList } from './client/components/team/TeamUserList.svelte';
@@ -31,7 +31,6 @@ export { default as BridgeSubscriptionStatus } from './client/components/subscri
31
31
  export { default as BridgeBillingNotice } from './client/components/subscription/BridgeBillingNotice.svelte';
32
32
  export { default as BridgePaywall } from './client/components/subscription/BridgePaywall.svelte';
33
33
  export { default as BridgeQuotaBanner } from './client/components/subscription/BridgeQuotaBanner.svelte';
34
- export * from './shared/feature-flag.js';
35
34
  export * from './auth/route-guard.js';
36
35
  export * from './shared/profile.js';
37
36
  export * from './shared/types/config.js';
@@ -41,6 +40,7 @@ export type { RedditConversionEvent, RedditEcommerce, RedditEcommerceItem, Reddi
41
40
  export { sha256Email } from './client/tracking/pii-hashing.js';
42
41
  export type { AuthConfigResponse, AuthResult, AuthState, BridgeAuthConfig, BridgeAuthEventName, BridgeAuthEvents, FederationConnection, MagicLinkResult, MfaResult, PasskeyAuthOptions, PasskeyRegistrationOptions, PasskeyVerificationResult, SignupResult, SsoOptions, SsoResult, TenantUser, } from '@nebulr-group/bridge-auth-core';
43
42
  export { BridgeAuth, BridgeAuthError, HttpError, TeamService, ApiTokenService } from '@nebulr-group/bridge-auth-core';
43
+ export type { SessionStalePayload } from '@nebulr-group/bridge-auth-core';
44
44
  export type { ApiToken, CreateApiTokenInput, CreateApiTokenResponse, } from '@nebulr-group/bridge-auth-core';
45
45
  export type { TeamProfile, TeamProfileUpdateInput, TeamUser, TeamUserListResult, TeamUserUpdateInput, TeamWorkspace, TeamWorkspaceUpdateInput, } from '@nebulr-group/bridge-auth-core';
46
46
  export type { Plan, PriceOfferSdk, SubscriptionStatus, CheckoutSession, Workspace, } from '@nebulr-group/bridge-auth-core';
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ export { bridge } from './core/bridge.js';
16
16
  // Components (Svelte components must have `export default`)
17
17
  export { default as BridgeBootstrap, default as BridgeProvider } from './client/BridgeBootstrap.svelte';
18
18
  export { default as ApiTokenManagement } from './client/components/developer/ApiTokenManagement.svelte';
19
- export { default as FeatureFlag } from './client/components/FeatureFlag.svelte';
19
+ export { default as FeatureFlag } from './flags/FeatureFlag.svelte';
20
20
  export { default as ProfileName } from './client/components/ProfileName.svelte';
21
21
  export { default as TeamManagementPanel } from './client/components/team/TeamManagementPanel.svelte';
22
22
  export { default as TeamUserList } from './client/components/team/TeamUserList.svelte';
@@ -49,8 +49,6 @@ export { default as BridgeBillingNotice } from './client/components/subscription
49
49
  export { default as BridgePaywall } from './client/components/subscription/BridgePaywall.svelte';
50
50
  // Billing 2.0 (Phase C / US-11) — live quota counter banner.
51
51
  export { default as BridgeQuotaBanner } from './client/components/subscription/BridgeQuotaBanner.svelte';
52
- // Feature flags
53
- export * from './shared/feature-flag.js';
54
52
  // Auth route guards
55
53
  export * from './auth/route-guard.js';
56
54
  // Types
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebulr-group/bridge-svelte",
3
- "version": "0.4.0-beta.2",
3
+ "version": "0.4.0-beta.4",
4
4
  "description": "Bridge Svelte library, This library helps you to add bridge authentication and feature flags, and payments to your svelte application.",
5
5
  "author": "Iman Pouya",
6
6
  "license": "MIT",
@@ -67,7 +67,7 @@
67
67
  }
68
68
  },
69
69
  "dependencies": {
70
- "@nebulr-group/bridge-auth-core": "0.4.0-beta.2"
70
+ "@nebulr-group/bridge-auth-core": "0.4.0-beta.5"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@sveltejs/kit": "^2.16.0",
@@ -1,37 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { onMount } from 'svelte';
4
- import { isFeatureEnabled } from '../../shared/feature-flag.js';
5
-
6
- type FlagRenderArgs = { enabled: boolean; rawEnabled: boolean };
7
-
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();
21
-
22
- let enabled = $state(false);
23
- let rawEnabled = $derived(enabled);
24
- let effectiveEnabled = $derived(negate ? !enabled : enabled);
25
-
26
- onMount(async () => {
27
- enabled = await isFeatureEnabled(flagName, forceLive);
28
- });
29
- </script>
30
-
31
- {#if children}
32
- {#if renderWhenDisabled}
33
- {@render children({ enabled: effectiveEnabled, rawEnabled })}
34
- {:else if effectiveEnabled}
35
- {@render children({ enabled: true, rawEnabled })}
36
- {/if}
37
- {/if}
@@ -1,15 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- type FlagRenderArgs = {
3
- enabled: boolean;
4
- rawEnabled: boolean;
5
- };
6
- type $$ComponentProps = {
7
- flagName: string;
8
- forceLive?: boolean;
9
- negate?: boolean;
10
- renderWhenDisabled?: boolean;
11
- children?: Snippet<[FlagRenderArgs]>;
12
- };
13
- declare const FeatureFlag: import("svelte").Component<$$ComponentProps, {}, "">;
14
- type FeatureFlag = ReturnType<typeof FeatureFlag>;
15
- export default FeatureFlag;
@@ -1,6 +0,0 @@
1
- export declare function loadFeatureFlags(): Promise<void>;
2
- export declare function isFeatureEnabled(flag: string, forceLive?: boolean): Promise<boolean>;
3
- export declare const featureFlags: {
4
- flags: import("svelte/store").Writable<Record<string, boolean>>;
5
- refresh: typeof loadFeatureFlags;
6
- };
@@ -1,13 +0,0 @@
1
- // src/lib/shared/feature-flag.ts — thin wrapper delegating to bridge-instance
2
- import { getBridgeAuth, _flagsWritable, flagsStore } from '../core/bridge-instance.js';
3
- export async function loadFeatureFlags() {
4
- const flags = await getBridgeAuth().loadFeatureFlags();
5
- _flagsWritable.set(flags);
6
- }
7
- export async function isFeatureEnabled(flag, forceLive = false) {
8
- return getBridgeAuth().isFeatureEnabled(flag, { forceLive });
9
- }
10
- export const featureFlags = {
11
- flags: flagsStore,
12
- refresh: loadFeatureFlags
13
- };