@playcademy/sandbox 0.3.17-beta.36 → 0.3.17-beta.38

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
@@ -338,7 +338,8 @@ var PLAYCADEMY_BASE_URLS, GAME_WORKER_DOMAINS;
338
338
  var init_domains = __esm(() => {
339
339
  PLAYCADEMY_BASE_URLS = {
340
340
  production: "https://hub.playcademy.net",
341
- staging: "https://hub.dev.playcademy.net"
341
+ staging: "https://hub.dev.playcademy.net",
342
+ local: "http://localhost:5174"
342
343
  };
343
344
  GAME_WORKER_DOMAINS = {
344
345
  production: "playcademy.gg",
@@ -446,7 +447,8 @@ var WORKER_NAMING, SECRETS_PREFIX = "secrets_", CLOUDFLARE_COMPATIBILITY_DATE =
446
447
  var init_workers = __esm(() => {
447
448
  WORKER_NAMING = {
448
449
  STAGING_PREFIX: "staging-",
449
- STAGING_SUFFIX: "-staging"
450
+ STAGING_SUFFIX: "-staging",
451
+ LOCAL_PREFIX: "local-"
450
452
  };
451
453
  });
452
454
 
@@ -1329,7 +1331,7 @@ var package_default;
1329
1331
  var init_package = __esm(() => {
1330
1332
  package_default = {
1331
1333
  name: "@playcademy/sandbox",
1332
- version: "0.3.17-beta.36",
1334
+ version: "0.3.17-beta.38",
1333
1335
  description: "Local development server for Playcademy game development",
1334
1336
  type: "module",
1335
1337
  exports: {
@@ -5874,21 +5876,16 @@ var init_esm = __esm(() => {
5874
5876
  // ../api-core/src/config/schema.ts
5875
5877
  function createMinimalConfig(overrides) {
5876
5878
  return apiConfigSchema.parse({
5877
- stage: "local",
5878
- isLocal: false,
5879
+ sstStage: "test",
5879
5880
  ...overrides
5880
5881
  });
5881
5882
  }
5882
5883
  function getPlatformEnvironment(config2) {
5883
- return config2.stage === "production" ? "production" : "staging";
5884
+ return config2.sstStage === "production" ? "production" : "staging";
5884
5885
  }
5885
- function isProduction2(config2) {
5886
- return config2.stage === "production";
5887
- }
5888
- var stageSchema, ltiConfigSchema, realtimeConfigSchema, apiConfigSchema;
5886
+ var ltiConfigSchema, realtimeConfigSchema, apiConfigSchema;
5889
5887
  var init_schema = __esm(() => {
5890
5888
  init_esm();
5891
- stageSchema = exports_external.enum(["production", "dev", "local"]);
5892
5889
  ltiConfigSchema = exports_external.object({
5893
5890
  audience: exports_external.string(),
5894
5891
  jwksUrl: exports_external.string().url(),
@@ -5899,7 +5896,7 @@ var init_schema = __esm(() => {
5899
5896
  publishSecret: exports_external.string()
5900
5897
  });
5901
5898
  apiConfigSchema = exports_external.object({
5902
- stage: stageSchema,
5899
+ sstStage: exports_external.string(),
5903
5900
  isLocal: exports_external.boolean().default(false),
5904
5901
  baseUrl: exports_external.string().url().optional(),
5905
5902
  gameDomain: exports_external.string().optional(),
@@ -11296,8 +11293,9 @@ var init_pg_core = __esm(() => {
11296
11293
  });
11297
11294
 
11298
11295
  // ../data/src/domains/game/table.ts
11299
- var gamePlatformEnum, gameTypeEnum, gameVisibilityEnum, games, gameSessions, gameStates, deploymentProviderEnum, deployJobStatusEnum, gameDeployments, gameDeployJobs, customHostnameStatusEnum, customHostnameSslStatusEnum, customHostnameEnvironmentEnum, gameCustomHostnames;
11296
+ var gamePlatformEnum, gameTypeEnum, gameVisibilityEnum, games, gameMemberRoleEnum, gameMembers, gameMembersRelations, gameSessions, gameStates, deploymentProviderEnum, deployJobStatusEnum, gameDeployments, gameDeployJobs, customHostnameStatusEnum, customHostnameSslStatusEnum, customHostnameEnvironmentEnum, gameCustomHostnames;
11300
11297
  var init_table3 = __esm(() => {
11298
+ init_drizzle_orm();
11301
11299
  init_pg_core();
11302
11300
  init_table5();
11303
11301
  init_table6();
@@ -11306,9 +11304,6 @@ var init_table3 = __esm(() => {
11306
11304
  gameVisibilityEnum = pgEnum("game_visibility", ["visible", "unlisted", "internal"]);
11307
11305
  games = pgTable("games", {
11308
11306
  id: uuid("id").primaryKey().defaultRandom(),
11309
- developerId: text("developer_id").references(() => users.id, {
11310
- onDelete: "set null"
11311
- }),
11312
11307
  slug: varchar("slug", { length: 255 }).notNull().unique(),
11313
11308
  displayName: varchar("display_name", { length: 255 }).notNull(),
11314
11309
  version: varchar("version", { length: 50 }).notNull(),
@@ -11324,6 +11319,24 @@ var init_table3 = __esm(() => {
11324
11319
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
11325
11320
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow()
11326
11321
  });
11322
+ gameMemberRoleEnum = pgEnum("game_member_role", ["owner", "collaborator"]);
11323
+ gameMembers = pgTable("game_members", {
11324
+ id: uuid("id").primaryKey().defaultRandom(),
11325
+ gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
11326
+ userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
11327
+ role: gameMemberRoleEnum("role").notNull().default("collaborator"),
11328
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
11329
+ }, (table3) => [uniqueIndex("game_members_game_user_idx").on(table3.gameId, table3.userId)]);
11330
+ gameMembersRelations = relations(gameMembers, ({ one }) => ({
11331
+ game: one(games, {
11332
+ fields: [gameMembers.gameId],
11333
+ references: [games.id]
11334
+ }),
11335
+ user: one(users, {
11336
+ fields: [gameMembers.userId],
11337
+ references: [users.id]
11338
+ })
11339
+ }));
11327
11340
  gameSessions = pgTable("game_sessions", {
11328
11341
  id: uuid("id").primaryKey().defaultRandom(),
11329
11342
  userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
@@ -12141,6 +12154,9 @@ __export(exports_tables_index, {
12141
12154
  gameScoresRelations: () => gameScoresRelations,
12142
12155
  gameScores: () => gameScores,
12143
12156
  gamePlatformEnum: () => gamePlatformEnum,
12157
+ gameMembersRelations: () => gameMembersRelations,
12158
+ gameMembers: () => gameMembers,
12159
+ gameMemberRoleEnum: () => gameMemberRoleEnum,
12144
12160
  gameDeployments: () => gameDeployments,
12145
12161
  gameDeployJobs: () => gameDeployJobs,
12146
12162
  gameCustomHostnames: () => gameCustomHostnames,
@@ -26272,9 +26288,38 @@ var init_playcademy = __esm(() => {
26272
26288
  init_infra();
26273
26289
  });
26274
26290
 
26291
+ // ../utils/src/tunnel.ts
26292
+ async function getTunnelUrl() {
26293
+ let response;
26294
+ try {
26295
+ response = await fetch(`${METRICS_BASE}/config`);
26296
+ } catch {
26297
+ throw new Error("Local tunnel is not running. Start it with `bun dev` or `bun scripts/infra/tunnel.ts`.");
26298
+ }
26299
+ if (!response.ok) {
26300
+ throw new Error(`Tunnel metrics endpoint returned ${response.status}`);
26301
+ }
26302
+ const data = await response.json();
26303
+ const hostname = data.config.ingress.find((r) => r.hostname)?.hostname;
26304
+ if (!hostname) {
26305
+ throw new Error("Tunnel is running but no hostname found in ingress config");
26306
+ }
26307
+ return `https://${hostname}`;
26308
+ }
26309
+ var TUNNEL_METRICS_PORT = 20241, METRICS_BASE;
26310
+ var init_tunnel = __esm(() => {
26311
+ METRICS_BASE = `http://127.0.0.1:${TUNNEL_METRICS_PORT}`;
26312
+ });
26313
+
26275
26314
  // ../api-core/src/utils/deployment.util.ts
26276
- function getDeploymentId(gameSlug, isProduction3) {
26277
- return isProduction3 ? gameSlug : `${WORKER_NAMING.STAGING_PREFIX}${gameSlug}`;
26315
+ function getDeploymentId(gameSlug, sstStage) {
26316
+ if (sstStage === "production") {
26317
+ return gameSlug;
26318
+ }
26319
+ if (sstStage === "dev") {
26320
+ return `${WORKER_NAMING.STAGING_PREFIX}${gameSlug}`;
26321
+ }
26322
+ return `${WORKER_NAMING.LOCAL_PREFIX}${sstStage}-${gameSlug}`;
26278
26323
  }
26279
26324
  function getGameWorkerApiKeyName(slug) {
26280
26325
  return `game-worker-${slug}`.substring(0, 32);
@@ -26499,8 +26544,7 @@ class DeployService {
26499
26544
  const game = await this.deps.validateDeveloperAccessBySlug(user, slug);
26500
26545
  const flags2 = this.validateDeployRequest(request, slug);
26501
26546
  const { hasBackend, hasFrontend } = flags2;
26502
- const isProd = isProduction2(this.deps.config);
26503
- const deploymentId = getDeploymentId(slug, isProd);
26547
+ const deploymentId = getDeploymentId(slug, this.deps.config.sstStage);
26504
26548
  let frontendAssetsPath;
26505
26549
  let tempDir;
26506
26550
  if (hasFrontend) {
@@ -26514,7 +26558,15 @@ class DeployService {
26514
26558
  frontendAssetsPath = extracted.assetsPath;
26515
26559
  yield { type: "status", data: { message: "Extracting assets" } };
26516
26560
  }
26517
- const env = { GAME_ID: game.id, PLAYCADEMY_BASE_URL: this.deps.config.baseUrl };
26561
+ let platformBaseUrl = this.deps.config.baseUrl;
26562
+ if (this.deps.config.isLocal) {
26563
+ try {
26564
+ platformBaseUrl = await getTunnelUrl();
26565
+ } catch {
26566
+ throw new ValidationError("Local tunnel is not running. Ensure cloudflared is installed (`brew install cloudflared`) and the tunnel DevCommand started successfully.");
26567
+ }
26568
+ }
26569
+ const env = { GAME_ID: game.id, PLAYCADEMY_BASE_URL: platformBaseUrl };
26518
26570
  yield {
26519
26571
  type: "status",
26520
26572
  data: { message: hasBackend ? "Deploying backend code" : "Deploying to platform" }
@@ -26563,14 +26615,14 @@ class DeployService {
26563
26615
  return;
26564
26616
  }
26565
26617
  const workerBindings = {};
26566
- if (bindings?.database?.length) {
26567
- workerBindings.d1 = bindings.database;
26618
+ if (bindings?.database) {
26619
+ workerBindings.d1 = [deploymentId];
26568
26620
  }
26569
- if (bindings?.keyValue?.length) {
26570
- workerBindings.kv = bindings.keyValue;
26621
+ if (bindings?.keyValue) {
26622
+ workerBindings.kv = [deploymentId];
26571
26623
  }
26572
- if (bindings?.bucket?.length) {
26573
- workerBindings.r2 = bindings.bucket;
26624
+ if (bindings?.bucket) {
26625
+ workerBindings.r2 = [deploymentId];
26574
26626
  }
26575
26627
  if (bindings?.queues) {
26576
26628
  let toQueueName = function(queueKey) {
@@ -26637,7 +26689,7 @@ var init_deploy_service = __esm(() => {
26637
26689
  init_src();
26638
26690
  init_tables_index();
26639
26691
  init_src2();
26640
- init_config2();
26692
+ init_tunnel();
26641
26693
  init_errors();
26642
26694
  init_deployment_util();
26643
26695
  logger3 = log.scope("DeployService");
@@ -26775,29 +26827,33 @@ var init_game_service = __esm(() => {
26775
26827
  const db2 = this.deps.db;
26776
26828
  const isAdmin = caller?.role === "admin";
26777
26829
  const isDeveloper = caller?.role === "developer";
26778
- let whereClause;
26779
26830
  if (isAdmin) {
26780
- whereClause = undefined;
26781
- } else if (isDeveloper && caller?.id) {
26782
- whereClause = or(ne(games.visibility, "internal"), eq(games.developerId, caller.id));
26783
- } else {
26784
- whereClause = ne(games.visibility, "internal");
26831
+ return db2.query.games.findMany({
26832
+ orderBy: [desc(games.createdAt)]
26833
+ });
26834
+ }
26835
+ if (isDeveloper && caller?.id) {
26836
+ const rows = await db2.select().from(games).where(or(ne(games.visibility, "internal"), exists(db2.select({ one: sql`1` }).from(gameMembers).where(and(eq(gameMembers.gameId, games.id), eq(gameMembers.userId, caller.id)))))).orderBy(desc(games.createdAt));
26837
+ return rows;
26785
26838
  }
26786
26839
  return db2.query.games.findMany({
26787
- where: whereClause,
26840
+ where: ne(games.visibility, "internal"),
26788
26841
  orderBy: [desc(games.createdAt)]
26789
26842
  });
26790
26843
  }
26791
- async listManageable(user) {
26844
+ async listAccessible(user) {
26792
26845
  const seesAllGames = user.role === "admin" || user.role === "teacher";
26793
26846
  if (!seesAllGames) {
26794
26847
  this.validateDeveloperStatus(user);
26795
26848
  }
26796
26849
  const db2 = this.deps.db;
26797
- return db2.query.games.findMany({
26798
- where: seesAllGames ? undefined : eq(games.developerId, user.id),
26799
- orderBy: [desc(games.createdAt)]
26800
- });
26850
+ if (seesAllGames) {
26851
+ return db2.query.games.findMany({
26852
+ orderBy: [desc(games.createdAt)]
26853
+ });
26854
+ }
26855
+ const rows = await db2.select({ games }).from(games).innerJoin(gameMembers, eq(gameMembers.gameId, games.id)).where(eq(gameMembers.userId, user.id)).orderBy(desc(games.createdAt));
26856
+ return rows.map((r) => r.games);
26801
26857
  }
26802
26858
  async getSubjects() {
26803
26859
  const db2 = this.deps.db;
@@ -26821,7 +26877,7 @@ var init_game_service = __esm(() => {
26821
26877
  if (!game) {
26822
26878
  throw new NotFoundError("Game", gameId);
26823
26879
  }
26824
- this.enforceVisibility(game, caller, gameId);
26880
+ await this.enforceVisibility(game, caller, gameId);
26825
26881
  return game;
26826
26882
  }
26827
26883
  async getBySlug(slug, caller) {
@@ -26832,7 +26888,7 @@ var init_game_service = __esm(() => {
26832
26888
  if (!game) {
26833
26889
  throw new NotFoundError("Game", slug);
26834
26890
  }
26835
- this.enforceVisibility(game, caller, slug);
26891
+ await this.enforceVisibility(game, caller, slug);
26836
26892
  return game;
26837
26893
  }
26838
26894
  async getManifest(gameId, caller) {
@@ -26998,15 +27054,20 @@ var init_game_service = __esm(() => {
26998
27054
  };
26999
27055
  }
27000
27056
  }
27001
- enforceVisibility(game, caller, lookupIdentifier) {
27057
+ async enforceVisibility(game, caller, lookupIdentifier) {
27002
27058
  if (game.visibility !== "internal") {
27003
27059
  return;
27004
27060
  }
27005
- const isAdmin = caller?.role === "admin";
27006
- const isOwner = caller?.id != null && caller.id === game.developerId;
27007
- if (!isAdmin && !isOwner) {
27061
+ if (!caller) {
27008
27062
  throw new NotFoundError("Game", lookupIdentifier);
27009
27063
  }
27064
+ if (caller.role === "admin") {
27065
+ return;
27066
+ }
27067
+ if (await this.hasGameMembership(game.id, caller.id)) {
27068
+ return;
27069
+ }
27070
+ throw new NotFoundError("Game", lookupIdentifier);
27010
27071
  }
27011
27072
  async upsertBySlug(slug, data, user) {
27012
27073
  const db2 = this.deps.db;
@@ -27043,17 +27104,24 @@ var init_game_service = __esm(() => {
27043
27104
  ...gameDataForDb,
27044
27105
  id: gameId,
27045
27106
  slug,
27046
- developerId: user.id,
27047
27107
  metadata: data.metadata || {},
27048
27108
  version: data.gameType === "external" ? "external" : "",
27049
27109
  deploymentUrl: null,
27050
27110
  createdAt: new Date
27051
27111
  };
27052
- const [createdGame] = await db2.insert(games).values(insertData).returning();
27053
- if (!createdGame) {
27054
- logger5.error("Game insert returned no rows", { slug, developerId: user.id });
27055
- throw new InternalError("DB insert failed to return result for new game");
27056
- }
27112
+ const createdGame = await db2.transaction(async (tx) => {
27113
+ const [game] = await tx.insert(games).values(insertData).returning();
27114
+ if (!game) {
27115
+ logger5.error("Game insert returned no rows", { slug, userId: user.id });
27116
+ throw new InternalError("DB insert failed to return result for new game");
27117
+ }
27118
+ await tx.insert(gameMembers).values({
27119
+ gameId: game.id,
27120
+ userId: user.id,
27121
+ role: "owner"
27122
+ });
27123
+ return game;
27124
+ });
27057
27125
  gameResponse = createdGame;
27058
27126
  }
27059
27127
  if (data.mapElementId) {
@@ -27151,51 +27219,43 @@ var init_game_service = __esm(() => {
27151
27219
  displayName: gameToDelete.displayName
27152
27220
  };
27153
27221
  }
27222
+ async hasGameMembership(gameId, userId) {
27223
+ const membership = await this.deps.db.query.gameMembers.findFirst({
27224
+ where: and(eq(gameMembers.gameId, gameId), eq(gameMembers.userId, userId)),
27225
+ columns: { id: true }
27226
+ });
27227
+ return Boolean(membership);
27228
+ }
27154
27229
  async validateOwnership(user, gameId) {
27155
- if (user.role === "admin") {
27156
- const gameExists = await this.deps.db.query.games.findFirst({
27157
- where: eq(games.id, gameId),
27158
- columns: { id: true }
27159
- });
27160
- if (!gameExists) {
27161
- throw new NotFoundError("Game", gameId);
27162
- }
27163
- return;
27164
- }
27165
27230
  const db2 = this.deps.db;
27166
- const gameOwnership = await db2.query.games.findFirst({
27167
- where: and(eq(games.id, gameId), eq(games.developerId, user.id)),
27231
+ const gameExists = await db2.query.games.findFirst({
27232
+ where: eq(games.id, gameId),
27168
27233
  columns: { id: true }
27169
27234
  });
27170
- if (!gameOwnership) {
27171
- const gameExists = await db2.query.games.findFirst({
27172
- where: eq(games.id, gameId),
27173
- columns: { id: true }
27174
- });
27175
- if (!gameExists) {
27176
- throw new NotFoundError("Game", gameId);
27177
- }
27235
+ if (!gameExists) {
27236
+ throw new NotFoundError("Game", gameId);
27237
+ }
27238
+ if (user.role === "admin") {
27239
+ return;
27240
+ }
27241
+ if (!await this.hasGameMembership(gameId, user.id)) {
27178
27242
  throw new AccessDeniedError("You do not own this game");
27179
27243
  }
27180
27244
  }
27181
27245
  async validateDeveloperAccess(user, gameId) {
27182
27246
  this.validateDeveloperStatus(user);
27183
- if (user.role === "admin") {
27184
- const gameExists = await this.deps.db.query.games.findFirst({
27185
- where: eq(games.id, gameId),
27186
- columns: { id: true }
27187
- });
27188
- if (!gameExists) {
27189
- throw new NotFoundError("Game", gameId);
27190
- }
27191
- return;
27192
- }
27193
27247
  const db2 = this.deps.db;
27194
- const existingGame = await db2.query.games.findFirst({
27195
- where: and(eq(games.id, gameId), eq(games.developerId, user.id)),
27248
+ const gameExists = await db2.query.games.findFirst({
27249
+ where: eq(games.id, gameId),
27196
27250
  columns: { id: true }
27197
27251
  });
27198
- if (!existingGame) {
27252
+ if (!gameExists) {
27253
+ throw new NotFoundError("Game", gameId);
27254
+ }
27255
+ if (user.role === "admin") {
27256
+ return;
27257
+ }
27258
+ if (!await this.hasGameMembership(gameId, user.id)) {
27199
27259
  throw new NotFoundError("Game", gameId);
27200
27260
  }
27201
27261
  }
@@ -27215,21 +27275,18 @@ var init_game_service = __esm(() => {
27215
27275
  async validateDeveloperAccessBySlug(user, slug) {
27216
27276
  this.validateDeveloperStatus(user);
27217
27277
  const db2 = this.deps.db;
27218
- if (user.role === "admin") {
27219
- const game2 = await db2.query.games.findFirst({
27220
- where: eq(games.slug, slug)
27221
- });
27222
- if (!game2) {
27223
- throw new NotFoundError("Game", slug);
27224
- }
27225
- return game2;
27226
- }
27227
27278
  const game = await db2.query.games.findFirst({
27228
- where: and(eq(games.slug, slug), eq(games.developerId, user.id))
27279
+ where: eq(games.slug, slug)
27229
27280
  });
27230
27281
  if (!game) {
27231
27282
  throw new NotFoundError("Game", slug);
27232
27283
  }
27284
+ if (user.role === "admin") {
27285
+ return game;
27286
+ }
27287
+ if (!await this.hasGameMembership(game.id, user.id)) {
27288
+ throw new NotFoundError("Game", slug);
27289
+ }
27233
27290
  return game;
27234
27291
  }
27235
27292
  validateDeveloperStatus(user) {
@@ -28192,8 +28249,7 @@ class DatabaseService {
28192
28249
  async reset(slug, user, schema2) {
28193
28250
  const d1 = this.getD1();
28194
28251
  const game = await this.deps.validateDeveloperAccessBySlug(user, slug);
28195
- const isProd = isProduction2(this.deps.config);
28196
- const deploymentId = getDeploymentId(slug, isProd);
28252
+ const deploymentId = getDeploymentId(slug, this.deps.config.sstStage);
28197
28253
  logger9.debug("Resetting database", {
28198
28254
  userId: user.id,
28199
28255
  gameId: game.id,
@@ -28269,7 +28325,6 @@ var init_database_service = __esm(() => {
28269
28325
  init_drizzle_orm();
28270
28326
  init_tables_index();
28271
28327
  init_src2();
28272
- init_config2();
28273
28328
  init_errors();
28274
28329
  init_deployment_util();
28275
28330
  logger9 = log.scope("DatabaseService");
@@ -28788,8 +28843,7 @@ class SecretsService {
28788
28843
  return this.deps.cloudflare;
28789
28844
  }
28790
28845
  getDeploymentId(slug) {
28791
- const isProd = isProduction2(this.deps.config);
28792
- return getDeploymentId(slug, isProd);
28846
+ return getDeploymentId(slug, this.deps.config.sstStage);
28793
28847
  }
28794
28848
  async listKeys(slug, user) {
28795
28849
  const game = await this.deps.validateDeveloperAccessBySlug(user, slug);
@@ -28917,7 +28971,6 @@ var logger13, INTERNAL_SECRET_KEYS;
28917
28971
  var init_secrets_service = __esm(() => {
28918
28972
  init_src();
28919
28973
  init_src2();
28920
- init_config2();
28921
28974
  init_errors();
28922
28975
  init_deployment_util();
28923
28976
  logger13 = log.scope("SecretsService");
@@ -28969,8 +29022,7 @@ class SeedService {
28969
29022
  async seed(slug, code, user, secrets) {
28970
29023
  const cf = this.getCloudflare();
28971
29024
  const game = await this.deps.validateDeveloperAccessBySlug(user, slug);
28972
- const isProd = isProduction2(this.deps.config);
28973
- const deploymentId = getDeploymentId(slug, isProd);
29025
+ const deploymentId = getDeploymentId(slug, this.deps.config.sstStage);
28974
29026
  const uniqueSuffix = Date.now().toString(36);
28975
29027
  const seedDeploymentId = `seed-${deploymentId}-${uniqueSuffix}`;
28976
29028
  logger14.debug("Seeding database", {
@@ -29209,7 +29261,6 @@ var init_seed_service = __esm(() => {
29209
29261
  init_src();
29210
29262
  init_setup2();
29211
29263
  init_src2();
29212
- init_config2();
29213
29264
  init_errors();
29214
29265
  init_deployment_util();
29215
29266
  logger14 = log.scope("SeedService");
@@ -34860,7 +34911,7 @@ class LogsService {
34860
34911
  constructor(deps) {
34861
34912
  this.deps = deps;
34862
34913
  }
34863
- async generateToken(user, slug2, environment) {
34914
+ async generateToken(user, slug2, sstStage) {
34864
34915
  const db2 = this.deps.db;
34865
34916
  if (user.role === "admin") {
34866
34917
  const game = await db2.query.games.findFirst({
@@ -34870,7 +34921,7 @@ class LogsService {
34870
34921
  if (!game) {
34871
34922
  throw new NotFoundError("Game", slug2);
34872
34923
  }
34873
- logger28.info("Admin accessing game logs", { adminId: user.id, slug: slug2, environment });
34924
+ logger28.info("Admin accessing game logs", { adminId: user.id, slug: slug2, sstStage });
34874
34925
  } else {
34875
34926
  const isApprovedDev = user.developerStatus === "approved";
34876
34927
  if (!isApprovedDev) {
@@ -34878,10 +34929,17 @@ class LogsService {
34878
34929
  throw new AccessDeniedError("Must be an approved developer");
34879
34930
  }
34880
34931
  const game = await db2.query.games.findFirst({
34881
- where: and(eq(games.slug, slug2), eq(games.developerId, user.id)),
34932
+ where: eq(games.slug, slug2),
34882
34933
  columns: { id: true }
34883
34934
  });
34884
34935
  if (!game) {
34936
+ throw new NotFoundError("Game", slug2);
34937
+ }
34938
+ const membership = await db2.query.gameMembers.findFirst({
34939
+ where: and(eq(gameMembers.gameId, game.id), eq(gameMembers.userId, user.id)),
34940
+ columns: { id: true }
34941
+ });
34942
+ if (!membership) {
34885
34943
  logger28.warn("Developer attempted access to unowned game logs", {
34886
34944
  userId: user.id,
34887
34945
  slug: slug2
@@ -34889,8 +34947,7 @@ class LogsService {
34889
34947
  throw new NotFoundError("Game", slug2);
34890
34948
  }
34891
34949
  }
34892
- const isProduction3 = environment === "production";
34893
- const workerId = getDeploymentId(slug2, isProduction3);
34950
+ const workerId = getDeploymentId(slug2, sstStage);
34894
34951
  const token = await this.deps.mintLogStreamToken(user.id, workerId);
34895
34952
  logger28.debug("Generated log stream token", {
34896
34953
  userId: user.id,
@@ -35636,7 +35693,7 @@ function createServices(ctx) {
35636
35693
  discord,
35637
35694
  cloudflare,
35638
35695
  storage,
35639
- stage: config2.stage
35696
+ stage: config2.sstStage
35640
35697
  });
35641
35698
  const player = createPlayerServices({
35642
35699
  db: db2,
@@ -38870,7 +38927,7 @@ var init_providers = __esm(() => {
38870
38927
  function buildConfig(options) {
38871
38928
  const baseUrl = `http://localhost:${options.port ?? 3000}`;
38872
38929
  return createMinimalConfig({
38873
- stage: "local",
38930
+ sstStage: "sandbox",
38874
38931
  baseUrl,
38875
38932
  gameDomain: "localhost",
38876
38933
  uploadBucket: "sandbox-uploads",
@@ -38907,7 +38964,7 @@ function createSandboxContext(options) {
38907
38964
  Object.assign(services, createServices(ctx));
38908
38965
  cachedServiceContext = ctx;
38909
38966
  log.debug("[Sandbox] ServiceContext initialized", {
38910
- stage: config2.stage,
38967
+ sstStage: config2.sstStage,
38911
38968
  baseUrl: config2.baseUrl,
38912
38969
  hasTimeback: Boolean(timeback2)
38913
38970
  });
@@ -93762,7 +93819,6 @@ async function seedCoreGames(db2) {
93762
93819
  const coreGames = [
93763
93820
  {
93764
93821
  id: CORE_GAME_UUIDS.PLAYGROUND,
93765
- developerId: DEMO_USERS.developer.id,
93766
93822
  slug: "playground",
93767
93823
  displayName: "Playground",
93768
93824
  version: "local",
@@ -93779,6 +93835,11 @@ async function seedCoreGames(db2) {
93779
93835
  for (const gameData of coreGames) {
93780
93836
  try {
93781
93837
  await db2.insert(games).values(gameData).onConflictDoNothing();
93838
+ await db2.insert(gameMembers).values({
93839
+ gameId: gameData.id,
93840
+ userId: DEMO_USERS.developer.id,
93841
+ role: "owner"
93842
+ }).onConflictDoNothing();
93782
93843
  } catch (error2) {
93783
93844
  logger37.error(`Error seeding core game '${gameData.slug}': ${error2}`);
93784
93845
  }
@@ -93803,7 +93864,6 @@ async function seedCurrentProjectGame(db2, project) {
93803
93864
  }
93804
93865
  const gameRecord = {
93805
93866
  id: desiredGameId ?? crypto.randomUUID(),
93806
- developerId: DEMO_USERS.developer.id,
93807
93867
  slug: project.slug,
93808
93868
  displayName: project.displayName,
93809
93869
  version: project.version,
@@ -93821,6 +93881,11 @@ async function seedCurrentProjectGame(db2, project) {
93821
93881
  if (!newGame) {
93822
93882
  throw new Error("Failed to create game record");
93823
93883
  }
93884
+ await db2.insert(gameMembers).values({
93885
+ gameId: newGame.id,
93886
+ userId: DEMO_USERS.developer.id,
93887
+ role: "owner"
93888
+ }).onConflictDoNothing();
93824
93889
  if (project.timebackCourses && project.timebackCourses.length > 0) {
93825
93890
  await seedTimebackIntegrations(db2, newGame.id, project.timebackCourses);
93826
93891
  }
@@ -94809,7 +94874,6 @@ var init_schemas2 = __esm(() => {
94809
94874
  id: true,
94810
94875
  slug: true,
94811
94876
  createdAt: true,
94812
- developerId: true,
94813
94877
  version: true
94814
94878
  }).refine((data) => {
94815
94879
  if (data.gameType === "hosted" && data.deploymentUrl === null) {
@@ -94902,9 +94966,9 @@ var init_schemas2 = __esm(() => {
94902
94966
  compatibilityDate: exports_external.string().optional(),
94903
94967
  compatibilityFlags: exports_external.array(exports_external.string()).optional(),
94904
94968
  bindings: exports_external.object({
94905
- database: exports_external.array(exports_external.string()).optional(),
94906
- keyValue: exports_external.array(exports_external.string()).optional(),
94907
- bucket: exports_external.array(exports_external.string()).optional(),
94969
+ database: exports_external.union([exports_external.literal(true), exports_external.array(exports_external.string())]).optional(),
94970
+ keyValue: exports_external.union([exports_external.literal(true), exports_external.array(exports_external.string())]).optional(),
94971
+ bucket: exports_external.union([exports_external.literal(true), exports_external.array(exports_external.string())]).optional(),
94908
94972
  queues: exports_external.record(exports_external.string(), exports_external.union([
94909
94973
  exports_external.literal(true),
94910
94974
  exports_external.object({
@@ -96211,7 +96275,7 @@ var init_domain_controller = __esm(() => {
96211
96275
  });
96212
96276
 
96213
96277
  // ../api-core/src/controllers/game.controller.ts
96214
- var logger48, list3, listManageable, getSubjects, getById2, getBySlug, getManifest, upsertBySlug, remove3, games2;
96278
+ var logger48, list3, listAccessible, getSubjects, getById2, getBySlug, getManifest, upsertBySlug, remove3, games2;
96215
96279
  var init_game_controller = __esm(() => {
96216
96280
  init_esm();
96217
96281
  init_schemas_index();
@@ -96224,9 +96288,9 @@ var init_game_controller = __esm(() => {
96224
96288
  logger48.debug("Listing games", { userId: ctx.user.id });
96225
96289
  return ctx.services.game.list(ctx.user);
96226
96290
  });
96227
- listManageable = requireNonAnonymous(async (ctx) => {
96228
- logger48.debug("Listing manageable games", { userId: ctx.user.id });
96229
- return ctx.services.game.listManageable(ctx.user);
96291
+ listAccessible = requireNonAnonymous(async (ctx) => {
96292
+ logger48.debug("Listing accessible games", { userId: ctx.user.id });
96293
+ return ctx.services.game.listAccessible(ctx.user);
96230
96294
  });
96231
96295
  getSubjects = requireNonAnonymous(async (ctx) => {
96232
96296
  logger48.debug("Getting game subjects", { userId: ctx.user.id });
@@ -96299,7 +96363,7 @@ var init_game_controller = __esm(() => {
96299
96363
  });
96300
96364
  games2 = {
96301
96365
  list: list3,
96302
- listManageable,
96366
+ listAccessible,
96303
96367
  getSubjects,
96304
96368
  getById: getById2,
96305
96369
  getManifest,
@@ -96875,8 +96939,8 @@ var init_logs_controller = __esm(() => {
96875
96939
  let body2;
96876
96940
  try {
96877
96941
  const json4 = await ctx.request.json();
96878
- if (json4.environment !== "staging" && json4.environment !== "production") {
96879
- throw ApiError.badRequest('Invalid environment. Must be "staging" or "production".');
96942
+ if (json4.environment !== "local" && json4.environment !== "staging" && json4.environment !== "production") {
96943
+ throw ApiError.badRequest('Invalid environment. Must be "local", "staging", or "production".');
96880
96944
  }
96881
96945
  body2 = json4;
96882
96946
  } catch (error2) {
@@ -96890,7 +96954,13 @@ var init_logs_controller = __esm(() => {
96890
96954
  slug: slug2,
96891
96955
  environment: body2.environment
96892
96956
  });
96893
- return ctx.services.logs.generateToken(ctx.user, slug2, body2.environment);
96957
+ const envToSstStage = {
96958
+ local: ctx.config.sstStage,
96959
+ staging: "dev",
96960
+ production: "production"
96961
+ };
96962
+ const sstStage = envToSstStage[body2.environment] ?? "dev";
96963
+ return ctx.services.logs.generateToken(ctx.user, slug2, sstStage);
96894
96964
  });
96895
96965
  logs = {
96896
96966
  generateToken
@@ -97242,6 +97312,7 @@ var init_seed_controller = __esm(() => {
97242
97312
  var logger61, start2, end, mintToken, sessions2;
97243
97313
  var init_session_controller = __esm(() => {
97244
97314
  init_src2();
97315
+ init_tunnel();
97245
97316
  init_errors();
97246
97317
  init_utils11();
97247
97318
  logger61 = log.scope("SessionController");
@@ -97276,7 +97347,14 @@ var init_session_controller = __esm(() => {
97276
97347
  throw ApiError.badRequest("Missing game ID or slug");
97277
97348
  }
97278
97349
  logger61.debug("Minting token", { userId: ctx.user.id, gameIdOrSlug, launchId: ctx.launchId });
97279
- return ctx.services.session.mintToken(gameIdOrSlug, ctx.user.id);
97350
+ const { token, exp } = await ctx.services.session.mintToken(gameIdOrSlug, ctx.user.id);
97351
+ let baseUrl;
97352
+ if (ctx.config.isLocal) {
97353
+ try {
97354
+ baseUrl = await getTunnelUrl();
97355
+ } catch {}
97356
+ }
97357
+ return { token, exp, baseUrl };
97280
97358
  });
97281
97359
  sessions2 = {
97282
97360
  start: start2,
@@ -98420,6 +98498,7 @@ var init_crud = __esm(() => {
98420
98498
  init_api();
98421
98499
  gameCrudRouter = new Hono2;
98422
98500
  gameCrudRouter.get("/", handle2(games2.list));
98501
+ gameCrudRouter.get("/accessible", handle2(games2.listAccessible));
98423
98502
  gameCrudRouter.get("/:gameId{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}}/manifest", handle2(games2.getManifest));
98424
98503
  gameCrudRouter.get("/:gameId{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}}", handle2(games2.getById));
98425
98504
  gameCrudRouter.get("/:slug", handle2(games2.getBySlug));
@@ -98562,10 +98641,12 @@ var init_deploy = __esm(() => {
98562
98641
  const db2 = ctx.db;
98563
98642
  const existingGames = await db2.select().from(games).where(eq(games.slug, slug2));
98564
98643
  const existingGame = existingGames[0];
98565
- if (existingGame) {
98566
- const isAdmin = user.role === "admin";
98567
- const isOwner = existingGame.developerId === user.id;
98568
- if (!isAdmin && !isOwner) {
98644
+ if (existingGame && user.role !== "admin") {
98645
+ const membership = await db2.query.gameMembers.findFirst({
98646
+ where: and(eq(gameMembers.gameId, existingGame.id), eq(gameMembers.userId, user.id)),
98647
+ columns: { id: true }
98648
+ });
98649
+ if (!membership) {
98569
98650
  return c2.json({
98570
98651
  error: {
98571
98652
  code: "FORBIDDEN",
@@ -98589,17 +98670,24 @@ var init_deploy = __esm(() => {
98589
98670
  if (!body2.metadata?.displayName) {
98590
98671
  return c2.json({ error: { code: "BAD_REQUEST", message: "Display name required for new game" } }, 400);
98591
98672
  }
98592
- const inserted = await db2.insert(games).values({
98673
+ const gameValues = {
98593
98674
  slug: slug2,
98594
98675
  displayName: body2.metadata.displayName,
98595
98676
  version: "1.0.0",
98596
98677
  platform: body2.metadata.platform ?? "web",
98597
98678
  gameType: "hosted",
98598
- developerId: user.id,
98599
98679
  deploymentUrl: `http://localhost:4321`,
98600
98680
  metadata: body2.metadata.metadata ?? {}
98601
- }).returning();
98602
- game = inserted[0];
98681
+ };
98682
+ game = await db2.transaction(async (tx) => {
98683
+ const [inserted] = await tx.insert(games).values(gameValues).returning();
98684
+ await tx.insert(gameMembers).values({
98685
+ gameId: inserted.id,
98686
+ userId: user.id,
98687
+ role: "owner"
98688
+ });
98689
+ return inserted;
98690
+ });
98603
98691
  }
98604
98692
  let backendCode = body2.code;
98605
98693
  if (!backendCode && body2.codeUploadToken) {
@@ -98678,13 +98766,19 @@ var init_deploy = __esm(() => {
98678
98766
  const ctx = getSandboxContext();
98679
98767
  const game = await ctx.db.query.games.findFirst({
98680
98768
  where: eq(games.slug, slug2 ?? ""),
98681
- columns: { id: true, developerId: true }
98769
+ columns: { id: true }
98682
98770
  });
98683
98771
  if (!game) {
98684
98772
  return c2.json({ error: { code: "NOT_FOUND", message: "Game not found" } }, 404);
98685
98773
  }
98686
- if (game.developerId !== user.id && user.role !== "admin") {
98687
- return c2.json({ error: { code: "NOT_FOUND", message: "Game not found" } }, 404);
98774
+ if (user.role !== "admin") {
98775
+ const membership = await ctx.db.query.gameMembers.findFirst({
98776
+ where: and(eq(gameMembers.gameId, game.id), eq(gameMembers.userId, user.id)),
98777
+ columns: { id: true }
98778
+ });
98779
+ if (!membership) {
98780
+ return c2.json({ error: { code: "NOT_FOUND", message: "Game not found" } }, 404);
98781
+ }
98688
98782
  }
98689
98783
  const job = await ctx.db.query.gameDeployJobs.findFirst({
98690
98784
  where: and(eq(gameDeployJobs.id, jobId), eq(gameDeployJobs.gameId, game.id))