@playcademy/sandbox 0.8.1-beta.7 → 0.9.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +92 -148
  2. package/dist/server.js +92 -148
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1128,7 +1128,7 @@ var package_default;
1128
1128
  var init_package = __esm(() => {
1129
1129
  package_default = {
1130
1130
  name: "@playcademy/sandbox",
1131
- version: "0.8.1-beta.7",
1131
+ version: "0.9.1-beta.1",
1132
1132
  description: "Local development server for Playcademy game development",
1133
1133
  type: "module",
1134
1134
  exports: {
@@ -100429,6 +100429,20 @@ function assessmentAttemptId(input) {
100429
100429
  String(input.attemptNumber)
100430
100430
  ].join("\x00"));
100431
100431
  }
100432
+ function buildAssessmentAttemptResultWhere(scope) {
100433
+ return {
100434
+ "student.sourcedId": scope.studentId,
100435
+ status: ONEROSTER_STATUS2.active,
100436
+ [`metadata.${PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY}.activityId`]: scope.activityId
100437
+ };
100438
+ }
100439
+ function buildAssessmentChildResultWhere(scope) {
100440
+ return {
100441
+ "student.sourcedId": scope.studentId,
100442
+ status: ONEROSTER_STATUS2.active,
100443
+ [`metadata.${PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY}.payload.attempt.activityId`]: scope.activityId
100444
+ };
100445
+ }
100432
100446
  async function findLatestAssessmentResult(results, match) {
100433
100447
  let latest = null;
100434
100448
  let latestTimestamp = 0;
@@ -100868,6 +100882,7 @@ var PLAYABLE_SHAPE_SET;
100868
100882
  var init_timeback_assessment_runtime_util = __esm(() => {
100869
100883
  init_src();
100870
100884
  init_assessment_runtime2();
100885
+ init_constants3();
100871
100886
  init_qti();
100872
100887
  init_types2();
100873
100888
  init_utils6();
@@ -103728,7 +103743,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103728
103743
  const startedAt = Date.now();
103729
103744
  const [candidates, results] = await Promise.all([
103730
103745
  this.candidateIntegrations(params.gameId, params.studentId, params.input),
103731
- this.studentAssessmentResults(params.studentId)
103746
+ this.activityAssessmentResults({
103747
+ studentId: params.studentId,
103748
+ activityId: params.input.activityId,
103749
+ purpose: params.input.purpose
103750
+ })
103732
103751
  ]);
103733
103752
  const context2 = await this.selectIntegration(candidates, params.input);
103734
103753
  const attemptScope = {
@@ -104791,12 +104810,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104791
104810
  standard: assessmentStandardForRow(row)
104792
104811
  })).filter((test) => !matchesMasteryStandard || matchesMasteryStandard(test.standard));
104793
104812
  }
104794
- async studentAssessmentResults(studentId) {
104813
+ async activityAssessmentResults(scope) {
104814
+ const [parents, children] = await Promise.all([
104815
+ this.collectAssessmentResults(buildAssessmentAttemptResultWhere(scope)),
104816
+ scope.purpose === "review" ? this.activityChildResults(scope) : []
104817
+ ]);
104818
+ return [...parents, ...children];
104819
+ }
104820
+ activityChildResults(scope) {
104821
+ return this.collectAssessmentResults(buildAssessmentChildResultWhere(scope));
104822
+ }
104823
+ async collectAssessmentResults(where) {
104795
104824
  const stream = this.requireClient().api.oneroster.assessmentResults.stream({
104796
- where: {
104797
- "student.sourcedId": studentId,
104798
- status: ONEROSTER_STATUS2.active
104799
- }
104825
+ where
104800
104826
  });
104801
104827
  const results = [];
104802
104828
  for await (const result of stream) {
@@ -104805,7 +104831,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104805
104831
  return results;
104806
104832
  }
104807
104833
  async listAttempts(scope) {
104808
- return this.attemptListing(await this.studentAssessmentResults(scope.studentId), scope);
104834
+ return this.attemptListing(await this.activityAssessmentResults(scope), scope);
104809
104835
  }
104810
104836
  async attemptListing(results, scope) {
104811
104837
  const attempts = new Map;
@@ -105570,7 +105596,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105570
105596
  const graded = await resumeAssessmentTranscript({
105571
105597
  suppliedTranscript,
105572
105598
  checkpointTranscript: metadata2.transcript,
105573
- recover: async () => recoverAssessmentTranscript(context2, await this.studentAssessmentResults(context2.studentId)),
105599
+ recover: async () => recoverAssessmentTranscript(context2, await this.activityChildResults({
105600
+ studentId: context2.studentId,
105601
+ activityId: context2.metadata.activityId
105602
+ })),
105574
105603
  grade: (transcript) => this.gradeTranscript(context2, transcript)
105575
105604
  });
105576
105605
  const session2 = await this.assessmentSession(context2, graded.transcript);
@@ -110720,10 +110749,10 @@ function createSandboxAuthProvider() {
110720
110749
  },
110721
110750
  async mintGameToken(gameId, userId) {
110722
110751
  const header = btoa(JSON.stringify({ alg: "none", typ: "sandbox" }));
110723
- const exp = Date.now() + 15 * 60 * 1000;
110752
+ const exp = Math.floor(Date.now() / 1000) + 15 * 60;
110724
110753
  const payload = btoa(JSON.stringify({ sub: gameId, uid: userId, exp }));
110725
110754
  const token = `${header}.${payload}.sandbox`;
110726
- return { token, exp };
110755
+ return { token, exp: exp * 1000 };
110727
110756
  },
110728
110757
  async validateGameToken(token) {
110729
110758
  try {
@@ -110731,18 +110760,9 @@ function createSandboxAuthProvider() {
110731
110760
  if (parts2.length !== 3) {
110732
110761
  return null;
110733
110762
  }
110734
- if (parts2[2] === "sandbox") {
110735
- const payload2 = JSON.parse(atob(parts2[1]));
110736
- if (payload2.sub && payload2.uid) {
110737
- if (payload2.exp && payload2.exp < Date.now()) {
110738
- log.debug("[SandboxAuthProvider] Token expired");
110739
- return null;
110740
- }
110741
- return payload2;
110742
- }
110743
- }
110744
110763
  const payload = JSON.parse(atob(parts2[1]));
110745
- if (payload.sub && payload.uid) {
110764
+ const nowSeconds = Date.now() / 1000;
110765
+ if (typeof payload?.sub === "string" && payload.sub.length > 0 && typeof payload.uid === "string" && payload.uid.length > 0 && typeof payload.exp === "number" && Number.isFinite(payload.exp) && payload.exp > nowSeconds && payload.exp < nowSeconds + MAX_TOKEN_LIFETIME_SECONDS) {
110746
110766
  return payload;
110747
110767
  }
110748
110768
  return null;
@@ -110840,7 +110860,7 @@ function createSandboxAuthProvider() {
110840
110860
  }
110841
110861
  };
110842
110862
  }
110843
- var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS, sandboxAssessmentTokens;
110863
+ var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS, MAX_TOKEN_LIFETIME_SECONDS, sandboxAssessmentTokens;
110844
110864
  var init_auth_provider = __esm(async () => {
110845
110865
  init_src6();
110846
110866
  init_constants();
@@ -110848,6 +110868,7 @@ var init_auth_provider = __esm(async () => {
110848
110868
  sandboxApiKeys = new Map;
110849
110869
  sandboxAssessmentPreparationReceipts = new Map;
110850
110870
  ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS = 5 * 60 * 1000;
110871
+ MAX_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
110851
110872
  sandboxAssessmentTokens = new TimebackCache({
110852
110873
  defaultTTL: 2 * 60 * 60 * 1000,
110853
110874
  maxSize: 1000
@@ -112875,56 +112896,13 @@ function extractBearerToken(authHeader) {
112875
112896
  }
112876
112897
  return authHeader.substring(7);
112877
112898
  }
112878
- function parseSandboxToken(token) {
112879
- try {
112880
- const parts2 = token.split(".");
112881
- if (parts2.length !== 3 || parts2[2] !== "sandbox") {
112882
- return null;
112883
- }
112884
- const header = JSON.parse(atob(parts2[0]));
112885
- if (header.typ !== "sandbox") {
112886
- return null;
112887
- }
112888
- const payload = JSON.parse(atob(parts2[1]));
112889
- if (!payload.uid || !payload.sub) {
112890
- return null;
112891
- }
112892
- return {
112893
- userId: payload.uid,
112894
- gameSlug: payload.sub
112895
- };
112896
- } catch {
112897
- return null;
112898
- }
112899
- }
112900
- function parseJwtClaims(token) {
112901
- try {
112902
- const parts2 = token.split(".");
112903
- if (parts2.length === 3 && parts2[1]) {
112904
- const payload = JSON.parse(atob(parts2[1]));
112905
- if (payload.uid) {
112906
- return {
112907
- userId: payload.uid,
112908
- gameId: payload.sub
112909
- };
112910
- }
112911
- }
112912
- } catch {}
112913
- return null;
112914
- }
112915
- function resolveAuth(token) {
112899
+ async function resolveAuth(token) {
112916
112900
  const demoUser = DEMO_TOKENS[token];
112917
112901
  if (demoUser) {
112918
112902
  return { userId: demoUser.id };
112919
112903
  }
112920
- if (token.includes(".")) {
112921
- const sandboxClaims = parseSandboxToken(token);
112922
- if (sandboxClaims) {
112923
- return sandboxClaims;
112924
- }
112925
- return parseJwtClaims(token);
112926
- }
112927
- return null;
112904
+ const payload = await sandboxAuth.validateGameToken(token);
112905
+ return payload ? { userId: payload.uid, gameId: payload.sub } : null;
112928
112906
  }
112929
112907
  async function fetchUserFromDatabase(db2, userId) {
112930
112908
  try {
@@ -112937,18 +112915,6 @@ async function fetchUserFromDatabase(db2, userId) {
112937
112915
  throw error88;
112938
112916
  }
112939
112917
  }
112940
- async function resolveGameIdFromSlug(db2, slug2) {
112941
- try {
112942
- const game2 = await db2.query.games.findFirst({
112943
- where: eq(games.slug, slug2),
112944
- columns: { id: true }
112945
- });
112946
- return game2?.id || null;
112947
- } catch (error88) {
112948
- console.error("[Auth] Error looking up game by slug:", error88);
112949
- return null;
112950
- }
112951
- }
112952
112918
  function isPublicRoute(path, exceptions) {
112953
112919
  return exceptions.some((exception) => {
112954
112920
  if (path === exception) {
@@ -112966,35 +112932,23 @@ async function authenticateRequest(c) {
112966
112932
  const apiKey = c.req.header("x-api-key");
112967
112933
  const token = bearerToken || apiKey;
112968
112934
  if (!token) {
112969
- return {
112970
- success: false,
112971
- error: "No authorization token provided",
112972
- shouldReturn404: true
112973
- };
112935
+ return { success: false, error: "No authorization token provided" };
112974
112936
  }
112975
- const resolved = resolveAuth(token);
112937
+ const resolved = await resolveAuth(token);
112976
112938
  if (!resolved) {
112977
- return {
112978
- success: false,
112979
- error: "Invalid or expired token",
112980
- shouldReturn404: true
112981
- };
112939
+ return { success: false, error: "Invalid or expired token" };
112982
112940
  }
112983
112941
  const claims = resolved;
112984
112942
  const db2 = c.get("db");
112985
112943
  if (!db2) {
112986
112944
  console.error("[Auth] Database not available in context");
112987
- return { success: false, error: "Internal server error", shouldReturn404: false };
112945
+ return { success: false, error: "Internal server error" };
112988
112946
  }
112989
112947
  const user = await fetchUserFromDatabase(db2, claims.userId);
112990
112948
  if (!user) {
112991
- return { success: false, error: "User not found or token invalid", shouldReturn404: true };
112949
+ return { success: false, error: "User not found or token invalid" };
112992
112950
  }
112993
- let gameId = claims.gameId;
112994
- if (!gameId && claims.gameSlug) {
112995
- gameId = await resolveGameIdFromSlug(db2, claims.gameSlug) ?? undefined;
112996
- }
112997
- return { success: true, user, gameId };
112951
+ return { success: true, user, gameId: claims.gameId };
112998
112952
  }
112999
112953
  function setupAuth(options = {}) {
113000
112954
  const { exceptions = [] } = options;
@@ -113014,20 +112968,19 @@ function setupAuth(options = {}) {
113014
112968
  }
113015
112969
  c.set("authError", result.error);
113016
112970
  await next();
113017
- if (result.shouldReturn404 && c.res.status === 404) {
113018
- return;
113019
- }
113020
- return c.json({ error: "Unauthorized", message: result.error }, 401);
113021
112971
  } catch (error88) {
113022
112972
  console.error("[Auth] Unexpected error in authentication middleware:", error88);
113023
112973
  return c.json({ error: "Internal Server Error", message: "Authentication failed" }, 500);
113024
112974
  }
113025
112975
  };
113026
112976
  }
113027
- var init_auth2 = __esm(() => {
112977
+ var sandboxAuth;
112978
+ var init_auth2 = __esm(async () => {
113028
112979
  init_drizzle_orm();
113029
112980
  init_tables_index();
113030
112981
  init_constants();
112982
+ await init_providers();
112983
+ sandboxAuth = createSandboxAuthProvider();
113031
112984
  });
113032
112985
 
113033
112986
  // src/server/app.ts
@@ -113046,11 +112999,11 @@ function createApp(db2, options) {
113046
112999
  }));
113047
113000
  return app;
113048
113001
  }
113049
- var init_app = __esm(() => {
113002
+ var init_app = __esm(async () => {
113050
113003
  init_dist7();
113051
113004
  init_cors();
113052
113005
  init_logger2();
113053
- init_auth2();
113006
+ await init_auth2();
113054
113007
  });
113055
113008
 
113056
113009
  // ../../node_modules/.bun/@electric-sql+pglite@0.3.16/node_modules/@electric-sql/pglite/dist/chunk-QY3QWFKW.js
@@ -169179,18 +169132,33 @@ var init_sessions = __esm(async () => {
169179
169132
  });
169180
169133
 
169181
169134
  // src/routes/platform/games/verify.ts
169182
- var gameVerifyRouter;
169135
+ function toUserInfo(userData) {
169136
+ return {
169137
+ sub: userData.id,
169138
+ email: userData.email || "",
169139
+ name: userData.name || userData.username || "",
169140
+ email_verified: true,
169141
+ given_name: undefined,
169142
+ family_name: undefined,
169143
+ timeback_id: userData.timebackId || undefined
169144
+ };
169145
+ }
169146
+ var gameVerifyRouter, sandboxAuth2;
169183
169147
  var init_verify = __esm(async () => {
169184
169148
  init_dist7();
169185
169149
  init_errors2();
169186
169150
  init_constants();
169187
169151
  init_error_handler();
169188
- await init_api3();
169152
+ await __promiseAll([
169153
+ init_api3(),
169154
+ init_providers()
169155
+ ]);
169189
169156
  gameVerifyRouter = new Hono2;
169157
+ sandboxAuth2 = createSandboxAuthProvider();
169190
169158
  gameVerifyRouter.post("/verify", async (c2) => {
169191
169159
  const body2 = await c2.req.json().catch(() => ({}));
169192
169160
  const token = body2?.token;
169193
- if (!token) {
169161
+ if (typeof token !== "string" || !token) {
169194
169162
  return c2.json({ error: "Missing token" }, 400);
169195
169163
  }
169196
169164
  const demoUser = DEMO_TOKENS[token];
@@ -169203,49 +169171,25 @@ var init_verify = __esm(async () => {
169203
169171
  return c2.json({ error: "User not found in sandbox" }, 500);
169204
169172
  }
169205
169173
  return c2.json({
169206
- claims: { sandbox: true },
169174
+ claims: { sandbox: true, exp: Math.floor(Date.now() / 1000) + 15 * 60 },
169207
169175
  gameId: MOCK_GAME_ID,
169208
- user: {
169209
- sub: userData.id,
169210
- email: userData.email || "",
169211
- name: userData.name || userData.username || "",
169212
- email_verified: true,
169213
- given_name: undefined,
169214
- family_name: undefined,
169215
- timeback_id: userData.timebackId || undefined
169216
- }
169176
+ user: toUserInfo(userData)
169217
169177
  });
169218
169178
  }
169219
169179
  if (token.endsWith(".sandbox")) {
169180
+ const payload = await sandboxAuth2.validateGameToken(token);
169181
+ if (!payload) {
169182
+ return c2.json({ error: "Invalid or expired sandbox token" }, 400);
169183
+ }
169220
169184
  try {
169221
- const parts2 = token.split(".");
169222
- if (parts2.length === 3) {
169223
- const payload = JSON.parse(atob(parts2[1]));
169224
- const userId = payload.uid;
169225
- const gameIdOrSlug = payload.sub;
169226
- const db2 = c2.get("db");
169227
- const userData = await db2.query.users.findFirst({
169228
- where: (users3, { eq: eq3 }) => eq3(users3.id, userId)
169229
- });
169230
- if (!userData) {
169231
- return c2.json({ error: "User not found in sandbox" }, 500);
169232
- }
169233
- return c2.json({
169234
- claims: payload,
169235
- gameId: gameIdOrSlug,
169236
- user: {
169237
- sub: userData.id,
169238
- email: userData.email || "",
169239
- name: userData.name || userData.username || "",
169240
- email_verified: true,
169241
- given_name: undefined,
169242
- family_name: undefined,
169243
- timeback_id: userData.timebackId || undefined
169244
- }
169245
- });
169246
- } else {
169247
- throw new Error("Malformed sandbox token: expected 3 parts");
169185
+ const db2 = c2.get("db");
169186
+ const userData = await db2.query.users.findFirst({
169187
+ where: (users3, { eq: eq3 }) => eq3(users3.id, payload.uid)
169188
+ });
169189
+ if (!userData) {
169190
+ return c2.json({ error: "User not found in sandbox" }, 500);
169248
169191
  }
169192
+ return c2.json({ claims: payload, gameId: payload.sub, user: toUserInfo(userData) });
169249
169193
  } catch {
169250
169194
  return c2.json({ error: "Invalid sandbox token" }, 400);
169251
169195
  }
@@ -169753,13 +169697,13 @@ var init_server = __esm(async () => {
169753
169697
  init_port();
169754
169698
  init_package();
169755
169699
  init_config();
169756
- init_app();
169757
169700
  init_database2();
169758
169701
  init_options();
169759
169702
  await __promiseAll([
169760
169703
  init_api3(),
169761
169704
  init_context(),
169762
- init_providers()
169705
+ init_providers(),
169706
+ init_app()
169763
169707
  ]);
169764
169708
  version5 = package_default.version;
169765
169709
  });
package/dist/server.js CHANGED
@@ -1127,7 +1127,7 @@ var package_default;
1127
1127
  var init_package = __esm(() => {
1128
1128
  package_default = {
1129
1129
  name: "@playcademy/sandbox",
1130
- version: "0.8.1-beta.7",
1130
+ version: "0.9.1-beta.1",
1131
1131
  description: "Local development server for Playcademy game development",
1132
1132
  type: "module",
1133
1133
  exports: {
@@ -100428,6 +100428,20 @@ function assessmentAttemptId(input) {
100428
100428
  String(input.attemptNumber)
100429
100429
  ].join("\x00"));
100430
100430
  }
100431
+ function buildAssessmentAttemptResultWhere(scope) {
100432
+ return {
100433
+ "student.sourcedId": scope.studentId,
100434
+ status: ONEROSTER_STATUS2.active,
100435
+ [`metadata.${PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY}.activityId`]: scope.activityId
100436
+ };
100437
+ }
100438
+ function buildAssessmentChildResultWhere(scope) {
100439
+ return {
100440
+ "student.sourcedId": scope.studentId,
100441
+ status: ONEROSTER_STATUS2.active,
100442
+ [`metadata.${PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY}.payload.attempt.activityId`]: scope.activityId
100443
+ };
100444
+ }
100431
100445
  async function findLatestAssessmentResult(results, match) {
100432
100446
  let latest = null;
100433
100447
  let latestTimestamp = 0;
@@ -100867,6 +100881,7 @@ var PLAYABLE_SHAPE_SET;
100867
100881
  var init_timeback_assessment_runtime_util = __esm(() => {
100868
100882
  init_src();
100869
100883
  init_assessment_runtime2();
100884
+ init_constants3();
100870
100885
  init_qti();
100871
100886
  init_types2();
100872
100887
  init_utils6();
@@ -103727,7 +103742,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103727
103742
  const startedAt = Date.now();
103728
103743
  const [candidates, results] = await Promise.all([
103729
103744
  this.candidateIntegrations(params.gameId, params.studentId, params.input),
103730
- this.studentAssessmentResults(params.studentId)
103745
+ this.activityAssessmentResults({
103746
+ studentId: params.studentId,
103747
+ activityId: params.input.activityId,
103748
+ purpose: params.input.purpose
103749
+ })
103731
103750
  ]);
103732
103751
  const context2 = await this.selectIntegration(candidates, params.input);
103733
103752
  const attemptScope = {
@@ -104790,12 +104809,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104790
104809
  standard: assessmentStandardForRow(row)
104791
104810
  })).filter((test) => !matchesMasteryStandard || matchesMasteryStandard(test.standard));
104792
104811
  }
104793
- async studentAssessmentResults(studentId) {
104812
+ async activityAssessmentResults(scope) {
104813
+ const [parents, children] = await Promise.all([
104814
+ this.collectAssessmentResults(buildAssessmentAttemptResultWhere(scope)),
104815
+ scope.purpose === "review" ? this.activityChildResults(scope) : []
104816
+ ]);
104817
+ return [...parents, ...children];
104818
+ }
104819
+ activityChildResults(scope) {
104820
+ return this.collectAssessmentResults(buildAssessmentChildResultWhere(scope));
104821
+ }
104822
+ async collectAssessmentResults(where) {
104794
104823
  const stream = this.requireClient().api.oneroster.assessmentResults.stream({
104795
- where: {
104796
- "student.sourcedId": studentId,
104797
- status: ONEROSTER_STATUS2.active
104798
- }
104824
+ where
104799
104825
  });
104800
104826
  const results = [];
104801
104827
  for await (const result of stream) {
@@ -104804,7 +104830,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104804
104830
  return results;
104805
104831
  }
104806
104832
  async listAttempts(scope) {
104807
- return this.attemptListing(await this.studentAssessmentResults(scope.studentId), scope);
104833
+ return this.attemptListing(await this.activityAssessmentResults(scope), scope);
104808
104834
  }
104809
104835
  async attemptListing(results, scope) {
104810
104836
  const attempts = new Map;
@@ -105569,7 +105595,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105569
105595
  const graded = await resumeAssessmentTranscript({
105570
105596
  suppliedTranscript,
105571
105597
  checkpointTranscript: metadata2.transcript,
105572
- recover: async () => recoverAssessmentTranscript(context2, await this.studentAssessmentResults(context2.studentId)),
105598
+ recover: async () => recoverAssessmentTranscript(context2, await this.activityChildResults({
105599
+ studentId: context2.studentId,
105600
+ activityId: context2.metadata.activityId
105601
+ })),
105573
105602
  grade: (transcript) => this.gradeTranscript(context2, transcript)
105574
105603
  });
105575
105604
  const session2 = await this.assessmentSession(context2, graded.transcript);
@@ -110719,10 +110748,10 @@ function createSandboxAuthProvider() {
110719
110748
  },
110720
110749
  async mintGameToken(gameId, userId) {
110721
110750
  const header = btoa(JSON.stringify({ alg: "none", typ: "sandbox" }));
110722
- const exp = Date.now() + 15 * 60 * 1000;
110751
+ const exp = Math.floor(Date.now() / 1000) + 15 * 60;
110723
110752
  const payload = btoa(JSON.stringify({ sub: gameId, uid: userId, exp }));
110724
110753
  const token = `${header}.${payload}.sandbox`;
110725
- return { token, exp };
110754
+ return { token, exp: exp * 1000 };
110726
110755
  },
110727
110756
  async validateGameToken(token) {
110728
110757
  try {
@@ -110730,18 +110759,9 @@ function createSandboxAuthProvider() {
110730
110759
  if (parts2.length !== 3) {
110731
110760
  return null;
110732
110761
  }
110733
- if (parts2[2] === "sandbox") {
110734
- const payload2 = JSON.parse(atob(parts2[1]));
110735
- if (payload2.sub && payload2.uid) {
110736
- if (payload2.exp && payload2.exp < Date.now()) {
110737
- log.debug("[SandboxAuthProvider] Token expired");
110738
- return null;
110739
- }
110740
- return payload2;
110741
- }
110742
- }
110743
110762
  const payload = JSON.parse(atob(parts2[1]));
110744
- if (payload.sub && payload.uid) {
110763
+ const nowSeconds = Date.now() / 1000;
110764
+ if (typeof payload?.sub === "string" && payload.sub.length > 0 && typeof payload.uid === "string" && payload.uid.length > 0 && typeof payload.exp === "number" && Number.isFinite(payload.exp) && payload.exp > nowSeconds && payload.exp < nowSeconds + MAX_TOKEN_LIFETIME_SECONDS) {
110745
110765
  return payload;
110746
110766
  }
110747
110767
  return null;
@@ -110839,7 +110859,7 @@ function createSandboxAuthProvider() {
110839
110859
  }
110840
110860
  };
110841
110861
  }
110842
- var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS, sandboxAssessmentTokens;
110862
+ var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS, MAX_TOKEN_LIFETIME_SECONDS, sandboxAssessmentTokens;
110843
110863
  var init_auth_provider = __esm(async () => {
110844
110864
  init_src6();
110845
110865
  init_constants();
@@ -110847,6 +110867,7 @@ var init_auth_provider = __esm(async () => {
110847
110867
  sandboxApiKeys = new Map;
110848
110868
  sandboxAssessmentPreparationReceipts = new Map;
110849
110869
  ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS = 5 * 60 * 1000;
110870
+ MAX_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
110850
110871
  sandboxAssessmentTokens = new TimebackCache({
110851
110872
  defaultTTL: 2 * 60 * 60 * 1000,
110852
110873
  maxSize: 1000
@@ -112874,56 +112895,13 @@ function extractBearerToken(authHeader) {
112874
112895
  }
112875
112896
  return authHeader.substring(7);
112876
112897
  }
112877
- function parseSandboxToken(token) {
112878
- try {
112879
- const parts2 = token.split(".");
112880
- if (parts2.length !== 3 || parts2[2] !== "sandbox") {
112881
- return null;
112882
- }
112883
- const header = JSON.parse(atob(parts2[0]));
112884
- if (header.typ !== "sandbox") {
112885
- return null;
112886
- }
112887
- const payload = JSON.parse(atob(parts2[1]));
112888
- if (!payload.uid || !payload.sub) {
112889
- return null;
112890
- }
112891
- return {
112892
- userId: payload.uid,
112893
- gameSlug: payload.sub
112894
- };
112895
- } catch {
112896
- return null;
112897
- }
112898
- }
112899
- function parseJwtClaims(token) {
112900
- try {
112901
- const parts2 = token.split(".");
112902
- if (parts2.length === 3 && parts2[1]) {
112903
- const payload = JSON.parse(atob(parts2[1]));
112904
- if (payload.uid) {
112905
- return {
112906
- userId: payload.uid,
112907
- gameId: payload.sub
112908
- };
112909
- }
112910
- }
112911
- } catch {}
112912
- return null;
112913
- }
112914
- function resolveAuth(token) {
112898
+ async function resolveAuth(token) {
112915
112899
  const demoUser = DEMO_TOKENS[token];
112916
112900
  if (demoUser) {
112917
112901
  return { userId: demoUser.id };
112918
112902
  }
112919
- if (token.includes(".")) {
112920
- const sandboxClaims = parseSandboxToken(token);
112921
- if (sandboxClaims) {
112922
- return sandboxClaims;
112923
- }
112924
- return parseJwtClaims(token);
112925
- }
112926
- return null;
112903
+ const payload = await sandboxAuth.validateGameToken(token);
112904
+ return payload ? { userId: payload.uid, gameId: payload.sub } : null;
112927
112905
  }
112928
112906
  async function fetchUserFromDatabase(db2, userId) {
112929
112907
  try {
@@ -112936,18 +112914,6 @@ async function fetchUserFromDatabase(db2, userId) {
112936
112914
  throw error88;
112937
112915
  }
112938
112916
  }
112939
- async function resolveGameIdFromSlug(db2, slug2) {
112940
- try {
112941
- const game2 = await db2.query.games.findFirst({
112942
- where: eq(games.slug, slug2),
112943
- columns: { id: true }
112944
- });
112945
- return game2?.id || null;
112946
- } catch (error88) {
112947
- console.error("[Auth] Error looking up game by slug:", error88);
112948
- return null;
112949
- }
112950
- }
112951
112917
  function isPublicRoute(path, exceptions) {
112952
112918
  return exceptions.some((exception) => {
112953
112919
  if (path === exception) {
@@ -112965,35 +112931,23 @@ async function authenticateRequest(c) {
112965
112931
  const apiKey = c.req.header("x-api-key");
112966
112932
  const token = bearerToken || apiKey;
112967
112933
  if (!token) {
112968
- return {
112969
- success: false,
112970
- error: "No authorization token provided",
112971
- shouldReturn404: true
112972
- };
112934
+ return { success: false, error: "No authorization token provided" };
112973
112935
  }
112974
- const resolved = resolveAuth(token);
112936
+ const resolved = await resolveAuth(token);
112975
112937
  if (!resolved) {
112976
- return {
112977
- success: false,
112978
- error: "Invalid or expired token",
112979
- shouldReturn404: true
112980
- };
112938
+ return { success: false, error: "Invalid or expired token" };
112981
112939
  }
112982
112940
  const claims = resolved;
112983
112941
  const db2 = c.get("db");
112984
112942
  if (!db2) {
112985
112943
  console.error("[Auth] Database not available in context");
112986
- return { success: false, error: "Internal server error", shouldReturn404: false };
112944
+ return { success: false, error: "Internal server error" };
112987
112945
  }
112988
112946
  const user = await fetchUserFromDatabase(db2, claims.userId);
112989
112947
  if (!user) {
112990
- return { success: false, error: "User not found or token invalid", shouldReturn404: true };
112948
+ return { success: false, error: "User not found or token invalid" };
112991
112949
  }
112992
- let gameId = claims.gameId;
112993
- if (!gameId && claims.gameSlug) {
112994
- gameId = await resolveGameIdFromSlug(db2, claims.gameSlug) ?? undefined;
112995
- }
112996
- return { success: true, user, gameId };
112950
+ return { success: true, user, gameId: claims.gameId };
112997
112951
  }
112998
112952
  function setupAuth(options = {}) {
112999
112953
  const { exceptions = [] } = options;
@@ -113013,20 +112967,19 @@ function setupAuth(options = {}) {
113013
112967
  }
113014
112968
  c.set("authError", result.error);
113015
112969
  await next();
113016
- if (result.shouldReturn404 && c.res.status === 404) {
113017
- return;
113018
- }
113019
- return c.json({ error: "Unauthorized", message: result.error }, 401);
113020
112970
  } catch (error88) {
113021
112971
  console.error("[Auth] Unexpected error in authentication middleware:", error88);
113022
112972
  return c.json({ error: "Internal Server Error", message: "Authentication failed" }, 500);
113023
112973
  }
113024
112974
  };
113025
112975
  }
113026
- var init_auth2 = __esm(() => {
112976
+ var sandboxAuth;
112977
+ var init_auth2 = __esm(async () => {
113027
112978
  init_drizzle_orm();
113028
112979
  init_tables_index();
113029
112980
  init_constants();
112981
+ await init_providers();
112982
+ sandboxAuth = createSandboxAuthProvider();
113030
112983
  });
113031
112984
 
113032
112985
  // src/server/app.ts
@@ -113045,11 +112998,11 @@ function createApp(db2, options) {
113045
112998
  }));
113046
112999
  return app;
113047
113000
  }
113048
- var init_app = __esm(() => {
113001
+ var init_app = __esm(async () => {
113049
113002
  init_dist7();
113050
113003
  init_cors();
113051
113004
  init_logger2();
113052
- init_auth2();
113005
+ await init_auth2();
113053
113006
  });
113054
113007
 
113055
113008
  // ../../node_modules/.bun/@electric-sql+pglite@0.3.16/node_modules/@electric-sql/pglite/dist/chunk-QY3QWFKW.js
@@ -169178,18 +169131,33 @@ var init_sessions = __esm(async () => {
169178
169131
  });
169179
169132
 
169180
169133
  // src/routes/platform/games/verify.ts
169181
- var gameVerifyRouter;
169134
+ function toUserInfo(userData) {
169135
+ return {
169136
+ sub: userData.id,
169137
+ email: userData.email || "",
169138
+ name: userData.name || userData.username || "",
169139
+ email_verified: true,
169140
+ given_name: undefined,
169141
+ family_name: undefined,
169142
+ timeback_id: userData.timebackId || undefined
169143
+ };
169144
+ }
169145
+ var gameVerifyRouter, sandboxAuth2;
169182
169146
  var init_verify = __esm(async () => {
169183
169147
  init_dist7();
169184
169148
  init_errors2();
169185
169149
  init_constants();
169186
169150
  init_error_handler();
169187
- await init_api3();
169151
+ await __promiseAll([
169152
+ init_api3(),
169153
+ init_providers()
169154
+ ]);
169188
169155
  gameVerifyRouter = new Hono2;
169156
+ sandboxAuth2 = createSandboxAuthProvider();
169189
169157
  gameVerifyRouter.post("/verify", async (c2) => {
169190
169158
  const body2 = await c2.req.json().catch(() => ({}));
169191
169159
  const token = body2?.token;
169192
- if (!token) {
169160
+ if (typeof token !== "string" || !token) {
169193
169161
  return c2.json({ error: "Missing token" }, 400);
169194
169162
  }
169195
169163
  const demoUser = DEMO_TOKENS[token];
@@ -169202,49 +169170,25 @@ var init_verify = __esm(async () => {
169202
169170
  return c2.json({ error: "User not found in sandbox" }, 500);
169203
169171
  }
169204
169172
  return c2.json({
169205
- claims: { sandbox: true },
169173
+ claims: { sandbox: true, exp: Math.floor(Date.now() / 1000) + 15 * 60 },
169206
169174
  gameId: MOCK_GAME_ID,
169207
- user: {
169208
- sub: userData.id,
169209
- email: userData.email || "",
169210
- name: userData.name || userData.username || "",
169211
- email_verified: true,
169212
- given_name: undefined,
169213
- family_name: undefined,
169214
- timeback_id: userData.timebackId || undefined
169215
- }
169175
+ user: toUserInfo(userData)
169216
169176
  });
169217
169177
  }
169218
169178
  if (token.endsWith(".sandbox")) {
169179
+ const payload = await sandboxAuth2.validateGameToken(token);
169180
+ if (!payload) {
169181
+ return c2.json({ error: "Invalid or expired sandbox token" }, 400);
169182
+ }
169219
169183
  try {
169220
- const parts2 = token.split(".");
169221
- if (parts2.length === 3) {
169222
- const payload = JSON.parse(atob(parts2[1]));
169223
- const userId = payload.uid;
169224
- const gameIdOrSlug = payload.sub;
169225
- const db2 = c2.get("db");
169226
- const userData = await db2.query.users.findFirst({
169227
- where: (users3, { eq: eq3 }) => eq3(users3.id, userId)
169228
- });
169229
- if (!userData) {
169230
- return c2.json({ error: "User not found in sandbox" }, 500);
169231
- }
169232
- return c2.json({
169233
- claims: payload,
169234
- gameId: gameIdOrSlug,
169235
- user: {
169236
- sub: userData.id,
169237
- email: userData.email || "",
169238
- name: userData.name || userData.username || "",
169239
- email_verified: true,
169240
- given_name: undefined,
169241
- family_name: undefined,
169242
- timeback_id: userData.timebackId || undefined
169243
- }
169244
- });
169245
- } else {
169246
- throw new Error("Malformed sandbox token: expected 3 parts");
169184
+ const db2 = c2.get("db");
169185
+ const userData = await db2.query.users.findFirst({
169186
+ where: (users3, { eq: eq3 }) => eq3(users3.id, payload.uid)
169187
+ });
169188
+ if (!userData) {
169189
+ return c2.json({ error: "User not found in sandbox" }, 500);
169247
169190
  }
169191
+ return c2.json({ claims: payload, gameId: payload.sub, user: toUserInfo(userData) });
169248
169192
  } catch {
169249
169193
  return c2.json({ error: "Invalid sandbox token" }, 400);
169250
169194
  }
@@ -169752,13 +169696,13 @@ var init_server = __esm(async () => {
169752
169696
  init_port();
169753
169697
  init_package();
169754
169698
  init_config();
169755
- init_app();
169756
169699
  init_database2();
169757
169700
  init_options();
169758
169701
  await __promiseAll([
169759
169702
  init_api3(),
169760
169703
  init_context(),
169761
- init_providers()
169704
+ init_providers(),
169705
+ init_app()
169762
169706
  ]);
169763
169707
  version5 = package_default.version;
169764
169708
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.8.1-beta.7",
3
+ "version": "0.9.1-beta.1",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {