@playcademy/sandbox 0.5.1-beta.3 → 0.5.1-beta.5

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/cli.js CHANGED
@@ -979,10 +979,10 @@ var init_dist = __esm(() => {
979
979
  // ../utils/src/port.ts
980
980
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
981
981
  import { createServer } from "node:net";
982
- import { homedir } from "node:os";
982
+ import * as os from "node:os";
983
983
  import { join } from "node:path";
984
984
  function getRegistryPath() {
985
- const home = homedir();
985
+ const home = os.homedir();
986
986
  const dir = join(home, ".playcademy");
987
987
  if (!existsSync(dir)) {
988
988
  mkdirSync(dir, { recursive: true });
@@ -1078,7 +1078,7 @@ var package_default;
1078
1078
  var init_package = __esm(() => {
1079
1079
  package_default = {
1080
1080
  name: "@playcademy/sandbox",
1081
- version: "0.5.1-beta.3",
1081
+ version: "0.5.1-beta.5",
1082
1082
  description: "Local development server for Playcademy game development",
1083
1083
  type: "module",
1084
1084
  exports: {
@@ -22505,7 +22505,6 @@ class DeployJobService {
22505
22505
  "app.deploy_job.outcome": "succeeded",
22506
22506
  "app.deploy_job.run_duration_ms": DeployJobService.elapsedMsSince(new Date(runStartedAt))
22507
22507
  });
22508
- await this.deps.cache.refreshGameOrigins().catch(catchAttrs("deploy_job.cache_refresh"));
22509
22508
  await this.deleteCodeBundle(jobId);
22510
22509
  } catch (error) {
22511
22510
  clearInterval(heartbeat);
@@ -26423,7 +26422,7 @@ ${file}:${line3}:${column2}: ERROR: ${pluginText}${e.text}`;
26423
26422
  return result;
26424
26423
  }
26425
26424
  var fs2 = __require("fs");
26426
- var os = __require("os");
26425
+ var os2 = __require("os");
26427
26426
  var path = __require("path");
26428
26427
  var ESBUILD_BINARY_PATH = process.env.ESBUILD_BINARY_PATH || ESBUILD_BINARY_PATH;
26429
26428
  var isValidBinaryPath = (x) => !!x && x !== "/usr/bin/esbuild";
@@ -26465,7 +26464,7 @@ ${file}:${line3}:${column2}: ERROR: ${pluginText}${e.text}`;
26465
26464
  let pkg;
26466
26465
  let subpath;
26467
26466
  let isWASM = false;
26468
- let platformKey = `${process.platform} ${os.arch()} ${os.endianness()}`;
26467
+ let platformKey = `${process.platform} ${os2.arch()} ${os2.endianness()}`;
26469
26468
  if (platformKey in knownWindowsPackages) {
26470
26469
  pkg = knownWindowsPackages[platformKey];
26471
26470
  subpath = "esbuild.exe";
@@ -26608,7 +26607,7 @@ for your current platform.`);
26608
26607
  var crypto3 = __require("crypto");
26609
26608
  var path2 = __require("path");
26610
26609
  var fs22 = __require("fs");
26611
- var os2 = __require("os");
26610
+ var os22 = __require("os");
26612
26611
  var tty = __require("tty");
26613
26612
  var worker_threads;
26614
26613
  if (process.env.ESBUILD_WORKER_THREADS !== "0") {
@@ -26919,7 +26918,7 @@ More information: The file containing the code for esbuild's JavaScript API (${_
26919
26918
  afterClose(null);
26920
26919
  };
26921
26920
  var randomFileName = () => {
26922
- return path2.join(os2.tmpdir(), `esbuild-${crypto3.randomBytes(32).toString("hex")}`);
26921
+ return path2.join(os22.tmpdir(), `esbuild-${crypto3.randomBytes(32).toString("hex")}`);
26923
26922
  };
26924
26923
  var workerThreadService = null;
26925
26924
  var startWorkerThreadService = (worker_threads2) => {
@@ -27181,7 +27180,8 @@ class DeployService {
27181
27180
  expiresIn: null,
27182
27181
  permissions: {
27183
27182
  games: [`read:${slug}`, `write:${slug}`]
27184
- }
27183
+ },
27184
+ rateLimitEnabled: false
27185
27185
  });
27186
27186
  setAttribute("app.deploy.api_key_outcome", "created");
27187
27187
  return apiKey;
@@ -27242,9 +27242,9 @@ class DeployService {
27242
27242
  throw new ValidationError("Uploaded file is empty or not found");
27243
27243
  }
27244
27244
  setAttribute("app.deploy.asset_upload_size", frontendZip.length);
27245
- const os = await import("os");
27245
+ const os2 = await import("os");
27246
27246
  const path = await import("path");
27247
- const tempDir = path.join(os.tmpdir(), `playcademy-deploy-${gameId}-${Date.now()}`);
27247
+ const tempDir = path.join(os2.tmpdir(), `playcademy-deploy-${gameId}-${Date.now()}`);
27248
27248
  const assetsPath = path.join(tempDir, "dist");
27249
27249
  await withSpan("deploy.extract_assets", () => extractZip(frontendZip, assetsPath));
27250
27250
  uploadDeps.deleteObject(uploadToken).catch(catchAttrs("deploy.temp_cleanup"));
@@ -27857,6 +27857,31 @@ var init_game_service = __esm(() => {
27857
27857
  static changedFields(data) {
27858
27858
  return Object.entries(data).filter(([, value]) => value !== undefined).map(([key]) => key).toSorted().join(",");
27859
27859
  }
27860
+ static safeOrigin(game2) {
27861
+ if (!game2 || game2.gameType !== "external" || !game2.externalUrl) {
27862
+ return;
27863
+ }
27864
+ try {
27865
+ return new URL(game2.externalUrl).origin;
27866
+ } catch {
27867
+ return;
27868
+ }
27869
+ }
27870
+ cleanupStaleOrigin(origin, excludeGameId) {
27871
+ if (!this.deps.corsKvs) {
27872
+ return;
27873
+ }
27874
+ const corsKvs = this.deps.corsKvs;
27875
+ const db2 = this.deps.db;
27876
+ (async () => {
27877
+ const sharesOrigin = await db2.select({ id: games.id }).from(games).where(and(ne(games.id, excludeGameId), eq(games.gameType, "external"), or(eq(games.externalUrl, origin), like(games.externalUrl, `${origin}/%`)))).limit(1);
27878
+ if (sharesOrigin.length === 0) {
27879
+ await corsKvs.deleteOrigin(origin);
27880
+ } else {
27881
+ setAttribute("app.cors_kvs.delete_skipped", true);
27882
+ }
27883
+ })().catch(catchAttrs("cors_kvs.delete_origin", {}));
27884
+ }
27860
27885
  async list(caller) {
27861
27886
  const db2 = this.deps.db;
27862
27887
  const isAdmin = caller?.role === "admin";
@@ -28159,6 +28184,18 @@ var init_game_service = __esm(() => {
28159
28184
  "app.game.next_visibility": data.visibility !== undefined ? gameResponse.visibility : undefined
28160
28185
  });
28161
28186
  GameService.recordGameShape(gameResponse);
28187
+ if (this.deps.corsKvs) {
28188
+ const prevOrigin = GameService.safeOrigin(existingGame);
28189
+ const nextOrigin = GameService.safeOrigin(gameResponse);
28190
+ if (nextOrigin) {
28191
+ setAttribute("app.cors_kvs.origin", nextOrigin);
28192
+ this.deps.corsKvs.putOrigin(nextOrigin).catch(catchAttrs("cors_kvs.put_origin", {}));
28193
+ }
28194
+ if (prevOrigin && prevOrigin !== nextOrigin) {
28195
+ setAttribute("app.cors_kvs.prev_origin", prevOrigin);
28196
+ this.cleanupStaleOrigin(prevOrigin, gameResponse.id);
28197
+ }
28198
+ }
28162
28199
  return gameResponse;
28163
28200
  }
28164
28201
  async updateMetadata(gameId, data, user) {
@@ -28277,6 +28314,11 @@ var init_game_service = __esm(() => {
28277
28314
  setAttribute("app.game.api_key_cleanup_error", errorMessage(error));
28278
28315
  }
28279
28316
  }
28317
+ const corsOrigin = GameService.safeOrigin(gameToDelete);
28318
+ if (this.deps.corsKvs && corsOrigin) {
28319
+ setAttribute("app.cors_kvs.origin", corsOrigin);
28320
+ this.cleanupStaleOrigin(corsOrigin, gameId);
28321
+ }
28280
28322
  return {
28281
28323
  slug: gameToDelete.slug,
28282
28324
  displayName: gameToDelete.displayName
@@ -28408,12 +28450,13 @@ var init_logs_service = __esm(() => {
28408
28450
 
28409
28451
  // ../api-core/src/services/factory/game.ts
28410
28452
  function createGameServices(deps) {
28411
- const { db: db2, config: config2, cloudflare: cloudflare2, auth: auth2, storage, cache, alerts } = deps;
28453
+ const { db: db2, config: config2, cloudflare: cloudflare2, corsKvs, auth: auth2, storage, cache, alerts } = deps;
28412
28454
  const game2 = new GameService({
28413
28455
  db: db2,
28414
28456
  alerts,
28415
28457
  cache,
28416
28458
  cloudflare: cloudflare2,
28459
+ corsKvs,
28417
28460
  deleteApiKeyByName: auth2.deleteApiKeyByName.bind(auth2)
28418
28461
  });
28419
28462
  const gameMember = new GameMemberService({
@@ -28434,7 +28477,6 @@ function createGameServices(deps) {
28434
28477
  db: db2,
28435
28478
  uploadBucket: config2.uploadBucket,
28436
28479
  storage,
28437
- cache,
28438
28480
  validateDeveloperAccessBySlug: (user, slug) => game2.validateDeveloperAccessBySlug(user, slug),
28439
28481
  runDeploy: (slug, request, user, uploadDeps, extractZip) => deploy.deploy(slug, request, user, uploadDeps, extractZip),
28440
28482
  notifyDeploymentFailure: (slug, displayName, error, developerInfo) => deploy.notifyDeploymentFailure(slug, displayName, error, developerInfo)
@@ -29319,6 +29361,9 @@ class DomainService {
29319
29361
  addEvent("domain.cloudflare_hostname_created", {
29320
29362
  "app.domain.cloudflare_id": cfHostname.id
29321
29363
  });
29364
+ const corsOrigin = `https://${hostname}`;
29365
+ setAttribute("app.cors_kvs.origin", corsOrigin);
29366
+ this.deps.corsKvs?.putOrigin(corsOrigin).catch(catchAttrs("cors_kvs.put_origin", {}));
29322
29367
  return customHostname;
29323
29368
  }
29324
29369
  async list(slug, environment, user) {
@@ -29402,6 +29447,9 @@ class DomainService {
29402
29447
  "app.domain.status": dbHostname.status,
29403
29448
  "app.domain.ssl_status": dbHostname.sslStatus
29404
29449
  });
29450
+ const corsOrigin = `https://${hostname}`;
29451
+ setAttribute("app.cors_kvs.origin", corsOrigin);
29452
+ this.deps.corsKvs?.deleteOrigin(corsOrigin).catch(catchAttrs("cors_kvs.delete_origin", {}));
29405
29453
  }
29406
29454
  }
29407
29455
  var init_domain_service = __esm(() => {
@@ -29696,12 +29744,13 @@ var init_secrets_service = __esm(() => {
29696
29744
  });
29697
29745
 
29698
29746
  // ../edge-play/src/constants.ts
29699
- var ROUTES;
29747
+ var ASSET_ROUTE_PREFIX = "/api/assets/", ROUTES;
29700
29748
  var init_constants3 = __esm(() => {
29701
29749
  init_src();
29702
29750
  ROUTES = {
29703
29751
  INDEX: "/api",
29704
29752
  HEALTH: "/api/health",
29753
+ ASSETS: `${ASSET_ROUTE_PREFIX}*`,
29705
29754
  TIMEBACK: {
29706
29755
  END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
29707
29756
  GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
@@ -29858,7 +29907,8 @@ class SeedService {
29858
29907
  }, {
29859
29908
  bindings: { d1: [deploymentId], r2: [], kv: [] },
29860
29909
  keepAssets: false,
29861
- compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
29910
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE,
29911
+ observability: false
29862
29912
  });
29863
29913
  if (secrets && Object.keys(secrets).length > 0) {
29864
29914
  await cf.setSecrets(seedDeploymentId, prefixSecrets(secrets));
@@ -32125,44 +32175,6 @@ function validateSessionData(sessionData) {
32125
32175
  throw new ConfigurationError("sensorUrl", 'Sensor URL is required for Caliper events. Provide it in sessionData.sensorUrl (e.g., "https://hub.playcademy.net/p/your-game")');
32126
32176
  }
32127
32177
  }
32128
- function getAttemptMultiplier(attemptNumber) {
32129
- switch (attemptNumber) {
32130
- case 1: {
32131
- return 1;
32132
- }
32133
- case 2: {
32134
- return 0.5;
32135
- }
32136
- case 3: {
32137
- return 0.25;
32138
- }
32139
- default: {
32140
- return 0;
32141
- }
32142
- }
32143
- }
32144
- function getAccuracyMultiplier(accuracy) {
32145
- if (!Number.isFinite(accuracy) || accuracy < 0) {
32146
- return 0;
32147
- }
32148
- if (accuracy >= PERFECT_ACCURACY_THRESHOLD) {
32149
- return 1.25;
32150
- } else if (accuracy >= 0.8) {
32151
- return 1;
32152
- } else {
32153
- return 0;
32154
- }
32155
- }
32156
- function calculateXp(durationSeconds, accuracy, attemptNumber) {
32157
- if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
32158
- return 0;
32159
- }
32160
- const durationMinutes = durationSeconds / 60;
32161
- const baseXp = Number(durationMinutes);
32162
- const accuracyMultiplier = getAccuracyMultiplier(accuracy);
32163
- const attemptMultiplier = getAttemptMultiplier(attemptNumber);
32164
- return Math.round(baseXp * accuracyMultiplier * attemptMultiplier * 10) / 10;
32165
- }
32166
32178
 
32167
32179
  class ProgressRecorder {
32168
32180
  studentResolver;
@@ -32181,10 +32193,15 @@ class ProgressRecorder {
32181
32193
  validateProgressData(progressData);
32182
32194
  const { ids, activityId, activityName, courseName, student } = await this.resolveContext(courseId, studentIdentifier, progressData);
32183
32195
  const { id: studentId, email: studentEmail } = student;
32184
- const { score, totalQuestions, correctQuestions, xpEarned, attemptNumber } = progressData;
32196
+ const {
32197
+ score,
32198
+ totalQuestions,
32199
+ correctQuestions,
32200
+ xpEarned = 0,
32201
+ attemptNumber
32202
+ } = progressData;
32185
32203
  const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
32186
32204
  const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
32187
- const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, currentAttemptNumber);
32188
32205
  let extensions = progressData.extensions;
32189
32206
  const masteryProgress = await this.masteryTracker.checkProgress({
32190
32207
  studentId,
@@ -32215,7 +32232,7 @@ class ProgressRecorder {
32215
32232
  studentId,
32216
32233
  attemptNumber: currentAttemptNumber,
32217
32234
  score,
32218
- xp: calculatedXp,
32235
+ xp: xpEarned,
32219
32236
  scoreStatus,
32220
32237
  inProgress,
32221
32238
  appName: progressData.appName,
@@ -32254,7 +32271,7 @@ class ProgressRecorder {
32254
32271
  courseName,
32255
32272
  totalQuestions,
32256
32273
  correctQuestions,
32257
- xpEarned: calculatedXp,
32274
+ xpEarned,
32258
32275
  masteredUnits: effectiveMasteredUnits || undefined,
32259
32276
  attemptNumber: currentAttemptNumber,
32260
32277
  progressData,
@@ -32262,7 +32279,7 @@ class ProgressRecorder {
32262
32279
  runId: progressData.runId
32263
32280
  });
32264
32281
  return {
32265
- xpAwarded: calculatedXp,
32282
+ xpAwarded: xpEarned,
32266
32283
  attemptNumber: currentAttemptNumber,
32267
32284
  masteredUnitsApplied: effectiveMasteredUnits,
32268
32285
  pctCompleteApp,
@@ -32296,16 +32313,6 @@ class ProgressRecorder {
32296
32313
  }
32297
32314
  return 1;
32298
32315
  }
32299
- calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, attemptNumber) {
32300
- if (xpEarned !== undefined) {
32301
- return xpEarned;
32302
- }
32303
- if (progressData.durationSeconds && totalQuestions && correctQuestions) {
32304
- const accuracy = correctQuestions / totalQuestions;
32305
- return calculateXp(progressData.durationSeconds, accuracy, attemptNumber);
32306
- }
32307
- return 0;
32308
- }
32309
32316
  async getOrCreateLineItem(lineItemId, activityName, classId, ids) {
32310
32317
  try {
32311
32318
  const lineItem = await this.onerosterNamespace.assessmentLineItems.findOrCreate(lineItemId, {
@@ -32963,7 +32970,7 @@ var __defProp2, __export2 = (target, all) => {
32963
32970
  configurable: true,
32964
32971
  set: (newValue) => all[name3] = () => newValue
32965
32972
  });
32966
- }, __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res), TIMEBACK_API_URLS, QTI_API_URL = "https://qti.alpha-1edtech.ai/api", TIMEBACK_AUTH_URLS, CALIPER_API_URLS, ONEROSTER_ENDPOINTS, QTI_ENDPOINTS, CALIPER_ENDPOINTS, CALIPER_CONSTANTS, TIMEBACK_EVENT_TYPES, TIMEBACK_ACTIONS, TIMEBACK_TYPES, ACTIVITY_METRIC_TYPES, TIME_METRIC_TYPES, TIMEBACK_SUBJECTS, TIMEBACK_GRADE_LEVELS, TIMEBACK_GRADE_LEVEL_LABELS, CALIPER_SUBJECTS, ONEROSTER_STATUS, SCORE_STATUS, ENV_VARS, HTTP_DEFAULTS, AUTH_DEFAULTS, CACHE_DEFAULTS, CONFIG_DEFAULTS, PLAYCADEMY_DEFAULTS, RESOURCE_DEFAULTS, HTTP_STATUS, ERROR_NAMES, init_constants4, exports_verify, init_verify, TimebackError, TimebackApiError, TimebackAuthenticationError, StudentNotFoundError, ConfigurationError, ResourceNotFoundError, SUBJECT_VALUES, GRADE_VALUES, TimebackAuthError, UUID_PATTERN, storage, PERFECT_ACCURACY_THRESHOLD = 0.999999, EmailSchema, StudentSourcedIdSchema, StudentIdentifierSchema;
32973
+ }, __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res), TIMEBACK_API_URLS, QTI_API_URL = "https://qti.alpha-1edtech.ai/api", TIMEBACK_AUTH_URLS, CALIPER_API_URLS, ONEROSTER_ENDPOINTS, QTI_ENDPOINTS, CALIPER_ENDPOINTS, CALIPER_CONSTANTS, TIMEBACK_EVENT_TYPES, TIMEBACK_ACTIONS, TIMEBACK_TYPES, ACTIVITY_METRIC_TYPES, TIME_METRIC_TYPES, TIMEBACK_SUBJECTS, TIMEBACK_GRADE_LEVELS, TIMEBACK_GRADE_LEVEL_LABELS, CALIPER_SUBJECTS, ONEROSTER_STATUS, SCORE_STATUS, ENV_VARS, HTTP_DEFAULTS, AUTH_DEFAULTS, CACHE_DEFAULTS, CONFIG_DEFAULTS, PLAYCADEMY_DEFAULTS, RESOURCE_DEFAULTS, HTTP_STATUS, ERROR_NAMES, init_constants4, exports_verify, init_verify, TimebackError, TimebackApiError, TimebackAuthenticationError, StudentNotFoundError, ConfigurationError, ResourceNotFoundError, SUBJECT_VALUES, GRADE_VALUES, TimebackAuthError, UUID_PATTERN, storage, EmailSchema, StudentSourcedIdSchema, StudentIdentifierSchema;
32967
32974
  var init_dist2 = __esm(() => {
32968
32975
  init_src();
32969
32976
  init_src();
@@ -34211,13 +34218,14 @@ var init_emoji = __esm(() => {
34211
34218
  });
34212
34219
 
34213
34220
  // ../data/src/domains/game/schemas.ts
34214
- var GameEmojiSchema, GameMetadataRecordSchema, InsertGameSchema, UpdateGameSchema, InsertGameDeploymentSchema, InsertGameDeployJobSchema, UpsertGameMetadataSchema, PatchGameMetadataSchema, AddGameMemberSchema, UpdateGameMemberRoleSchema, ALLOWED_UPLOAD_EXTENSIONS, InitiateUploadSchema, AddCustomHostnameSchema, SetSecretsRequestSchema, SeedRequestSchema, SchemaInfoSchema, DatabaseResetRequestSchema, VerifyTokenSchema, KVSeedRequestSchema, DeployRequestSchema;
34221
+ var HttpUrlSchema, GameEmojiSchema, GameMetadataRecordSchema, InsertGameSchema, UpdateGameSchema, InsertGameDeploymentSchema, InsertGameDeployJobSchema, UpsertGameMetadataSchema, PatchGameMetadataSchema, AddGameMemberSchema, UpdateGameMemberRoleSchema, ALLOWED_UPLOAD_EXTENSIONS, InitiateUploadSchema, AddCustomHostnameSchema, SetSecretsRequestSchema, SeedRequestSchema, SchemaInfoSchema, DatabaseResetRequestSchema, VerifyTokenSchema, KVSeedRequestSchema, DeployRequestSchema;
34215
34222
  var init_schemas2 = __esm(() => {
34216
34223
  init_drizzle_zod();
34217
34224
  init_esm();
34218
34225
  init_src();
34219
34226
  init_emoji();
34220
34227
  init_table5();
34228
+ HttpUrlSchema = exports_external.string().url().refine((url2) => /^https?:\/\//i.test(url2), { message: "URL must use http or https" });
34221
34229
  GameEmojiSchema = exports_external.string().max(16).refine((value) => value.length === 0 || isSingleEmoji(value), {
34222
34230
  message: "Emoji must be a single emoji."
34223
34231
  });
@@ -34242,7 +34250,7 @@ var init_schemas2 = __esm(() => {
34242
34250
  gameType: exports_external.enum(gameTypeEnum.enumValues).default("hosted"),
34243
34251
  visibility: exports_external.enum(gameVisibilityEnum.enumValues).default("visible"),
34244
34252
  deploymentUrl: exports_external.string().nullable().optional(),
34245
- externalUrl: exports_external.string().url().nullable().optional()
34253
+ externalUrl: HttpUrlSchema.nullable().optional()
34246
34254
  }).omit({
34247
34255
  slug: true,
34248
34256
  version: true
@@ -34265,7 +34273,7 @@ var init_schemas2 = __esm(() => {
34265
34273
  gameType: exports_external.enum(gameTypeEnum.enumValues).optional(),
34266
34274
  visibility: exports_external.enum(gameVisibilityEnum.enumValues).optional(),
34267
34275
  deploymentUrl: exports_external.string().nullable().optional(),
34268
- externalUrl: exports_external.string().url().nullable().optional()
34276
+ externalUrl: HttpUrlSchema.nullable().optional()
34269
34277
  }).omit({
34270
34278
  id: true,
34271
34279
  slug: true,
@@ -34295,7 +34303,7 @@ var init_schemas2 = __esm(() => {
34295
34303
  metadata: GameMetadataRecordSchema.optional().default({}),
34296
34304
  gameType: exports_external.enum(gameTypeEnum.enumValues).optional().default("hosted"),
34297
34305
  visibility: exports_external.enum(gameVisibilityEnum.enumValues).optional(),
34298
- externalUrl: exports_external.string().url().optional()
34306
+ externalUrl: HttpUrlSchema.optional()
34299
34307
  }).refine((data) => {
34300
34308
  if (data.gameType === "external" && !data.externalUrl) {
34301
34309
  return false;
@@ -34494,7 +34502,7 @@ var init_schemas4 = __esm(() => {
34494
34502
  activeSeconds: exports_external.number().nonnegative(),
34495
34503
  inactiveSeconds: exports_external.number().nonnegative().optional()
34496
34504
  }).optional(),
34497
- xpEarned: exports_external.number().optional(),
34505
+ xpEarned: exports_external.number(),
34498
34506
  masteredUnits: exports_external.number().optional(),
34499
34507
  masteredUnitsAbsolute: exports_external.number().int().nonnegative().optional(),
34500
34508
  extensions: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
@@ -39206,6 +39214,7 @@ function createPlatformServices(deps) {
39206
39214
  db: db2,
39207
39215
  config: config2,
39208
39216
  cloudflare: cloudflare2,
39217
+ corsKvs,
39209
39218
  storage: storage2,
39210
39219
  r2Storage,
39211
39220
  timebackClient,
@@ -39227,7 +39236,7 @@ function createPlatformServices(deps) {
39227
39236
  });
39228
39237
  const kv = new KVService({ db: db2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
39229
39238
  const secrets = new SecretsService({ config: config2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
39230
- const domain = new DomainService({ db: db2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
39239
+ const domain = new DomainService({ db: db2, cloudflare: cloudflare2, corsKvs, validateDeveloperAccessBySlug });
39231
39240
  const database = new DatabaseService({
39232
39241
  db: db2,
39233
39242
  config: config2,
@@ -39905,7 +39914,7 @@ var init_standalone = __esm(() => {
39905
39914
 
39906
39915
  // ../api-core/src/services/factory/index.ts
39907
39916
  function createServices(ctx) {
39908
- const { db: db2, config: config2, providers, cloudflare: cloudflare2, timeback: timeback2, discord } = ctx;
39917
+ const { db: db2, config: config2, providers, cloudflare: cloudflare2, timeback: timeback2, discord, corsKvs } = ctx;
39909
39918
  const { auth: auth2, storage: storage2, r2Storage, cache } = providers;
39910
39919
  const infra2 = createInfraServices({
39911
39920
  db: db2,
@@ -39919,6 +39928,7 @@ function createServices(ctx) {
39919
39928
  db: db2,
39920
39929
  config: config2,
39921
39930
  cloudflare: cloudflare2,
39931
+ corsKvs,
39922
39932
  auth: auth2,
39923
39933
  storage: storage2,
39924
39934
  cache,
@@ -39928,6 +39938,7 @@ function createServices(ctx) {
39928
39938
  db: db2,
39929
39939
  config: config2,
39930
39940
  cloudflare: cloudflare2,
39941
+ corsKvs,
39931
39942
  storage: storage2,
39932
39943
  r2Storage,
39933
39944
  timebackClient: timeback2,
@@ -40116,16 +40127,6 @@ var init_auth_provider = __esm(() => {
40116
40127
  // src/infrastructure/api/providers/cache.provider.ts
40117
40128
  function createSandboxCacheProvider() {
40118
40129
  return {
40119
- async refreshGameOrigins() {
40120
- gameOrigins = ["http://localhost:3000", "http://localhost:5173"];
40121
- lastRefreshTime = Date.now();
40122
- },
40123
- getGameOriginState() {
40124
- return {
40125
- origins: gameOrigins,
40126
- lastRefreshTime
40127
- };
40128
- },
40129
40130
  async get(key) {
40130
40131
  const entry = cache.get(key);
40131
40132
  if (!entry) {
@@ -40150,13 +40151,10 @@ function createSandboxCacheProvider() {
40150
40151
  }
40151
40152
  function clearSandboxCache() {
40152
40153
  cache.clear();
40153
- gameOrigins = [];
40154
- lastRefreshTime = 0;
40155
40154
  }
40156
- var cache, gameOrigins, lastRefreshTime = 0;
40155
+ var cache;
40157
40156
  var init_cache_provider = __esm(() => {
40158
40157
  cache = new Map;
40159
- gameOrigins = [];
40160
40158
  });
40161
40159
 
40162
40160
  // src/infrastructure/api/providers/storage.provider.ts
@@ -49365,7 +49363,7 @@ var require_has_flag = __commonJS((exports, module2) => {
49365
49363
 
49366
49364
  // ../../node_modules/.bun/supports-color@7.2.0/node_modules/supports-color/index.js
49367
49365
  var require_supports_color = __commonJS((exports, module2) => {
49368
- var os = __require("os");
49366
+ var os2 = __require("os");
49369
49367
  var tty = __require("tty");
49370
49368
  var hasFlag = require_has_flag();
49371
49369
  var { env } = process;
@@ -49413,7 +49411,7 @@ var require_supports_color = __commonJS((exports, module2) => {
49413
49411
  return min;
49414
49412
  }
49415
49413
  if (process.platform === "win32") {
49416
- const osRelease = os.release().split(".");
49414
+ const osRelease = os2.release().split(".");
49417
49415
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
49418
49416
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
49419
49417
  }
@@ -54506,7 +54504,7 @@ __export(exports_api, {
54506
54504
  generateDrizzleJson: () => generateDrizzleJson
54507
54505
  });
54508
54506
  import process2 from "process";
54509
- import os from "os";
54507
+ import os2 from "os";
54510
54508
  import tty from "tty";
54511
54509
  import { randomUUID } from "crypto";
54512
54510
  function assembleStyles() {
@@ -54677,7 +54675,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
54677
54675
  return min2;
54678
54676
  }
54679
54677
  if (process2.platform === "win32") {
54680
- const osRelease = os.release().split(".");
54678
+ const osRelease = os2.release().split(".");
54681
54679
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
54682
54680
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
54683
54681
  }
@@ -67517,7 +67515,7 @@ Is ${source_default.bold.blue(this.base.name)} schema created or renamed from an
67517
67515
  });
67518
67516
  require_supports_colors = __commonJS2({
67519
67517
  "../node_modules/.pnpm/colors@1.4.0/node_modules/colors/lib/system/supports-colors.js"(exports, module2) {
67520
- var os2 = __require2("os");
67518
+ var os22 = __require2("os");
67521
67519
  var hasFlag2 = require_has_flag2();
67522
67520
  var env2 = process.env;
67523
67521
  var forceColor = undefined;
@@ -67555,7 +67553,7 @@ Is ${source_default.bold.blue(this.base.name)} schema created or renamed from an
67555
67553
  }
67556
67554
  var min2 = forceColor ? 1 : 0;
67557
67555
  if (process.platform === "win32") {
67558
- var osRelease = os2.release().split(".");
67556
+ var osRelease = os22.release().split(".");
67559
67557
  if (Number(process.versions.node.split(".")[0]) >= 8 && Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
67560
67558
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
67561
67559
  }
@@ -95977,28 +95975,6 @@ var init_utils11 = __esm(() => {
95977
95975
  init_validation_util();
95978
95976
  });
95979
95977
 
95980
- // ../api-core/src/controllers/admin.controller.ts
95981
- var getAllowedOrigins, admin;
95982
- var init_admin_controller = __esm(() => {
95983
- init_utils11();
95984
- getAllowedOrigins = requireAdmin(async (ctx) => {
95985
- const shouldRefresh = ctx.url.searchParams.get("refresh") === "true";
95986
- if (shouldRefresh) {
95987
- await ctx.providers.cache.refreshGameOrigins();
95988
- }
95989
- const { origins, lastRefreshTime: lastRefreshTime2 } = ctx.providers.cache.getGameOriginState();
95990
- return {
95991
- origins,
95992
- count: origins.length,
95993
- lastRefresh: lastRefreshTime2 > 0 ? new Date(lastRefreshTime2).toISOString() : null,
95994
- cacheAge: lastRefreshTime2 > 0 ? Date.now() - lastRefreshTime2 : null
95995
- };
95996
- });
95997
- admin = defineControllerNames("admin", {
95998
- getAllowedOrigins
95999
- });
96000
- });
96001
-
96002
95978
  // ../api-core/src/controllers/bucket.controller.ts
96003
95979
  var listFiles, getFile, putFile, deleteFile, initiateUpload, bucket;
96004
95980
  var init_bucket_controller = __esm(() => {
@@ -97430,7 +97406,6 @@ var init_verify_controller = __esm(() => {
97430
97406
 
97431
97407
  // ../api-core/src/controllers/index.ts
97432
97408
  var init_controllers = __esm(() => {
97433
- init_admin_controller();
97434
97409
  init_bucket_controller();
97435
97410
  init_database_controller();
97436
97411
  init_deploy_controller();
@@ -1,18 +1,3 @@
1
- /**
2
- * Sandbox Cache Provider
3
- *
4
- * In-memory cache for local development. Simple TTL-based expiration.
5
- *
6
- * @module infrastructure/api/providers/cache
7
- */
8
1
  import type { CacheProvider } from '@playcademy/api-core/providers';
9
- /**
10
- * Create a sandbox cache provider.
11
- *
12
- * Uses in-memory storage with TTL support.
13
- */
14
2
  export declare function createSandboxCacheProvider(): CacheProvider;
15
- /**
16
- * Clear all sandbox cache (useful for testing).
17
- */
18
3
  export declare function clearSandboxCache(): void;
package/dist/server.js CHANGED
@@ -978,10 +978,10 @@ var init_dist = __esm(() => {
978
978
  // ../utils/src/port.ts
979
979
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
980
980
  import { createServer } from "node:net";
981
- import { homedir } from "node:os";
981
+ import * as os from "node:os";
982
982
  import { join } from "node:path";
983
983
  function getRegistryPath() {
984
- const home = homedir();
984
+ const home = os.homedir();
985
985
  const dir = join(home, ".playcademy");
986
986
  if (!existsSync(dir)) {
987
987
  mkdirSync(dir, { recursive: true });
@@ -1077,7 +1077,7 @@ var package_default;
1077
1077
  var init_package = __esm(() => {
1078
1078
  package_default = {
1079
1079
  name: "@playcademy/sandbox",
1080
- version: "0.5.1-beta.3",
1080
+ version: "0.5.1-beta.5",
1081
1081
  description: "Local development server for Playcademy game development",
1082
1082
  type: "module",
1083
1083
  exports: {
@@ -22504,7 +22504,6 @@ class DeployJobService {
22504
22504
  "app.deploy_job.outcome": "succeeded",
22505
22505
  "app.deploy_job.run_duration_ms": DeployJobService.elapsedMsSince(new Date(runStartedAt))
22506
22506
  });
22507
- await this.deps.cache.refreshGameOrigins().catch(catchAttrs("deploy_job.cache_refresh"));
22508
22507
  await this.deleteCodeBundle(jobId);
22509
22508
  } catch (error) {
22510
22509
  clearInterval(heartbeat);
@@ -26422,7 +26421,7 @@ ${file}:${line3}:${column2}: ERROR: ${pluginText}${e.text}`;
26422
26421
  return result;
26423
26422
  }
26424
26423
  var fs2 = __require("fs");
26425
- var os = __require("os");
26424
+ var os2 = __require("os");
26426
26425
  var path = __require("path");
26427
26426
  var ESBUILD_BINARY_PATH = process.env.ESBUILD_BINARY_PATH || ESBUILD_BINARY_PATH;
26428
26427
  var isValidBinaryPath = (x) => !!x && x !== "/usr/bin/esbuild";
@@ -26464,7 +26463,7 @@ ${file}:${line3}:${column2}: ERROR: ${pluginText}${e.text}`;
26464
26463
  let pkg;
26465
26464
  let subpath;
26466
26465
  let isWASM = false;
26467
- let platformKey = `${process.platform} ${os.arch()} ${os.endianness()}`;
26466
+ let platformKey = `${process.platform} ${os2.arch()} ${os2.endianness()}`;
26468
26467
  if (platformKey in knownWindowsPackages) {
26469
26468
  pkg = knownWindowsPackages[platformKey];
26470
26469
  subpath = "esbuild.exe";
@@ -26607,7 +26606,7 @@ for your current platform.`);
26607
26606
  var crypto3 = __require("crypto");
26608
26607
  var path2 = __require("path");
26609
26608
  var fs22 = __require("fs");
26610
- var os2 = __require("os");
26609
+ var os22 = __require("os");
26611
26610
  var tty = __require("tty");
26612
26611
  var worker_threads;
26613
26612
  if (process.env.ESBUILD_WORKER_THREADS !== "0") {
@@ -26918,7 +26917,7 @@ More information: The file containing the code for esbuild's JavaScript API (${_
26918
26917
  afterClose(null);
26919
26918
  };
26920
26919
  var randomFileName = () => {
26921
- return path2.join(os2.tmpdir(), `esbuild-${crypto3.randomBytes(32).toString("hex")}`);
26920
+ return path2.join(os22.tmpdir(), `esbuild-${crypto3.randomBytes(32).toString("hex")}`);
26922
26921
  };
26923
26922
  var workerThreadService = null;
26924
26923
  var startWorkerThreadService = (worker_threads2) => {
@@ -27180,7 +27179,8 @@ class DeployService {
27180
27179
  expiresIn: null,
27181
27180
  permissions: {
27182
27181
  games: [`read:${slug}`, `write:${slug}`]
27183
- }
27182
+ },
27183
+ rateLimitEnabled: false
27184
27184
  });
27185
27185
  setAttribute("app.deploy.api_key_outcome", "created");
27186
27186
  return apiKey;
@@ -27241,9 +27241,9 @@ class DeployService {
27241
27241
  throw new ValidationError("Uploaded file is empty or not found");
27242
27242
  }
27243
27243
  setAttribute("app.deploy.asset_upload_size", frontendZip.length);
27244
- const os = await import("os");
27244
+ const os2 = await import("os");
27245
27245
  const path = await import("path");
27246
- const tempDir = path.join(os.tmpdir(), `playcademy-deploy-${gameId}-${Date.now()}`);
27246
+ const tempDir = path.join(os2.tmpdir(), `playcademy-deploy-${gameId}-${Date.now()}`);
27247
27247
  const assetsPath = path.join(tempDir, "dist");
27248
27248
  await withSpan("deploy.extract_assets", () => extractZip(frontendZip, assetsPath));
27249
27249
  uploadDeps.deleteObject(uploadToken).catch(catchAttrs("deploy.temp_cleanup"));
@@ -27856,6 +27856,31 @@ var init_game_service = __esm(() => {
27856
27856
  static changedFields(data) {
27857
27857
  return Object.entries(data).filter(([, value]) => value !== undefined).map(([key]) => key).toSorted().join(",");
27858
27858
  }
27859
+ static safeOrigin(game2) {
27860
+ if (!game2 || game2.gameType !== "external" || !game2.externalUrl) {
27861
+ return;
27862
+ }
27863
+ try {
27864
+ return new URL(game2.externalUrl).origin;
27865
+ } catch {
27866
+ return;
27867
+ }
27868
+ }
27869
+ cleanupStaleOrigin(origin, excludeGameId) {
27870
+ if (!this.deps.corsKvs) {
27871
+ return;
27872
+ }
27873
+ const corsKvs = this.deps.corsKvs;
27874
+ const db2 = this.deps.db;
27875
+ (async () => {
27876
+ const sharesOrigin = await db2.select({ id: games.id }).from(games).where(and(ne(games.id, excludeGameId), eq(games.gameType, "external"), or(eq(games.externalUrl, origin), like(games.externalUrl, `${origin}/%`)))).limit(1);
27877
+ if (sharesOrigin.length === 0) {
27878
+ await corsKvs.deleteOrigin(origin);
27879
+ } else {
27880
+ setAttribute("app.cors_kvs.delete_skipped", true);
27881
+ }
27882
+ })().catch(catchAttrs("cors_kvs.delete_origin", {}));
27883
+ }
27859
27884
  async list(caller) {
27860
27885
  const db2 = this.deps.db;
27861
27886
  const isAdmin = caller?.role === "admin";
@@ -28158,6 +28183,18 @@ var init_game_service = __esm(() => {
28158
28183
  "app.game.next_visibility": data.visibility !== undefined ? gameResponse.visibility : undefined
28159
28184
  });
28160
28185
  GameService.recordGameShape(gameResponse);
28186
+ if (this.deps.corsKvs) {
28187
+ const prevOrigin = GameService.safeOrigin(existingGame);
28188
+ const nextOrigin = GameService.safeOrigin(gameResponse);
28189
+ if (nextOrigin) {
28190
+ setAttribute("app.cors_kvs.origin", nextOrigin);
28191
+ this.deps.corsKvs.putOrigin(nextOrigin).catch(catchAttrs("cors_kvs.put_origin", {}));
28192
+ }
28193
+ if (prevOrigin && prevOrigin !== nextOrigin) {
28194
+ setAttribute("app.cors_kvs.prev_origin", prevOrigin);
28195
+ this.cleanupStaleOrigin(prevOrigin, gameResponse.id);
28196
+ }
28197
+ }
28161
28198
  return gameResponse;
28162
28199
  }
28163
28200
  async updateMetadata(gameId, data, user) {
@@ -28276,6 +28313,11 @@ var init_game_service = __esm(() => {
28276
28313
  setAttribute("app.game.api_key_cleanup_error", errorMessage(error));
28277
28314
  }
28278
28315
  }
28316
+ const corsOrigin = GameService.safeOrigin(gameToDelete);
28317
+ if (this.deps.corsKvs && corsOrigin) {
28318
+ setAttribute("app.cors_kvs.origin", corsOrigin);
28319
+ this.cleanupStaleOrigin(corsOrigin, gameId);
28320
+ }
28279
28321
  return {
28280
28322
  slug: gameToDelete.slug,
28281
28323
  displayName: gameToDelete.displayName
@@ -28407,12 +28449,13 @@ var init_logs_service = __esm(() => {
28407
28449
 
28408
28450
  // ../api-core/src/services/factory/game.ts
28409
28451
  function createGameServices(deps) {
28410
- const { db: db2, config: config2, cloudflare: cloudflare2, auth: auth2, storage, cache, alerts } = deps;
28452
+ const { db: db2, config: config2, cloudflare: cloudflare2, corsKvs, auth: auth2, storage, cache, alerts } = deps;
28411
28453
  const game2 = new GameService({
28412
28454
  db: db2,
28413
28455
  alerts,
28414
28456
  cache,
28415
28457
  cloudflare: cloudflare2,
28458
+ corsKvs,
28416
28459
  deleteApiKeyByName: auth2.deleteApiKeyByName.bind(auth2)
28417
28460
  });
28418
28461
  const gameMember = new GameMemberService({
@@ -28433,7 +28476,6 @@ function createGameServices(deps) {
28433
28476
  db: db2,
28434
28477
  uploadBucket: config2.uploadBucket,
28435
28478
  storage,
28436
- cache,
28437
28479
  validateDeveloperAccessBySlug: (user, slug) => game2.validateDeveloperAccessBySlug(user, slug),
28438
28480
  runDeploy: (slug, request, user, uploadDeps, extractZip) => deploy.deploy(slug, request, user, uploadDeps, extractZip),
28439
28481
  notifyDeploymentFailure: (slug, displayName, error, developerInfo) => deploy.notifyDeploymentFailure(slug, displayName, error, developerInfo)
@@ -29318,6 +29360,9 @@ class DomainService {
29318
29360
  addEvent("domain.cloudflare_hostname_created", {
29319
29361
  "app.domain.cloudflare_id": cfHostname.id
29320
29362
  });
29363
+ const corsOrigin = `https://${hostname}`;
29364
+ setAttribute("app.cors_kvs.origin", corsOrigin);
29365
+ this.deps.corsKvs?.putOrigin(corsOrigin).catch(catchAttrs("cors_kvs.put_origin", {}));
29321
29366
  return customHostname;
29322
29367
  }
29323
29368
  async list(slug, environment, user) {
@@ -29401,6 +29446,9 @@ class DomainService {
29401
29446
  "app.domain.status": dbHostname.status,
29402
29447
  "app.domain.ssl_status": dbHostname.sslStatus
29403
29448
  });
29449
+ const corsOrigin = `https://${hostname}`;
29450
+ setAttribute("app.cors_kvs.origin", corsOrigin);
29451
+ this.deps.corsKvs?.deleteOrigin(corsOrigin).catch(catchAttrs("cors_kvs.delete_origin", {}));
29404
29452
  }
29405
29453
  }
29406
29454
  var init_domain_service = __esm(() => {
@@ -29695,12 +29743,13 @@ var init_secrets_service = __esm(() => {
29695
29743
  });
29696
29744
 
29697
29745
  // ../edge-play/src/constants.ts
29698
- var ROUTES;
29746
+ var ASSET_ROUTE_PREFIX = "/api/assets/", ROUTES;
29699
29747
  var init_constants3 = __esm(() => {
29700
29748
  init_src();
29701
29749
  ROUTES = {
29702
29750
  INDEX: "/api",
29703
29751
  HEALTH: "/api/health",
29752
+ ASSETS: `${ASSET_ROUTE_PREFIX}*`,
29704
29753
  TIMEBACK: {
29705
29754
  END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
29706
29755
  GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
@@ -29857,7 +29906,8 @@ class SeedService {
29857
29906
  }, {
29858
29907
  bindings: { d1: [deploymentId], r2: [], kv: [] },
29859
29908
  keepAssets: false,
29860
- compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
29909
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE,
29910
+ observability: false
29861
29911
  });
29862
29912
  if (secrets && Object.keys(secrets).length > 0) {
29863
29913
  await cf.setSecrets(seedDeploymentId, prefixSecrets(secrets));
@@ -32124,44 +32174,6 @@ function validateSessionData(sessionData) {
32124
32174
  throw new ConfigurationError("sensorUrl", 'Sensor URL is required for Caliper events. Provide it in sessionData.sensorUrl (e.g., "https://hub.playcademy.net/p/your-game")');
32125
32175
  }
32126
32176
  }
32127
- function getAttemptMultiplier(attemptNumber) {
32128
- switch (attemptNumber) {
32129
- case 1: {
32130
- return 1;
32131
- }
32132
- case 2: {
32133
- return 0.5;
32134
- }
32135
- case 3: {
32136
- return 0.25;
32137
- }
32138
- default: {
32139
- return 0;
32140
- }
32141
- }
32142
- }
32143
- function getAccuracyMultiplier(accuracy) {
32144
- if (!Number.isFinite(accuracy) || accuracy < 0) {
32145
- return 0;
32146
- }
32147
- if (accuracy >= PERFECT_ACCURACY_THRESHOLD) {
32148
- return 1.25;
32149
- } else if (accuracy >= 0.8) {
32150
- return 1;
32151
- } else {
32152
- return 0;
32153
- }
32154
- }
32155
- function calculateXp(durationSeconds, accuracy, attemptNumber) {
32156
- if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
32157
- return 0;
32158
- }
32159
- const durationMinutes = durationSeconds / 60;
32160
- const baseXp = Number(durationMinutes);
32161
- const accuracyMultiplier = getAccuracyMultiplier(accuracy);
32162
- const attemptMultiplier = getAttemptMultiplier(attemptNumber);
32163
- return Math.round(baseXp * accuracyMultiplier * attemptMultiplier * 10) / 10;
32164
- }
32165
32177
 
32166
32178
  class ProgressRecorder {
32167
32179
  studentResolver;
@@ -32180,10 +32192,15 @@ class ProgressRecorder {
32180
32192
  validateProgressData(progressData);
32181
32193
  const { ids, activityId, activityName, courseName, student } = await this.resolveContext(courseId, studentIdentifier, progressData);
32182
32194
  const { id: studentId, email: studentEmail } = student;
32183
- const { score, totalQuestions, correctQuestions, xpEarned, attemptNumber } = progressData;
32195
+ const {
32196
+ score,
32197
+ totalQuestions,
32198
+ correctQuestions,
32199
+ xpEarned = 0,
32200
+ attemptNumber
32201
+ } = progressData;
32184
32202
  const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
32185
32203
  const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
32186
- const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, currentAttemptNumber);
32187
32204
  let extensions = progressData.extensions;
32188
32205
  const masteryProgress = await this.masteryTracker.checkProgress({
32189
32206
  studentId,
@@ -32214,7 +32231,7 @@ class ProgressRecorder {
32214
32231
  studentId,
32215
32232
  attemptNumber: currentAttemptNumber,
32216
32233
  score,
32217
- xp: calculatedXp,
32234
+ xp: xpEarned,
32218
32235
  scoreStatus,
32219
32236
  inProgress,
32220
32237
  appName: progressData.appName,
@@ -32253,7 +32270,7 @@ class ProgressRecorder {
32253
32270
  courseName,
32254
32271
  totalQuestions,
32255
32272
  correctQuestions,
32256
- xpEarned: calculatedXp,
32273
+ xpEarned,
32257
32274
  masteredUnits: effectiveMasteredUnits || undefined,
32258
32275
  attemptNumber: currentAttemptNumber,
32259
32276
  progressData,
@@ -32261,7 +32278,7 @@ class ProgressRecorder {
32261
32278
  runId: progressData.runId
32262
32279
  });
32263
32280
  return {
32264
- xpAwarded: calculatedXp,
32281
+ xpAwarded: xpEarned,
32265
32282
  attemptNumber: currentAttemptNumber,
32266
32283
  masteredUnitsApplied: effectiveMasteredUnits,
32267
32284
  pctCompleteApp,
@@ -32295,16 +32312,6 @@ class ProgressRecorder {
32295
32312
  }
32296
32313
  return 1;
32297
32314
  }
32298
- calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, attemptNumber) {
32299
- if (xpEarned !== undefined) {
32300
- return xpEarned;
32301
- }
32302
- if (progressData.durationSeconds && totalQuestions && correctQuestions) {
32303
- const accuracy = correctQuestions / totalQuestions;
32304
- return calculateXp(progressData.durationSeconds, accuracy, attemptNumber);
32305
- }
32306
- return 0;
32307
- }
32308
32315
  async getOrCreateLineItem(lineItemId, activityName, classId, ids) {
32309
32316
  try {
32310
32317
  const lineItem = await this.onerosterNamespace.assessmentLineItems.findOrCreate(lineItemId, {
@@ -32962,7 +32969,7 @@ var __defProp2, __export2 = (target, all) => {
32962
32969
  configurable: true,
32963
32970
  set: (newValue) => all[name3] = () => newValue
32964
32971
  });
32965
- }, __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res), TIMEBACK_API_URLS, QTI_API_URL = "https://qti.alpha-1edtech.ai/api", TIMEBACK_AUTH_URLS, CALIPER_API_URLS, ONEROSTER_ENDPOINTS, QTI_ENDPOINTS, CALIPER_ENDPOINTS, CALIPER_CONSTANTS, TIMEBACK_EVENT_TYPES, TIMEBACK_ACTIONS, TIMEBACK_TYPES, ACTIVITY_METRIC_TYPES, TIME_METRIC_TYPES, TIMEBACK_SUBJECTS, TIMEBACK_GRADE_LEVELS, TIMEBACK_GRADE_LEVEL_LABELS, CALIPER_SUBJECTS, ONEROSTER_STATUS, SCORE_STATUS, ENV_VARS, HTTP_DEFAULTS, AUTH_DEFAULTS, CACHE_DEFAULTS, CONFIG_DEFAULTS, PLAYCADEMY_DEFAULTS, RESOURCE_DEFAULTS, HTTP_STATUS, ERROR_NAMES, init_constants4, exports_verify, init_verify, TimebackError, TimebackApiError, TimebackAuthenticationError, StudentNotFoundError, ConfigurationError, ResourceNotFoundError, SUBJECT_VALUES, GRADE_VALUES, TimebackAuthError, UUID_PATTERN, storage, PERFECT_ACCURACY_THRESHOLD = 0.999999, EmailSchema, StudentSourcedIdSchema, StudentIdentifierSchema;
32972
+ }, __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res), TIMEBACK_API_URLS, QTI_API_URL = "https://qti.alpha-1edtech.ai/api", TIMEBACK_AUTH_URLS, CALIPER_API_URLS, ONEROSTER_ENDPOINTS, QTI_ENDPOINTS, CALIPER_ENDPOINTS, CALIPER_CONSTANTS, TIMEBACK_EVENT_TYPES, TIMEBACK_ACTIONS, TIMEBACK_TYPES, ACTIVITY_METRIC_TYPES, TIME_METRIC_TYPES, TIMEBACK_SUBJECTS, TIMEBACK_GRADE_LEVELS, TIMEBACK_GRADE_LEVEL_LABELS, CALIPER_SUBJECTS, ONEROSTER_STATUS, SCORE_STATUS, ENV_VARS, HTTP_DEFAULTS, AUTH_DEFAULTS, CACHE_DEFAULTS, CONFIG_DEFAULTS, PLAYCADEMY_DEFAULTS, RESOURCE_DEFAULTS, HTTP_STATUS, ERROR_NAMES, init_constants4, exports_verify, init_verify, TimebackError, TimebackApiError, TimebackAuthenticationError, StudentNotFoundError, ConfigurationError, ResourceNotFoundError, SUBJECT_VALUES, GRADE_VALUES, TimebackAuthError, UUID_PATTERN, storage, EmailSchema, StudentSourcedIdSchema, StudentIdentifierSchema;
32966
32973
  var init_dist2 = __esm(() => {
32967
32974
  init_src();
32968
32975
  init_src();
@@ -34210,13 +34217,14 @@ var init_emoji = __esm(() => {
34210
34217
  });
34211
34218
 
34212
34219
  // ../data/src/domains/game/schemas.ts
34213
- var GameEmojiSchema, GameMetadataRecordSchema, InsertGameSchema, UpdateGameSchema, InsertGameDeploymentSchema, InsertGameDeployJobSchema, UpsertGameMetadataSchema, PatchGameMetadataSchema, AddGameMemberSchema, UpdateGameMemberRoleSchema, ALLOWED_UPLOAD_EXTENSIONS, InitiateUploadSchema, AddCustomHostnameSchema, SetSecretsRequestSchema, SeedRequestSchema, SchemaInfoSchema, DatabaseResetRequestSchema, VerifyTokenSchema, KVSeedRequestSchema, DeployRequestSchema;
34220
+ var HttpUrlSchema, GameEmojiSchema, GameMetadataRecordSchema, InsertGameSchema, UpdateGameSchema, InsertGameDeploymentSchema, InsertGameDeployJobSchema, UpsertGameMetadataSchema, PatchGameMetadataSchema, AddGameMemberSchema, UpdateGameMemberRoleSchema, ALLOWED_UPLOAD_EXTENSIONS, InitiateUploadSchema, AddCustomHostnameSchema, SetSecretsRequestSchema, SeedRequestSchema, SchemaInfoSchema, DatabaseResetRequestSchema, VerifyTokenSchema, KVSeedRequestSchema, DeployRequestSchema;
34214
34221
  var init_schemas2 = __esm(() => {
34215
34222
  init_drizzle_zod();
34216
34223
  init_esm();
34217
34224
  init_src();
34218
34225
  init_emoji();
34219
34226
  init_table5();
34227
+ HttpUrlSchema = exports_external.string().url().refine((url2) => /^https?:\/\//i.test(url2), { message: "URL must use http or https" });
34220
34228
  GameEmojiSchema = exports_external.string().max(16).refine((value) => value.length === 0 || isSingleEmoji(value), {
34221
34229
  message: "Emoji must be a single emoji."
34222
34230
  });
@@ -34241,7 +34249,7 @@ var init_schemas2 = __esm(() => {
34241
34249
  gameType: exports_external.enum(gameTypeEnum.enumValues).default("hosted"),
34242
34250
  visibility: exports_external.enum(gameVisibilityEnum.enumValues).default("visible"),
34243
34251
  deploymentUrl: exports_external.string().nullable().optional(),
34244
- externalUrl: exports_external.string().url().nullable().optional()
34252
+ externalUrl: HttpUrlSchema.nullable().optional()
34245
34253
  }).omit({
34246
34254
  slug: true,
34247
34255
  version: true
@@ -34264,7 +34272,7 @@ var init_schemas2 = __esm(() => {
34264
34272
  gameType: exports_external.enum(gameTypeEnum.enumValues).optional(),
34265
34273
  visibility: exports_external.enum(gameVisibilityEnum.enumValues).optional(),
34266
34274
  deploymentUrl: exports_external.string().nullable().optional(),
34267
- externalUrl: exports_external.string().url().nullable().optional()
34275
+ externalUrl: HttpUrlSchema.nullable().optional()
34268
34276
  }).omit({
34269
34277
  id: true,
34270
34278
  slug: true,
@@ -34294,7 +34302,7 @@ var init_schemas2 = __esm(() => {
34294
34302
  metadata: GameMetadataRecordSchema.optional().default({}),
34295
34303
  gameType: exports_external.enum(gameTypeEnum.enumValues).optional().default("hosted"),
34296
34304
  visibility: exports_external.enum(gameVisibilityEnum.enumValues).optional(),
34297
- externalUrl: exports_external.string().url().optional()
34305
+ externalUrl: HttpUrlSchema.optional()
34298
34306
  }).refine((data) => {
34299
34307
  if (data.gameType === "external" && !data.externalUrl) {
34300
34308
  return false;
@@ -34493,7 +34501,7 @@ var init_schemas4 = __esm(() => {
34493
34501
  activeSeconds: exports_external.number().nonnegative(),
34494
34502
  inactiveSeconds: exports_external.number().nonnegative().optional()
34495
34503
  }).optional(),
34496
- xpEarned: exports_external.number().optional(),
34504
+ xpEarned: exports_external.number(),
34497
34505
  masteredUnits: exports_external.number().optional(),
34498
34506
  masteredUnitsAbsolute: exports_external.number().int().nonnegative().optional(),
34499
34507
  extensions: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
@@ -39205,6 +39213,7 @@ function createPlatformServices(deps) {
39205
39213
  db: db2,
39206
39214
  config: config2,
39207
39215
  cloudflare: cloudflare2,
39216
+ corsKvs,
39208
39217
  storage: storage2,
39209
39218
  r2Storage,
39210
39219
  timebackClient,
@@ -39226,7 +39235,7 @@ function createPlatformServices(deps) {
39226
39235
  });
39227
39236
  const kv = new KVService({ db: db2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
39228
39237
  const secrets = new SecretsService({ config: config2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
39229
- const domain = new DomainService({ db: db2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
39238
+ const domain = new DomainService({ db: db2, cloudflare: cloudflare2, corsKvs, validateDeveloperAccessBySlug });
39230
39239
  const database = new DatabaseService({
39231
39240
  db: db2,
39232
39241
  config: config2,
@@ -39904,7 +39913,7 @@ var init_standalone = __esm(() => {
39904
39913
 
39905
39914
  // ../api-core/src/services/factory/index.ts
39906
39915
  function createServices(ctx) {
39907
- const { db: db2, config: config2, providers, cloudflare: cloudflare2, timeback: timeback2, discord } = ctx;
39916
+ const { db: db2, config: config2, providers, cloudflare: cloudflare2, timeback: timeback2, discord, corsKvs } = ctx;
39908
39917
  const { auth: auth2, storage: storage2, r2Storage, cache } = providers;
39909
39918
  const infra2 = createInfraServices({
39910
39919
  db: db2,
@@ -39918,6 +39927,7 @@ function createServices(ctx) {
39918
39927
  db: db2,
39919
39928
  config: config2,
39920
39929
  cloudflare: cloudflare2,
39930
+ corsKvs,
39921
39931
  auth: auth2,
39922
39932
  storage: storage2,
39923
39933
  cache,
@@ -39927,6 +39937,7 @@ function createServices(ctx) {
39927
39937
  db: db2,
39928
39938
  config: config2,
39929
39939
  cloudflare: cloudflare2,
39940
+ corsKvs,
39930
39941
  storage: storage2,
39931
39942
  r2Storage,
39932
39943
  timebackClient: timeback2,
@@ -40115,16 +40126,6 @@ var init_auth_provider = __esm(() => {
40115
40126
  // src/infrastructure/api/providers/cache.provider.ts
40116
40127
  function createSandboxCacheProvider() {
40117
40128
  return {
40118
- async refreshGameOrigins() {
40119
- gameOrigins = ["http://localhost:3000", "http://localhost:5173"];
40120
- lastRefreshTime = Date.now();
40121
- },
40122
- getGameOriginState() {
40123
- return {
40124
- origins: gameOrigins,
40125
- lastRefreshTime
40126
- };
40127
- },
40128
40129
  async get(key) {
40129
40130
  const entry = cache.get(key);
40130
40131
  if (!entry) {
@@ -40149,13 +40150,10 @@ function createSandboxCacheProvider() {
40149
40150
  }
40150
40151
  function clearSandboxCache() {
40151
40152
  cache.clear();
40152
- gameOrigins = [];
40153
- lastRefreshTime = 0;
40154
40153
  }
40155
- var cache, gameOrigins, lastRefreshTime = 0;
40154
+ var cache;
40156
40155
  var init_cache_provider = __esm(() => {
40157
40156
  cache = new Map;
40158
- gameOrigins = [];
40159
40157
  });
40160
40158
 
40161
40159
  // src/infrastructure/api/providers/storage.provider.ts
@@ -49364,7 +49362,7 @@ var require_has_flag = __commonJS((exports, module2) => {
49364
49362
 
49365
49363
  // ../../node_modules/.bun/supports-color@7.2.0/node_modules/supports-color/index.js
49366
49364
  var require_supports_color = __commonJS((exports, module2) => {
49367
- var os = __require("os");
49365
+ var os2 = __require("os");
49368
49366
  var tty = __require("tty");
49369
49367
  var hasFlag = require_has_flag();
49370
49368
  var { env } = process;
@@ -49412,7 +49410,7 @@ var require_supports_color = __commonJS((exports, module2) => {
49412
49410
  return min;
49413
49411
  }
49414
49412
  if (process.platform === "win32") {
49415
- const osRelease = os.release().split(".");
49413
+ const osRelease = os2.release().split(".");
49416
49414
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
49417
49415
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
49418
49416
  }
@@ -54505,7 +54503,7 @@ __export(exports_api, {
54505
54503
  generateDrizzleJson: () => generateDrizzleJson
54506
54504
  });
54507
54505
  import process2 from "process";
54508
- import os from "os";
54506
+ import os2 from "os";
54509
54507
  import tty from "tty";
54510
54508
  import { randomUUID } from "crypto";
54511
54509
  function assembleStyles() {
@@ -54676,7 +54674,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
54676
54674
  return min2;
54677
54675
  }
54678
54676
  if (process2.platform === "win32") {
54679
- const osRelease = os.release().split(".");
54677
+ const osRelease = os2.release().split(".");
54680
54678
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
54681
54679
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
54682
54680
  }
@@ -67516,7 +67514,7 @@ Is ${source_default.bold.blue(this.base.name)} schema created or renamed from an
67516
67514
  });
67517
67515
  require_supports_colors = __commonJS2({
67518
67516
  "../node_modules/.pnpm/colors@1.4.0/node_modules/colors/lib/system/supports-colors.js"(exports, module2) {
67519
- var os2 = __require2("os");
67517
+ var os22 = __require2("os");
67520
67518
  var hasFlag2 = require_has_flag2();
67521
67519
  var env2 = process.env;
67522
67520
  var forceColor = undefined;
@@ -67554,7 +67552,7 @@ Is ${source_default.bold.blue(this.base.name)} schema created or renamed from an
67554
67552
  }
67555
67553
  var min2 = forceColor ? 1 : 0;
67556
67554
  if (process.platform === "win32") {
67557
- var osRelease = os2.release().split(".");
67555
+ var osRelease = os22.release().split(".");
67558
67556
  if (Number(process.versions.node.split(".")[0]) >= 8 && Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
67559
67557
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
67560
67558
  }
@@ -95976,28 +95974,6 @@ var init_utils11 = __esm(() => {
95976
95974
  init_validation_util();
95977
95975
  });
95978
95976
 
95979
- // ../api-core/src/controllers/admin.controller.ts
95980
- var getAllowedOrigins, admin;
95981
- var init_admin_controller = __esm(() => {
95982
- init_utils11();
95983
- getAllowedOrigins = requireAdmin(async (ctx) => {
95984
- const shouldRefresh = ctx.url.searchParams.get("refresh") === "true";
95985
- if (shouldRefresh) {
95986
- await ctx.providers.cache.refreshGameOrigins();
95987
- }
95988
- const { origins, lastRefreshTime: lastRefreshTime2 } = ctx.providers.cache.getGameOriginState();
95989
- return {
95990
- origins,
95991
- count: origins.length,
95992
- lastRefresh: lastRefreshTime2 > 0 ? new Date(lastRefreshTime2).toISOString() : null,
95993
- cacheAge: lastRefreshTime2 > 0 ? Date.now() - lastRefreshTime2 : null
95994
- };
95995
- });
95996
- admin = defineControllerNames("admin", {
95997
- getAllowedOrigins
95998
- });
95999
- });
96000
-
96001
95977
  // ../api-core/src/controllers/bucket.controller.ts
96002
95978
  var listFiles, getFile, putFile, deleteFile, initiateUpload, bucket;
96003
95979
  var init_bucket_controller = __esm(() => {
@@ -97429,7 +97405,6 @@ var init_verify_controller = __esm(() => {
97429
97405
 
97430
97406
  // ../api-core/src/controllers/index.ts
97431
97407
  var init_controllers = __esm(() => {
97432
- init_admin_controller();
97433
97408
  init_bucket_controller();
97434
97409
  init_database_controller();
97435
97410
  init_deploy_controller();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.5.1-beta.3",
3
+ "version": "0.5.1-beta.5",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {