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

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.
@@ -3,6 +3,7 @@ import { redirect, isRedirect } from '@sveltejs/kit';
3
3
  import { get } from 'svelte/store';
4
4
  import { createRouteGuard } from '../auth/route-guard.js';
5
5
  import { getBridgeAuth, bridgeReadyStore, markReady, waitForBridge as _waitForBridge, } from '../core/bridge-instance.js';
6
+ import { installBridgeAuthFetch } from '../core/bridge-runtime.js';
6
7
  import { useBridge } from '@nebulr-group/bridge-auth-core';
7
8
  import { featureFlags } from '../shared/feature-flag.js';
8
9
  import { logger } from '../shared/logger.js';
@@ -21,6 +22,11 @@ export async function bridgeBootstrap(url, config, routeConfig = { rules: [], de
21
22
  }
22
23
  // 1. Initialize configuration (synchronously) — this also calls initBridge()
23
24
  bridgeConfig.initConfig(finalConfig, routeConfig);
25
+ // 1b. Patch globalThis.fetch early so GraphQL/HTTP calls made in this load()
26
+ // function (before any component mounts) already carry the Bearer token.
27
+ // installBridgeAuthFetch() is idempotent — startBridgeRuntime() (onMount)
28
+ // is a no-op when it finds the patch already in place.
29
+ installBridgeAuthFetch();
24
30
  // 1a. Unified callback handler — detects what is calling back and routes accordingly
25
31
  try {
26
32
  const resolvedCallbackUrl = getConfig().callbackUrl;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Wraps a fetch function with Bridge auth concerns for requests to `apiBaseUrl`.
3
+ * Requests to other URLs pass through completely untouched.
4
+ *
5
+ * Two responsibilities:
6
+ * 1. Inject the current access token as `Authorization: Bearer` so consumers
7
+ * never need auth code in their HTTP/GraphQL clients.
8
+ * 2. Detect `TOKEN_VERSION_STALE` in GraphQL 200 responses (the reactive
9
+ * fallback when the WebSocket broadcast was missed), call `refreshTokens()`,
10
+ * and retry once with the fresh token.
11
+ *
12
+ * Both paths converge on `getBridgeAuth().refreshTokens()` — the same call the
13
+ * WebSocket `user.state_changed` handler uses. The dedup gate in `BridgeAuth`
14
+ * ensures only one POST /auth/token goes out even if both paths fire at once.
15
+ *
16
+ * Installed by `startBridgeRuntime()` patching `globalThis.fetch`.
17
+ */
18
+ export declare function wrapFetchWithBridgeAuth(baseFetch: typeof fetch, apiBaseUrl: string): typeof fetch;
@@ -0,0 +1,59 @@
1
+ import { getBridgeAuth } from './bridge-instance.js';
2
+ /**
3
+ * Wraps a fetch function with Bridge auth concerns for requests to `apiBaseUrl`.
4
+ * Requests to other URLs pass through completely untouched.
5
+ *
6
+ * Two responsibilities:
7
+ * 1. Inject the current access token as `Authorization: Bearer` so consumers
8
+ * never need auth code in their HTTP/GraphQL clients.
9
+ * 2. Detect `TOKEN_VERSION_STALE` in GraphQL 200 responses (the reactive
10
+ * fallback when the WebSocket broadcast was missed), call `refreshTokens()`,
11
+ * and retry once with the fresh token.
12
+ *
13
+ * Both paths converge on `getBridgeAuth().refreshTokens()` — the same call the
14
+ * WebSocket `user.state_changed` handler uses. The dedup gate in `BridgeAuth`
15
+ * ensures only one POST /auth/token goes out even if both paths fire at once.
16
+ *
17
+ * Installed by `startBridgeRuntime()` patching `globalThis.fetch`.
18
+ */
19
+ export function wrapFetchWithBridgeAuth(baseFetch, apiBaseUrl) {
20
+ return async function bridgeAuthFetch(input, init) {
21
+ const url = typeof input === 'string'
22
+ ? input
23
+ : input instanceof URL
24
+ ? input.href
25
+ : input.url;
26
+ // Only act on requests to the bridge API — everything else passes through.
27
+ if (!url.startsWith(apiBaseUrl))
28
+ return baseFetch(input, init);
29
+ // 1. Inject current access token.
30
+ const token = getBridgeAuth().getTokens()?.accessToken;
31
+ const headers = new Headers(init?.headers);
32
+ if (token)
33
+ headers.set('Authorization', `Bearer ${token}`);
34
+ const response = await baseFetch(input, { ...init, headers });
35
+ // Non-200s are returned as-is; httpFetch handles REST auth errors separately.
36
+ if (!response.ok)
37
+ return response;
38
+ // 2. Inspect body for TOKEN_VERSION_STALE without consuming the original
39
+ // response (URQL / callers need to read it themselves).
40
+ const clone = response.clone();
41
+ const body = await clone.json().catch(() => null);
42
+ const isStale = Array.isArray(body?.errors) && body.errors.some((e) => typeof e === 'object' &&
43
+ e !== null &&
44
+ e
45
+ .extensions?.response?.code === 'TOKEN_VERSION_STALE');
46
+ if (!isStale)
47
+ return response;
48
+ // 3. Refresh — same call as the WebSocket user.state_changed path.
49
+ // Dedup gate in BridgeAuth coalesces concurrent calls into one HTTP request.
50
+ await getBridgeAuth().refreshTokens().catch(() => { });
51
+ const freshToken = getBridgeAuth().getTokens()?.accessToken;
52
+ const freshHeaders = new Headers(init?.headers);
53
+ if (freshToken)
54
+ freshHeaders.set('Authorization', `Bearer ${freshToken}`);
55
+ else
56
+ freshHeaders.delete('Authorization');
57
+ return baseFetch(input, { ...init, headers: freshHeaders });
58
+ };
59
+ }
@@ -61,6 +61,17 @@ export interface StartBridgeRuntimeOptions {
61
61
  * Must be called AFTER `bridgeConfig.initConfig({...})` runs — typically from
62
62
  * `<BridgeBootstrap />`'s onMount.
63
63
  */
64
+ /**
65
+ * Patch globalThis.fetch so every request to the bridge API automatically
66
+ * gets the current access token injected as Authorization: Bearer, and
67
+ * TOKEN_VERSION_STALE responses are retried with a fresh token.
68
+ *
69
+ * Idempotent — safe to call from both bridgeBootstrap() (load-function context,
70
+ * before any component mounts) and startBridgeRuntime() (onMount). Whichever
71
+ * runs first installs the patch; the second call is a no-op.
72
+ * Restored by stopBridgeRuntime().
73
+ */
74
+ export declare function installBridgeAuthFetch(): void;
64
75
  export declare function startBridgeRuntime(options?: StartBridgeRuntimeOptions): void;
65
76
  /**
66
77
  * Stop the runtime. Idempotent — safe to call without a prior start. Flushes
@@ -44,12 +44,14 @@
44
44
  import { RealtimeClient, useBridge, } from '@nebulr-group/bridge-auth-core';
45
45
  import { getConfig } from '../client/stores/config.store.js';
46
46
  import { getBridgeAuth, tokenStore } from './bridge-instance.js';
47
+ import { wrapFetchWithBridgeAuth } from './bridge-fetch.js';
47
48
  import { applySessionSnapshot } from './snapshot-stores.js';
48
49
  import { bridgeEvents } from './events.js';
49
50
  import { _setRealtimeStatus } from './realtime-status.js';
50
51
  let _realtime;
51
52
  let _unsubscribeAuth;
52
53
  let _currentAuthToken;
54
+ let _originalFetch;
53
55
  const _onOpenSubs = new Set();
54
56
  const _onCloseSubs = new Set();
55
57
  const _onSnapshotSubs = new Set();
@@ -60,10 +62,30 @@ const _onUserStateSubs = new Set();
60
62
  * Must be called AFTER `bridgeConfig.initConfig({...})` runs — typically from
61
63
  * `<BridgeBootstrap />`'s onMount.
62
64
  */
65
+ /**
66
+ * Patch globalThis.fetch so every request to the bridge API automatically
67
+ * gets the current access token injected as Authorization: Bearer, and
68
+ * TOKEN_VERSION_STALE responses are retried with a fresh token.
69
+ *
70
+ * Idempotent — safe to call from both bridgeBootstrap() (load-function context,
71
+ * before any component mounts) and startBridgeRuntime() (onMount). Whichever
72
+ * runs first installs the patch; the second call is a no-op.
73
+ * Restored by stopBridgeRuntime().
74
+ */
75
+ export function installBridgeAuthFetch() {
76
+ if (_originalFetch)
77
+ return; // already installed
78
+ if (typeof globalThis === 'undefined' || typeof globalThis.fetch === 'undefined')
79
+ return;
80
+ const config = getConfig();
81
+ _originalFetch = globalThis.fetch;
82
+ globalThis.fetch = wrapFetchWithBridgeAuth(_originalFetch, config.apiBaseUrl ?? 'https://api.thebridge.dev');
83
+ }
63
84
  export function startBridgeRuntime(options = {}) {
64
85
  if (_realtime)
65
86
  return;
66
87
  const config = getConfig();
88
+ installBridgeAuthFetch();
67
89
  // `appId` may come from BridgeAuth's API context if available; falls back
68
90
  // to the value from `getConfig()`. The auth context one is what gets bound
69
91
  // to the per-app channel scope at boot, before any user JWT arrives.
@@ -81,8 +103,18 @@ export function startBridgeRuntime(options = {}) {
81
103
  appId: _bootstrapAppId,
82
104
  getAuthToken: () => _currentAuthToken,
83
105
  });
106
+ let _connectedOnce = false;
84
107
  _realtime.setOnOpen(() => {
85
108
  _setRealtimeStatus('open');
109
+ // On reconnect (not initial connect), proactively refresh tokens.
110
+ // If the WS was down when tokenVersion was bumped on the server, the
111
+ // client missed the user.state_changed broadcast. Refreshing here
112
+ // syncs tokens before the first post-reconnect request can fail with
113
+ // TOKEN_VERSION_STALE.
114
+ if (_connectedOnce) {
115
+ getBridgeAuth().refreshTokens().catch(() => { });
116
+ }
117
+ _connectedOnce = true;
86
118
  for (const fn of _onOpenSubs) {
87
119
  try {
88
120
  fn();
@@ -217,6 +249,10 @@ export async function stopBridgeRuntime() {
217
249
  _realtime = undefined;
218
250
  }
219
251
  _currentAuthToken = undefined;
252
+ if (_originalFetch) {
253
+ globalThis.fetch = _originalFetch;
254
+ _originalFetch = undefined;
255
+ }
220
256
  }
221
257
  /**
222
258
  * Get the shared RealtimeClient. Returns `undefined` if `startBridgeRuntime()`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebulr-group/bridge-svelte",
3
- "version": "0.4.0-beta.0",
3
+ "version": "0.4.0-beta.2",
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.0"
70
+ "@nebulr-group/bridge-auth-core": "0.4.0-beta.2"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@sveltejs/kit": "^2.16.0",