@playcademy/sandbox 0.3.5 → 0.3.7

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
@@ -996,7 +996,7 @@ ${t}`), i3 = h(this, y2).getSize();
996
996
 
997
997
  // ../../node_modules/esbuild/lib/main.js
998
998
  var require_main = __commonJS((exports, module2) => {
999
- var __dirname = "/Users/hbauer/work/projects/playcademy/node_modules/esbuild/lib", __filename = "/Users/hbauer/work/projects/playcademy/node_modules/esbuild/lib/main.js";
999
+ var __dirname = "/Users/hbauer/work/clones/playcademy-test/node_modules/esbuild/lib", __filename = "/Users/hbauer/work/clones/playcademy-test/node_modules/esbuild/lib/main.js";
1000
1000
  var __defProp2 = Object.defineProperty;
1001
1001
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
1002
1002
  var __getOwnPropNames2 = Object.getOwnPropertyNames;
@@ -86664,9 +86664,9 @@ var require_serde = __commonJS((exports, module2) => {
86664
86664
  strictParseShort: () => strictParseShort2
86665
86665
  });
86666
86666
  module2.exports = __toCommonJS(serde_exports);
86667
- var import_schema2 = require_schema();
86667
+ var import_schema3 = require_schema();
86668
86668
  var copyDocumentWithTransform2 = (source, schemaRef, transform = (_5) => _5) => {
86669
- const ns = import_schema2.NormalizedSchema.of(schemaRef);
86669
+ const ns = import_schema3.NormalizedSchema.of(schemaRef);
86670
86670
  switch (typeof source) {
86671
86671
  case "undefined":
86672
86672
  case "boolean":
@@ -87279,7 +87279,7 @@ var require_protocols = __commonJS((exports, module2) => {
87279
87279
  return "%" + c3.charCodeAt(0).toString(16).toUpperCase();
87280
87280
  });
87281
87281
  }
87282
- var import_schema2 = require_schema();
87282
+ var import_schema22 = require_schema();
87283
87283
  var import_protocol_http2 = require_dist_cjs2();
87284
87284
  var import_schema3 = require_schema();
87285
87285
  var import_serde = require_serde();
@@ -87445,7 +87445,7 @@ var require_protocols = __commonJS((exports, module2) => {
87445
87445
  const query = {};
87446
87446
  const headers = {};
87447
87447
  const endpoint = await context.endpoint();
87448
- const ns = import_schema2.NormalizedSchema.of(operationSchema?.input);
87448
+ const ns = import_schema22.NormalizedSchema.of(operationSchema?.input);
87449
87449
  const schema4 = ns.getSchema();
87450
87450
  let hasNonHttpBindingMember = false;
87451
87451
  let payload;
@@ -87462,7 +87462,7 @@ var require_protocols = __commonJS((exports, module2) => {
87462
87462
  if (endpoint) {
87463
87463
  this.updateServiceEndpoint(request3, endpoint);
87464
87464
  this.setHostPrefix(request3, operationSchema, input);
87465
- const opTraits = import_schema2.NormalizedSchema.translateTraits(operationSchema.traits);
87465
+ const opTraits = import_schema22.NormalizedSchema.translateTraits(operationSchema.traits);
87466
87466
  if (opTraits.http) {
87467
87467
  request3.method = opTraits.http[0];
87468
87468
  const [path3, search] = opTraits.http[1].split("?");
@@ -87540,7 +87540,7 @@ var require_protocols = __commonJS((exports, module2) => {
87540
87540
  if (traits.httpQueryParams) {
87541
87541
  for (const [key, val2] of Object.entries(data)) {
87542
87542
  if (!(key in query)) {
87543
- this.serializeQuery(import_schema2.NormalizedSchema.of([
87543
+ this.serializeQuery(import_schema22.NormalizedSchema.of([
87544
87544
  ns.getValueSchema(),
87545
87545
  {
87546
87546
  ...traits,
@@ -87570,12 +87570,12 @@ var require_protocols = __commonJS((exports, module2) => {
87570
87570
  }
87571
87571
  async deserializeResponse(operationSchema, context, response) {
87572
87572
  const deserializer = this.deserializer;
87573
- const ns = import_schema2.NormalizedSchema.of(operationSchema.output);
87573
+ const ns = import_schema22.NormalizedSchema.of(operationSchema.output);
87574
87574
  const dataObject = {};
87575
87575
  if (response.statusCode >= 300) {
87576
87576
  const bytes = await collectBody2(response.body, context);
87577
87577
  if (bytes.byteLength > 0) {
87578
- Object.assign(dataObject, await deserializer.read(import_schema2.SCHEMA.DOCUMENT, bytes));
87578
+ Object.assign(dataObject, await deserializer.read(import_schema22.SCHEMA.DOCUMENT, bytes));
87579
87579
  }
87580
87580
  await this.handleError(operationSchema, context, response, dataObject, this.deserializeMetadata(response));
87581
87581
  throw new Error("@smithy/core/protocols - HTTP Protocol error handler failed to throw.");
@@ -92260,7 +92260,7 @@ var require_protocols2 = __commonJS((exports, module2) => {
92260
92260
  this.serdeContext = serdeContext;
92261
92261
  }
92262
92262
  };
92263
- var import_schema2 = require_schema();
92263
+ var import_schema4 = require_schema();
92264
92264
  var import_serde2 = require_serde();
92265
92265
  var import_util_base64 = require_dist_cjs9();
92266
92266
  var import_serde = require_serde();
@@ -92351,7 +92351,7 @@ var require_protocols2 = __commonJS((exports, module2) => {
92351
92351
  }
92352
92352
  _read(schema4, value) {
92353
92353
  const isObject4 = value !== null && typeof value === "object";
92354
- const ns = import_schema2.NormalizedSchema.of(schema4);
92354
+ const ns = import_schema4.NormalizedSchema.of(schema4);
92355
92355
  if (ns.isListSchema() && Array.isArray(value)) {
92356
92356
  const listMember = ns.getValueSchema();
92357
92357
  const out2 = [];
@@ -92395,13 +92395,13 @@ var require_protocols2 = __commonJS((exports, module2) => {
92395
92395
  }
92396
92396
  if (ns.isTimestampSchema()) {
92397
92397
  const options = this.settings.timestampFormat;
92398
- const format = options.useTrait ? ns.getSchema() === import_schema2.SCHEMA.TIMESTAMP_DEFAULT ? options.default : ns.getSchema() ?? options.default : options.default;
92398
+ const format = options.useTrait ? ns.getSchema() === import_schema4.SCHEMA.TIMESTAMP_DEFAULT ? options.default : ns.getSchema() ?? options.default : options.default;
92399
92399
  switch (format) {
92400
- case import_schema2.SCHEMA.TIMESTAMP_DATE_TIME:
92400
+ case import_schema4.SCHEMA.TIMESTAMP_DATE_TIME:
92401
92401
  return (0, import_serde2.parseRfc3339DateTimeWithOffset)(value);
92402
- case import_schema2.SCHEMA.TIMESTAMP_HTTP_DATE:
92402
+ case import_schema4.SCHEMA.TIMESTAMP_HTTP_DATE:
92403
92403
  return (0, import_serde2.parseRfc7231DateTime)(value);
92404
- case import_schema2.SCHEMA.TIMESTAMP_EPOCH_SECONDS:
92404
+ case import_schema4.SCHEMA.TIMESTAMP_EPOCH_SECONDS:
92405
92405
  return (0, import_serde2.parseEpochTimestamp)(value);
92406
92406
  default:
92407
92407
  console.warn("Missing timestamp format, parsing value with Date constructor:", value);
@@ -92710,7 +92710,7 @@ var require_protocols2 = __commonJS((exports, module2) => {
92710
92710
  }
92711
92711
  };
92712
92712
  var import_protocols2 = require_protocols();
92713
- var import_schema4 = require_schema();
92713
+ var import_schema42 = require_schema();
92714
92714
  var import_util_body_length_browser2 = require_dist_cjs21();
92715
92715
  var AwsRestJsonProtocol = class extends import_protocols2.HttpBindingProtocol {
92716
92716
  static {
@@ -92726,7 +92726,7 @@ var require_protocols2 = __commonJS((exports, module2) => {
92726
92726
  const settings = {
92727
92727
  timestampFormat: {
92728
92728
  useTrait: true,
92729
- default: import_schema4.SCHEMA.TIMESTAMP_EPOCH_SECONDS
92729
+ default: import_schema42.SCHEMA.TIMESTAMP_EPOCH_SECONDS
92730
92730
  },
92731
92731
  httpBindings: true,
92732
92732
  jsonName: true
@@ -92747,7 +92747,7 @@ var require_protocols2 = __commonJS((exports, module2) => {
92747
92747
  }
92748
92748
  async serializeRequest(operationSchema, input, context) {
92749
92749
  const request3 = await super.serializeRequest(operationSchema, input, context);
92750
- const inputSchema = import_schema4.NormalizedSchema.of(operationSchema.input);
92750
+ const inputSchema = import_schema42.NormalizedSchema.of(operationSchema.input);
92751
92751
  const members = inputSchema.getMemberSchemas();
92752
92752
  if (!request3.headers["content-type"]) {
92753
92753
  const httpPayloadMember = Object.values(members).find((m5) => {
@@ -92791,19 +92791,19 @@ var require_protocols2 = __commonJS((exports, module2) => {
92791
92791
  if (errorIdentifier.includes("#")) {
92792
92792
  [namespace, errorName] = errorIdentifier.split("#");
92793
92793
  }
92794
- const registry2 = import_schema4.TypeRegistry.for(namespace);
92794
+ const registry2 = import_schema42.TypeRegistry.for(namespace);
92795
92795
  let errorSchema;
92796
92796
  try {
92797
92797
  errorSchema = registry2.getSchema(errorIdentifier);
92798
92798
  } catch (e2) {
92799
- const baseExceptionSchema = import_schema4.TypeRegistry.for("smithy.ts.sdk.synthetic." + namespace).getBaseException();
92799
+ const baseExceptionSchema = import_schema42.TypeRegistry.for("smithy.ts.sdk.synthetic." + namespace).getBaseException();
92800
92800
  if (baseExceptionSchema) {
92801
92801
  const ErrorCtor = baseExceptionSchema.ctor;
92802
92802
  throw Object.assign(new ErrorCtor(errorName), dataObject);
92803
92803
  }
92804
92804
  throw new Error(errorName);
92805
92805
  }
92806
- const ns = import_schema4.NormalizedSchema.of(errorSchema);
92806
+ const ns = import_schema42.NormalizedSchema.of(errorSchema);
92807
92807
  const message2 = dataObject.message ?? dataObject.Message ?? "Unknown";
92808
92808
  const exception = new errorSchema.ctor(message2);
92809
92809
  await this.deserializeHttpMessage(errorSchema, context, response, dataObject);
@@ -119571,7 +119571,7 @@ async function requirePortAvailable(port, timeoutMs = 100) {
119571
119571
  // package.json
119572
119572
  var package_default = {
119573
119573
  name: "@playcademy/sandbox",
119574
- version: "0.3.5",
119574
+ version: "0.3.7",
119575
119575
  description: "Local development server for Playcademy game development",
119576
119576
  type: "module",
119577
119577
  exports: {
@@ -134350,6 +134350,21 @@ async function mintGameJwt(gameId, userId) {
134350
134350
  const token = await signJwt({ uid: userId }, "game", expirationTimeSeconds, gameId);
134351
134351
  return { token, exp: expirationTimestampMillis };
134352
134352
  }
134353
+ async function mintRealtimeJwt(userId, gameId, username, role) {
134354
+ const payload = {
134355
+ sub: userId
134356
+ };
134357
+ if (gameId) {
134358
+ payload.gameId = gameId;
134359
+ }
134360
+ if (username) {
134361
+ payload.username = username;
134362
+ }
134363
+ if (role) {
134364
+ payload.role = role;
134365
+ }
134366
+ return await signJwt(payload, "realtime", "30m");
134367
+ }
134353
134368
  // ../api-core/src/utils/validation.ts
134354
134369
  function formatValidationErrors(error2) {
134355
134370
  const flattened = error2.flatten();
@@ -143454,6 +143469,17 @@ function createCustomHostnamesNamespace(config2) {
143454
143469
  }
143455
143470
  };
143456
143471
  }
143472
+ // ../cloudflare/src/utils/schema.ts
143473
+ function buildSchemaSql(sql3) {
143474
+ const trimmed = sql3.trim();
143475
+ if (!trimmed) {
143476
+ return "";
143477
+ }
143478
+ return `PRAGMA defer_foreign_keys = on;
143479
+ ${trimmed}
143480
+ PRAGMA defer_foreign_keys = off;`;
143481
+ }
143482
+
143457
143483
  // ../cloudflare/src/core/namespaces/d1.ts
143458
143484
  function createD1Namespace(config2) {
143459
143485
  const { client, accountId } = config2;
@@ -143488,26 +143514,22 @@ function createD1Namespace(config2) {
143488
143514
  async executeSchema(databaseId, schema4) {
143489
143515
  log2.debug("[Cloudflare D1] Executing schema", {
143490
143516
  databaseId,
143491
- schemaHash: schema4.hash
143517
+ schemaHash: schema4.hash,
143518
+ sqlLength: schema4.sql.length
143492
143519
  });
143493
143520
  try {
143494
- const statements = schema4.sql.split(";").map((stmt) => stmt.trim()).filter((stmt) => stmt.length > 0 && !stmt.startsWith("--"));
143495
- for (const statement of statements) {
143496
- if (statement.trim()) {
143497
- log2.debug("[Cloudflare D1] Executing SQL statement", {
143498
- databaseId,
143499
- statement: statement.substring(0, 100) + (statement.length > 100 ? "..." : "")
143500
- });
143501
- await client.d1.database.query(databaseId, {
143502
- account_id: accountId,
143503
- sql: statement
143504
- });
143505
- }
143506
- }
143521
+ const sql3 = buildSchemaSql(schema4.sql);
143522
+ log2.debug("[Cloudflare D1] Executing schema", {
143523
+ databaseId,
143524
+ sql: sql3
143525
+ });
143526
+ await client.d1.database.query(databaseId, {
143527
+ account_id: accountId,
143528
+ sql: sql3
143529
+ });
143507
143530
  log2.info("[Cloudflare D1] Schema executed", {
143508
143531
  databaseId,
143509
- schemaHash: schema4.hash,
143510
- statementsExecuted: statements.length
143532
+ schemaHash: schema4.hash
143511
143533
  });
143512
143534
  } catch (error2) {
143513
143535
  log2.error("[Cloudflare D1] Failed to execute schema", {
@@ -143537,52 +143559,31 @@ function createD1Namespace(config2) {
143537
143559
  },
143538
143560
  async reset(name4) {
143539
143561
  log2.debug("[Cloudflare D1] Resetting database", { name: name4 });
143540
- const databases = await client.d1.database.list({ account_id: accountId });
143541
- let databaseId = null;
143542
- for await (const db of databases) {
143543
- if (db.name === name4 && db.uuid) {
143544
- databaseId = db.uuid;
143545
- break;
143562
+ try {
143563
+ const databases = await client.d1.database.list({ account_id: accountId });
143564
+ for await (const db of databases) {
143565
+ if (db.name === name4 && db.uuid) {
143566
+ log2.debug("[Cloudflare D1] Deleting existing database", {
143567
+ name: name4,
143568
+ uuid: db.uuid
143569
+ });
143570
+ await client.d1.database.delete(db.uuid, { account_id: accountId });
143571
+ log2.info("[Cloudflare D1] Deleted existing database", { name: name4 });
143572
+ break;
143573
+ }
143546
143574
  }
143575
+ } catch (error2) {
143576
+ log2.warn("[Cloudflare D1] Failed to delete existing database", { name: name4, error: error2 });
143547
143577
  }
143548
- if (!databaseId) {
143549
- throw new Error(`D1 database not found: ${name4}`);
143550
- }
143551
- const tablesResult = await client.d1.database.query(databaseId, {
143578
+ const result = await client.d1.database.create({
143552
143579
  account_id: accountId,
143553
- sql: "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%'"
143580
+ name: name4
143554
143581
  });
143555
- const tables = tablesResult.result?.[0]?.results || [];
143556
- if (tables.length === 0) {
143557
- log2.info("[Cloudflare D1] No tables to drop", { name: name4, databaseId });
143558
- return;
143582
+ if (!result.uuid) {
143583
+ throw new Error("Database creation succeeded but no UUID returned");
143559
143584
  }
143560
- log2.debug("[Cloudflare D1] Found tables to drop", {
143561
- databaseId,
143562
- count: tables.length,
143563
- tables: tables.map((t2) => t2.name)
143564
- });
143565
- await client.d1.database.query(databaseId, {
143566
- account_id: accountId,
143567
- sql: "PRAGMA defer_foreign_keys = on"
143568
- });
143569
- for (const table14 of tables) {
143570
- if (!table14.name)
143571
- continue;
143572
- await client.d1.database.query(databaseId, {
143573
- account_id: accountId,
143574
- sql: `DROP TABLE IF EXISTS "${table14.name}"`
143575
- });
143576
- }
143577
- await client.d1.database.query(databaseId, {
143578
- account_id: accountId,
143579
- sql: "PRAGMA defer_foreign_keys = off"
143580
- });
143581
- log2.info("[Cloudflare D1] Database reset complete", {
143582
- name: name4,
143583
- databaseId,
143584
- tablesDropped: tables.length
143585
- });
143585
+ log2.info("[Cloudflare D1] Database reset complete", { name: name4, uuid: result.uuid });
143586
+ return result.uuid;
143586
143587
  }
143587
143588
  };
143588
143589
  }
@@ -144788,6 +144789,56 @@ async function verifyGameAccessBySlug(slug2, user) {
144788
144789
  function getGameWorkerApiKeyName(slug2) {
144789
144790
  return `game-worker-${slug2}`.substring(0, 32);
144790
144791
  }
144792
+ // ../api-core/src/utils/secrets-storage.ts
144793
+ var import_client_s32 = __toESM(require_dist_cjs81(), 1);
144794
+ function getSecretsConfig() {
144795
+ const bucketName = process.env.GAME_SECRETS_BUCKET;
144796
+ const masterKey = process.env.GAME_SECRETS_MASTER_KEY;
144797
+ if (!bucketName || !masterKey) {
144798
+ throw new Error("Secrets storage not configured");
144799
+ }
144800
+ const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
144801
+ const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
144802
+ const endpoint = process.env.CLOUDFLARE_R2_DEFAULT_ENDPOINT;
144803
+ if (!accessKeyId || !secretAccessKey || !endpoint) {
144804
+ throw new Error("R2 credentials not configured");
144805
+ }
144806
+ return {
144807
+ bucketName,
144808
+ credentials: {
144809
+ accessKeyId,
144810
+ secretAccessKey,
144811
+ endpoint
144812
+ },
144813
+ masterKey
144814
+ };
144815
+ }
144816
+ function getSecretsKey(gameId) {
144817
+ return `${gameId}.json.enc`;
144818
+ }
144819
+ function createR2Client(config2) {
144820
+ return new import_client_s32.S3Client({
144821
+ region: "auto",
144822
+ endpoint: config2.credentials.endpoint,
144823
+ credentials: {
144824
+ accessKeyId: config2.credentials.accessKeyId,
144825
+ secretAccessKey: config2.credentials.secretAccessKey
144826
+ }
144827
+ });
144828
+ }
144829
+ async function deleteSecrets(gameId, config2) {
144830
+ const client = createR2Client(config2);
144831
+ const key = getSecretsKey(gameId);
144832
+ try {
144833
+ await client.send(new import_client_s32.DeleteObjectCommand({
144834
+ Bucket: config2.bucketName,
144835
+ Key: key
144836
+ }));
144837
+ } catch (error2) {
144838
+ log2.error("[SecretsStorage] Failed to delete secrets", { gameId, error: error2 });
144839
+ throw new Error(`Failed to delete secrets for game ${gameId}`);
144840
+ }
144841
+ }
144791
144842
  // src/database/seed/achievements.ts
144792
144843
  async function seedAchievements(db) {
144793
144844
  const now2 = new Date;
@@ -144831,6 +144882,30 @@ async function seedCurrencies(db) {
144831
144882
  }
144832
144883
  }
144833
144884
 
144885
+ // src/lib/logging/adapter.ts
144886
+ var customLogger;
144887
+ function setLogger(logger3) {
144888
+ customLogger = logger3;
144889
+ }
144890
+ function getLogger() {
144891
+ if (customLogger) {
144892
+ return customLogger;
144893
+ }
144894
+ return {
144895
+ info: (msg) => console.log(msg),
144896
+ warn: (msg) => console.warn(msg),
144897
+ error: (msg) => console.error(msg)
144898
+ };
144899
+ }
144900
+ var logger3 = {
144901
+ info: (msg) => {
144902
+ if (customLogger || !config.embedded) {
144903
+ getLogger().info(msg);
144904
+ }
144905
+ },
144906
+ warn: (msg) => getLogger().warn(msg),
144907
+ error: (msg) => getLogger().error(msg)
144908
+ };
144834
144909
  // src/database/seed/timeback.ts
144835
144910
  function generateMockStudentId(userId) {
144836
144911
  return `mock-student-${userId.slice(-8)}`;
@@ -144895,7 +144970,7 @@ async function seedCoreGames(db) {
144895
144970
  try {
144896
144971
  await db.insert(games).values(gameData).onConflictDoNothing();
144897
144972
  } catch (error2) {
144898
- console.error(`Error seeding core game '${gameData.slug}':`, error2);
144973
+ logger3.error(`Error seeding core game '${gameData.slug}': ${error2}`);
144899
144974
  }
144900
144975
  }
144901
144976
  }
@@ -144936,7 +145011,7 @@ async function seedCurrentProjectGame(db, project) {
144936
145011
  }
144937
145012
  return newGame;
144938
145013
  } catch (error2) {
144939
- console.error("❌ Error seeding project game:", error2);
145014
+ logger3.error(`❌ Error seeding project game: ${error2}`);
144940
145015
  throw error2;
144941
145016
  }
144942
145017
  }
@@ -145063,13 +145138,6 @@ async function setupServerDatabase(processedOptions, project) {
145063
145138
 
145064
145139
  // src/server/options.ts
145065
145140
  var import_json_colorizer = __toESM(require_dist2(), 1);
145066
-
145067
- // src/lib/logging/adapter.ts
145068
- var customLogger;
145069
- function setLogger(logger3) {
145070
- customLogger = logger3;
145071
- }
145072
- // src/server/options.ts
145073
145141
  function processServerOptions(_port, options) {
145074
145142
  const {
145075
145143
  verbose = false,
@@ -145197,6 +145265,8 @@ manifestRouter.get("/", async (c3) => {
145197
145265
  { method: "GET", url: `${baseUrl}/api/notifications/stats/:userId` },
145198
145266
  { method: "POST", url: `${baseUrl}/api/notifications/deliver` },
145199
145267
  { method: "POST", url: `${baseUrl}/api/timeback/populate-student` },
145268
+ { method: "GET", url: `${baseUrl}/api/timeback/user` },
145269
+ { method: "GET", url: `${baseUrl}/api/timeback/user/:timebackId` },
145200
145270
  { method: "GET", url: `${baseUrl}/api/timeback/xp/today` },
145201
145271
  { method: "PUT", url: `${baseUrl}/api/timeback/xp/today` },
145202
145272
  { method: "GET", url: `${baseUrl}/api/timeback/xp/total` },
@@ -150456,6 +150526,10 @@ async function getMockTimebackData(db, timebackId, gameId) {
150456
150526
  });
150457
150527
  return { id: timebackId, role, enrollments, organizations };
150458
150528
  }
150529
+ async function getMockTimebackUser(db, gameId) {
150530
+ const timebackId = config.timeback.timebackId || "mock-student-00000001";
150531
+ return getMockTimebackData(db, timebackId, gameId);
150532
+ }
150459
150533
  async function buildMockUserResponse(db, user, gameId) {
150460
150534
  const timeback3 = user.timebackId ? await getMockTimebackData(db, user.timebackId, gameId) : undefined;
150461
150535
  if (gameId) {
@@ -150586,57 +150660,6 @@ usersRouter.all("/", async (c3) => {
150586
150660
  const error2 = ApiError.methodNotAllowed("Method not allowed");
150587
150661
  return c3.json(createErrorResponse(error2), 405);
150588
150662
  });
150589
- // ../api-core/src/utils/secrets-storage.ts
150590
- var import_client_s32 = __toESM(require_dist_cjs81(), 1);
150591
- function getSecretsConfig() {
150592
- const bucketName = process.env.GAME_SECRETS_BUCKET;
150593
- const masterKey = process.env.GAME_SECRETS_MASTER_KEY;
150594
- if (!bucketName || !masterKey) {
150595
- throw new Error("Secrets storage not configured");
150596
- }
150597
- const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
150598
- const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
150599
- const endpoint = process.env.CLOUDFLARE_R2_DEFAULT_ENDPOINT;
150600
- if (!accessKeyId || !secretAccessKey || !endpoint) {
150601
- throw new Error("R2 credentials not configured");
150602
- }
150603
- return {
150604
- bucketName,
150605
- credentials: {
150606
- accessKeyId,
150607
- secretAccessKey,
150608
- endpoint
150609
- },
150610
- masterKey
150611
- };
150612
- }
150613
- function getSecretsKey(gameId) {
150614
- return `${gameId}.json.enc`;
150615
- }
150616
- function createR2Client(config2) {
150617
- return new import_client_s32.S3Client({
150618
- region: "auto",
150619
- endpoint: config2.credentials.endpoint,
150620
- credentials: {
150621
- accessKeyId: config2.credentials.accessKeyId,
150622
- secretAccessKey: config2.credentials.secretAccessKey
150623
- }
150624
- });
150625
- }
150626
- async function deleteSecrets(gameId, config2) {
150627
- const client = createR2Client(config2);
150628
- const key = getSecretsKey(gameId);
150629
- try {
150630
- await client.send(new import_client_s32.DeleteObjectCommand({
150631
- Bucket: config2.bucketName,
150632
- Key: key
150633
- }));
150634
- } catch (error2) {
150635
- log2.error("[SecretsStorage] Failed to delete secrets", { gameId, error: error2 });
150636
- throw new Error(`Failed to delete secrets for game ${gameId}`);
150637
- }
150638
- }
150639
-
150640
150663
  // ../../node_modules/ulidx/dist/node/index.js
150641
150664
  import crypto6 from "node:crypto";
150642
150665
 
@@ -153033,6 +153056,7 @@ gameUploadsRouter.post("/uploads/initiate", async (c3) => {
153033
153056
  // src/routes/platform/games/verify.ts
153034
153057
  var gameVerifyRouter = new Hono2;
153035
153058
  gameVerifyRouter.post("/verify", async (c3) => {
153059
+ const clonedRequest = c3.req.raw.clone();
153036
153060
  const body2 = await c3.req.json().catch(() => ({}));
153037
153061
  const token2 = body2?.token;
153038
153062
  if (!token2) {
@@ -153061,11 +153085,43 @@ gameVerifyRouter.post("/verify", async (c3) => {
153061
153085
  }
153062
153086
  });
153063
153087
  }
153088
+ if (token2.endsWith(".sandbox")) {
153089
+ try {
153090
+ const parts2 = token2.split(".");
153091
+ if (parts2.length === 3) {
153092
+ const payload = JSON.parse(atob(parts2[1]));
153093
+ const userId = payload.uid;
153094
+ const gameIdOrSlug = payload.sub;
153095
+ const db = c3.get("db");
153096
+ const userData = await db.query.users.findFirst({
153097
+ where: (users2, { eq: eq3 }) => eq3(users2.id, userId)
153098
+ });
153099
+ if (!userData) {
153100
+ return c3.json({ error: "User not found in sandbox" }, 500);
153101
+ }
153102
+ return c3.json({
153103
+ claims: payload,
153104
+ gameId: gameIdOrSlug,
153105
+ user: {
153106
+ sub: userData.id,
153107
+ email: userData.email || "",
153108
+ name: userData.name || userData.username || "",
153109
+ email_verified: true,
153110
+ given_name: undefined,
153111
+ family_name: undefined,
153112
+ timeback_id: userData.timebackId || undefined
153113
+ }
153114
+ });
153115
+ }
153116
+ } catch {
153117
+ return c3.json({ error: "Invalid sandbox token" }, 400);
153118
+ }
153119
+ }
153064
153120
  const ctx = {
153065
153121
  user: undefined,
153066
153122
  params: {},
153067
153123
  url: new URL(c3.req.url),
153068
- request: c3.req.raw
153124
+ request: clonedRequest
153069
153125
  };
153070
153126
  try {
153071
153127
  const result = await verifyGameToken(ctx);
@@ -155212,7 +155268,7 @@ async function deliverNotifications(ctx) {
155212
155268
  if (!user) {
155213
155269
  throw ApiError.unauthorized("Must be logged in to deliver notifications");
155214
155270
  }
155215
- log2.info("[API] Delivering pending notifications", {
155271
+ log2.debug("[API] Delivering pending notifications", {
155216
155272
  userId: user.id
155217
155273
  });
155218
155274
  try {
@@ -155320,6 +155376,58 @@ notificationsRouter.post("/deliver", async (c3) => {
155320
155376
  return c3.json(createUnknownErrorResponse(error2), 500);
155321
155377
  }
155322
155378
  });
155379
+ // ../api-core/src/handlers/realtime/token.ts
155380
+ async function generateRealtimeToken(ctx) {
155381
+ const user = ctx.user;
155382
+ const gameId = ctx.params?.gameId;
155383
+ log2.debug("[API] generating realtime token", {
155384
+ userId: user?.id || "anonymous",
155385
+ gameId: gameId || "none"
155386
+ });
155387
+ if (!user) {
155388
+ throw ApiError.unauthorized("Valid session or bearer token required");
155389
+ }
155390
+ try {
155391
+ if (gameId) {
155392
+ const db = getDatabase();
155393
+ const game = await db.query.games.findFirst({
155394
+ where: eq(games.id, gameId),
155395
+ columns: { id: true }
155396
+ });
155397
+ if (!game) {
155398
+ throw ApiError.notFound("Game not found");
155399
+ }
155400
+ }
155401
+ const displayName = user.username || (user.name ? user.name.split(" ")[0] : undefined) || undefined;
155402
+ const token2 = await mintRealtimeJwt(user.id, gameId, displayName, user.role || undefined);
155403
+ return { token: token2 };
155404
+ } catch (error2) {
155405
+ if (error2 instanceof ApiError)
155406
+ throw error2;
155407
+ log2.error("[API /realtime/token] Failed to generate realtime token:", { error: error2 });
155408
+ throw ApiError.internal("Internal server error", error2);
155409
+ }
155410
+ }
155411
+ // src/routes/platform/realtime.ts
155412
+ var realtimeRouter = new Hono2;
155413
+ realtimeRouter.post("/token", async (c3) => {
155414
+ const ctx = {
155415
+ user: c3.get("user"),
155416
+ params: {},
155417
+ url: new URL(c3.req.url),
155418
+ request: c3.req.raw
155419
+ };
155420
+ try {
155421
+ const result = await generateRealtimeToken(ctx);
155422
+ return c3.json(result);
155423
+ } catch (error2) {
155424
+ if (error2 instanceof ApiError) {
155425
+ return c3.json(createErrorResponse(error2), error2.statusCode);
155426
+ }
155427
+ console.error("Error in realtime/token:", error2);
155428
+ return c3.json(createUnknownErrorResponse(error2), 500);
155429
+ }
155430
+ });
155323
155431
  // ../api-core/src/utils/timeback-errors.ts
155324
155432
  function isTimebackApiError(error2) {
155325
155433
  return typeof error2 === "object" && error2 !== null && "name" in error2 && error2.name === "TimebackApiError" && "status" in error2 && "details" in error2;
@@ -155854,24 +155962,59 @@ async function getTimeBackXpHistory(ctx) {
155854
155962
  throw ApiError.internal("Failed to get TimeBack XP history", error2);
155855
155963
  }
155856
155964
  }
155857
- // ../api-core/src/handlers/timeback/enrollments.ts
155858
- async function getStudentEnrollments(ctx) {
155965
+ // ../api-core/src/handlers/timeback/user.ts
155966
+ async function getTimebackUser(ctx) {
155967
+ const user = ctx.user;
155968
+ if (!user) {
155969
+ throw ApiError.unauthorized("Must be logged in to get timeback data");
155970
+ }
155971
+ const db = getDatabase();
155972
+ const userData = await db.query.users.findFirst({
155973
+ where: eq(users.id, user.id)
155974
+ });
155975
+ if (!userData) {
155976
+ throw ApiError.notFound("User not found");
155977
+ }
155978
+ if (!userData.timebackId) {
155979
+ throw ApiError.notFound("User does not have a TimeBack account");
155980
+ }
155981
+ log2.debug("[API] Getting timeback user data", {
155982
+ userId: user.id,
155983
+ timebackId: userData.timebackId,
155984
+ gameId: ctx.gameId
155985
+ });
155986
+ const timeback3 = await fetchUserTimebackData(userData.timebackId, ctx.gameId);
155987
+ log2.info("[API] Retrieved timeback user data", {
155988
+ userId: user.id,
155989
+ timebackId: userData.timebackId,
155990
+ role: timeback3.role,
155991
+ enrollmentCount: timeback3.enrollments.length,
155992
+ organizationCount: timeback3.organizations.length
155993
+ });
155994
+ return timeback3;
155995
+ }
155996
+ async function getTimebackUserById(ctx) {
155859
155997
  const user = ctx.user;
155860
155998
  if (!user) {
155861
- throw ApiError.unauthorized("Must be logged in to get enrollments");
155999
+ throw ApiError.unauthorized("Must be logged in to get timeback data");
155862
156000
  }
155863
156001
  const timebackId = ctx.params.timebackId;
155864
156002
  if (!timebackId) {
155865
156003
  throw ApiError.badRequest("Missing timebackId parameter");
155866
156004
  }
155867
- log2.debug("[API] Getting student enrollments", { userId: user.id, timebackId });
155868
- const enrollments = await fetchEnrollmentsFromEduBridge(timebackId);
155869
- log2.info("[API] Retrieved student enrollments", {
155870
- userId: user.id,
156005
+ log2.debug("[API] Getting timeback user by ID", {
156006
+ requesterId: user.id,
156007
+ timebackId
156008
+ });
156009
+ const timeback3 = await fetchUserTimebackData(timebackId);
156010
+ log2.info("[API] Retrieved timeback user by ID", {
156011
+ requesterId: user.id,
155871
156012
  timebackId,
155872
- enrollmentCount: enrollments.length
156013
+ role: timeback3.role,
156014
+ enrollmentCount: timeback3.enrollments.length,
156015
+ organizationCount: timeback3.organizations.length
155873
156016
  });
155874
- return { enrollments };
156017
+ return timeback3;
155875
156018
  }
155876
156019
  // src/routes/integrations/timeback.ts
155877
156020
  var timebackRouter = new Hono2;
@@ -156064,32 +156207,62 @@ timebackRouter.post("/end-activity", async (c3) => {
156064
156207
  return c3.json(createUnknownErrorResponse(error2), 500);
156065
156208
  }
156066
156209
  });
156067
- timebackRouter.get("/enrollments/:timebackId", async (c3) => {
156210
+ timebackRouter.get("/user", async (c3) => {
156211
+ const user2 = c3.get("user");
156212
+ const gameId = c3.get("gameId");
156213
+ if (!user2) {
156214
+ const error2 = ApiError.unauthorized("Must be logged in to get timeback data");
156215
+ return c3.json(createErrorResponse(error2), error2.statusCode);
156216
+ }
156217
+ try {
156218
+ if (shouldMockTimeback()) {
156219
+ const db = c3.get("db");
156220
+ const timeback3 = await getMockTimebackUser(db, gameId);
156221
+ return c3.json(timeback3);
156222
+ }
156223
+ const ctx = {
156224
+ user: user2,
156225
+ params: {},
156226
+ url: new URL(c3.req.url),
156227
+ request: c3.req.raw,
156228
+ gameId
156229
+ };
156230
+ const result = await getTimebackUser(ctx);
156231
+ return c3.json(result);
156232
+ } catch (error2) {
156233
+ if (error2 instanceof ApiError) {
156234
+ return c3.json(createErrorResponse(error2), error2.statusCode);
156235
+ }
156236
+ console.error("Error in getTimebackUser:", error2);
156237
+ return c3.json(createUnknownErrorResponse(error2), 500);
156238
+ }
156239
+ });
156240
+ timebackRouter.get("/user/:timebackId", async (c3) => {
156068
156241
  const timebackId = c3.req.param("timebackId");
156069
- const user = c3.get("user");
156070
- if (!user) {
156071
- const error2 = ApiError.unauthorized("Must be logged in to get enrollments");
156242
+ const user2 = c3.get("user");
156243
+ if (!user2) {
156244
+ const error2 = ApiError.unauthorized("Must be logged in to get timeback data");
156072
156245
  return c3.json(createErrorResponse(error2), error2.statusCode);
156073
156246
  }
156074
156247
  try {
156075
156248
  if (shouldMockTimeback()) {
156076
156249
  const db = c3.get("db");
156077
- const enrollments2 = await getMockEnrollments(db);
156078
- return c3.json({ enrollments: enrollments2 });
156250
+ const timeback3 = await getMockTimebackUser(db);
156251
+ return c3.json(timeback3);
156079
156252
  }
156080
156253
  const ctx = {
156081
- user,
156254
+ user: user2,
156082
156255
  params: { timebackId },
156083
156256
  url: new URL(c3.req.url),
156084
156257
  request: c3.req.raw
156085
156258
  };
156086
- const result = await getStudentEnrollments(ctx);
156259
+ const result = await getTimebackUserById(ctx);
156087
156260
  return c3.json(result);
156088
156261
  } catch (error2) {
156089
156262
  if (error2 instanceof ApiError) {
156090
156263
  return c3.json(createErrorResponse(error2), error2.statusCode);
156091
156264
  }
156092
- console.error("Error in getStudentEnrollments:", error2);
156265
+ console.error("Error in getTimebackUserById:", error2);
156093
156266
  return c3.json(createUnknownErrorResponse(error2), 500);
156094
156267
  }
156095
156268
  });
@@ -156126,29 +156299,29 @@ async function provisionUserFromLti(claims) {
156126
156299
  where: and(eq(accounts.accountId, ltiTimebackId), eq(accounts.providerId, providerId))
156127
156300
  });
156128
156301
  if (existingAccount) {
156129
- const user2 = await db.query.users.findFirst({
156302
+ const user3 = await db.query.users.findFirst({
156130
156303
  where: eq(users.id, existingAccount.userId)
156131
156304
  });
156132
- if (user2) {
156305
+ if (user3) {
156133
156306
  log2.info("[lti-timeback] Found existing user by LTI account linkage", {
156134
- userId: user2.id,
156307
+ userId: user3.id,
156135
156308
  ltiTimebackId
156136
156309
  });
156137
- return user2;
156310
+ return user3;
156138
156311
  }
156139
156312
  }
156140
- const user = await db.query.users.findFirst({
156313
+ const user2 = await db.query.users.findFirst({
156141
156314
  where: eq(users.email, email)
156142
156315
  });
156143
- if (user) {
156316
+ if (user2) {
156144
156317
  await db.transaction(async (tx) => {
156145
156318
  const existingLtiAccount = await tx.query.accounts.findFirst({
156146
- where: and(eq(accounts.userId, user.id), eq(accounts.providerId, providerId))
156319
+ where: and(eq(accounts.userId, user2.id), eq(accounts.providerId, providerId))
156147
156320
  });
156148
156321
  if (!existingLtiAccount) {
156149
156322
  await tx.insert(accounts).values({
156150
156323
  id: crypto7.randomUUID(),
156151
- userId: user.id,
156324
+ userId: user2.id,
156152
156325
  accountId: ltiTimebackId,
156153
156326
  providerId,
156154
156327
  accessToken: null,
@@ -156159,14 +156332,14 @@ async function provisionUserFromLti(claims) {
156159
156332
  updatedAt: new Date
156160
156333
  });
156161
156334
  log2.info("[lti-timeback] Linked existing user to LTI provider", {
156162
- userId: user.id,
156163
- email: user.email,
156335
+ userId: user2.id,
156336
+ email: user2.email,
156164
156337
  ltiTimebackId,
156165
156338
  note: "User may have different TimeBack ID from OAuth flow"
156166
156339
  });
156167
156340
  }
156168
156341
  });
156169
- return user;
156342
+ return user2;
156170
156343
  }
156171
156344
  const newUserId = crypto7.randomUUID();
156172
156345
  const createdUser = await db.transaction(async (tx) => {
@@ -156209,10 +156382,10 @@ async function processLtiLaunch(idToken) {
156209
156382
  try {
156210
156383
  const claims = await verifyLtiToken(idToken);
156211
156384
  validateLtiClaims(claims);
156212
- const user = await provisionUserFromLti(claims);
156385
+ const user2 = await provisionUserFromLti(claims);
156213
156386
  const targetUri = claims["https://purl.imsglobal.org/spec/lti/claim/target_link_uri"];
156214
156387
  return {
156215
- user,
156388
+ user: user2,
156216
156389
  targetUri,
156217
156390
  claims
156218
156391
  };
@@ -156232,45 +156405,45 @@ async function processTimeBackLtiLaunch(ctx) {
156232
156405
  }
156233
156406
  const currentHost = ctx.url.hostname;
156234
156407
  const launchResult = await processLtiLaunch(idToken);
156235
- const { user, targetUri, claims } = launchResult;
156408
+ const { user: user2, targetUri, claims } = launchResult;
156236
156409
  log2.info("[lti-timeback] User roles", {
156237
- userId: user.id,
156410
+ userId: user2.id,
156238
156411
  isLearner: LtiRoleChecks.isLearner(claims),
156239
156412
  isInstructor: LtiRoleChecks.isInstructor(claims),
156240
156413
  isAdministrator: LtiRoleChecks.isAdministrator(claims),
156241
156414
  allRoles: claims["https://purl.imsglobal.org/spec/lti/claim/roles"]
156242
156415
  });
156243
- const sessionToken = await createLtiSession(user.id);
156416
+ const sessionToken = await createLtiSession(user2.id);
156244
156417
  const redirectPath = extractRedirectPath(targetUri, currentHost);
156245
156418
  log2.info("[lti-timeback] LTI launch successful", {
156246
- userId: user.id,
156419
+ userId: user2.id,
156247
156420
  redirectPath
156248
156421
  });
156249
156422
  return {
156250
- user,
156423
+ user: user2,
156251
156424
  redirectPath,
156252
156425
  sessionToken
156253
156426
  };
156254
156427
  }
156255
156428
  async function getTimeBackLtiStatus(ctx) {
156256
- const user = ctx.user;
156257
- if (!user) {
156429
+ const user2 = ctx.user;
156430
+ if (!user2) {
156258
156431
  throw ApiError.unauthorized("Must be logged in");
156259
156432
  }
156260
156433
  const db = getDatabase();
156261
156434
  const ltiAccount = await db.query.accounts.findFirst({
156262
- where: and(eq(accounts.userId, user.id), eq(accounts.providerId, AUTH_PROVIDER_IDS.TIMEBACK_LTI))
156435
+ where: and(eq(accounts.userId, user2.id), eq(accounts.providerId, AUTH_PROVIDER_IDS.TIMEBACK_LTI))
156263
156436
  });
156264
156437
  const oauthAccount = await db.query.accounts.findFirst({
156265
- where: and(eq(accounts.userId, user.id), eq(accounts.providerId, AUTH_PROVIDER_IDS.TIMEBACK))
156438
+ where: and(eq(accounts.userId, user2.id), eq(accounts.providerId, AUTH_PROVIDER_IDS.TIMEBACK))
156266
156439
  });
156267
156440
  return {
156268
156441
  hasLtiAccount: !!ltiAccount,
156269
156442
  ltiTimebackId: ltiAccount?.accountId || undefined,
156270
156443
  hasOAuthAccount: !!oauthAccount,
156271
- oauthTimebackId: oauthAccount?.accountId || user.timebackId || undefined,
156272
- email: user.email,
156273
- userId: user.id
156444
+ oauthTimebackId: oauthAccount?.accountId || user2.timebackId || undefined,
156445
+ email: user2.email,
156446
+ userId: user2.id
156274
156447
  };
156275
156448
  }
156276
156449
 
@@ -156309,20 +156482,20 @@ ltiRouter.all("/launch", async (c3) => {
156309
156482
  }
156310
156483
  });
156311
156484
  ltiRouter.get("/status", async (c3) => {
156312
- let user = undefined;
156485
+ let user2 = undefined;
156313
156486
  const authHeader = c3.req.header("Authorization");
156314
156487
  if (authHeader?.startsWith("Bearer ")) {
156315
156488
  const token2 = authHeader.substring(7);
156316
156489
  const demoUser = DEMO_TOKENS[token2];
156317
156490
  if (demoUser) {
156318
156491
  const db = c3.get("db");
156319
- user = await db.query.users.findFirst({
156492
+ user2 = await db.query.users.findFirst({
156320
156493
  where: eq(users.id, demoUser.id)
156321
156494
  });
156322
156495
  }
156323
156496
  }
156324
156497
  const ctx = {
156325
- user,
156498
+ user: user2,
156326
156499
  params: {},
156327
156500
  url: new URL(c3.req.url),
156328
156501
  request: c3.req.raw
@@ -156357,6 +156530,7 @@ function registerRoutes(app) {
156357
156530
  app.route("/api/sprites", spriteRouter);
156358
156531
  app.route("/api/achievements", achievementsRouter);
156359
156532
  app.route("/api/notifications", notificationsRouter);
156533
+ app.route("/api/realtime", realtimeRouter);
156360
156534
  app.route("/api/dev", devRouter);
156361
156535
  app.route("/api/timeback", timebackRouter);
156362
156536
  app.route("/api/lti", ltiRouter);