@playcademy/sandbox 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6389,7 +6389,7 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
6389
6389
  resolve2(data);
6390
6390
  });
6391
6391
  });
6392
- var readFileSync = (fp) => {
6392
+ var readFileSync2 = (fp) => {
6393
6393
  return _fs.default.readFileSync(fp, "utf8");
6394
6394
  };
6395
6395
  var pathExists = (fp) => new Promise((resolve2) => {
@@ -6620,7 +6620,7 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
6620
6620
  data: this.packageJsonCache.get(filepath)[options.packageKey]
6621
6621
  };
6622
6622
  }
6623
- const data = this.options.parseJSON(readFileSync(filepath));
6623
+ const data = this.options.parseJSON(readFileSync2(filepath));
6624
6624
  return {
6625
6625
  path: filepath,
6626
6626
  data
@@ -6628,7 +6628,7 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
6628
6628
  }
6629
6629
  return {
6630
6630
  path: filepath,
6631
- data: readFileSync(filepath)
6631
+ data: readFileSync2(filepath)
6632
6632
  };
6633
6633
  }
6634
6634
  return {};
@@ -8227,19 +8227,19 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
8227
8227
  return walkForTsConfig(parentDirectory, readdirSync);
8228
8228
  }
8229
8229
  exports2.walkForTsConfig = walkForTsConfig;
