@looplay/sdk 0.5.1 → 0.7.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.
@@ -244,47 +244,49 @@ var ServiceClient = class {
244
244
  return this.http.request("POST", "/account/logout", { body });
245
245
  }
246
246
  // ─────────────────────────────────────────────────────────────────────────────
247
- // Games
247
+ // Games — one SDK integration = one game, resolved on every call from the
248
+ // public `x-game-key` header (the game's `appId`). There is no multi-game
249
+ // catalog browsing here (list/search/detail across games) and no `:id`/
250
+ // `storeId` path segments — that's the app-facing surface, out of scope
251
+ // for this per-game SDK. Never use the creator-secret tier
252
+ // (`x-creator-key`/`x-game-secret`) from client/browser code — that would
253
+ // leak the secret to every player.
248
254
  // ─────────────────────────────────────────────────────────────────────────────
249
- async listGames(query) {
250
- return this.http.request("GET", "/sdk/games", { query });
251
- }
252
- async getGameDetail(gameId, bearerToken) {
253
- return this.http.request("GET", `/sdk/games/${encodeURIComponent(gameId)}`, {
254
- bearerToken
255
+ async recordGameView(gameKey, auth = {}) {
256
+ await this.http.request("POST", "/sdk/games/views/record", {
257
+ ...auth,
258
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) }
255
259
  });
260
+ return true;
256
261
  }
257
- async listRecentPlayed(bearerToken, query) {
258
- return this.http.request("GET", "/sdk/games/recent-play", { bearerToken, query });
259
- }
260
- async trackView(auth, gameId) {
261
- await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/view`, {
262
+ /** Signs a fresh `attemptToken` — required by `completeGameplayAttempt`/`completeGameplayMatch`/`recordGameplayAction`. */
263
+ async startGameplayAttempt(gameKey, auth = {}, body = {}) {
264
+ return this.http.request("POST", "/sdk/games/attempts/start", {
262
265
  ...auth,
263
- headers: this.trackingHeaders(auth)
266
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
267
+ body
264
268
  });
265
- return true;
266
269
  }
267
- async trackPlay(auth, gameId, attemptId, playTimeSeconds) {
268
- const body = { playTimeSeconds, attemptId };
269
- await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/play`, {
270
+ async completeGameplayAttempt(gameKey, auth, body) {
271
+ await this.http.request("POST", "/sdk/games/attempts/complete", {
270
272
  ...auth,
271
- headers: this.trackingHeaders(auth),
273
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
272
274
  body
273
275
  });
274
276
  return true;
275
277
  }
276
- async trackMatchEnd(auth, gameId, body) {
277
- await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/end`, {
278
+ async completeGameplayMatch(gameKey, auth, body) {
279
+ await this.http.request("POST", "/sdk/games/matches/complete", {
278
280
  ...auth,
279
- headers: this.trackingHeaders(auth),
281
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
280
282
  body
281
283
  });
282
284
  return true;
283
285
  }
284
- async emitGameEvent(auth, gameId, body) {
285
- return this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/emit`, {
286
+ async recordGameplayAction(gameKey, auth, body) {
287
+ return this.http.request("POST", "/sdk/games/actions/record", {
286
288
  ...auth,
287
- headers: this.trackingHeaders(auth),
289
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
288
290
  body
289
291
  });
290
292
  }
@@ -293,7 +295,7 @@ var ServiceClient = class {
293
295
  * runtimes should still call it to get a `trackingSessionId` for unified
294
296
  * tracing — see `GameTrackingSessionDto`.
295
297
  */
296
- async issueTrackingSession(anonId) {
298
+ async bootstrapTrackingSession(anonId) {
297
299
  return this.http.request("POST", "/sdk/games/tracking/bootstrap", {
298
300
  headers: { "x-looplay-anon-id": anonId }
299
301
  });
@@ -304,22 +306,36 @@ var ServiceClient = class {
304
306
  "x-looplay-anon-token": auth.anonTrackingToken
305
307
  };
306
308
  }
307
- // ─────────────────────────────────────────────────────────────────────────────
308
- // Game assets — spend coin balance on assets defined by the game's
309
- // creator. Coin balance itself comes from getBalances()/getMyBalance().
310
- // ─────────────────────────────────────────────────────────────────────────────
311
- async listGameAssets(gameId, bearerToken) {
312
- return this.http.request("GET", `/sdk/games/${encodeURIComponent(gameId)}/assets`, {
313
- bearerToken
309
+ /** Public storefront (sections + offers) for the integration's game. Read-only. */
310
+ async getStore(gameKey) {
311
+ return this.http.request("GET", "/sdk/games/store/current", {
312
+ headers: { "x-game-key": gameKey }
314
313
  });
315
314
  }
316
- async purchaseGameAsset(bearerToken, gameId, assetCode, body = {}) {
317
- return this.http.request(
318
- "POST",
319
- `/sdk/games/${encodeURIComponent(gameId)}/assets/${encodeURIComponent(assetCode)}/purchase`,
320
- { bearerToken, body }
321
- );
315
+ /** Public catalog of purchasable/ownable assets for the game. Read-only. */
316
+ async listGameAssets(gameKey) {
317
+ return this.http.request("GET", "/sdk/games/store/assets", {
318
+ headers: { "x-game-key": gameKey }
319
+ });
322
320
  }
321
+ /** Assets the authenticated player already owns for this game. */
322
+ async listMyGameAssets(bearerToken, gameKey) {
323
+ return this.http.request("GET", "/sdk/games/store/assets/me", {
324
+ bearerToken,
325
+ headers: { "x-game-key": gameKey }
326
+ });
327
+ }
328
+ /** The authenticated player's past store purchases for this game. */
329
+ async listMyStorePurchases(bearerToken, gameKey) {
330
+ return this.http.request("GET", "/sdk/games/store/purchases/me", {
331
+ bearerToken,
332
+ headers: { "x-game-key": gameKey }
333
+ });
334
+ }
335
+ // No purchase route is reachable from client/browser code — spending coin
336
+ // balance always requires the creator+game secret pair
337
+ // (`x-creator-key`/`x-game-secret`), called from the game's own backend.
338
+ // This SDK intentionally never wires that tier up.
323
339
  // ─────────────────────────────────────────────────────────────────────────────
324
340
  // Balance
325
341
  // ─────────────────────────────────────────────────────────────────────────────
@@ -350,11 +366,11 @@ var ServiceClient = class {
350
366
  // ─────────────────────────────────────────────────────────────────────────────
351
367
  // Tasks
352
368
  // ─────────────────────────────────────────────────────────────────────────────
353
- async listTasks(bearerToken) {
354
- return this.http.request("GET", "/sdk/task", { bearerToken });
369
+ async listTasks(bearerToken, query) {
370
+ return this.http.request("GET", "/sdk/task", { bearerToken, query: { ...query } });
355
371
  }
356
- async listFinishedTasks(bearerToken) {
357
- return this.http.request("GET", "/sdk/task/finished", { bearerToken });
372
+ async listFinishedTasks(bearerToken, query) {
373
+ return this.http.request("GET", "/sdk/task/finished", { bearerToken, query: { ...query } });
358
374
  }
359
375
  async startTask(bearerToken, body) {
360
376
  return this.http.request("POST", "/sdk/task/start", { bearerToken, body });
@@ -382,6 +398,8 @@ var ApiClient = class {
382
398
  trackingSession;
383
399
  trackingSessionPromise;
384
400
  currentAttemptId;
401
+ gameplayAttempt;
402
+ gameplayAttemptPromise;
385
403
  constructor(options) {
386
404
  if (!options.baseUrl) throw new MissingBaseUrlError();
387
405
  this.raw = new ServiceClient({
@@ -392,15 +410,10 @@ var ApiClient = class {
392
410
  this.getAccessToken = options.getAccessToken;
393
411
  this.getAnonymousId = options.getAnonymousId;
394
412
  }
395
- /** Expose the underlying route-level client (requires manual bearerToken passing). */
413
+ /** Expose the underlying route-level client (requires manual bearerToken/gameKey passing). */
396
414
  unsafeRaw() {
397
415
  return this.raw;
398
416
  }
399
- // Public endpoints
400
- async listGames(query) {
401
- return this.raw.listGames(query);
402
- }
403
- // Authenticated endpoints
404
417
  async requireToken() {
405
418
  const token = await this.getAccessToken?.();
406
419
  if (!token) throw new NotAuthenticatedError();
@@ -437,7 +450,7 @@ var ApiClient = class {
437
450
  return this.trackingSession;
438
451
  }
439
452
  if (!this.trackingSessionPromise) {
440
- this.trackingSessionPromise = this.raw.issueTrackingSession(anonId).then((dto) => {
453
+ this.trackingSessionPromise = this.raw.bootstrapTrackingSession(anonId).then((dto) => {
441
454
  this.trackingSession = {
442
455
  trackingSessionId: dto.trackingSessionId,
443
456
  anonTrackingToken: dto.trackingToken,
@@ -450,57 +463,99 @@ var ApiClient = class {
450
463
  }
451
464
  return this.trackingSessionPromise;
452
465
  }
453
- async getGameDetail(gameId) {
454
- const token = await this.getAccessToken?.();
455
- return this.raw.getGameDetail(gameId, token);
456
- }
457
- async listRecentPlayed(query) {
458
- const token = await this.requireToken();
459
- return this.raw.listRecentPlayed(token, query);
466
+ /**
467
+ * Signs (and caches until near expiry) the `attemptToken` required by
468
+ * `trackPlay`/`trackMatch`/`emit` for the current attempt — does not
469
+ * swallow failures, unlike `ensureTrackingSession`, since these calls
470
+ * are rejected outright without a valid token.
471
+ */
472
+ async ensureGameplayAttempt(gameKey, auth, attemptId) {
473
+ const now = Date.now();
474
+ if (this.gameplayAttempt && this.gameplayAttempt.gameKey === gameKey && this.gameplayAttempt.attemptId === attemptId && this.gameplayAttempt.expiresAtMs > now + 1e4) {
475
+ return this.gameplayAttempt;
476
+ }
477
+ if (!this.gameplayAttemptPromise) {
478
+ this.gameplayAttemptPromise = this.raw.startGameplayAttempt(gameKey, auth, { attemptId }).then((dto) => {
479
+ this.gameplayAttempt = {
480
+ gameKey,
481
+ attemptId: dto.attemptId,
482
+ attemptToken: dto.attemptToken,
483
+ expiresAtMs: new Date(dto.expiresAt).getTime()
484
+ };
485
+ return this.gameplayAttempt;
486
+ }).finally(() => {
487
+ this.gameplayAttemptPromise = void 0;
488
+ });
489
+ }
490
+ return this.gameplayAttemptPromise;
460
491
  }
461
- /** Call once when the game view opens, before any play/match tracking. */
462
- async trackView(gameId) {
492
+ /**
493
+ * Call once when the game view opens, before any play/match tracking.
494
+ * `gameKey` is the game's public `appId`.
495
+ */
496
+ async trackView(gameKey) {
463
497
  const auth = await this.resolveTrackingAuth();
464
- return this.raw.trackView(auth, gameId);
498
+ return this.raw.recordGameView(gameKey, auth);
465
499
  }
466
500
  /**
467
501
  * Starts a new play attempt and returns its id. Call when the user
468
- * presses Play; `trackPlay` reuses this id across calls until the next
502
+ * presses Play; `trackPlay`/`trackMatch`/`emit` reuse this id (and the
503
+ * signed `attemptToken` backing it) across calls until the next
469
504
  * `startAttempt()`, auto-starting one on first use if none was started.
470
505
  */
471
506
  startAttempt() {
472
507
  this.currentAttemptId = generateId();
508
+ this.gameplayAttempt = void 0;
473
509
  return this.currentAttemptId;
474
510
  }
475
- async trackPlay(gameId, playTimeSeconds) {
511
+ async trackPlay(gameKey, playTimeSeconds) {
476
512
  const auth = await this.resolveTrackingAuth();
477
513
  const attemptId = this.currentAttemptId ?? this.startAttempt();
478
- return this.raw.trackPlay(auth, gameId, attemptId, playTimeSeconds);
514
+ const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
515
+ return this.raw.completeGameplayAttempt(gameKey, auth, { playTimeSeconds, attemptId, attemptToken });
479
516
  }
480
- async trackMatch(gameId, body) {
517
+ async trackMatch(gameKey, body) {
481
518
  const auth = await this.resolveTrackingAuth();
482
- return this.raw.trackMatchEnd(auth, gameId, body);
519
+ const attemptId = this.currentAttemptId ?? this.startAttempt();
520
+ const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
521
+ return this.raw.completeGameplayMatch(gameKey, auth, { ...body, attemptId, attemptToken });
483
522
  }
484
- async emit(gameId, actionCode, opts) {
523
+ async emit(gameKey, actionCode, opts) {
485
524
  const auth = await this.resolveTrackingAuth();
525
+ const attemptId = this.currentAttemptId ?? this.startAttempt();
526
+ const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
486
527
  const body = {
528
+ attemptId,
529
+ attemptToken,
487
530
  actionCode,
488
531
  value: opts?.value,
489
532
  refId: opts?.refId,
490
533
  payload: opts?.payload
491
534
  };
492
- return this.raw.emitGameEvent(auth, gameId, body);
535
+ return this.raw.recordGameplayAction(gameKey, auth, body);
493
536
  }
494
- /** Lists assets purchasable in `gameId`, defined by that game's creator. */
495
- async listGameAssets(gameId) {
496
- const token = await this.getAccessToken?.();
497
- return this.raw.listGameAssets(gameId, token);
537
+ /** Fetches the storefront (sections + offers) for the integration's game. Read-only, no auth required. */
538
+ async getStore(gameKey) {
539
+ return this.raw.getStore(gameKey);
540
+ }
541
+ /** Public catalog of purchasable/ownable assets for the game. Read-only, no auth required. */
542
+ async listGameAssets(gameKey) {
543
+ return this.raw.listGameAssets(gameKey);
544
+ }
545
+ /** Assets the authenticated player already owns for this game. */
546
+ async listMyGameAssets(gameKey) {
547
+ const token = await this.requireToken();
548
+ return this.raw.listMyGameAssets(token, gameKey);
498
549
  }
499
- /** Spends coin balance to purchase an asset see PurchaseGameAssetRequest for dedupe/quantity. */
500
- async purchaseGameAsset(gameId, assetCode, opts) {
550
+ /** Purchase history for the authenticated player in the game's store. */
551
+ async listMyStorePurchases(gameKey) {
501
552
  const token = await this.requireToken();
502
- return this.raw.purchaseGameAsset(token, gameId, assetCode, opts);
553
+ return this.raw.listMyStorePurchases(token, gameKey);
503
554
  }
555
+ // There is no purchaseStoreOffer() here by design — spending coin balance
556
+ // requires the creator+game secret pair, which must be called from the
557
+ // game's own backend, never from this client-side SDK. Implement a
558
+ // checkout endpoint on your backend that calls gbs-service directly.
504
559
  async getMyProfile() {
505
560
  const token = await this.requireToken();
506
561
  return this.raw.getMyProfile(token);
@@ -525,13 +580,13 @@ var ApiClient = class {
525
580
  const token = await this.requireToken();
526
581
  return this.raw.setReferral(token, body);
527
582
  }
528
- async listTasks() {
583
+ async listTasks(query) {
529
584
  const token = await this.requireToken();
530
- return this.raw.listTasks(token);
585
+ return this.raw.listTasks(token, query);
531
586
  }
532
- async listFinishedTasks() {
587
+ async listFinishedTasks(query) {
533
588
  const token = await this.requireToken();
534
- return this.raw.listFinishedTasks(token);
589
+ return this.raw.listFinishedTasks(token, query);
535
590
  }
536
591
  async startTask(body) {
537
592
  const token = await this.requireToken();
@@ -631,9 +686,8 @@ var LooplaySDK = class {
631
686
  }
632
687
  async verifyGameId(params) {
633
688
  if ((params.verifyMode ?? "none") === "none") return;
634
- if (!this.auth) throw new MissingAuthError();
635
689
  if (!this.api) throw new MissingBaseUrlError();
636
- await this.api.getGameDetail(params.gameId);
690
+ await this.api.listGameAssets(params.gameId);
637
691
  }
638
692
  };
639
693