@nebulr-group/bridge-svelte 0.3.2 → 0.4.0-beta.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/dist/client/BridgeBootstrap.d.ts +1 -1
- package/dist/client/BridgeBootstrap.js +104 -27
- package/dist/client/BridgeBootstrap.svelte +65 -4
- package/dist/client/BridgeBootstrap.svelte.d.ts +2 -0
- package/dist/client/BridgeProvider.svelte +31 -0
- package/dist/client/BridgeProvider.svelte.d.ts +8 -0
- package/dist/client/components/developer/ApiTokenManagement.svelte +1 -1
- package/dist/client/components/sdk-auth/LoginForm.svelte +10 -7
- package/dist/client/components/sdk-auth/LoginForm.svelte.d.ts +1 -1
- package/dist/client/components/sdk-auth/PasskeyLogin.svelte +1 -1
- package/dist/client/components/sdk-auth/PasskeySetup.svelte +1 -1
- package/dist/client/components/sdk-auth/SsoButton.svelte +1 -1
- package/dist/client/components/sdk-auth/SsoButton.svelte.d.ts +1 -1
- package/dist/client/components/sdk-auth/TenantSelector.svelte +1 -1
- package/dist/client/components/sdk-auth/TenantSelector.svelte.d.ts +1 -1
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte +3 -3
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte.d.ts +3 -3
- package/dist/client/components/subscription/BridgeBillingNotice.svelte +389 -0
- package/dist/client/components/subscription/BridgeBillingNotice.svelte.d.ts +19 -0
- package/dist/client/components/subscription/BridgePaywall.svelte +94 -0
- package/dist/client/components/subscription/BridgePaywall.svelte.d.ts +19 -0
- package/dist/client/components/subscription/BridgeQuotaBanner.svelte +275 -0
- package/dist/client/components/subscription/BridgeQuotaBanner.svelte.d.ts +24 -0
- package/dist/client/components/subscription/BridgeSubscriptionStatus.svelte +116 -0
- package/dist/client/components/subscription/BridgeSubscriptionStatus.svelte.d.ts +7 -0
- package/dist/client/components/subscription/PlanSelector.svelte +22 -14
- package/dist/client/components/subscription/PlanSelector.svelte.d.ts +3 -3
- package/dist/client/components/subscription/billing-notice-gate.test.d.ts +1 -0
- package/dist/client/components/subscription/billing-notice-gate.test.js +83 -0
- package/dist/client/components/team/TeamAddUserDialog.svelte +1 -1
- package/dist/client/components/team/TeamAddUserDialog.svelte.d.ts +1 -1
- package/dist/client/components/team/TeamEditUserDialog.svelte +1 -1
- package/dist/client/components/team/TeamEditUserDialog.svelte.d.ts +1 -1
- package/dist/client/components/team/TeamProfileForm.svelte +1 -1
- package/dist/client/components/team/TeamUserList.svelte +1 -1
- package/dist/client/components/team/TeamWorkspaceForm.svelte +1 -1
- package/dist/client/tracking/pii-hashing.spec.js +4 -14
- package/dist/core/bridge-instance.d.ts +1 -2
- package/dist/core/bridge-instance.js +0 -4
- package/dist/core/bridge-runtime.d.ts +101 -0
- package/dist/core/bridge-runtime.js +290 -0
- package/dist/core/bridge-runtime.test.d.ts +1 -0
- package/dist/core/bridge-runtime.test.js +203 -0
- package/dist/core/bridge.d.ts +93 -0
- package/dist/core/bridge.js +108 -0
- package/dist/core/bridge.test.d.ts +1 -0
- package/dist/core/bridge.test.js +224 -0
- package/dist/core/events.d.ts +74 -0
- package/dist/core/events.js +64 -0
- package/dist/core/events.test.d.ts +1 -0
- package/dist/core/events.test.js +116 -0
- package/dist/core/lazy-slice.d.ts +74 -0
- package/dist/core/lazy-slice.js +124 -0
- package/dist/core/lazy-slice.test.d.ts +1 -0
- package/dist/core/lazy-slice.test.js +144 -0
- package/dist/core/realtime-status.d.ts +6 -0
- package/dist/core/realtime-status.js +15 -0
- package/dist/core/snapshot-stores.d.ts +69 -0
- package/dist/core/snapshot-stores.js +64 -0
- package/dist/core/use-bridge.d.ts +13 -0
- package/dist/core/use-bridge.js +46 -0
- package/dist/core/use-bridge.test.d.ts +1 -0
- package/dist/core/use-bridge.test.js +24 -0
- package/dist/flags/FeatureFlag.svelte +57 -0
- package/dist/flags/FeatureFlag.svelte.d.ts +37 -0
- package/dist/flags/bootstrap.d.ts +72 -0
- package/dist/flags/bootstrap.js +323 -0
- package/dist/flags/bootstrap.test.d.ts +1 -0
- package/dist/flags/bootstrap.test.js +195 -0
- package/dist/flags/flag.svelte.d.ts +31 -0
- package/dist/flags/flag.svelte.js +99 -0
- package/dist/flags/index.d.ts +8 -0
- package/dist/flags/index.js +20 -0
- package/dist/flags/index.test.d.ts +1 -0
- package/dist/flags/index.test.js +81 -0
- package/dist/flags/realtime-status.d.ts +1 -0
- package/dist/flags/realtime-status.js +4 -0
- package/dist/flags/registry.d.ts +27 -0
- package/dist/flags/registry.js +63 -0
- package/dist/flags/us13-integration.test.d.ts +1 -0
- package/dist/flags/us13-integration.test.js +168 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +31 -2
- package/dist/styles.d.ts +2 -0
- package/package.json +92 -86
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 (TBP-288/320) — `useBridge()` Svelte context hook.
|
|
3
|
+
*
|
|
4
|
+
* Default behavior: returns the module-level singleton `bridge` (from
|
|
5
|
+
* `bridge.ts`). Works from any context — components, regular .ts modules,
|
|
6
|
+
* tests — because the singleton is always available.
|
|
7
|
+
*
|
|
8
|
+
* Component-scoped override: a parent `<BridgeProvider>` may set a different
|
|
9
|
+
* bridge instance into Svelte context. Useful for SSR, multi-tenant test
|
|
10
|
+
* harnesses, or storybook fixtures that need isolated bridge state.
|
|
11
|
+
*
|
|
12
|
+
* Naming collision with auth-core's `useBridge` (billing/quota factory):
|
|
13
|
+
* bridge-svelte's `useBridge()` SHADOWS the auth-core import. Consumers
|
|
14
|
+
* who explicitly want the auth-core factory should import it as
|
|
15
|
+
* `useBillingBridge` (alias added in auth-core re-exports in TBP-324).
|
|
16
|
+
*/
|
|
17
|
+
import { getContext, hasContext, setContext } from 'svelte';
|
|
18
|
+
import { bridge as _singleton } from './bridge.js';
|
|
19
|
+
const BRIDGE_CONTEXT_KEY = Symbol('bridge-svelte:bridge');
|
|
20
|
+
/**
|
|
21
|
+
* Set a bridge instance into Svelte context. Called by `<BridgeProvider>`
|
|
22
|
+
* during component initialization. Calling this outside a component
|
|
23
|
+
* initialization phase will throw (Svelte's `setContext` contract).
|
|
24
|
+
*/
|
|
25
|
+
export function setBridgeContext(b) {
|
|
26
|
+
setContext(BRIDGE_CONTEXT_KEY, b);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Return the bridge surface for the current component scope, falling back
|
|
30
|
+
* to the module-level singleton outside components (or when no provider
|
|
31
|
+
* has been mounted). Safe to call from any code path.
|
|
32
|
+
*/
|
|
33
|
+
export function useBridge() {
|
|
34
|
+
try {
|
|
35
|
+
if (hasContext(BRIDGE_CONTEXT_KEY)) {
|
|
36
|
+
const fromCtx = getContext(BRIDGE_CONTEXT_KEY);
|
|
37
|
+
if (fromCtx)
|
|
38
|
+
return fromCtx;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// `getContext`/`hasContext` throw outside a component init phase —
|
|
43
|
+
// fall through to the singleton, which is the right answer there.
|
|
44
|
+
}
|
|
45
|
+
return _singleton;
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Phase 4 (TBP-288/320) — useBridge hook tests.
|
|
2
|
+
//
|
|
3
|
+
// Verifies:
|
|
4
|
+
// 1. Outside a component (regular .ts module / vitest test), useBridge()
|
|
5
|
+
// returns the singleton without throwing.
|
|
6
|
+
// 2. setBridgeContext outside a component scope throws (Svelte contract);
|
|
7
|
+
// useBridge() catches and falls back to the singleton.
|
|
8
|
+
// 3. Provided override via Svelte context wins over the singleton —
|
|
9
|
+
// covered by integration in BridgeProvider.test.ts where a real
|
|
10
|
+
// component mount sets context.
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import { useBridge } from './use-bridge.js';
|
|
13
|
+
import { bridge as singleton } from './bridge.js';
|
|
14
|
+
describe('useBridge() — outside component scope (Phase 4, TBP-320)', () => {
|
|
15
|
+
it('returns the singleton when called from regular .ts code', () => {
|
|
16
|
+
expect(useBridge()).toBe(singleton);
|
|
17
|
+
});
|
|
18
|
+
it('does not throw even when no Svelte context is available', () => {
|
|
19
|
+
expect(() => useBridge()).not.toThrow();
|
|
20
|
+
});
|
|
21
|
+
it('returns a stable reference across calls', () => {
|
|
22
|
+
expect(useBridge()).toBe(useBridge());
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
bridge-svelte/flags — declarative component for Bridge feature flags.
|
|
3
|
+
|
|
4
|
+
Two snippets:
|
|
5
|
+
- `children` — rendered when Bridge's rule passed (flag is on for this user)
|
|
6
|
+
- `fallback` — rendered when the flag is off or no rule matched
|
|
7
|
+
|
|
8
|
+
Both snippets receive the Bridge-decided value so you can use it directly.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
<FeatureFlag key="new-dashboard" defaultValue={false}>
|
|
12
|
+
{#snippet children()}<NewDashboard />{/snippet}
|
|
13
|
+
</FeatureFlag>
|
|
14
|
+
|
|
15
|
+
<FeatureFlag key="ui-theme" defaultValue="light-mode">
|
|
16
|
+
{#snippet children(value)}<App theme={value} />{/snippet}
|
|
17
|
+
{#snippet fallback(value)}<App theme={value} />{/snippet}
|
|
18
|
+
</FeatureFlag>
|
|
19
|
+
-->
|
|
20
|
+
<script lang="ts" generics="T = boolean">
|
|
21
|
+
import type { Snippet } from 'svelte';
|
|
22
|
+
import type { EvalContext } from '@nebulr-group/bridge-auth-core';
|
|
23
|
+
import { evaluateFlag } from './registry.js';
|
|
24
|
+
import { _flagVersionsRune } from './flag.svelte.js';
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
key,
|
|
28
|
+
defaultValue,
|
|
29
|
+
context,
|
|
30
|
+
children,
|
|
31
|
+
fallback,
|
|
32
|
+
}: {
|
|
33
|
+
key: string;
|
|
34
|
+
defaultValue: T;
|
|
35
|
+
/**
|
|
36
|
+
* Optional per-call EvalContext. Use when a flag's rule targets
|
|
37
|
+
* dev-supplied attributes (e.g. `{ attributes: { plan } }`). Per-call
|
|
38
|
+
* attributes win on key collision over Bridge-managed providers.
|
|
39
|
+
*/
|
|
40
|
+
context?: Partial<EvalContext>;
|
|
41
|
+
children?: Snippet<[T]>;
|
|
42
|
+
fallback?: Snippet<[T]>;
|
|
43
|
+
} = $props();
|
|
44
|
+
|
|
45
|
+
const result = $derived.by(() => {
|
|
46
|
+
// Reactive dependency: read the per-key version so the derived re-runs
|
|
47
|
+
// whenever this flag changes in the cache.
|
|
48
|
+
_flagVersionsRune().get(key);
|
|
49
|
+
return evaluateFlag<T>(key, defaultValue, context);
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
{#if result.passed}
|
|
54
|
+
{#if children}{@render children(result.value)}{/if}
|
|
55
|
+
{:else if fallback}
|
|
56
|
+
{@render fallback(result.value)}
|
|
57
|
+
{/if}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { EvalContext } from '@nebulr-group/bridge-auth-core';
|
|
3
|
+
declare function $$render<T = boolean>(): {
|
|
4
|
+
props: {
|
|
5
|
+
key: string;
|
|
6
|
+
defaultValue: T;
|
|
7
|
+
/**
|
|
8
|
+
* Optional per-call EvalContext. Use when a flag's rule targets
|
|
9
|
+
* dev-supplied attributes (e.g. `{ attributes: { plan } }`). Per-call
|
|
10
|
+
* attributes win on key collision over Bridge-managed providers.
|
|
11
|
+
*/
|
|
12
|
+
context?: Partial<EvalContext>;
|
|
13
|
+
children?: Snippet<[T]>;
|
|
14
|
+
fallback?: Snippet<[T]>;
|
|
15
|
+
};
|
|
16
|
+
exports: {};
|
|
17
|
+
bindings: "";
|
|
18
|
+
slots: {};
|
|
19
|
+
events: {};
|
|
20
|
+
};
|
|
21
|
+
declare class __sveltets_Render<T = boolean> {
|
|
22
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
23
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
24
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
25
|
+
bindings(): "";
|
|
26
|
+
exports(): {};
|
|
27
|
+
}
|
|
28
|
+
interface $$IsomorphicComponent {
|
|
29
|
+
new <T = boolean>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
30
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
31
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
32
|
+
<T = boolean>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
33
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
34
|
+
}
|
|
35
|
+
declare const FeatureFlag: $$IsomorphicComponent;
|
|
36
|
+
type FeatureFlag<T = boolean> = InstanceType<typeof FeatureFlag<T>>;
|
|
37
|
+
export default FeatureFlag;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { BridgeFlags, type BridgeFlagsMode, type BridgeFlagsHooks, type BridgeIdentity, type IdentityStorage, type AnonymousTrackingMode, TelemetryBatcher, type TelemetryBatcherConfig, AuthAttributeProvider, BillingAttributeProvider } from '@nebulr-group/bridge-auth-core';
|
|
2
|
+
/** A storage implementation that uses `localStorage` (persistent) or `sessionStorage` (per-tab). */
|
|
3
|
+
export declare class BrowserIdentityStorage implements IdentityStorage {
|
|
4
|
+
readonly mode: AnonymousTrackingMode;
|
|
5
|
+
private readonly storage;
|
|
6
|
+
private readonly key;
|
|
7
|
+
constructor(mode: 'persistent' | 'session', key?: string);
|
|
8
|
+
read(): string | undefined;
|
|
9
|
+
write(id: string): void;
|
|
10
|
+
clear(): void;
|
|
11
|
+
}
|
|
12
|
+
export interface CreateBridgeFlagsConfig {
|
|
13
|
+
/**
|
|
14
|
+
* Bridge API base URL. Optional — defaults to the value stored by
|
|
15
|
+
* `bridgeConfig.initConfig({...})` (which itself defaults to
|
|
16
|
+
* `https://api.thebridge.dev`). Bridge developers override this for
|
|
17
|
+
* local / stage envs; product consumers never set it.
|
|
18
|
+
*/
|
|
19
|
+
apiBaseUrl?: string;
|
|
20
|
+
/**
|
|
21
|
+
* JWT-shaped workspace API key — sent as `x-api-key`. Optional — defaults
|
|
22
|
+
* to `appId` from `bridgeConfig.initConfig({...})`. The two values are the
|
|
23
|
+
* same thing (the workspace identity) under different names; the auth
|
|
24
|
+
* config call is the single source of truth.
|
|
25
|
+
*/
|
|
26
|
+
apiKey?: string;
|
|
27
|
+
/** Frontend (default) or backend. Use 'backend' in SSR-load() functions. */
|
|
28
|
+
mode?: BridgeFlagsMode;
|
|
29
|
+
/** Anonymous identity options. Persistent (localStorage) by default in browsers. */
|
|
30
|
+
identity?: {
|
|
31
|
+
/** 'persistent' (localStorage), 'session' (sessionStorage), 'none' (memory only). Default 'persistent'. */
|
|
32
|
+
tracking?: AnonymousTrackingMode;
|
|
33
|
+
/** Override storage entirely (e.g. cookie-backed). */
|
|
34
|
+
storage?: IdentityStorage;
|
|
35
|
+
/** localStorage key when using the default browser storage. Default 'bridge.anon_id'. */
|
|
36
|
+
storageKey?: string;
|
|
37
|
+
};
|
|
38
|
+
/** Telemetry opts — set `enabled: false` to skip. */
|
|
39
|
+
telemetry?: Partial<Omit<TelemetryBatcherConfig, 'apiBaseUrl' | 'apiKey'>>;
|
|
40
|
+
/**
|
|
41
|
+
* If false, this bootstrap won't register itself as the global Svelte
|
|
42
|
+
* instance used by `useFlag` / `<FeatureFlag>`. Useful when multiple
|
|
43
|
+
* BridgeFlags are needed in the same app. Defaults to true.
|
|
44
|
+
*/
|
|
45
|
+
registerGlobal?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Optional extra hooks the consumer wants chained on top of the
|
|
48
|
+
* built-in (telemetry + reactivity) hooks. Errors in user hooks are
|
|
49
|
+
* caught — they will never break flag eval.
|
|
50
|
+
*/
|
|
51
|
+
hooks?: BridgeFlagsHooks;
|
|
52
|
+
}
|
|
53
|
+
export interface BridgeFlagsBundle {
|
|
54
|
+
bridge: BridgeFlags;
|
|
55
|
+
identity: BridgeIdentity;
|
|
56
|
+
telemetry: TelemetryBatcher;
|
|
57
|
+
authAttributeProvider: AuthAttributeProvider;
|
|
58
|
+
billingAttributeProvider: BillingAttributeProvider;
|
|
59
|
+
/** Stop telemetry + unsubscribe from auth events. Idempotent. */
|
|
60
|
+
stop: () => Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build a fully wired BridgeFlags bundle. Bootstraps the flag-specific
|
|
64
|
+
* runtime ON TOP OF the core Bridge runtime — meaning `startBridgeRuntime()`
|
|
65
|
+
* must already have run (typically from `<BridgeBootstrap />`). The shared
|
|
66
|
+
* RealtimeClient is read via `getBridgeRealtime()` and re-used; no second
|
|
67
|
+
* websocket is opened.
|
|
68
|
+
*
|
|
69
|
+
* The returned instance is also registered as the global instance used by
|
|
70
|
+
* `useFlag` and `<FeatureFlag>` unless `registerGlobal` is explicitly false.
|
|
71
|
+
*/
|
|
72
|
+
export declare function createBridgeFlags(config?: CreateBridgeFlagsConfig): BridgeFlagsBundle;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// bridge-svelte/flags — bootstrap (TBP-200, hoisted in TBP-Live-Channel-Unification)
|
|
2
|
+
//
|
|
3
|
+
// Auth-free entry point for Feature Flags 2.0. After the runtime hoist, this
|
|
4
|
+
// module is ONLY about flag-specific wiring: the BridgeFlags eval cache,
|
|
5
|
+
// attribute providers, the telemetry batcher, hydrate, Svelte reactivity, and
|
|
6
|
+
// the auth-token subscription that drives flag eval context. The realtime
|
|
7
|
+
// client, billing-store binding, session.snapshot fanout, billing-family
|
|
8
|
+
// event dispatch, and token-driven channel scoping all live in
|
|
9
|
+
// `core/bridge-runtime.ts` and are mounted by `<BridgeBootstrap />`.
|
|
10
|
+
//
|
|
11
|
+
// Consumers don't call this directly anymore — `<BridgeBootstrap />` does it
|
|
12
|
+
// for them when `@nebulr-group/bridge-svelte/flags` is on the dependency
|
|
13
|
+
// graph. `createBridgeFlags(...)` stays exported as an advanced API for tests
|
|
14
|
+
// + standalone-SDK use, but its `apiBaseUrl` / `apiKey` args are now optional
|
|
15
|
+
// and default to the values stored by `bridgeConfig.initConfig({...})`.
|
|
16
|
+
import { BridgeFlags, attachIdentity, MemoryIdentityStorage, TelemetryBatcher, AuthAttributeProvider, BillingAttributeProvider, useBridge, } from '@nebulr-group/bridge-auth-core';
|
|
17
|
+
import { setBridgeFlagsInstance, notifyFlagChanged, notifyAllFlagsChanged } from './registry.js';
|
|
18
|
+
import { tokenStore, getBridgeAuth } from '../core/bridge-instance.js';
|
|
19
|
+
import { _getDevAttributeProvider } from '../core/bridge.js';
|
|
20
|
+
import { getBridgeRealtime, onBridgeRealtimeOpen, } from '../core/bridge-runtime.js';
|
|
21
|
+
import { getConfig } from '../client/stores/config.store.js';
|
|
22
|
+
/** A storage implementation that uses `localStorage` (persistent) or `sessionStorage` (per-tab). */
|
|
23
|
+
export class BrowserIdentityStorage {
|
|
24
|
+
mode;
|
|
25
|
+
storage;
|
|
26
|
+
key;
|
|
27
|
+
constructor(mode, key = 'bridge.anon_id') {
|
|
28
|
+
this.mode = mode;
|
|
29
|
+
this.key = key;
|
|
30
|
+
if (typeof globalThis === 'undefined' || !globalThis.window) {
|
|
31
|
+
throw new Error('BrowserIdentityStorage requires a window — use MemoryIdentityStorage on the server');
|
|
32
|
+
}
|
|
33
|
+
const w = globalThis.window;
|
|
34
|
+
this.storage = mode === 'persistent' ? w.localStorage : w.sessionStorage;
|
|
35
|
+
}
|
|
36
|
+
read() {
|
|
37
|
+
try {
|
|
38
|
+
return this.storage.getItem(this.key) ?? undefined;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
write(id) {
|
|
45
|
+
try {
|
|
46
|
+
this.storage.setItem(this.key, id);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Quota / privacy mode — silently degrade.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
clear() {
|
|
53
|
+
try {
|
|
54
|
+
this.storage.removeItem(this.key);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function pickIdentityStorage(cfg) {
|
|
62
|
+
if (cfg?.storage)
|
|
63
|
+
return cfg.storage;
|
|
64
|
+
const tracking = cfg?.tracking ?? 'persistent';
|
|
65
|
+
if (tracking === 'none')
|
|
66
|
+
return new MemoryIdentityStorage('none');
|
|
67
|
+
// SSR path — no `window`. Fall back to memory; the browser-side bootstrap
|
|
68
|
+
// (re-running with `window` present) will install the real storage.
|
|
69
|
+
if (typeof globalThis === 'undefined' || !globalThis.window) {
|
|
70
|
+
return new MemoryIdentityStorage(tracking);
|
|
71
|
+
}
|
|
72
|
+
return new BrowserIdentityStorage(tracking === 'session' ? 'session' : 'persistent', cfg?.storageKey);
|
|
73
|
+
}
|
|
74
|
+
/** Decode a JWT payload without signature verification (client-side context only). */
|
|
75
|
+
function decodeJwtPayload(token) {
|
|
76
|
+
try {
|
|
77
|
+
const part = token.split('.')[1];
|
|
78
|
+
if (!part)
|
|
79
|
+
return null;
|
|
80
|
+
const json = atob(part.replace(/-/g, '+').replace(/_/g, '/'));
|
|
81
|
+
return JSON.parse(json);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build a fully wired BridgeFlags bundle. Bootstraps the flag-specific
|
|
89
|
+
* runtime ON TOP OF the core Bridge runtime — meaning `startBridgeRuntime()`
|
|
90
|
+
* must already have run (typically from `<BridgeBootstrap />`). The shared
|
|
91
|
+
* RealtimeClient is read via `getBridgeRealtime()` and re-used; no second
|
|
92
|
+
* websocket is opened.
|
|
93
|
+
*
|
|
94
|
+
* The returned instance is also registered as the global instance used by
|
|
95
|
+
* `useFlag` and `<FeatureFlag>` unless `registerGlobal` is explicitly false.
|
|
96
|
+
*/
|
|
97
|
+
export function createBridgeFlags(config = {}) {
|
|
98
|
+
// Resolve config from the auth config store when the consumer doesn't pass
|
|
99
|
+
// explicit values. `bridgeConfig.initConfig({...})` is the single source of
|
|
100
|
+
// truth — consumers never plumb apiBaseUrl / apiKey through twice.
|
|
101
|
+
let resolvedApiBaseUrl = config.apiBaseUrl;
|
|
102
|
+
let resolvedApiKey = config.apiKey;
|
|
103
|
+
try {
|
|
104
|
+
const stored = getConfig();
|
|
105
|
+
resolvedApiBaseUrl ??= stored.apiBaseUrl ?? 'https://api.thebridge.dev';
|
|
106
|
+
resolvedApiKey ??= stored.appId;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// No initConfig call — only OK when the caller passed everything.
|
|
110
|
+
}
|
|
111
|
+
if (!resolvedApiBaseUrl)
|
|
112
|
+
resolvedApiBaseUrl = 'https://api.thebridge.dev';
|
|
113
|
+
if (!resolvedApiKey) {
|
|
114
|
+
throw new Error('createBridgeFlags: apiKey/appId could not be resolved. Call bridgeConfig.initConfig({ appId }) first or pass apiKey explicitly.');
|
|
115
|
+
}
|
|
116
|
+
const bridge = new BridgeFlags({ mode: config.mode });
|
|
117
|
+
// Billing 2.0 US-11 — self-report `bridge.flag_evaluations` to the usage
|
|
118
|
+
// pipeline. Best-effort: in SSR test harnesses without BridgeAuth, skip.
|
|
119
|
+
try {
|
|
120
|
+
const auth = getBridgeAuth();
|
|
121
|
+
bridge.setUsageReporter(auth.usage);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// No BridgeAuth instance yet — flags can still operate without usage reporting.
|
|
125
|
+
}
|
|
126
|
+
// US-11 — wire the QuotaStore HTTP options so `useBridge().quota(metric)`
|
|
127
|
+
// can hydrate on first access. Core's tokenStore subscription refreshes the
|
|
128
|
+
// accessToken on login/logout/refresh; here we snapshot the initial value.
|
|
129
|
+
try {
|
|
130
|
+
const auth = getBridgeAuth();
|
|
131
|
+
useBridge().quotas.configure({
|
|
132
|
+
apiBaseUrl: resolvedApiBaseUrl,
|
|
133
|
+
appId: auth.getApiContext().appId,
|
|
134
|
+
accessToken: auth.getApiContext().accessToken,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// No BridgeAuth — quota hydration falls back to live pushes only.
|
|
139
|
+
}
|
|
140
|
+
const identity = attachIdentity(bridge, pickIdentityStorage(config.identity));
|
|
141
|
+
// Phase 1 / US-13 (TBP-295) — current decoded JWT claims. The
|
|
142
|
+
// AuthAttributeProvider's `getClaims()` returns this on every flag eval so
|
|
143
|
+
// `user.role`, `user.email`, `tenant.id`, `tenant.plan`, `privileges` flow
|
|
144
|
+
// through the registry instead of via direct `bridge.setContext()` calls.
|
|
145
|
+
let _currentClaims;
|
|
146
|
+
// Patch upsert/remove on the bridge so realtime mutations re-notify the
|
|
147
|
+
// Svelte reactivity layer. We use prototype-style monkey-patching on the
|
|
148
|
+
// instance so we don't touch auth-core itself. This is intentionally a
|
|
149
|
+
// narrow seam — the public API surface (upsert / remove / hydrate) is
|
|
150
|
+
// explicitly the cache write path.
|
|
151
|
+
const originalUpsert = bridge.upsert.bind(bridge);
|
|
152
|
+
bridge.upsert = (flag) => {
|
|
153
|
+
originalUpsert(flag);
|
|
154
|
+
notifyFlagChanged(flag.key, _BUMP_SENTINEL);
|
|
155
|
+
};
|
|
156
|
+
const originalRemove = bridge.remove.bind(bridge);
|
|
157
|
+
bridge.remove = (key) => {
|
|
158
|
+
originalRemove(key);
|
|
159
|
+
notifyFlagChanged(key, _BUMP_SENTINEL);
|
|
160
|
+
};
|
|
161
|
+
const originalHydrate = bridge.hydrate.bind(bridge);
|
|
162
|
+
bridge.hydrate = (flags) => {
|
|
163
|
+
originalHydrate(flags);
|
|
164
|
+
notifyAllFlagsChanged();
|
|
165
|
+
};
|
|
166
|
+
// Attach the flag cache to the SHARED RealtimeClient owned by core. Calling
|
|
167
|
+
// attach() on a started client is safe — the realtime client tolerates
|
|
168
|
+
// post-start cache binding (it just registers an upsert/remove dispatcher).
|
|
169
|
+
// If core hasn't started yet (e.g. standalone-SDK harness), skip the attach
|
|
170
|
+
// — the consumer can wire it manually on `getBridgeRealtime()` once started.
|
|
171
|
+
const realtime = getBridgeRealtime();
|
|
172
|
+
if (realtime) {
|
|
173
|
+
realtime.attach(bridge);
|
|
174
|
+
}
|
|
175
|
+
// Phase 1 / US-13 (TBP-296) — bridge-managed AttributeProviders. Both are
|
|
176
|
+
// auto-registered below; consumers never wire these by hand.
|
|
177
|
+
const billingAttributeProvider = new BillingAttributeProvider();
|
|
178
|
+
billingAttributeProvider.bindStores({
|
|
179
|
+
subscription: useBridge().subscription,
|
|
180
|
+
quotas: useBridge().quotas,
|
|
181
|
+
entitlements: useBridge().entitlementsStore,
|
|
182
|
+
});
|
|
183
|
+
const authAttributeProvider = new AuthAttributeProvider({
|
|
184
|
+
getClaims: () => _currentClaims,
|
|
185
|
+
});
|
|
186
|
+
bridge.registerAttributeProvider(authAttributeProvider);
|
|
187
|
+
bridge.registerAttributeProvider(billingAttributeProvider);
|
|
188
|
+
// Phase 5 (TBP-328) — register the dev-managed provider LAST so its
|
|
189
|
+
// set/bind/bindMany keys win on collision with framework providers.
|
|
190
|
+
bridge.registerAttributeProvider(_getDevAttributeProvider());
|
|
191
|
+
const telemetry = new TelemetryBatcher({
|
|
192
|
+
apiBaseUrl: resolvedApiBaseUrl,
|
|
193
|
+
apiKey: resolvedApiKey,
|
|
194
|
+
...config.telemetry,
|
|
195
|
+
});
|
|
196
|
+
// Compose hooks: telemetry (baseline) + reactivity (eval-driven bumps) +
|
|
197
|
+
// user-supplied.
|
|
198
|
+
attachWithCompositeHooks(bridge, telemetry, config.hooks ?? {});
|
|
199
|
+
// Hydrate the flag cache so the first `bridge.flag()` call returns the right
|
|
200
|
+
// value instead of the developer-supplied default. Best-effort.
|
|
201
|
+
const hydrateFlagsCache = async () => {
|
|
202
|
+
try {
|
|
203
|
+
const res = await fetch(`${resolvedApiBaseUrl.replace(/\/+$/, '')}/admin/flags-internal/flags-cache/${encodeURIComponent(resolvedApiKey)}`);
|
|
204
|
+
if (!res.ok)
|
|
205
|
+
return;
|
|
206
|
+
const flags = (await res.json());
|
|
207
|
+
if (Array.isArray(flags) && flags.length > 0) {
|
|
208
|
+
bridge.hydrate(flags);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Hydration is best-effort.
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
// Re-hydrate every time the shared realtime client (re)opens. Covers
|
|
216
|
+
// initial-hydrate-failed and flag-mutations-during-offline gaps.
|
|
217
|
+
const unsubscribeOpen = onBridgeRealtimeOpen(() => {
|
|
218
|
+
void hydrateFlagsCache();
|
|
219
|
+
});
|
|
220
|
+
// Initial hydrate fires here too — covers the case where realtime is
|
|
221
|
+
// disabled and onOpen never fires.
|
|
222
|
+
void hydrateFlagsCache();
|
|
223
|
+
if (config.registerGlobal !== false) {
|
|
224
|
+
setBridgeFlagsInstance(bridge);
|
|
225
|
+
}
|
|
226
|
+
// Flag-context concerns on token change: setContext identity + claims +
|
|
227
|
+
// re-eval. Core handles channel scoping + quotas + reauthorize separately;
|
|
228
|
+
// two subscribers is fine (idempotent and cheap).
|
|
229
|
+
const unsubscribeAuth = tokenStore.subscribe((tokens) => {
|
|
230
|
+
if (!tokens?.accessToken) {
|
|
231
|
+
_currentClaims = undefined;
|
|
232
|
+
bridge.setContext({ identity: undefined, attributes: {} });
|
|
233
|
+
notifyAllFlagsChanged();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const claims = decodeJwtPayload(tokens.accessToken);
|
|
237
|
+
if (!claims)
|
|
238
|
+
return;
|
|
239
|
+
_currentClaims = claims;
|
|
240
|
+
bridge.setContext({
|
|
241
|
+
identity: typeof claims.sub === 'string' ? claims.sub : undefined,
|
|
242
|
+
attributes: {},
|
|
243
|
+
});
|
|
244
|
+
notifyAllFlagsChanged();
|
|
245
|
+
});
|
|
246
|
+
const stop = async () => {
|
|
247
|
+
unsubscribeAuth();
|
|
248
|
+
unsubscribeOpen();
|
|
249
|
+
await telemetry.stop();
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
bridge,
|
|
253
|
+
identity,
|
|
254
|
+
telemetry,
|
|
255
|
+
authAttributeProvider,
|
|
256
|
+
billingAttributeProvider,
|
|
257
|
+
stop,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
261
|
+
const _BUMP_SENTINEL = Symbol('bridge.flags.bump');
|
|
262
|
+
/**
|
|
263
|
+
* Telemetry's `attach` calls `bridge.setHooks` and overwrites whatever was
|
|
264
|
+
* there. We capture the batcher's onEval/onDiscover/onAttributeDeclaration
|
|
265
|
+
* by snooping via a one-shot setHooks override, then re-install a composite
|
|
266
|
+
* that calls them + our reactivity bump + the user's hooks.
|
|
267
|
+
*/
|
|
268
|
+
function attachWithCompositeHooks(bridge, telemetry, userHooks) {
|
|
269
|
+
let captured = {};
|
|
270
|
+
const originalSetHooks = bridge.setHooks.bind(bridge);
|
|
271
|
+
bridge.setHooks = (hooks) => {
|
|
272
|
+
captured = hooks ?? {};
|
|
273
|
+
};
|
|
274
|
+
try {
|
|
275
|
+
telemetry.attach(bridge);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
bridge.setHooks = originalSetHooks;
|
|
279
|
+
}
|
|
280
|
+
bridge.setHooks({
|
|
281
|
+
onEval: (ev) => {
|
|
282
|
+
try {
|
|
283
|
+
captured.onEval?.(ev);
|
|
284
|
+
}
|
|
285
|
+
catch { /* telemetry hook errors swallowed */ }
|
|
286
|
+
try {
|
|
287
|
+
userHooks.onEval?.(ev);
|
|
288
|
+
}
|
|
289
|
+
catch { /* user hook errors swallowed */ }
|
|
290
|
+
notifyFlagChanged(ev.flag, ev.value);
|
|
291
|
+
},
|
|
292
|
+
onDiscover: (ev) => {
|
|
293
|
+
try {
|
|
294
|
+
captured.onDiscover?.(ev);
|
|
295
|
+
}
|
|
296
|
+
catch { /* ignore */ }
|
|
297
|
+
try {
|
|
298
|
+
userHooks.onDiscover?.(ev);
|
|
299
|
+
}
|
|
300
|
+
catch { /* ignore */ }
|
|
301
|
+
},
|
|
302
|
+
onAttributeDeclaration: (decl) => {
|
|
303
|
+
try {
|
|
304
|
+
captured.onAttributeDeclaration?.(decl);
|
|
305
|
+
}
|
|
306
|
+
catch { /* ignore */ }
|
|
307
|
+
try {
|
|
308
|
+
userHooks.onAttributeDeclaration?.(decl);
|
|
309
|
+
}
|
|
310
|
+
catch { /* ignore */ }
|
|
311
|
+
},
|
|
312
|
+
onAttributeObserved: (obs) => {
|
|
313
|
+
try {
|
|
314
|
+
captured.onAttributeObserved?.(obs);
|
|
315
|
+
}
|
|
316
|
+
catch { /* ignore */ }
|
|
317
|
+
try {
|
|
318
|
+
userHooks.onAttributeObserved?.(obs);
|
|
319
|
+
}
|
|
320
|
+
catch { /* ignore */ }
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|