@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.
|
|
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.
|
|
70
|
+
"@nebulr-group/bridge-auth-core": "0.4.0-beta.2"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@sveltejs/kit": "^2.16.0",
|