@playcademy/sandbox 0.2.3 → 0.3.1

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/server.js CHANGED
@@ -6388,7 +6388,7 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
6388
6388
  resolve2(data);
6389
6389
  });
6390
6390
  });
6391
- var readFileSync = (fp) => {
6391
+ var readFileSync2 = (fp) => {
6392
6392
  return _fs.default.readFileSync(fp, "utf8");
6393
6393
  };
6394
6394
  var pathExists = (fp) => new Promise((resolve2) => {
@@ -6619,7 +6619,7 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
6619
6619
  data: this.packageJsonCache.get(filepath)[options.packageKey]
6620
6620
  };
6621
6621
  }
6622
- const data = this.options.parseJSON(readFileSync(filepath));
6622
+ const data = this.options.parseJSON(readFileSync2(filepath));
6623
6623
  return {
6624
6624
  path: filepath,
6625
6625
  data
@@ -6627,7 +6627,7 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
6627
6627
  }
6628
6628
  return {
6629
6629
  path: filepath,
6630
- data: readFileSync(filepath)
6630
+ data: readFileSync2(filepath)
6631
6631
  };
6632
6632
  }
6633
6633
  return {};
@@ -8226,19 +8226,19 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
8226
8226
  return walkForTsConfig(parentDirectory, readdirSync);
8227
8227
  }
8228
8228
  exports2.walkForTsConfig = walkForTsConfig;
