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