@playcademy/vite-plugin 0.2.22-beta.4 → 0.2.22-beta.6

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.
Files changed (2) hide show
  1. package/dist/index.js +153 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25334,7 +25334,7 @@ var package_default;
25334
25334
  var init_package = __esm(() => {
25335
25335
  package_default = {
25336
25336
  name: "@playcademy/sandbox",
25337
- version: "0.3.16-beta.4",
25337
+ version: "0.3.16-beta.6",
25338
25338
  description: "Local development server for Playcademy game development",
25339
25339
  type: "module",
25340
25340
  exports: {
@@ -50501,9 +50501,37 @@ var init_developer_service = __esm(() => {
50501
50501
 
50502
50502
  class GameService {
50503
50503
  deps;
50504
+ static MANIFEST_FETCH_TIMEOUT_MS = 5000;
50505
+ static MAX_FETCH_ERROR_MESSAGE_LENGTH = 512;
50504
50506
  constructor(deps) {
50505
50507
  this.deps = deps;
50506
50508
  }
50509
+ static getManifestHost(manifestUrl) {
50510
+ try {
50511
+ return new URL(manifestUrl).host;
50512
+ } catch {
50513
+ return manifestUrl;
50514
+ }
50515
+ }
50516
+ static getFetchErrorMessage(error) {
50517
+ let raw;
50518
+ if (error instanceof Error) {
50519
+ raw = error.message;
50520
+ } else if (typeof error === "string") {
50521
+ raw = error;
50522
+ }
50523
+ if (!raw) {
50524
+ return;
50525
+ }
50526
+ const normalized = raw.replace(/\s+/g, " ").trim();
50527
+ if (!normalized) {
50528
+ return;
50529
+ }
50530
+ return normalized.slice(0, GameService.MAX_FETCH_ERROR_MESSAGE_LENGTH);
50531
+ }
50532
+ static isRetryableStatus(status) {
50533
+ return status === 429 || status >= 500;
50534
+ }
50507
50535
  async list(caller) {
50508
50536
  const db2 = this.deps.db;
50509
50537
  const isAdmin = caller?.role === "admin";
@@ -50565,6 +50593,109 @@ class GameService {
50565
50593
  this.enforceVisibility(game, caller, slug);
50566
50594
  return game;
50567
50595
  }
50596
+ async getManifest(gameId, caller) {
50597
+ const game = await this.getById(gameId, caller);
50598
+ if (game.gameType !== "hosted" || !game.deploymentUrl) {
50599
+ throw new BadRequestError("Game does not have a deployment manifest");
50600
+ }
50601
+ const deploymentUrl = game.deploymentUrl;
50602
+ const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
50603
+ const manifestHost = GameService.getManifestHost(manifestUrl);
50604
+ const startedAt = Date.now();
50605
+ const controller = new AbortController;
50606
+ const timeout = setTimeout(() => controller.abort(), GameService.MANIFEST_FETCH_TIMEOUT_MS);
50607
+ function buildDetails(fetchOutcome, manifestErrorKind, extra = {}) {
50608
+ return {
50609
+ manifestUrl,
50610
+ manifestHost,
50611
+ deploymentUrl,
50612
+ fetchOutcome,
50613
+ retryCount: 0,
50614
+ durationMs: Date.now() - startedAt,
50615
+ manifestErrorKind,
50616
+ ...extra
50617
+ };
50618
+ }
50619
+ let response;
50620
+ try {
50621
+ response = await fetch(manifestUrl, {
50622
+ method: "GET",
50623
+ headers: {
50624
+ Accept: "application/json"
50625
+ },
50626
+ signal: controller.signal
50627
+ });
50628
+ } catch (error) {
50629
+ clearTimeout(timeout);
50630
+ const fetchErrorMessage = GameService.getFetchErrorMessage(error);
50631
+ const details = buildDetails("network_error", "temporary", fetchErrorMessage ? { fetchErrorMessage } : {});
50632
+ logger5.error("Failed to fetch game manifest", {
50633
+ gameId,
50634
+ manifestUrl,
50635
+ error,
50636
+ details
50637
+ });
50638
+ if (error instanceof Error && error.name === "AbortError") {
50639
+ throw new TimeoutError("Timed out loading game manifest", details);
50640
+ }
50641
+ throw new ServiceUnavailableError("Failed to load game manifest", details);
50642
+ } finally {
50643
+ clearTimeout(timeout);
50644
+ }
50645
+ if (!response.ok) {
50646
+ const resolvedManifestUrl = response.url || manifestUrl;
50647
+ const resolvedManifestHost = GameService.getManifestHost(resolvedManifestUrl);
50648
+ const manifestErrorKind = GameService.isRetryableStatus(response.status) ? "temporary" : "permanent";
50649
+ const details = buildDetails("bad_status", manifestErrorKind, {
50650
+ manifestUrl: resolvedManifestUrl,
50651
+ manifestHost: resolvedManifestHost,
50652
+ status: response.status,
50653
+ contentType: response.headers.get("content-type") ?? undefined,
50654
+ cfRay: response.headers.get("cf-ray") ?? undefined,
50655
+ redirected: response.redirected,
50656
+ ...response.redirected ? {
50657
+ originalManifestUrl: manifestUrl,
50658
+ originalManifestHost: manifestHost
50659
+ } : {}
50660
+ });
50661
+ const message = `Failed to fetch manifest: ${response.status} ${response.statusText}`;
50662
+ logger5.error("Game manifest returned non-ok response", {
50663
+ gameId,
50664
+ manifestUrl,
50665
+ status: response.status,
50666
+ details
50667
+ });
50668
+ if (manifestErrorKind === "temporary") {
50669
+ throw new ServiceUnavailableError(message, details);
50670
+ }
50671
+ throw new BadRequestError(message, details);
50672
+ }
50673
+ try {
50674
+ return await response.json();
50675
+ } catch (error) {
50676
+ const resolvedManifestUrl = response.url || manifestUrl;
50677
+ const resolvedManifestHost = GameService.getManifestHost(resolvedManifestUrl);
50678
+ const details = buildDetails("invalid_body", "permanent", {
50679
+ manifestUrl: resolvedManifestUrl,
50680
+ manifestHost: resolvedManifestHost,
50681
+ status: response.status,
50682
+ contentType: response.headers.get("content-type") ?? undefined,
50683
+ cfRay: response.headers.get("cf-ray") ?? undefined,
50684
+ redirected: response.redirected,
50685
+ ...response.redirected ? {
50686
+ originalManifestUrl: manifestUrl,
50687
+ originalManifestHost: manifestHost
50688
+ } : {}
50689
+ });
50690
+ logger5.error("Failed to parse game manifest", {
50691
+ gameId,
50692
+ manifestUrl,
50693
+ error,
50694
+ details
50695
+ });
50696
+ throw new BadRequestError("Failed to parse game manifest", details);
50697
+ }
50698
+ }
50568
50699
  enforceVisibility(game, caller, lookupIdentifier) {
50569
50700
  if (game.visibility !== "internal") {
50570
50701
  return;
@@ -58886,7 +59017,7 @@ function createOneRosterNamespace(client) {
58886
59017
  limit: options?.limit,
58887
59018
  offset: options?.offset,
58888
59019
  fields: options?.fields,
58889
- filter: `student.sourcedId='${studentSourcedId}'`
59020
+ filter: `student.sourcedId='${escapeFilterValue2(studentSourcedId)}'`
58890
59021
  });
58891
59022
  } catch (error) {
58892
59023
  logTimebackError("list assessment results for student", error, {
@@ -120722,6 +120853,7 @@ var listManageable;
120722
120853
  var getSubjects;
120723
120854
  var getById2;
120724
120855
  var getBySlug;
120856
+ var getManifest;
120725
120857
  var upsertBySlug;
120726
120858
  var remove3;
120727
120859
  var games2;
@@ -120764,6 +120896,21 @@ var init_game_controller = __esm(() => {
120764
120896
  logger46.debug("Getting game by slug", { userId: ctx.user.id, slug: slug2, launchId: ctx.launchId });
120765
120897
  return ctx.services.game.getBySlug(slug2, ctx.user);
120766
120898
  });
120899
+ getManifest = requireAuth(async (ctx) => {
120900
+ const gameId = ctx.params.gameId;
120901
+ if (!gameId) {
120902
+ throw ApiError.badRequest("Missing game ID");
120903
+ }
120904
+ if (!isValidUUID(gameId)) {
120905
+ throw ApiError.unprocessableEntity("gameId must be a valid UUID format");
120906
+ }
120907
+ logger46.debug("Getting game manifest by ID", {
120908
+ userId: ctx.user.id,
120909
+ gameId,
120910
+ launchId: ctx.launchId
120911
+ });
120912
+ return ctx.services.game.getManifest(gameId, ctx.user);
120913
+ });
120767
120914
  upsertBySlug = requireAuth(async (ctx) => {
120768
120915
  const slug2 = ctx.params.slug;
120769
120916
  if (!slug2) {
@@ -120800,6 +120947,7 @@ var init_game_controller = __esm(() => {
120800
120947
  listManageable,
120801
120948
  getSubjects,
120802
120949
  getById: getById2,
120950
+ getManifest,
120803
120951
  getBySlug,
120804
120952
  upsertBySlug,
120805
120953
  remove: remove3
@@ -122674,7 +122822,8 @@ var init_crud = __esm(() => {
122674
122822
  init_api();
122675
122823
  gameCrudRouter = new Hono2;
122676
122824
  gameCrudRouter.get("/", handle2(games2.list));
122677
- gameCrudRouter.get("/:gameId{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}", handle2(games2.getById));
122825
+ gameCrudRouter.get("/:gameId{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}}/manifest", handle2(games2.getManifest));
122826
+ gameCrudRouter.get("/:gameId{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}}", handle2(games2.getById));
122678
122827
  gameCrudRouter.get("/:slug", handle2(games2.getBySlug));
122679
122828
  gameCrudRouter.put("/:slug", handle2(games2.upsertBySlug));
122680
122829
  gameCrudRouter.delete("/:gameId", handle2(games2.remove, { status: 204 }));
@@ -124987,7 +125136,7 @@ var import_picocolors12 = __toESM(require_picocolors(), 1);
124987
125136
  // package.json
124988
125137
  var package_default2 = {
124989
125138
  name: "@playcademy/vite-plugin",
124990
- version: "0.2.22-beta.4",
125139
+ version: "0.2.22-beta.6",
124991
125140
  type: "module",
124992
125141
  exports: {
124993
125142
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "0.2.22-beta.4",
3
+ "version": "0.2.22-beta.6",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {