@bounded-sh/core 0.0.20 → 0.0.22
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/config.d.ts +5 -4
- package/dist/client/subscription-v2.d.ts +3 -3
- package/dist/index.js +102 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +102 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/client/config.d.ts
CHANGED
|
@@ -2,12 +2,13 @@ import { AuthProvider } from '../types';
|
|
|
2
2
|
export interface ClientConfig {
|
|
3
3
|
name: string;
|
|
4
4
|
logoUrl: string;
|
|
5
|
-
/** Auth method.
|
|
6
|
-
*
|
|
5
|
+
/** Auth method. Configure exactly one method at init time.
|
|
6
|
+
* 'email' = Bounded Auth human login (email OTP, inline).
|
|
7
|
+
* OAuth/social uses loginWithRedirect/
|
|
7
8
|
* loginWithPopup rather than authMethod. Text OTP uses hosted/headless OTP
|
|
8
9
|
* helpers only when explicitly enabled by the Bounded issuer. 'phantom' = connect a Solana wallet (Phantom), for
|
|
9
|
-
* crypto/onchain apps that need a real @user.address. 'guest' =
|
|
10
|
-
*
|
|
10
|
+
* crypto/onchain apps that need a real @user.address. 'guest' = anonymous
|
|
11
|
+
* device-key auth. */
|
|
11
12
|
authMethod: 'none' | 'email' | 'guest' | 'wallet' | 'rainbowkit' | 'coinbase-smart-wallet' | 'onboard' | 'phantom' | 'mobile-wallet-adapter' | 'privy' | 'privy-expo';
|
|
12
13
|
wsApiUrl: string;
|
|
13
14
|
apiUrl: string;
|
|
@@ -60,7 +60,7 @@ export declare function hasActiveConnection(): boolean;
|
|
|
60
60
|
* room authority — the connection is already routed to the room DO, so the
|
|
61
61
|
* client never names a destination.
|
|
62
62
|
*/
|
|
63
|
-
export declare function wsIntent(appId: string, roomRoutePath: string, intent: unknown): boolean
|
|
63
|
+
export declare function wsIntent(appId: string, roomRoutePath: string, intent: unknown, isServer: boolean): Promise<boolean>;
|
|
64
64
|
/**
|
|
65
65
|
* Send a live-room intent over the EXISTING per-room socket and AWAIT the server
|
|
66
66
|
* ack — so a policy/auth denial (401/403/404/503) REJECTS instead of being
|
|
@@ -69,6 +69,6 @@ export declare function wsIntent(appId: string, roomRoutePath: string, intent: u
|
|
|
69
69
|
* Promise that resolves on ack / rejects on error, or `undefined` if no live
|
|
70
70
|
* socket exists yet (caller falls back to HTTP, which also surfaces errors).
|
|
71
71
|
*/
|
|
72
|
-
export declare function wsIntentReliable(appId: string, roomRoutePath: string, intent: unknown): Promise<{
|
|
72
|
+
export declare function wsIntentReliable(appId: string, roomRoutePath: string, intent: unknown, isServer: boolean): Promise<{
|
|
73
73
|
ok: true;
|
|
74
|
-
}
|
|
74
|
+
} | undefined>;
|
package/dist/index.js
CHANGED
|
@@ -30,8 +30,8 @@ let clientConfig = {
|
|
|
30
30
|
// User configured settings
|
|
31
31
|
name: '',
|
|
32
32
|
logoUrl: '',
|
|
33
|
-
// Bounded production is the
|
|
34
|
-
//
|
|
33
|
+
// Bounded production is the endpoint default. Apps still choose one explicit
|
|
34
|
+
// auth method at init time. Pass `network: 'bounded-staging'` to target staging.
|
|
35
35
|
network: 'bounded-production',
|
|
36
36
|
wsApiUrl: 'wss://realtime.bounded.sh',
|
|
37
37
|
apiUrl: 'https://realtime.bounded.sh',
|
|
@@ -39,14 +39,15 @@ let clientConfig = {
|
|
|
39
39
|
humanAuthApiUrl: 'https://auth.bounded.sh',
|
|
40
40
|
functionsUrl: 'https://functions.bounded.sh',
|
|
41
41
|
appId: '',
|
|
42
|
-
//
|
|
43
|
-
// for
|
|
42
|
+
// No hidden auth fallback: browser clients must pass authMethod explicitly
|
|
43
|
+
// (for example 'email', 'guest', 'phantom', 'privy', or 'privy-expo').
|
|
44
|
+
// Hosted OAuth/social uses loginWithRedirect/loginWithPopup with authMethod:'email'.
|
|
44
45
|
// Text OTP is off by default and uses hosted/headless text helpers only when
|
|
45
46
|
// Bounded explicitly enables it for the issuer. For
|
|
46
47
|
// crypto/onchain wallet login use authMethod:'phantom' (Solana / Phantom), or
|
|
47
48
|
// signInAnonymously() for zero-friction 'guest' accounts. ('wallet' is an
|
|
48
49
|
// unimplemented stub; don't use.)
|
|
49
|
-
authMethod: '
|
|
50
|
+
authMethod: 'none',
|
|
50
51
|
chain: '',
|
|
51
52
|
rpcUrl: '',
|
|
52
53
|
skipBackendInit: true,
|
|
@@ -112,10 +113,14 @@ function init(newConfig) {
|
|
|
112
113
|
}
|
|
113
114
|
// Bounded is client-driven: defaults are Bounded production, `network`
|
|
114
115
|
// switches the whole endpoint set (e.g. 'bounded-staging'), and anything
|
|
115
|
-
// passed explicitly wins. No `/config` round-trip
|
|
116
|
-
//
|
|
116
|
+
// passed explicitly wins. No `/config` round-trip; browser SDKs still pass
|
|
117
|
+
// one explicit authMethod so there is no hidden auth-provider fallback.
|
|
117
118
|
// defaults (bounded-production) < network preset < explicit newConfig
|
|
118
|
-
|
|
119
|
+
if (newConfig.network !== undefined && !(newConfig.network in BOUNDED_NETWORKS)) {
|
|
120
|
+
reject(new Error(`Unsupported Bounded network "${String(newConfig.network)}". Expected bounded, bounded-staging, or bounded-production.`));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const preset = newConfig.network ? BOUNDED_NETWORKS[newConfig.network] : {};
|
|
119
124
|
clientConfig = Object.assign(Object.assign(Object.assign({}, clientConfig), preset), newConfig);
|
|
120
125
|
isInitialized = true;
|
|
121
126
|
resolve();
|
|
@@ -5430,6 +5435,48 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
|
|
|
5430
5435
|
function principalFromIdToken(idToken) {
|
|
5431
5436
|
return idToken ? `t${hashForKey(idToken)}` : null;
|
|
5432
5437
|
}
|
|
5438
|
+
function livePrincipalFromIdToken(idToken) {
|
|
5439
|
+
if (!idToken)
|
|
5440
|
+
return null;
|
|
5441
|
+
try {
|
|
5442
|
+
const payload = JSON.parse(decodeBase64Url(idToken.split('.')[1]));
|
|
5443
|
+
const userId = payload['custom:userId'];
|
|
5444
|
+
if (typeof userId === 'string' && userId.length > 0)
|
|
5445
|
+
return userId;
|
|
5446
|
+
const walletAddress = payload['custom:walletAddress'];
|
|
5447
|
+
if (typeof walletAddress === 'string' && walletAddress.length > 0)
|
|
5448
|
+
return walletAddress;
|
|
5449
|
+
const subject = payload.sub;
|
|
5450
|
+
return typeof subject === 'string' && subject.length > 0 ? subject : null;
|
|
5451
|
+
}
|
|
5452
|
+
catch (_a) {
|
|
5453
|
+
return null;
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
function authSnapshotFromToken(idToken) {
|
|
5457
|
+
return {
|
|
5458
|
+
tokenFingerprint: principalFromIdToken(idToken),
|
|
5459
|
+
principal: livePrincipalFromIdToken(idToken),
|
|
5460
|
+
};
|
|
5461
|
+
}
|
|
5462
|
+
function connectionMatchesAuthToken(connection, idToken) {
|
|
5463
|
+
const current = authSnapshotFromToken(idToken);
|
|
5464
|
+
if (!current.tokenFingerprint) {
|
|
5465
|
+
return connection.authTokenFingerprint === null && connection.authPrincipal === null;
|
|
5466
|
+
}
|
|
5467
|
+
if (connection.authTokenFingerprint && connection.authTokenFingerprint === current.tokenFingerprint) {
|
|
5468
|
+
return true;
|
|
5469
|
+
}
|
|
5470
|
+
return !!connection.authPrincipal && !!current.principal && connection.authPrincipal === current.principal;
|
|
5471
|
+
}
|
|
5472
|
+
async function canSendAuthenticatedLiveIntent(connection, isServer) {
|
|
5473
|
+
const currentToken = await getIdToken(isServer);
|
|
5474
|
+
if (!currentToken)
|
|
5475
|
+
return false;
|
|
5476
|
+
if (!connection.authTokenFingerprint && !connection.authPrincipal)
|
|
5477
|
+
return false;
|
|
5478
|
+
return connectionMatchesAuthToken(connection, currentToken);
|
|
5479
|
+
}
|
|
5433
5480
|
async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
|
|
5434
5481
|
// Per-subscription wallet override (server WalletClient.subscribe): key by
|
|
5435
5482
|
// the wallet's own opaque token material, mirroring getReadPrincipalKey. Do
|
|
@@ -5505,6 +5552,9 @@ function rememberAmbientAuthFailure(error) {
|
|
|
5505
5552
|
function failConnectionAuth(connection, error) {
|
|
5506
5553
|
connection.authFailure = error;
|
|
5507
5554
|
connection.pendingAuthToken = null;
|
|
5555
|
+
connection.pendingAuthSnapshot = null;
|
|
5556
|
+
connection.authTokenFingerprint = null;
|
|
5557
|
+
connection.authPrincipal = null;
|
|
5508
5558
|
connection.isConnecting = false;
|
|
5509
5559
|
connection.isConnected = false;
|
|
5510
5560
|
connection.isAuthenticating = false;
|
|
@@ -5723,16 +5773,37 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5723
5773
|
if (connection.authFailure) {
|
|
5724
5774
|
throw connection.authFailure;
|
|
5725
5775
|
}
|
|
5776
|
+
let currentAuthToken;
|
|
5726
5777
|
try {
|
|
5727
|
-
await getConnectionAuthToken(isServer, authTokenProvider);
|
|
5778
|
+
currentAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
|
|
5728
5779
|
}
|
|
5729
5780
|
catch (error) {
|
|
5730
5781
|
const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
|
|
5731
5782
|
failConnectionAuth(connection, authError);
|
|
5732
5783
|
throw authError;
|
|
5733
5784
|
}
|
|
5785
|
+
if (connection.isConnected &&
|
|
5786
|
+
!connection.isAuthenticating &&
|
|
5787
|
+
!connectionMatchesAuthToken(connection, currentAuthToken)) {
|
|
5788
|
+
if (connection.tokenRefreshTimer) {
|
|
5789
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
5790
|
+
connection.tokenRefreshTimer = null;
|
|
5791
|
+
}
|
|
5792
|
+
try {
|
|
5793
|
+
connection.ws.close();
|
|
5794
|
+
}
|
|
5795
|
+
catch (_a) {
|
|
5796
|
+
// Already closing.
|
|
5797
|
+
}
|
|
5798
|
+
connection.ws = null;
|
|
5799
|
+
connection.isConnected = false;
|
|
5800
|
+
connections.delete(connection.key);
|
|
5801
|
+
}
|
|
5802
|
+
else {
|
|
5803
|
+
connection.authFailure = null;
|
|
5804
|
+
return connection;
|
|
5805
|
+
}
|
|
5734
5806
|
connection.authFailure = null;
|
|
5735
|
-
return connection;
|
|
5736
5807
|
}
|
|
5737
5808
|
let initialAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
|
|
5738
5809
|
// Create new connection
|
|
@@ -5750,6 +5821,9 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5750
5821
|
routePath: roomKey ? routePath : undefined,
|
|
5751
5822
|
authTokenProvider,
|
|
5752
5823
|
pendingAuthToken: null,
|
|
5824
|
+
pendingAuthSnapshot: null,
|
|
5825
|
+
authTokenFingerprint: null,
|
|
5826
|
+
authPrincipal: null,
|
|
5753
5827
|
tokenRefreshTimer: null,
|
|
5754
5828
|
consecutiveAuthFailures: 0,
|
|
5755
5829
|
authFailure: null,
|
|
@@ -5787,6 +5861,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5787
5861
|
throw authError;
|
|
5788
5862
|
}
|
|
5789
5863
|
connection.pendingAuthToken = authToken || null;
|
|
5864
|
+
connection.pendingAuthSnapshot = authSnapshotFromToken(authToken);
|
|
5790
5865
|
if (authToken) {
|
|
5791
5866
|
// Successful token acquisition — reset failure counter
|
|
5792
5867
|
connection.consecutiveAuthFailures = 0;
|
|
@@ -5826,6 +5901,8 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5826
5901
|
}
|
|
5827
5902
|
else {
|
|
5828
5903
|
connection.isAuthenticating = false;
|
|
5904
|
+
connection.authTokenFingerprint = null;
|
|
5905
|
+
connection.authPrincipal = null;
|
|
5829
5906
|
replaySubscriptions(connection);
|
|
5830
5907
|
}
|
|
5831
5908
|
});
|
|
@@ -5865,10 +5942,12 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5865
5942
|
return connection;
|
|
5866
5943
|
}
|
|
5867
5944
|
function handleServerMessage(connection, message) {
|
|
5868
|
-
var _a;
|
|
5945
|
+
var _a, _b, _c, _d, _e;
|
|
5869
5946
|
switch (message.type) {
|
|
5870
5947
|
case 'authenticated': {
|
|
5871
5948
|
connection.isAuthenticating = false;
|
|
5949
|
+
connection.authTokenFingerprint = (_b = (_a = connection.pendingAuthSnapshot) === null || _a === void 0 ? void 0 : _a.tokenFingerprint) !== null && _b !== void 0 ? _b : null;
|
|
5950
|
+
connection.authPrincipal = (_d = (_c = connection.pendingAuthSnapshot) === null || _c === void 0 ? void 0 : _c.principal) !== null && _d !== void 0 ? _d : null;
|
|
5872
5951
|
replaySubscriptions(connection);
|
|
5873
5952
|
break;
|
|
5874
5953
|
}
|
|
@@ -5939,7 +6018,7 @@ function handleServerMessage(connection, message) {
|
|
|
5939
6018
|
connection.pendingRequests.delete(message.requestId);
|
|
5940
6019
|
clearTimeout(pendingSet.timer);
|
|
5941
6020
|
if (message.statusCode >= 400) {
|
|
5942
|
-
pendingSet.reject(new Error(((
|
|
6021
|
+
pendingSet.reject(new Error(((_e = message.body) === null || _e === void 0 ? void 0 : _e.message) || `Set failed with status ${message.statusCode}`));
|
|
5943
6022
|
}
|
|
5944
6023
|
else {
|
|
5945
6024
|
pendingSet.resolve(message.body);
|
|
@@ -6467,13 +6546,16 @@ function hasActiveConnection() {
|
|
|
6467
6546
|
* room authority — the connection is already routed to the room DO, so the
|
|
6468
6547
|
* client never names a destination.
|
|
6469
6548
|
*/
|
|
6470
|
-
function wsIntent(appId, roomRoutePath, intent) {
|
|
6549
|
+
async function wsIntent(appId, roomRoutePath, intent, isServer) {
|
|
6471
6550
|
const roomKey = roomKeyFromRoutePath(roomRoutePath);
|
|
6472
6551
|
const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
|
|
6473
6552
|
const connection = connections.get(connKey);
|
|
6474
6553
|
if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
|
|
6475
6554
|
return false;
|
|
6476
6555
|
}
|
|
6556
|
+
if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
|
|
6557
|
+
return false;
|
|
6558
|
+
}
|
|
6477
6559
|
try {
|
|
6478
6560
|
connection.ws.send(JSON.stringify({ type: 'intent', intent }));
|
|
6479
6561
|
return true;
|
|
@@ -6490,13 +6572,16 @@ function wsIntent(appId, roomRoutePath, intent) {
|
|
|
6490
6572
|
* Promise that resolves on ack / rejects on error, or `undefined` if no live
|
|
6491
6573
|
* socket exists yet (caller falls back to HTTP, which also surfaces errors).
|
|
6492
6574
|
*/
|
|
6493
|
-
function wsIntentReliable(appId, roomRoutePath, intent) {
|
|
6575
|
+
async function wsIntentReliable(appId, roomRoutePath, intent, isServer) {
|
|
6494
6576
|
const roomKey = roomKeyFromRoutePath(roomRoutePath);
|
|
6495
6577
|
const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
|
|
6496
6578
|
const connection = connections.get(connKey);
|
|
6497
6579
|
if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
|
|
6498
6580
|
return undefined;
|
|
6499
6581
|
}
|
|
6582
|
+
if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
|
|
6583
|
+
return undefined;
|
|
6584
|
+
}
|
|
6500
6585
|
const requestId = generateRequestId();
|
|
6501
6586
|
return new Promise((resolve, reject) => {
|
|
6502
6587
|
const timer = setTimeout(() => {
|
|
@@ -6908,13 +6993,13 @@ async function intent(roomPath, intent, opts = {}) {
|
|
|
6908
6993
|
const hasAuthOverride = !!((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders);
|
|
6909
6994
|
if (!hasAuthOverride) {
|
|
6910
6995
|
if (opts.fireAndForget) {
|
|
6911
|
-
if (wsIntent(config.appId, normalizedRoomPath, intent))
|
|
6996
|
+
if (await wsIntent(config.appId, normalizedRoomPath, intent, config.isServer))
|
|
6912
6997
|
return { ok: true };
|
|
6913
6998
|
}
|
|
6914
6999
|
else {
|
|
6915
|
-
const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
|
|
7000
|
+
const ack = await wsIntentReliable(config.appId, normalizedRoomPath, intent, config.isServer);
|
|
6916
7001
|
if (ack)
|
|
6917
|
-
return
|
|
7002
|
+
return ack;
|
|
6918
7003
|
}
|
|
6919
7004
|
}
|
|
6920
7005
|
const base = realtimeHttpBase(config.wsApiUrl);
|