@usions/sdk 2.1.5 → 2.2.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/package.json +5 -3
- package/src/browser.js +172 -1
- package/src/modules/core.js +55 -0
- package/src/modules/game-methods.js +107 -1
- package/src/modules/game-proxy.js +4 -0
- package/src/modules/misc.js +4 -0
- package/src/modules/wallet.js +5 -1
- package/types/index.d.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usions/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Usion Mini App SDK for iframe games and services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/modules/index.js",
|
|
@@ -25,10 +25,12 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "rollup -c",
|
|
27
27
|
"build:copy": "npm run build && cp src/browser.js ../../web/public/usion-sdk.js && cp src/browser.js ../../microservices/shared/usion-sdk.js",
|
|
28
|
-
"watch": "rollup -c --watch"
|
|
28
|
+
"watch": "rollup -c --watch",
|
|
29
|
+
"test": "npm run build && vitest run"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
|
-
"rollup": "^4.0.0"
|
|
32
|
+
"rollup": "^4.0.0",
|
|
33
|
+
"vitest": "^2.1.9"
|
|
32
34
|
},
|
|
33
35
|
"keywords": [
|
|
34
36
|
"usion",
|
package/src/browser.js
CHANGED
|
@@ -13,6 +13,58 @@ var Usion = (function () {
|
|
|
13
13
|
return ++_requestId;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// Trusted origin of the host shell that embedded us (web iframe only).
|
|
17
|
+
// Resolved lazily from the real embedder, never from message contents.
|
|
18
|
+
let _parentOrigin = null;
|
|
19
|
+
|
|
20
|
+
function _resolveParentOrigin() {
|
|
21
|
+
try {
|
|
22
|
+
if (window.location.ancestorOrigins && window.location.ancestorOrigins.length) {
|
|
23
|
+
return window.location.ancestorOrigins[0];
|
|
24
|
+
}
|
|
25
|
+
} catch (e) { /* not supported */ }
|
|
26
|
+
try {
|
|
27
|
+
if (typeof document !== 'undefined' && document.referrer) {
|
|
28
|
+
return new URL(document.referrer).origin;
|
|
29
|
+
}
|
|
30
|
+
} catch (e) { /* malformed referrer */ }
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decide whether an incoming postMessage may be trusted.
|
|
36
|
+
*
|
|
37
|
+
* The host shell is the only legitimate sender. A sibling iframe or any other
|
|
38
|
+
* script on the page must NOT be able to forge messages (e.g. a fake
|
|
39
|
+
* PAYMENT_SUCCESS that unlocks paid value for free).
|
|
40
|
+
*
|
|
41
|
+
* - React Native WebView: messages are delivered in-process and carry no
|
|
42
|
+
* usable origin/source, so they are trusted.
|
|
43
|
+
* - Web iframe: the only window that equals `window.parent` is the real
|
|
44
|
+
* embedder. `event.source` is set by the browser and cannot be spoofed, so
|
|
45
|
+
* `event.source === window.parent` rejects siblings and self-posts. We then
|
|
46
|
+
* cross-check `event.origin` against the embedder's origin as defense-in-depth.
|
|
47
|
+
* - Not embedded (standalone / tests): nothing to protect against; allowed.
|
|
48
|
+
*
|
|
49
|
+
* @param {MessageEvent} event
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
function isTrustedMessageSource(event) {
|
|
53
|
+
if (typeof window === 'undefined') return true;
|
|
54
|
+
if (window.ReactNativeWebView) return true;
|
|
55
|
+
if (window.parent === window) return true;
|
|
56
|
+
|
|
57
|
+
if (event && event.source && event.source !== window.parent) return false;
|
|
58
|
+
|
|
59
|
+
if (_parentOrigin === null) {
|
|
60
|
+
_parentOrigin = _resolveParentOrigin();
|
|
61
|
+
}
|
|
62
|
+
if (_parentOrigin && event && event.origin && event.origin !== _parentOrigin) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
16
68
|
/**
|
|
17
69
|
* Core Usion object with init, _post, _request
|
|
18
70
|
*/
|
|
@@ -49,6 +101,9 @@ var Usion = (function () {
|
|
|
49
101
|
|
|
50
102
|
// Setup global message handler
|
|
51
103
|
window.addEventListener('message', function(event) {
|
|
104
|
+
// Reject messages from anything other than the host shell.
|
|
105
|
+
if (!isTrustedMessageSource(event)) return;
|
|
106
|
+
|
|
52
107
|
let data;
|
|
53
108
|
try {
|
|
54
109
|
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
@@ -394,6 +449,10 @@ var Usion = (function () {
|
|
|
394
449
|
|
|
395
450
|
// Listen for response
|
|
396
451
|
function handler(event) {
|
|
452
|
+
// Only honor payment results from the trusted host shell — a forged
|
|
453
|
+
// PAYMENT_SUCCESS must never resolve this promise.
|
|
454
|
+
if (!isTrustedMessageSource(event)) return;
|
|
455
|
+
|
|
397
456
|
let response;
|
|
398
457
|
try {
|
|
399
458
|
response = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
@@ -780,6 +839,7 @@ var Usion = (function () {
|
|
|
780
839
|
* Usion SDK Misc — submit, error, exit, share, log, on, requestPayment (legacy)
|
|
781
840
|
*/
|
|
782
841
|
|
|
842
|
+
|
|
783
843
|
const miscMethods = {
|
|
784
844
|
/**
|
|
785
845
|
* Request payment from user (legacy method)
|
|
@@ -886,6 +946,8 @@ var Usion = (function () {
|
|
|
886
946
|
*/
|
|
887
947
|
on: function(type, callback) {
|
|
888
948
|
window.addEventListener('message', function(event) {
|
|
949
|
+
if (!isTrustedMessageSource(event)) return;
|
|
950
|
+
|
|
889
951
|
let data;
|
|
890
952
|
try {
|
|
891
953
|
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
@@ -1367,6 +1429,7 @@ var Usion = (function () {
|
|
|
1367
1429
|
* Usion SDK Game Proxy — postMessage relay through parent app
|
|
1368
1430
|
*/
|
|
1369
1431
|
|
|
1432
|
+
|
|
1370
1433
|
/**
|
|
1371
1434
|
* Add proxy connection methods to game module
|
|
1372
1435
|
* @param {object} game - The game module object
|
|
@@ -1423,6 +1486,8 @@ var Usion = (function () {
|
|
|
1423
1486
|
self._proxyListenerSetup = true;
|
|
1424
1487
|
|
|
1425
1488
|
window.addEventListener('message', function(event) {
|
|
1489
|
+
if (!isTrustedMessageSource(event)) return;
|
|
1490
|
+
|
|
1426
1491
|
var data;
|
|
1427
1492
|
try {
|
|
1428
1493
|
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
@@ -1636,7 +1701,10 @@ var Usion = (function () {
|
|
|
1636
1701
|
const self = this;
|
|
1637
1702
|
|
|
1638
1703
|
if (self.directMode) {
|
|
1639
|
-
self._sendDirect(
|
|
1704
|
+
self._sendDirect('action', {
|
|
1705
|
+
action_type: actionType || 'default',
|
|
1706
|
+
action_data: actionData || {}
|
|
1707
|
+
});
|
|
1640
1708
|
return Promise.resolve({ success: true });
|
|
1641
1709
|
}
|
|
1642
1710
|
|
|
@@ -1836,6 +1904,109 @@ var Usion = (function () {
|
|
|
1836
1904
|
}
|
|
1837
1905
|
};
|
|
1838
1906
|
|
|
1907
|
+
// ───────────────────────────────────────────────────────────
|
|
1908
|
+
// Cross-reload state persistence
|
|
1909
|
+
// ───────────────────────────────────────────────────────────
|
|
1910
|
+
// When a mini-app's iframe is unmounted and later re-mounted (e.g. the
|
|
1911
|
+
// user navigates back to the chat and re-opens the game from the same
|
|
1912
|
+
// room), the entire JS context is destroyed. Server-side room state is
|
|
1913
|
+
// preserved, but anything the game holds in memory — board state, phase,
|
|
1914
|
+
// whose turn it is, placement choices — is lost. The platform sync
|
|
1915
|
+
// mechanism only replays raw actions; reconstructing client-visible
|
|
1916
|
+
// state from a zero baseline is fragile or impossible.
|
|
1917
|
+
//
|
|
1918
|
+
// These helpers give every game a uniform way to snapshot whatever it
|
|
1919
|
+
// needs to localStorage. Keys are scoped to (player_id, room_id) so a
|
|
1920
|
+
// single browser can hold independent state for different rooms or
|
|
1921
|
+
// accounts without collision, and "play a different match in the same
|
|
1922
|
+
// room" naturally collides — which is the correct outcome.
|
|
1923
|
+
|
|
1924
|
+
const STATE_KEY_PREFIX = '_usion_game_state:';
|
|
1925
|
+
|
|
1926
|
+
function _stateKey(self) {
|
|
1927
|
+
const rid = self.roomId || (Usion.config && Usion.config.roomId);
|
|
1928
|
+
const pid = self.playerId
|
|
1929
|
+
|| (Usion.user && typeof Usion.user.getId === 'function' && Usion.user.getId())
|
|
1930
|
+
|| (Usion.config && Usion.config.userId);
|
|
1931
|
+
if (!rid || !pid) return null;
|
|
1932
|
+
return STATE_KEY_PREFIX + pid + ':' + rid;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/**
|
|
1936
|
+
* Persist arbitrary JSON-serializable game state across iframe reloads.
|
|
1937
|
+
* The mini-app decides the schema; the SDK only stores/retrieves.
|
|
1938
|
+
* @param {*} state - Any JSON-serializable value
|
|
1939
|
+
* @returns {boolean} true if saved, false if not (no room/player yet, or storage error)
|
|
1940
|
+
*/
|
|
1941
|
+
game.saveState = function(state) {
|
|
1942
|
+
const self = this;
|
|
1943
|
+
const key = _stateKey(self);
|
|
1944
|
+
if (!key) return false;
|
|
1945
|
+
try {
|
|
1946
|
+
localStorage.setItem(key, JSON.stringify({ state: state, savedAt: Date.now() }));
|
|
1947
|
+
return true;
|
|
1948
|
+
} catch (e) {
|
|
1949
|
+
// Quota exceeded, private-mode rejection, etc. — non-fatal.
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
/**
|
|
1955
|
+
* Retrieve previously-saved state for the current (player, room).
|
|
1956
|
+
* @returns {*} The saved state value, or null if none / unreadable.
|
|
1957
|
+
*/
|
|
1958
|
+
game.loadState = function() {
|
|
1959
|
+
const self = this;
|
|
1960
|
+
const key = _stateKey(self);
|
|
1961
|
+
if (!key) return null;
|
|
1962
|
+
try {
|
|
1963
|
+
const raw = localStorage.getItem(key);
|
|
1964
|
+
if (!raw) return null;
|
|
1965
|
+
const parsed = JSON.parse(raw);
|
|
1966
|
+
return parsed && parsed.state !== undefined ? parsed.state : null;
|
|
1967
|
+
} catch (e) {
|
|
1968
|
+
return null;
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
/**
|
|
1973
|
+
* Drop any persisted state for the current (player, room).
|
|
1974
|
+
* Call this when the game ends or starts fresh, so the next iframe
|
|
1975
|
+
* mount in the same room doesn't pick up stale data.
|
|
1976
|
+
*/
|
|
1977
|
+
game.clearState = function() {
|
|
1978
|
+
const self = this;
|
|
1979
|
+
const key = _stateKey(self);
|
|
1980
|
+
if (!key) return;
|
|
1981
|
+
try { localStorage.removeItem(key); } catch (e) { /* non-fatal */ }
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Forward a debug snapshot to the parent platform. The platform renders
|
|
1986
|
+
* it in a top-right overlay when the iframe host is opened with
|
|
1987
|
+
* `?debug=1`. The payload schema is up to the game — anything JSON-
|
|
1988
|
+
* serializable. No-op when not running inside an iframe.
|
|
1989
|
+
*
|
|
1990
|
+
* Games should call this at every meaningful state transition (turn
|
|
1991
|
+
* change, action sent, action received, sync, phase change, etc.) so
|
|
1992
|
+
* the overlay reflects live state.
|
|
1993
|
+
*
|
|
1994
|
+
* @param {object} payload - Arbitrary JSON-serializable debug data
|
|
1995
|
+
*/
|
|
1996
|
+
game.debug = function(payload) {
|
|
1997
|
+
try {
|
|
1998
|
+
// Must work in both web iframes (window.parent !== window) and
|
|
1999
|
+
// React Native WebView (window.parent === window, but a host bridge
|
|
2000
|
+
// exists at window.ReactNativeWebView). Usion._post handles routing
|
|
2001
|
+
// for both; this guard just avoids no-op work in standalone pages.
|
|
2002
|
+
var inFrame = window.parent && window.parent !== window;
|
|
2003
|
+
var inRNWebView = !!window.ReactNativeWebView;
|
|
2004
|
+
if (inFrame || inRNWebView) {
|
|
2005
|
+
Usion._post({ type: 'GAME_DEBUG', payload: payload || {} });
|
|
2006
|
+
}
|
|
2007
|
+
} catch (e) { /* non-fatal */ }
|
|
2008
|
+
};
|
|
2009
|
+
|
|
1839
2010
|
/**
|
|
1840
2011
|
* Get connection status
|
|
1841
2012
|
* @returns {boolean}
|
package/src/modules/core.js
CHANGED
|
@@ -10,6 +10,58 @@ export function getNextRequestId() {
|
|
|
10
10
|
return ++_requestId;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Trusted origin of the host shell that embedded us (web iframe only).
|
|
14
|
+
// Resolved lazily from the real embedder, never from message contents.
|
|
15
|
+
let _parentOrigin = null;
|
|
16
|
+
|
|
17
|
+
function _resolveParentOrigin() {
|
|
18
|
+
try {
|
|
19
|
+
if (window.location.ancestorOrigins && window.location.ancestorOrigins.length) {
|
|
20
|
+
return window.location.ancestorOrigins[0];
|
|
21
|
+
}
|
|
22
|
+
} catch (e) { /* not supported */ }
|
|
23
|
+
try {
|
|
24
|
+
if (typeof document !== 'undefined' && document.referrer) {
|
|
25
|
+
return new URL(document.referrer).origin;
|
|
26
|
+
}
|
|
27
|
+
} catch (e) { /* malformed referrer */ }
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Decide whether an incoming postMessage may be trusted.
|
|
33
|
+
*
|
|
34
|
+
* The host shell is the only legitimate sender. A sibling iframe or any other
|
|
35
|
+
* script on the page must NOT be able to forge messages (e.g. a fake
|
|
36
|
+
* PAYMENT_SUCCESS that unlocks paid value for free).
|
|
37
|
+
*
|
|
38
|
+
* - React Native WebView: messages are delivered in-process and carry no
|
|
39
|
+
* usable origin/source, so they are trusted.
|
|
40
|
+
* - Web iframe: the only window that equals `window.parent` is the real
|
|
41
|
+
* embedder. `event.source` is set by the browser and cannot be spoofed, so
|
|
42
|
+
* `event.source === window.parent` rejects siblings and self-posts. We then
|
|
43
|
+
* cross-check `event.origin` against the embedder's origin as defense-in-depth.
|
|
44
|
+
* - Not embedded (standalone / tests): nothing to protect against; allowed.
|
|
45
|
+
*
|
|
46
|
+
* @param {MessageEvent} event
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
export function isTrustedMessageSource(event) {
|
|
50
|
+
if (typeof window === 'undefined') return true;
|
|
51
|
+
if (window.ReactNativeWebView) return true;
|
|
52
|
+
if (window.parent === window) return true;
|
|
53
|
+
|
|
54
|
+
if (event && event.source && event.source !== window.parent) return false;
|
|
55
|
+
|
|
56
|
+
if (_parentOrigin === null) {
|
|
57
|
+
_parentOrigin = _resolveParentOrigin();
|
|
58
|
+
}
|
|
59
|
+
if (_parentOrigin && event && event.origin && event.origin !== _parentOrigin) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
13
65
|
/**
|
|
14
66
|
* Core Usion object with init, _post, _request
|
|
15
67
|
*/
|
|
@@ -46,6 +98,9 @@ export const core = {
|
|
|
46
98
|
|
|
47
99
|
// Setup global message handler
|
|
48
100
|
window.addEventListener('message', function(event) {
|
|
101
|
+
// Reject messages from anything other than the host shell.
|
|
102
|
+
if (!isTrustedMessageSource(event)) return;
|
|
103
|
+
|
|
49
104
|
let data;
|
|
50
105
|
try {
|
|
51
106
|
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
@@ -121,7 +121,10 @@ export function applyGameMethods(game, Usion) {
|
|
|
121
121
|
const self = this;
|
|
122
122
|
|
|
123
123
|
if (self.directMode) {
|
|
124
|
-
self._sendDirect(
|
|
124
|
+
self._sendDirect('action', {
|
|
125
|
+
action_type: actionType || 'default',
|
|
126
|
+
action_data: actionData || {}
|
|
127
|
+
});
|
|
125
128
|
return Promise.resolve({ success: true });
|
|
126
129
|
}
|
|
127
130
|
|
|
@@ -321,6 +324,109 @@ export function applyGameMethods(game, Usion) {
|
|
|
321
324
|
}
|
|
322
325
|
};
|
|
323
326
|
|
|
327
|
+
// ───────────────────────────────────────────────────────────
|
|
328
|
+
// Cross-reload state persistence
|
|
329
|
+
// ───────────────────────────────────────────────────────────
|
|
330
|
+
// When a mini-app's iframe is unmounted and later re-mounted (e.g. the
|
|
331
|
+
// user navigates back to the chat and re-opens the game from the same
|
|
332
|
+
// room), the entire JS context is destroyed. Server-side room state is
|
|
333
|
+
// preserved, but anything the game holds in memory — board state, phase,
|
|
334
|
+
// whose turn it is, placement choices — is lost. The platform sync
|
|
335
|
+
// mechanism only replays raw actions; reconstructing client-visible
|
|
336
|
+
// state from a zero baseline is fragile or impossible.
|
|
337
|
+
//
|
|
338
|
+
// These helpers give every game a uniform way to snapshot whatever it
|
|
339
|
+
// needs to localStorage. Keys are scoped to (player_id, room_id) so a
|
|
340
|
+
// single browser can hold independent state for different rooms or
|
|
341
|
+
// accounts without collision, and "play a different match in the same
|
|
342
|
+
// room" naturally collides — which is the correct outcome.
|
|
343
|
+
|
|
344
|
+
const STATE_KEY_PREFIX = '_usion_game_state:';
|
|
345
|
+
|
|
346
|
+
function _stateKey(self) {
|
|
347
|
+
const rid = self.roomId || (Usion.config && Usion.config.roomId);
|
|
348
|
+
const pid = self.playerId
|
|
349
|
+
|| (Usion.user && typeof Usion.user.getId === 'function' && Usion.user.getId())
|
|
350
|
+
|| (Usion.config && Usion.config.userId);
|
|
351
|
+
if (!rid || !pid) return null;
|
|
352
|
+
return STATE_KEY_PREFIX + pid + ':' + rid;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Persist arbitrary JSON-serializable game state across iframe reloads.
|
|
357
|
+
* The mini-app decides the schema; the SDK only stores/retrieves.
|
|
358
|
+
* @param {*} state - Any JSON-serializable value
|
|
359
|
+
* @returns {boolean} true if saved, false if not (no room/player yet, or storage error)
|
|
360
|
+
*/
|
|
361
|
+
game.saveState = function(state) {
|
|
362
|
+
const self = this;
|
|
363
|
+
const key = _stateKey(self);
|
|
364
|
+
if (!key) return false;
|
|
365
|
+
try {
|
|
366
|
+
localStorage.setItem(key, JSON.stringify({ state: state, savedAt: Date.now() }));
|
|
367
|
+
return true;
|
|
368
|
+
} catch (e) {
|
|
369
|
+
// Quota exceeded, private-mode rejection, etc. — non-fatal.
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Retrieve previously-saved state for the current (player, room).
|
|
376
|
+
* @returns {*} The saved state value, or null if none / unreadable.
|
|
377
|
+
*/
|
|
378
|
+
game.loadState = function() {
|
|
379
|
+
const self = this;
|
|
380
|
+
const key = _stateKey(self);
|
|
381
|
+
if (!key) return null;
|
|
382
|
+
try {
|
|
383
|
+
const raw = localStorage.getItem(key);
|
|
384
|
+
if (!raw) return null;
|
|
385
|
+
const parsed = JSON.parse(raw);
|
|
386
|
+
return parsed && parsed.state !== undefined ? parsed.state : null;
|
|
387
|
+
} catch (e) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Drop any persisted state for the current (player, room).
|
|
394
|
+
* Call this when the game ends or starts fresh, so the next iframe
|
|
395
|
+
* mount in the same room doesn't pick up stale data.
|
|
396
|
+
*/
|
|
397
|
+
game.clearState = function() {
|
|
398
|
+
const self = this;
|
|
399
|
+
const key = _stateKey(self);
|
|
400
|
+
if (!key) return;
|
|
401
|
+
try { localStorage.removeItem(key); } catch (e) { /* non-fatal */ }
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Forward a debug snapshot to the parent platform. The platform renders
|
|
406
|
+
* it in a top-right overlay when the iframe host is opened with
|
|
407
|
+
* `?debug=1`. The payload schema is up to the game — anything JSON-
|
|
408
|
+
* serializable. No-op when not running inside an iframe.
|
|
409
|
+
*
|
|
410
|
+
* Games should call this at every meaningful state transition (turn
|
|
411
|
+
* change, action sent, action received, sync, phase change, etc.) so
|
|
412
|
+
* the overlay reflects live state.
|
|
413
|
+
*
|
|
414
|
+
* @param {object} payload - Arbitrary JSON-serializable debug data
|
|
415
|
+
*/
|
|
416
|
+
game.debug = function(payload) {
|
|
417
|
+
try {
|
|
418
|
+
// Must work in both web iframes (window.parent !== window) and
|
|
419
|
+
// React Native WebView (window.parent === window, but a host bridge
|
|
420
|
+
// exists at window.ReactNativeWebView). Usion._post handles routing
|
|
421
|
+
// for both; this guard just avoids no-op work in standalone pages.
|
|
422
|
+
var inFrame = window.parent && window.parent !== window;
|
|
423
|
+
var inRNWebView = !!window.ReactNativeWebView;
|
|
424
|
+
if (inFrame || inRNWebView) {
|
|
425
|
+
Usion._post({ type: 'GAME_DEBUG', payload: payload || {} });
|
|
426
|
+
}
|
|
427
|
+
} catch (e) { /* non-fatal */ }
|
|
428
|
+
};
|
|
429
|
+
|
|
324
430
|
/**
|
|
325
431
|
* Get connection status
|
|
326
432
|
* @returns {boolean}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Usion SDK Game Proxy — postMessage relay through parent app
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { isTrustedMessageSource } from './core.js';
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Add proxy connection methods to game module
|
|
7
9
|
* @param {object} game - The game module object
|
|
@@ -58,6 +60,8 @@ export function applyGameProxy(game, Usion) {
|
|
|
58
60
|
self._proxyListenerSetup = true;
|
|
59
61
|
|
|
60
62
|
window.addEventListener('message', function(event) {
|
|
63
|
+
if (!isTrustedMessageSource(event)) return;
|
|
64
|
+
|
|
61
65
|
var data;
|
|
62
66
|
try {
|
|
63
67
|
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
package/src/modules/misc.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Usion SDK Misc — submit, error, exit, share, log, on, requestPayment (legacy)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { isTrustedMessageSource } from './core.js';
|
|
6
|
+
|
|
5
7
|
export const miscMethods = {
|
|
6
8
|
/**
|
|
7
9
|
* Request payment from user (legacy method)
|
|
@@ -108,6 +110,8 @@ export const miscMethods = {
|
|
|
108
110
|
*/
|
|
109
111
|
on: function(type, callback) {
|
|
110
112
|
window.addEventListener('message', function(event) {
|
|
113
|
+
if (!isTrustedMessageSource(event)) return;
|
|
114
|
+
|
|
111
115
|
let data;
|
|
112
116
|
try {
|
|
113
117
|
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
package/src/modules/wallet.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Usion SDK Wallet Module — wallet and payment operations
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { getNextRequestId } from './core.js';
|
|
5
|
+
import { getNextRequestId, isTrustedMessageSource } from './core.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @param {object} Usion - Reference to the main Usion object
|
|
@@ -57,6 +57,10 @@ export function createWalletModule(Usion) {
|
|
|
57
57
|
|
|
58
58
|
// Listen for response
|
|
59
59
|
function handler(event) {
|
|
60
|
+
// Only honor payment results from the trusted host shell — a forged
|
|
61
|
+
// PAYMENT_SUCCESS must never resolve this promise.
|
|
62
|
+
if (!isTrustedMessageSource(event)) return;
|
|
63
|
+
|
|
60
64
|
let response;
|
|
61
65
|
try {
|
|
62
66
|
response = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
package/types/index.d.ts
CHANGED
|
@@ -213,6 +213,16 @@ export interface GameModule {
|
|
|
213
213
|
requestRematch(): void;
|
|
214
214
|
forfeit(): Promise<{ success: boolean; error?: string }>;
|
|
215
215
|
|
|
216
|
+
// Persisted state — survives iframe unmount/remount.
|
|
217
|
+
// Keyed by (player_id, room_id) in localStorage. Schema is up to the game.
|
|
218
|
+
saveState<T = any>(state: T): boolean;
|
|
219
|
+
loadState<T = any>(): T | null;
|
|
220
|
+
clearState(): void;
|
|
221
|
+
|
|
222
|
+
// Forward a debug snapshot to the parent. Rendered as a top-right
|
|
223
|
+
// overlay over the iframe when the host page is opened with `?debug=1`.
|
|
224
|
+
debug(payload: Record<string, any>): void;
|
|
225
|
+
|
|
216
226
|
// Event handlers
|
|
217
227
|
onJoined(callback: (data: GameJoinResult) => void): void;
|
|
218
228
|
onPlayerJoined(callback: (data: PlayerJoinedData) => void): void;
|
|
@@ -306,6 +316,10 @@ export interface UsionSDK {
|
|
|
306
316
|
|
|
307
317
|
// Sharing
|
|
308
318
|
share(contentType: 'audio' | 'image' | 'video' | 'text' | 'mixed', data: ShareData): void;
|
|
319
|
+
shareToFeed(contentType: 'text' | 'image' | 'video' | 'audio' | 'mixed', data: {
|
|
320
|
+
text?: string;
|
|
321
|
+
media?: Array<{ type: 'image' | 'video' | 'audio'; url: string; thumbnailUrl?: string; width?: number; height?: number; duration?: number }>;
|
|
322
|
+
}): Promise<{ success: boolean; postId?: string; shareUrl?: string }>;
|
|
309
323
|
|
|
310
324
|
// Logging
|
|
311
325
|
log(msg: string): void;
|