8230
- function loadTsconfig(configFilePath, existsSync2, readFileSync) {
8231
- if (existsSync2 === undefined) {
8232
- existsSync2 = fs3.existsSync;
8230
+ function loadTsconfig(configFilePath, existsSync3, readFileSync2) {
8231
+ if (existsSync3 === undefined) {
8232
+ existsSync3 = fs3.existsSync;
8233
8233
  }
8234
- if (readFileSync === undefined) {
8235
- readFileSync = function(filename) {
8234
+ if (readFileSync2 === undefined) {
8235
+ readFileSync2 = function(filename) {
8236
8236
  return fs3.readFileSync(filename, "utf8");
8237
8237
  };
8238
8238
  }
8239
- if (!existsSync2(configFilePath)) {
8239
+ if (!existsSync3(configFilePath)) {
8240
8240
  return;
8241
8241
  }
8242
- var configString = readFileSync(configFilePath);
8242
+ var configString = readFileSync2(configFilePath);
8243
8243
  var cleanedJson = StripBom(configString);
8244
8244
  var config2;
8245
8245
  try {
@@ -8252,27 +8252,27 @@ If you have no idea what this means or what Pirates is, let me explain: Pirates
8252
8252
  var base = undefined;
8253
8253
  if (Array.isArray(extendedConfig)) {
8254
8254
  base = extendedConfig.reduce(function(currBase, extendedConfigElement) {
8255
- return mergeTsconfigs(currBase, loadTsconfigFromExtends(configFilePath, extendedConfigElement, existsSync2, readFileSync));
8255
+ return mergeTsconfigs(currBase, loadTsconfigFromExtends(configFilePath, extendedConfigElement, existsSync3, readFileSync2));
8256
8256
  }, {});
8257
8257
  } else {
8258
- base = loadTsconfigFromExtends(configFilePath, extendedConfig, existsSync2, readFileSync);
8258
+ base = loadTsconfigFromExtends(configFilePath, extendedConfig, existsSync3, readFileSync2);
8259
8259
  }
8260
8260
  return mergeTsconfigs(base, config2);
8261
8261
  }
8262
8262
  return config2;
8263
8263
  }
8264
8264
  exports2.loadTsconfig = loadTsconfig;
8265
- function loadTsconfigFromExtends(configFilePath, extendedConfigValue, existsSync2, readFileSync) {
8265
+ function loadTsconfigFromExtends(configFilePath, extendedConfigValue, existsSync3, readFileSync2) {
8266
8266
  var _a;
8267
8267
  if (typeof extendedConfigValue === "string" && extendedConfigValue.indexOf(".json") === -1) {
8268
8268
  extendedConfigValue += ".json";
8269
8269
  }
8270
8270
  var currentDir = path.dirname(configFilePath);
8271
8271
  var extendedConfigPath = path.join(currentDir, extendedConfigValue);
8272
- if (extendedConfigValue.indexOf("/") !== -1 && extendedConfigValue.indexOf(".") !== -1 && !existsSync2(extendedConfigPath)) {
8272
+ if (extendedConfigValue.indexOf("/") !== -1 && extendedConfigValue.indexOf(".") !== -1 && !existsSync3(extendedConfigPath)) {
8273
8273
  extendedConfigPath = path.join(currentDir, "node_modules", extendedConfigValue);
8274
8274
  }
8275
- var config2 = loadTsconfig(extendedConfigPath, existsSync2, readFileSync) || {};
8275
+ var config2 = loadTsconfig(extendedConfigPath, existsSync3, readFileSync2) || {};
8276
8276
  if ((_a = config2.compilerOptions) === null || _a === undefined ? undefined : _a.baseUrl) {
8277
8277
  var extendsDir = path.dirname(extendedConfigValue);
8278
8278
  config2.compilerOptions.baseUrl = path.join(extendsDir, config2.compilerOptions.baseUrl);
@@ -32322,7 +32322,7 @@ globstar while`, file, fr, pattern, pr2, swallowee);
32322
32322
  return new SQL2([new StringChunk2(str)]);
32323
32323
  }
32324
32324
  sql22.raw = raw2;
32325
- function join3(chunks, separator) {
32325
+ function join4(chunks, separator) {
32326
32326
  const result = [];
32327
32327
  for (const [i3, chunk] of chunks.entries()) {
32328
32328
  if (i3 > 0 && separator !== undefined) {
@@ -32332,7 +32332,7 @@ globstar while`, file, fr, pattern, pr2, swallowee);
32332
32332
  }
32333
32333
  return new SQL2(result);
32334
32334
  }
32335
- sql22.join = join3;
32335
+ sql22.join = join4;
32336
32336
  function identifier(value) {
32337
32337
  return new Name2(value);
32338
32338
  }
@@ -35162,7 +35162,7 @@ params: ${params}`);
35162
35162
  const tableName = getTableLikeName2(table62);
35163
35163
  for (const item of extractUsedTable(table62))
35164
35164
  this.usedTables.add(item);
35165
- if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
35165
+ if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
35166
35166
  throw new Error(`Alias "${tableName}" is already used in this query`);
35167
35167
  }
35168
35168
  if (!this.isPartialSelect) {
@@ -36020,7 +36020,7 @@ params: ${params}`);
36020
36020
  createJoin(joinType) {
36021
36021
  return (table62, on2) => {
36022
36022
  const tableName = getTableLikeName2(table62);
36023
- if (typeof tableName === "string" && this.config.joins.some((join3) => join3.alias === tableName)) {
36023
+ if (typeof tableName === "string" && this.config.joins.some((join4) => join4.alias === tableName)) {
36024
36024
  throw new Error(`Alias "${tableName}" is already used in this query`);
36025
36025
  }
36026
36026
  if (typeof on2 === "function") {
@@ -36066,10 +36066,10 @@ params: ${params}`);
36066
36066
  const fromFields = this.getTableLikeFields(this.config.from);
36067
36067
  fields[tableName] = fromFields;
36068
36068
  }
36069
- for (const join3 of this.config.joins) {
36070
- const tableName2 = getTableLikeName2(join3.table);
36071
- if (typeof tableName2 === "string" && !is2(join3.table, SQL2)) {
36072
- const fromFields = this.getTableLikeFields(join3.table);
36069
+ for (const join4 of this.config.joins) {
36070
+ const tableName2 = getTableLikeName2(join4.table);
36071
+ if (typeof tableName2 === "string" && !is2(join4.table, SQL2)) {
36072
+ const fromFields = this.getTableLikeFields(join4.table);
36073
36073
  fields[tableName2] = fromFields;
36074
36074
  }
36075
36075
  }
@@ -39644,7 +39644,7 @@ ORDER BY
39644
39644
  const tableName = getTableLikeName2(table62);
39645
39645
  for (const item of extractUsedTable2(table62))
39646
39646
  this.usedTables.add(item);
39647
- if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
39647
+ if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
39648
39648
  throw new Error(`Alias "${tableName}" is already used in this query`);
39649
39649
  }
39650
39650
  if (!this.isPartialSelect) {
@@ -40085,7 +40085,7 @@ ORDER BY
40085
40085
  createJoin(joinType) {
40086
40086
  return (table62, on2) => {
40087
40087
  const tableName = getTableLikeName2(table62);
40088
- if (typeof tableName === "string" && this.config.joins.some((join3) => join3.alias === tableName)) {
40088
+ if (typeof tableName === "string" && this.config.joins.some((join4) => join4.alias === tableName)) {
40089
40089
  throw new Error(`Alias "${tableName}" is already used in this query`);
40090
40090
  }
40091
40091
  if (typeof on2 === "function") {
@@ -43692,7 +43692,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
43692
43692
  const tableName = getTableLikeName2(table62);
43693
43693
  for (const item of extractUsedTable3(table62))
43694
43694
  this.usedTables.add(item);
43695
- if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
43695
+ if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
43696
43696
  throw new Error(`Alias "${tableName}" is already used in this query`);
43697
43697
  }
43698
43698
  if (!this.isPartialSelect) {
@@ -47734,7 +47734,7 @@ AND
47734
47734
  const tableName = getTableLikeName2(table62);
47735
47735
  for (const item of extractUsedTable4(table62))
47736
47736
  this.usedTables.add(item);
47737
- if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
47737
+ if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
47738
47738
  throw new Error(`Alias "${tableName}" is already used in this query`);
47739
47739
  }
47740
47740
  if (!this.isPartialSelect) {
@@ -135071,7 +135071,7 @@ function hasTimebackCredentials() {
135071
135071
  return false;
135072
135072
  }
135073
135073
  function hasTimebackFullConfig() {
135074
- return hasTimebackCredentials() && !!(config.timeback.courseId && config.timeback.studentId);
135074
+ return hasTimebackCredentials() && !!(config.timeback.courseId && config.timeback.timebackId);
135075
135075
  }
135076
135076
  function requireTimebackCredentials() {
135077
135077
  if (hasTimebackCredentials())
@@ -135125,11 +135125,36 @@ function configureTimeback(options) {
135125
135125
  config.timeback.courseId = options.courseId;
135126
135126
  process.env.SANDBOX_TIMEBACK_COURSE_ID = options.courseId;
135127
135127
  }
135128
- if (options.studentId) {
135129
- config.timeback.studentId = options.studentId;
135130
- process.env.SANDBOX_TIMEBACK_STUDENT_ID = options.studentId;
135128
+ if (options.timebackId) {
135129
+ config.timeback.timebackId = options.timebackId;
135130
+ process.env.SANDBOX_TIMEBACK_STUDENT_ID = options.timebackId;
135131
+ const isMockMode = options.timebackId === "mock";
135132
+ process.env.SANDBOX_TIMEBACK_MOCK_MODE = isMockMode ? "true" : "false";
135133
+ }
135134
+ if (options.organization) {
135135
+ config.timeback.organization = options.organization;
135136
+ process.env.SANDBOX_TIMEBACK_ORG_ID = options.organization.id;
135137
+ if (options.organization.name) {
135138
+ process.env.SANDBOX_TIMEBACK_ORG_NAME = options.organization.name;
135139
+ }
135140
+ if (options.organization.type) {
135141
+ process.env.SANDBOX_TIMEBACK_ORG_TYPE = options.organization.type;
135142
+ }
135143
+ }
135144
+ if (options.role) {
135145
+ config.timeback.role = options.role;
135146
+ process.env.SANDBOX_TIMEBACK_ROLE = options.role;
135131
135147
  }
135132
135148
  }
135149
+ function getTimebackDisplayMode() {
135150
+ const { timebackId, mode } = config.timeback;
135151
+ if (!timebackId)
135152
+ return null;
135153
+ if (timebackId === "mock" || !hasTimebackCredentials()) {
135154
+ return "mock";
135155
+ }
135156
+ return mode;
135157
+ }
135133
135158
 
135134
135159
  // src/config/mutators.ts
135135
135160
  function setEmbeddedMode(embedded) {
@@ -135152,13 +135177,203 @@ var config = {
135152
135177
  clientSecret: process.env.TIMEBACK_API_CLIENT_SECRET,
135153
135178
  authUrl: process.env.TIMEBACK_API_AUTH_URL,
135154
135179
  courseId: process.env.SANDBOX_TIMEBACK_COURSE_ID,
135155
- studentId: process.env.SANDBOX_TIMEBACK_STUDENT_ID
135180
+ timebackId: process.env.SANDBOX_TIMEBACK_STUDENT_ID
135156
135181
  }
135157
135182
  };
135158
135183
  process.env.BETTER_AUTH_SECRET = config.auth.betterAuthSecret;
135159
135184
  process.env.GAME_JWT_SECRET = config.auth.gameJwtSecret;
135160
135185
  process.env.PUBLIC_IS_LOCAL = "true";
135161
135186
 
135187
+ // src/constants/demo-users.ts
135188
+ var now = new Date;
135189
+ var DEMO_USER_IDS = {
135190
+ player: "00000000-0000-0000-0000-000000000001",
135191
+ developer: "00000000-0000-0000-0000-000000000002",
135192
+ admin: "00000000-0000-0000-0000-000000000003",
135193
+ pendingDeveloper: "00000000-0000-0000-0000-000000000004",
135194
+ unverifiedPlayer: "00000000-0000-0000-0000-000000000005"
135195
+ };
135196
+ var DEMO_USERS = {
135197
+ admin: {
135198
+ id: DEMO_USER_IDS.admin,
135199
+ name: "Admin User",
135200
+ username: "admin_user",
135201
+ email: "admin@playcademy.com",
135202
+ emailVerified: true,
135203
+ image: null,
135204
+ role: "admin",
135205
+ developerStatus: "approved",
135206
+ createdAt: now,
135207
+ updatedAt: now
135208
+ },
135209
+ player: {
135210
+ id: DEMO_USER_IDS.player,
135211
+ name: "Player User",
135212
+ username: "player_user",
135213
+ email: "player@playcademy.com",
135214
+ emailVerified: true,
135215
+ image: null,
135216
+ role: "player",
135217
+ developerStatus: "none",
135218
+ createdAt: now,
135219
+ updatedAt: now
135220
+ },
135221
+ developer: {
135222
+ id: DEMO_USER_IDS.developer,
135223
+ name: "Developer User",
135224
+ username: "developer_user",
135225
+ email: "developer@playcademy.com",
135226
+ emailVerified: true,
135227
+ image: null,
135228
+ role: "developer",
135229
+ developerStatus: "approved",
135230
+ createdAt: now,
135231
+ updatedAt: now
135232
+ },
135233
+ pendingDeveloper: {
135234
+ id: DEMO_USER_IDS.pendingDeveloper,
135235
+ name: "Pending Developer",
135236
+ username: "pending_dev",
135237
+ email: "pending@playcademy.com",
135238
+ emailVerified: true,
135239
+ image: null,
135240
+ role: "developer",
135241
+ developerStatus: "pending",
135242
+ createdAt: now,
135243
+ updatedAt: now
135244
+ },
135245
+ unverifiedPlayer: {
135246
+ id: DEMO_USER_IDS.unverifiedPlayer,
135247
+ name: "Unverified Player",
135248
+ username: "unverified_player",
135249
+ email: "unverified@playcademy.com",
135250
+ emailVerified: false,
135251
+ image: null,
135252
+ role: "player",
135253
+ developerStatus: "none",
135254
+ createdAt: now,
135255
+ updatedAt: now
135256
+ }
135257
+ };
135258
+ var DEMO_USER = DEMO_USERS.player;
135259
+ // src/constants/demo-tokens.ts
135260
+ var DEMO_TOKENS = {
135261
+ "sandbox-demo-token": DEMO_USERS.player,
135262
+ "sandbox-admin-token": DEMO_USERS.admin,
135263
+ "sandbox-player-token": DEMO_USERS.player,
135264
+ "sandbox-developer-token": DEMO_USERS.developer,
135265
+ "sandbox-pending-dev-token": DEMO_USERS.pendingDeveloper,
135266
+ "sandbox-unverified-token": DEMO_USERS.unverifiedPlayer,
135267
+ "mock-game-token-for-local-dev": DEMO_USERS.player
135268
+ };
135269
+ var DEMO_TOKEN = "sandbox-demo-token";
135270
+ var MOCK_GAME_ID = "mock-game-id-from-template";
135271
+ // src/constants/demo-items.ts
135272
+ var DEMO_ITEM_IDS = {
135273
+ playcademyCredits: "10000000-0000-0000-0000-000000000001",
135274
+ foundingMemberBadge: "10000000-0000-0000-0000-000000000002",
135275
+ earlyAdopterBadge: "10000000-0000-0000-0000-000000000003",
135276
+ firstGameBadge: "10000000-0000-0000-0000-000000000004",
135277
+ commonSword: "10000000-0000-0000-0000-000000000005",
135278
+ smallHealthPotion: "10000000-0000-0000-0000-000000000006",
135279
+ smallBackpack: "10000000-0000-0000-0000-000000000007"
135280
+ };
135281
+ var PLAYCADEMY_CREDITS_ID = DEMO_ITEM_IDS.playcademyCredits;
135282
+ var SAMPLE_ITEMS = [
135283
+ {
135284
+ id: PLAYCADEMY_CREDITS_ID,
135285
+ slug: "PLAYCADEMY_CREDITS",
135286
+ gameId: null,
135287
+ displayName: "PLAYCADEMY credits",
135288
+ description: "The main currency used across PLAYCADEMY.",
135289
+ type: "currency",
135290
+ isPlaceable: false,
135291
+ imageUrl: "http://playcademy-sandbox.local/playcademy-credit.png",
135292
+ metadata: {
135293
+ rarity: "common"
135294
+ }
135295
+ },
135296
+ {
135297
+ id: DEMO_ITEM_IDS.foundingMemberBadge,
135298
+ slug: "FOUNDING_MEMBER_BADGE",
135299
+ gameId: null,
135300
+ displayName: "Founding Member Badge",
135301
+ description: "Reserved for founding core team of the PLAYCADEMY platform.",
135302
+ type: "badge",
135303
+ isPlaceable: false,
135304
+ imageUrl: null,
135305
+ metadata: {
135306
+ rarity: "legendary"
135307
+ }
135308
+ },
135309
+ {
135310
+ id: DEMO_ITEM_IDS.earlyAdopterBadge,
135311
+ slug: "EARLY_ADOPTER_BADGE",
135312
+ gameId: null,
135313
+ displayName: "Early Adopter Badge",
135314
+ description: "Awarded to users who joined during the beta phase.",
135315
+ type: "badge",
135316
+ isPlaceable: false,
135317
+ imageUrl: null,
135318
+ metadata: {
135319
+ rarity: "epic"
135320
+ }
135321
+ },
135322
+ {
135323
+ id: DEMO_ITEM_IDS.firstGameBadge,
135324
+ slug: "FIRST_GAME_BADGE",
135325
+ gameId: null,
135326
+ displayName: "First Game Played",
135327
+ description: "Awarded for playing your first game in the Playcademy platform.",
135328
+ type: "badge",
135329
+ isPlaceable: false,
135330
+ imageUrl: "http://playcademy-sandbox.local/first-game-badge.png",
135331
+ metadata: {
135332
+ rarity: "uncommon"
135333
+ }
135334
+ },
135335
+ {
135336
+ id: DEMO_ITEM_IDS.commonSword,
135337
+ slug: "COMMON_SWORD",
135338
+ gameId: null,
135339
+ displayName: "Common Sword",
135340
+ description: "A basic sword, good for beginners.",
135341
+ type: "unlock",
135342
+ isPlaceable: false,
135343
+ imageUrl: "http://playcademy-sandbox.local/common-sword.png",
135344
+ metadata: undefined
135345
+ },
135346
+ {
135347
+ id: DEMO_ITEM_IDS.smallHealthPotion,
135348
+ slug: "SMALL_HEALTH_POTION",
135349
+ gameId: null,
135350
+ displayName: "Small Health Potion",
135351
+ description: "Restores a small amount of health.",
135352
+ type: "other",
135353
+ isPlaceable: false,
135354
+ imageUrl: "http://playcademy-sandbox.local/small-health-potion.png",
135355
+ metadata: undefined
135356
+ },
135357
+ {
135358
+ id: DEMO_ITEM_IDS.smallBackpack,
135359
+ slug: "SMALL_BACKPACK",
135360
+ gameId: null,
135361
+ displayName: "Small Backpack",
135362
+ description: "Increases your inventory capacity by 5 slots.",
135363
+ type: "upgrade",
135364
+ isPlaceable: false,
135365
+ imageUrl: "http://playcademy-sandbox.local/small-backpack.png",
135366
+ metadata: undefined
135367
+ }
135368
+ ];
135369
+ var SAMPLE_INVENTORY = [
135370
+ {
135371
+ id: "20000000-0000-0000-0000-000000000001",
135372
+ userId: DEMO_USER.id,
135373
+ itemId: PLAYCADEMY_CREDITS_ID,
135374
+ quantity: 1000
135375
+ }
135376
+ ];
135162
135377
  // ../../node_modules/@hono/node-server/dist/index.mjs
135163
135378
  import { createServer as createServerHTTP } from "http";
135164
135379
  import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
@@ -135700,10 +135915,106 @@ var serve = (options, listeningListener) => {
135700
135915
  });
135701
135916
  return server;
135702
135917
  };
135918
+
135919
+ // ../utils/src/port.ts
135920
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
135921
+ import { createServer } from "node:net";
135922
+ import { homedir } from "node:os";
135923
+ import { join } from "node:path";
135924
+ function getRegistryPath() {
135925
+ const home = homedir();
135926
+ const dir = join(home, ".playcademy");
135927
+ if (!existsSync(dir)) {
135928
+ mkdirSync(dir, { recursive: true });
135929
+ }
135930
+ return join(dir, ".proc");
135931
+ }
135932
+ function readRegistry() {
135933
+ const registryPath = getRegistryPath();
135934
+ if (!existsSync(registryPath)) {
135935
+ return {};
135936
+ }
135937
+ try {
135938
+ const content = readFileSync(registryPath, "utf-8");
135939
+ return JSON.parse(content);
135940
+ } catch {
135941
+ return {};
135942
+ }
135943
+ }
135944
+ function writeRegistry(registry) {
135945
+ const registryPath = getRegistryPath();
135946
+ writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf-8");
135947
+ }
135948
+ function getServerKey(type, port) {
135949
+ return `${type}-${port}`;
135950
+ }
135951
+ function writeServerInfo(type, info2) {
135952
+ const registry = readRegistry();
135953
+ const key = getServerKey(type, info2.port);
135954
+ registry[key] = info2;
135955
+ writeRegistry(registry);
135956
+ }
135957
+ function cleanupServerInfo(type, projectRoot, pid) {
135958
+ const registry = readRegistry();
135959
+ const keysToRemove = [];
135960
+ for (const [key, info2] of Object.entries(registry)) {
135961
+ if (key.startsWith(`${type}-`)) {
135962
+ let matches = true;
135963
+ if (projectRoot && info2.projectRoot !== projectRoot) {
135964
+ matches = false;
135965
+ }
135966
+ if (pid !== undefined && info2.pid !== pid) {
135967
+ matches = false;
135968
+ }
135969
+ if (matches) {
135970
+ keysToRemove.push(key);
135971
+ }
135972
+ }
135973
+ }
135974
+ for (const key of keysToRemove) {
135975
+ delete registry[key];
135976
+ }
135977
+ if (keysToRemove.length > 0) {
135978
+ writeRegistry(registry);
135979
+ }
135980
+ }
135981
+ async function isPortInUse(port) {
135982
+ return new Promise((resolve) => {
135983
+ const server = createServer();
135984
+ server.once("error", () => {
135985
+ resolve(true);
135986
+ });
135987
+ server.once("listening", () => {
135988
+ server.close();
135989
+ resolve(false);
135990
+ });
135991
+ server.listen(port);
135992
+ });
135993
+ }
135994
+ async function waitForPort(port, timeoutMs = 5000) {
135995
+ const start2 = Date.now();
135996
+ while (await isPortInUse(port)) {
135997
+ if (Date.now() - start2 > timeoutMs) {
135998
+ throw new Error(`Port ${port} is already in use.
135999
+ ` + `Stop the other server or specify a different port with --port <number>.`);
136000
+ }
136001
+ await new Promise((resolve) => setTimeout(resolve, 100));
136002
+ }
136003
+ }
136004
+ async function requirePortAvailable(port, timeoutMs = 100) {
136005
+ const start2 = Date.now();
136006
+ while (await isPortInUse(port)) {
136007
+ if (Date.now() - start2 > timeoutMs) {
136008
+ throw new Error(`Port ${port} is already in use.
136009
+ ` + `Stop the other server or specify a different port with --port <number>.`);
136010
+ }
136011
+ await new Promise((resolve) => setTimeout(resolve, 50));
136012
+ }
136013
+ }
135703
136014
  // package.json
135704
136015
  var package_default = {
135705
136016
  name: "@playcademy/sandbox",
135706
- version: "0.2.3",
136017
+ version: "0.3.0",
135707
136018
  description: "Local development server for Playcademy game development",
135708
136019
  type: "module",
135709
136020
  exports: {
@@ -135718,6 +136029,10 @@ var package_default = {
135718
136029
  "./config": {
135719
136030
  import: "./dist/config.js",
135720
136031
  types: "./dist/config.d.ts"
136032
+ },
136033
+ "./constants": {
136034
+ import: "./dist/constants.js",
136035
+ types: "./dist/constants.d.ts"
135721
136036
  }
135722
136037
  },
135723
136038
  bin: {
@@ -138213,7 +138528,7 @@ function sql(strings, ...params) {
138213
138528
  return new SQL([new StringChunk(str)]);
138214
138529
  }
138215
138530
  sql2.raw = raw2;
138216
- function join(chunks, separator) {
138531
+ function join2(chunks, separator) {
138217
138532
  const result = [];
138218
138533
  for (const [i2, chunk] of chunks.entries()) {
138219
138534
  if (i2 > 0 && separator !== undefined) {
@@ -138223,7 +138538,7 @@ function sql(strings, ...params) {
138223
138538
  }
138224
138539
  return new SQL(result);
138225
138540
  }
138226
- sql2.join = join;
138541
+ sql2.join = join2;
138227
138542
  function identifier(value) {
138228
138543
  return new Name(value);
138229
138544
  }
@@ -141200,7 +141515,7 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
141200
141515
  return (table, on) => {
141201
141516
  const baseTableName = this.tableName;
141202
141517
  const tableName = getTableLikeName(table);
141203
- if (typeof tableName === "string" && this.config.joins?.some((join) => join.alias === tableName)) {
141518
+ if (typeof tableName === "string" && this.config.joins?.some((join2) => join2.alias === tableName)) {
141204
141519
  throw new Error(`Alias "${tableName}" is already used in this query`);
141205
141520
  }
141206
141521
  if (!this.isPartialSelect) {
@@ -141709,7 +142024,7 @@ class PgUpdateBase extends QueryPromise {
141709
142024
  createJoin(joinType) {
141710
142025
  return (table, on) => {
141711
142026
  const tableName = getTableLikeName(table);
141712
- if (typeof tableName === "string" && this.config.joins.some((join) => join.alias === tableName)) {
142027
+ if (typeof tableName === "string" && this.config.joins.some((join2) => join2.alias === tableName)) {
141713
142028
  throw new Error(`Alias "${tableName}" is already used in this query`);
141714
142029
  }
141715
142030
  if (typeof on === "function") {
@@ -141759,10 +142074,10 @@ class PgUpdateBase extends QueryPromise {
141759
142074
  const fromFields = this.getTableLikeFields(this.config.from);
141760
142075
  fields[tableName] = fromFields;
141761
142076
  }
141762
- for (const join of this.config.joins) {
141763
- const tableName2 = getTableLikeName(join.table);
141764
- if (typeof tableName2 === "string" && !is(join.table, SQL)) {
141765
- const fromFields = this.getTableLikeFields(join.table);
142077
+ for (const join2 of this.config.joins) {
142078
+ const tableName2 = getTableLikeName(join2.table);
142079
+ if (typeof tableName2 === "string" && !is(join2.table, SQL)) {
142080
+ const fromFields = this.getTableLikeFields(join2.table);
141766
142081
  fields[tableName2] = fromFields;
141767
142082
  }
141768
142083
  }
@@ -142905,195 +143220,6 @@ var notificationsRelations = relations(notifications, ({ one }) => ({
142905
143220
  references: [users.id]
142906
143221
  })
142907
143222
  }));
142908
- // src/constants/demo-users.ts
142909
- var now = new Date;
142910
- var DEMO_USER_IDS = {
142911
- admin: "00000000-0000-0000-0000-000000000001",
142912
- player: "00000000-0000-0000-0000-000000000002",
142913
- developer: "00000000-0000-0000-0000-000000000003",
142914
- pendingDeveloper: "00000000-0000-0000-0000-000000000004",
142915
- unverifiedPlayer: "00000000-0000-0000-0000-000000000005"
142916
- };
142917
- var DEMO_USERS = {
142918
- admin: {
142919
- id: DEMO_USER_IDS.admin,
142920
- name: "Admin User",
142921
- username: "admin_user",
142922
- email: "admin@playcademy.com",
142923
- emailVerified: true,
142924
- image: null,
142925
- role: "admin",
142926
- developerStatus: "approved",
142927
- createdAt: now,
142928
- updatedAt: now
142929
- },
142930
- player: {
142931
- id: DEMO_USER_IDS.player,
142932
- name: "Player User",
142933
- username: "player_user",
142934
- email: "player@playcademy.com",
142935
- emailVerified: true,
142936
- image: null,
142937
- role: "player",
142938
- developerStatus: "none",
142939
- createdAt: now,
142940
- updatedAt: now
142941
- },
142942
- developer: {
142943
- id: DEMO_USER_IDS.developer,
142944
- name: "Developer User",
142945
- username: "developer_user",
142946
- email: "developer@playcademy.com",
142947
- emailVerified: true,
142948
- image: null,
142949
- role: "developer",
142950
- developerStatus: "approved",
142951
- createdAt: now,
142952
- updatedAt: now
142953
- },
142954
- pendingDeveloper: {
142955
- id: DEMO_USER_IDS.pendingDeveloper,
142956
- name: "Pending Developer",
142957
- username: "pending_dev",
142958
- email: "pending@playcademy.com",
142959
- emailVerified: true,
142960
- image: null,
142961
- role: "developer",
142962
- developerStatus: "pending",
142963
- createdAt: now,
142964
- updatedAt: now
142965
- },
142966
- unverifiedPlayer: {
142967
- id: DEMO_USER_IDS.unverifiedPlayer,
142968
- name: "Unverified Player",
142969
- username: "unverified_player",
142970
- email: "unverified@playcademy.com",
142971
- emailVerified: false,
142972
- image: null,
142973
- role: "player",
142974
- developerStatus: "none",
142975
- createdAt: now,
142976
- updatedAt: now
142977
- }
142978
- };
142979
- var DEMO_USER = DEMO_USERS.admin;
142980
- // src/constants/demo-tokens.ts
142981
- var DEMO_TOKENS = {
142982
- "sandbox-demo-token": DEMO_USERS.admin,
142983
- "sandbox-admin-token": DEMO_USERS.admin,
142984
- "sandbox-player-token": DEMO_USERS.player,
142985
- "sandbox-developer-token": DEMO_USERS.developer,
142986
- "sandbox-pending-dev-token": DEMO_USERS.pendingDeveloper,
142987
- "sandbox-unverified-token": DEMO_USERS.unverifiedPlayer,
142988
- "mock-game-token-for-local-dev": DEMO_USERS.admin
142989
- };
142990
- var MOCK_GAME_ID = "mock-game-id-from-template";
142991
- // src/constants/demo-items.ts
142992
- var DEMO_ITEM_IDS = {
142993
- playcademyCredits: "10000000-0000-0000-0000-000000000001",
142994
- foundingMemberBadge: "10000000-0000-0000-0000-000000000002",
142995
- earlyAdopterBadge: "10000000-0000-0000-0000-000000000003",
142996
- firstGameBadge: "10000000-0000-0000-0000-000000000004",
142997
- commonSword: "10000000-0000-0000-0000-000000000005",
142998
- smallHealthPotion: "10000000-0000-0000-0000-000000000006",
142999
- smallBackpack: "10000000-0000-0000-0000-000000000007"
143000
- };
143001
- var PLAYCADEMY_CREDITS_ID = DEMO_ITEM_IDS.playcademyCredits;
143002
- var SAMPLE_ITEMS = [
143003
- {
143004
- id: PLAYCADEMY_CREDITS_ID,
143005
- slug: "PLAYCADEMY_CREDITS",
143006
- gameId: null,
143007
- displayName: "PLAYCADEMY credits",
143008
- description: "The main currency used across PLAYCADEMY.",
143009
- type: "currency",
143010
- isPlaceable: false,
143011
- imageUrl: "http://playcademy-sandbox.local/playcademy-credit.png",
143012
- metadata: {
143013
- rarity: "common"
143014
- }
143015
- },
143016
- {
143017
- id: DEMO_ITEM_IDS.foundingMemberBadge,
143018
- slug: "FOUNDING_MEMBER_BADGE",
143019
- gameId: null,
143020
- displayName: "Founding Member Badge",
143021
- description: "Reserved for founding core team of the PLAYCADEMY platform.",
143022
- type: "badge",
143023
- isPlaceable: false,
143024
- imageUrl: null,
143025
- metadata: {
143026
- rarity: "legendary"
143027
- }
143028
- },
143029
- {
143030
- id: DEMO_ITEM_IDS.earlyAdopterBadge,
143031
- slug: "EARLY_ADOPTER_BADGE",
143032
- gameId: null,
143033
- displayName: "Early Adopter Badge",
143034
- description: "Awarded to users who joined during the beta phase.",
143035
- type: "badge",
143036
- isPlaceable: false,
143037
- imageUrl: null,
143038
- metadata: {
143039
- rarity: "epic"
143040
- }
143041
- },
143042
- {
143043
- id: DEMO_ITEM_IDS.firstGameBadge,
143044
- slug: "FIRST_GAME_BADGE",
143045
- gameId: null,
143046
- displayName: "First Game Played",
143047
- description: "Awarded for playing your first game in the Playcademy platform.",
143048
- type: "badge",
143049
- isPlaceable: false,
143050
- imageUrl: "http://playcademy-sandbox.local/first-game-badge.png",
143051
- metadata: {
143052
- rarity: "uncommon"
143053
- }
143054
- },
143055
- {
143056
- id: DEMO_ITEM_IDS.commonSword,
143057
- slug: "COMMON_SWORD",
143058
- gameId: null,
143059
- displayName: "Common Sword",
143060
- description: "A basic sword, good for beginners.",
143061
- type: "unlock",
143062
- isPlaceable: false,
143063
- imageUrl: "http://playcademy-sandbox.local/common-sword.png",
143064
- metadata: undefined
143065
- },
143066
- {
143067
- id: DEMO_ITEM_IDS.smallHealthPotion,
143068
- slug: "SMALL_HEALTH_POTION",
143069
- gameId: null,
143070
- displayName: "Small Health Potion",
143071
- description: "Restores a small amount of health.",
143072
- type: "other",
143073
- isPlaceable: false,
143074
- imageUrl: "http://playcademy-sandbox.local/small-health-potion.png",
143075
- metadata: undefined
143076
- },
143077
- {
143078
- id: DEMO_ITEM_IDS.smallBackpack,
143079
- slug: "SMALL_BACKPACK",
143080
- gameId: null,
143081
- displayName: "Small Backpack",
143082
- description: "Increases your inventory capacity by 5 slots.",
143083
- type: "upgrade",
143084
- isPlaceable: false,
143085
- imageUrl: "http://playcademy-sandbox.local/small-backpack.png",
143086
- metadata: undefined
143087
- }
143088
- ];
143089
- var SAMPLE_INVENTORY = [
143090
- {
143091
- id: "20000000-0000-0000-0000-000000000001",
143092
- userId: DEMO_USER.id,
143093
- itemId: PLAYCADEMY_CREDITS_ID,
143094
- quantity: 1000
143095
- }
143096
- ];
143097
143223
  // src/server/auth.ts
143098
143224
  function extractBearerToken(authHeader) {
143099
143225
  if (!authHeader?.startsWith("Bearer ")) {
@@ -143101,25 +143227,53 @@ function extractBearerToken(authHeader) {
143101
143227
  }
143102
143228
  return authHeader.substring(7);
143103
143229
  }
143104
- function parseJwtUserId(token) {
143230
+ function parseSandboxToken(token) {
143231
+ try {
143232
+ const parts2 = token.split(".");
143233
+ if (parts2.length !== 3 || parts2[2] !== "sandbox") {
143234
+ return null;
143235
+ }
143236
+ const header = JSON.parse(atob(parts2[0]));
143237
+ if (header.typ !== "sandbox") {
143238
+ return null;
143239
+ }
143240
+ const payload = JSON.parse(atob(parts2[1]));
143241
+ if (!payload.uid || !payload.sub) {
143242
+ return null;
143243
+ }
143244
+ return {
143245
+ userId: payload.uid,
143246
+ gameSlug: payload.sub
143247
+ };
143248
+ } catch {
143249
+ return null;
143250
+ }
143251
+ }
143252
+ function parseJwtClaims(token) {
143105
143253
  try {
143106
143254
  const parts2 = token.split(".");
143107
143255
  if (parts2.length === 3 && parts2[1]) {
143108
143256
  const payload = JSON.parse(atob(parts2[1]));
143109
- return payload.uid || null;
143257
+ if (payload.uid) {
143258
+ return {
143259
+ userId: payload.uid,
143260
+ gameId: payload.sub
143261
+ };
143262
+ }
143110
143263
  }
143111
- } catch (error) {
143112
- console.warn("[Auth] Failed to decode JWT token:", error);
143113
- }
143264
+ } catch {}
143114
143265
  return null;
143115
143266
  }
143116
- function resolveUserId(token) {
143267
+ function resolveAuth(token) {
143117
143268
  const demoUser = DEMO_TOKENS[token];
143118
- if (demoUser) {
143119
- return demoUser.id;
143120
- }
143269
+ if (demoUser)
143270
+ return { userId: demoUser.id };
143121
143271
  if (token.includes(".")) {
143122
- return parseJwtUserId(token);
143272
+ const sandboxClaims = parseSandboxToken(token);
143273
+ if (sandboxClaims) {
143274
+ return sandboxClaims;
143275
+ }
143276
+ return parseJwtClaims(token);
143123
143277
  }
143124
143278
  return null;
143125
143279
  }
@@ -143134,6 +143288,18 @@ async function fetchUserFromDatabase(db, userId) {
143134
143288
  throw error;
143135
143289
  }
143136
143290
  }
143291
+ async function resolveGameIdFromSlug(db, slug) {
143292
+ try {
143293
+ const game = await db.query.games.findFirst({
143294
+ where: eq(games.slug, slug),
143295
+ columns: { id: true }
143296
+ });
143297
+ return game?.id || null;
143298
+ } catch (error) {
143299
+ console.error("[Auth] Error looking up game by slug:", error);
143300
+ return null;
143301
+ }
143302
+ }
143137
143303
  function isPublicRoute(path, exceptions) {
143138
143304
  return exceptions.some((exception) => {
143139
143305
  if (path === exception)
@@ -143155,11 +143321,11 @@ async function authenticateRequest(c) {
143155
143321
  shouldReturn404: true
143156
143322
  };
143157
143323
  }
143158
- let targetUserId;
143324
+ let claims;
143159
143325
  if (apiKey && !bearerToken) {
143160
- targetUserId = DEMO_USERS.admin.id;
143326
+ claims = { userId: DEMO_USERS.admin.id };
143161
143327
  } else {
143162
- const resolved = resolveUserId(token);
143328
+ const resolved = resolveAuth(token);
143163
143329
  if (!resolved) {
143164
143330
  return {
143165
143331
  success: false,
@@ -143167,26 +143333,22 @@ async function authenticateRequest(c) {
143167
143333
  shouldReturn404: true
143168
143334
  };
143169
143335
  }
143170
- targetUserId = resolved;
143336
+ claims = resolved;
143171
143337
  }
143172
143338
  const db = c.get("db");
143173
143339
  if (!db) {
143174
143340
  console.error("[Auth] Database not available in context");
143175
- return {
143176
- success: false,
143177
- error: "Internal server error",
143178
- shouldReturn404: false
143179
- };
143341
+ return { success: false, error: "Internal server error", shouldReturn404: false };
143180
143342
  }
143181
- const user = await fetchUserFromDatabase(db, targetUserId);
143343
+ const user = await fetchUserFromDatabase(db, claims.userId);
143182
143344
  if (!user) {
143183
- return {
143184
- success: false,
143185
- error: "User not found or token invalid",
143186
- shouldReturn404: true
143187
- };
143345
+ return { success: false, error: "User not found or token invalid", shouldReturn404: true };
143188
143346
  }
143189
- return { success: true, user };
143347
+ let gameId = claims.gameId;
143348
+ if (!gameId && claims.gameSlug) {
143349
+ gameId = await resolveGameIdFromSlug(db, claims.gameSlug) ?? undefined;
143350
+ }
143351
+ return { success: true, user, gameId };
143190
143352
  }
143191
143353
  function setupAuth(options = {}) {
143192
143354
  const { exceptions = [] } = options;
@@ -143198,6 +143360,8 @@ function setupAuth(options = {}) {
143198
143360
  const result = await authenticateRequest(c);
143199
143361
  if (result.success) {
143200
143362
  c.set("user", result.user);
143363
+ if (result.gameId)
143364
+ c.set("gameId", result.gameId);
143201
143365
  await next();
143202
143366
  return;
143203
143367
  }
@@ -149059,34 +149223,34 @@ function getDatabase() {
149059
149223
 
149060
149224
  // src/database/path-manager.ts
149061
149225
  import fs2 from "node:fs";
149062
- import { dirname, isAbsolute, join as join2 } from "node:path";
149226
+ import { dirname, isAbsolute, join as join3 } from "node:path";
149063
149227
 
149064
149228
  class DatabasePathManager {
149065
- static DEFAULT_DB_SUBPATH = join2("@playcademy", "vite-plugin", "node_modules", ".playcademy", "sandbox.db");
149229
+ static DEFAULT_DB_SUBPATH = join3("@playcademy", "vite-plugin", "node_modules", ".playcademy", "sandbox.db");
149066
149230
  static findNodeModulesPath() {
149067
149231
  let currentDir = process.cwd();
149068
149232
  while (currentDir !== dirname(currentDir)) {
149069
- const nodeModulesPath = join2(currentDir, "node_modules");
149233
+ const nodeModulesPath = join3(currentDir, "node_modules");
149070
149234
  if (fs2.existsSync(nodeModulesPath)) {
149071
149235
  return nodeModulesPath;
149072
149236
  }
149073
149237
  currentDir = dirname(currentDir);
149074
149238
  }
149075
- return join2(process.cwd(), "node_modules");
149239
+ return join3(process.cwd(), "node_modules");
149076
149240
  }
149077
149241
  static resolveDatabasePath(customPath) {
149078
149242
  if (customPath) {
149079
149243
  if (customPath === ":memory:")
149080
149244
  return ":memory:";
149081
- return isAbsolute(customPath) ? customPath : join2(process.cwd(), customPath);
149245
+ return isAbsolute(customPath) ? customPath : join3(process.cwd(), customPath);
149082
149246
  }
149083
- return join2(this.findNodeModulesPath(), this.DEFAULT_DB_SUBPATH);
149247
+ return join3(this.findNodeModulesPath(), this.DEFAULT_DB_SUBPATH);
149084
149248
  }
149085
149249
  static ensureDatabaseDirectory(dbPath) {
149086
149250
  if (dbPath === ":memory:")
149087
149251
  return;
149088
149252
  const dirPath = dirname(dbPath);
149089
- const absolutePath = isAbsolute(dirPath) ? dirPath : join2(process.cwd(), dirPath);
149253
+ const absolutePath = isAbsolute(dirPath) ? dirPath : join3(process.cwd(), dirPath);
149090
149254
  try {
149091
149255
  if (!fs2.existsSync(absolutePath)) {
149092
149256
  fs2.mkdirSync(absolutePath, { recursive: true });
@@ -149413,7 +149577,51 @@ var init_overworld = __esm3(() => {
149413
149577
  FIRST_GAME: ITEM_SLUGS2.FIRST_GAME_BADGE
149414
149578
  };
149415
149579
  });
149416
- var init_timeback = () => {};
149580
+ var TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY";
149581
+ var TIMEBACK_ORG_NAME = "Playcademy Studios";
149582
+ var TIMEBACK_ORG_TYPE = "department";
149583
+ var TIMEBACK_COURSE_DEFAULTS;
149584
+ var TIMEBACK_RESOURCE_DEFAULTS;
149585
+ var TIMEBACK_COMPONENT_DEFAULTS;
149586
+ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS;
149587
+ var init_timeback = __esm3(() => {
149588
+ TIMEBACK_COURSE_DEFAULTS = {
149589
+ gradingScheme: "STANDARD",
149590
+ level: {
149591
+ elementary: "Elementary",
149592
+ middle: "Middle",
149593
+ high: "High",
149594
+ ap: "AP"
149595
+ },
149596
+ goals: {
149597
+ dailyXp: 50,
149598
+ dailyLessons: 3
149599
+ },
149600
+ metrics: {
149601
+ totalXp: 1000,
149602
+ totalLessons: 50
149603
+ }
149604
+ };
149605
+ TIMEBACK_RESOURCE_DEFAULTS = {
149606
+ vendorId: "playcademy",
149607
+ roles: ["primary"],
149608
+ importance: "primary",
149609
+ metadata: {
149610
+ type: "interactive",
149611
+ toolProvider: "Playcademy",
149612
+ instructionalMethod: "exploratory",
149613
+ language: "en-US"
149614
+ }
149615
+ };
149616
+ TIMEBACK_COMPONENT_DEFAULTS = {
149617
+ sortOrder: 1,
149618
+ prerequisiteCriteria: "ALL"
149619
+ };
149620
+ TIMEBACK_COMPONENT_RESOURCE_DEFAULTS = {
149621
+ sortOrder: 1,
149622
+ lessonType: "quiz"
149623
+ };
149624
+ });
149417
149625
  var init_workers = () => {};
149418
149626
  var init_src2 = __esm3(() => {
149419
149627
  init_auth();
@@ -149454,7 +149662,6 @@ var HTTP_DEFAULTS;
149454
149662
  var AUTH_DEFAULTS;
149455
149663
  var CACHE_DEFAULTS;
149456
149664
  var CONFIG_DEFAULTS;
149457
- var DEFAULT_PLAYCADEMY_ORGANIZATION_ID;
149458
149665
  var PLAYCADEMY_DEFAULTS;
149459
149666
  var RESOURCE_DEFAULTS;
149460
149667
  var HTTP_STATUS;
@@ -149601,54 +149808,26 @@ var init_constants = __esm3(() => {
149601
149808
  CONFIG_DEFAULTS = {
149602
149809
  fileNames: ["timeback.config.js", "timeback.config.json"]
149603
149810
  };
149604
- DEFAULT_PLAYCADEMY_ORGANIZATION_ID = process.env.TIMEBACK_ORG_SOURCE_ID || "PLAYCADEMY";
149605
149811
  PLAYCADEMY_DEFAULTS = {
149606
- organization: DEFAULT_PLAYCADEMY_ORGANIZATION_ID,
149812
+ organization: TIMEBACK_ORG_SOURCED_ID,
149607
149813
  launchBaseUrls: PLAYCADEMY_BASE_URLS
149608
149814
  };
149609
149815
  RESOURCE_DEFAULTS = {
149610
149816
  organization: {
149611
- name: "Playcademy Studios",
149612
- type: "department"
149817
+ name: TIMEBACK_ORG_NAME,
149818
+ type: TIMEBACK_ORG_TYPE
149613
149819
  },
149614
149820
  course: {
149615
- gradingScheme: "STANDARD",
149616
- level: {
149617
- elementary: "Elementary",
149618
- middle: "Middle",
149619
- high: "High",
149620
- ap: "AP"
149621
- },
149821
+ gradingScheme: TIMEBACK_COURSE_DEFAULTS.gradingScheme,
149822
+ level: TIMEBACK_COURSE_DEFAULTS.level,
149622
149823
  metadata: {
149623
- goals: {
149624
- dailyXp: 50,
149625
- dailyLessons: 3
149626
- },
149627
- metrics: {
149628
- totalXp: 1000,
149629
- totalLessons: 50
149630
- }
149824
+ goals: TIMEBACK_COURSE_DEFAULTS.goals,
149825
+ metrics: TIMEBACK_COURSE_DEFAULTS.metrics
149631
149826
  }
149632
149827
  },
149633
- component: {
149634
- sortOrder: 1,
149635
- prerequisiteCriteria: "ALL"
149636
- },
149637
- resource: {
149638
- vendorId: "playcademy",
149639
- roles: ["primary"],
149640
- importance: "primary",
149641
- metadata: {
149642
- type: "interactive",
149643
- toolProvider: "Playcademy",
149644
- instructionalMethod: "exploratory",
149645
- language: "en-US"
149646
- }
149647
- },
149648
- componentResource: {
149649
- sortOrder: 1,
149650
- lessonType: "quiz"
149651
- }
149828
+ component: TIMEBACK_COMPONENT_DEFAULTS,
149829
+ resource: TIMEBACK_RESOURCE_DEFAULTS,
149830
+ componentResource: TIMEBACK_COMPONENT_RESOURCE_DEFAULTS
149652
149831
  };
149653
149832
  HTTP_STATUS = {
149654
149833
  CLIENT_ERROR_MIN: 400,
@@ -155656,7 +155835,8 @@ class TimebackClient {
155656
155835
  courseId: enrollment.course.id,
155657
155836
  status: "active",
155658
155837
  grades,
155659
- subjects
155838
+ subjects,
155839
+ school: enrollment.school
155660
155840
  };
155661
155841
  });
155662
155842
  this.cacheManager.setEnrollments(studentId, enrollments);
@@ -155729,7 +155909,51 @@ var init_overworld2 = __esm4(() => {
155729
155909
  FIRST_GAME: ITEM_SLUGS3.FIRST_GAME_BADGE
155730
155910
  };
155731
155911
  });
155732
- var init_timeback2 = () => {};
155912
+ var TIMEBACK_ORG_SOURCED_ID2 = "PLAYCADEMY";
155913
+ var TIMEBACK_ORG_NAME2 = "Playcademy Studios";
155914
+ var TIMEBACK_ORG_TYPE2 = "department";
155915
+ var TIMEBACK_COURSE_DEFAULTS2;
155916
+ var TIMEBACK_RESOURCE_DEFAULTS2;
155917
+ var TIMEBACK_COMPONENT_DEFAULTS2;
155918
+ var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2;
155919
+ var init_timeback2 = __esm4(() => {
155920
+ TIMEBACK_COURSE_DEFAULTS2 = {
155921
+ gradingScheme: "STANDARD",
155922
+ level: {
155923
+ elementary: "Elementary",
155924
+ middle: "Middle",
155925
+ high: "High",
155926
+ ap: "AP"
155927
+ },
155928
+ goals: {
155929
+ dailyXp: 50,
155930
+ dailyLessons: 3
155931
+ },
155932
+ metrics: {
155933
+ totalXp: 1000,
155934
+ totalLessons: 50
155935
+ }
155936
+ };
155937
+ TIMEBACK_RESOURCE_DEFAULTS2 = {
155938
+ vendorId: "playcademy",
155939
+ roles: ["primary"],
155940
+ importance: "primary",
155941
+ metadata: {
155942
+ type: "interactive",
155943
+ toolProvider: "Playcademy",
155944
+ instructionalMethod: "exploratory",
155945
+ language: "en-US"
155946
+ }
155947
+ };
155948
+ TIMEBACK_COMPONENT_DEFAULTS2 = {
155949
+ sortOrder: 1,
155950
+ prerequisiteCriteria: "ALL"
155951
+ };
155952
+ TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2 = {
155953
+ sortOrder: 1,
155954
+ lessonType: "quiz"
155955
+ };
155956
+ });
155733
155957
  var init_workers2 = () => {};
155734
155958
  var init_src3 = __esm4(() => {
155735
155959
  init_auth2();
@@ -155761,7 +155985,6 @@ var HTTP_DEFAULTS2;
155761
155985
  var AUTH_DEFAULTS2;
155762
155986
  var CACHE_DEFAULTS2;
155763
155987
  var CONFIG_DEFAULTS2;
155764
- var DEFAULT_PLAYCADEMY_ORGANIZATION_ID2;
155765
155988
  var PLAYCADEMY_DEFAULTS2;
155766
155989
  var RESOURCE_DEFAULTS2;
155767
155990
  var HTTP_STATUS2;
@@ -155908,54 +156131,26 @@ var init_constants2 = __esm4(() => {
155908
156131
  CONFIG_DEFAULTS2 = {
155909
156132
  fileNames: ["timeback.config.js", "timeback.config.json"]
155910
156133
  };
155911
- DEFAULT_PLAYCADEMY_ORGANIZATION_ID2 = process.env.TIMEBACK_ORG_SOURCE_ID || "PLAYCADEMY";
155912
156134
  PLAYCADEMY_DEFAULTS2 = {
155913
- organization: DEFAULT_PLAYCADEMY_ORGANIZATION_ID2,
156135
+ organization: TIMEBACK_ORG_SOURCED_ID2,
155914
156136
  launchBaseUrls: PLAYCADEMY_BASE_URLS2
155915
156137
  };
155916
156138
  RESOURCE_DEFAULTS2 = {
155917
156139
  organization: {
155918
- name: "Playcademy Studios",
155919
- type: "department"
156140
+ name: TIMEBACK_ORG_NAME2,
156141
+ type: TIMEBACK_ORG_TYPE2
155920
156142
  },
155921
156143
  course: {
155922
- gradingScheme: "STANDARD",
155923
- level: {
155924
- elementary: "Elementary",
155925
- middle: "Middle",
155926
- high: "High",
155927
- ap: "AP"
155928
- },
156144
+ gradingScheme: TIMEBACK_COURSE_DEFAULTS2.gradingScheme,
156145
+ level: TIMEBACK_COURSE_DEFAULTS2.level,
155929
156146
  metadata: {
155930
- goals: {
155931
- dailyXp: 50,
155932
- dailyLessons: 3
155933
- },
155934
- metrics: {
155935
- totalXp: 1000,
155936
- totalLessons: 50
155937
- }
156147
+ goals: TIMEBACK_COURSE_DEFAULTS2.goals,
156148
+ metrics: TIMEBACK_COURSE_DEFAULTS2.metrics
155938
156149
  }
155939
156150
  },
155940
- component: {
155941
- sortOrder: 1,
155942
- prerequisiteCriteria: "ALL"
155943
- },
155944
- resource: {
155945
- vendorId: "playcademy",
155946
- roles: ["primary"],
155947
- importance: "primary",
155948
- metadata: {
155949
- type: "interactive",
155950
- toolProvider: "Playcademy",
155951
- instructionalMethod: "exploratory",
155952
- language: "en-US"
155953
- }
155954
- },
155955
- componentResource: {
155956
- sortOrder: 1,
155957
- lessonType: "quiz"
155958
- }
156151
+ component: TIMEBACK_COMPONENT_DEFAULTS2,
156152
+ resource: TIMEBACK_RESOURCE_DEFAULTS2,
156153
+ componentResource: TIMEBACK_COMPONENT_RESOURCE_DEFAULTS2
155959
156154
  };
155960
156155
  HTTP_STATUS2 = {
155961
156156
  CLIENT_ERROR_MIN: 400,
@@ -156306,68 +156501,89 @@ function buildResourceMetadata({
156306
156501
  }
156307
156502
  return metadata2;
156308
156503
  }
156309
- // ../api-core/src/utils/timeback-enrollments.ts
156504
+ // ../api-core/src/utils/timeback-profile.ts
156310
156505
  init_src();
156311
- async function fetchEnrollmentsForUser(timebackId) {
156312
- const db = getDatabase();
156313
- const isLocal = process.env.PUBLIC_IS_LOCAL === "true";
156314
- if (isLocal) {
156315
- const allIntegrations = await db.query.gameTimebackIntegrations.findMany();
156316
- return allIntegrations.map((integration) => ({
156317
- gameId: integration.gameId,
156318
- grade: integration.grade,
156319
- subject: integration.subject,
156320
- courseId: integration.courseId
156321
- }));
156506
+ async function fetchStudentFromOneRoster(timebackId) {
156507
+ log2.debug("[OneRoster] Fetching student profile", { timebackId });
156508
+ try {
156509
+ const client = await getTimebackClient();
156510
+ const user = await client.oneroster.users.get(timebackId);
156511
+ const primaryRoleEntry = user.roles.find((r2) => r2.roleType === "primary");
156512
+ const role = primaryRoleEntry?.role ?? user.roles[0]?.role ?? "student";
156513
+ const orgMap = new Map;
156514
+ if (user.primaryOrg) {
156515
+ orgMap.set(user.primaryOrg.sourcedId, {
156516
+ id: user.primaryOrg.sourcedId,
156517
+ name: user.primaryOrg.name ?? null,
156518
+ type: user.primaryOrg.type || "school",
156519
+ isPrimary: true
156520
+ });
156521
+ }
156522
+ for (const r2 of user.roles) {
156523
+ if (r2.org && !orgMap.has(r2.org.sourcedId)) {
156524
+ orgMap.set(r2.org.sourcedId, {
156525
+ id: r2.org.sourcedId,
156526
+ name: null,
156527
+ type: "school",
156528
+ isPrimary: false
156529
+ });
156530
+ }
156531
+ }
156532
+ const organizations = Array.from(orgMap.values());
156533
+ return { role, organizations };
156534
+ } catch (error2) {
156535
+ log2.warn("[OneRoster] Failed to fetch student, using defaults", { error: error2, timebackId });
156536
+ return { role: "student", organizations: [] };
156322
156537
  }
156323
- log2.debug("[timeback-enrollments] Fetching student enrollments from TimeBack", { timebackId });
156538
+ }
156539
+ async function fetchEnrollmentsFromEduBridge(timebackId) {
156540
+ const db = getDatabase();
156541
+ log2.debug("[EduBridge] Fetching student enrollments", { timebackId });
156324
156542
  try {
156325
156543
  const client = await getTimebackClient();
156326
- const classes = await client.getEnrollments(timebackId);
156327
- const courseIds = classes.map((cls) => cls.courseId).filter((id) => Boolean(id));
156328
- if (courseIds.length === 0) {
156544
+ const enrollments = await client.getEnrollments(timebackId);
156545
+ const courseIds = enrollments.map((e) => e.courseId).filter((id) => Boolean(id));
156546
+ if (courseIds.length === 0)
156329
156547
  return [];
156330
- }
156548
+ const courseToSchool = new Map(enrollments.filter((e) => e.school?.id).map((e) => [e.courseId, e.school.id]));
156331
156549
  const integrations = await db.query.gameTimebackIntegrations.findMany({
156332
156550
  where: inArray(gameTimebackIntegrations.courseId, courseIds)
156333
156551
  });
156334
- return integrations.map((integration) => ({
156335
- gameId: integration.gameId,
156336
- grade: integration.grade,
156337
- subject: integration.subject,
156338
- courseId: integration.courseId
156552
+ return integrations.map((i3) => ({
156553
+ gameId: i3.gameId,
156554
+ grade: i3.grade,
156555
+ subject: i3.subject,
156556
+ courseId: i3.courseId,
156557
+ orgId: courseToSchool.get(i3.courseId)
156339
156558
  }));
156340
156559
  } catch (error2) {
156341
- log2.warn("[timeback-enrollments] Failed to fetch TimeBack enrollments:", {
156342
- error: error2,
156343
- timebackId
156344
- });
156560
+ log2.warn("[EduBridge] Failed to fetch enrollments", { error: error2, timebackId });
156345
156561
  return [];
156346
156562
  }
156347
156563
  }
156348
- async function fetchUserRole(timebackId) {
156349
- log2.debug("[timeback] Fetching user role from TimeBack", { timebackId });
156350
- try {
156351
- const client = await getTimebackClient();
156352
- const user = await client.oneroster.users.get(timebackId);
156353
- const primaryRole = user.roles.find((r2) => r2.roleType === "primary");
156354
- const role = primaryRole?.role ?? user.roles[0]?.role ?? "student";
156355
- log2.debug("[timeback] Resolved user role", { timebackId, role });
156356
- return role;
156357
- } catch (error2) {
156358
- log2.warn("[timeback] Failed to fetch user role, defaulting to student:", {
156359
- error: error2,
156360
- timebackId
156361
- });
156362
- return "student";
156363
- }
156564
+ function filterEnrollmentsByGame(enrollments, gameId) {
156565
+ return enrollments.filter((e) => e.gameId === gameId).map(({ gameId: _4, ...rest }) => rest);
156566
+ }
156567
+ function filterOrganizationsByEnrollments(organizations, enrollments) {
156568
+ const enrollmentOrgIds = new Set(enrollments.map((e) => e.orgId).filter(Boolean));
156569
+ if (enrollmentOrgIds.size === 0)
156570
+ return [];
156571
+ return organizations.filter((o4) => enrollmentOrgIds.has(o4.id));
156364
156572
  }
156365
- async function fetchUserTimebackData(timebackId) {
156366
- const [role, enrollments] = await Promise.all([
156367
- fetchUserRole(timebackId),
156368
- fetchEnrollmentsForUser(timebackId)
156573
+ async function fetchUserTimebackData(timebackId, gameId) {
156574
+ const [{ role, organizations: allOrganizations }, allEnrollments] = await Promise.all([
156575
+ fetchStudentFromOneRoster(timebackId),
156576
+ fetchEnrollmentsFromEduBridge(timebackId)
156369
156577
  ]);
156370
- return { role, enrollments };
156578
+ const enrollments = gameId ? filterEnrollmentsByGame(allEnrollments, gameId) : allEnrollments;
156579
+ const organizations = gameId ? filterOrganizationsByEnrollments(allOrganizations, enrollments) : allOrganizations;
156580
+ log2.debug("[Timeback] Fetched student data", {
156581
+ timebackId,
156582
+ role,
156583
+ enrollments: enrollments.map((e) => `${e.subject}:${e.grade}`),
156584
+ organizations: organizations.map((o4) => `${o4.name ?? o4.id} (${o4.type})`)
156585
+ });
156586
+ return { id: timebackId, role, enrollments, organizations };
156371
156587
  }
156372
156588
  // ../data/src/domains/achievement/types.ts
156373
156589
  var AchievementCompletionType;
@@ -158285,7 +158501,7 @@ async function publishPersonalBestNotification(userId, gameId, rank, newScore, p
158285
158501
  }
158286
158502
  // ../cloudflare/src/playcademy/provider.ts
158287
158503
  import { readdir as readdir2, readFile as readFile2, stat } from "node:fs/promises";
158288
- import { join as join4, relative } from "node:path";
158504
+ import { join as join5, relative } from "node:path";
158289
158505
  init_src();
158290
158506
 
158291
158507
  // ../cloudflare/src/core/client.ts
@@ -158812,7 +159028,7 @@ init_src();
158812
159028
  // ../cloudflare/src/utils/assets.ts
158813
159029
  import { createHash } from "node:crypto";
158814
159030
  import { readdir, readFile } from "node:fs/promises";
158815
- import { join as join3 } from "node:path";
159031
+ import { join as join4 } from "node:path";
158816
159032
  function hashFile(content) {
158817
159033
  return createHash("md5").update(content).digest("hex");
158818
159034
  }
@@ -158832,7 +159048,7 @@ async function scanAssetDirectory(distPath) {
158832
159048
  async function scanFiles(dir, baseDir = dir) {
158833
159049
  const entries = await readdir(dir, { withFileTypes: true });
158834
159050
  for (const entry of entries) {
158835
- const fullPath = join3(dir, entry.name);
159051
+ const fullPath = join4(dir, entry.name);
158836
159052
  if (entry.isDirectory()) {
158837
159053
  await scanFiles(fullPath, baseDir);
158838
159054
  } else {
@@ -159204,7 +159420,7 @@ class CloudflareProvider {
159204
159420
  async function scanDirectory(dir) {
159205
159421
  const entries = await readdir2(dir, { withFileTypes: true });
159206
159422
  for (const entry of entries) {
159207
- const fullPath = join4(dir, entry.name);
159423
+ const fullPath = join5(dir, entry.name);
159208
159424
  if (entry.isDirectory()) {
159209
159425
  if (await scanDirectory(fullPath))
159210
159426
  return true;
@@ -159226,7 +159442,7 @@ class CloudflareProvider {
159226
159442
  async resolveAssetBasePath(dirPath) {
159227
159443
  const entries = await readdir2(dirPath, { withFileTypes: true });
159228
159444
  if (entries.length === 1 && entries[0]?.isDirectory()) {
159229
- const unwrappedPath = join4(dirPath, entries[0].name);
159445
+ const unwrappedPath = join5(dirPath, entries[0].name);
159230
159446
  log2.debug("[CloudflareProvider] Unwrapping wrapper directory", {
159231
159447
  wrapper: entries[0].name
159232
159448
  });
@@ -159237,7 +159453,7 @@ class CloudflareProvider {
159237
159453
  async uploadFilesToR2(dir, baseDir, bucketName) {
159238
159454
  const entries = await readdir2(dir, { withFileTypes: true });
159239
159455
  for (const entry of entries) {
159240
- const fullPath = join4(dir, entry.name);
159456
+ const fullPath = join5(dir, entry.name);
159241
159457
  if (entry.isDirectory()) {
159242
159458
  await this.uploadFilesToR2(fullPath, baseDir, bucketName);
159243
159459
  } else {
@@ -159725,40 +159941,19 @@ async function seedCurrencies(db) {
159725
159941
  }
159726
159942
  }
159727
159943
 
159728
- // src/lib/logging/adapter.ts
159729
- var customLogger;
159730
- function setLogger(logger3) {
159731
- customLogger = logger3;
159732
- }
159733
- function getLogger() {
159734
- if (customLogger) {
159735
- return customLogger;
159736
- }
159737
- return {
159738
- info: (msg) => console.log(msg),
159739
- warn: (msg) => console.warn(msg),
159740
- error: (msg) => console.error(msg)
159741
- };
159742
- }
159743
- var logger3 = {
159744
- info: (msg) => {
159745
- if (customLogger || !config.embedded) {
159746
- getLogger().info(msg);
159747
- }
159748
- },
159749
- warn: (msg) => getLogger().warn(msg),
159750
- error: (msg) => getLogger().error(msg)
159751
- };
159752
159944
  // src/database/seed/timeback.ts
159753
- function resolveStudentId(studentId) {
159754
- if (!studentId)
159755
- return null;
159756
- if (studentId === "mock")
159757
- return `mock-student-${crypto.randomUUID().slice(0, 8)}`;
159758
- return studentId;
159945
+ function generateMockStudentId(userId) {
159946
+ return `mock-student-${userId.slice(-8)}`;
159759
159947
  }
159760
- function getAdminTimebackId() {
159761
- return resolveStudentId(config.timeback.studentId);
159948
+ function generateTimebackId(userId, isPrimaryUser = false) {
159949
+ const timebackId = config.timeback.timebackId;
159950
+ if (!timebackId) {
159951
+ return null;
159952
+ }
159953
+ if (timebackId === "mock") {
159954
+ return generateMockStudentId(userId);
159955
+ }
159956
+ return isPrimaryUser ? timebackId : generateMockStudentId(userId);
159762
159957
  }
159763
159958
  async function seedTimebackIntegrations(db, gameId, courses) {
159764
159959
  const now2 = new Date;
@@ -159782,12 +159977,10 @@ async function seedTimebackIntegrations(db, gameId, courses) {
159782
159977
  });
159783
159978
  seededCount++;
159784
159979
  } catch (error2) {
159785
- console.error(`❌ Error seeding TimeBack integration for ${course.subject}:${course.grade}:`, error2);
159980
+ console.error(`❌ Error seeding Timeback integration for ${course.subject}:${course.grade}:`, error2);
159786
159981
  }
159787
159982
  }
159788
- if (seededCount > 0) {
159789
- logger3.info(`\uD83D\uDCDA Seeded ${seededCount} TimeBack integration(s)`);
159790
- }
159983
+ return seededCount;
159791
159984
  }
159792
159985
 
159793
159986
  // src/database/seed/games.ts
@@ -159823,7 +160016,6 @@ async function seedCurrentProjectGame(db, project) {
159823
160016
  where: (games3, { eq: eq3 }) => eq3(games3.slug, project.slug)
159824
160017
  });
159825
160018
  if (existingGame) {
159826
- logger3.info(`\uD83C\uDFAE Game "${project.displayName}" (${project.slug}) already exists`);
159827
160019
  if (project.timebackCourses && project.timebackCourses.length > 0) {
159828
160020
  await seedTimebackIntegrations(db, existingGame.id, project.timebackCourses);
159829
160021
  }
@@ -159916,11 +160108,12 @@ async function seedSpriteTemplates(db) {
159916
160108
  // src/database/seed/index.ts
159917
160109
  async function seedDemoData(db) {
159918
160110
  try {
159919
- const adminTimebackId = getAdminTimebackId();
159920
- for (const [role, user] of Object.entries(DEMO_USERS)) {
160111
+ const primaryUserId = DEMO_USERS.player.id;
160112
+ for (const user of Object.values(DEMO_USERS)) {
160113
+ const isPrimaryUser = user.id === primaryUserId;
159921
160114
  const userValues = {
159922
160115
  ...user,
159923
- timebackId: role === "admin" ? adminTimebackId : null
160116
+ timebackId: generateTimebackId(user.id, isPrimaryUser)
159924
160117
  };
159925
160118
  await db.insert(users).values(userValues).onConflictDoNothing();
159926
160119
  }
@@ -159941,7 +160134,7 @@ async function seedDemoData(db) {
159941
160134
  console.error("❌ Error seeding demo data:", error2);
159942
160135
  throw error2;
159943
160136
  }
159944
- return DEMO_USERS.admin;
160137
+ return DEMO_USERS.player;
159945
160138
  }
159946
160139
 
159947
160140
  // src/server/database.ts
@@ -159981,6 +160174,13 @@ async function setupServerDatabase(processedOptions, project) {
159981
160174
  // src/server/options.ts
159982
160175
  init_src();
159983
160176
  var import_json_colorizer = __toESM(require_dist2(), 1);
160177
+
160178
+ // src/lib/logging/adapter.ts
160179
+ var customLogger;
160180
+ function setLogger(logger3) {
160181
+ customLogger = logger3;
160182
+ }
160183
+ // src/server/options.ts
159984
160184
  function processServerOptions(port, options) {
159985
160185
  const {
159986
160186
  verbose = false,
@@ -160031,6 +160231,10 @@ async function startRealtimeServer(realtimeOptions, betterAuthSecret) {
160031
160231
  if (!realtimeOptions.enabled) {
160032
160232
  return null;
160033
160233
  }
160234
+ if (!realtimeOptions.port) {
160235
+ return null;
160236
+ }
160237
+ await waitForPort(realtimeOptions.port);
160034
160238
  if (typeof Bun === "undefined") {
160035
160239
  try {
160036
160240
  return Promise.resolve().then(() => (init_sandbox(), exports_sandbox)).then(({ createSandboxRealtimeServer: createSandboxRealtimeServer2 }) => createSandboxRealtimeServer2({
@@ -165324,12 +165528,32 @@ async function getUserMe(ctx) {
165324
165528
  log2.error(`[API /users/me] User not found in DB for valid token ID: ${user.id}`);
165325
165529
  throw ApiError.notFound("User not found");
165326
165530
  }
165531
+ const timeback3 = userData.timebackId ? await fetchUserTimebackData(userData.timebackId, ctx.gameId) : undefined;
165532
+ if (ctx.gameId) {
165533
+ return {
165534
+ id: userData.id,
165535
+ name: userData.name,
165536
+ role: userData.role,
165537
+ username: userData.username,
165538
+ email: userData.email,
165539
+ timeback: timeback3
165540
+ };
165541
+ }
165327
165542
  const timebackAccount = await db.query.accounts.findFirst({
165328
165543
  where: and(eq(accounts.userId, user.id), eq(accounts.providerId, "timeback"))
165329
165544
  });
165330
- const timeback3 = userData.timebackId ? await fetchUserTimebackData(userData.timebackId) : undefined;
165331
165545
  return {
165332
- ...userData,
165546
+ id: userData.id,
165547
+ name: userData.name,
165548
+ username: userData.username,
165549
+ email: userData.email,
165550
+ emailVerified: userData.emailVerified,
165551
+ image: userData.image,
165552
+ role: userData.role,
165553
+ developerStatus: userData.developerStatus,
165554
+ characterCreated: userData.characterCreated,
165555
+ createdAt: userData.createdAt,
165556
+ updatedAt: userData.updatedAt,
165333
165557
  hasTimebackAccount: !!timebackAccount,
165334
165558
  timeback: timeback3
165335
165559
  };
@@ -165341,16 +165565,107 @@ async function getUserMe(ctx) {
165341
165565
  }
165342
165566
  }
165343
165567
 
165568
+ // src/mocks/timeback.ts
165569
+ init_src();
165570
+ function shouldMockTimeback() {
165571
+ return config.timeback.timebackId === "mock";
165572
+ }
165573
+ function getMockStudentProfile() {
165574
+ const { organization: org, role } = config.timeback;
165575
+ return {
165576
+ role: role ?? "student",
165577
+ organizations: [
165578
+ {
165579
+ id: org?.id ?? "PLAYCADEMY",
165580
+ name: org?.name ?? "Playcademy Studios",
165581
+ type: org?.type ?? "department",
165582
+ isPrimary: true
165583
+ }
165584
+ ]
165585
+ };
165586
+ }
165587
+ async function getMockEnrollments(db) {
165588
+ const allIntegrations = await db.query.gameTimebackIntegrations.findMany();
165589
+ return allIntegrations.map((i4) => ({
165590
+ gameId: i4.gameId,
165591
+ grade: i4.grade,
165592
+ subject: i4.subject,
165593
+ courseId: i4.courseId
165594
+ }));
165595
+ }
165596
+ async function getMockTimebackData(db, timebackId, gameId) {
165597
+ const { role, organizations } = getMockStudentProfile();
165598
+ const allEnrollments = await getMockEnrollments(db);
165599
+ const enrollments = gameId ? allEnrollments.filter((e2) => e2.gameId === gameId).map(({ gameId: _5, ...rest }) => rest) : allEnrollments;
165600
+ log2.debug("[Timeback] Sandbox is using mock data", {
165601
+ timebackId,
165602
+ role,
165603
+ enrollments: enrollments.map((e2) => `${e2.subject}:${e2.grade}`),
165604
+ organizations: organizations.map((o5) => `${o5.name ?? o5.id}`)
165605
+ });
165606
+ return { id: timebackId, role, enrollments, organizations };
165607
+ }
165608
+ async function buildMockUserResponse(db, user, gameId) {
165609
+ const timeback3 = user.timebackId ? await getMockTimebackData(db, user.timebackId, gameId) : undefined;
165610
+ if (gameId) {
165611
+ return {
165612
+ id: user.id,
165613
+ name: user.name,
165614
+ role: user.role,
165615
+ username: user.username,
165616
+ email: user.email,
165617
+ timeback: timeback3
165618
+ };
165619
+ }
165620
+ const timebackAccount = await db.query.accounts.findFirst({
165621
+ where: and(eq(accounts.userId, user.id), eq(accounts.providerId, "timeback"))
165622
+ });
165623
+ return {
165624
+ id: user.id,
165625
+ name: user.name,
165626
+ username: user.username,
165627
+ email: user.email,
165628
+ emailVerified: user.emailVerified,
165629
+ image: user.image,
165630
+ role: user.role,
165631
+ developerStatus: user.developerStatus,
165632
+ characterCreated: user.characterCreated,
165633
+ createdAt: user.createdAt,
165634
+ updatedAt: user.updatedAt,
165635
+ hasTimebackAccount: !!timebackAccount,
165636
+ timeback: timeback3
165637
+ };
165638
+ }
165639
+
165344
165640
  // src/routes/platform/users.ts
165345
165641
  var usersRouter = new Hono2;
165346
165642
  usersRouter.get("/me", async (c3) => {
165347
- const ctx = {
165348
- user: c3.get("user"),
165349
- params: {},
165350
- url: new URL(c3.req.url),
165351
- request: c3.req.raw
165352
- };
165643
+ const user = c3.get("user");
165644
+ const gameId = c3.get("gameId");
165645
+ if (!user) {
165646
+ const error2 = ApiError.unauthorized("Valid session or bearer token required");
165647
+ return c3.json(createErrorResponse(error2), error2.statusCode);
165648
+ }
165353
165649
  try {
165650
+ if (shouldMockTimeback()) {
165651
+ const db = c3.get("db");
165652
+ const userData2 = await db.query.users.findFirst({
165653
+ where: eq(users.id, user.id)
165654
+ });
165655
+ if (!userData2) {
165656
+ const error2 = ApiError.notFound("User not found");
165657
+ return c3.json(createErrorResponse(error2), error2.statusCode);
165658
+ }
165659
+ const response = await buildMockUserResponse(db, userData2, gameId);
165660
+ return c3.json(response);
165661
+ }
165662
+ const ctx = {
165663
+ user,
165664
+ params: {},
165665
+ url: new URL(c3.req.url),
165666
+ request: c3.req.raw,
165667
+ gameId
165668
+ };
165354
165669
  const userData = await getUserMe(ctx);
165355
165670
  return c3.json(userData);
165356
165671
  } catch (error2) {
@@ -170690,7 +171005,7 @@ async function getTodayTimeBackXp(ctx) {
170690
171005
  throw error2;
170691
171006
  if (error2 instanceof InvalidTimezoneError)
170692
171007
  throw ApiError.badRequest(error2.message);
170693
- log2.error("[timeback] getTodayTimeBackXp failed", { error: error2 });
171008
+ log2.error("[Timeback] getTodayTimeBackXp failed", { error: error2 });
170694
171009
  throw ApiError.internal("Failed to get today's TimeBack XP", error2);
170695
171010
  }
170696
171011
  }
@@ -170706,7 +171021,7 @@ async function getTotalTimeBackXp(ctx) {
170706
171021
  totalXp: Number(result[0]?.totalXp) || 0
170707
171022
  };
170708
171023
  } catch (error2) {
170709
- log2.error("[timeback] getTotalTimeBackXp failed", { error: error2 });
171024
+ log2.error("[Timeback] getTotalTimeBackXp failed", { error: error2 });
170710
171025
  throw ApiError.internal("Failed to get total TimeBack XP", error2);
170711
171026
  }
170712
171027
  }
@@ -170756,7 +171071,7 @@ async function updateTodayTimeBackXp(ctx) {
170756
171071
  } catch (error2) {
170757
171072
  if (error2 instanceof ApiError)
170758
171073
  throw error2;
170759
- log2.error("[timeback] updateTodayTimeBackXp failed", { error: error2 });
171074
+ log2.error("[Timeback] updateTodayTimeBackXp failed", { error: error2 });
170760
171075
  throw ApiError.internal("Failed to update today's TimeBack XP", error2);
170761
171076
  }
170762
171077
  }
@@ -170791,7 +171106,7 @@ async function getTimeBackXpHistory(ctx) {
170791
171106
  }))
170792
171107
  };
170793
171108
  } catch (error2) {
170794
- log2.error("[timeback] getTimeBackXpHistory failed", { error: error2 });
171109
+ log2.error("[Timeback] getTimeBackXpHistory failed", { error: error2 });
170795
171110
  throw ApiError.internal("Failed to get TimeBack XP history", error2);
170796
171111
  }
170797
171112
  }
@@ -170807,7 +171122,7 @@ async function getStudentEnrollments(ctx) {
170807
171122
  throw ApiError.badRequest("Missing timebackId parameter");
170808
171123
  }
170809
171124
  log2.debug("[API] Getting student enrollments", { userId: user.id, timebackId });
170810
- const enrollments = await fetchEnrollmentsForUser(timebackId);
171125
+ const enrollments = await fetchEnrollmentsFromEduBridge(timebackId);
170811
171126
  log2.info("[API] Retrieved student enrollments", {
170812
171127
  userId: user.id,
170813
171128
  timebackId,
@@ -171008,13 +171323,23 @@ timebackRouter.post("/end-activity", async (c3) => {
171008
171323
  });
171009
171324
  timebackRouter.get("/enrollments/:timebackId", async (c3) => {
171010
171325
  const timebackId = c3.req.param("timebackId");
171011
- const ctx = {
171012
- user: c3.get("user"),
171013
- params: { timebackId },
171014
- url: new URL(c3.req.url),
171015
- request: c3.req.raw
171016
- };
171326
+ const user = c3.get("user");
171327
+ if (!user) {
171328
+ const error2 = ApiError.unauthorized("Must be logged in to get enrollments");
171329
+ return c3.json(createErrorResponse(error2), error2.statusCode);
171330
+ }
171017
171331
  try {
171332
+ if (shouldMockTimeback()) {
171333
+ const db = c3.get("db");
171334
+ const enrollments2 = await getMockEnrollments(db);
171335
+ return c3.json({ enrollments: enrollments2 });
171336
+ }
171337
+ const ctx = {
171338
+ user,
171339
+ params: { timebackId },
171340
+ url: new URL(c3.req.url),
171341
+ request: c3.req.raw
171342
+ };
171018
171343
  const result = await getStudentEnrollments(ctx);
171019
171344
  return c3.json(result);
171020
171345
  } catch (error2) {
@@ -171330,6 +171655,7 @@ function registerRoutes(app) {
171330
171655
  // src/server/index.ts
171331
171656
  var version3 = package_default.version;
171332
171657
  async function startServer(port, project, options = {}) {
171658
+ await waitForPort(port);
171333
171659
  const processedOptions = processServerOptions(port, options);
171334
171660
  const db = await setupServerDatabase(processedOptions, project);
171335
171661
  const app = createApp(db, {
@@ -171342,11 +171668,20 @@ async function startServer(port, project, options = {}) {
171342
171668
  return {
171343
171669
  main: mainServer,
171344
171670
  realtime: realtimeServer,
171671
+ timebackMode: getTimebackDisplayMode(),
171672
+ setRole: (role) => {
171673
+ config.timeback.role = role;
171674
+ },
171345
171675
  stop: () => {
171346
- if (mainServer.close)
171347
- mainServer.close();
171348
- if (realtimeServer?.stop)
171349
- realtimeServer.stop();
171676
+ return new Promise((resolve2) => {
171677
+ if (realtimeServer?.stop)
171678
+ realtimeServer.stop();
171679
+ if (mainServer.close) {
171680
+ mainServer.close(() => resolve2());
171681
+ } else {
171682
+ resolve2();
171683
+ }
171684
+ });
171350
171685
  }
171351
171686
  };
171352
171687
  }
@@ -171367,105 +171702,6 @@ var {
171367
171702
  Help
171368
171703
  } = import__4.default;
171369
171704
 
171370
- // ../utils/src/port.ts
171371
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
171372
- import { createServer } from "node:net";
171373
- import { homedir } from "node:os";
171374
- import { join as join5 } from "node:path";
171375
- async function isPortAvailableOnHost(port, host) {
171376
- return new Promise((resolve2) => {
171377
- const server2 = createServer();
171378
- let resolved = false;
171379
- const cleanup = (result) => {
171380
- if (resolved)
171381
- return;
171382
- resolved = true;
171383
- try {
171384
- server2.close();
171385
- } catch {}
171386
- resolve2(result);
171387
- };
171388
- const timeout = setTimeout(() => cleanup(true), 100);
171389
- server2.once("error", (err2) => {
171390
- clearTimeout(timeout);
171391
- if (err2.code === "EAFNOSUPPORT" || err2.code === "EADDRNOTAVAIL") {
171392
- cleanup(true);
171393
- } else {
171394
- cleanup(false);
171395
- }
171396
- });
171397
- server2.once("listening", () => {
171398
- clearTimeout(timeout);
171399
- cleanup(true);
171400
- });
171401
- server2.listen(port, host).unref();
171402
- });
171403
- }
171404
- async function findAvailablePort(startPort = 4321) {
171405
- if (await isPortAvailableOnHost(startPort, "0.0.0.0")) {
171406
- return startPort;
171407
- } else {
171408
- return findAvailablePort(startPort + 1);
171409
- }
171410
- }
171411
- function getRegistryPath() {
171412
- const home = homedir();
171413
- const dir = join5(home, ".playcademy");
171414
- if (!existsSync2(dir)) {
171415
- mkdirSync2(dir, { recursive: true });
171416
- }
171417
- return join5(dir, ".proc");
171418
- }
171419
- function readRegistry() {
171420
- const registryPath = getRegistryPath();
171421
- if (!existsSync2(registryPath)) {
171422
- return {};
171423
- }
171424
- try {
171425
- const content = readFileSync(registryPath, "utf-8");
171426
- return JSON.parse(content);
171427
- } catch {
171428
- return {};
171429
- }
171430
- }
171431
- function writeRegistry(registry2) {
171432
- const registryPath = getRegistryPath();
171433
- writeFileSync(registryPath, JSON.stringify(registry2, null, 2), "utf-8");
171434
- }
171435
- function getServerKey(type, port) {
171436
- return `${type}-${port}`;
171437
- }
171438
- function writeServerInfo(type, info2) {
171439
- const registry2 = readRegistry();
171440
- const key = getServerKey(type, info2.port);
171441
- registry2[key] = info2;
171442
- writeRegistry(registry2);
171443
- }
171444
- function cleanupServerInfo(type, projectRoot, pid) {
171445
- const registry2 = readRegistry();
171446
- const keysToRemove = [];
171447
- for (const [key, info2] of Object.entries(registry2)) {
171448
- if (key.startsWith(`${type}-`)) {
171449
- let matches = true;
171450
- if (projectRoot && info2.projectRoot !== projectRoot) {
171451
- matches = false;
171452
- }
171453
- if (pid !== undefined && info2.pid !== pid) {
171454
- matches = false;
171455
- }
171456
- if (matches) {
171457
- keysToRemove.push(key);
171458
- }
171459
- }
171460
- }
171461
- for (const key of keysToRemove) {
171462
- delete registry2[key];
171463
- }
171464
- if (keysToRemove.length > 0) {
171465
- writeRegistry(registry2);
171466
- }
171467
- }
171468
-
171469
171705
  // src/cli/display.ts
171470
171706
  var import_picocolors = __toESM(require_picocolors(), 1);
171471
171707
  function printBanner(options) {
@@ -171687,7 +171923,7 @@ function parseTimebackOptions(options) {
171687
171923
  onerosterApiUrl: options.timebackOnerosterUrl || "http://localhost:9000",
171688
171924
  caliperApiUrl: options.timebackCaliperUrl || "http://localhost:9001",
171689
171925
  courseId: options.timebackCourseId,
171690
- studentId: options.timebackStudentId
171926
+ timebackId: options.timebackStudentId
171691
171927
  };
171692
171928
  }
171693
171929
  if (options.timebackOnerosterUrl) {
@@ -171696,7 +171932,7 @@ function parseTimebackOptions(options) {
171696
171932
  onerosterApiUrl: options.timebackOnerosterUrl,
171697
171933
  caliperApiUrl: options.timebackCaliperUrl,
171698
171934
  courseId: options.timebackCourseId,
171699
- studentId: options.timebackStudentId
171935
+ timebackId: options.timebackStudentId
171700
171936
  };
171701
171937
  }
171702
171938
  return;
@@ -171706,12 +171942,15 @@ function parseTimebackOptions(options) {
171706
171942
  var program2 = new Command;
171707
171943
  program2.name("playcademy-sandbox").description("Local development server for Playcademy game development").version(version3).option("-p, --port <number>", "Port to run the server on", "4321").option("-v, --verbose", "Enable verbose logging", false).option("--project-name <name>", "Name of the current project").option("--project-slug <slug>", "Slug of the current project").option("--realtime", "Enable the realtime server", false).option("--realtime-port <number>", "Port for the realtime server (defaults to main port + 1)").option("--no-seed", "Do not seed the database with demo data").option("--recreate-db", "Recreate the on-disk database on start", false).option("--memory", "Use in-memory database (no persistence)", false).option("--db-path <path>", "Custom path for the database file (relative to cwd or absolute)").option("--config-path <path>", "Path to playcademy.config.json (defaults to cwd)").option("--timeback-local", "Use local TimeBack instance").option("--timeback-oneroster-url <url>", "TimeBack OneRoster API URL").option("--timeback-caliper-url <url>", "TimeBack Caliper API URL").option("--timeback-course-id <id>", "TimeBack course ID for seeding").option("--timeback-student-id <id>", "TimeBack student ID for demo user").action(async (options) => {
171708
171944
  try {
171709
- const requestedPort = parseInt(options.port);
171710
- const availablePort = await findAvailablePort(requestedPort);
171711
- const realtimePort = options.realtimePort ? parseInt(options.realtimePort) : availablePort + 1;
171945
+ const port = parseInt(options.port);
171946
+ const realtimePort = options.realtimePort ? parseInt(options.realtimePort) : port + 1;
171947
+ await requirePortAvailable(port);
171948
+ if (options.realtime) {
171949
+ await requirePortAvailable(realtimePort);
171950
+ }
171712
171951
  const project = await parseProjectInfo(options);
171713
171952
  const timebackOptions = parseTimebackOptions(options);
171714
- const servers = await startServer(availablePort, project, {
171953
+ const servers = await startServer(port, project, {
171715
171954
  seed: options.seed,
171716
171955
  verbose: options.verbose,
171717
171956
  memoryOnly: options.memory,
@@ -171725,14 +171964,14 @@ program2.name("playcademy-sandbox").description("Local development server for Pl
171725
171964
  });
171726
171965
  writeServerInfo("sandbox", {
171727
171966
  pid: process.pid,
171728
- port: availablePort,
171729
- url: `http://localhost:${availablePort}/api`,
171967
+ port,
171968
+ url: `http://localhost:${port}/api`,
171730
171969
  startedAt: Date.now(),
171731
171970
  projectRoot: process.cwd()
171732
171971
  });
171733
171972
  printBanner({
171734
171973
  version: version3,
171735
- port: availablePort,
171974
+ port,
171736
171975
  realtimePort,
171737
171976
  hasRealtime: !!servers.realtime
171738
171977
  });