@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.
Files changed (85) hide show
  1. package/dist/client/BridgeBootstrap.d.ts +1 -1
  2. package/dist/client/BridgeBootstrap.js +104 -27
  3. package/dist/client/BridgeBootstrap.svelte +65 -4
  4. package/dist/client/BridgeBootstrap.svelte.d.ts +2 -0
  5. package/dist/client/BridgeProvider.svelte +31 -0
  6. package/dist/client/BridgeProvider.svelte.d.ts +8 -0
  7. package/dist/client/components/developer/ApiTokenManagement.svelte +1 -1
  8. package/dist/client/components/sdk-auth/LoginForm.svelte +10 -7
  9. package/dist/client/components/sdk-auth/LoginForm.svelte.d.ts +1 -1
  10. package/dist/client/components/sdk-auth/PasskeyLogin.svelte +1 -1
  11. package/dist/client/components/sdk-auth/PasskeySetup.svelte +1 -1
  12. package/dist/client/components/sdk-auth/SsoButton.svelte +1 -1
  13. package/dist/client/components/sdk-auth/SsoButton.svelte.d.ts +1 -1
  14. package/dist/client/components/sdk-auth/TenantSelector.svelte +1 -1
  15. package/dist/client/components/sdk-auth/TenantSelector.svelte.d.ts +1 -1
  16. package/dist/client/components/sdk-auth/WorkspaceSelector.svelte +3 -3
  17. package/dist/client/components/sdk-auth/WorkspaceSelector.svelte.d.ts +3 -3
  18. package/dist/client/components/subscription/BridgeBillingNotice.svelte +389 -0
  19. package/dist/client/components/subscription/BridgeBillingNotice.svelte.d.ts +19 -0
  20. package/dist/client/components/subscription/BridgePaywall.svelte +94 -0
  21. package/dist/client/components/subscription/BridgePaywall.svelte.d.ts +19 -0
  22. package/dist/client/components/subscription/BridgeQuotaBanner.svelte +275 -0
  23. package/dist/client/components/subscription/BridgeQuotaBanner.svelte.d.ts +24 -0
  24. package/dist/client/components/subscription/BridgeSubscriptionStatus.svelte +116 -0
  25. package/dist/client/components/subscription/BridgeSubscriptionStatus.svelte.d.ts +7 -0
  26. package/dist/client/components/subscription/PlanSelector.svelte +22 -14
  27. package/dist/client/components/subscription/PlanSelector.svelte.d.ts +3 -3
  28. package/dist/client/components/subscription/billing-notice-gate.test.d.ts +1 -0
  29. package/dist/client/components/subscription/billing-notice-gate.test.js +83 -0
  30. package/dist/client/components/team/TeamAddUserDialog.svelte +1 -1
  31. package/dist/client/components/team/TeamAddUserDialog.svelte.d.ts +1 -1
  32. package/dist/client/components/team/TeamEditUserDialog.svelte +1 -1
  33. package/dist/client/components/team/TeamEditUserDialog.svelte.d.ts +1 -1
  34. package/dist/client/components/team/TeamProfileForm.svelte +1 -1
  35. package/dist/client/components/team/TeamUserList.svelte +1 -1
  36. package/dist/client/components/team/TeamWorkspaceForm.svelte +1 -1
  37. package/dist/client/tracking/pii-hashing.spec.js +4 -14
  38. package/dist/core/bridge-instance.d.ts +1 -2
  39. package/dist/core/bridge-instance.js +0 -4
  40. package/dist/core/bridge-runtime.d.ts +101 -0
  41. package/dist/core/bridge-runtime.js +290 -0
  42. package/dist/core/bridge-runtime.test.d.ts +1 -0
  43. package/dist/core/bridge-runtime.test.js +203 -0
  44. package/dist/core/bridge.d.ts +93 -0
  45. package/dist/core/bridge.js +108 -0
  46. package/dist/core/bridge.test.d.ts +1 -0
  47. package/dist/core/bridge.test.js +224 -0
  48. package/dist/core/events.d.ts +74 -0
  49. package/dist/core/events.js +64 -0
  50. package/dist/core/events.test.d.ts +1 -0
  51. package/dist/core/events.test.js +116 -0
  52. package/dist/core/lazy-slice.d.ts +74 -0
  53. package/dist/core/lazy-slice.js +124 -0
  54. package/dist/core/lazy-slice.test.d.ts +1 -0
  55. package/dist/core/lazy-slice.test.js +144 -0
  56. package/dist/core/realtime-status.d.ts +6 -0
  57. package/dist/core/realtime-status.js +15 -0
  58. package/dist/core/snapshot-stores.d.ts +69 -0
  59. package/dist/core/snapshot-stores.js +64 -0
  60. package/dist/core/use-bridge.d.ts +13 -0
  61. package/dist/core/use-bridge.js +46 -0
  62. package/dist/core/use-bridge.test.d.ts +1 -0
  63. package/dist/core/use-bridge.test.js +24 -0
  64. package/dist/flags/FeatureFlag.svelte +57 -0
  65. package/dist/flags/FeatureFlag.svelte.d.ts +37 -0
  66. package/dist/flags/bootstrap.d.ts +72 -0
  67. package/dist/flags/bootstrap.js +323 -0
  68. package/dist/flags/bootstrap.test.d.ts +1 -0
  69. package/dist/flags/bootstrap.test.js +195 -0
  70. package/dist/flags/flag.svelte.d.ts +31 -0
  71. package/dist/flags/flag.svelte.js +99 -0
  72. package/dist/flags/index.d.ts +8 -0
  73. package/dist/flags/index.js +20 -0
  74. package/dist/flags/index.test.d.ts +1 -0
  75. package/dist/flags/index.test.js +81 -0
  76. package/dist/flags/realtime-status.d.ts +1 -0
  77. package/dist/flags/realtime-status.js +4 -0
  78. package/dist/flags/registry.d.ts +27 -0
  79. package/dist/flags/registry.js +63 -0
  80. package/dist/flags/us13-integration.test.d.ts +1 -0
  81. package/dist/flags/us13-integration.test.js +168 -0
  82. package/dist/index.d.ts +14 -1
  83. package/dist/index.js +31 -2
  84. package/dist/styles.d.ts +2 -0
  85. 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 {};