@powly/sdk 0.4.0 → 0.6.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fiioonnn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -10,7 +10,7 @@ invites, multiplayer rooms, Twitch chat).
10
10
  npm install @powly/sdk
11
11
  ```
12
12
 
13
- Starting a new game? `npm create powly-game@latest my-game` sets up a React + Vite project with
13
+ Starting a new game? `npm create powly@latest my-game` sets up a React + Vite project with
14
14
  the SDK.
15
15
 
16
16
  ## Use
@@ -31,7 +31,7 @@ await powly.leaderboards.submit("high-score", 1200);
31
31
  Without a bundler, load the classic script instead; it defines `window.Powly`:
32
32
 
33
33
  ```html
34
- <script src="https://cdn.jsdelivr.net/npm/@powly/sdk@0.4.0/dist/powly.js"></script>
34
+ <script src="https://cdn.jsdelivr.net/npm/@powly/sdk@0.6.0/dist/powly.js"></script>
35
35
  ```
36
36
 
37
37
  ## Testing
@@ -54,3 +54,18 @@ await Powly.init({ powlyOrigin: "https://games.example.com" });
54
54
  The game runs in the player's browser, so nothing here is a security boundary. Saves and
55
55
  leaderboard scores are written by your backend unless the studio allows browser writes for them;
56
56
  your backend verifies session tokens or exchanges them for an access token.
57
+
58
+ ## This repository
59
+
60
+ The SDK is developed in Powly's main repository and mirrored here with each release; this is
61
+ `@powly/sdk` 0.6.0. Issues are welcome; changes land through the main repository.
62
+
63
+ To build it yourself (Node.js 24 or newer):
64
+
65
+ ```bash
66
+ npm install
67
+ npm run build
68
+ ```
69
+
70
+ `dist/powly.mjs`, `dist/powly.js` and `dist/powly.d.ts` are what npm ships. `examples/` has
71
+ Starfall, a small example game, and the SDK playground (every SDK function behind a button).
package/dist/powly.d.ts CHANGED
@@ -1,9 +1,13 @@
1
- /* Powly SDK 0.4.0: types for powly.js and powly.mjs */
1
+ /* Powly SDK 0.6.0: types for powly.js and powly.mjs */
2
2
  /** This SDK's version; Powly's Studio shows it next to the downloads. */
3
- export declare const SDK_VERSION = "0.4.0";
3
+ export declare const SDK_VERSION = "0.6.0";
4
4
 
5
5
  export declare const PROTOCOL = "powly-sdk";
6
6
  export declare const PROTOCOL_VERSION = 1;
7
+ /** The corners Powly's options button (its mascot, which opens the player's options) can be in. */
8
+ export declare const OPTIONS_BUTTON_CORNERS: readonly ["top-left", "top-right", "bottom-left", "bottom-right"];
9
+ export type OptionsButtonCorner = (typeof OPTIONS_BUTTON_CORNERS)[number];
10
+ export declare const isOptionsButtonCorner: (value: unknown) => value is OptionsButtonCorner;
7
11
  export type InitMessage = {
8
12
  protocol: typeof PROTOCOL;
9
13
  version: number;
@@ -14,6 +18,11 @@ export type InitMessage = {
14
18
  * SDKs leave it out.
15
19
  */
16
20
  pac?: 1;
21
+ /**
22
+ * Where the game wants Powly's options button to start (`Powly.init({ optionsButton })`); the
23
+ * player's own choice wins. Left out: Powly's default (top left).
24
+ */
25
+ optionsButton?: OptionsButtonCorner;
17
26
  };
18
27
  export type PlayerProfile = {
19
28
  /** This player's ID in this game: stable, and different in every game. */
@@ -45,7 +54,7 @@ export type FailedMessage = {
45
54
  type: "failed";
46
55
  error: string;
47
56
  };
48
- export type HostMethod = "getSessionToken" | "purchases.open" | "fullscreen.toggle" | "invites.open";
57
+ export type HostMethod = "getSessionToken" | "purchases.open" | "fullscreen.toggle" | "invites.open" | "quit";
49
58
  export type RequestMessage = {
50
59
  type: "request";
51
60
  id: number;
@@ -73,7 +82,11 @@ export type ResponseMessage = {
73
82
  ok: false;
74
83
  error: string;
75
84
  };
76
- export type HostEventName = "pause" | "resume" | "ended";
85
+ /**
86
+ * "shown": Powly's loading screen is gone and the game is on screen (once per launch). "pause" /
87
+ * "resume": Powly UI covers the game or the tab is hidden, and back. "ended": the launch ended.
88
+ */
89
+ export type HostEventName = "shown" | "pause" | "resume" | "ended";
77
90
  export type EventMessage = {
78
91
  type: "event";
79
92
  event: HostEventName;
@@ -113,6 +126,8 @@ export type PacSample = {
113
126
  keyIntervalSd: number | null;
114
127
  clickIntervals: number;
115
128
  clickIntervalSd: number | null;
129
+ /** Every trusted interaction: key presses, clicks, taps, pointer movement, wheel, gamepads. */
130
+ activity: number;
116
131
  };
117
132
  };
118
133
  export declare const isPacSample: (data: unknown) => data is PacSample;
@@ -515,6 +530,12 @@ export type InitOptions = {
515
530
  powlyOrigin?: string | string[];
516
531
  /** How long to wait for Powly to answer (ms). Default 10 s. */
517
532
  timeoutMs?: number;
533
+ /**
534
+ * Which corner Powly's options button (its mascot, which opens the player's options) starts in:
535
+ * "top-left" (the default), "top-right", "bottom-left" or "bottom-right". Pick the one your own
536
+ * UI leaves free. Players can still move it, and their choice wins.
537
+ */
538
+ optionsButton?: OptionsButtonCorner;
518
539
  };
519
540
  export type LeaderboardEntry = {
520
541
  rank: number;
@@ -647,8 +668,20 @@ export type PowlySession = {
647
668
  fullscreen: {
648
669
  toggle(): Promise<void>;
649
670
  };
650
- /** Powly UI opened over the game ("pause"), closed ("resume"), or the launch ended ("ended"). */
671
+ /**
672
+ * Closes the game, like the exit button in Powly's options bar: the player goes back to the
673
+ * game's store page and the launch ends ("ended" fires). Save the player's progress first.
674
+ */
675
+ quit(): Promise<void>;
676
+ /**
677
+ * Powly's loading screen is gone and the game is on screen ("shown": start the intro or music
678
+ * here), Powly UI opened over the game ("pause"), closed ("resume"), or the launch ended
679
+ * ("ended"). "shown" comes once per launch; listening for it later calls the listener right
680
+ * away.
681
+ */
651
682
  on(event: HostEventName, listener: () => void): () => void;
683
+ /** Whether Powly's loading screen is gone and the game is on screen ("shown" came). */
684
+ readonly shown: boolean;
652
685
  };
653
686
  /** Starts the SDK. Rejects when the game wasn't launched from Powly. */
654
687
  export declare function init(options?: InitOptions): Promise<PowlySession>;
package/dist/powly.js CHANGED
@@ -1,8 +1,8 @@
1
- /* Powly SDK 0.4.0: defines window.Powly */
1
+ /* Powly SDK 0.6.0: defines window.Powly */
2
2
  (function () {
3
3
  "use strict";
4
4
  /** This SDK's version; Powly's Studio shows it next to the downloads. */
5
- const SDK_VERSION = "0.4.0";
5
+ const SDK_VERSION = "0.6.0";
6
6
 
7
7
  // The messages between a game (this SDK, inside Powly's iframe) and the Powly page that framed it.
8
8
  // Shared with the host side (apps/web/components/player/sdk-host.ts).
@@ -14,6 +14,14 @@ const SDK_VERSION = "0.4.0";
14
14
  // 3. From then on the game also reports to Powly Anti-Cheat over the port (`PacSample`, src/pac.ts).
15
15
  const PROTOCOL = "powly-sdk";
16
16
  const PROTOCOL_VERSION = 1;
17
+ /** The corners Powly's options button (its mascot, which opens the player's options) can be in. */
18
+ const OPTIONS_BUTTON_CORNERS = [
19
+ "top-left",
20
+ "top-right",
21
+ "bottom-left",
22
+ "bottom-right",
23
+ ];
24
+ const isOptionsButtonCorner = (value) => OPTIONS_BUTTON_CORNERS.includes(value);
17
25
  const isInitMessage = (data) => typeof data === "object" &&
18
26
  data !== null &&
19
27
  data.protocol === PROTOCOL &&
@@ -23,7 +31,7 @@ const isRequestMessage = (data) => typeof data === "object" &&
23
31
  data !== null &&
24
32
  data.type === "request" &&
25
33
  Number.isInteger(data.id) &&
26
- ["getSessionToken", "purchases.open", "fullscreen.toggle", "invites.open"].includes(data.method);
34
+ ["getSessionToken", "purchases.open", "fullscreen.toggle", "invites.open", "quit"].includes(data.method);
27
35
 
28
36
  // The Powly origins this build of the SDK trusts by default: a game only handshakes with a parent
29
37
  // page on one of them (unless it passes `powlyOrigin` to `Powly.init()` itself). Set at build
@@ -49,8 +57,9 @@ class PowlyError extends Error {
49
57
  // the server's to catch speed hacks), browser functions that were replaced (speed hack scripts and
50
58
  // extensions patch timers), traces of automated browsers, and how input arrives (scripted events,
51
59
  // clicks without pointer movement as AI agents make them, machine-regular timing as macros and
52
- // auto-clickers make them). Nothing is sent anywhere else, and nothing here reads what the player
53
- // types: only event counts and timings.
60
+ // auto-clickers make them), and whether the player plays at all (any trusted input, gamepads
61
+ // included: Powly closes games nobody plays). Nothing is sent anywhere else, and nothing here
62
+ // reads what the player types: only event counts and timings.
54
63
  //
55
64
  // In real players' runs (not developer mode) the SDK also guards the game's code against being
56
65
  // copied (`guard`): the browser's context menu and its view-source, save-page and developer-tools
@@ -62,6 +71,8 @@ class PowlyError extends Error {
62
71
  // `tamperedFunctions` are shared with the Powly page (apps/web/components/player/pac-sensor.ts).
63
72
  /** How often the SDK reports (ms). */
64
73
  const PAC_SAMPLE_MS = 5000;
74
+ /** A gamepad axis past this counts as input (sticks rest a little off centre). */
75
+ const GAMEPAD_DEADZONE = 0.3;
65
76
  const isPacSample = (data) => typeof data === "object" &&
66
77
  data !== null &&
67
78
  data.type === "pac" &&
@@ -210,6 +221,7 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
210
221
  synthetic: 0,
211
222
  clicks: 0,
212
223
  teleportClicks: 0,
224
+ activity: 0,
213
225
  };
214
226
  const keyTimes = [];
215
227
  const clickTimes = [];
@@ -227,6 +239,9 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
227
239
  input.synthetic += 1;
228
240
  return;
229
241
  }
242
+ input.activity += 1;
243
+ if (event.type === "wheel")
244
+ return;
230
245
  if (event.type === "pointermove" || event.type === "pointerover") {
231
246
  const move = event;
232
247
  pointer = { x: move.clientX, y: move.clientY };
@@ -252,7 +267,26 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
252
267
  }
253
268
  pointer = { x: down.clientX, y: down.clientY };
254
269
  };
255
- const types = ["keydown", "pointerdown", "pointermove", "pointerover"];
270
+ const types = ["keydown", "pointerdown", "pointermove", "pointerover", "wheel"];
271
+ // Gamepads send no events: a pressed button or a pushed stick, looked at a few times a second.
272
+ const watchGamepads = () => {
273
+ let pads;
274
+ try {
275
+ pads = [...(win.navigator.getGamepads?.() ?? [])];
276
+ }
277
+ catch {
278
+ return;
279
+ }
280
+ for (const pad of pads) {
281
+ if (!pad?.connected)
282
+ continue;
283
+ if (pad.buttons.some((button) => button.pressed) ||
284
+ pad.axes.some((axis) => Math.abs(axis) > GAMEPAD_DEADZONE)) {
285
+ input.activity += 1;
286
+ return;
287
+ }
288
+ }
289
+ };
256
290
  for (const type of types) {
257
291
  win.addEventListener(type, onInput, { capture: true, passive: true });
258
292
  }
@@ -320,6 +354,7 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
320
354
  input.synthetic = 0;
321
355
  input.clicks = 0;
322
356
  input.teleportClicks = 0;
357
+ input.activity = 0;
323
358
  dateJump = 0;
324
359
  devtools = false;
325
360
  };
@@ -328,11 +363,13 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
328
363
  const clearInterval = win.clearInterval.bind(win);
329
364
  const dateTimer = setInterval(watchDate, 1000);
330
365
  const reportTimer = setInterval(report, PAC_SAMPLE_MS);
366
+ const gamepadTimer = setInterval(watchGamepads, 250);
331
367
  const trapTimer = trap ? setInterval(springTrap, 1500) : undefined;
332
368
  report();
333
369
  return () => {
334
370
  clearInterval(dateTimer);
335
371
  clearInterval(reportTimer);
372
+ clearInterval(gamepadTimer);
336
373
  if (trapTimer !== undefined)
337
374
  clearInterval(trapTimer);
338
375
  for (const type of types)
@@ -1174,6 +1211,9 @@ function init(options = {}) {
1174
1211
  type: "init",
1175
1212
  sdkVersion: SDK_VERSION,
1176
1213
  pac: 1,
1214
+ ...(isOptionsButtonCorner(options.optionsButton) && {
1215
+ optionsButton: options.optionsButton,
1216
+ }),
1177
1217
  };
1178
1218
  channel.port1.onmessage = (event) => {
1179
1219
  const data = event.data;
@@ -1207,6 +1247,8 @@ function createSession(port, ready) {
1207
1247
  let nextId = 1;
1208
1248
  const pending = new Map();
1209
1249
  const listeners = new Map();
1250
+ // "shown" comes once; games that listen for it later still hear it.
1251
+ let shown = false;
1210
1252
  port.onmessage = (event) => {
1211
1253
  const data = event.data;
1212
1254
  if (data?.type === "response") {
@@ -1220,7 +1262,13 @@ function createSession(port, ready) {
1220
1262
  waiting.reject(new PowlyError(data.error));
1221
1263
  }
1222
1264
  else if (data?.type === "event") {
1223
- for (const listener of listeners.get(data.event) ?? [])
1265
+ const name = data.event;
1266
+ if (name === "shown") {
1267
+ if (shown)
1268
+ return;
1269
+ shown = true;
1270
+ }
1271
+ for (const listener of listeners.get(name) ?? [])
1224
1272
  listener();
1225
1273
  }
1226
1274
  };
@@ -1362,12 +1410,24 @@ function createSession(port, ready) {
1362
1410
  await call("fullscreen.toggle");
1363
1411
  },
1364
1412
  },
1413
+ quit: async () => {
1414
+ await call("quit");
1415
+ },
1365
1416
  on(event, listener) {
1366
1417
  const set = listeners.get(event) ?? new Set();
1367
1418
  set.add(listener);
1368
1419
  listeners.set(event, set);
1420
+ if (event === "shown" && shown) {
1421
+ queueMicrotask(() => {
1422
+ if (set.has(listener))
1423
+ listener();
1424
+ });
1425
+ }
1369
1426
  return () => set.delete(listener);
1370
1427
  },
1428
+ get shown() {
1429
+ return shown;
1430
+ },
1371
1431
  };
1372
1432
  }
1373
1433
  const Powly = { init, SDK_VERSION, PowlyError };
package/dist/powly.mjs CHANGED
@@ -1,6 +1,6 @@
1
- /* Powly SDK 0.4.0: ES module */
1
+ /* Powly SDK 0.6.0: ES module */
2
2
  /** This SDK's version; Powly's Studio shows it next to the downloads. */
3
- const SDK_VERSION = "0.4.0";
3
+ const SDK_VERSION = "0.6.0";
4
4
 
5
5
  // The messages between a game (this SDK, inside Powly's iframe) and the Powly page that framed it.
6
6
  // Shared with the host side (apps/web/components/player/sdk-host.ts).
@@ -12,6 +12,14 @@ const SDK_VERSION = "0.4.0";
12
12
  // 3. From then on the game also reports to Powly Anti-Cheat over the port (`PacSample`, src/pac.ts).
13
13
  const PROTOCOL = "powly-sdk";
14
14
  const PROTOCOL_VERSION = 1;
15
+ /** The corners Powly's options button (its mascot, which opens the player's options) can be in. */
16
+ const OPTIONS_BUTTON_CORNERS = [
17
+ "top-left",
18
+ "top-right",
19
+ "bottom-left",
20
+ "bottom-right",
21
+ ];
22
+ const isOptionsButtonCorner = (value) => OPTIONS_BUTTON_CORNERS.includes(value);
15
23
  const isInitMessage = (data) => typeof data === "object" &&
16
24
  data !== null &&
17
25
  data.protocol === PROTOCOL &&
@@ -21,7 +29,7 @@ const isRequestMessage = (data) => typeof data === "object" &&
21
29
  data !== null &&
22
30
  data.type === "request" &&
23
31
  Number.isInteger(data.id) &&
24
- ["getSessionToken", "purchases.open", "fullscreen.toggle", "invites.open"].includes(data.method);
32
+ ["getSessionToken", "purchases.open", "fullscreen.toggle", "invites.open", "quit"].includes(data.method);
25
33
 
26
34
  // The Powly origins this build of the SDK trusts by default: a game only handshakes with a parent
27
35
  // page on one of them (unless it passes `powlyOrigin` to `Powly.init()` itself). Set at build
@@ -47,8 +55,9 @@ class PowlyError extends Error {
47
55
  // the server's to catch speed hacks), browser functions that were replaced (speed hack scripts and
48
56
  // extensions patch timers), traces of automated browsers, and how input arrives (scripted events,
49
57
  // clicks without pointer movement as AI agents make them, machine-regular timing as macros and
50
- // auto-clickers make them). Nothing is sent anywhere else, and nothing here reads what the player
51
- // types: only event counts and timings.
58
+ // auto-clickers make them), and whether the player plays at all (any trusted input, gamepads
59
+ // included: Powly closes games nobody plays). Nothing is sent anywhere else, and nothing here
60
+ // reads what the player types: only event counts and timings.
52
61
  //
53
62
  // In real players' runs (not developer mode) the SDK also guards the game's code against being
54
63
  // copied (`guard`): the browser's context menu and its view-source, save-page and developer-tools
@@ -60,6 +69,8 @@ class PowlyError extends Error {
60
69
  // `tamperedFunctions` are shared with the Powly page (apps/web/components/player/pac-sensor.ts).
61
70
  /** How often the SDK reports (ms). */
62
71
  const PAC_SAMPLE_MS = 5000;
72
+ /** A gamepad axis past this counts as input (sticks rest a little off centre). */
73
+ const GAMEPAD_DEADZONE = 0.3;
63
74
  const isPacSample = (data) => typeof data === "object" &&
64
75
  data !== null &&
65
76
  data.type === "pac" &&
@@ -208,6 +219,7 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
208
219
  synthetic: 0,
209
220
  clicks: 0,
210
221
  teleportClicks: 0,
222
+ activity: 0,
211
223
  };
212
224
  const keyTimes = [];
213
225
  const clickTimes = [];
@@ -225,6 +237,9 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
225
237
  input.synthetic += 1;
226
238
  return;
227
239
  }
240
+ input.activity += 1;
241
+ if (event.type === "wheel")
242
+ return;
228
243
  if (event.type === "pointermove" || event.type === "pointerover") {
229
244
  const move = event;
230
245
  pointer = { x: move.clientX, y: move.clientY };
@@ -250,7 +265,26 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
250
265
  }
251
266
  pointer = { x: down.clientX, y: down.clientY };
252
267
  };
253
- const types = ["keydown", "pointerdown", "pointermove", "pointerover"];
268
+ const types = ["keydown", "pointerdown", "pointermove", "pointerover", "wheel"];
269
+ // Gamepads send no events: a pressed button or a pushed stick, looked at a few times a second.
270
+ const watchGamepads = () => {
271
+ let pads;
272
+ try {
273
+ pads = [...(win.navigator.getGamepads?.() ?? [])];
274
+ }
275
+ catch {
276
+ return;
277
+ }
278
+ for (const pad of pads) {
279
+ if (!pad?.connected)
280
+ continue;
281
+ if (pad.buttons.some((button) => button.pressed) ||
282
+ pad.axes.some((axis) => Math.abs(axis) > GAMEPAD_DEADZONE)) {
283
+ input.activity += 1;
284
+ return;
285
+ }
286
+ }
287
+ };
254
288
  for (const type of types) {
255
289
  win.addEventListener(type, onInput, { capture: true, passive: true });
256
290
  }
@@ -318,6 +352,7 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
318
352
  input.synthetic = 0;
319
353
  input.clicks = 0;
320
354
  input.teleportClicks = 0;
355
+ input.activity = 0;
321
356
  dateJump = 0;
322
357
  devtools = false;
323
358
  };
@@ -326,11 +361,13 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
326
361
  const clearInterval = win.clearInterval.bind(win);
327
362
  const dateTimer = setInterval(watchDate, 1000);
328
363
  const reportTimer = setInterval(report, PAC_SAMPLE_MS);
364
+ const gamepadTimer = setInterval(watchGamepads, 250);
329
365
  const trapTimer = trap ? setInterval(springTrap, 1500) : undefined;
330
366
  report();
331
367
  return () => {
332
368
  clearInterval(dateTimer);
333
369
  clearInterval(reportTimer);
370
+ clearInterval(gamepadTimer);
334
371
  if (trapTimer !== undefined)
335
372
  clearInterval(trapTimer);
336
373
  for (const type of types)
@@ -1172,6 +1209,9 @@ function init(options = {}) {
1172
1209
  type: "init",
1173
1210
  sdkVersion: SDK_VERSION,
1174
1211
  pac: 1,
1212
+ ...(isOptionsButtonCorner(options.optionsButton) && {
1213
+ optionsButton: options.optionsButton,
1214
+ }),
1175
1215
  };
1176
1216
  channel.port1.onmessage = (event) => {
1177
1217
  const data = event.data;
@@ -1205,6 +1245,8 @@ function createSession(port, ready) {
1205
1245
  let nextId = 1;
1206
1246
  const pending = new Map();
1207
1247
  const listeners = new Map();
1248
+ // "shown" comes once; games that listen for it later still hear it.
1249
+ let shown = false;
1208
1250
  port.onmessage = (event) => {
1209
1251
  const data = event.data;
1210
1252
  if (data?.type === "response") {
@@ -1218,7 +1260,13 @@ function createSession(port, ready) {
1218
1260
  waiting.reject(new PowlyError(data.error));
1219
1261
  }
1220
1262
  else if (data?.type === "event") {
1221
- for (const listener of listeners.get(data.event) ?? [])
1263
+ const name = data.event;
1264
+ if (name === "shown") {
1265
+ if (shown)
1266
+ return;
1267
+ shown = true;
1268
+ }
1269
+ for (const listener of listeners.get(name) ?? [])
1222
1270
  listener();
1223
1271
  }
1224
1272
  };
@@ -1360,12 +1408,24 @@ function createSession(port, ready) {
1360
1408
  await call("fullscreen.toggle");
1361
1409
  },
1362
1410
  },
1411
+ quit: async () => {
1412
+ await call("quit");
1413
+ },
1363
1414
  on(event, listener) {
1364
1415
  const set = listeners.get(event) ?? new Set();
1365
1416
  set.add(listener);
1366
1417
  listeners.set(event, set);
1418
+ if (event === "shown" && shown) {
1419
+ queueMicrotask(() => {
1420
+ if (set.has(listener))
1421
+ listener();
1422
+ });
1423
+ }
1367
1424
  return () => set.delete(listener);
1368
1425
  },
1426
+ get shown() {
1427
+ return shown;
1428
+ },
1369
1429
  };
1370
1430
  }
1371
1431
  const Powly = { init, SDK_VERSION, PowlyError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powly/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Powly game SDK: launch handshake, session tokens and platform services.",
6
6
  "license": "MIT",
@@ -12,6 +12,13 @@
12
12
  "browser-games"
13
13
  ],
14
14
  "homepage": "https://powly.gg",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/fiioonnn/powly-sdk.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/fiioonnn/powly-sdk/issues"
21
+ },
15
22
  "exports": {
16
23
  ".": {
17
24
  "types": "./dist/powly.d.ts",
@@ -36,6 +43,7 @@
36
43
  "scripts": {
37
44
  "build": "tsc -p tsconfig.build.json && node scripts/bundle.ts",
38
45
  "typecheck": "tsc --noEmit",
39
- "example:zip": "node scripts/example-zip.ts"
46
+ "example:zip": "node scripts/example-zip.ts",
47
+ "mirror": "node scripts/mirror.ts"
40
48
  }
41
49
  }