@playcademy/sdk 0.0.1-beta.2 → 0.0.1-beta.21

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 ADDED
@@ -0,0 +1,584 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, {
5
+ get: all[name],
6
+ enumerable: true,
7
+ configurable: true,
8
+ set: (newValue) => all[name] = () => newValue
9
+ });
10
+ };
11
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
12
+
13
+ // src/core/errors.ts
14
+ var PlaycademyError, ApiError;
15
+ var init_errors = __esm(() => {
16
+ PlaycademyError = class PlaycademyError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "PlaycademyError";
20
+ }
21
+ };
22
+ ApiError = class ApiError extends Error {
23
+ status;
24
+ details;
25
+ constructor(status, message, details) {
26
+ super(`${status} ${message}`);
27
+ this.status = status;
28
+ this.details = details;
29
+ Object.setPrototypeOf(this, ApiError.prototype);
30
+ }
31
+ };
32
+ });
33
+
34
+ // src/core/request.ts
35
+ async function request({
36
+ path,
37
+ baseUrl,
38
+ token,
39
+ method = "GET",
40
+ body,
41
+ extraHeaders = {}
42
+ }) {
43
+ const url = baseUrl.replace(/\/$/, "") + (path.startsWith("/") ? path : `/${path}`);
44
+ const headers = { ...extraHeaders };
45
+ let payload;
46
+ if (body instanceof FormData) {
47
+ payload = body;
48
+ } else if (body !== undefined && body !== null) {
49
+ payload = JSON.stringify(body);
50
+ headers["Content-Type"] = "application/json";
51
+ }
52
+ if (token)
53
+ headers["Authorization"] = `Bearer ${token}`;
54
+ const res = await fetch(url, {
55
+ method,
56
+ headers,
57
+ body: payload,
58
+ credentials: "omit"
59
+ });
60
+ if (!res.ok) {
61
+ const errorBody = await res.clone().json().catch(() => res.text().catch(() => {
62
+ return;
63
+ })) ?? undefined;
64
+ throw new ApiError(res.status, res.statusText, errorBody);
65
+ }
66
+ if (res.status === 204)
67
+ return;
68
+ const contentType = res.headers.get("content-type") ?? "";
69
+ if (contentType.includes("application/json")) {
70
+ return await res.json();
71
+ }
72
+ return await res.text();
73
+ }
74
+ async function fetchManifest(assetBundleBase) {
75
+ const manifestUrl = `${assetBundleBase.replace(/\/$/, "")}/playcademy.manifest.json`;
76
+ try {
77
+ const response = await fetch(manifestUrl);
78
+ if (!response.ok) {
79
+ console.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
80
+ throw new PlaycademyError(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
81
+ }
82
+ return await response.json();
83
+ } catch (error) {
84
+ if (error instanceof PlaycademyError) {
85
+ throw error;
86
+ }
87
+ console.error(`[fetchManifest] Error fetching or parsing manifest from ${manifestUrl}:`, error);
88
+ throw new PlaycademyError("Failed to load or parse game manifest");
89
+ }
90
+ }
91
+ var init_request = __esm(() => {
92
+ init_errors();
93
+ init_errors();
94
+ });
95
+
96
+ // src/core/namespaces/auth.ts
97
+ function createAuthNamespace(client) {
98
+ return {
99
+ logout: async () => {
100
+ await client["request"](`/auth/logout`, "DELETE");
101
+ client.setToken(null);
102
+ }
103
+ };
104
+ }
105
+
106
+ // src/messaging.ts
107
+ class PlaycademyMessaging {
108
+ listeners = new Map;
109
+ send(type, payload, options) {
110
+ if (options?.target) {
111
+ this.sendViaPostMessage(type, payload, options.target, options.origin || "*");
112
+ return;
113
+ }
114
+ const context = this.getMessagingContext(type);
115
+ if (context.shouldUsePostMessage) {
116
+ this.sendViaPostMessage(type, payload, context.target, context.origin);
117
+ } else {
118
+ this.sendViaCustomEvent(type, payload);
119
+ }
120
+ }
121
+ listen(type, handler) {
122
+ const postMessageListener = (event) => {
123
+ const messageEvent = event;
124
+ if (messageEvent.data?.type === type) {
125
+ handler(messageEvent.data.payload || messageEvent.data);
126
+ }
127
+ };
128
+ const customEventListener = (event) => {
129
+ handler(event.detail);
130
+ };
131
+ if (!this.listeners.has(type)) {
132
+ this.listeners.set(type, new Map);
133
+ }
134
+ const listenerMap = this.listeners.get(type);
135
+ listenerMap.set(handler, {
136
+ postMessage: postMessageListener,
137
+ customEvent: customEventListener
138
+ });
139
+ window.addEventListener("message", postMessageListener);
140
+ window.addEventListener(type, customEventListener);
141
+ }
142
+ unlisten(type, handler) {
143
+ const typeListeners = this.listeners.get(type);
144
+ if (!typeListeners || !typeListeners.has(handler)) {
145
+ return;
146
+ }
147
+ const listeners = typeListeners.get(handler);
148
+ window.removeEventListener("message", listeners.postMessage);
149
+ window.removeEventListener(type, listeners.customEvent);
150
+ typeListeners.delete(handler);
151
+ if (typeListeners.size === 0) {
152
+ this.listeners.delete(type);
153
+ }
154
+ }
155
+ getMessagingContext(eventType) {
156
+ const isIframe = typeof window !== "undefined" && window.self !== window.top;
157
+ const iframeToParentEvents = [
158
+ "PLAYCADEMY_READY" /* READY */,
159
+ "PLAYCADEMY_EXIT" /* EXIT */,
160
+ "PLAYCADEMY_TELEMETRY" /* TELEMETRY */
161
+ ];
162
+ const shouldUsePostMessage = isIframe && iframeToParentEvents.includes(eventType);
163
+ return {
164
+ shouldUsePostMessage,
165
+ target: shouldUsePostMessage ? window.parent : undefined,
166
+ origin: "*"
167
+ };
168
+ }
169
+ sendViaPostMessage(type, payload, target = window.parent, origin = "*") {
170
+ const messageData = { type };
171
+ if (payload !== undefined) {
172
+ messageData.payload = payload;
173
+ }
174
+ target.postMessage(messageData, origin);
175
+ }
176
+ sendViaCustomEvent(type, payload) {
177
+ window.dispatchEvent(new CustomEvent(type, { detail: payload }));
178
+ }
179
+ }
180
+ var MessageEvents, messaging;
181
+ var init_messaging = __esm(() => {
182
+ ((MessageEvents2) => {
183
+ MessageEvents2["INIT"] = "PLAYCADEMY_INIT";
184
+ MessageEvents2["TOKEN_REFRESH"] = "PLAYCADEMY_TOKEN_REFRESH";
185
+ MessageEvents2["PAUSE"] = "PLAYCADEMY_PAUSE";
186
+ MessageEvents2["RESUME"] = "PLAYCADEMY_RESUME";
187
+ MessageEvents2["FORCE_EXIT"] = "PLAYCADEMY_FORCE_EXIT";
188
+ MessageEvents2["OVERLAY"] = "PLAYCADEMY_OVERLAY";
189
+ MessageEvents2["READY"] = "PLAYCADEMY_READY";
190
+ MessageEvents2["EXIT"] = "PLAYCADEMY_EXIT";
191
+ MessageEvents2["TELEMETRY"] = "PLAYCADEMY_TELEMETRY";
192
+ })(MessageEvents ||= {});
193
+ messaging = new PlaycademyMessaging;
194
+ });
195
+
196
+ // src/core/namespaces/runtime.ts
197
+ function createRuntimeNamespace(client) {
198
+ return {
199
+ getGameToken: async (gameId, options) => {
200
+ const res = await client["request"](`/games/${gameId}/token`, "POST");
201
+ if (options?.apply) {
202
+ client.setToken(res.token);
203
+ }
204
+ return res;
205
+ },
206
+ exit: async () => {
207
+ if (client["internalClientSessionId"] && client["gameId"]) {
208
+ try {
209
+ await client.games.endSession(client["internalClientSessionId"], client["gameId"]);
210
+ } catch (error) {
211
+ console.error("[SDK] Failed to auto-end session:", client["internalClientSessionId"], error);
212
+ }
213
+ }
214
+ messaging.send("PLAYCADEMY_EXIT" /* EXIT */, undefined);
215
+ }
216
+ };
217
+ }
218
+ var init_runtime = __esm(() => {
219
+ init_messaging();
220
+ });
221
+
222
+ // src/core/namespaces/games.ts
223
+ function createGamesNamespace(client) {
224
+ return {
225
+ fetch: async (gameIdOrSlug) => {
226
+ const baseGameData = await client["request"](`/games/${gameIdOrSlug}`, "GET");
227
+ const manifestData = await fetchManifest(baseGameData.assetBundleBase);
228
+ return {
229
+ ...baseGameData,
230
+ manifest: manifestData
231
+ };
232
+ },
233
+ list: () => client["request"]("/games", "GET"),
234
+ saveState: async (state) => {
235
+ const gameId = client["_ensureGameId"]();
236
+ await client["request"](`/games/${gameId}/state`, "POST", state);
237
+ },
238
+ loadState: async () => {
239
+ const gameId = client["_ensureGameId"]();
240
+ return client["request"](`/games/${gameId}/state`, "GET");
241
+ },
242
+ startSession: async (gameId) => {
243
+ const idToUse = gameId ?? client["_ensureGameId"]();
244
+ return client["request"](`/games/${idToUse}/sessions`, "POST", {});
245
+ },
246
+ endSession: async (sessionId, gameId) => {
247
+ const effectiveGameIdToEnd = gameId ?? client["_ensureGameId"]();
248
+ if (client["internalClientSessionId"] && sessionId === client["internalClientSessionId"] && effectiveGameIdToEnd === client["gameId"]) {
249
+ client["internalClientSessionId"] = undefined;
250
+ }
251
+ await client["request"](`/games/${effectiveGameIdToEnd}/sessions/${sessionId}/end`, "POST");
252
+ }
253
+ };
254
+ }
255
+ var init_games = __esm(() => {
256
+ init_request();
257
+ });
258
+
259
+ // src/core/namespaces/users.ts
260
+ function createUsersNamespace(client) {
261
+ return {
262
+ me: async () => {
263
+ return client["request"]("/users/me", "GET");
264
+ },
265
+ inventory: {
266
+ get: async () => client["request"](`/inventory`, "GET"),
267
+ add: async (itemId, qty) => {
268
+ const res = await client["request"](`/inventory/add`, "POST", { itemId, qty });
269
+ client["emit"]("inventoryChange", {
270
+ itemId,
271
+ delta: qty,
272
+ newTotal: res.newTotal
273
+ });
274
+ return res;
275
+ },
276
+ spend: async (itemId, qty) => {
277
+ const res = await client["request"](`/inventory/spend`, "POST", { itemId, qty });
278
+ client["emit"]("inventoryChange", {
279
+ itemId,
280
+ delta: -qty,
281
+ newTotal: res.newTotal
282
+ });
283
+ return res;
284
+ }
285
+ }
286
+ };
287
+ }
288
+
289
+ // src/core/namespaces/dev.ts
290
+ function createDevNamespace(client) {
291
+ return {
292
+ auth: {
293
+ applyForDeveloper: () => client["request"]("/dev/apply", "POST"),
294
+ getDeveloperStatus: async () => {
295
+ const response = await client["request"]("/dev/status", "GET");
296
+ return response.status;
297
+ }
298
+ },
299
+ games: {
300
+ upsert: (slug, metadata, file) => {
301
+ const form = new FormData;
302
+ form.append("metadata", JSON.stringify(metadata));
303
+ form.append("file", file);
304
+ return client["request"](`/games/${slug}`, "PUT", form);
305
+ },
306
+ update: (gameId, props) => client["request"](`/games/${gameId}`, "PATCH", props),
307
+ delete: (gameId) => client["request"](`/games/${gameId}`, "DELETE")
308
+ },
309
+ keys: {
310
+ createKey: (gameId, label) => client["request"](`/dev/games/${gameId}/keys`, "POST", { label }),
311
+ listKeys: (gameId) => client["request"](`/dev/games/${gameId}/keys`, "GET"),
312
+ revokeKey: (keyId) => client["request"](`/dev/keys/${keyId}`, "DELETE")
313
+ }
314
+ };
315
+ }
316
+
317
+ // src/core/namespaces/maps.ts
318
+ function createMapsNamespace(client) {
319
+ return {
320
+ elements: (mapId) => client["request"](`/map/elements?mapId=${mapId}`, "GET")
321
+ };
322
+ }
323
+
324
+ // src/core/namespaces/admin.ts
325
+ function createAdminNamespace(client) {
326
+ return {
327
+ games: {
328
+ pauseGame: (gameId) => client["request"](`/admin/games/${gameId}/pause`, "POST"),
329
+ resumeGame: (gameId) => client["request"](`/admin/games/${gameId}/resume`, "POST")
330
+ },
331
+ items: {
332
+ create: (props) => client["request"]("/items", "POST", props),
333
+ get: (itemId) => client["request"](`/items/${itemId}`, "GET"),
334
+ list: () => client["request"]("/items", "GET"),
335
+ update: (itemId, props) => client["request"](`/items/${itemId}`, "PATCH", props),
336
+ delete: (itemId) => client["request"](`/items/${itemId}`, "DELETE")
337
+ },
338
+ currencies: {
339
+ create: (props) => client["request"]("/currencies", "POST", props),
340
+ get: (currencyId) => client["request"](`/currencies/${currencyId}`, "GET"),
341
+ list: () => client["request"]("/currencies", "GET"),
342
+ update: (currencyId, props) => client["request"](`/currencies/${currencyId}`, "PATCH", props),
343
+ delete: (currencyId) => client["request"](`/currencies/${currencyId}`, "DELETE")
344
+ },
345
+ shopListings: {
346
+ create: (props) => client["request"]("/shop-listings", "POST", props),
347
+ get: (listingId) => client["request"](`/shop-listings/${listingId}`, "GET"),
348
+ list: () => client["request"]("/shop-listings", "GET"),
349
+ update: (listingId, props) => client["request"](`/shop-listings/${listingId}`, "PATCH", props),
350
+ delete: (listingId) => client["request"](`/shop-listings/${listingId}`, "DELETE")
351
+ }
352
+ };
353
+ }
354
+
355
+ // src/core/namespaces/shop.ts
356
+ function createShopNamespace(client) {
357
+ return {
358
+ view: () => {
359
+ return client["request"]("/shop/view", "GET");
360
+ }
361
+ };
362
+ }
363
+
364
+ // src/core/namespaces/telemetry.ts
365
+ function createTelemetryNamespace(client) {
366
+ return {
367
+ pushMetrics: (metrics) => client["request"](`/telemetry/metrics`, "POST", metrics)
368
+ };
369
+ }
370
+
371
+ // src/core/namespaces/index.ts
372
+ var init_namespaces = __esm(() => {
373
+ init_runtime();
374
+ init_games();
375
+ });
376
+
377
+ // src/core/static/init.ts
378
+ async function getPlaycademyConfig() {
379
+ const preloaded = window.PLAYCADEMY;
380
+ if (preloaded?.token) {
381
+ return preloaded;
382
+ }
383
+ if (window.self !== window.top) {
384
+ return await waitForPlaycademyInit();
385
+ } else {
386
+ return createStandaloneConfig();
387
+ }
388
+ }
389
+ async function waitForPlaycademyInit() {
390
+ return new Promise((resolve, reject) => {
391
+ let contextReceived = false;
392
+ const timeoutDuration = 5000;
393
+ const handleMessage = (event) => {
394
+ if (event.data?.type === "PLAYCADEMY_INIT" /* INIT */) {
395
+ contextReceived = true;
396
+ window.removeEventListener("message", handleMessage);
397
+ clearTimeout(timeoutId);
398
+ window.PLAYCADEMY = event.data.payload;
399
+ resolve(event.data.payload);
400
+ }
401
+ };
402
+ window.addEventListener("message", handleMessage);
403
+ const timeoutId = setTimeout(() => {
404
+ if (!contextReceived) {
405
+ window.removeEventListener("message", handleMessage);
406
+ reject(new Error(`${"PLAYCADEMY_INIT" /* INIT */} not received within ${timeoutDuration}ms`));
407
+ }
408
+ }, timeoutDuration);
409
+ });
410
+ }
411
+ function createStandaloneConfig() {
412
+ const mockConfig = {
413
+ baseUrl: "/api",
414
+ token: "mock-game-token-for-local-dev",
415
+ gameId: "mock-game-id-from-template"
416
+ };
417
+ window.PLAYCADEMY = mockConfig;
418
+ return mockConfig;
419
+ }
420
+ async function init() {
421
+ const { PlaycademyClient } = await Promise.resolve().then(() => (init_client(), exports_client));
422
+ if (typeof window === "undefined") {
423
+ throw new Error("Playcademy SDK must run in a browser context");
424
+ }
425
+ const config = await getPlaycademyConfig();
426
+ const client = new PlaycademyClient({
427
+ baseUrl: config.baseUrl,
428
+ token: config.token,
429
+ gameId: config.gameId
430
+ });
431
+ messaging.listen("PLAYCADEMY_TOKEN_REFRESH" /* TOKEN_REFRESH */, ({ token }) => client.setToken(token));
432
+ messaging.send("PLAYCADEMY_READY" /* READY */, undefined);
433
+ if (import.meta.env?.MODE === "development") {
434
+ window.PLAYCADEMY_CLIENT = client;
435
+ }
436
+ return client;
437
+ }
438
+ var init_init = __esm(() => {
439
+ init_messaging();
440
+ });
441
+
442
+ // src/core/static/login.ts
443
+ async function login(baseUrl, email, password) {
444
+ let url = baseUrl;
445
+ if (baseUrl.startsWith("/") && typeof window !== "undefined") {
446
+ url = window.location.origin + baseUrl;
447
+ }
448
+ url = url + "/auth/login";
449
+ const response = await fetch(url, {
450
+ method: "POST",
451
+ headers: {
452
+ "Content-Type": "application/json"
453
+ },
454
+ body: JSON.stringify({ email, password })
455
+ });
456
+ if (!response.ok) {
457
+ try {
458
+ const errorData = await response.json();
459
+ const errorMessage = errorData && errorData.message ? String(errorData.message) : response.statusText;
460
+ throw new PlaycademyError(errorMessage);
461
+ } catch (error) {
462
+ console.error("[SDK] Failed to parse error response JSON, using status text instead:", error);
463
+ throw new PlaycademyError(response.statusText);
464
+ }
465
+ }
466
+ return response.json();
467
+ }
468
+ var init_login = __esm(() => {
469
+ init_errors();
470
+ });
471
+
472
+ // src/core/static/index.ts
473
+ var init_static = __esm(() => {
474
+ init_init();
475
+ init_login();
476
+ });
477
+
478
+ // src/core/client.ts
479
+ var exports_client = {};
480
+ __export(exports_client, {
481
+ PlaycademyClient: () => PlaycademyClient
482
+ });
483
+ var PlaycademyClient;
484
+ var init_client = __esm(() => {
485
+ init_errors();
486
+ init_request();
487
+ init_namespaces();
488
+ init_static();
489
+ PlaycademyClient = class PlaycademyClient {
490
+ baseUrl;
491
+ token;
492
+ gameId;
493
+ listeners = {};
494
+ internalClientSessionId;
495
+ constructor(config) {
496
+ if (!config) {
497
+ this.baseUrl = "/api";
498
+ this.token = undefined;
499
+ this.gameId = undefined;
500
+ } else {
501
+ this.baseUrl = config.baseUrl || "/api";
502
+ this.token = config.token;
503
+ this.gameId = config.gameId;
504
+ }
505
+ if (this.gameId) {
506
+ this._initializeInternalSession().catch((error) => {
507
+ console.error("[SDK] Background initialization of auto-session failed:", error);
508
+ });
509
+ }
510
+ }
511
+ getBaseUrl() {
512
+ const isRelative = this.baseUrl.startsWith("/");
513
+ const isBrowser = typeof window !== "undefined";
514
+ return isRelative && isBrowser ? `${window.location.origin}${this.baseUrl}` : this.baseUrl;
515
+ }
516
+ ping() {
517
+ return "pong";
518
+ }
519
+ setToken(token) {
520
+ this.token = token ?? undefined;
521
+ this.emit("authChange", { token: this.token ?? null });
522
+ }
523
+ onAuthChange(callback) {
524
+ this.on("authChange", (payload) => callback(payload.token));
525
+ }
526
+ on(event, callback) {
527
+ this.listeners[event] = this.listeners[event] ?? [];
528
+ this.listeners[event].push(callback);
529
+ }
530
+ emit(event, payload) {
531
+ (this.listeners[event] ?? []).forEach((listener) => {
532
+ listener(payload);
533
+ });
534
+ }
535
+ async request(path, method, body, headers) {
536
+ const effectiveHeaders = { ...headers };
537
+ return request({
538
+ path,
539
+ method,
540
+ body,
541
+ baseUrl: this.baseUrl,
542
+ token: this.token,
543
+ extraHeaders: effectiveHeaders
544
+ });
545
+ }
546
+ _ensureGameId() {
547
+ if (!this.gameId) {
548
+ throw new PlaycademyError("This operation requires a gameId, but none was provided when initializing the client.");
549
+ }
550
+ return this.gameId;
551
+ }
552
+ async _initializeInternalSession() {
553
+ if (!this.gameId || this.internalClientSessionId) {
554
+ return;
555
+ }
556
+ try {
557
+ const response = await this.games.startSession(this.gameId);
558
+ this.internalClientSessionId = response.sessionId;
559
+ } catch (error) {
560
+ console.error("[SDK] Auto-starting session failed for game", this.gameId, error);
561
+ }
562
+ }
563
+ auth = createAuthNamespace(this);
564
+ runtime = createRuntimeNamespace(this);
565
+ games = createGamesNamespace(this);
566
+ users = createUsersNamespace(this);
567
+ dev = createDevNamespace(this);
568
+ maps = createMapsNamespace(this);
569
+ admin = createAdminNamespace(this);
570
+ shop = createShopNamespace(this);
571
+ telemetry = createTelemetryNamespace(this);
572
+ static init = init;
573
+ static login = login;
574
+ };
575
+ });
576
+
577
+ // src/index.ts
578
+ init_client();
579
+ init_messaging();
580
+ export {
581
+ messaging,
582
+ PlaycademyClient,
583
+ MessageEvents
584
+ };