8229
- function loadTsconfig(configFilePath, existsSync2, readFileSync) {
8230
- if (existsSync2 === undefined) {
8231
- existsSync2 = fs3.existsSync;
8229
+ function loadTsconfig(configFilePath, existsSync3, readFileSync2) {
8230
+ if (existsSync3 === undefined) {
8231
+ existsSync3 = fs3.existsSync;
8232
8232
  }
8233
- if (readFileSync === undefined) {
8234
- readFileSync = function(filename) {
8233
+ if (readFileSync2 === undefined) {
8234
+ readFileSync2 = function(filename) {
8235
8235
  return fs3.readFileSync(filename, "utf8");
8236
8236
  };
8237
8237
  }
8238
- if (!existsSync2(configFilePath)) {
8238
+ if (!existsSync3(configFilePath)) {
8239
8239
  return;
8240
8240
  }
8241
- var configString = readFileSync(configFilePath);
8241
+ var configString = readFileSync2(configFilePath);
8242
8242
  var cleanedJson = StripBom(configString);
8243
8243
  var config2;
8244
8244
  try {
@@ -8251,27 +8251,27 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
8251
8251
  var base = undefined;
8252
8252
  if (Array.isArray(extendedConfig)) {
8253
8253
  base = extendedConfig.reduce(function(currBase, extendedConfigElement) {
8254
- return mergeTsconfigs(currBase, loadTsconfigFromExtends(configFilePath, extendedConfigElement, existsSync2, readFileSync));
8254
+ return mergeTsconfigs(currBase, loadTsconfigFromExtends(configFilePath, extendedConfigElement, existsSync3, readFileSync2));
8255
8255
  }, {});
8256
8256
  } else {
8257
- base = loadTsconfigFromExtends(configFilePath, extendedConfig, existsSync2, readFileSync);
8257
+ base = loadTsconfigFromExtends(configFilePath, extendedConfig, existsSync3, readFileSync2);
8258
8258
  }
8259
8259
  return mergeTsconfigs(base, config2);
8260
8260
  }
8261
8261
  return config2;
8262
8262
  }
8263
8263
  exports2.loadTsconfig = loadTsconfig;
8264
- function loadTsconfigFromExtends(configFilePath, extendedConfigValue, existsSync2, readFileSync) {
8264
+ function loadTsconfigFromExtends(configFilePath, extendedConfigValue, existsSync3, readFileSync2) {
8265
8265
  var _a;
8266
8266
  if (typeof extendedConfigValue === "string" && extendedConfigValue.indexOf(".json") === -1) {
8267
8267
  extendedConfigValue += ".json";
8268
8268
  }
8269
8269
  var currentDir = path.dirname(configFilePath);
8270
8270
  var extendedConfigPath = path.join(currentDir, extendedConfigValue);
8271
- if (extendedConfigValue.indexOf("/") !== -1 && extendedConfigValue.indexOf(".") !== -1 && !existsSync2(extendedConfigPath)) {
8271
+ if (extendedConfigValue.indexOf("/") !== -1 && extendedConfigValue.indexOf(".") !== -1 && !existsSync3(extendedConfigPath)) {
8272
8272
  extendedConfigPath = path.join(currentDir, "node_modules", extendedConfigValue);
8273
8273
  }
8274
- var config2 = loadTsconfig(extendedConfigPath, existsSync2, readFileSync) || {};
8274
+ var config2 = loadTsconfig(extendedConfigPath, existsSync3, readFileSync2) || {};
8275
8275
  if ((_a = config2.compilerOptions) === null || _a === undefined ? undefined : _a.baseUrl) {
8276
8276
  var extendsDir = path.dirname(extendedConfigValue);
8277
8277
  config2.compilerOptions.baseUrl = path.join(extendsDir, config2.compilerOptions.baseUrl);
@@ -32321,7 +32321,7 @@ globstar while`, file, fr, pattern, pr2, swallowee);
32321
32321
  return new SQL2([new StringChunk2(str)]);
32322
32322
  }
32323
32323
  sql22.raw = raw2;
32324
- function join3(chunks, separator) {
32324
+ function join4(chunks, separator) {
32325
32325
  const result = [];
32326
32326
  for (const [i3, chunk] of chunks.entries()) {
32327
32327
  if (i3 > 0 && separator !== undefined) {
@@ -32331,7 +32331,7 @@ globstar while`, file, fr, pattern, pr2, swallowee);
32331
32331
  }
32332
32332
  return new SQL2(result);
32333
32333
  }
32334
- sql22.join = join3;
32334
+ sql22.join = join4;
32335
32335
  function identifier(value) {
32336
32336
  return new Name2(value);
32337
32337
  }
@@ -35161,7 +35161,7 @@ params: ${params}`);
35161
35161
  const tableName = getTableLikeName2(table62);
35162
35162
  for (const item of extractUsedTable(table62))
35163
35163
  this.usedTables.add(item);
35164
- if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
35164
+ if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
35165
35165
  throw new Error(`Alias "${tableName}" is already used in this query`);
35166
35166
  }
35167
35167
  if (!this.isPartialSelect) {
@@ -36019,7 +36019,7 @@ params: ${params}`);
36019
36019
  createJoin(joinType) {
36020
36020
  return (table62, on2) => {
36021
36021
  const tableName = getTableLikeName2(table62);
36022
- if (typeof tableName === "string" && this.config.joins.some((join3) => join3.alias === tableName)) {
36022
+ if (typeof tableName === "string" && this.config.joins.some((join4) => join4.alias === tableName)) {
36023
36023
  throw new Error(`Alias "${tableName}" is already used in this query`);
36024
36024
  }
36025
36025
  if (typeof on2 === "function") {
@@ -36065,10 +36065,10 @@ params: ${params}`);
36065
36065
  const fromFields = this.getTableLikeFields(this.config.from);
36066
36066
  fields[tableName] = fromFields;
36067
36067
  }
36068
- for (const join3 of this.config.joins) {
36069
- const tableName2 = getTableLikeName2(join3.table);
36070
- if (typeof tableName2 === "string" && !is2(join3.table, SQL2)) {
36071
- const fromFields = this.getTableLikeFields(join3.table);
36068
+ for (const join4 of this.config.joins) {
36069
+ const tableName2 = getTableLikeName2(join4.table);
36070
+ if (typeof tableName2 === "string" && !is2(join4.table, SQL2)) {
36071
+ const fromFields = this.getTableLikeFields(join4.table);
36072
36072
  fields[tableName2] = fromFields;
36073
36073
  }
36074
36074
  }
@@ -39643,7 +39643,7 @@ ORDER BY
39643
39643
  const tableName = getTableLikeName2(table62);
39644
39644
  for (const item of extractUsedTable2(table62))
39645
39645
  this.usedTables.add(item);
39646
- if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
39646
+ if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
39647
39647
  throw new Error(`Alias "${tableName}" is already used in this query`);
39648
39648
  }
39649
39649
  if (!this.isPartialSelect) {
@@ -40084,7 +40084,7 @@ ORDER BY
40084
40084
  createJoin(joinType) {
40085
40085
  return (table62, on2) => {
40086
40086
  const tableName = getTableLikeName2(table62);
40087
- if (typeof tableName === "string" && this.config.joins.some((join3) => join3.alias === tableName)) {
40087
+ if (typeof tableName === "string" && this.config.joins.some((join4) => join4.alias === tableName)) {
40088
40088
  throw new Error(`Alias "${tableName}" is already used in this query`);
40089
40089
  }
40090
40090
  if (typeof on2 === "function") {
@@ -43691,7 +43691,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
43691
43691
  const tableName = getTableLikeName2(table62);
43692
43692
  for (const item of extractUsedTable3(table62))
43693
43693
  this.usedTables.add(item);
43694
- if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
43694
+ if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
43695
43695
  throw new Error(`Alias "${tableName}" is already used in this query`);
43696
43696
  }
43697
43697
  if (!this.isPartialSelect) {
@@ -47733,7 +47733,7 @@ AND
47733
47733
  const tableName = getTableLikeName2(table62);
47734
47734
  for (const item of extractUsedTable4(table62))
47735
47735
  this.usedTables.add(item);
47736
- if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
47736
+ if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
47737
47737
  throw new Error(`Alias "${tableName}" is already used in this query`);
47738
47738
  }
47739
47739
  if (!this.isPartialSelect) {
@@ -133161,7 +133161,7 @@ function hasTimebackCredentials() {
133161
133161
  return false;
133162
133162
  }
133163
133163
  function hasTimebackFullConfig() {
133164
- return hasTimebackCredentials() && !!(config.timeback.courseId && config.timeback.studentId);
133164
+ return hasTimebackCredentials() && !!(config.timeback.courseId && config.timeback.timebackId);
133165
133165
  }
133166
133166
  function requireTimebackCredentials() {
133167
133167
  if (hasTimebackCredentials())
@@ -133215,10 +133215,35 @@ function configureTimeback(options) {
133215
133215
  config.timeback.courseId = options.courseId;
133216
133216
  process.env.SANDBOX_TIMEBACK_COURSE_ID = options.courseId;
133217
133217
  }
133218
- if (options.studentId) {
133219
- config.timeback.studentId = options.studentId;
133220
- process.env.SANDBOX_TIMEBACK_STUDENT_ID = options.studentId;
133218
+ if (options.timebackId) {
133219
+ config.timeback.timebackId = options.timebackId;
133220
+ process.env.SANDBOX_TIMEBACK_STUDENT_ID = options.timebackId;
133221
+ const isMockMode = options.timebackId === "mock";
133222
+ process.env.SANDBOX_TIMEBACK_MOCK_MODE = isMockMode ? "true" : "false";
133221
133223
  }
133224
+ if (options.organization) {
133225
+ config.timeback.organization = options.organization;
133226
+ process.env.SANDBOX_TIMEBACK_ORG_ID = options.organization.id;
133227
+ if (options.organization.name) {
133228
+ process.env.SANDBOX_TIMEBACK_ORG_NAME = options.organization.name;
133229
+ }
133230
+ if (options.organization.type) {
133231
+ process.env.SANDBOX_TIMEBACK_ORG_TYPE = options.organization.type;
133232
+ }
133233
+ }
133234
+ if (options.role) {
133235
+ config.timeback.role = options.role;
133236
+ process.env.SANDBOX_TIMEBACK_ROLE = options.role;
133237
+ }
133238
+ }
133239
+ function getTimebackDisplayMode() {
133240
+ const { timebackId, mode } = config.timeback;
133241
+ if (!timebackId)
133242
+ return null;
133243
+ if (timebackId === "mock" || !hasTimebackCredentials()) {
133244
+ return "mock";
133245
+ }
133246
+ return mode;
133222
133247
  }
133223
133248
 
133224
133249
  // src/config/mutators.ts
@@ -133242,13 +133267,203 @@ var config = {
133242
133267
  clientSecret: process.env.TIMEBACK_API_CLIENT_SECRET,
133243
133268
  authUrl: process.env.TIMEBACK_API_AUTH_URL,
133244
133269
  courseId: process.env.SANDBOX_TIMEBACK_COURSE_ID,
133245
- studentId: process.env.SANDBOX_TIMEBACK_STUDENT_ID
133270
+ timebackId: process.env.SANDBOX_TIMEBACK_STUDENT_ID
133246
133271
  }
133247
133272
  };
133248
133273
  process.env.BETTER_AUTH_SECRET = config.auth.betterAuthSecret;
133249
133274
  process.env.GAME_JWT_SECRET = config.auth.gameJwtSecret;
133250
133275
  process.env.PUBLIC_IS_LOCAL = "true";
133251
133276
 
133277
+ // src/constants/demo-users.ts
133278
+ var now = new Date;
133279
+ var DEMO_USER_IDS = {
133280
+ player: "00000000-0000-0000-0000-000000000001",
133281
+ developer: "00000000-0000-0000-0000-000000000002",
133282
+ admin: "00000000-0000-0000-0000-000000000003",
133283
+ pendingDeveloper: "00000000-0000-0000-0000-000000000004",
133284
+ unverifiedPlayer: "00000000-0000-0000-0000-000000000005"
133285
+ };
133286
+ var DEMO_USERS = {
133287
+ admin: {
133288
+ id: DEMO_USER_IDS.admin,
133289
+ name: "Admin User",
133290
+ username: "admin_user",
133291
+ email: "admin@playcademy.com",
133292
+ emailVerified: true,
133293
+ image: null,
133294
+ role: "admin",
133295
+ developerStatus: "approved",
133296
+ createdAt: now,
133297
+ updatedAt: now
133298
+ },
133299
+ player: {
133300
+ id: DEMO_USER_IDS.player,
133301
+ name: "Player User",
133302
+ username: "player_user",
133303
+ email: "player@playcademy.com",
133304
+ emailVerified: true,
133305
+ image: null,
133306
+ role: "player",
133307
+ developerStatus: "none",
133308
+ createdAt: now,
133309
+ updatedAt: now
133310
+ },
133311
+ developer: {
133312
+ id: DEMO_USER_IDS.developer,
133313
+ name: "Developer User",
133314
+ username: "developer_user",
133315
+ email: "developer@playcademy.com",
133316
+ emailVerified: true,
133317
+ image: null,
133318
+ role: "developer",
133319
+ developerStatus: "approved",
133320
+ createdAt: now,
133321
+ updatedAt: now
133322
+ },
133323
+ pendingDeveloper: {
133324
+ id: DEMO_USER_IDS.pendingDeveloper,
133325
+ name: "Pending Developer",
133326
+ username: "pending_dev",
133327
+ email: "pending@playcademy.com",
133328
+ emailVerified: true,
133329
+ image: null,
133330
+ role: "developer",
133331
+ developerStatus: "pending",
133332
+ createdAt: now,
133333
+ updatedAt: now
133334
+ },
133335
+ unverifiedPlayer: {
133336
+ id: DEMO_USER_IDS.unverifiedPlayer,
133337
+ name: "Unverified Player",
133338
+ username: "unverified_player",
133339
+ email: "unverified@playcademy.com",
133340
+ emailVerified: false,
133341
+ image: null,
133342
+ role: "player",
133343
+ developerStatus: "none",
133344
+ createdAt: now,
133345
+ updatedAt: now
133346
+ }
133347
+ };
133348
+ var DEMO_USER = DEMO_USERS.player;
133349
+ // src/constants/demo-tokens.ts
133350
+ var DEMO_TOKENS = {
133351
+ "sandbox-demo-token": DEMO_USERS.player,
133352
+ "sandbox-admin-token": DEMO_USERS.admin,
133353
+ "sandbox-player-token": DEMO_USERS.player,
133354
+ "sandbox-developer-token": DEMO_USERS.developer,
133355
+ "sandbox-pending-dev-token": DEMO_USERS.pendingDeveloper,
133356
+ "sandbox-unverified-token": DEMO_USERS.unverifiedPlayer,
133357
+ "mock-game-token-for-local-dev": DEMO_USERS.player
133358
+ };
133359
+ var DEMO_TOKEN = "sandbox-demo-token";
133360
+ var MOCK_GAME_ID = "mock-game-id-from-template";
133361
+ // src/constants/demo-items.ts
133362
+ var DEMO_ITEM_IDS = {
133363
+ playcademyCredits: "10000000-0000-0000-0000-000000000001",
133364
+ foundingMemberBadge: "10000000-0000-0000-0000-000000000002",
133365
+ earlyAdopterBadge: "10000000-0000-0000-0000-000000000003",
133366
+ firstGameBadge: "10000000-0000-0000-0000-000000000004",
133367
+ commonSword: "10000000-0000-0000-0000-000000000005",
133368
+ smallHealthPotion: "10000000-0000-0000-0000-000000000006",
133369
+ smallBackpack: "10000000-0000-0000-0000-000000000007"
133370
+ };
133371
+ var PLAYCADEMY_CREDITS_ID = DEMO_ITEM_IDS.playcademyCredits;
133372
+ var SAMPLE_ITEMS = [
133373
+ {
133374
+ id: PLAYCADEMY_CREDITS_ID,
133375
+ slug: "PLAYCADEMY_CREDITS",
133376
+ gameId: null,
133377
+ displayName: "PLAYCADEMY credits",
133378
+ description: "The main currency used across PLAYCADEMY.",
133379
+ type: "currency",
133380
+ isPlaceable: false,
133381
+ imageUrl: "http://playcademy-sandbox.local/playcademy-credit.png",
133382
+ metadata: {
133383
+ rarity: "common"
133384
+ }
133385
+ },
133386
+ {
133387
+ id: DEMO_ITEM_IDS.foundingMemberBadge,
133388
+ slug: "FOUNDING_MEMBER_BADGE",
133389
+ gameId: null,
133390
+ displayName: "Founding Member Badge",
133391
+ description: "Reserved for founding core team of the PLAYCADEMY platform.",
133392
+ type: "badge",
133393
+ isPlaceable: false,
133394
+ imageUrl: null,
133395
+ metadata: {
133396
+ rarity: "legendary"
133397
+ }
133398
+ },
133399
+ {
133400
+ id: DEMO_ITEM_IDS.earlyAdopterBadge,
133401
+ slug: "EARLY_ADOPTER_BADGE",
133402
+ gameId: null,
133403
+ displayName: "Early Adopter Badge",
133404
+ description: "Awarded to users who joined during the beta phase.",
133405
+ type: "badge",
133406
+ isPlaceable: false,
133407
+ imageUrl: null,
133408
+ metadata: {
133409
+ rarity: "epic"
133410
+ }
133411
+ },
133412
+ {
133413
+ id: DEMO_ITEM_IDS.firstGameBadge,
133414
+ slug: "FIRST_GAME_BADGE",
133415
+ gameId: null,
133416
+ displayName: "First Game Played",
133417
+ description: "Awarded for playing your first game in the Playcademy platform.",
133418
+ type: "badge",
133419
+ isPlaceable: false,
133420
+ imageUrl: "http://playcademy-sandbox.local/first-game-badge.png",
133421
+ metadata: {
133422
+ rarity: "uncommon"
133423
+ }
133424
+ },
133425
+ {
133426
+ id: DEMO_ITEM_IDS.commonSword,
133427
+ slug: "COMMON_SWORD",
133428
+ gameId: null,
133429
+ displayName: "Common Sword",
133430
+ description: "A basic sword, good for beginners.",
133431
+ type: "unlock",
133432
+ isPlaceable: false,
133433
+ imageUrl: "http://playcademy-sandbox.local/common-sword.png",
133434
+ metadata: undefined
133435
+ },
133436
+ {
133437
+ id: DEMO_ITEM_IDS.smallHealthPotion,
133438
+ slug: "SMALL_HEALTH_POTION",
133439
+ gameId: null,
133440
+ displayName: "Small Health Potion",
133441
+ description: "Restores a small amount of health.",
133442
+ type: "other",
133443
+ isPlaceable: false,
133444
+ imageUrl: "http://playcademy-sandbox.local/small-health-potion.png",
133445
+ metadata: undefined
133446
+ },
133447
+ {
133448
+ id: DEMO_ITEM_IDS.smallBackpack,
133449
+ slug: "SMALL_BACKPACK",
133450
+ gameId: null,
133451
+ displayName: "Small Backpack",
133452
+ description: "Increases your inventory capacity by 5 slots.",
133453
+ type: "upgrade",
133454
+ isPlaceable: false,
133455
+ imageUrl: "http://playcademy-sandbox.local/small-backpack.png",
133456
+ metadata: undefined
133457
+ }
133458
+ ];
133459
+ var SAMPLE_INVENTORY = [
133460
+ {
133461
+ id: "20000000-0000-0000-0000-000000000001",
133462
+ userId: DEMO_USER.id,
133463
+ itemId: PLAYCADEMY_CREDITS_ID,
133464
+ quantity: 1000
133465
+ }
133466
+ ];
133252
133467
  // ../../node_modules/@hono/node-server/dist/index.mjs
133253
133468
  import { createServer as createServerHTTP } from "http";
133254
133469
  import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
@@ -133790,10 +134005,106 @@ var serve = (options, listeningListener) => {
133790
134005
  });
133791
134006
  return server;
133792
134007
  };
134008
+
134009
+ // ../utils/src/port.ts
134010
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
134011
+ import { createServer } from "node:net";
134012
+ import { homedir } from "node:os";
134013
+ import { join } from "node:path";
134014
+ function getRegistryPath() {
134015
+ const home = homedir();
134016
+ const dir = join(home, ".playcademy");
134017
+ if (!existsSync(dir)) {
134018
+ mkdirSync(dir, { recursive: true });
134019
+ }
134020
+ return join(dir, ".proc");
134021
+ }
134022
+ function readRegistry() {
134023
+ const registryPath = getRegistryPath();
134024
+ if (!existsSync(registryPath)) {
134025
+ return {};
134026
+ }
134027
+ try {
134028
+ const content = readFileSync(registryPath, "utf-8");
134029
+ return JSON.parse(content);
134030
+ } catch {
134031
+ return {};
134032
+ }
134033
+ }
134034
+ function writeRegistry(registry) {
134035
+ const registryPath = getRegistryPath();
134036
+ writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf-8");
134037
+ }
134038
+ function getServerKey(type, port) {
134039
+ return `${type}-${port}`;
134040
+ }
134041
+ function writeServerInfo(type, info2) {
134042
+ const registry = readRegistry();
134043
+ const key = getServerKey(type, info2.port);
134044
+ registry[key] = info2;
134045
+ writeRegistry(registry);
134046
+ }
134047
+ function cleanupServerInfo(type, projectRoot, pid) {
134048
+ const registry = readRegistry();
134049
+ const keysToRemove = [];
134050
+ for (const [key, info2] of Object.entries(registry)) {
134051
+ if (key.startsWith(`${type}-`)) {
134052
+ let matches = true;
134053
+ if (projectRoot && info2.projectRoot !== projectRoot) {
134054
+ matches = false;
134055
+ }
134056
+ if (pid !== undefined && info2.pid !== pid) {
134057
+ matches = false;
134058
+ }
134059
+ if (matches) {
134060
+ keysToRemove.push(key);
134061
+ }
134062
+ }
134063
+ }
134064
+ for (const key of keysToRemove) {
134065
+ delete registry[key];
134066
+ }
134067
+ if (keysToRemove.length > 0) {
134068
+ writeRegistry(registry);
134069
+ }
134070
+ }
134071
+ async function isPortInUse(port) {
134072
+ return new Promise((resolve) => {
134073
+ const server = createServer();
134074
+ server.once("error", () => {
134075
+ resolve(true);
134076
+ });
134077
+ server.once("listening", () => {
134078
+ server.close();
134079
+ resolve(false);
134080
+ });
134081
+ server.listen(port);
134082
+ });
134083
+ }
134084
+ async function waitForPort(port, timeoutMs = 5000) {
134085
+ const start2 = Date.now();
134086
+ while (await isPortInUse(port)) {
134087
+ if (Date.now() - start2 > timeoutMs) {
134088
+ throw new Error(`Port ${port} is already in use.
134089
+ ` + `Stop the other server or specify a different port with --port <number>.`);
134090
+ }
134091
+ await new Promise((resolve) => setTimeout(resolve, 100));
134092
+ }
134093
+ }
134094
+ async function requirePortAvailable(port, timeoutMs = 100) {
134095
+ const start2 = Date.now();
134096
+ while (await isPortInUse(port)) {
134097
+ if (Date.now() - start2 > timeoutMs) {
134098
+ throw new Error(`Port ${port} is already in use.
134099
+ ` + `Stop the other server or specify a different port with --port <number>.`);
134100
+ }
134101
+ await new Promise((resolve) => setTimeout(resolve, 50));
134102
+ }
134103
+ }
133793
134104
  // package.json
133794
134105
  var package_default = {
133795
134106
  name: "@playcademy/sandbox",
133796
- version: "0.2.3",
134107
+ version: "0.3.1",
133797
134108
  description: "Local development server for Playcademy game development",
133798
134109
  type: "module",
133799
134110
  exports: {
@@ -133808,6 +134119,10 @@ var package_default = {
133808
134119
  "./config": {
133809
134120
  import: "./dist/config.js",
133810
134121
  types: "./dist/config.d.ts"
134122
+ },
134123
+ "./constants": {
134124
+ import: "./dist/constants.js",
134125
+ types: "./dist/constants.d.ts"
133811
134126
  }
133812
134127
  },
133813
134128
  bin: {
@@ -136303,7 +136618,7 @@ function sql(strings, ...params) {
136303
136618
  return new SQL([new StringChunk(str)]);
136304
136619
  }
136305
136620
  sql2.raw = raw2;
136306
- function join(chunks, separator) {
136621
+ function join2(chunks, separator) {
136307
136622
  const result = [];
136308
136623
  for (const [i2, chunk] of chunks.entries()) {
136309
136624
  if (i2 > 0 && separator !== undefined) {
@@ -136313,7 +136628,7 @@ function sql(strings, ...params) {
136313
136628
  }
136314
136629
  return new SQL(result);
136315
136630
  }
136316
- sql2.join = join;
136631
+ sql2.join = join2;
136317
136632
  function identifier(value) {
136318
136633
  return new Name(value);
136319
136634
  }
@@ -139290,7 +139605,7 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
139290
139605
  return (table, on) => {
139291
139606
  const baseTableName = this.tableName;
139292
139607
  const tableName = getTableLikeName(table);
139293
- if (typeof tableName === "string" && this.config.joins?.some((join) => join.alias === tableName)) {
139608
+ if (typeof tableName === "string" && this.config.joins?.some((join2) => join2.alias === tableName)) {
139294
139609
  throw new Error(`Alias "${tableName}" is already used in this query`);
139295
139610
  }
139296
139611
  if (!this.isPartialSelect) {
@@ -139799,7 +140114,7 @@ class PgUpdateBase extends QueryPromise {
139799
140114
  createJoin(joinType) {
139800
140115
  return (table, on) => {
139801
140116
  const tableName = getTableLikeName(table);
139802
- if (typeof tableName === "string" && this.config.joins.some((join) => join.alias === tableName)) {
140117
+ if (typeof tableName === "string" && this.config.joins.some((join2) => join2.alias === tableName)) {
139803
140118
  throw new Error(`Alias "${tableName}" is already used in this query`);
139804
140119
  }
139805
140120
  if (typeof on === "function") {
@@ -139849,10 +140164,10 @@ class PgUpdateBase extends QueryPromise {
139849
140164
  const fromFields = this.getTableLikeFields(this.config.from);
139850
140165
  fields[tableName] = fromFields;
139851
140166
  }
139852
- for (const join of this.config.joins) {
139853
- const tableName2 = getTableLikeName(join.table);
139854
- if (typeof tableName2 === "string" && !is(join.table, SQL)) {
139855
- const fromFields = this.getTableLikeFields(join.table);
140167
+ for (const join2 of this.config.joins) {
140168
+ const tableName2 = getTableLikeName(join2.table);
140169
+ if (typeof tableName2 === "string" && !is(join2.table, SQL)) {
140170
+ const fromFields = this.getTableLikeFields(join2.table);
139856
140171
  fields[tableName2] = fromFields;
139857
140172
  }
139858
140173
  }
@@ -140995,195 +141310,6 @@ var notificationsRelations = relations(notifications, ({ one }) => ({
140995
141310
  references: [users.id]
140996
141311
  })
140997
141312
  }));
140998
- // src/constants/demo-users.ts
140999
- var now = new Date;
141000
- var DEMO_USER_IDS = {
141001
- admin: "00000000-0000-0000-0000-000000000001",
141002
- player: "00000000-0000-0000-0000-000000000002",
141003
- developer: "00000000-0000-0000-0000-000000000003",
141004
- pendingDeveloper: "00000000-0000-0000-0000-000000000004",
141005
- unverifiedPlayer: "00000000-0000-0000-0000-000000000005"
141006
- };
141007
- var DEMO_USERS = {
141008
- admin: {
141009
- id: DEMO_USER_IDS.admin,
141010
- name: "Admin User",
141011
- username: "admin_user",
141012
- email: "admin@playcademy.com",
141013
- emailVerified: true,
141014
- image: null,
141015
- role: "admin",
141016
- developerStatus: "approved",
141017
- createdAt: now,
141018
- updatedAt: now
141019
- },
141020
- player: {
141021
- id: DEMO_USER_IDS.player,
141022
- name: "Player User",
141023
- username: "player_user",
141024
- email: "player@playcademy.com",
141025
- emailVerified: true,
141026
- image: null,
141027
- role: "player",
141028
- developerStatus: "none",
141029
- createdAt: now,
141030
- updatedAt: now
141031
- },
141032
- developer: {
141033
- id: DEMO_USER_IDS.developer,
141034
- name: "Developer User",
141035
- username: "developer_user",
141036
- email: "developer@playcademy.com",
141037
- emailVerified: true,
141038
- image: null,
141039
- role: "developer",
141040
- developerStatus: "approved",
141041
- createdAt: now,
141042
- updatedAt: now
141043
- },
141044
- pendingDeveloper: {
141045
- id: DEMO_USER_IDS.pendingDeveloper,
141046
- name: "Pending Developer",
141047
- username: "pending_dev",
141048
- email: "pending@playcademy.com",
141049
- emailVerified: true,
141050
- image: null,
141051
- role: "developer",
141052
- developerStatus: "pending",
141053
- createdAt: now,
141054
- updatedAt: now
141055
- },
141056
- unverifiedPlayer: {
141057
- id: DEMO_USER_IDS.unverifiedPlayer,
141058
- name: "Unverified Player",
141059
- username: "unverified_player",
141060
- email: "unverified@playcademy.com",
141061
- emailVerified: false,
141062
- image: null,
141063
- role: "player",
141064
- developerStatus: "none",
141065
- createdAt: now,
141066
- updatedAt: now
141067
- }
141068
- };
141069
- var DEMO_USER = DEMO_USERS.admin;
141070
- // src/constants/demo-tokens.ts
141071
- var DEMO_TOKENS = {
141072
- "sandbox-demo-token": DEMO_USERS.admin,
141073
- "sandbox-admin-token": DEMO_USERS.admin,
141074
- "sandbox-player-token": DEMO_USERS.player,
141075
- "sandbox-developer-token": DEMO_USERS.developer,
141076
- "sandbox-pending-dev-token": DEMO_USERS.pendingDeveloper,
141077
- "sandbox-unverified-token": DEMO_USERS.unverifiedPlayer,
141078
- "mock-game-token-for-local-dev": DEMO_USERS.admin
141079
- };
141080
- var MOCK_GAME_ID = "mock-game-id-from-template";
141081
- // src/constants/demo-items.ts
141082
- var DEMO_ITEM_IDS = {
141083
- playcademyCredits: "10000000-0000-0000-0000-000000000001",
141084
- foundingMemberBadge: "10000000-0000-0000-0000-000000000002",
141085
- earlyAdopterBadge: "10000000-0000-0000-0000-000000000003",
141086
- firstGameBadge: "10000000-0000-0000-0000-000000000004",
141087
- commonSword: "10000000-0000-0000-0000-000000000005",
141088
- smallHealthPotion: "10000000-0000-0000-0000-000000000006",
141089
- smallBackpack: "10000000-0000-0000-0000-000000000007"
141090
- };
141091
- var PLAYCADEMY_CREDITS_ID = DEMO_ITEM_IDS.playcademyCredits;
141092
- var SAMPLE_ITEMS = [
141093
- {
141094
- id: PLAYCADEMY_CREDITS_ID,
141095
- slug: "PLAYCADEMY_CREDITS",
141096
- gameId: null,
141097
- displayName: "PLAYCADEMY credits",
141098
- description: "The main currency used across PLAYCADEMY.",
141099
- type: "currency",
141100
- isPlaceable: false,
141101
- imageUrl: "http://playcademy-sandbox.local/playcademy-credit.png",
141102
- metadata: {
141103
- rarity: "common"
141104
- }
141105
- },
141106
- {
141107
- id: DEMO_ITEM_IDS.foundingMemberBadge,
141108
- slug: "FOUNDING_MEMBER_BADGE",
141109
- gameId: null,
141110
- displayName: "Founding Member Badge",
141111
- description: "Reserved for founding core team of the PLAYCADEMY platform.",
141112
- type: "badge",
141113
- isPlaceable: false,
141114
- imageUrl: null,
141115
- metadata: {
141116
- rarity: "legendary"
141117
- }
141118
- },
141119
- {
141120
- id: DEMO_ITEM_IDS.earlyAdopterBadge,
141121
- slug: "EARLY_ADOPTER_BADGE",
141122
- gameId: null,
141123
- displayName: "Early Adopter Badge",
141124
- description: "Awarded to users who joined during the beta phase.",
141125
- type: "badge",
141126
- isPlaceable: false,
141127
- imageUrl: null,
141128
- metadata: {
141129
- rarity: "epic"
141130
- }
141131
- },
141132
- {
141133
- id: DEMO_ITEM_IDS.firstGameBadge,
141134
- slug: "FIRST_GAME_BADGE",
141135
- gameId: null,
141136
- displayName: "First Game Played",
141137
- description: "Awarded for playing your first game in the Playcademy platform.",
141138
- type: "badge",
141139
- isPlaceable: false,
141140
- imageUrl: "http://playcademy-sandbox.local/first-game-badge.png",
141141
- metadata: {
141142
- rarity: "uncommon"
141143
- }
141144
- },
141145
- {
141146
- id: DEMO_ITEM_IDS.commonSword,
141147
- slug: "COMMON_SWORD",
141148
- gameId: null,
141149
- displayName: "Common Sword",
141150
- description: "A basic sword, good for beginners.",
141151
- type: "unlock",
141152
- isPlaceable: false,
141153
- imageUrl: "http://playcademy-sandbox.local/common-sword.png",
141154
- metadata: undefined
141155
- },
141156
- {
141157
- id: DEMO_ITEM_IDS.smallHealthPotion,
141158
- slug: "SMALL_HEALTH_POTION",
141159
- gameId: null,
141160
- displayName: "Small Health Potion",
141161
- description: "Restores a small amount of health.",
141162
- type: "other",
141163
- isPlaceable: false,
141164
- imageUrl: "http://playcademy-sandbox.local/small-health-potion.png",
141165
- metadata: undefined
141166
- },
141167
- {
141168
- id: DEMO_ITEM_IDS.smallBackpack,
141169
- slug: "SMALL_BACKPACK",
141170
- gameId: null,
141171
- displayName: "Small Backpack",
141172
- description: "Increases your inventory capacity by 5 slots.",
141173
- type: "upgrade",
141174
- isPlaceable: false,
141175
- imageUrl: "http://playcademy-sandbox.local/small-backpack.png",
141176
- metadata: undefined
141177
- }
141178
- ];
141179
- var SAMPLE_INVENTORY = [
141180
- {
141181
- id: "20000000-0000-0000-0000-000000000001",
141182
- userId: DEMO_USER.id,
141183
- itemId: PLAYCADEMY_CREDITS_ID,
141184
- quantity: 1000
141185
- }
141186
- ];
141187
141313
  // src/server/auth.ts
141188
141314
  function extractBearerToken(authHeader) {
141189
141315
  if (!authHeader?.startsWith("Bearer ")) {
@@ -141191,25 +141317,53 @@ function extractBearerToken(authHeader) {
141191
141317
  }
141192
141318
  return authHeader.substring(7);
141193
141319
  }
141194
- function parseJwtUserId(token) {
141320
+ function parseSandboxToken(token) {
141321
+ try {
141322
+ const parts2 = token.split(".");
141323
+ if (parts2.length !== 3 || parts2[2] !== "sandbox") {
141324
+ return null;
141325
+ }
141326
+ const header = JSON.parse(atob(parts2[0]));
141327
+ if (header.typ !== "sandbox") {
141328
+ return null;
141329
+ }
141330
+ const payload = JSON.parse(atob(parts2[1]));
141331
+ if (!payload.uid || !payload.sub) {
141332
+ return null;
141333
+ }
141334
+ return {
141335
+ userId: payload.uid,
141336
+ gameSlug: payload.sub
141337
+ };
141338
+ } catch {
141339
+ return null;
141340
+ }
141341
+ }
141342
+ function parseJwtClaims(token) {
141195
141343
  try {
141196
141344
  const parts2 = token.split(".");
141197
141345
  if (parts2.length === 3 && parts2[1]) {
141198
141346
  const payload = JSON.parse(atob(parts2[1]));
141199
- return payload.uid || null;
141347
+ if (payload.uid) {
141348
+ return {
141349
+ userId: payload.uid,
141350
+ gameId: payload.sub
141351
+ };
141352
+ }
141200
141353
  }
141201
- } catch (error) {
141202
- console.warn("[Auth] Failed to decode JWT token:", error);
141203
- }
141354
+ } catch {}
141204
141355
  return null;
141205
141356
  }
141206
- function resolveUserId(token) {
141357
+ function resolveAuth(token) {
141207
141358
  const demoUser = DEMO_TOKENS[token];
141208
- if (demoUser) {
141209
- return demoUser.id;
141210
- }
141359
+ if (demoUser)
141360
+ return { userId: demoUser.id };
141211
141361
  if (token.includes(".")) {
141212
- return parseJwtUserId(token);
141362
+ const sandboxClaims = parseSandboxToken(token);
141363
+ if (sandboxClaims) {
141364
+ return sandboxClaims;
141365
+ }
141366
+ return parseJwtClaims(token);
141213
141367
  }
141214
141368
  return null;
141215
141369
  }
@@ -141224,6 +141378,18 @@ async function fetchUserFromDatabase(db, userId) {
141224
141378
  throw error;
141225
141379
  }
141226
141380
  }
141381
+ async function resolveGameIdFromSlug(db, slug) {
141382
+ try {
141383
+ const game = await db.query.games.findFirst({
141384
+ where: eq(games.slug, slug),
141385
+ columns: { id: true }
141386
+ });
141387
+ return game?.id || null;
141388
+ } catch (error) {
141389
+ console.error("[Auth] Error looking up game by slug:", error);
141390
+ return null;
141391
+ }
141392
+ }
141227
141393
  function isPublicRoute(path, exceptions) {
141228
141394
  return exceptions.some((exception) => {
141229
141395
  if (path === exception)
@@ -141245,11 +141411,11 @@ async function authenticateRequest(c) {
141245
141411
  shouldReturn404: true
141246
141412
  };
141247
141413
  }
141248
- let targetUserId;
141414
+ let claims;
141249
141415
  if (apiKey && !bearerToken) {
141250
- targetUserId = DEMO_USERS.admin.id;
141416
+ claims = { userId: DEMO_USERS.admin.id };
141251
141417
  } else {
141252
- const resolved = resolveUserId(token);
141418
+ const resolved = resolveAuth(token);
141253
141419
  if (!resolved) {
141254
141420
  return {
141255
141421
  success: false,
@@ -141257,26 +141423,22 @@ async function authenticateRequest(c) {
141257
141423
  shouldReturn404: true
141258
141424
  };
141259
141425
  }
141260
- targetUserId = resolved;
141426
+ claims = resolved;
141261
141427
  }
141262
141428
  const db = c.get("db");
141263
141429
  if (!db) {
141264
141430
  console.error("[Auth] Database not available in context");
141265
- return {
141266
- success: false,
141267
- error: "Internal server error",
141268
- shouldReturn404: false
141269
- };
141431
+ return { success: false, error: "Internal server error", shouldReturn404: false };
141270
141432
  }
141271
- const user = await fetchUserFromDatabase(db, targetUserId);
141433
+ const user = await fetchUserFromDatabase(db, claims.userId);
141272
141434
  if (!user) {
141273
- return {
141274
- success: false,
141275
- error: "User not found or token invalid",
141276
- shouldReturn404: true
141277
- };
141435
+ return { success: false, error: "User not found or token invalid", shouldReturn404: true };
141436
+ }
141437
+ let gameId = claims.gameId;
141438
+ if (!gameId && claims.gameSlug) {
141439
+ gameId = await resolveGameIdFromSlug(db, claims.gameSlug) ?? undefined;
141278
141440
  }
141279
- return { success: true, user };
141441
+ return { success: true, user, gameId };
141280
141442
  }
141281
141443
  function setupAuth(options = {}) {
141282
141444
  const { exceptions = [] } = options;
@@ -141288,6 +141450,8 @@ function setupAuth(options = {}) {
141288
141450
  const result = await authenticateRequest(c);
141289
141451
  if (result.success) {
141290
141452
  c.set("user", result.user);
141453
+ if (result.gameId)
141454
+ c.set("gameId", result.gameId);
141291
141455
  await next();
141292
141456
  return;
141293
141457
  }
@@ -147149,34 +147313,34 @@ function getDatabase() {
147149
147313
 
147150
147314
  // src/database/path-manager.ts
147151
147315
  import fs2 from "node:fs";
147152
- import { dirname, isAbsolute, join as join2 } from "node:path";
147316
+ import { dirname, isAbsolute, join as join3 } from "node:path";
147153
147317
 
147154
147318
  class DatabasePathManager {
147155
- static DEFAULT_DB_SUBPATH = join2("@playcademy", "vite-plugin", "node_modules", ".playcademy", "sandbox.db");
147319
+ static DEFAULT_DB_SUBPATH = join3("@playcademy", "vite-plugin", "node_modules", ".playcademy", "sandbox.db");
147156
147320
  static findNodeModulesPath() {
147157
147321
  let currentDir = process.cwd();
147158
147322
  while (currentDir !== dirname(currentDir)) {
147159
- const nodeModulesPath = join2(currentDir, "node_modules");
147323
+ const nodeModulesPath = join3(currentDir, "node_modules");
147160
147324
  if (fs2.existsSync(nodeModulesPath)) {
147161
147325
  return nodeModulesPath;
147162
147326
  }
147163
147327
  currentDir = dirname(currentDir);
147164
147328
  }
147165
- return join2(process.cwd(), "node_modules");
147329
+ return join3(process.cwd(), "node_modules");
147166
147330
  }
147167
147331
  static resolveDatabasePath(customPath) {
147168
147332
  if (customPath) {
147169
147333
  if (customPath === ":memory:")
147170
147334
  return ":memory:";
147171
- return isAbsolute(customPath) ? customPath : join2(process.cwd(), customPath);
147335
+ return isAbsolute(customPath) ? customPath : join3(process.cwd(), customPath);
147172
147336
  }
147173
- return join2(this.findNodeModulesPath(), this.DEFAULT_DB_SUBPATH);
147337
+ return join3(this.findNodeModulesPath(), this.DEFAULT_DB_SUBPATH);
147174
147338
  }
147175
147339
  static ensureDatabaseDirectory(dbPath) {
147176
147340
  if (dbPath === ":memory:")
147177
147341
  return;
147178
147342
  const dirPath = dirname(dbPath);
147179
- const absolutePath = isAbsolute(dirPath) ? dirPath : join2(process.cwd(), dirPath);
147343
+ const absolutePath = isAbsolute(dirPath) ? dirPath : join3(process.cwd(), dirPath);
147180
147344
  try {
147181
147345
  if (!fs2.existsSync(absolutePath)) {
147182
147346
  fs2.mkdirSync(absolutePath, { recursive: true });
@@ -147503,7 +147667,51 @@ var init_overworld = __esm3(() => {
147503
147667
  FIRST_GAME: ITEM_SLUGS2.FIRST_GAME_BADGE
147504
147668
  };
147505
147669
  });
147506
- var init_timeback = () => {};
147670
+ var TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY";
147671
+ var TIMEBACK_ORG_NAME = "Playcademy Studios";
147672
+ var TIMEBACK_ORG_TYPE = "department";
147673
+ var TIMEBACK_COURSE_DEFAULTS;
147674
+ var TIMEBACK_RESOURCE_DEFAULTS;
147675
+ var TIMEBACK_COMPONENT_DEFAULTS;
147676
+ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS;
147677
+ var init_timeback = __esm3(() => {
147678
+ TIMEBACK_COURSE_DEFAULTS = {
147679
+ gradingScheme: "STANDARD",
147680
+ level: {
147681
+ elementary: "Elementary",
147682
+ middle: "Middle",
147683
+ high: "High",
147684
+ ap: "AP"
147685
+ },
147686
+ goals: {
147687
+ dailyXp: 50,
147688
+ dailyLessons: 3
147689
+ },
147690
+ metrics: {
147691
+ totalXp: 1000,
147692
+ totalLessons: 50
147693
+ }
147694
+ };
147695
+ TIMEBACK_RESOURCE_DEFAULTS = {
147696
+ vendorId: "playcademy",
147697
+ roles: ["primary"],
147698
+ importance: "primary",
147699
+ metadata: {
147700
+ type: "interactive",
147701
+ toolProvider: "Playcademy",
147702
+ instructionalMethod: "exploratory",
147703
+ language: "en-US"
147704
+ }
147705
+ };
147706
+ TIMEBACK_COMPONENT_DEFAULTS = {
147707
+ sortOrder: 1,
147708
+ prerequisiteCriteria: "ALL"
147709
+ };
147710
+ TIMEBACK_COMPONENT_RESOURCE_DEFAULTS = {
147711
+ sortOrder: 1,
147712
+ lessonType: "quiz"
147713
+ };
147714
+ });
147507
147715
  var init_workers = () => {};
147508
147716
  var init_src2 = __esm3(() => {
147509
147717
  init_auth();
@@ -147544,7 +147752,6 @@ var HTTP_DEFAULTS;
147544
147752
  var AUTH_DEFAULTS;
147545
147753
  var CACHE_DEFAULTS;
147546
147754
  var CONFIG_DEFAULTS;
147547
- var DEFAULT_PLAYCADEMY_ORGANIZATION_ID;
147548
147755
  var PLAYCADEMY_DEFAULTS;
147549
147756
  var RESOURCE_DEFAULTS;
147550
147757
  var HTTP_STATUS;
@@ -147691,54 +147898,26 @@ var init_constants = __esm3(() => {
147691
147898
  CONFIG_DEFAULTS = {
147692
147899
  fileNames: ["timeback.config.js", "timeback.config.json"]
147693
147900
  };
147694
- DEFAULT_PLAYCADEMY_ORGANIZATION_ID = process.env.TIMEBACK_ORG_SOURCE_ID || "PLAYCADEMY";
147695
147901
  PLAYCADEMY_DEFAULTS = {
147696
- organization: DEFAULT_PLAYCADEMY_ORGANIZATION_ID,
147902
+ organization: TIMEBACK_ORG_SOURCED_ID,
147697
147903
  launchBaseUrls: PLAYCADEMY_BASE_URLS
147698
147904
  };
147699
147905
  RESOURCE_DEFAULTS = {
147700
147906
  organization: {
147701
- name: "Playcademy Studios",
147702
- type: "department"
147907
+ name: TIMEBACK_ORG_NAME,
147908
+ type: TIMEBACK_ORG_TYPE
147703
147909
  },
147704
147910
  course: {
147705
- gradingScheme: "STANDARD",
147706
- level: {
147707
- elementary: "Elementary",
147708
- middle: "Middle",
147709
- high: "High",
147710
- ap: "AP"
147711
- },
147911
+ gradingScheme: TIMEBACK_COURSE_DEFAULTS.gradingScheme,
147912
+ level: TIMEBACK_COURSE_DEFAULTS.level,
147712
147913
  metadata: {
147713
- goals: {
147714
- dailyXp: 50,
147715
- dailyLessons: 3
147716
- },
147717
- metrics: {
147718
- totalXp: 1000,
147719
- totalLessons: 50
147720
- }
147914
+ goals: TIMEBACK_COURSE_DEFAULTS.goals,
147915
+ metrics: TIMEBACK_COURSE_DEFAULTS.metrics
147721
147916
  }
147722
147917
  },
147723
- component: {
147724
- sortOrder: 1,
147725
- prerequisiteCriteria: "ALL"
147726
- },
147727
- resource: {
147728
- vendorId: "playcademy",
147729
- roles: ["primary"],
147730
- importance: "primary",
147731
- metadata: {
147732
- type: "interactive",
147733
- toolProvider: "Playcademy",
147734
- instructionalMethod: "exploratory",
147735
- language: "en-US"
147736
- }
147737
- },
147738
- componentResource: {
147739
- sortOrder: 1,
147740
- lessonType: "quiz"
147741
- }
147918
+ component: TIMEBACK_COMPONENT_DEFAULTS,
147919
+ resource: TIMEBACK_RESOURCE_DEFAULTS,
147920
+ componentResource: TIMEBACK_COMPONENT_RESOURCE_DEFAULTS
147742
147921
  };
147743
147922
  HTTP_STATUS = {
147744
147923
  CLIENT_ERROR_MIN: 400,
@@ -153746,7 +153925,8 @@ class TimebackClient {
153746
153925
  courseId: enrollment.course.id,
153747
153926
  status: "active",
153748
153927
  grades,
153749
- subjects
153928
+ subjects,
153929
+ school: enrollment.school
153750
153930
  };
153751
153931
  });
153752
153932
  this.cacheManager.setEnrollments(studentId, enrollments);
@@ -153819,7 +153999,51 @@ var init_overworld2 = __esm4(() => {
153819
153999
  FIRST_GAME: ITEM_SLUGS3.FIRST_GAME_BADGE
153820
154000
  };
153821
154001
  });
153822
- var init_timeback2 = () => {};
154002
+ var TIMEBACK_ORG_SOURCED_ID2 = "PLAYCADEMY";
154003
+ var TIMEBACK_ORG_NAME2 = "Playcademy Studios";
154004
+ var TIMEBACK_ORG_TYPE2 = "department";
154005
+ var TIMEBACK_COURSE_DEFAULTS2;
154006
+ var TIMEBACK_RESOURCE_DEFAULTS2;
154007
+ var TIMEBACK_COMPONENT_DEFAULTS2;
154008
+ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2;
154009
+ var init_timeback2 = __esm4(() => {
154010
+ TIMEBACK_COURSE_DEFAULTS2 = {
154011
+ gradingScheme: "STANDARD",
154012
+ level: {
154013
+ elementary: "Elementary",
154014
+ middle: "Middle",
154015
+ high: "High",
154016
+ ap: "AP"
154017
+ },
154018
+ goals: {
154019
+ dailyXp: 50,
154020
+ dailyLessons: 3
154021
+ },
154022
+ metrics: {
154023
+ totalXp: 1000,
154024
+ totalLessons: 50
154025
+ }
154026
+ };
154027
+ TIMEBACK_RESOURCE_DEFAULTS2 = {
154028
+ vendorId: "playcademy",
154029
+ roles: ["primary"],
154030
+ importance: "primary",
154031
+ metadata: {
154032
+ type: "interactive",
154033
+ toolProvider: "Playcademy",
154034
+ instructionalMethod: "exploratory",
154035
+ language: "en-US"
154036
+ }
154037
+ };
154038
+ TIMEBACK_COMPONENT_DEFAULTS2 = {
154039
+ sortOrder: 1,
154040
+ prerequisiteCriteria: "ALL"
154041
+ };
154042
+ TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2 = {
154043
+ sortOrder: 1,
154044
+ lessonType: "quiz"
154045
+ };
154046
+ });
153823
154047
  var init_workers2 = () => {};
153824
154048
  var init_src3 = __esm4(() => {
153825
154049
  init_auth2();
@@ -153851,7 +154075,6 @@ var HTTP_DEFAULTS2;
153851
154075
  var AUTH_DEFAULTS2;
153852
154076
  var CACHE_DEFAULTS2;
153853
154077
  var CONFIG_DEFAULTS2;
153854
- var DEFAULT_PLAYCADEMY_ORGANIZATION_ID2;
153855
154078
  var PLAYCADEMY_DEFAULTS2;
153856
154079
  var RESOURCE_DEFAULTS2;
153857
154080
  var HTTP_STATUS2;
@@ -153998,54 +154221,26 @@ var init_constants2 = __esm4(() => {
153998
154221
  CONFIG_DEFAULTS2 = {
153999
154222
  fileNames: ["timeback.config.js", "timeback.config.json"]
154000
154223
  };
154001
- DEFAULT_PLAYCADEMY_ORGANIZATION_ID2 = process.env.TIMEBACK_ORG_SOURCE_ID || "PLAYCADEMY";
154002
154224
  PLAYCADEMY_DEFAULTS2 = {
154003
- organization: DEFAULT_PLAYCADEMY_ORGANIZATION_ID2,
154225
+ organization: TIMEBACK_ORG_SOURCED_ID2,
154004
154226
  launchBaseUrls: PLAYCADEMY_BASE_URLS2
154005
154227
  };
154006
154228
  RESOURCE_DEFAULTS2 = {
154007
154229
  organization: {
154008
- name: "Playcademy Studios",
154009
- type: "department"
154230
+ name: TIMEBACK_ORG_NAME2,
154231
+ type: TIMEBACK_ORG_TYPE2
154010
154232
  },
154011
154233
  course: {
154012
- gradingScheme: "STANDARD",
154013
- level: {
154014
- elementary: "Elementary",
154015
- middle: "Middle",
154016
- high: "High",
154017
- ap: "AP"
154018
- },
154019
- metadata: {
154020
- goals: {
154021
- dailyXp: 50,
154022
- dailyLessons: 3
154023
- },
154024
- metrics: {
154025
- totalXp: 1000,
154026
- totalLessons: 50
154027
- }
154028
- }
154029
- },
154030
- component: {
154031
- sortOrder: 1,
154032
- prerequisiteCriteria: "ALL"
154033
- },
154034
- resource: {
154035
- vendorId: "playcademy",
154036
- roles: ["primary"],
154037
- importance: "primary",
154234
+ gradingScheme: TIMEBACK_COURSE_DEFAULTS2.gradingScheme,
154235
+ level: TIMEBACK_COURSE_DEFAULTS2.level,
154038
154236
  metadata: {
154039
- type: "interactive",
154040
- toolProvider: "Playcademy",
154041
- instructionalMethod: "exploratory",
154042
- language: "en-US"
154237
+ goals: TIMEBACK_COURSE_DEFAULTS2.goals,
154238
+ metrics: TIMEBACK_COURSE_DEFAULTS2.metrics
154043
154239
  }
154044
154240
  },
154045
- componentResource: {
154046
- sortOrder: 1,
154047
- lessonType: "quiz"
154048
- }
154241
+ component: TIMEBACK_COMPONENT_DEFAULTS2,
154242
+ resource: TIMEBACK_RESOURCE_DEFAULTS2,
154243
+ componentResource: TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2
154049
154244
  };
154050
154245
  HTTP_STATUS2 = {
154051
154246
  CLIENT_ERROR_MIN: 400,
@@ -154396,68 +154591,89 @@ function buildResourceMetadata({
154396
154591
  }
154397
154592
  return metadata2;
154398
154593
  }
154399
- // ../api-core/src/utils/timeback-enrollments.ts
154594
+ // ../api-core/src/utils/timeback-profile.ts
154400
154595
  init_src();
154401
- async function fetchEnrollmentsForUser(timebackId) {
154402
- const db = getDatabase();
154403
- const isLocal = process.env.PUBLIC_IS_LOCAL === "true";
154404
- if (isLocal) {
154405
- const allIntegrations = await db.query.gameTimebackIntegrations.findMany();
154406
- return allIntegrations.map((integration) => ({
154407
- gameId: integration.gameId,
154408
- grade: integration.grade,
154409
- subject: integration.subject,
154410
- courseId: integration.courseId
154411
- }));
154596
+ async function fetchStudentFromOneRoster(timebackId) {
154597
+ log2.debug("[OneRoster] Fetching student profile", { timebackId });
154598
+ try {
154599
+ const client = await getTimebackClient();
154600
+ const user = await client.oneroster.users.get(timebackId);
154601
+ const primaryRoleEntry = user.roles.find((r2) => r2.roleType === "primary");
154602
+ const role = primaryRoleEntry?.role ?? user.roles[0]?.role ?? "student";
154603
+ const orgMap = new Map;
154604
+ if (user.primaryOrg) {
154605
+ orgMap.set(user.primaryOrg.sourcedId, {
154606
+ id: user.primaryOrg.sourcedId,
154607
+ name: user.primaryOrg.name ?? null,
154608
+ type: user.primaryOrg.type || "school",
154609
+ isPrimary: true
154610
+ });
154611
+ }
154612
+ for (const r2 of user.roles) {
154613
+ if (r2.org && !orgMap.has(r2.org.sourcedId)) {
154614
+ orgMap.set(r2.org.sourcedId, {
154615
+ id: r2.org.sourcedId,
154616
+ name: null,
154617
+ type: "school",
154618
+ isPrimary: false
154619
+ });
154620
+ }
154621
+ }
154622
+ const organizations = Array.from(orgMap.values());
154623
+ return { role, organizations };
154624
+ } catch (error2) {
154625
+ log2.warn("[OneRoster] Failed to fetch student, using defaults", { error: error2, timebackId });
154626
+ return { role: "student", organizations: [] };
154412
154627
  }
154413
- log2.debug("[timeback-enrollments] Fetching student enrollments from TimeBack", { timebackId });
154628
+ }
154629
+ async function fetchEnrollmentsFromEduBridge(timebackId) {
154630
+ const db = getDatabase();
154631
+ log2.debug("[EduBridge] Fetching student enrollments", { timebackId });
154414
154632
  try {
154415
154633
  const client = await getTimebackClient();
154416
- const classes = await client.getEnrollments(timebackId);
154417
- const courseIds = classes.map((cls) => cls.courseId).filter((id) => Boolean(id));
154418
- if (courseIds.length === 0) {
154634
+ const enrollments = await client.getEnrollments(timebackId);
154635
+ const courseIds = enrollments.map((e) => e.courseId).filter((id) => Boolean(id));
154636
+ if (courseIds.length === 0)
154419
154637
  return [];
154420
- }
154638
+ const courseToSchool = new Map(enrollments.filter((e) => e.school?.id).map((e) => [e.courseId, e.school.id]));
154421
154639
  const integrations = await db.query.gameTimebackIntegrations.findMany({
154422
154640
  where: inArray(gameTimebackIntegrations.courseId, courseIds)
154423
154641
  });
154424
- return integrations.map((integration) => ({
154425
- gameId: integration.gameId,
154426
- grade: integration.grade,
154427
- subject: integration.subject,
154428
- courseId: integration.courseId
154642
+ return integrations.map((i3) => ({
154643
+ gameId: i3.gameId,
154644
+ grade: i3.grade,
154645
+ subject: i3.subject,
154646
+ courseId: i3.courseId,
154647
+ orgId: courseToSchool.get(i3.courseId)
154429
154648
  }));
154430
154649
  } catch (error2) {
154431
- log2.warn("[timeback-enrollments] Failed to fetch TimeBack enrollments:", {
154432
- error: error2,
154433
- timebackId
154434
- });
154650
+ log2.warn("[EduBridge] Failed to fetch enrollments", { error: error2, timebackId });
154435
154651
  return [];
154436
154652
  }
154437
154653
  }
154438
- async function fetchUserRole(timebackId) {
154439
- log2.debug("[timeback] Fetching user role from TimeBack", { timebackId });
154440
- try {
154441
- const client = await getTimebackClient();
154442
- const user = await client.oneroster.users.get(timebackId);
154443
- const primaryRole = user.roles.find((r2) => r2.roleType === "primary");
154444
- const role = primaryRole?.role ?? user.roles[0]?.role ?? "student";
154445
- log2.debug("[timeback] Resolved user role", { timebackId, role });
154446
- return role;
154447
- } catch (error2) {
154448
- log2.warn("[timeback] Failed to fetch user role, defaulting to student:", {
154449
- error: error2,
154450
- timebackId
154451
- });
154452
- return "student";
154453
- }
154654
+ function filterEnrollmentsByGame(enrollments, gameId) {
154655
+ return enrollments.filter((e) => e.gameId === gameId).map(({ gameId: _4, ...rest }) => rest);
154656
+ }
154657
+ function filterOrganizationsByEnrollments(organizations, enrollments) {
154658
+ const enrollmentOrgIds = new Set(enrollments.map((e) => e.orgId).filter(Boolean));
154659
+ if (enrollmentOrgIds.size === 0)
154660
+ return [];
154661
+ return organizations.filter((o4) => enrollmentOrgIds.has(o4.id));
154454
154662
  }
154455
- async function fetchUserTimebackData(timebackId) {
154456
- const [role, enrollments] = await Promise.all([
154457
- fetchUserRole(timebackId),
154458
- fetchEnrollmentsForUser(timebackId)
154663
+ async function fetchUserTimebackData(timebackId, gameId) {
154664
+ const [{ role, organizations: allOrganizations }, allEnrollments] = await Promise.all([
154665
+ fetchStudentFromOneRoster(timebackId),
154666
+ fetchEnrollmentsFromEduBridge(timebackId)
154459
154667
  ]);
154460
- return { role, enrollments };
154668
+ const enrollments = gameId ? filterEnrollmentsByGame(allEnrollments, gameId) : allEnrollments;
154669
+ const organizations = gameId ? filterOrganizationsByEnrollments(allOrganizations, enrollments) : allOrganizations;
154670
+ log2.debug("[Timeback] Fetched student data", {
154671
+ timebackId,
154672
+ role,
154673
+ enrollments: enrollments.map((e) => `${e.subject}:${e.grade}`),
154674
+ organizations: organizations.map((o4) => `${o4.name ?? o4.id} (${o4.type})`)
154675
+ });
154676
+ return { id: timebackId, role, enrollments, organizations };
154461
154677
  }
154462
154678
  // ../data/src/domains/achievement/types.ts
154463
154679
  var AchievementCompletionType;
@@ -156375,7 +156591,7 @@ async function publishPersonalBestNotification(userId, gameId, rank, newScore, p
156375
156591
  }
156376
156592
  // ../cloudflare/src/playcademy/provider.ts
156377
156593
  import { readdir as readdir2, readFile as readFile2, stat } from "node:fs/promises";
156378
- import { join as join4, relative } from "node:path";
156594
+ import { join as join5, relative } from "node:path";
156379
156595
  init_src();
156380
156596
 
156381
156597
  // ../cloudflare/src/core/client.ts
@@ -156902,7 +157118,7 @@ init_src();
156902
157118
  // ../cloudflare/src/utils/assets.ts
156903
157119
  import { createHash } from "node:crypto";
156904
157120
  import { readdir, readFile } from "node:fs/promises";
156905
- import { join as join3 } from "node:path";
157121
+ import { join as join4 } from "node:path";
156906
157122
  function hashFile(content) {
156907
157123
  return createHash("md5").update(content).digest("hex");
156908
157124
  }
@@ -156922,7 +157138,7 @@ async function scanAssetDirectory(distPath) {
156922
157138
  async function scanFiles(dir, baseDir = dir) {
156923
157139
  const entries = await readdir(dir, { withFileTypes: true });
156924
157140
  for (const entry of entries) {
156925
- const fullPath = join3(dir, entry.name);
157141
+ const fullPath = join4(dir, entry.name);
156926
157142
  if (entry.isDirectory()) {
156927
157143
  await scanFiles(fullPath, baseDir);
156928
157144
  } else {
@@ -157294,7 +157510,7 @@ class CloudflareProvider {
157294
157510
  async function scanDirectory(dir) {
157295
157511
  const entries = await readdir2(dir, { withFileTypes: true });
157296
157512
  for (const entry of entries) {
157297
- const fullPath = join4(dir, entry.name);
157513
+ const fullPath = join5(dir, entry.name);
157298
157514
  if (entry.isDirectory()) {
157299
157515
  if (await scanDirectory(fullPath))
157300
157516
  return true;
@@ -157316,7 +157532,7 @@ class CloudflareProvider {
157316
157532
  async resolveAssetBasePath(dirPath) {
157317
157533
  const entries = await readdir2(dirPath, { withFileTypes: true });
157318
157534
  if (entries.length === 1 && entries[0]?.isDirectory()) {
157319
- const unwrappedPath = join4(dirPath, entries[0].name);
157535
+ const unwrappedPath = join5(dirPath, entries[0].name);
157320
157536
  log2.debug("[CloudflareProvider] Unwrapping wrapper directory", {
157321
157537
  wrapper: entries[0].name
157322
157538
  });
@@ -157327,7 +157543,7 @@ class CloudflareProvider {
157327
157543
  async uploadFilesToR2(dir, baseDir, bucketName) {
157328
157544
  const entries = await readdir2(dir, { withFileTypes: true });
157329
157545
  for (const entry of entries) {
157330
- const fullPath = join4(dir, entry.name);
157546
+ const fullPath = join5(dir, entry.name);
157331
157547
  if (entry.isDirectory()) {
157332
157548
  await this.uploadFilesToR2(fullPath, baseDir, bucketName);
157333
157549
  } else {
@@ -157815,40 +158031,19 @@ async function seedCurrencies(db) {
157815
158031
  }
157816
158032
  }
157817
158033
 
157818
- // src/lib/logging/adapter.ts
157819
- var customLogger;
157820
- function setLogger(logger3) {
157821
- customLogger = logger3;
157822
- }
157823
- function getLogger() {
157824
- if (customLogger) {
157825
- return customLogger;
157826
- }
157827
- return {
157828
- info: (msg) => console.log(msg),
157829
- warn: (msg) => console.warn(msg),
157830
- error: (msg) => console.error(msg)
157831
- };
157832
- }
157833
- var logger3 = {
157834
- info: (msg) => {
157835
- if (customLogger || !config.embedded) {
157836
- getLogger().info(msg);
157837
- }
157838
- },
157839
- warn: (msg) => getLogger().warn(msg),
157840
- error: (msg) => getLogger().error(msg)
157841
- };
157842
158034
  // src/database/seed/timeback.ts
157843
- function resolveStudentId(studentId) {
157844
- if (!studentId)
157845
- return null;
157846
- if (studentId === "mock")
157847
- return `mock-student-${crypto.randomUUID().slice(0, 8)}`;
157848
- return studentId;
158035
+ function generateMockStudentId(userId) {
158036
+ return `mock-student-${userId.slice(-8)}`;
157849
158037
  }
157850
- function getAdminTimebackId() {
157851
- return resolveStudentId(config.timeback.studentId);
158038
+ function generateTimebackId(userId, isPrimaryUser = false) {
158039
+ const timebackId = config.timeback.timebackId;
158040
+ if (!timebackId) {
158041
+ return null;
158042
+ }
158043
+ if (timebackId === "mock") {
158044
+ return generateMockStudentId(userId);
158045
+ }
158046
+ return isPrimaryUser ? timebackId : generateMockStudentId(userId);
157852
158047
  }
157853
158048
  async function seedTimebackIntegrations(db, gameId, courses) {
157854
158049
  const now2 = new Date;
@@ -157872,12 +158067,10 @@ async function seedTimebackIntegrations(db, gameId, courses) {
157872
158067
  });
157873
158068
  seededCount++;
157874
158069
  } catch (error2) {
157875
- console.error(`❌ Error seeding TimeBack integration for ${course.subject}:${course.grade}:`, error2);
158070
+ console.error(`❌ Error seeding Timeback integration for ${course.subject}:${course.grade}:`, error2);
157876
158071
  }
157877
158072
  }
157878
- if (seededCount > 0) {
157879
- logger3.info(`\uD83D\uDCDA Seeded ${seededCount} TimeBack integration(s)`);
157880
- }
158073
+ return seededCount;
157881
158074
  }
157882
158075
 
157883
158076
  // src/database/seed/games.ts
@@ -157913,7 +158106,6 @@ async function seedCurrentProjectGame(db, project) {
157913
158106
  where: (games3, { eq: eq3 }) => eq3(games3.slug, project.slug)
157914
158107
  });
157915
158108
  if (existingGame) {
157916
- logger3.info(`\uD83C\uDFAE Game "${project.displayName}" (${project.slug}) already exists`);
157917
158109
  if (project.timebackCourses && project.timebackCourses.length > 0) {
157918
158110
  await seedTimebackIntegrations(db, existingGame.id, project.timebackCourses);
157919
158111
  }
@@ -158006,11 +158198,12 @@ async function seedSpriteTemplates(db) {
158006
158198
  // src/database/seed/index.ts
158007
158199
  async function seedDemoData(db) {
158008
158200
  try {
158009
- const adminTimebackId = getAdminTimebackId();
158010
- for (const [role, user] of Object.entries(DEMO_USERS)) {
158201
+ const primaryUserId = DEMO_USERS.player.id;
158202
+ for (const user of Object.values(DEMO_USERS)) {
158203
+ const isPrimaryUser = user.id === primaryUserId;
158011
158204
  const userValues = {
158012
158205
  ...user,
158013
- timebackId: role === "admin" ? adminTimebackId : null
158206
+ timebackId: generateTimebackId(user.id, isPrimaryUser)
158014
158207
  };
158015
158208
  await db.insert(users).values(userValues).onConflictDoNothing();
158016
158209
  }
@@ -158031,7 +158224,7 @@ async function seedDemoData(db) {
158031
158224
  console.error("❌ Error seeding demo data:", error2);
158032
158225
  throw error2;
158033
158226
  }
158034
- return DEMO_USERS.admin;
158227
+ return DEMO_USERS.player;
158035
158228
  }
158036
158229
 
158037
158230
  // src/server/database.ts
@@ -158071,6 +158264,13 @@ async function setupServerDatabase(processedOptions, project) {
158071
158264
  // src/server/options.ts
158072
158265
  init_src();
158073
158266
  var import_json_colorizer = __toESM(require_dist2(), 1);
158267
+
158268
+ // src/lib/logging/adapter.ts
158269
+ var customLogger;
158270
+ function setLogger(logger3) {
158271
+ customLogger = logger3;
158272
+ }
158273
+ // src/server/options.ts
158074
158274
  function processServerOptions(port, options) {
158075
158275
  const {
158076
158276
  verbose = false,
@@ -158121,6 +158321,10 @@ async function startRealtimeServer(realtimeOptions, betterAuthSecret) {
158121
158321
  if (!realtimeOptions.enabled) {
158122
158322
  return null;
158123
158323
  }
158324
+ if (!realtimeOptions.port) {
158325
+ return null;
158326
+ }
158327
+ await waitForPort(realtimeOptions.port);
158124
158328
  if (typeof Bun === "undefined") {
158125
158329
  try {
158126
158330
  return Promise.resolve().then(() => (init_sandbox(), exports_sandbox)).then(({ createSandboxRealtimeServer: createSandboxRealtimeServer2 }) => createSandboxRealtimeServer2({
@@ -163414,12 +163618,32 @@ async function getUserMe(ctx) {
163414
163618
  log2.error(`[API /users/me] User not found in DB for valid token ID: ${user.id}`);
163415
163619
  throw ApiError.notFound("User not found");
163416
163620
  }
163621
+ const timeback3 = userData.timebackId ? await fetchUserTimebackData(userData.timebackId, ctx.gameId) : undefined;
163622
+ if (ctx.gameId) {
163623
+ return {
163624
+ id: userData.id,
163625
+ name: userData.name,
163626
+ role: userData.role,
163627
+ username: userData.username,
163628
+ email: userData.email,
163629
+ timeback: timeback3
163630
+ };
163631
+ }
163417
163632
  const timebackAccount = await db.query.accounts.findFirst({
163418
163633
  where: and(eq(accounts.userId, user.id), eq(accounts.providerId, "timeback"))
163419
163634
  });
163420
- const timeback3 = userData.timebackId ? await fetchUserTimebackData(userData.timebackId) : undefined;
163421
163635
  return {
163422
- ...userData,
163636
+ id: userData.id,
163637
+ name: userData.name,
163638
+ username: userData.username,
163639
+ email: userData.email,
163640
+ emailVerified: userData.emailVerified,
163641
+ image: userData.image,
163642
+ role: userData.role,
163643
+ developerStatus: userData.developerStatus,
163644
+ characterCreated: userData.characterCreated,
163645
+ createdAt: userData.createdAt,
163646
+ updatedAt: userData.updatedAt,
163423
163647
  hasTimebackAccount: !!timebackAccount,
163424
163648
  timeback: timeback3
163425
163649
  };
@@ -163431,16 +163655,107 @@ async function getUserMe(ctx) {
163431
163655
  }
163432
163656
  }
163433
163657
 
163658
+ // src/mocks/timeback.ts
163659
+ init_src();
163660
+ function shouldMockTimeback() {
163661
+ return config.timeback.timebackId === "mock";
163662
+ }
163663
+ function getMockStudentProfile() {
163664
+ const { organization: org, role } = config.timeback;
163665
+ return {
163666
+ role: role ?? "student",
163667
+ organizations: [
163668
+ {
163669
+ id: org?.id ?? "PLAYCADEMY",
163670
+ name: org?.name ?? "Playcademy Studios",
163671
+ type: org?.type ?? "department",
163672
+ isPrimary: true
163673
+ }
163674
+ ]
163675
+ };
163676
+ }
163677
+ async function getMockEnrollments(db) {
163678
+ const allIntegrations = await db.query.gameTimebackIntegrations.findMany();
163679
+ return allIntegrations.map((i4) => ({
163680
+ gameId: i4.gameId,
163681
+ grade: i4.grade,
163682
+ subject: i4.subject,
163683
+ courseId: i4.courseId
163684
+ }));
163685
+ }
163686
+ async function getMockTimebackData(db, timebackId, gameId) {
163687
+ const { role, organizations } = getMockStudentProfile();
163688
+ const allEnrollments = await getMockEnrollments(db);
163689
+ const enrollments = gameId ? allEnrollments.filter((e2) => e2.gameId === gameId).map(({ gameId: _5, ...rest }) => rest) : allEnrollments;
163690
+ log2.debug("[Timeback] Sandbox is using mock data", {
163691
+ timebackId,
163692
+ role,
163693
+ enrollments: enrollments.map((e2) => `${e2.subject}:${e2.grade}`),
163694
+ organizations: organizations.map((o5) => `${o5.name ?? o5.id}`)
163695
+ });
163696
+ return { id: timebackId, role, enrollments, organizations };
163697
+ }
163698
+ async function buildMockUserResponse(db, user, gameId) {
163699
+ const timeback3 = user.timebackId ? await getMockTimebackData(db, user.timebackId, gameId) : undefined;
163700
+ if (gameId) {
163701
+ return {
163702
+ id: user.id,
163703
+ name: user.name,
163704
+ role: user.role,
163705
+ username: user.username,
163706
+ email: user.email,
163707
+ timeback: timeback3
163708
+ };
163709
+ }
163710
+ const timebackAccount = await db.query.accounts.findFirst({
163711
+ where: and(eq(accounts.userId, user.id), eq(accounts.providerId, "timeback"))
163712
+ });
163713
+ return {
163714
+ id: user.id,
163715
+ name: user.name,
163716
+ username: user.username,
163717
+ email: user.email,
163718
+ emailVerified: user.emailVerified,
163719
+ image: user.image,
163720
+ role: user.role,
163721
+ developerStatus: user.developerStatus,
163722
+ characterCreated: user.characterCreated,
163723
+ createdAt: user.createdAt,
163724
+ updatedAt: user.updatedAt,
163725
+ hasTimebackAccount: !!timebackAccount,
163726
+ timeback: timeback3
163727
+ };
163728
+ }
163729
+
163434
163730
  // src/routes/platform/users.ts
163435
163731
  var usersRouter = new Hono2;
163436
163732
  usersRouter.get("/me", async (c3) => {
163437
- const ctx = {
163438
- user: c3.get("user"),
163439
- params: {},
163440
- url: new URL(c3.req.url),
163441
- request: c3.req.raw
163442
- };
163733
+ const user = c3.get("user");
163734
+ const gameId = c3.get("gameId");
163735
+ if (!user) {
163736
+ const error2 = ApiError.unauthorized("Valid session or bearer token required");
163737
+ return c3.json(createErrorResponse(error2), error2.statusCode);
163738
+ }
163443
163739
  try {
163740
+ if (shouldMockTimeback()) {
163741
+ const db = c3.get("db");
163742
+ const userData2 = await db.query.users.findFirst({
163743
+ where: eq(users.id, user.id)
163744
+ });
163745
+ if (!userData2) {
163746
+ const error2 = ApiError.notFound("User not found");
163747
+ return c3.json(createErrorResponse(error2), error2.statusCode);
163748
+ }
163749
+ const response = await buildMockUserResponse(db, userData2, gameId);
163750
+ return c3.json(response);
163751
+ }
163752
+ const ctx = {
163753
+ user,
163754
+ params: {},
163755
+ url: new URL(c3.req.url),
163756
+ request: c3.req.raw,
163757
+ gameId
163758
+ };
163444
163759
  const userData = await getUserMe(ctx);
163445
163760
  return c3.json(userData);
163446
163761
  } catch (error2) {
@@ -168780,7 +169095,7 @@ async function getTodayTimeBackXp(ctx) {
168780
169095
  throw error2;
168781
169096
  if (error2 instanceof InvalidTimezoneError)
168782
169097
  throw ApiError.badRequest(error2.message);
168783
- log2.error("[timeback] getTodayTimeBackXp failed", { error: error2 });
169098
+ log2.error("[Timeback] getTodayTimeBackXp failed", { error: error2 });
168784
169099
  throw ApiError.internal("Failed to get today's TimeBack XP", error2);
168785
169100
  }
168786
169101
  }
@@ -168796,7 +169111,7 @@ async function getTotalTimeBackXp(ctx) {
168796
169111
  totalXp: Number(result[0]?.totalXp) || 0
168797
169112
  };
168798
169113
  } catch (error2) {
168799
- log2.error("[timeback] getTotalTimeBackXp failed", { error: error2 });
169114
+ log2.error("[Timeback] getTotalTimeBackXp failed", { error: error2 });
168800
169115
  throw ApiError.internal("Failed to get total TimeBack XP", error2);
168801
169116
  }
168802
169117
  }
@@ -168846,7 +169161,7 @@ async function updateTodayTimeBackXp(ctx) {
168846
169161
  } catch (error2) {
168847
169162
  if (error2 instanceof ApiError)
168848
169163
  throw error2;
168849
- log2.error("[timeback] updateTodayTimeBackXp failed", { error: error2 });
169164
+ log2.error("[Timeback] updateTodayTimeBackXp failed", { error: error2 });
168850
169165
  throw ApiError.internal("Failed to update today's TimeBack XP", error2);
168851
169166
  }
168852
169167
  }
@@ -168881,7 +169196,7 @@ async function getTimeBackXpHistory(ctx) {
168881
169196
  }))
168882
169197
  };
168883
169198
  } catch (error2) {
168884
- log2.error("[timeback] getTimeBackXpHistory failed", { error: error2 });
169199
+ log2.error("[Timeback] getTimeBackXpHistory failed", { error: error2 });
168885
169200
  throw ApiError.internal("Failed to get TimeBack XP history", error2);
168886
169201
  }
168887
169202
  }
@@ -168897,7 +169212,7 @@ async function getStudentEnrollments(ctx) {
168897
169212
  throw ApiError.badRequest("Missing timebackId parameter");
168898
169213
  }
168899
169214
  log2.debug("[API] Getting student enrollments", { userId: user.id, timebackId });
168900
- const enrollments = await fetchEnrollmentsForUser(timebackId);
169215
+ const enrollments = await fetchEnrollmentsFromEduBridge(timebackId);
168901
169216
  log2.info("[API] Retrieved student enrollments", {
168902
169217
  userId: user.id,
168903
169218
  timebackId,
@@ -169098,13 +169413,23 @@ timebackRouter.post("/end-activity", async (c3) => {
169098
169413
  });
169099
169414
  timebackRouter.get("/enrollments/:timebackId", async (c3) => {
169100
169415
  const timebackId = c3.req.param("timebackId");
169101
- const ctx = {
169102
- user: c3.get("user"),
169103
- params: { timebackId },
169104
- url: new URL(c3.req.url),
169105
- request: c3.req.raw
169106
- };
169416
+ const user = c3.get("user");
169417
+ if (!user) {
169418
+ const error2 = ApiError.unauthorized("Must be logged in to get enrollments");
169419
+ return c3.json(createErrorResponse(error2), error2.statusCode);
169420
+ }
169107
169421
  try {
169422
+ if (shouldMockTimeback()) {
169423
+ const db = c3.get("db");
169424
+ const enrollments2 = await getMockEnrollments(db);
169425
+ return c3.json({ enrollments: enrollments2 });
169426
+ }
169427
+ const ctx = {
169428
+ user,
169429
+ params: { timebackId },
169430
+ url: new URL(c3.req.url),
169431
+ request: c3.req.raw
169432
+ };
169108
169433
  const result = await getStudentEnrollments(ctx);
169109
169434
  return c3.json(result);
169110
169435
  } catch (error2) {
@@ -169420,6 +169745,7 @@ function registerRoutes(app) {
169420
169745
  // src/server/index.ts
169421
169746
  var version3 = package_default.version;
169422
169747
  async function startServer(port, project, options = {}) {
169748
+ await waitForPort(port);
169423
169749
  const processedOptions = processServerOptions(port, options);
169424
169750
  const db = await setupServerDatabase(processedOptions, project);
169425
169751
  const app = createApp(db, {
@@ -169432,11 +169758,20 @@ async function startServer(port, project, options = {}) {
169432
169758
  return {
169433
169759
  main: mainServer,
169434
169760
  realtime: realtimeServer,
169761
+ timebackMode: getTimebackDisplayMode(),
169762
+ setRole: (role) => {
169763
+ config.timeback.role = role;
169764
+ },
169435
169765
  stop: () => {
169436
- if (mainServer.close)
169437
- mainServer.close();
169438
- if (realtimeServer?.stop)
169439
- realtimeServer.stop();
169766
+ return new Promise((resolve2) => {
169767
+ if (realtimeServer?.stop)
169768
+ realtimeServer.stop();
169769
+ if (mainServer.close) {
169770
+ mainServer.close(() => resolve2());
169771
+ } else {
169772
+ resolve2();
169773
+ }
169774
+ });
169440
169775
  }
169441
169776
  };
169442
169777
  }