@playcademy/sandbox 0.3.6 → 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/cli.js CHANGED
@@ -997,7 +997,7 @@ ${t}`), i3 = h(this, y2).getSize();
997
997
 
998
998
  // ../../node_modules/esbuild/lib/main.js
999
999
  var require_main = __commonJS((exports, module2) => {
1000
- var __dirname = "/Users/hbauer/work/projects/playcademy/node_modules/esbuild/lib", __filename = "/Users/hbauer/work/projects/playcademy/node_modules/esbuild/lib/main.js";
1000
+ 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";
1001
1001
  var __defProp2 = Object.defineProperty;
1002
1002
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
1003
1003
  var __getOwnPropNames2 = Object.getOwnPropertyNames;
@@ -86665,9 +86665,9 @@ var require_serde = __commonJS((exports, module2) => {
86665
86665
  strictParseShort: () => strictParseShort2
86666
86666
  });
86667
86667
  module2.exports = __toCommonJS(serde_exports);
86668
- var import_schema2 = require_schema();
86668
+ var import_schema3 = require_schema();
86669
86669
  var copyDocumentWithTransform2 = (source, schemaRef, transform = (_5) => _5) => {
86670
- const ns = import_schema2.NormalizedSchema.of(schemaRef);
86670
+ const ns = import_schema3.NormalizedSchema.of(schemaRef);
86671
86671
  switch (typeof source) {
86672
86672
  case "undefined":
86673
86673
  case "boolean":
@@ -87280,7 +87280,7 @@ var require_protocols = __commonJS((exports, module2) => {
87280
87280
  return "%" + c3.charCodeAt(0).toString(16).toUpperCase();
87281
87281
  });
87282
87282
  }
87283
- var import_schema2 = require_schema();
87283
+ var import_schema22 = require_schema();
87284
87284
  var import_protocol_http2 = require_dist_cjs2();
87285
87285
  var import_schema3 = require_schema();
87286
87286
  var import_serde = require_serde();
@@ -87446,7 +87446,7 @@ var require_protocols = __commonJS((exports, module2) => {
87446
87446
  const query = {};
87447
87447
  const headers = {};
87448
87448
  const endpoint = await context.endpoint();
87449
- const ns = import_schema2.NormalizedSchema.of(operationSchema?.input);
87449
+ const ns = import_schema22.NormalizedSchema.of(operationSchema?.input);
87450
87450
  const schema4 = ns.getSchema();
87451
87451
  let hasNonHttpBindingMember = false;
87452
87452
  let payload;
@@ -87463,7 +87463,7 @@ var require_protocols = __commonJS((exports, module2) => {
87463
87463
  if (endpoint) {
87464
87464
  this.updateServiceEndpoint(request3, endpoint);
87465
87465
  this.setHostPrefix(request3, operationSchema, input);
87466
- const opTraits = import_schema2.NormalizedSchema.translateTraits(operationSchema.traits);
87466
+ const opTraits = import_schema22.NormalizedSchema.translateTraits(operationSchema.traits);
87467
87467
  if (opTraits.http) {
87468
87468
  request3.method = opTraits.http[0];
87469
87469
  const [path3, search] = opTraits.http[1].split("?");
@@ -87541,7 +87541,7 @@ var require_protocols = __commonJS((exports, module2) => {
87541
87541
  if (traits.httpQueryParams) {
87542
87542
  for (const [key, val2] of Object.entries(data)) {
87543
87543
  if (!(key in query)) {
87544
- this.serializeQuery(import_schema2.NormalizedSchema.of([
87544
+ this.serializeQuery(import_schema22.NormalizedSchema.of([
87545
87545
  ns.getValueSchema(),
87546
87546
  {
87547
87547
  ...traits,
@@ -87571,12 +87571,12 @@ var require_protocols = __commonJS((exports, module2) => {
87571
87571
  }
87572
87572
  async deserializeResponse(operationSchema, context, response) {
87573
87573
  const deserializer = this.deserializer;
87574
- const ns = import_schema2.NormalizedSchema.of(operationSchema.output);
87574
+ const ns = import_schema22.NormalizedSchema.of(operationSchema.output);
87575
87575
  const dataObject = {};
87576
87576
  if (response.statusCode >= 300) {
87577
87577
  const bytes = await collectBody2(response.body, context);
87578
87578
  if (bytes.byteLength > 0) {
87579
- Object.assign(dataObject, await deserializer.read(import_schema2.SCHEMA.DOCUMENT, bytes));
87579
+ Object.assign(dataObject, await deserializer.read(import_schema22.SCHEMA.DOCUMENT, bytes));
87580
87580
  }
87581
87581
  await this.handleError(operationSchema, context, response, dataObject, this.deserializeMetadata(response));
87582
87582
  throw new Error("@smithy/core/protocols - HTTP Protocol error handler failed to throw.");
@@ -92261,7 +92261,7 @@ var require_protocols2 = __commonJS((exports, module2) => {
92261
92261
  this.serdeContext = serdeContext;
92262
92262
  }
92263
92263
  };
92264
- var import_schema2 = require_schema();
92264
+ var import_schema4 = require_schema();
92265
92265
  var import_serde2 = require_serde();
92266
92266
  var import_util_base64 = require_dist_cjs9();
92267
92267
  var import_serde = require_serde();
@@ -92352,7 +92352,7 @@ var require_protocols2 = __commonJS((exports, module2) => {
92352
92352
  }
92353
92353
  _read(schema4, value) {
92354
92354
  const isObject4 = value !== null && typeof value === "object";
92355
- const ns = import_schema2.NormalizedSchema.of(schema4);
92355
+ const ns = import_schema4.NormalizedSchema.of(schema4);
92356
92356
  if (ns.isListSchema() && Array.isArray(value)) {
92357
92357
  const listMember = ns.getValueSchema();
92358
92358
  const out2 = [];
@@ -92396,13 +92396,13 @@ var require_protocols2 = __commonJS((exports, module2) => {
92396
92396
  }
92397
92397
  if (ns.isTimestampSchema()) {
92398
92398
  const options = this.settings.timestampFormat;
92399
- const format = options.useTrait ? ns.getSchema() === import_schema2.SCHEMA.TIMESTAMP_DEFAULT ? options.default : ns.getSchema() ?? options.default : options.default;
92399
+ const format = options.useTrait ? ns.getSchema() === import_schema4.SCHEMA.TIMESTAMP_DEFAULT ? options.default : ns.getSchema() ?? options.default : options.default;
92400
92400
  switch (format) {
92401
- case import_schema2.SCHEMA.TIMESTAMP_DATE_TIME:
92401
+ case import_schema4.SCHEMA.TIMESTAMP_DATE_TIME:
92402
92402
  return (0, import_serde2.parseRfc3339DateTimeWithOffset)(value);
92403
- case import_schema2.SCHEMA.TIMESTAMP_HTTP_DATE:
92403
+ case import_schema4.SCHEMA.TIMESTAMP_HTTP_DATE:
92404
92404
  return (0, import_serde2.parseRfc7231DateTime)(value);
92405
- case import_schema2.SCHEMA.TIMESTAMP_EPOCH_SECONDS:
92405
+ case import_schema4.SCHEMA.TIMESTAMP_EPOCH_SECONDS:
92406
92406
  return (0, import_serde2.parseEpochTimestamp)(value);
92407
92407
  default:
92408
92408
  console.warn("Missing timestamp format, parsing value with Date constructor:", value);
@@ -92711,7 +92711,7 @@ var require_protocols2 = __commonJS((exports, module2) => {
92711
92711
  }
92712
92712
  };
92713
92713
  var import_protocols2 = require_protocols();
92714
- var import_schema4 = require_schema();
92714
+ var import_schema42 = require_schema();
92715
92715
  var import_util_body_length_browser2 = require_dist_cjs21();
92716
92716
  var AwsRestJsonProtocol = class extends import_protocols2.HttpBindingProtocol {
92717
92717
  static {
@@ -92727,7 +92727,7 @@ var require_protocols2 = __commonJS((exports, module2) => {
92727
92727
  const settings = {
92728
92728
  timestampFormat: {
92729
92729
  useTrait: true,
92730
- default: import_schema4.SCHEMA.TIMESTAMP_EPOCH_SECONDS
92730
+ default: import_schema42.SCHEMA.TIMESTAMP_EPOCH_SECONDS
92731
92731
  },
92732
92732
  httpBindings: true,
92733
92733
  jsonName: true
@@ -92748,7 +92748,7 @@ var require_protocols2 = __commonJS((exports, module2) => {
92748
92748
  }
92749
92749
  async serializeRequest(operationSchema, input, context) {
92750
92750
  const request3 = await super.serializeRequest(operationSchema, input, context);
92751
- const inputSchema = import_schema4.NormalizedSchema.of(operationSchema.input);
92751
+ const inputSchema = import_schema42.NormalizedSchema.of(operationSchema.input);
92752
92752
  const members = inputSchema.getMemberSchemas();
92753
92753
  if (!request3.headers["content-type"]) {
92754
92754
  const httpPayloadMember = Object.values(members).find((m5) => {
@@ -92792,19 +92792,19 @@ var require_protocols2 = __commonJS((exports, module2) => {
92792
92792
  if (errorIdentifier.includes("#")) {
92793
92793
  [namespace, errorName] = errorIdentifier.split("#");
92794
92794
  }
92795
- const registry2 = import_schema4.TypeRegistry.for(namespace);
92795
+ const registry2 = import_schema42.TypeRegistry.for(namespace);
92796
92796
  let errorSchema;
92797
92797
  try {
92798
92798
  errorSchema = registry2.getSchema(errorIdentifier);
92799
92799
  } catch (e2) {
92800
- const baseExceptionSchema = import_schema4.TypeRegistry.for("smithy.ts.sdk.synthetic." + namespace).getBaseException();
92800
+ const baseExceptionSchema = import_schema42.TypeRegistry.for("smithy.ts.sdk.synthetic." + namespace).getBaseException();
92801
92801
  if (baseExceptionSchema) {
92802
92802
  const ErrorCtor = baseExceptionSchema.ctor;
92803
92803
  throw Object.assign(new ErrorCtor(errorName), dataObject);
92804
92804
  }
92805
92805
  throw new Error(errorName);
92806
92806
  }
92807
- const ns = import_schema4.NormalizedSchema.of(errorSchema);
92807
+ const ns = import_schema42.NormalizedSchema.of(errorSchema);
92808
92808
  const message2 = dataObject.message ?? dataObject.Message ?? "Unknown";
92809
92809
  const exception = new errorSchema.ctor(message2);
92810
92810
  await this.deserializeHttpMessage(errorSchema, context, response, dataObject);
@@ -121481,7 +121481,7 @@ async function requirePortAvailable(port, timeoutMs = 100) {
121481
121481
  // package.json
121482
121482
  var package_default = {
121483
121483
  name: "@playcademy/sandbox",
121484
- version: "0.3.6",
121484
+ version: "0.3.7",
121485
121485
  description: "Local development server for Playcademy game development",
121486
121486
  type: "module",
121487
121487
  exports: {
@@ -136260,6 +136260,21 @@ async function mintGameJwt(gameId, userId) {
136260
136260
  const token = await signJwt({ uid: userId }, "game", expirationTimeSeconds, gameId);
136261
136261
  return { token, exp: expirationTimestampMillis };
136262
136262
  }
136263
+ async function mintRealtimeJwt(userId, gameId, username, role) {
136264
+ const payload = {
136265
+ sub: userId
136266
+ };
136267
+ if (gameId) {
136268
+ payload.gameId = gameId;
136269
+ }
136270
+ if (username) {
136271
+ payload.username = username;
136272
+ }
136273
+ if (role) {
136274
+ payload.role = role;
136275
+ }
136276
+ return await signJwt(payload, "realtime", "30m");
136277
+ }
136263
136278
  // ../api-core/src/utils/validation.ts
136264
136279
  function formatValidationErrors(error2) {
136265
136280
  const flattened = error2.flatten();
@@ -145364,6 +145379,17 @@ function createCustomHostnamesNamespace(config2) {
145364
145379
  }
145365
145380
  };
145366
145381
  }
145382
+ // ../cloudflare/src/utils/schema.ts
145383
+ function buildSchemaSql(sql3) {
145384
+ const trimmed = sql3.trim();
145385
+ if (!trimmed) {
145386
+ return "";
145387
+ }
145388
+ return `PRAGMA defer_foreign_keys = on;
145389
+ ${trimmed}
145390
+ PRAGMA defer_foreign_keys = off;`;
145391
+ }
145392
+
145367
145393
  // ../cloudflare/src/core/namespaces/d1.ts
145368
145394
  function createD1Namespace(config2) {
145369
145395
  const { client, accountId } = config2;
@@ -145398,26 +145424,22 @@ function createD1Namespace(config2) {
145398
145424
  async executeSchema(databaseId, schema4) {
145399
145425
  log2.debug("[Cloudflare D1] Executing schema", {
145400
145426
  databaseId,
145401
- schemaHash: schema4.hash
145427
+ schemaHash: schema4.hash,
145428
+ sqlLength: schema4.sql.length
145402
145429
  });
145403
145430
  try {
145404
- const statements = schema4.sql.split(";").map((stmt) => stmt.trim()).filter((stmt) => stmt.length > 0 && !stmt.startsWith("--"));
145405
- for (const statement of statements) {
145406
- if (statement.trim()) {
145407
- log2.debug("[Cloudflare D1] Executing SQL statement", {
145408
- databaseId,
145409
- statement: statement.substring(0, 100) + (statement.length > 100 ? "..." : "")
145410
- });
145411
- await client.d1.database.query(databaseId, {
145412
- account_id: accountId,
145413
- sql: statement
145414
- });
145415
- }
145416
- }
145431
+ const sql3 = buildSchemaSql(schema4.sql);
145432
+ log2.debug("[Cloudflare D1] Executing schema", {
145433
+ databaseId,
145434
+ sql: sql3
145435
+ });
145436
+ await client.d1.database.query(databaseId, {
145437
+ account_id: accountId,
145438
+ sql: sql3
145439
+ });
145417
145440
  log2.info("[Cloudflare D1] Schema executed", {
145418
145441
  databaseId,
145419
- schemaHash: schema4.hash,
145420
- statementsExecuted: statements.length
145442
+ schemaHash: schema4.hash
145421
145443
  });
145422
145444
  } catch (error2) {
145423
145445
  log2.error("[Cloudflare D1] Failed to execute schema", {
@@ -145447,52 +145469,31 @@ function createD1Namespace(config2) {
145447
145469
  },
145448
145470
  async reset(name4) {
145449
145471
  log2.debug("[Cloudflare D1] Resetting database", { name: name4 });
145450
- const databases = await client.d1.database.list({ account_id: accountId });
145451
- let databaseId = null;
145452
- for await (const db of databases) {
145453
- if (db.name === name4 && db.uuid) {
145454
- databaseId = db.uuid;
145455
- break;
145472
+ try {
145473
+ const databases = await client.d1.database.list({ account_id: accountId });
145474
+ for await (const db of databases) {
145475
+ if (db.name === name4 && db.uuid) {
145476
+ log2.debug("[Cloudflare D1] Deleting existing database", {
145477
+ name: name4,
145478
+ uuid: db.uuid
145479
+ });
145480
+ await client.d1.database.delete(db.uuid, { account_id: accountId });
145481
+ log2.info("[Cloudflare D1] Deleted existing database", { name: name4 });
145482
+ break;
145483
+ }
145456
145484
  }
145485
+ } catch (error2) {
145486
+ log2.warn("[Cloudflare D1] Failed to delete existing database", { name: name4, error: error2 });
145457
145487
  }
145458
- if (!databaseId) {
145459
- throw new Error(`D1 database not found: ${name4}`);
145460
- }
145461
- const tablesResult = await client.d1.database.query(databaseId, {
145488
+ const result = await client.d1.database.create({
145462
145489
  account_id: accountId,
145463
- sql: "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%'"
145490
+ name: name4
145464
145491
  });
145465
- const tables = tablesResult.result?.[0]?.results || [];
145466
- if (tables.length === 0) {
145467
- log2.info("[Cloudflare D1] No tables to drop", { name: name4, databaseId });
145468
- return;
145492
+ if (!result.uuid) {
145493
+ throw new Error("Database creation succeeded but no UUID returned");
145469
145494
  }
145470
- log2.debug("[Cloudflare D1] Found tables to drop", {
145471
- databaseId,
145472
- count: tables.length,
145473
- tables: tables.map((t2) => t2.name)
145474
- });
145475
- await client.d1.database.query(databaseId, {
145476
- account_id: accountId,
145477
- sql: "PRAGMA defer_foreign_keys = on"
145478
- });
145479
- for (const table14 of tables) {
145480
- if (!table14.name)
145481
- continue;
145482
- await client.d1.database.query(databaseId, {
145483
- account_id: accountId,
145484
- sql: `DROP TABLE IF EXISTS "${table14.name}"`
145485
- });
145486
- }
145487
- await client.d1.database.query(databaseId, {
145488
- account_id: accountId,
145489
- sql: "PRAGMA defer_foreign_keys = off"
145490
- });
145491
- log2.info("[Cloudflare D1] Database reset complete", {
145492
- name: name4,
145493
- databaseId,
145494
- tablesDropped: tables.length
145495
- });
145495
+ log2.info("[Cloudflare D1] Database reset complete", { name: name4, uuid: result.uuid });
145496
+ return result.uuid;
145496
145497
  }
145497
145498
  };
145498
145499
  }
@@ -146698,6 +146699,56 @@ async function verifyGameAccessBySlug(slug2, user) {
146698
146699
  function getGameWorkerApiKeyName(slug2) {
146699
146700
  return `game-worker-${slug2}`.substring(0, 32);
146700
146701
  }
146702
+ // ../api-core/src/utils/secrets-storage.ts
146703
+ var import_client_s32 = __toESM(require_dist_cjs81(), 1);
146704
+ function getSecretsConfig() {
146705
+ const bucketName = process.env.GAME_SECRETS_BUCKET;
146706
+ const masterKey = process.env.GAME_SECRETS_MASTER_KEY;
146707
+ if (!bucketName || !masterKey) {
146708
+ throw new Error("Secrets storage not configured");
146709
+ }
146710
+ const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
146711
+ const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
146712
+ const endpoint = process.env.CLOUDFLARE_R2_DEFAULT_ENDPOINT;
146713
+ if (!accessKeyId || !secretAccessKey || !endpoint) {
146714
+ throw new Error("R2 credentials not configured");
146715
+ }
146716
+ return {
146717
+ bucketName,
146718
+ credentials: {
146719
+ accessKeyId,
146720
+ secretAccessKey,
146721
+ endpoint
146722
+ },
146723
+ masterKey
146724
+ };
146725
+ }
146726
+ function getSecretsKey(gameId) {
146727
+ return `${gameId}.json.enc`;
146728
+ }
146729
+ function createR2Client(config2) {
146730
+ return new import_client_s32.S3Client({
146731
+ region: "auto",
146732
+ endpoint: config2.credentials.endpoint,
146733
+ credentials: {
146734
+ accessKeyId: config2.credentials.accessKeyId,
146735
+ secretAccessKey: config2.credentials.secretAccessKey
146736
+ }
146737
+ });
146738
+ }
146739
+ async function deleteSecrets(gameId, config2) {
146740
+ const client = createR2Client(config2);
146741
+ const key = getSecretsKey(gameId);
146742
+ try {
146743
+ await client.send(new import_client_s32.DeleteObjectCommand({
146744
+ Bucket: config2.bucketName,
146745
+ Key: key
146746
+ }));
146747
+ } catch (error2) {
146748
+ log2.error("[SecretsStorage] Failed to delete secrets", { gameId, error: error2 });
146749
+ throw new Error(`Failed to delete secrets for game ${gameId}`);
146750
+ }
146751
+ }
146701
146752
  // src/database/seed/achievements.ts
146702
146753
  async function seedAchievements(db) {
146703
146754
  const now2 = new Date;
@@ -146741,6 +146792,30 @@ async function seedCurrencies(db) {
146741
146792
  }
146742
146793
  }
146743
146794
 
146795
+ // src/lib/logging/adapter.ts
146796
+ var customLogger;
146797
+ function setLogger(logger3) {
146798
+ customLogger = logger3;
146799
+ }
146800
+ function getLogger() {
146801
+ if (customLogger) {
146802
+ return customLogger;
146803
+ }
146804
+ return {
146805
+ info: (msg) => console.log(msg),
146806
+ warn: (msg) => console.warn(msg),
146807
+ error: (msg) => console.error(msg)
146808
+ };
146809
+ }
146810
+ var logger3 = {
146811
+ info: (msg) => {
146812
+ if (customLogger || !config.embedded) {
146813
+ getLogger().info(msg);
146814
+ }
146815
+ },
146816
+ warn: (msg) => getLogger().warn(msg),
146817
+ error: (msg) => getLogger().error(msg)
146818
+ };
146744
146819
  // src/database/seed/timeback.ts
146745
146820
  function generateMockStudentId(userId) {
146746
146821
  return `mock-student-${userId.slice(-8)}`;
@@ -146805,7 +146880,7 @@ async function seedCoreGames(db) {
146805
146880
  try {
146806
146881
  await db.insert(games).values(gameData).onConflictDoNothing();
146807
146882
  } catch (error2) {
146808
- console.error(`Error seeding core game '${gameData.slug}':`, error2);
146883
+ logger3.error(`Error seeding core game '${gameData.slug}': ${error2}`);
146809
146884
  }
146810
146885
  }
146811
146886
  }
@@ -146846,7 +146921,7 @@ async function seedCurrentProjectGame(db, project) {
146846
146921
  }
146847
146922
  return newGame;
146848
146923
  } catch (error2) {
146849
- console.error("❌ Error seeding project game:", error2);
146924
+ logger3.error(`❌ Error seeding project game: ${error2}`);
146850
146925
  throw error2;
146851
146926
  }
146852
146927
  }
@@ -146973,13 +147048,6 @@ async function setupServerDatabase(processedOptions, project) {
146973
147048
 
146974
147049
  // src/server/options.ts
146975
147050
  var import_json_colorizer = __toESM(require_dist2(), 1);
146976
-
146977
- // src/lib/logging/adapter.ts
146978
- var customLogger;
146979
- function setLogger(logger3) {
146980
- customLogger = logger3;
146981
- }
146982
- // src/server/options.ts
146983
147051
  function processServerOptions(_port, options) {
146984
147052
  const {
146985
147053
  verbose = false,
@@ -147107,6 +147175,8 @@ manifestRouter.get("/", async (c3) => {
147107
147175
  { method: "GET", url: `${baseUrl}/api/notifications/stats/:userId` },
147108
147176
  { method: "POST", url: `${baseUrl}/api/notifications/deliver` },
147109
147177
  { method: "POST", url: `${baseUrl}/api/timeback/populate-student` },
147178
+ { method: "GET", url: `${baseUrl}/api/timeback/user` },
147179
+ { method: "GET", url: `${baseUrl}/api/timeback/user/:timebackId` },
147110
147180
  { method: "GET", url: `${baseUrl}/api/timeback/xp/today` },
147111
147181
  { method: "PUT", url: `${baseUrl}/api/timeback/xp/today` },
147112
147182
  { method: "GET", url: `${baseUrl}/api/timeback/xp/total` },
@@ -152366,6 +152436,10 @@ async function getMockTimebackData(db, timebackId, gameId) {
152366
152436
  });
152367
152437
  return { id: timebackId, role, enrollments, organizations };
152368
152438
  }
152439
+ async function getMockTimebackUser(db, gameId) {
152440
+ const timebackId = config.timeback.timebackId || "mock-student-00000001";
152441
+ return getMockTimebackData(db, timebackId, gameId);
152442
+ }
152369
152443
  async function buildMockUserResponse(db, user, gameId) {
152370
152444
  const timeback3 = user.timebackId ? await getMockTimebackData(db, user.timebackId, gameId) : undefined;
152371
152445
  if (gameId) {
@@ -152496,57 +152570,6 @@ usersRouter.all("/", async (c3) => {
152496
152570
  const error2 = ApiError.methodNotAllowed("Method not allowed");
152497
152571
  return c3.json(createErrorResponse(error2), 405);
152498
152572
  });
152499
- // ../api-core/src/utils/secrets-storage.ts
152500
- var import_client_s32 = __toESM(require_dist_cjs81(), 1);
152501
- function getSecretsConfig() {
152502
- const bucketName = process.env.GAME_SECRETS_BUCKET;
152503
- const masterKey = process.env.GAME_SECRETS_MASTER_KEY;
152504
- if (!bucketName || !masterKey) {
152505
- throw new Error("Secrets storage not configured");
152506
- }
152507
- const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
152508
- const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
152509
- const endpoint = process.env.CLOUDFLARE_R2_DEFAULT_ENDPOINT;
152510
- if (!accessKeyId || !secretAccessKey || !endpoint) {
152511
- throw new Error("R2 credentials not configured");
152512
- }
152513
- return {
152514
- bucketName,
152515
- credentials: {
152516
- accessKeyId,
152517
- secretAccessKey,
152518
- endpoint
152519
- },
152520
- masterKey
152521
- };
152522
- }
152523
- function getSecretsKey(gameId) {
152524
- return `${gameId}.json.enc`;
152525
- }
152526
- function createR2Client(config2) {
152527
- return new import_client_s32.S3Client({
152528
- region: "auto",
152529
- endpoint: config2.credentials.endpoint,
152530
- credentials: {
152531
- accessKeyId: config2.credentials.accessKeyId,
152532
- secretAccessKey: config2.credentials.secretAccessKey
152533
- }
152534
- });
152535
- }
152536
- async function deleteSecrets(gameId, config2) {
152537
- const client = createR2Client(config2);
152538
- const key = getSecretsKey(gameId);
152539
- try {
152540
- await client.send(new import_client_s32.DeleteObjectCommand({
152541
- Bucket: config2.bucketName,
152542
- Key: key
152543
- }));
152544
- } catch (error2) {
152545
- log2.error("[SecretsStorage] Failed to delete secrets", { gameId, error: error2 });
152546
- throw new Error(`Failed to delete secrets for game ${gameId}`);
152547
- }
152548
- }
152549
-
152550
152573
  // ../../node_modules/ulidx/dist/node/index.js
152551
152574
  import crypto6 from "node:crypto";
152552
152575
 
@@ -154943,6 +154966,7 @@ gameUploadsRouter.post("/uploads/initiate", async (c3) => {
154943
154966
  // src/routes/platform/games/verify.ts
154944
154967
  var gameVerifyRouter = new Hono2;
154945
154968
  gameVerifyRouter.post("/verify", async (c3) => {
154969
+ const clonedRequest = c3.req.raw.clone();
154946
154970
  const body2 = await c3.req.json().catch(() => ({}));
154947
154971
  const token2 = body2?.token;
154948
154972
  if (!token2) {
@@ -154971,11 +154995,43 @@ gameVerifyRouter.post("/verify", async (c3) => {
154971
154995
  }
154972
154996
  });
154973
154997
  }
154998
+ if (token2.endsWith(".sandbox")) {
154999
+ try {
155000
+ const parts2 = token2.split(".");
155001
+ if (parts2.length === 3) {
155002
+ const payload = JSON.parse(atob(parts2[1]));
155003
+ const userId = payload.uid;
155004
+ const gameIdOrSlug = payload.sub;
155005
+ const db = c3.get("db");
155006
+ const userData = await db.query.users.findFirst({
155007
+ where: (users2, { eq: eq3 }) => eq3(users2.id, userId)
155008
+ });
155009
+ if (!userData) {
155010
+ return c3.json({ error: "User not found in sandbox" }, 500);
155011
+ }
155012
+ return c3.json({
155013
+ claims: payload,
155014
+ gameId: gameIdOrSlug,
155015
+ user: {
155016
+ sub: userData.id,
155017
+ email: userData.email || "",
155018
+ name: userData.name || userData.username || "",
155019
+ email_verified: true,
155020
+ given_name: undefined,
155021
+ family_name: undefined,
155022
+ timeback_id: userData.timebackId || undefined
155023
+ }
155024
+ });
155025
+ }
155026
+ } catch {
155027
+ return c3.json({ error: "Invalid sandbox token" }, 400);
155028
+ }
155029
+ }
154974
155030
  const ctx = {
154975
155031
  user: undefined,
154976
155032
  params: {},
154977
155033
  url: new URL(c3.req.url),
154978
- request: c3.req.raw
155034
+ request: clonedRequest
154979
155035
  };
154980
155036
  try {
154981
155037
  const result = await verifyGameToken(ctx);
@@ -157122,7 +157178,7 @@ async function deliverNotifications(ctx) {
157122
157178
  if (!user) {
157123
157179
  throw ApiError.unauthorized("Must be logged in to deliver notifications");
157124
157180
  }
157125
- log2.info("[API] Delivering pending notifications", {
157181
+ log2.debug("[API] Delivering pending notifications", {
157126
157182
  userId: user.id
157127
157183
  });
157128
157184
  try {
@@ -157230,6 +157286,58 @@ notificationsRouter.post("/deliver", async (c3) => {
157230
157286
  return c3.json(createUnknownErrorResponse(error2), 500);
157231
157287
  }
157232
157288
  });
157289
+ // ../api-core/src/handlers/realtime/token.ts
157290
+ async function generateRealtimeToken(ctx) {
157291
+ const user = ctx.user;
157292
+ const gameId = ctx.params?.gameId;
157293
+ log2.debug("[API] generating realtime token", {
157294
+ userId: user?.id || "anonymous",
157295
+ gameId: gameId || "none"
157296
+ });
157297
+ if (!user) {
157298
+ throw ApiError.unauthorized("Valid session or bearer token required");
157299
+ }
157300
+ try {
157301
+ if (gameId) {
157302
+ const db = getDatabase();
157303
+ const game = await db.query.games.findFirst({
157304
+ where: eq(games.id, gameId),
157305
+ columns: { id: true }
157306
+ });
157307
+ if (!game) {
157308
+ throw ApiError.notFound("Game not found");
157309
+ }
157310
+ }
157311
+ const displayName = user.username || (user.name ? user.name.split(" ")[0] : undefined) || undefined;
157312
+ const token2 = await mintRealtimeJwt(user.id, gameId, displayName, user.role || undefined);
157313
+ return { token: token2 };
157314
+ } catch (error2) {
157315
+ if (error2 instanceof ApiError)
157316
+ throw error2;
157317
+ log2.error("[API /realtime/token] Failed to generate realtime token:", { error: error2 });
157318
+ throw ApiError.internal("Internal server error", error2);
157319
+ }
157320
+ }
157321
+ // src/routes/platform/realtime.ts
157322
+ var realtimeRouter = new Hono2;
157323
+ realtimeRouter.post("/token", async (c3) => {
157324
+ const ctx = {
157325
+ user: c3.get("user"),
157326
+ params: {},
157327
+ url: new URL(c3.req.url),
157328
+ request: c3.req.raw
157329
+ };
157330
+ try {
157331
+ const result = await generateRealtimeToken(ctx);
157332
+ return c3.json(result);
157333
+ } catch (error2) {
157334
+ if (error2 instanceof ApiError) {
157335
+ return c3.json(createErrorResponse(error2), error2.statusCode);
157336
+ }
157337
+ console.error("Error in realtime/token:", error2);
157338
+ return c3.json(createUnknownErrorResponse(error2), 500);
157339
+ }
157340
+ });
157233
157341
  // ../api-core/src/utils/timeback-errors.ts
157234
157342
  function isTimebackApiError(error2) {
157235
157343
  return typeof error2 === "object" && error2 !== null && "name" in error2 && error2.name === "TimebackApiError" && "status" in error2 && "details" in error2;
@@ -157764,24 +157872,59 @@ async function getTimeBackXpHistory(ctx) {
157764
157872
  throw ApiError.internal("Failed to get TimeBack XP history", error2);
157765
157873
  }
157766
157874
  }
157767
- // ../api-core/src/handlers/timeback/enrollments.ts
157768
- async function getStudentEnrollments(ctx) {
157875
+ // ../api-core/src/handlers/timeback/user.ts
157876
+ async function getTimebackUser(ctx) {
157877
+ const user = ctx.user;
157878
+ if (!user) {
157879
+ throw ApiError.unauthorized("Must be logged in to get timeback data");
157880
+ }
157881
+ const db = getDatabase();
157882
+ const userData = await db.query.users.findFirst({
157883
+ where: eq(users.id, user.id)
157884
+ });
157885
+ if (!userData) {
157886
+ throw ApiError.notFound("User not found");
157887
+ }
157888
+ if (!userData.timebackId) {
157889
+ throw ApiError.notFound("User does not have a TimeBack account");
157890
+ }
157891
+ log2.debug("[API] Getting timeback user data", {
157892
+ userId: user.id,
157893
+ timebackId: userData.timebackId,
157894
+ gameId: ctx.gameId
157895
+ });
157896
+ const timeback3 = await fetchUserTimebackData(userData.timebackId, ctx.gameId);
157897
+ log2.info("[API] Retrieved timeback user data", {
157898
+ userId: user.id,
157899
+ timebackId: userData.timebackId,
157900
+ role: timeback3.role,
157901
+ enrollmentCount: timeback3.enrollments.length,
157902
+ organizationCount: timeback3.organizations.length
157903
+ });
157904
+ return timeback3;
157905
+ }
157906
+ async function getTimebackUserById(ctx) {
157769
157907
  const user = ctx.user;
157770
157908
  if (!user) {
157771
- throw ApiError.unauthorized("Must be logged in to get enrollments");
157909
+ throw ApiError.unauthorized("Must be logged in to get timeback data");
157772
157910
  }
157773
157911
  const timebackId = ctx.params.timebackId;
157774
157912
  if (!timebackId) {
157775
157913
  throw ApiError.badRequest("Missing timebackId parameter");
157776
157914
  }
157777
- log2.debug("[API] Getting student enrollments", { userId: user.id, timebackId });
157778
- const enrollments = await fetchEnrollmentsFromEduBridge(timebackId);
157779
- log2.info("[API] Retrieved student enrollments", {
157780
- userId: user.id,
157915
+ log2.debug("[API] Getting timeback user by ID", {
157916
+ requesterId: user.id,
157917
+ timebackId
157918
+ });
157919
+ const timeback3 = await fetchUserTimebackData(timebackId);
157920
+ log2.info("[API] Retrieved timeback user by ID", {
157921
+ requesterId: user.id,
157781
157922
  timebackId,
157782
- enrollmentCount: enrollments.length
157923
+ role: timeback3.role,
157924
+ enrollmentCount: timeback3.enrollments.length,
157925
+ organizationCount: timeback3.organizations.length
157783
157926
  });
157784
- return { enrollments };
157927
+ return timeback3;
157785
157928
  }
157786
157929
  // src/routes/integrations/timeback.ts
157787
157930
  var timebackRouter = new Hono2;
@@ -157974,32 +158117,62 @@ timebackRouter.post("/end-activity", async (c3) => {
157974
158117
  return c3.json(createUnknownErrorResponse(error2), 500);
157975
158118
  }
157976
158119
  });
157977
- timebackRouter.get("/enrollments/:timebackId", async (c3) => {
158120
+ timebackRouter.get("/user", async (c3) => {
158121
+ const user2 = c3.get("user");
158122
+ const gameId = c3.get("gameId");
158123
+ if (!user2) {
158124
+ const error2 = ApiError.unauthorized("Must be logged in to get timeback data");
158125
+ return c3.json(createErrorResponse(error2), error2.statusCode);
158126
+ }
158127
+ try {
158128
+ if (shouldMockTimeback()) {
158129
+ const db = c3.get("db");
158130
+ const timeback3 = await getMockTimebackUser(db, gameId);
158131
+ return c3.json(timeback3);
158132
+ }
158133
+ const ctx = {
158134
+ user: user2,
158135
+ params: {},
158136
+ url: new URL(c3.req.url),
158137
+ request: c3.req.raw,
158138
+ gameId
158139
+ };
158140
+ const result = await getTimebackUser(ctx);
158141
+ return c3.json(result);
158142
+ } catch (error2) {
158143
+ if (error2 instanceof ApiError) {
158144
+ return c3.json(createErrorResponse(error2), error2.statusCode);
158145
+ }
158146
+ console.error("Error in getTimebackUser:", error2);
158147
+ return c3.json(createUnknownErrorResponse(error2), 500);
158148
+ }
158149
+ });
158150
+ timebackRouter.get("/user/:timebackId", async (c3) => {
157978
158151
  const timebackId = c3.req.param("timebackId");
157979
- const user = c3.get("user");
157980
- if (!user) {
157981
- const error2 = ApiError.unauthorized("Must be logged in to get enrollments");
158152
+ const user2 = c3.get("user");
158153
+ if (!user2) {
158154
+ const error2 = ApiError.unauthorized("Must be logged in to get timeback data");
157982
158155
  return c3.json(createErrorResponse(error2), error2.statusCode);
157983
158156
  }
157984
158157
  try {
157985
158158
  if (shouldMockTimeback()) {
157986
158159
  const db = c3.get("db");
157987
- const enrollments2 = await getMockEnrollments(db);
157988
- return c3.json({ enrollments: enrollments2 });
158160
+ const timeback3 = await getMockTimebackUser(db);
158161
+ return c3.json(timeback3);
157989
158162
  }
157990
158163
  const ctx = {
157991
- user,
158164
+ user: user2,
157992
158165
  params: { timebackId },
157993
158166
  url: new URL(c3.req.url),
157994
158167
  request: c3.req.raw
157995
158168
  };
157996
- const result = await getStudentEnrollments(ctx);
158169
+ const result = await getTimebackUserById(ctx);
157997
158170
  return c3.json(result);
157998
158171
  } catch (error2) {
157999
158172
  if (error2 instanceof ApiError) {
158000
158173
  return c3.json(createErrorResponse(error2), error2.statusCode);
158001
158174
  }
158002
- console.error("Error in getStudentEnrollments:", error2);
158175
+ console.error("Error in getTimebackUserById:", error2);
158003
158176
  return c3.json(createUnknownErrorResponse(error2), 500);
158004
158177
  }
158005
158178
  });
@@ -158036,29 +158209,29 @@ async function provisionUserFromLti(claims) {
158036
158209
  where: and(eq(accounts.accountId, ltiTimebackId), eq(accounts.providerId, providerId))
158037
158210
  });
158038
158211
  if (existingAccount) {
158039
- const user2 = await db.query.users.findFirst({
158212
+ const user3 = await db.query.users.findFirst({
158040
158213
  where: eq(users.id, existingAccount.userId)
158041
158214
  });
158042
- if (user2) {
158215
+ if (user3) {
158043
158216
  log2.info("[lti-timeback] Found existing user by LTI account linkage", {
158044
- userId: user2.id,
158217
+ userId: user3.id,
158045
158218
  ltiTimebackId
158046
158219
  });
158047
- return user2;
158220
+ return user3;
158048
158221
  }
158049
158222
  }
158050
- const user = await db.query.users.findFirst({
158223
+ const user2 = await db.query.users.findFirst({
158051
158224
  where: eq(users.email, email)
158052
158225
  });
158053
- if (user) {
158226
+ if (user2) {
158054
158227
  await db.transaction(async (tx) => {
158055
158228
  const existingLtiAccount = await tx.query.accounts.findFirst({
158056
- where: and(eq(accounts.userId, user.id), eq(accounts.providerId, providerId))
158229
+ where: and(eq(accounts.userId, user2.id), eq(accounts.providerId, providerId))
158057
158230
  });
158058
158231
  if (!existingLtiAccount) {
158059
158232
  await tx.insert(accounts).values({
158060
158233
  id: crypto7.randomUUID(),
158061
- userId: user.id,
158234
+ userId: user2.id,
158062
158235
  accountId: ltiTimebackId,
158063
158236
  providerId,
158064
158237
  accessToken: null,
@@ -158069,14 +158242,14 @@ async function provisionUserFromLti(claims) {
158069
158242
  updatedAt: new Date
158070
158243
  });
158071
158244
  log2.info("[lti-timeback] Linked existing user to LTI provider", {
158072
- userId: user.id,
158073
- email: user.email,
158245
+ userId: user2.id,
158246
+ email: user2.email,
158074
158247
  ltiTimebackId,
158075
158248
  note: "User may have different TimeBack ID from OAuth flow"
158076
158249
  });
158077
158250
  }
158078
158251
  });
158079
- return user;
158252
+ return user2;
158080
158253
  }
158081
158254
  const newUserId = crypto7.randomUUID();
158082
158255
  const createdUser = await db.transaction(async (tx) => {
@@ -158119,10 +158292,10 @@ async function processLtiLaunch(idToken) {
158119
158292
  try {
158120
158293
  const claims = await verifyLtiToken(idToken);
158121
158294
  validateLtiClaims(claims);
158122
- const user = await provisionUserFromLti(claims);
158295
+ const user2 = await provisionUserFromLti(claims);
158123
158296
  const targetUri = claims["https://purl.imsglobal.org/spec/lti/claim/target_link_uri"];
158124
158297
  return {
158125
- user,
158298
+ user: user2,
158126
158299
  targetUri,
158127
158300
  claims
158128
158301
  };
@@ -158142,45 +158315,45 @@ async function processTimeBackLtiLaunch(ctx) {
158142
158315
  }
158143
158316
  const currentHost = ctx.url.hostname;
158144
158317
  const launchResult = await processLtiLaunch(idToken);
158145
- const { user, targetUri, claims } = launchResult;
158318
+ const { user: user2, targetUri, claims } = launchResult;
158146
158319
  log2.info("[lti-timeback] User roles", {
158147
- userId: user.id,
158320
+ userId: user2.id,
158148
158321
  isLearner: LtiRoleChecks.isLearner(claims),
158149
158322
  isInstructor: LtiRoleChecks.isInstructor(claims),
158150
158323
  isAdministrator: LtiRoleChecks.isAdministrator(claims),
158151
158324
  allRoles: claims["https://purl.imsglobal.org/spec/lti/claim/roles"]
158152
158325
  });
158153
- const sessionToken = await createLtiSession(user.id);
158326
+ const sessionToken = await createLtiSession(user2.id);
158154
158327
  const redirectPath = extractRedirectPath(targetUri, currentHost);
158155
158328
  log2.info("[lti-timeback] LTI launch successful", {
158156
- userId: user.id,
158329
+ userId: user2.id,
158157
158330
  redirectPath
158158
158331
  });
158159
158332
  return {
158160
- user,
158333
+ user: user2,
158161
158334
  redirectPath,
158162
158335
  sessionToken
158163
158336
  };
158164
158337
  }
158165
158338
  async function getTimeBackLtiStatus(ctx) {
158166
- const user = ctx.user;
158167
- if (!user) {
158339
+ const user2 = ctx.user;
158340
+ if (!user2) {
158168
158341
  throw ApiError.unauthorized("Must be logged in");
158169
158342
  }
158170
158343
  const db = getDatabase();
158171
158344
  const ltiAccount = await db.query.accounts.findFirst({
158172
- where: and(eq(accounts.userId, user.id), eq(accounts.providerId, AUTH_PROVIDER_IDS.TIMEBACK_LTI))
158345
+ where: and(eq(accounts.userId, user2.id), eq(accounts.providerId, AUTH_PROVIDER_IDS.TIMEBACK_LTI))
158173
158346
  });
158174
158347
  const oauthAccount = await db.query.accounts.findFirst({
158175
- where: and(eq(accounts.userId, user.id), eq(accounts.providerId, AUTH_PROVIDER_IDS.TIMEBACK))
158348
+ where: and(eq(accounts.userId, user2.id), eq(accounts.providerId, AUTH_PROVIDER_IDS.TIMEBACK))
158176
158349
  });
158177
158350
  return {
158178
158351
  hasLtiAccount: !!ltiAccount,
158179
158352
  ltiTimebackId: ltiAccount?.accountId || undefined,
158180
158353
  hasOAuthAccount: !!oauthAccount,
158181
- oauthTimebackId: oauthAccount?.accountId || user.timebackId || undefined,
158182
- email: user.email,
158183
- userId: user.id
158354
+ oauthTimebackId: oauthAccount?.accountId || user2.timebackId || undefined,
158355
+ email: user2.email,
158356
+ userId: user2.id
158184
158357
  };
158185
158358
  }
158186
158359
 
@@ -158219,20 +158392,20 @@ ltiRouter.all("/launch", async (c3) => {
158219
158392
  }
158220
158393
  });
158221
158394
  ltiRouter.get("/status", async (c3) => {
158222
- let user = undefined;
158395
+ let user2 = undefined;
158223
158396
  const authHeader = c3.req.header("Authorization");
158224
158397
  if (authHeader?.startsWith("Bearer ")) {
158225
158398
  const token2 = authHeader.substring(7);
158226
158399
  const demoUser = DEMO_TOKENS[token2];
158227
158400
  if (demoUser) {
158228
158401
  const db = c3.get("db");
158229
- user = await db.query.users.findFirst({
158402
+ user2 = await db.query.users.findFirst({
158230
158403
  where: eq(users.id, demoUser.id)
158231
158404
  });
158232
158405
  }
158233
158406
  }
158234
158407
  const ctx = {
158235
- user,
158408
+ user: user2,
158236
158409
  params: {},
158237
158410
  url: new URL(c3.req.url),
158238
158411
  request: c3.req.raw
@@ -158267,6 +158440,7 @@ function registerRoutes(app) {
158267
158440
  app.route("/api/sprites", spriteRouter);
158268
158441
  app.route("/api/achievements", achievementsRouter);
158269
158442
  app.route("/api/notifications", notificationsRouter);
158443
+ app.route("/api/realtime", realtimeRouter);
158270
158444
  app.route("/api/dev", devRouter);
158271
158445
  app.route("/api/timeback", timebackRouter);
158272
158446
  app.route("/api/lti", ltiRouter);