@oyna360/game-sdk 0.6.3 → 0.7.1

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/index.js CHANGED
@@ -12,14 +12,21 @@ exports.unlockAchievement = unlockAchievement;
12
12
  exports.getAchievements = getAchievements;
13
13
  exports.getWallet = getWallet;
14
14
  exports.convertGems = convertGems;
15
+ exports.getProducts = getProducts;
16
+ exports.purchaseProduct = purchaseProduct;
17
+ exports.getPurchaseStatus = getPurchaseStatus;
18
+ exports.getPurchaseReceipts = getPurchaseReceipts;
19
+ exports.getInventory = getInventory;
20
+ exports.consumeItem = consumeItem;
21
+ exports.getExchangeStatus = getExchangeStatus;
15
22
  exports.endSession = endSession;
16
23
  const types_1 = require("./types");
17
24
  Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return types_1.SDK_VERSION; } });
18
25
  const DEFAULT_INIT_TIMEOUT_MS = 10_000;
19
26
  const IFRAME_PROBE_MS = 400;
20
27
  const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
21
- const DEV_AUTH_MESSAGE = 'oyna360:dev-auth';
22
- const DEV_CODE_QUERY = 'oyna_dev_code';
28
+ const DEV_AUTH_MESSAGE = "oyna360:dev-auth";
29
+ const DEV_CODE_QUERY = "oyna_dev_code";
23
30
  let initPayload = null;
24
31
  /** Origin of the platform parent after the first trusted iframe message. */
25
32
  let trustedPlatformOrigin = null;
@@ -28,14 +35,14 @@ let directPlatformUrl = null;
28
35
  const initWaiters = [];
29
36
  function isEmbeddedInPlatform() {
30
37
  try {
31
- return typeof window !== 'undefined' && window.parent !== window;
38
+ return typeof window !== "undefined" && window.parent !== window;
32
39
  }
33
40
  catch {
34
41
  return true;
35
42
  }
36
43
  }
37
44
  function rememberDirectPlatformUrl(url) {
38
- const trimmed = url.replace(/\/$/, '');
45
+ const trimmed = url.replace(/\/$/, "");
39
46
  if (trimmed)
40
47
  directPlatformUrl = trimmed;
41
48
  }
@@ -50,17 +57,17 @@ function asInitPayload(message) {
50
57
  };
51
58
  }
52
59
  function isCompleteInit(data) {
53
- if (!data || typeof data !== 'object')
60
+ if (!data || typeof data !== "object")
54
61
  return false;
55
62
  const m = data;
56
- return (m.type === 'platform:init' &&
63
+ return (m.type === "platform:init" &&
57
64
  !!m.session?.token &&
58
65
  !!m.user?.id &&
59
66
  !!m.game?.slug &&
60
67
  !!m.avatar?.presetKey);
61
68
  }
62
69
  function rememberTrustedOrigin(origin) {
63
- if (!trustedPlatformOrigin && origin && origin !== 'null') {
70
+ if (!trustedPlatformOrigin && origin && origin !== "null") {
64
71
  trustedPlatformOrigin = origin;
65
72
  }
66
73
  }
@@ -70,7 +77,7 @@ function rememberTrustedOrigin(origin) {
70
77
  function getParentMessageTarget() {
71
78
  if (trustedPlatformOrigin)
72
79
  return trustedPlatformOrigin;
73
- if (typeof window !== 'undefined') {
80
+ if (typeof window !== "undefined") {
74
81
  const raw = window.__OYNA360_DEV__?.platformWebUrl;
75
82
  if (raw) {
76
83
  try {
@@ -81,7 +88,7 @@ function getParentMessageTarget() {
81
88
  }
82
89
  }
83
90
  try {
84
- if (typeof document !== 'undefined' && document.referrer) {
91
+ if (typeof document !== "undefined" && document.referrer) {
85
92
  return new URL(document.referrer).origin;
86
93
  }
87
94
  }
@@ -89,7 +96,7 @@ function getParentMessageTarget() {
89
96
  /* ignore */
90
97
  }
91
98
  }
92
- return '*';
99
+ return "*";
93
100
  }
94
101
  /**
95
102
  * Only accept postMessage traffic from the real platform parent.
@@ -97,7 +104,7 @@ function getParentMessageTarget() {
97
104
  * here so settleInit → publish → onMessage cannot recurse.
98
105
  */
99
106
  function isTrustedPlatformMessage(event) {
100
- if (typeof window === 'undefined')
107
+ if (typeof window === "undefined")
101
108
  return false;
102
109
  if (!isEmbeddedInPlatform())
103
110
  return false;
@@ -109,16 +116,16 @@ function isTrustedPlatformMessage(event) {
109
116
  }
110
117
  /** Publish standard platform:init for lobby-sdk / game listeners (same-window). */
111
118
  function publishPlatformInit(payload) {
112
- if (typeof window === 'undefined')
119
+ if (typeof window === "undefined")
113
120
  return;
114
121
  const message = {
115
- type: 'platform:init',
122
+ type: "platform:init",
116
123
  version: types_1.SDK_VERSION,
117
124
  ...payload,
118
125
  };
119
126
  window.__OYNA360_PLATFORM_INIT__ = message;
120
127
  window.__OYNA360_PLATFORM_INIT_OK__ = true;
121
- window.dispatchEvent(new MessageEvent('message', {
128
+ window.dispatchEvent(new MessageEvent("message", {
122
129
  data: message,
123
130
  origin: window.location.origin,
124
131
  }));
@@ -142,16 +149,18 @@ function onMessage(event) {
142
149
  const data = event.data;
143
150
  if (!isCompleteInit(data)) {
144
151
  // Production parent may send init; accept even if avatar missing for backward compat
145
- if (!data || typeof data !== 'object' || data.type !== 'platform:init') {
152
+ if (!data ||
153
+ typeof data !== "object" ||
154
+ data.type !== "platform:init") {
146
155
  return;
147
156
  }
148
157
  const message = data;
149
158
  if (!message.session?.token || !message.user?.id || !message.game?.slug)
150
159
  return;
151
160
  const avatar = message.avatar ?? {
152
- presetId: 'default',
153
- presetKey: 'default-1',
154
- presetKind: 'procedural',
161
+ presetId: "default",
162
+ presetKey: "default-1",
163
+ presetKind: "procedural",
155
164
  customConfig: {},
156
165
  };
157
166
  rememberTrustedOrigin(event.origin);
@@ -165,14 +174,14 @@ function onMessage(event) {
165
174
  settleInit(asInitPayload(data));
166
175
  }
167
176
  function signalReady() {
168
- if (typeof window === 'undefined')
177
+ if (typeof window === "undefined")
169
178
  return;
170
179
  if (!isEmbeddedInPlatform())
171
180
  return;
172
- window.parent.postMessage({ type: 'platform:ready' }, getParentMessageTarget());
181
+ window.parent.postMessage({ type: "platform:ready" }, getParentMessageTarget());
173
182
  }
174
- if (typeof window !== 'undefined') {
175
- window.addEventListener('message', onMessage);
183
+ if (typeof window !== "undefined") {
184
+ window.addEventListener("message", onMessage);
176
185
  signalReady();
177
186
  }
178
187
  function postToPlatform(outboundType, payload, resultType, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, stableRequestId) {
@@ -181,23 +190,24 @@ function postToPlatform(outboundType, payload, resultType, timeoutMs = DEFAULT_R
181
190
  reject(new Error(`${outboundType} requires the platform iframe parent`));
182
191
  return;
183
192
  }
184
- const requestId = stableRequestId ?? (typeof crypto !== 'undefined' && crypto.randomUUID
185
- ? crypto.randomUUID()
186
- : `${Date.now()}-${Math.random()}`);
193
+ const requestId = stableRequestId ??
194
+ (typeof crypto !== "undefined" && crypto.randomUUID
195
+ ? crypto.randomUUID()
196
+ : `${Date.now()}-${Math.random()}`);
187
197
  const timer = setTimeout(() => {
188
- window.removeEventListener('message', onResult);
189
- reject(new Error('Platform SDK request timeout'));
198
+ window.removeEventListener("message", onResult);
199
+ reject(new Error("Platform SDK request timeout"));
190
200
  }, timeoutMs);
191
201
  function onResult(event) {
192
202
  if (!isTrustedPlatformMessage(event))
193
203
  return;
194
204
  const data = event.data;
195
- if (!data || typeof data !== 'object' || data.type !== resultType)
205
+ if (!data || typeof data !== "object" || data.type !== resultType)
196
206
  return;
197
207
  if (data.requestId !== requestId)
198
208
  return;
199
209
  clearTimeout(timer);
200
- window.removeEventListener('message', onResult);
210
+ window.removeEventListener("message", onResult);
201
211
  rememberTrustedOrigin(event.origin);
202
212
  const error = data.error;
203
213
  if (error) {
@@ -206,41 +216,41 @@ function postToPlatform(outboundType, payload, resultType, timeoutMs = DEFAULT_R
206
216
  }
207
217
  resolve(data.result);
208
218
  }
209
- window.addEventListener('message', onResult);
219
+ window.addEventListener("message", onResult);
210
220
  window.parent.postMessage({ type: outboundType, requestId, ...payload }, getParentMessageTarget());
211
221
  });
212
222
  }
213
223
  function nestErrorMessage(data, fallback) {
214
- if (!data || typeof data !== 'object')
224
+ if (!data || typeof data !== "object")
215
225
  return fallback;
216
226
  const message = data.message;
217
- if (typeof message === 'string')
227
+ if (typeof message === "string")
218
228
  return message;
219
229
  if (Array.isArray(message))
220
- return message.join(', ');
230
+ return message.join(", ");
221
231
  return fallback;
222
232
  }
223
233
  /** Direct Development / top-level: call platform REST with the game session token. */
224
234
  async function apiRequest(path, options = {}) {
225
235
  const base = directPlatformUrl;
226
236
  if (!base) {
227
- throw new Error('Direct API calls require platformUrl. Pass PlatformSDK.init({ platformUrl, gameSlug }) or window.__OYNA360_DEV__.');
237
+ throw new Error("Direct API calls require platformUrl. Pass PlatformSDK.init({ platformUrl, gameSlug }) or window.__OYNA360_DEV__.");
228
238
  }
229
239
  const headers = {
230
- Accept: 'application/json',
240
+ Accept: "application/json",
231
241
  };
232
242
  if (options.body !== undefined) {
233
- headers['Content-Type'] = 'application/json';
243
+ headers["Content-Type"] = "application/json";
234
244
  }
235
245
  if (options.auth !== false) {
236
246
  const token = initPayload?.session?.token;
237
247
  if (!token) {
238
- throw new Error('PlatformSDK.init() must complete before calling this API');
248
+ throw new Error("PlatformSDK.init() must complete before calling this API");
239
249
  }
240
250
  headers.Authorization = `Bearer ${token}`;
241
251
  }
242
252
  const response = await fetch(`${base}${path}`, {
243
- method: options.method ?? (options.body !== undefined ? 'POST' : 'GET'),
253
+ method: options.method ?? (options.body !== undefined ? "POST" : "GET"),
244
254
  headers,
245
255
  body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
246
256
  });
@@ -260,19 +270,27 @@ async function markDirectSessionReady() {
260
270
  if (!directPlatformUrl || !initPayload?.session?.token)
261
271
  return;
262
272
  try {
263
- await apiRequest('/sessions/ready', { method: 'POST' });
273
+ await apiRequest("/sessions/ready", { method: "POST" });
264
274
  }
265
275
  catch {
266
276
  // Non-fatal: presence analytics only
267
277
  }
268
278
  }
269
279
  function resolveDevConfig(options) {
270
- const fromWindow = typeof window !== 'undefined' ? window.__OYNA360_DEV__ : undefined;
271
- const platformUrl = (options?.platformUrl || fromWindow?.platformUrl || '').replace(/\/$/, '');
272
- const gameSlug = (options?.gameSlug || fromWindow?.gameSlug || '').trim();
273
- const clientId = (options?.dev?.clientId || fromWindow?.clientId || '').trim();
274
- const credential = (options?.dev?.credential || fromWindow?.credential || '').trim();
275
- let platformWebUrl = (options?.platformWebUrl || fromWindow?.platformWebUrl || '').replace(/\/$/, '');
280
+ const fromWindow = typeof window !== "undefined" ? window.__OYNA360_DEV__ : undefined;
281
+ const platformUrl = (options?.platformUrl ||
282
+ fromWindow?.platformUrl ||
283
+ "").replace(/\/$/, "");
284
+ const gameSlug = (options?.gameSlug || fromWindow?.gameSlug || "").trim();
285
+ const clientId = (options?.dev?.clientId ||
286
+ fromWindow?.clientId ||
287
+ "").trim();
288
+ const credential = (options?.dev?.credential ||
289
+ fromWindow?.credential ||
290
+ "").trim();
291
+ let platformWebUrl = (options?.platformWebUrl ||
292
+ fromWindow?.platformWebUrl ||
293
+ "").replace(/\/$/, "");
276
294
  if (!platformWebUrl && platformUrl) {
277
295
  try {
278
296
  // Same host as API by default (production reverse-proxy). Locally set platformWebUrl explicitly
@@ -280,7 +298,7 @@ function resolveDevConfig(options) {
280
298
  platformWebUrl = new URL(platformUrl).origin;
281
299
  }
282
300
  catch {
283
- platformWebUrl = '';
301
+ platformWebUrl = "";
284
302
  }
285
303
  }
286
304
  return { platformUrl, platformWebUrl, gameSlug, clientId, credential };
@@ -288,41 +306,41 @@ function resolveDevConfig(options) {
288
306
  async function runCredentialGatewayBootstrap(options) {
289
307
  const { platformUrl, clientId, credential } = resolveDevConfig(options);
290
308
  if (!platformUrl || !clientId || !credential) {
291
- throw new Error('Developer Environment requires platformUrl + dev.clientId + dev.credential. ' +
292
- 'Create them at /developer and pass PlatformSDK.init({ platformUrl, dev: { clientId, credential } }).');
309
+ throw new Error("Developer Environment requires platformUrl + dev.clientId + dev.credential. " +
310
+ "Create them at /developer and pass PlatformSDK.init({ platformUrl, dev: { clientId, credential } }).");
293
311
  }
294
312
  rememberDirectPlatformUrl(platformUrl);
295
313
  const origin = window.location.origin;
296
314
  const response = await fetch(`${platformUrl}/dev/gateway/session`, {
297
- method: 'POST',
298
- headers: { 'Content-Type': 'application/json' },
315
+ method: "POST",
316
+ headers: { "Content-Type": "application/json" },
299
317
  body: JSON.stringify({ clientId, credential, origin }),
300
318
  });
301
319
  const data = await response.json().catch(() => ({}));
302
320
  if (!response.ok) {
303
- const msg = typeof data.message === 'string'
321
+ const msg = typeof data.message === "string"
304
322
  ? data.message
305
323
  : Array.isArray(data.message)
306
- ? data.message.join(', ')
307
- : 'Dev gateway session failed';
324
+ ? data.message.join(", ")
325
+ : "Dev gateway session failed";
308
326
  throw new Error(msg);
309
327
  }
310
328
  const payload = data;
311
329
  if (!payload?.session?.token || !payload.avatar || !payload.lobby?.wsUrl) {
312
- throw new Error('Invalid SdkInitPayload from dev gateway');
330
+ throw new Error("Invalid SdkInitPayload from dev gateway");
313
331
  }
314
332
  return payload;
315
333
  }
316
334
  function readCodeFromUrl() {
317
- if (typeof window === 'undefined')
335
+ if (typeof window === "undefined")
318
336
  return null;
319
337
  try {
320
338
  const url = new URL(window.location.href);
321
339
  const code = url.searchParams.get(DEV_CODE_QUERY);
322
340
  if (code) {
323
341
  url.searchParams.delete(DEV_CODE_QUERY);
324
- url.searchParams.delete('oyna_dev_slug');
325
- window.history.replaceState({}, '', url.toString());
342
+ url.searchParams.delete("oyna_dev_slug");
343
+ window.history.replaceState({}, "", url.toString());
326
344
  }
327
345
  return code;
328
346
  }
@@ -332,22 +350,22 @@ function readCodeFromUrl() {
332
350
  }
333
351
  async function exchangeDevCode(platformUrl, code, gameOrigin) {
334
352
  const response = await fetch(`${platformUrl}/dev/game-auth/exchange`, {
335
- method: 'POST',
336
- headers: { 'Content-Type': 'application/json' },
353
+ method: "POST",
354
+ headers: { "Content-Type": "application/json" },
337
355
  body: JSON.stringify({ code, gameOrigin }),
338
356
  });
339
357
  const data = await response.json().catch(() => ({}));
340
358
  if (!response.ok) {
341
- const msg = typeof data.message === 'string'
359
+ const msg = typeof data.message === "string"
342
360
  ? data.message
343
361
  : Array.isArray(data.message)
344
- ? data.message.join(', ')
345
- : 'Development authorization exchange failed';
362
+ ? data.message.join(", ")
363
+ : "Development authorization exchange failed";
346
364
  throw new Error(msg);
347
365
  }
348
366
  const payload = data;
349
367
  if (!payload?.session?.token || !payload.avatar || !payload.lobby?.wsUrl) {
350
- throw new Error('Invalid SdkInitPayload from development exchange');
368
+ throw new Error("Invalid SdkInitPayload from development exchange");
351
369
  }
352
370
  return payload;
353
371
  }
@@ -356,13 +374,13 @@ function openDevAuthorizePopup(authUrl) {
356
374
  const height = 720;
357
375
  const left = Math.max(0, Math.floor(window.screenX + (window.outerWidth - width) / 2));
358
376
  const top = Math.max(0, Math.floor(window.screenY + (window.outerHeight - height) / 2));
359
- return window.open(authUrl, 'oyna360-dev-auth', `popup=yes,width=${width},height=${height},left=${left},top=${top}`);
377
+ return window.open(authUrl, "oyna360-dev-auth", `popup=yes,width=${width},height=${height},left=${left},top=${top}`);
360
378
  }
361
379
  async function runDirectDevelopmentBootstrap(options) {
362
380
  const { platformUrl, platformWebUrl, gameSlug } = resolveDevConfig(options);
363
381
  if (!platformUrl || !gameSlug || !platformWebUrl) {
364
- throw new Error('Direct Development Mode requires platformUrl + gameSlug (and platformWebUrl). ' +
365
- 'Set PlatformSDK.init({ platformUrl, gameSlug }) or window.__OYNA360_DEV__.');
382
+ throw new Error("Direct Development Mode requires platformUrl + gameSlug (and platformWebUrl). " +
383
+ "Set PlatformSDK.init({ platformUrl, gameSlug }) or window.__OYNA360_DEV__.");
366
384
  }
367
385
  rememberDirectPlatformUrl(platformUrl);
368
386
  const gameOrigin = window.location.origin;
@@ -370,17 +388,18 @@ async function runDirectDevelopmentBootstrap(options) {
370
388
  if (existingCode) {
371
389
  return exchangeDevCode(platformUrl, existingCode, gameOrigin);
372
390
  }
373
- const returnPath = `${window.location.pathname}${window.location.search}${window.location.hash}` || '/';
374
- const authUrl = new URL('/dev/game-auth', platformWebUrl);
375
- authUrl.searchParams.set('slug', gameSlug);
376
- authUrl.searchParams.set('origin', gameOrigin);
377
- authUrl.searchParams.set('return', returnPath.startsWith('/') ? returnPath : '/');
391
+ const returnPath = `${window.location.pathname}${window.location.search}${window.location.hash}` ||
392
+ "/";
393
+ const authUrl = new URL("/dev/game-auth", platformWebUrl);
394
+ authUrl.searchParams.set("slug", gameSlug);
395
+ authUrl.searchParams.set("origin", gameOrigin);
396
+ authUrl.searchParams.set("return", returnPath.startsWith("/") ? returnPath : "/");
378
397
  return new Promise((resolve, reject) => {
379
398
  const timeoutMs = options?.timeout ?? 120_000;
380
399
  let settled = false;
381
400
  const cleanup = () => {
382
401
  window.clearTimeout(timer);
383
- window.removeEventListener('message', onAuthMessage);
402
+ window.removeEventListener("message", onAuthMessage);
384
403
  };
385
404
  const finish = (payload) => {
386
405
  if (settled)
@@ -397,15 +416,15 @@ async function runDirectDevelopmentBootstrap(options) {
397
416
  reject(err);
398
417
  };
399
418
  const timer = window.setTimeout(() => {
400
- fail(new Error('Direct Development authorization timed out. Log in on the Oyna360 authorize page and allow popups.'));
419
+ fail(new Error("Direct Development authorization timed out. Log in on the Oyna360 authorize page and allow popups."));
401
420
  }, timeoutMs);
402
421
  const onAuthMessage = (event) => {
403
422
  const data = event.data;
404
- if (!data || typeof data !== 'object')
423
+ if (!data || typeof data !== "object")
405
424
  return;
406
425
  if (data.type !== DEV_AUTH_MESSAGE)
407
426
  return;
408
- let expectedOrigin = '';
427
+ let expectedOrigin = "";
409
428
  try {
410
429
  expectedOrigin = new URL(platformWebUrl).origin;
411
430
  }
@@ -419,7 +438,7 @@ async function runDirectDevelopmentBootstrap(options) {
419
438
  return;
420
439
  void exchangeDevCode(platformUrl, code, gameOrigin).then(finish, fail);
421
440
  };
422
- window.addEventListener('message', onAuthMessage);
441
+ window.addEventListener("message", onAuthMessage);
423
442
  const popup = openDevAuthorizePopup(authUrl.toString());
424
443
  if (!popup) {
425
444
  // Popup blocked → full redirect; after login, game reloads with ?oyna_dev_code=
@@ -438,7 +457,7 @@ function waitForIframeInit(timeoutMs) {
438
457
  const index = initWaiters.findIndex((w) => w.timer === timer);
439
458
  if (index >= 0)
440
459
  initWaiters.splice(index, 1);
441
- reject(new Error('Platform SDK init timeout — is the game running inside the platform?'));
460
+ reject(new Error("Platform SDK init timeout — is the game running inside the platform?"));
442
461
  }, timeoutMs);
443
462
  initWaiters.push({ resolve, reject, timer });
444
463
  signalReady();
@@ -454,8 +473,8 @@ async function init(options) {
454
473
  if (initPayload) {
455
474
  return initPayload;
456
475
  }
457
- if (typeof window === 'undefined') {
458
- throw new Error('PlatformSDK.init() requires a browser environment');
476
+ if (typeof window === "undefined") {
477
+ throw new Error("PlatformSDK.init() requires a browser environment");
459
478
  }
460
479
  // Pin API base early so Direct Dev feature calls work after init.
461
480
  const earlyDev = resolveDevConfig(options);
@@ -472,13 +491,14 @@ async function init(options) {
472
491
  }
473
492
  }
474
493
  // Already delivered by a trusted writer (this SDK or lobby-sdk after origin check)
475
- if (window.__OYNA360_PLATFORM_INIT_OK__ && window.__OYNA360_PLATFORM_INIT__?.session?.token) {
494
+ if (window.__OYNA360_PLATFORM_INIT_OK__ &&
495
+ window.__OYNA360_PLATFORM_INIT__?.session?.token) {
476
496
  const cached = asInitPayload(window.__OYNA360_PLATFORM_INIT__);
477
497
  if (!cached.avatar) {
478
498
  cached.avatar = {
479
- presetId: 'default',
480
- presetKey: 'default-1',
481
- presetKind: 'procedural',
499
+ presetId: "default",
500
+ presetKey: "default-1",
501
+ presetKind: "procedural",
482
502
  customConfig: {},
483
503
  };
484
504
  }
@@ -524,11 +544,11 @@ function isReady() {
524
544
  async function submitScore(score) {
525
545
  const payload = await init();
526
546
  if (score < 0 || !Number.isFinite(score)) {
527
- throw new Error('Score must be a non-negative number');
547
+ throw new Error("Score must be a non-negative number");
528
548
  }
529
549
  const floorScore = Math.floor(score);
530
- return callPlatformFeature('platform:score:submit', { sessionToken: payload.session.token, score: floorScore }, 'platform:score:result', () => apiRequest(`/games/${payload.game.slug}/scores`, {
531
- method: 'POST',
550
+ return callPlatformFeature("platform:score:submit", { sessionToken: payload.session.token, score: floorScore }, "platform:score:result", () => apiRequest(`/games/${payload.game.slug}/scores`, {
551
+ method: "POST",
532
552
  body: { score: floorScore },
533
553
  }));
534
554
  }
@@ -536,63 +556,118 @@ async function submitScore(score) {
536
556
  async function getLeaderboard(limit = 20) {
537
557
  const payload = await init();
538
558
  const safeLimit = Math.min(Math.max(Math.floor(limit) || 20, 1), 100);
539
- return callPlatformFeature('platform:leaderboard:get', { limit: safeLimit }, 'platform:leaderboard:result', () => apiRequest(`/games/${payload.game.slug}/leaderboard?limit=${safeLimit}`, { auth: false }));
559
+ return callPlatformFeature("platform:leaderboard:get", { limit: safeLimit }, "platform:leaderboard:result", () => apiRequest(`/games/${payload.game.slug}/leaderboard?limit=${safeLimit}`, { auth: false }));
540
560
  }
541
561
  /** Unlock an achievement for the current player. Idempotent. */
542
562
  async function unlockAchievement(key) {
543
563
  const payload = await init();
544
564
  if (!key.trim()) {
545
- throw new Error('Achievement key is required');
565
+ throw new Error("Achievement key is required");
546
566
  }
547
567
  const achievementKey = key.trim();
548
- return callPlatformFeature('platform:achievement:unlock', { sessionToken: payload.session.token, key: achievementKey }, 'platform:achievement:unlock:result', () => apiRequest(`/games/${payload.game.slug}/achievements/unlock`, {
549
- method: 'POST',
568
+ return callPlatformFeature("platform:achievement:unlock", { sessionToken: payload.session.token, key: achievementKey }, "platform:achievement:unlock:result", () => apiRequest(`/games/${payload.game.slug}/achievements/unlock`, {
569
+ method: "POST",
550
570
  body: { key: achievementKey },
551
571
  }));
552
572
  }
553
573
  /** List achievements with unlock status for the current player. */
554
574
  async function getAchievements() {
555
575
  const payload = await init();
556
- return callPlatformFeature('platform:achievements:get', {}, 'platform:achievements:result', () => apiRequest(`/games/${payload.game.slug}/achievements/me`));
576
+ return callPlatformFeature("platform:achievements:get", {}, "platform:achievements:result", () => apiRequest(`/games/${payload.game.slug}/achievements/me`));
557
577
  }
558
578
  /** Platform gem balance + coins-per-gem rate for this game. */
559
579
  async function getWallet() {
560
580
  const payload = await init();
561
- return callPlatformFeature('platform:wallet:get', {}, 'platform:wallet:result', () => apiRequest(`/games/${payload.game.slug}/wallet`));
581
+ return callPlatformFeature("platform:wallet:get", {}, "platform:wallet:result", () => apiRequest(`/games/${payload.game.slug}/wallet`));
562
582
  }
563
583
  /**
564
584
  * Convert platform gems into in-game coins.
565
585
  * Platform deducts gems; the game must credit `coinsReceived` to its own economy.
566
586
  */
567
- async function convertGems(gems, requestId) {
587
+ function operationId(value) {
588
+ const id = value ??
589
+ (typeof crypto !== "undefined" && crypto.randomUUID
590
+ ? crypto.randomUUID()
591
+ : Date.now().toString(36) + "-" + Math.random().toString(36).slice(2));
592
+ if (!/^[A-Za-z0-9_-]{8,128}$/.test(id))
593
+ throw new Error("requestId must contain 8-128 ASCII letters, digits, underscores or hyphens");
594
+ return id;
595
+ }
596
+ function requiredOperationId(value) {
597
+ if (typeof value !== "string")
598
+ throw new Error("requestId is required");
599
+ return operationId(value);
600
+ }
601
+ function itemKey(value) {
602
+ if (!/^[A-Za-z0-9:_-]{1,100}$/.test(value))
603
+ throw new Error("Invalid item ID");
604
+ return value;
605
+ }
606
+ async function commerce(operation, args = {}) {
568
607
  const payload = await init();
569
- if (!Number.isInteger(gems) || gems < 1) {
570
- throw new Error('gems must be a positive integer');
571
- }
572
- if (requestId !== undefined && !/^[A-Za-z0-9_-]{8,128}$/.test(requestId)) {
573
- throw new Error('requestId must contain 8-128 ASCII letters, digits, underscores or hyphens');
574
- }
575
- return callPlatformFeature('platform:gems:exchange', { sessionToken: payload.session.token, gems }, 'platform:gems:exchange:result', () => apiRequest(`/games/${payload.game.slug}/wallet/exchange`, {
576
- method: 'POST',
577
- body: { gems, requestId },
578
- }), requestId);
608
+ return callPlatformFeature("platform:commerce:request", { operation, args }, "platform:commerce:result", () => apiRequest(`/games/${encodeURIComponent(payload.game.slug)}/commerce`, { method: "POST", body: { operation, args } }));
609
+ }
610
+ async function convertGems(gems, requestId) {
611
+ if (!Number.isSafeInteger(gems) || gems < 1)
612
+ throw new Error("gems must be a positive safe integer");
613
+ return commerce("exchange", {
614
+ gems,
615
+ requestId: operationId(requestId),
616
+ });
617
+ }
618
+ function getProducts() {
619
+ return commerce("products");
620
+ }
621
+ /** Persist requestId before sending and reuse it when recovering/retrying the same purchase. */
622
+ function purchaseProduct(itemId, requestId) {
623
+ return commerce("purchase", {
624
+ itemId: itemKey(itemId),
625
+ requestId: operationId(requestId),
626
+ });
627
+ }
628
+ function getPurchaseStatus(requestId) {
629
+ return commerce("purchaseStatus", {
630
+ requestId: requiredOperationId(requestId),
631
+ });
632
+ }
633
+ function getPurchaseReceipts(cursor) {
634
+ if (cursor !== undefined && !/^[A-Za-z0-9-]{1,100}$/.test(cursor))
635
+ throw new Error("Invalid receipt cursor");
636
+ return commerce("receipts", cursor ? { cursor } : {});
637
+ }
638
+ function getInventory() {
639
+ return commerce("inventory");
640
+ }
641
+ function consumeItem(itemId, quantity, requestId) {
642
+ if (!Number.isSafeInteger(quantity) || quantity < 1)
643
+ throw new Error("quantity must be a positive safe integer");
644
+ return commerce("consume", {
645
+ itemId: itemKey(itemId),
646
+ quantity,
647
+ requestId: operationId(requestId),
648
+ });
649
+ }
650
+ function getExchangeStatus(requestId) {
651
+ return commerce("exchangeStatus", {
652
+ requestId: requiredOperationId(requestId),
653
+ });
579
654
  }
580
655
  async function endSession() {
581
656
  const token = initPayload?.session.token;
582
- if (!token || typeof window === 'undefined')
657
+ if (!token || typeof window === "undefined")
583
658
  return;
584
659
  if (isEmbeddedInPlatform()) {
585
660
  const target = getParentMessageTarget();
586
661
  window.parent.postMessage({
587
- type: 'platform:session:end',
662
+ type: "platform:session:end",
588
663
  sessionToken: token,
589
664
  }, target);
590
665
  return;
591
666
  }
592
667
  if (directPlatformUrl) {
593
668
  try {
594
- await apiRequest('/sessions/end', {
595
- method: 'POST',
669
+ await apiRequest("/sessions/end", {
670
+ method: "POST",
596
671
  body: { token },
597
672
  auth: false,
598
673
  });
@@ -615,6 +690,13 @@ exports.PlatformSDK = {
615
690
  getAchievements,
616
691
  getWallet,
617
692
  convertGems,
693
+ getProducts,
694
+ purchaseProduct,
695
+ getPurchaseStatus,
696
+ getPurchaseReceipts,
697
+ getInventory,
698
+ consumeItem,
699
+ getExchangeStatus,
618
700
  endSession,
619
701
  };
620
702
  exports.default = exports.PlatformSDK;