@nebulr-group/bridge-auth-core 0.1.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/billing/bridge-subscription.d.ts +71 -0
- package/dist/billing/bridge-subscription.d.ts.map +1 -0
- package/dist/billing/bridge-subscription.js +209 -0
- package/dist/billing/bridge-subscription.js.map +1 -0
- package/dist/billing/entitlements-store.d.ts +61 -0
- package/dist/billing/entitlements-store.d.ts.map +1 -0
- package/dist/billing/entitlements-store.js +127 -0
- package/dist/billing/entitlements-store.js.map +1 -0
- package/dist/billing/fetch-billing-state.d.ts +9 -0
- package/dist/billing/fetch-billing-state.d.ts.map +1 -0
- package/dist/billing/fetch-billing-state.js +22 -0
- package/dist/billing/fetch-billing-state.js.map +1 -0
- package/dist/billing/lock-signal.d.ts +6 -0
- package/dist/billing/lock-signal.d.ts.map +1 -0
- package/dist/billing/lock-signal.js +8 -0
- package/dist/billing/lock-signal.js.map +1 -0
- package/dist/billing/quota-store.d.ts +86 -0
- package/dist/billing/quota-store.d.ts.map +1 -0
- package/dist/billing/quota-store.js +180 -0
- package/dist/billing/quota-store.js.map +1 -0
- package/dist/billing/types.d.ts +64 -0
- package/dist/billing/types.d.ts.map +1 -0
- package/dist/billing/types.js +49 -0
- package/dist/billing/types.js.map +1 -0
- package/dist/billing/use-bridge.d.ts +131 -0
- package/dist/billing/use-bridge.d.ts.map +1 -0
- package/dist/billing/use-bridge.js +181 -0
- package/dist/billing/use-bridge.js.map +1 -0
- package/dist/bridge-auth.d.ts +36 -1
- package/dist/bridge-auth.d.ts.map +1 -1
- package/dist/bridge-auth.js +57 -3
- package/dist/bridge-auth.js.map +1 -1
- package/dist/direct-auth.d.ts +1 -0
- package/dist/direct-auth.d.ts.map +1 -1
- package/dist/direct-auth.js +7 -0
- package/dist/direct-auth.js.map +1 -1
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -1
- package/dist/flags/attribute-providers.d.ts +229 -0
- package/dist/flags/attribute-providers.d.ts.map +1 -0
- package/dist/flags/attribute-providers.js +375 -0
- package/dist/flags/attribute-providers.js.map +1 -0
- package/dist/flags/dev-attribute-provider.d.ts +67 -0
- package/dist/flags/dev-attribute-provider.d.ts.map +1 -0
- package/dist/flags/dev-attribute-provider.js +200 -0
- package/dist/flags/dev-attribute-provider.js.map +1 -0
- package/dist/flags/evaluator.d.ts +58 -0
- package/dist/flags/evaluator.d.ts.map +1 -0
- package/dist/flags/evaluator.js +160 -0
- package/dist/flags/evaluator.js.map +1 -0
- package/dist/flags/flag.d.ts +199 -0
- package/dist/flags/flag.d.ts.map +1 -0
- package/dist/flags/flag.js +337 -0
- package/dist/flags/flag.js.map +1 -0
- package/dist/flags/identity.d.ts +77 -0
- package/dist/flags/identity.d.ts.map +1 -0
- package/dist/flags/identity.js +134 -0
- package/dist/flags/identity.js.map +1 -0
- package/dist/flags/index.d.ts +20 -0
- package/dist/flags/index.d.ts.map +1 -0
- package/dist/flags/index.js +21 -0
- package/dist/flags/index.js.map +1 -0
- package/dist/flags/operators.d.ts +63 -0
- package/dist/flags/operators.d.ts.map +1 -0
- package/dist/flags/operators.js +299 -0
- package/dist/flags/operators.js.map +1 -0
- package/dist/flags/propagation.d.ts +16 -0
- package/dist/flags/propagation.d.ts.map +1 -0
- package/dist/flags/propagation.js +107 -0
- package/dist/flags/propagation.js.map +1 -0
- package/dist/flags/realtime.d.ts +328 -0
- package/dist/flags/realtime.d.ts.map +1 -0
- package/dist/flags/realtime.js +504 -0
- package/dist/flags/realtime.js.map +1 -0
- package/dist/flags/runtime-mode.d.ts +32 -0
- package/dist/flags/runtime-mode.d.ts.map +1 -0
- package/dist/flags/runtime-mode.js +84 -0
- package/dist/flags/runtime-mode.js.map +1 -0
- package/dist/flags/telemetry.d.ts +79 -0
- package/dist/flags/telemetry.d.ts.map +1 -0
- package/dist/flags/telemetry.js +286 -0
- package/dist/flags/telemetry.js.map +1 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +11 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/management-types.d.ts +38 -0
- package/dist/management-types.d.ts.map +1 -1
- package/dist/route-guard.d.ts.map +1 -1
- package/dist/route-guard.js +15 -5
- package/dist/route-guard.js.map +1 -1
- package/dist/token-manager.d.ts +3 -1
- package/dist/token-manager.d.ts.map +1 -1
- package/dist/token-manager.js +10 -6
- package/dist/token-manager.js.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/usage/index.d.ts +3 -0
- package/dist/usage/index.d.ts.map +1 -0
- package/dist/usage/index.js +4 -0
- package/dist/usage/index.js.map +1 -0
- package/dist/usage/storage/durable-storage.d.ts +47 -0
- package/dist/usage/storage/durable-storage.d.ts.map +1 -0
- package/dist/usage/storage/durable-storage.js +27 -0
- package/dist/usage/storage/durable-storage.js.map +1 -0
- package/dist/usage/storage/index.d.ts +7 -0
- package/dist/usage/storage/index.d.ts.map +1 -0
- package/dist/usage/storage/index.js +28 -0
- package/dist/usage/storage/index.js.map +1 -0
- package/dist/usage/storage/indexeddb-storage.d.ts +18 -0
- package/dist/usage/storage/indexeddb-storage.d.ts.map +1 -0
- package/dist/usage/storage/indexeddb-storage.js +159 -0
- package/dist/usage/storage/indexeddb-storage.js.map +1 -0
- package/dist/usage/storage/node-fs-storage.d.ts +29 -0
- package/dist/usage/storage/node-fs-storage.d.ts.map +1 -0
- package/dist/usage/storage/node-fs-storage.js +180 -0
- package/dist/usage/storage/node-fs-storage.js.map +1 -0
- package/dist/usage/storage/noop-storage.d.ts +17 -0
- package/dist/usage/storage/noop-storage.d.ts.map +1 -0
- package/dist/usage/storage/noop-storage.js +67 -0
- package/dist/usage/storage/noop-storage.js.map +1 -0
- package/dist/usage/usage-reporter.d.ts +64 -0
- package/dist/usage/usage-reporter.d.ts.map +1 -0
- package/dist/usage/usage-reporter.js +234 -0
- package/dist/usage/usage-reporter.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// SDK realtime client (TBP-150).
|
|
2
|
+
//
|
|
3
|
+
// Auto-discovers the workspace's pub/sub protocol from `GET /realtime/config`
|
|
4
|
+
// (TBP-147), authorizes via `POST /realtime/authorize` (TBP-151), and connects
|
|
5
|
+
// to receive live flag updates + per-user identity changes.
|
|
6
|
+
//
|
|
7
|
+
// Two protocols, one shape:
|
|
8
|
+
// - `centrifugo`: WebSocket to a Centrifugo server using a signed connect token
|
|
9
|
+
// - `appsync`: planned (TBP-148); not implemented here
|
|
10
|
+
//
|
|
11
|
+
// `noop` server-side → realtime is disabled; the SDK falls back to periodic
|
|
12
|
+
// poll or simply doesn't get live updates.
|
|
13
|
+
//
|
|
14
|
+
// Messages received on the workspace channel update the BridgeFlags cache
|
|
15
|
+
// (`upsert` / `remove`). Per-user channel messages are handled via callbacks
|
|
16
|
+
// the framework SDK supplies (token refresh, attribute changes — see TBP-90).
|
|
17
|
+
export class RealtimeClient {
|
|
18
|
+
cfg;
|
|
19
|
+
ws;
|
|
20
|
+
state = 'idle';
|
|
21
|
+
reconnectDelayMs;
|
|
22
|
+
reconnectTimer;
|
|
23
|
+
bridge;
|
|
24
|
+
onUserStateHook;
|
|
25
|
+
onSubscriptionPlanChangedHook;
|
|
26
|
+
onBillingLifecycleHook;
|
|
27
|
+
onQuotaUpdatedHook;
|
|
28
|
+
onEntitlementsChangedHook;
|
|
29
|
+
// Phase 3 (TBP-287/314) — fans `session.snapshot` out to whichever slices
|
|
30
|
+
// the consumer has wired (app.branding, tenant.subscription, etc.).
|
|
31
|
+
onSnapshotHook;
|
|
32
|
+
onOpenHook;
|
|
33
|
+
onCloseHook;
|
|
34
|
+
stopped = false;
|
|
35
|
+
constructor(cfg) {
|
|
36
|
+
const defaultWs = ((url, protocols) =>
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
new globalThis.WebSocket(url, protocols));
|
|
39
|
+
this.cfg = {
|
|
40
|
+
apiBaseUrl: cfg.apiBaseUrl.replace(/\/+$/, ''),
|
|
41
|
+
apiKey: cfg.apiKey,
|
|
42
|
+
appId: cfg.appId,
|
|
43
|
+
workspaceId: cfg.workspaceId,
|
|
44
|
+
userId: cfg.userId,
|
|
45
|
+
enabled: cfg.enabled !== false,
|
|
46
|
+
reconnectBaseMs: cfg.reconnectBaseMs ?? 1000,
|
|
47
|
+
reconnectMaxMs: cfg.reconnectMaxMs ?? 30_000,
|
|
48
|
+
websocketFactory: cfg.websocketFactory ?? defaultWs,
|
|
49
|
+
fetchFn: cfg.fetchFn ?? (typeof fetch !== 'undefined' ? fetch : undefined),
|
|
50
|
+
getAuthToken: cfg.getAuthToken,
|
|
51
|
+
};
|
|
52
|
+
this.reconnectDelayMs = this.cfg.reconnectBaseMs;
|
|
53
|
+
}
|
|
54
|
+
/** Attach to a BridgeFlags instance — flag updates auto-apply to its cache. */
|
|
55
|
+
attach(bridge) {
|
|
56
|
+
this.bridge = bridge;
|
|
57
|
+
}
|
|
58
|
+
/** Register a hook for per-user channel messages. */
|
|
59
|
+
setOnUserState(hook) {
|
|
60
|
+
this.onUserStateHook = hook;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Billing 2.0 US-3 — register a hook for canonical subscription plan-change
|
|
64
|
+
* events. The billing reactive surface (`BridgeSubscription.attach(rt)`)
|
|
65
|
+
* wires this up; framework SDKs typically don't call it directly.
|
|
66
|
+
*/
|
|
67
|
+
setOnSubscriptionPlanChanged(hook) {
|
|
68
|
+
this.onSubscriptionPlanChangedHook = hook;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Billing 2.0 US-5+ — register a hook for all canonical lifecycle events
|
|
72
|
+
* (payment.*, subscription.*, dunning.*, entitlements.*). `BridgeSubscription.attach(rt)`
|
|
73
|
+
* wires this; user-level event handlers can also register here via
|
|
74
|
+
* `useBridge().handle({ "payment.failed": ... })`.
|
|
75
|
+
*/
|
|
76
|
+
setOnBillingLifecycle(hook) {
|
|
77
|
+
this.onBillingLifecycleHook = hook;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Billing 2.0 US-11 — register a hook for `quota.updated` payloads on the
|
|
81
|
+
* workspace channel. `useBridge().quota(metric)` consumers wire this up
|
|
82
|
+
* so live counter UI reflects server-side ingest without polling.
|
|
83
|
+
*/
|
|
84
|
+
setOnQuotaUpdated(hook) {
|
|
85
|
+
this.onQuotaUpdatedHook = hook;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Billing 2.0 US-12 — register a hook for `entitlements.changed` payloads
|
|
89
|
+
* on the workspace channel that carry the full entitlements map.
|
|
90
|
+
* `useBridge().entitlements.can(...)` consumers wire this up so the cache
|
|
91
|
+
* replaces wholesale on every diff.
|
|
92
|
+
*
|
|
93
|
+
* Note: the legacy `BillingLifecycleMessage` `entitlements.changed` kind
|
|
94
|
+
* (signal-only, no payload) keeps firing through `setOnBillingLifecycle`
|
|
95
|
+
* — this hook ONLY fires when the wire payload includes the `entitlements`
|
|
96
|
+
* field. Allows both old and new consumers to coexist during rollout.
|
|
97
|
+
*/
|
|
98
|
+
setOnEntitlementsChanged(hook) {
|
|
99
|
+
this.onEntitlementsChangedHook = hook;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Phase 3 (TBP-287/314) — register a hook for `session.snapshot`. The server
|
|
103
|
+
* publishes one on every successful per-user channel subscribe (initial
|
|
104
|
+
* connect AND reconnect). Framework SDKs use this to pre-populate the
|
|
105
|
+
* `bridge.app.branding` / `bridge.tenant.{subscription,entitlements}` /
|
|
106
|
+
* `bridge.user` slices on first paint, eliminating the per-slice REST
|
|
107
|
+
* hydrate round-trips that the legacy bootstrap path required.
|
|
108
|
+
*
|
|
109
|
+
* Composition is fixed (no consumer config). If a later release promotes
|
|
110
|
+
* another slice into the snapshot, that slice's `.load()` becomes a no-op
|
|
111
|
+
* automatically — consumers don't need code changes.
|
|
112
|
+
*/
|
|
113
|
+
setOnSnapshot(hook) {
|
|
114
|
+
this.onSnapshotHook = hook;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Register a hook fired when the WebSocket transitions to `'open'` —
|
|
118
|
+
* fires on initial connect AND on every successful reconnect. Framework
|
|
119
|
+
* SDKs use this to re-fire startup tasks (e.g. cache hydration) that
|
|
120
|
+
* may have been missed during an outage.
|
|
121
|
+
*/
|
|
122
|
+
setOnOpen(hook) {
|
|
123
|
+
this.onOpenHook = hook;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Register a hook fired when the WebSocket transitions to `'closed'` —
|
|
127
|
+
* use for surfacing connection status to consumers (e.g. an "offline"
|
|
128
|
+
* indicator). Fires on intentional close as well; check `getState()`
|
|
129
|
+
* if you need to distinguish.
|
|
130
|
+
*/
|
|
131
|
+
setOnClose(hook) {
|
|
132
|
+
this.onCloseHook = hook;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Re-run the authorize step against the current `getAuthToken()` value and
|
|
136
|
+
* re-open the WebSocket. Used by the framework SDK on every token rotation
|
|
137
|
+
* where the userId is unchanged but the JWT value rotated (post-refresh).
|
|
138
|
+
*
|
|
139
|
+
* Without this, the existing connection keeps riding the OLD token until
|
|
140
|
+
* Centrifugo's own connection-token TTL drops it — a strictly-larger
|
|
141
|
+
* blast-radius window than necessary.
|
|
142
|
+
*
|
|
143
|
+
* Behavior:
|
|
144
|
+
* - Disabled / stopped → no-op.
|
|
145
|
+
* - Mid-`connecting` → no-op; the in-flight authorize() reads the current
|
|
146
|
+
* token by closure, so it will already pick up the new value.
|
|
147
|
+
* - Open → drop the ws ref (so the old socket's onclose treats itself as
|
|
148
|
+
* stale via the identity guard in `openWebSocket`), close it with
|
|
149
|
+
* `1000 / sdk.reauthorize`, reset backoff, then `start()` immediately.
|
|
150
|
+
*/
|
|
151
|
+
async reauthorize() {
|
|
152
|
+
if (!this.cfg.enabled || this.stopped)
|
|
153
|
+
return;
|
|
154
|
+
if (this.state === 'connecting')
|
|
155
|
+
return;
|
|
156
|
+
if (this.reconnectTimer) {
|
|
157
|
+
clearTimeout(this.reconnectTimer);
|
|
158
|
+
this.reconnectTimer = undefined;
|
|
159
|
+
}
|
|
160
|
+
this.reconnectDelayMs = this.cfg.reconnectBaseMs;
|
|
161
|
+
if (this.state === 'open' && this.ws) {
|
|
162
|
+
const oldWs = this.ws;
|
|
163
|
+
this.ws = undefined;
|
|
164
|
+
this.state = 'closed';
|
|
165
|
+
try {
|
|
166
|
+
oldWs.close(1000, 'sdk.reauthorize');
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// ignore
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await this.start();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Phase 2 (TBP-307) — Set/update the appId after initial start. Framework
|
|
176
|
+
* SDKs call this when the app context first lands (e.g. after the first
|
|
177
|
+
* authorize round-trip exposes the JWT `aid` claim). Triggers a reconnect
|
|
178
|
+
* so the new `app:<appId>` channel is included in the next authorize.
|
|
179
|
+
*/
|
|
180
|
+
setAppId(appId) {
|
|
181
|
+
if (this.cfg.appId === appId)
|
|
182
|
+
return;
|
|
183
|
+
this.cfg.appId = appId;
|
|
184
|
+
if (this.state === 'open' && this.ws) {
|
|
185
|
+
this.ws.close(1000, 'sdk.setAppId');
|
|
186
|
+
// onclose → scheduleReconnect → start() picks up updated channelsToSubscribe()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Phase 2 (TBP-307) — Set/update the workspaceId after initial start. Used
|
|
191
|
+
* by framework SDKs when the tenant context lands or changes (workspace
|
|
192
|
+
* switcher, tenant join/leave). Triggers a reconnect.
|
|
193
|
+
*/
|
|
194
|
+
setWorkspaceId(workspaceId) {
|
|
195
|
+
if (this.cfg.workspaceId === workspaceId)
|
|
196
|
+
return;
|
|
197
|
+
this.cfg.workspaceId = workspaceId;
|
|
198
|
+
if (this.state === 'open' && this.ws) {
|
|
199
|
+
this.ws.close(1000, 'sdk.setWorkspaceId');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Update the userId after initial start — used by the framework SDK to
|
|
204
|
+
* subscribe to the per-user channel when the user logs in post-bootstrap.
|
|
205
|
+
* Triggers a reconnect so the new channel is included in the next authorize.
|
|
206
|
+
*/
|
|
207
|
+
setUserId(userId) {
|
|
208
|
+
if (this.cfg.userId === userId)
|
|
209
|
+
return;
|
|
210
|
+
this.cfg.userId = userId;
|
|
211
|
+
if (this.state === 'open' && this.ws) {
|
|
212
|
+
this.ws.close(1000, 'sdk.setUserId');
|
|
213
|
+
// onclose fires → scheduleReconnect → start() picks up updated channelsToSubscribe()
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Begin connecting. Idempotent. */
|
|
217
|
+
async start() {
|
|
218
|
+
if (!this.cfg.enabled || this.stopped)
|
|
219
|
+
return;
|
|
220
|
+
if (this.state !== 'idle' && this.state !== 'closed')
|
|
221
|
+
return;
|
|
222
|
+
this.state = 'connecting';
|
|
223
|
+
try {
|
|
224
|
+
const serverConfig = await this.fetchServerConfig();
|
|
225
|
+
if (serverConfig.kind === 'noop' || !serverConfig.endpoint) {
|
|
226
|
+
this.state = 'closed';
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (serverConfig.kind !== 'centrifugo') {
|
|
230
|
+
// appsync impl is deferred (TBP-148). Silently no-op for now.
|
|
231
|
+
this.state = 'closed';
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const channels = this.channelsToSubscribe();
|
|
235
|
+
const auth = await this.authorize(channels);
|
|
236
|
+
this.openWebSocket(serverConfig.endpoint, auth);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
this.state = 'closed';
|
|
240
|
+
this.scheduleReconnect();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Close the connection. Idempotent. */
|
|
244
|
+
async stop() {
|
|
245
|
+
this.stopped = true;
|
|
246
|
+
if (this.reconnectTimer) {
|
|
247
|
+
clearTimeout(this.reconnectTimer);
|
|
248
|
+
this.reconnectTimer = undefined;
|
|
249
|
+
}
|
|
250
|
+
if (this.ws) {
|
|
251
|
+
try {
|
|
252
|
+
this.ws.close(1000, 'sdk.stop');
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// ignore
|
|
256
|
+
}
|
|
257
|
+
this.ws = undefined;
|
|
258
|
+
}
|
|
259
|
+
this.state = 'closed';
|
|
260
|
+
}
|
|
261
|
+
/** Read connection state. */
|
|
262
|
+
getState() {
|
|
263
|
+
return this.state;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Channels this client subscribes to — the three canonical channels:
|
|
267
|
+
* - `app:<appId>` — flag mutations, app config (app-scoped)
|
|
268
|
+
* - `workspace:<wsId>` — subscription, quota, entitlement (tenant-scoped)
|
|
269
|
+
* - `user:<userId>` — user-state, role/attr change (user-scoped)
|
|
270
|
+
*
|
|
271
|
+
* Each id is optional; the SDK skips the channel if its id isn't configured.
|
|
272
|
+
* The anonymous-only standalone case ends up with just `app:<appId>`.
|
|
273
|
+
*/
|
|
274
|
+
channelsToSubscribe() {
|
|
275
|
+
const out = [];
|
|
276
|
+
if (this.cfg.appId)
|
|
277
|
+
out.push(`app:${this.cfg.appId}`);
|
|
278
|
+
if (this.cfg.workspaceId)
|
|
279
|
+
out.push(`workspace:${this.cfg.workspaceId}`);
|
|
280
|
+
if (this.cfg.userId)
|
|
281
|
+
out.push(`user:${this.cfg.userId}`);
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
// ── private ───────────────────────────────────────────────────────────────
|
|
285
|
+
async fetchServerConfig() {
|
|
286
|
+
const res = await this.cfg.fetchFn(`${this.cfg.apiBaseUrl}/realtime/config`, {
|
|
287
|
+
method: 'GET',
|
|
288
|
+
headers: { 'x-api-key': this.cfg.apiKey },
|
|
289
|
+
});
|
|
290
|
+
if (!res.ok) {
|
|
291
|
+
throw new Error(`realtime config fetch failed: ${res.status}`);
|
|
292
|
+
}
|
|
293
|
+
return (await res.json());
|
|
294
|
+
}
|
|
295
|
+
async authorize(channels) {
|
|
296
|
+
const token = this.cfg.getAuthToken?.() ?? this.cfg.apiKey;
|
|
297
|
+
const res = await this.cfg.fetchFn(`${this.cfg.apiBaseUrl}/realtime/authorize`, {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: {
|
|
300
|
+
Authorization: `Bearer ${token}`,
|
|
301
|
+
'Content-Type': 'application/json',
|
|
302
|
+
},
|
|
303
|
+
body: JSON.stringify({ channels }),
|
|
304
|
+
});
|
|
305
|
+
if (!res.ok) {
|
|
306
|
+
throw new Error(`realtime authorize failed: ${res.status}`);
|
|
307
|
+
}
|
|
308
|
+
return (await res.json());
|
|
309
|
+
}
|
|
310
|
+
openWebSocket(endpoint, auth) {
|
|
311
|
+
const ws = this.cfg.websocketFactory(endpoint);
|
|
312
|
+
this.ws = ws;
|
|
313
|
+
ws.onopen = () => {
|
|
314
|
+
if (this.ws !== ws)
|
|
315
|
+
return;
|
|
316
|
+
this.state = 'open';
|
|
317
|
+
this.reconnectDelayMs = this.cfg.reconnectBaseMs;
|
|
318
|
+
// Send connect with the signed token + channels. Centrifugo expects a
|
|
319
|
+
// command frame like `{ "connect": { "token": "..." }, "id": 1 }` and
|
|
320
|
+
// separate subscribe frames per channel. For v1 we send one connect
|
|
321
|
+
// and let the server-side token's `channels` claim handle subscription.
|
|
322
|
+
try {
|
|
323
|
+
ws.send(JSON.stringify({ id: 1, connect: { token: auth.signedToken } }));
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// ignore
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
this.onOpenHook?.();
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// hook errors must not break the connection
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
ws.onmessage = (ev) => {
|
|
336
|
+
if (this.ws !== ws)
|
|
337
|
+
return;
|
|
338
|
+
// Centrifugo v5 JSON protocol keepalive: the server periodically sends
|
|
339
|
+
// an empty `{}` frame and expects an empty `{}` reply. Without this
|
|
340
|
+
// echo the server closes the connection on its pong-timeout and the
|
|
341
|
+
// SDK ends up in a perpetual reconnect loop.
|
|
342
|
+
if (typeof ev.data === 'string' && ev.data === '{}') {
|
|
343
|
+
try {
|
|
344
|
+
ws.send('{}');
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// ignore — onclose will pick up a broken socket
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const parsed = parseMessage(ev.data);
|
|
353
|
+
if (parsed)
|
|
354
|
+
this.handleMessage(parsed);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// ignore malformed
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
ws.onclose = () => {
|
|
361
|
+
// Identity guard — if we've already replaced this ws (e.g. via
|
|
362
|
+
// reauthorize() dropping the ref before close), the late-firing onclose
|
|
363
|
+
// is from a stale socket. Don't flap state or fire hooks.
|
|
364
|
+
if (this.ws !== ws)
|
|
365
|
+
return;
|
|
366
|
+
this.state = 'closed';
|
|
367
|
+
try {
|
|
368
|
+
this.onCloseHook?.();
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// hook errors must not block reconnect scheduling
|
|
372
|
+
}
|
|
373
|
+
this.scheduleReconnect();
|
|
374
|
+
};
|
|
375
|
+
ws.onerror = () => {
|
|
376
|
+
// Let onclose handle reconnect — errors are noisy but not actionable.
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
handleMessage(msg) {
|
|
380
|
+
switch (msg.kind) {
|
|
381
|
+
case 'flag.updated':
|
|
382
|
+
if (this.bridge && msg.flag)
|
|
383
|
+
this.bridge.upsert(msg.flag);
|
|
384
|
+
break;
|
|
385
|
+
case 'flag.removed':
|
|
386
|
+
if (this.bridge && msg.key)
|
|
387
|
+
this.bridge.remove(msg.key);
|
|
388
|
+
break;
|
|
389
|
+
case 'user.state_changed':
|
|
390
|
+
if (this.onUserStateHook) {
|
|
391
|
+
try {
|
|
392
|
+
this.onUserStateHook(msg);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// ignore
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
case 'subscription.plan_changed':
|
|
400
|
+
if (this.onSubscriptionPlanChangedHook) {
|
|
401
|
+
try {
|
|
402
|
+
this.onSubscriptionPlanChangedHook(msg);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// ignore
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
case 'payment.failed':
|
|
410
|
+
case 'payment.succeeded':
|
|
411
|
+
case 'subscription.created':
|
|
412
|
+
case 'subscription.updated':
|
|
413
|
+
case 'subscription.canceled':
|
|
414
|
+
case 'subscription.reactivated':
|
|
415
|
+
case 'subscription.trial_started':
|
|
416
|
+
case 'subscription.trial_ending_soon':
|
|
417
|
+
case 'subscription.trial_converted':
|
|
418
|
+
case 'subscription.trial_expired':
|
|
419
|
+
case 'dunning.entered':
|
|
420
|
+
case 'dunning.retry_scheduled':
|
|
421
|
+
case 'dunning.recovered':
|
|
422
|
+
case 'dunning.exhausted':
|
|
423
|
+
if (this.onBillingLifecycleHook) {
|
|
424
|
+
try {
|
|
425
|
+
this.onBillingLifecycleHook(msg);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// ignore
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
432
|
+
case 'entitlements.changed':
|
|
433
|
+
// bridge-api always publishes the payload-carrying shape (US-12);
|
|
434
|
+
// the pre-prod signal-only fallback was removed at milestone close-out.
|
|
435
|
+
if (this.onEntitlementsChangedHook) {
|
|
436
|
+
try {
|
|
437
|
+
this.onEntitlementsChangedHook(msg);
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// ignore
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
break;
|
|
444
|
+
case 'quota.updated':
|
|
445
|
+
if (this.onQuotaUpdatedHook) {
|
|
446
|
+
try {
|
|
447
|
+
this.onQuotaUpdatedHook(msg);
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
// ignore
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
case 'session.snapshot':
|
|
455
|
+
// Phase 3 (TBP-287/314) — first-paint snapshot. Defensive check on
|
|
456
|
+
// `data` because the wire shape is deeper than the other kinds and
|
|
457
|
+
// a partial server might omit it; we never want to call the hook
|
|
458
|
+
// with `undefined`.
|
|
459
|
+
if (this.onSnapshotHook && msg.data) {
|
|
460
|
+
try {
|
|
461
|
+
this.onSnapshotHook(msg);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// ignore
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
scheduleReconnect() {
|
|
471
|
+
if (this.stopped)
|
|
472
|
+
return;
|
|
473
|
+
if (this.reconnectTimer)
|
|
474
|
+
return;
|
|
475
|
+
this.reconnectTimer = setTimeout(() => {
|
|
476
|
+
this.reconnectTimer = undefined;
|
|
477
|
+
this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, this.cfg.reconnectMaxMs);
|
|
478
|
+
void this.start();
|
|
479
|
+
}, this.reconnectDelayMs);
|
|
480
|
+
if (this.reconnectTimer?.unref)
|
|
481
|
+
this.reconnectTimer.unref();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function parseMessage(raw) {
|
|
485
|
+
if (typeof raw !== 'string')
|
|
486
|
+
return null;
|
|
487
|
+
let parsed;
|
|
488
|
+
try {
|
|
489
|
+
parsed = JSON.parse(raw);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
// Centrifugo wraps publish data in `{ push: { channel, pub: { data: {...} } } }`.
|
|
495
|
+
// We accept both that shape and a flat `{ kind: ... }` shape so the same
|
|
496
|
+
// client works with simpler transports too.
|
|
497
|
+
const data = parsed?.push?.pub?.data ??
|
|
498
|
+
parsed?.pub?.data ??
|
|
499
|
+
parsed;
|
|
500
|
+
if (!data || typeof data !== 'object' || typeof data.kind !== 'string')
|
|
501
|
+
return null;
|
|
502
|
+
return data;
|
|
503
|
+
}
|
|
504
|
+
//# sourceMappingURL=realtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime.js","sourceRoot":"","sources":["../../src/flags/realtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type BridgeRuntimeMode = 'channel' | 'pull';
|
|
2
|
+
export interface PullCacheOptions {
|
|
3
|
+
/** TTL for cached values in ms. Default 30000 (30s). */
|
|
4
|
+
ttlMs?: number;
|
|
5
|
+
/** Optional override for the default "now" clock — useful in tests. */
|
|
6
|
+
now?: () => number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Tiny TTL-bounded read cache for pull-mode SDKs.
|
|
10
|
+
*
|
|
11
|
+
* Semantics:
|
|
12
|
+
* - `get(key, fetcher)` returns the cached value if not expired; otherwise
|
|
13
|
+
* awaits `fetcher()` to populate and returns the fresh value.
|
|
14
|
+
* - Concurrent `get(key, ...)` calls share the in-flight promise (no
|
|
15
|
+
* thundering-herd against the upstream REST endpoint).
|
|
16
|
+
* - `invalidate(key?)` clears a specific key or the entire cache.
|
|
17
|
+
*
|
|
18
|
+
* Best-effort: a fetcher rejection clears the in-flight promise so the next
|
|
19
|
+
* `get()` retries instead of returning a stale value forever.
|
|
20
|
+
*/
|
|
21
|
+
export declare class BridgePullCache {
|
|
22
|
+
private readonly _entries;
|
|
23
|
+
private readonly _ttlMs;
|
|
24
|
+
private readonly _now;
|
|
25
|
+
constructor(opts?: PullCacheOptions);
|
|
26
|
+
get<T>(key: string, fetcher: () => Promise<T>): Promise<T>;
|
|
27
|
+
/** Force a re-fetch on the next get(). Pass no args to clear all keys. */
|
|
28
|
+
invalidate(key?: string): void;
|
|
29
|
+
/** Number of cached entries currently held (test/diagnostic). */
|
|
30
|
+
size(): number;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=runtime-mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-mode.d.ts","sourceRoot":"","sources":["../../src/flags/runtime-mode.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,MAAM,CAAC;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAQD;;;;;;;;;;;;GAYG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0C;IACnE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;gBAExB,IAAI,CAAC,EAAE,gBAAgB;IAK7B,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IA6BhE,0EAA0E;IAC1E,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IAQ9B,iEAAiE;IACjE,IAAI,IAAI,MAAM;CAGf"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Phase 6 (TBP-290/340) — runtime-mode abstraction.
|
|
2
|
+
//
|
|
3
|
+
// `BridgeRuntimeMode` is orthogonal to `BridgeFlagsMode` (which governs the
|
|
4
|
+
// flag-eval semantics on backend vs frontend). Runtime mode governs HOW the
|
|
5
|
+
// SDK stays fresh:
|
|
6
|
+
//
|
|
7
|
+
// 'channel' — open a WebSocket and consume push events (live updates).
|
|
8
|
+
// Use for long-running processes: SvelteKit SSR servers, NestJS
|
|
9
|
+
// services, Express APIs serving many users from one process.
|
|
10
|
+
//
|
|
11
|
+
// 'pull' — never open a WebSocket. Every read may hit REST with a small
|
|
12
|
+
// TTL cache to deduplicate close-in-time reads. Use for
|
|
13
|
+
// ephemeral runtimes: cron jobs, serverless functions, webhook
|
|
14
|
+
// handlers, CLI scripts.
|
|
15
|
+
//
|
|
16
|
+
// In 'pull' mode `bridge.events.handle()` is a no-op (push events don't exist
|
|
17
|
+
// — for server-side event reactions, use Bridge webhooks instead).
|
|
18
|
+
//
|
|
19
|
+
// In 'channel' mode `bridge.refresh()` is supported but rarely needed (the
|
|
20
|
+
// channel keeps state fresh automatically); in 'pull' mode `bridge.refresh()`
|
|
21
|
+
// explicitly invalidates the cache so the next read re-fetches.
|
|
22
|
+
/**
|
|
23
|
+
* Tiny TTL-bounded read cache for pull-mode SDKs.
|
|
24
|
+
*
|
|
25
|
+
* Semantics:
|
|
26
|
+
* - `get(key, fetcher)` returns the cached value if not expired; otherwise
|
|
27
|
+
* awaits `fetcher()` to populate and returns the fresh value.
|
|
28
|
+
* - Concurrent `get(key, ...)` calls share the in-flight promise (no
|
|
29
|
+
* thundering-herd against the upstream REST endpoint).
|
|
30
|
+
* - `invalidate(key?)` clears a specific key or the entire cache.
|
|
31
|
+
*
|
|
32
|
+
* Best-effort: a fetcher rejection clears the in-flight promise so the next
|
|
33
|
+
* `get()` retries instead of returning a stale value forever.
|
|
34
|
+
*/
|
|
35
|
+
export class BridgePullCache {
|
|
36
|
+
_entries = new Map();
|
|
37
|
+
_ttlMs;
|
|
38
|
+
_now;
|
|
39
|
+
constructor(opts) {
|
|
40
|
+
this._ttlMs = opts?.ttlMs ?? 30_000;
|
|
41
|
+
this._now = opts?.now ?? (() => Date.now());
|
|
42
|
+
}
|
|
43
|
+
async get(key, fetcher) {
|
|
44
|
+
const existing = this._entries.get(key);
|
|
45
|
+
if (existing) {
|
|
46
|
+
if (existing.inflight)
|
|
47
|
+
return existing.inflight;
|
|
48
|
+
if (existing.expiresAt > this._now())
|
|
49
|
+
return existing.value;
|
|
50
|
+
}
|
|
51
|
+
const inflight = fetcher()
|
|
52
|
+
.then((v) => {
|
|
53
|
+
this._entries.set(key, {
|
|
54
|
+
value: v,
|
|
55
|
+
expiresAt: this._now() + this._ttlMs,
|
|
56
|
+
});
|
|
57
|
+
return v;
|
|
58
|
+
})
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
// Clear so the next call retries instead of returning stale forever.
|
|
61
|
+
this._entries.delete(key);
|
|
62
|
+
throw err;
|
|
63
|
+
});
|
|
64
|
+
this._entries.set(key, {
|
|
65
|
+
value: existing?.value,
|
|
66
|
+
expiresAt: 0,
|
|
67
|
+
inflight,
|
|
68
|
+
});
|
|
69
|
+
return inflight;
|
|
70
|
+
}
|
|
71
|
+
/** Force a re-fetch on the next get(). Pass no args to clear all keys. */
|
|
72
|
+
invalidate(key) {
|
|
73
|
+
if (key === undefined) {
|
|
74
|
+
this._entries.clear();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this._entries.delete(key);
|
|
78
|
+
}
|
|
79
|
+
/** Number of cached entries currently held (test/diagnostic). */
|
|
80
|
+
size() {
|
|
81
|
+
return this._entries.size;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=runtime-mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-mode.js","sourceRoot":"","sources":["../../src/flags/runtime-mode.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,mBAAmB;AACnB,EAAE;AACF,yEAAyE;AACzE,8EAA8E;AAC9E,4EAA4E;AAC5E,EAAE;AACF,6EAA6E;AAC7E,sEAAsE;AACtE,6EAA6E;AAC7E,uCAAuC;AACvC,EAAE;AACF,8EAA8E;AAC9E,mEAAmE;AACnE,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,gEAAgE;AAiBhE;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,eAAe;IACT,QAAQ,GAAG,IAAI,GAAG,EAA+B,CAAC;IAClD,MAAM,CAAS;IACf,IAAI,CAAe;IAEpC,YAAY,IAAuB;QACjC,IAAI,CAAC,MAAM,GAAG,IAAI,EAAE,KAAK,IAAI,MAAM,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,GAAG,CAAI,GAAW,EAAE,OAAyB;QACjD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAA8B,CAAC;QACrE,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,QAAQ,CAAC,QAAQ;gBAAE,OAAO,QAAQ,CAAC,QAAQ,CAAC;YAChD,IAAI,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE;gBAAE,OAAO,QAAQ,CAAC,KAAK,CAAC;QAC9D,CAAC;QAED,MAAM,QAAQ,GAAG,OAAO,EAAE;aACvB,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YACV,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE;gBACrB,KAAK,EAAE,CAAC;gBACR,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,MAAM;aACrC,CAAC,CAAC;YACH,OAAO,CAAC,CAAC;QACX,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,qEAAqE;YACrE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1B,MAAM,GAAG,CAAC;QACZ,CAAC,CAAC,CAAC;QAEL,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE;YACrB,KAAK,EAAE,QAAQ,EAAE,KAAU;YAC3B,SAAS,EAAE,CAAC;YACZ,QAAQ;SACT,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,0EAA0E;IAC1E,UAAU,CAAC,GAAY;QACrB,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IAED,iEAAiE;IACjE,IAAI;QACF,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;CACF"}
|