@playcademy/sandbox 0.10.1-beta.7 → 0.10.1-beta.8

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 +160 -20
  2. package/dist/server.js +160 -20
  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.10.1-beta.7",
1131
+ version: "0.10.1-beta.8",
1132
1132
  description: "Local development server for Playcademy game development",
1133
1133
  type: "module",
1134
1134
  exports: {
@@ -18645,7 +18645,10 @@ var init_table5 = __esm(() => {
18645
18645
  bookmarkCapturedAt: timestamp("bookmark_captured_at", { withTimezone: true }),
18646
18646
  isActive: boolean("is_active").notNull().default(false),
18647
18647
  resources: jsonb("resources").$type(),
18648
- deployedAt: timestamp("deployed_at", { withTimezone: true }).notNull().defaultNow()
18648
+ deployedAt: timestamp("deployed_at", { withTimezone: true }).notNull().defaultNow(),
18649
+ deployJobId: uuid("deploy_job_id").references(() => gameDeployJobs.id, {
18650
+ onDelete: "set null"
18651
+ })
18649
18652
  }, (table3) => [index("game_deployments_game_target_idx").on(table3.gameId, table3.target)]);
18650
18653
  gameDeployJobs = pgTable("game_deploy_jobs", {
18651
18654
  id: uuid("id").primaryKey().defaultRandom(),
@@ -30464,7 +30467,11 @@ class DeployJobService {
30464
30467
  const uploadDeps = this.buildUploadDeps();
30465
30468
  try {
30466
30469
  assertLease();
30467
- for await (const step of this.deps.runDeploy(game2.slug, request, user, uploadDeps, extractZipToDirectory)) {
30470
+ for await (const step of this.deps.runDeploy(game2.slug, request, user, {
30471
+ uploadDeps,
30472
+ extractZip: extractZipToDirectory,
30473
+ jobId
30474
+ })) {
30468
30475
  assertLease();
30469
30476
  if (step.type === "status" && "message" in step.data && typeof step.data.message === "string") {
30470
30477
  const details = "details" in step.data && isEventDetails(step.data.details) ? step.data.details : undefined;
@@ -35892,9 +35899,9 @@ class DeployService {
35892
35899
  }
35893
35900
  return cf;
35894
35901
  }
35895
- async* deploy(slug, request, user, uploadDeps, extractZip) {
35902
+ async* deploy(slug, request, user, options = {}) {
35896
35903
  const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
35897
- const context2 = { slug, request, user, game: game2, uploadDeps, extractZip };
35904
+ const context2 = { slug, request, user, game: game2, ...options };
35898
35905
  switch (request.target ?? "game") {
35899
35906
  case "dashboard": {
35900
35907
  yield* this.deployDashboard(context2);
@@ -35910,7 +35917,7 @@ class DeployService {
35910
35917
  }
35911
35918
  }
35912
35919
  async* deployGame(context2) {
35913
- const { slug, request, user, game: game2, uploadDeps, extractZip } = context2;
35920
+ const { slug, request, user, game: game2, uploadDeps, extractZip, jobId } = context2;
35914
35921
  const cf = this.getCloudflare();
35915
35922
  const db2 = this.deps.db;
35916
35923
  const flags2 = this.validateGameRequest(request);
@@ -36015,7 +36022,8 @@ class DeployService {
36015
36022
  request,
36016
36023
  user,
36017
36024
  flags: flags2,
36018
- database: databaseOutcome
36025
+ database: databaseOutcome,
36026
+ jobId
36019
36027
  });
36020
36028
  if (request.pruneSecrets?.length) {
36021
36029
  yield* this.pruneManagedSecretsStep(game2.id, result.deploymentId, request.pruneSecrets);
@@ -36023,7 +36031,7 @@ class DeployService {
36023
36031
  yield { type: "complete", data: updatedGame };
36024
36032
  }
36025
36033
  async* deployDashboard(context2) {
36026
- const { slug, request, user, game: game2 } = context2;
36034
+ const { slug, request, user, game: game2, jobId } = context2;
36027
36035
  const db2 = this.deps.db;
36028
36036
  const contract = this.validateDashboardRequest(request, context2.uploadDeps, context2.extractZip);
36029
36037
  const deploymentId = getDashboardDeploymentId(slug, this.deps.config.sstStage);
@@ -36113,7 +36121,8 @@ class DeployService {
36113
36121
  url: result.url,
36114
36122
  codeHash,
36115
36123
  resources: result.resources,
36116
- target: "dashboard"
36124
+ target: "dashboard",
36125
+ deployJobId: jobId
36117
36126
  });
36118
36127
  setAttribute("app.deploy.code_hash", codeHash);
36119
36128
  try {
@@ -36743,7 +36752,8 @@ class DeployService {
36743
36752
  request,
36744
36753
  user,
36745
36754
  flags: flags2,
36746
- database
36755
+ database,
36756
+ jobId
36747
36757
  }) {
36748
36758
  const { hasBackend, hasFrontend, hasMetadata } = flags2;
36749
36759
  const db2 = this.deps.db;
@@ -36758,7 +36768,8 @@ class DeployService {
36758
36768
  timeTravelBookmark: database.timeTravelBookmark,
36759
36769
  bookmarkCapturedAt: database.bookmarkCapturedAt,
36760
36770
  resources: result.resources,
36761
- target: "game"
36771
+ target: "game",
36772
+ deployJobId: jobId
36762
36773
  });
36763
36774
  await this.persistArtifactHashes(game2.id, {
36764
36775
  buildHash: hasFrontend ? request.buildHash : undefined,
@@ -37032,7 +37043,8 @@ class DeployService {
37032
37043
  timeTravelBookmark: record.timeTravelBookmark ?? null,
37033
37044
  bookmarkCapturedAt: record.bookmarkCapturedAt ?? null,
37034
37045
  resources: record.resources,
37035
- isActive: true
37046
+ isActive: true,
37047
+ deployJobId: record.deployJobId ?? null
37036
37048
  });
37037
37049
  });
37038
37050
  }
@@ -38455,7 +38467,7 @@ function createGameServices(deps) {
38455
38467
  uploadBucket: config3.uploadBucket,
38456
38468
  storage,
38457
38469
  validateDeveloperAccessBySlug: (user, slug) => game2.validateDeveloperAccessBySlug(user, slug),
38458
- runDeploy: (slug, request, user, uploadDeps, extractZip) => deploy.deploy(slug, request, user, uploadDeps, extractZip),
38470
+ runDeploy: (slug, request, user, options) => deploy.deploy(slug, request, user, options),
38459
38471
  notifyDeploymentFailure: (failure) => deploy.notifyDeploymentFailure(failure)
38460
38472
  });
38461
38473
  const logs = new LogsService({
@@ -46851,6 +46863,95 @@ var init_kv_service = __esm(() => {
46851
46863
  init_errors2();
46852
46864
  });
46853
46865
 
46866
+ // ../utils/src/url.ts
46867
+ function publishableRepositoryUrl(value) {
46868
+ let url;
46869
+ try {
46870
+ url = new URL(value);
46871
+ } catch {
46872
+ return null;
46873
+ }
46874
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
46875
+ return null;
46876
+ }
46877
+ return value;
46878
+ }
46879
+
46880
+ // ../api-core/src/services/releases.service.ts
46881
+ class ReleasesService {
46882
+ deps;
46883
+ constructor(deps) {
46884
+ this.deps = deps;
46885
+ }
46886
+ async list() {
46887
+ const rows = await this.deps.db.select(releaseColumns()).from(gameDeployments).innerJoin(games, eq(gameDeployments.gameId, games.id)).leftJoin(gameDeployJobs, eq(gameDeployments.deployJobId, gameDeployJobs.id)).where(and(activeWorkerDeployment(), listedGame())).orderBy(games.slug);
46888
+ setAttributes({ "app.releases.count": rows.length });
46889
+ return { releases: rows.map(toRelease) };
46890
+ }
46891
+ async get(slug, { history } = {}) {
46892
+ const game2 = await this.deps.db.query.games.findFirst({
46893
+ where: and(eq(games.slug, slug), listedGame()),
46894
+ columns: { id: true }
46895
+ });
46896
+ if (!game2) {
46897
+ throw new NotFoundError("Game", slug);
46898
+ }
46899
+ const rows = await this.deps.db.select(releaseColumns()).from(gameDeployments).innerJoin(games, eq(gameDeployments.gameId, games.id)).leftJoin(gameDeployJobs, eq(gameDeployments.deployJobId, gameDeployJobs.id)).where(and(eq(gameDeployments.gameId, game2.id), eq(gameDeployments.target, "game"))).orderBy(desc(gameDeployments.deployedAt)).limit(Math.max(1, history ?? 0));
46900
+ setAttributes({
46901
+ "app.releases.history_requested": history ?? 0,
46902
+ "app.releases.history_rows": rows.length
46903
+ });
46904
+ const releases = rows.map(toRelease);
46905
+ const current = releases[0] ?? null;
46906
+ return history === undefined ? { current } : { current, history: releases };
46907
+ }
46908
+ }
46909
+ function releaseColumns() {
46910
+ return {
46911
+ slug: games.slug,
46912
+ url: games.deploymentUrl,
46913
+ source: sql`${gameDeployJobs.request} -> 'source'`,
46914
+ deployedAt: gameDeployments.deployedAt
46915
+ };
46916
+ }
46917
+ function listedGame() {
46918
+ return ne(games.visibility, "internal");
46919
+ }
46920
+ function activeWorkerDeployment() {
46921
+ return and(eq(gameDeployments.isActive, true), eq(gameDeployments.target, "game"));
46922
+ }
46923
+ function toRelease(row) {
46924
+ return {
46925
+ slug: row.slug,
46926
+ url: row.url,
46927
+ source: normalizeSource(row.source),
46928
+ deployedAt: row.deployedAt.toISOString()
46929
+ };
46930
+ }
46931
+ function normalizeSource(value) {
46932
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
46933
+ return null;
46934
+ }
46935
+ const raw = value;
46936
+ const repository = nullableString(raw.repository);
46937
+ return {
46938
+ repository: repository === null ? null : publishableRepositoryUrl(repository),
46939
+ commit: nullableString(raw.commit),
46940
+ branch: nullableString(raw.branch),
46941
+ dirty: raw.dirty === true,
46942
+ externalBuild: raw.externalBuild === true
46943
+ };
46944
+ }
46945
+ function nullableString(value) {
46946
+ return typeof value === "string" && value.length > 0 ? value : null;
46947
+ }
46948
+ var init_releases_service = __esm(() => {
46949
+ init_drizzle_orm();
46950
+ init_tables_index();
46951
+ init_spans();
46952
+ init_errors2();
46953
+ });
46954
+
46854
46955
  // ../api-core/src/services/secrets.service.ts
46855
46956
  class SecretsService {
46856
46957
  deps;
@@ -47837,13 +47938,7 @@ var init_schemas2 = __esm(() => {
47837
47938
  path: ["lastAppliedMigrationTag"]
47838
47939
  });
47839
47940
  DeploySourceSchema = exports_external.object({
47840
- repository: exports_external.string().url().nullable().transform((value) => {
47841
- if (value === null) {
47842
- return null;
47843
- }
47844
- const url = new URL(value);
47845
- return url.username || url.password || url.search || url.hash ? null : value;
47846
- }),
47941
+ repository: exports_external.string().url().nullable().transform((value) => value === null ? null : publishableRepositoryUrl(value)),
47847
47942
  commit: exports_external.string().regex(/^[0-9a-f]{40}$/, "commit must be a full 40-character lowercase SHA").nullable(),
47848
47943
  branch: exports_external.string().min(1).nullable(),
47849
47944
  dirty: exports_external.boolean(),
@@ -109839,6 +109934,7 @@ function createPlatformServices(deps) {
109839
109934
  const kv = new KVService({ db: db2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
109840
109935
  const secrets = new SecretsService({ db: db2, config: config5, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
109841
109936
  const domain3 = new DomainService({ db: db2, cloudflare: cloudflare2, corsKvs, validateDeveloperAccessBySlug });
109937
+ const releases = new ReleasesService({ db: db2 });
109842
109938
  const deploymentState = new DeploymentStateService({
109843
109939
  db: db2,
109844
109940
  config: config5,
@@ -109898,6 +109994,7 @@ function createPlatformServices(deps) {
109898
109994
  kv,
109899
109995
  secrets,
109900
109996
  domain: domain3,
109997
+ releases,
109901
109998
  deploymentState,
109902
109999
  database,
109903
110000
  seed,
@@ -109916,6 +110013,7 @@ var init_platform2 = __esm(async () => {
109916
110013
  init_deployment_state_service();
109917
110014
  init_domain_service();
109918
110015
  init_kv_service();
110016
+ init_releases_service();
109919
110017
  init_secrets_service();
109920
110018
  init_seed_service();
109921
110019
  init_upload_service();
@@ -167647,6 +167745,47 @@ var init_lti_controller = __esm(() => {
167647
167745
  });
167648
167746
  });
167649
167747
 
167748
+ // ../api-core/src/controllers/releases.controller.ts
167749
+ function requireNoQuery(url4) {
167750
+ if (url4.search !== "") {
167751
+ throw ApiError.badRequest("This endpoint takes no query parameters");
167752
+ }
167753
+ }
167754
+ function parseHistoryParam(url4) {
167755
+ if (url4.search === "") {
167756
+ return;
167757
+ }
167758
+ const match3 = CANONICAL_HISTORY.exec(url4.search);
167759
+ if (!match3) {
167760
+ throw ApiError.badRequest(`The only supported query is ?history=N with N an integer between 1 and ${MAX_HISTORY}`);
167761
+ }
167762
+ return Number(match3[1]);
167763
+ }
167764
+ function cached3(body2) {
167765
+ return Response.json(body2, {
167766
+ headers: {
167767
+ "Content-Type": "application/json",
167768
+ "Cache-Control": CACHE_CONTROL2
167769
+ }
167770
+ });
167771
+ }
167772
+ async function list3(ctx) {
167773
+ requireNoQuery(ctx.url);
167774
+ return cached3(await ctx.services.releases.list());
167775
+ }
167776
+ async function get2(ctx) {
167777
+ const slug2 = requireSlug(ctx.params.slug);
167778
+ const history2 = parseHistoryParam(ctx.url);
167779
+ return cached3(await ctx.services.releases.get(slug2, { history: history2 }));
167780
+ }
167781
+ var CACHE_CONTROL2 = "public, max-age=60", MAX_HISTORY = 100, CANONICAL_HISTORY, releases;
167782
+ var init_releases_controller = __esm(() => {
167783
+ init_errors2();
167784
+ init_utils7();
167785
+ CANONICAL_HISTORY = /^\?history=([1-9]\d?|100)$/;
167786
+ releases = defineControllerNames("releases", { list: list3, get: get2 });
167787
+ });
167788
+
167650
167789
  // ../api-core/src/controllers/secrets.controller.ts
167651
167790
  var listKeys2, setSecrets, deleteSecret, diff, secrets;
167652
167791
  var init_secrets_controller = __esm(() => {
@@ -168809,6 +168948,7 @@ var init_controllers = __esm(() => {
168809
168948
  init_leaderboard_controller();
168810
168949
  init_logs_controller();
168811
168950
  init_lti_controller();
168951
+ init_releases_controller();
168812
168952
  init_secrets_controller();
168813
168953
  init_seed_controller();
168814
168954
  init_session_controller();
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.10.1-beta.7",
1130
+ version: "0.10.1-beta.8",
1131
1131
  description: "Local development server for Playcademy game development",
1132
1132
  type: "module",
1133
1133
  exports: {
@@ -18644,7 +18644,10 @@ var init_table5 = __esm(() => {
18644
18644
  bookmarkCapturedAt: timestamp("bookmark_captured_at", { withTimezone: true }),
18645
18645
  isActive: boolean("is_active").notNull().default(false),
18646
18646
  resources: jsonb("resources").$type(),
18647
- deployedAt: timestamp("deployed_at", { withTimezone: true }).notNull().defaultNow()
18647
+ deployedAt: timestamp("deployed_at", { withTimezone: true }).notNull().defaultNow(),
18648
+ deployJobId: uuid("deploy_job_id").references(() => gameDeployJobs.id, {
18649
+ onDelete: "set null"
18650
+ })
18648
18651
  }, (table3) => [index("game_deployments_game_target_idx").on(table3.gameId, table3.target)]);
18649
18652
  gameDeployJobs = pgTable("game_deploy_jobs", {
18650
18653
  id: uuid("id").primaryKey().defaultRandom(),
@@ -30463,7 +30466,11 @@ class DeployJobService {
30463
30466
  const uploadDeps = this.buildUploadDeps();
30464
30467
  try {
30465
30468
  assertLease();
30466
- for await (const step of this.deps.runDeploy(game2.slug, request, user, uploadDeps, extractZipToDirectory)) {
30469
+ for await (const step of this.deps.runDeploy(game2.slug, request, user, {
30470
+ uploadDeps,
30471
+ extractZip: extractZipToDirectory,
30472
+ jobId
30473
+ })) {
30467
30474
  assertLease();
30468
30475
  if (step.type === "status" && "message" in step.data && typeof step.data.message === "string") {
30469
30476
  const details = "details" in step.data && isEventDetails(step.data.details) ? step.data.details : undefined;
@@ -35891,9 +35898,9 @@ class DeployService {
35891
35898
  }
35892
35899
  return cf;
35893
35900
  }
35894
- async* deploy(slug, request, user, uploadDeps, extractZip) {
35901
+ async* deploy(slug, request, user, options = {}) {
35895
35902
  const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
35896
- const context2 = { slug, request, user, game: game2, uploadDeps, extractZip };
35903
+ const context2 = { slug, request, user, game: game2, ...options };
35897
35904
  switch (request.target ?? "game") {
35898
35905
  case "dashboard": {
35899
35906
  yield* this.deployDashboard(context2);
@@ -35909,7 +35916,7 @@ class DeployService {
35909
35916
  }
35910
35917
  }
35911
35918
  async* deployGame(context2) {
35912
- const { slug, request, user, game: game2, uploadDeps, extractZip } = context2;
35919
+ const { slug, request, user, game: game2, uploadDeps, extractZip, jobId } = context2;
35913
35920
  const cf = this.getCloudflare();
35914
35921
  const db2 = this.deps.db;
35915
35922
  const flags2 = this.validateGameRequest(request);
@@ -36014,7 +36021,8 @@ class DeployService {
36014
36021
  request,
36015
36022
  user,
36016
36023
  flags: flags2,
36017
- database: databaseOutcome
36024
+ database: databaseOutcome,
36025
+ jobId
36018
36026
  });
36019
36027
  if (request.pruneSecrets?.length) {
36020
36028
  yield* this.pruneManagedSecretsStep(game2.id, result.deploymentId, request.pruneSecrets);
@@ -36022,7 +36030,7 @@ class DeployService {
36022
36030
  yield { type: "complete", data: updatedGame };
36023
36031
  }
36024
36032
  async* deployDashboard(context2) {
36025
- const { slug, request, user, game: game2 } = context2;
36033
+ const { slug, request, user, game: game2, jobId } = context2;
36026
36034
  const db2 = this.deps.db;
36027
36035
  const contract = this.validateDashboardRequest(request, context2.uploadDeps, context2.extractZip);
36028
36036
  const deploymentId = getDashboardDeploymentId(slug, this.deps.config.sstStage);
@@ -36112,7 +36120,8 @@ class DeployService {
36112
36120
  url: result.url,
36113
36121
  codeHash,
36114
36122
  resources: result.resources,
36115
- target: "dashboard"
36123
+ target: "dashboard",
36124
+ deployJobId: jobId
36116
36125
  });
36117
36126
  setAttribute("app.deploy.code_hash", codeHash);
36118
36127
  try {
@@ -36742,7 +36751,8 @@ class DeployService {
36742
36751
  request,
36743
36752
  user,
36744
36753
  flags: flags2,
36745
- database
36754
+ database,
36755
+ jobId
36746
36756
  }) {
36747
36757
  const { hasBackend, hasFrontend, hasMetadata } = flags2;
36748
36758
  const db2 = this.deps.db;
@@ -36757,7 +36767,8 @@ class DeployService {
36757
36767
  timeTravelBookmark: database.timeTravelBookmark,
36758
36768
  bookmarkCapturedAt: database.bookmarkCapturedAt,
36759
36769
  resources: result.resources,
36760
- target: "game"
36770
+ target: "game",
36771
+ deployJobId: jobId
36761
36772
  });
36762
36773
  await this.persistArtifactHashes(game2.id, {
36763
36774
  buildHash: hasFrontend ? request.buildHash : undefined,
@@ -37031,7 +37042,8 @@ class DeployService {
37031
37042
  timeTravelBookmark: record.timeTravelBookmark ?? null,
37032
37043
  bookmarkCapturedAt: record.bookmarkCapturedAt ?? null,
37033
37044
  resources: record.resources,
37034
- isActive: true
37045
+ isActive: true,
37046
+ deployJobId: record.deployJobId ?? null
37035
37047
  });
37036
37048
  });
37037
37049
  }
@@ -38454,7 +38466,7 @@ function createGameServices(deps) {
38454
38466
  uploadBucket: config3.uploadBucket,
38455
38467
  storage,
38456
38468
  validateDeveloperAccessBySlug: (user, slug) => game2.validateDeveloperAccessBySlug(user, slug),
38457
- runDeploy: (slug, request, user, uploadDeps, extractZip) => deploy.deploy(slug, request, user, uploadDeps, extractZip),
38469
+ runDeploy: (slug, request, user, options) => deploy.deploy(slug, request, user, options),
38458
38470
  notifyDeploymentFailure: (failure) => deploy.notifyDeploymentFailure(failure)
38459
38471
  });
38460
38472
  const logs = new LogsService({
@@ -46850,6 +46862,95 @@ var init_kv_service = __esm(() => {
46850
46862
  init_errors2();
46851
46863
  });
46852
46864
 
46865
+ // ../utils/src/url.ts
46866
+ function publishableRepositoryUrl(value) {
46867
+ let url;
46868
+ try {
46869
+ url = new URL(value);
46870
+ } catch {
46871
+ return null;
46872
+ }
46873
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
46874
+ return null;
46875
+ }
46876
+ return value;
46877
+ }
46878
+
46879
+ // ../api-core/src/services/releases.service.ts
46880
+ class ReleasesService {
46881
+ deps;
46882
+ constructor(deps) {
46883
+ this.deps = deps;
46884
+ }
46885
+ async list() {
46886
+ const rows = await this.deps.db.select(releaseColumns()).from(gameDeployments).innerJoin(games, eq(gameDeployments.gameId, games.id)).leftJoin(gameDeployJobs, eq(gameDeployments.deployJobId, gameDeployJobs.id)).where(and(activeWorkerDeployment(), listedGame())).orderBy(games.slug);
46887
+ setAttributes({ "app.releases.count": rows.length });
46888
+ return { releases: rows.map(toRelease) };
46889
+ }
46890
+ async get(slug, { history } = {}) {
46891
+ const game2 = await this.deps.db.query.games.findFirst({
46892
+ where: and(eq(games.slug, slug), listedGame()),
46893
+ columns: { id: true }
46894
+ });
46895
+ if (!game2) {
46896
+ throw new NotFoundError("Game", slug);
46897
+ }
46898
+ const rows = await this.deps.db.select(releaseColumns()).from(gameDeployments).innerJoin(games, eq(gameDeployments.gameId, games.id)).leftJoin(gameDeployJobs, eq(gameDeployments.deployJobId, gameDeployJobs.id)).where(and(eq(gameDeployments.gameId, game2.id), eq(gameDeployments.target, "game"))).orderBy(desc(gameDeployments.deployedAt)).limit(Math.max(1, history ?? 0));
46899
+ setAttributes({
46900
+ "app.releases.history_requested": history ?? 0,
46901
+ "app.releases.history_rows": rows.length
46902
+ });
46903
+ const releases = rows.map(toRelease);
46904
+ const current = releases[0] ?? null;
46905
+ return history === undefined ? { current } : { current, history: releases };
46906
+ }
46907
+ }
46908
+ function releaseColumns() {
46909
+ return {
46910
+ slug: games.slug,
46911
+ url: games.deploymentUrl,
46912
+ source: sql`${gameDeployJobs.request} -> 'source'`,
46913
+ deployedAt: gameDeployments.deployedAt
46914
+ };
46915
+ }
46916
+ function listedGame() {
46917
+ return ne(games.visibility, "internal");
46918
+ }
46919
+ function activeWorkerDeployment() {
46920
+ return and(eq(gameDeployments.isActive, true), eq(gameDeployments.target, "game"));
46921
+ }
46922
+ function toRelease(row) {
46923
+ return {
46924
+ slug: row.slug,
46925
+ url: row.url,
46926
+ source: normalizeSource(row.source),
46927
+ deployedAt: row.deployedAt.toISOString()
46928
+ };
46929
+ }
46930
+ function normalizeSource(value) {
46931
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
46932
+ return null;
46933
+ }
46934
+ const raw = value;
46935
+ const repository = nullableString(raw.repository);
46936
+ return {
46937
+ repository: repository === null ? null : publishableRepositoryUrl(repository),
46938
+ commit: nullableString(raw.commit),
46939
+ branch: nullableString(raw.branch),
46940
+ dirty: raw.dirty === true,
46941
+ externalBuild: raw.externalBuild === true
46942
+ };
46943
+ }
46944
+ function nullableString(value) {
46945
+ return typeof value === "string" && value.length > 0 ? value : null;
46946
+ }
46947
+ var init_releases_service = __esm(() => {
46948
+ init_drizzle_orm();
46949
+ init_tables_index();
46950
+ init_spans();
46951
+ init_errors2();
46952
+ });
46953
+
46853
46954
  // ../api-core/src/services/secrets.service.ts
46854
46955
  class SecretsService {
46855
46956
  deps;
@@ -47836,13 +47937,7 @@ var init_schemas2 = __esm(() => {
47836
47937
  path: ["lastAppliedMigrationTag"]
47837
47938
  });
47838
47939
  DeploySourceSchema = exports_external.object({
47839
- repository: exports_external.string().url().nullable().transform((value) => {
47840
- if (value === null) {
47841
- return null;
47842
- }
47843
- const url = new URL(value);
47844
- return url.username || url.password || url.search || url.hash ? null : value;
47845
- }),
47940
+ repository: exports_external.string().url().nullable().transform((value) => value === null ? null : publishableRepositoryUrl(value)),
47846
47941
  commit: exports_external.string().regex(/^[0-9a-f]{40}$/, "commit must be a full 40-character lowercase SHA").nullable(),
47847
47942
  branch: exports_external.string().min(1).nullable(),
47848
47943
  dirty: exports_external.boolean(),
@@ -109838,6 +109933,7 @@ function createPlatformServices(deps) {
109838
109933
  const kv = new KVService({ db: db2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
109839
109934
  const secrets = new SecretsService({ db: db2, config: config5, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
109840
109935
  const domain3 = new DomainService({ db: db2, cloudflare: cloudflare2, corsKvs, validateDeveloperAccessBySlug });
109936
+ const releases = new ReleasesService({ db: db2 });
109841
109937
  const deploymentState = new DeploymentStateService({
109842
109938
  db: db2,
109843
109939
  config: config5,
@@ -109897,6 +109993,7 @@ function createPlatformServices(deps) {
109897
109993
  kv,
109898
109994
  secrets,
109899
109995
  domain: domain3,
109996
+ releases,
109900
109997
  deploymentState,
109901
109998
  database,
109902
109999
  seed,
@@ -109915,6 +110012,7 @@ var init_platform2 = __esm(async () => {
109915
110012
  init_deployment_state_service();
109916
110013
  init_domain_service();
109917
110014
  init_kv_service();
110015
+ init_releases_service();
109918
110016
  init_secrets_service();
109919
110017
  init_seed_service();
109920
110018
  init_upload_service();
@@ -167646,6 +167744,47 @@ var init_lti_controller = __esm(() => {
167646
167744
  });
167647
167745
  });
167648
167746
 
167747
+ // ../api-core/src/controllers/releases.controller.ts
167748
+ function requireNoQuery(url4) {
167749
+ if (url4.search !== "") {
167750
+ throw ApiError.badRequest("This endpoint takes no query parameters");
167751
+ }
167752
+ }
167753
+ function parseHistoryParam(url4) {
167754
+ if (url4.search === "") {
167755
+ return;
167756
+ }
167757
+ const match3 = CANONICAL_HISTORY.exec(url4.search);
167758
+ if (!match3) {
167759
+ throw ApiError.badRequest(`The only supported query is ?history=N with N an integer between 1 and ${MAX_HISTORY}`);
167760
+ }
167761
+ return Number(match3[1]);
167762
+ }
167763
+ function cached3(body2) {
167764
+ return Response.json(body2, {
167765
+ headers: {
167766
+ "Content-Type": "application/json",
167767
+ "Cache-Control": CACHE_CONTROL2
167768
+ }
167769
+ });
167770
+ }
167771
+ async function list3(ctx) {
167772
+ requireNoQuery(ctx.url);
167773
+ return cached3(await ctx.services.releases.list());
167774
+ }
167775
+ async function get2(ctx) {
167776
+ const slug2 = requireSlug(ctx.params.slug);
167777
+ const history2 = parseHistoryParam(ctx.url);
167778
+ return cached3(await ctx.services.releases.get(slug2, { history: history2 }));
167779
+ }
167780
+ var CACHE_CONTROL2 = "public, max-age=60", MAX_HISTORY = 100, CANONICAL_HISTORY, releases;
167781
+ var init_releases_controller = __esm(() => {
167782
+ init_errors2();
167783
+ init_utils7();
167784
+ CANONICAL_HISTORY = /^\?history=([1-9]\d?|100)$/;
167785
+ releases = defineControllerNames("releases", { list: list3, get: get2 });
167786
+ });
167787
+
167649
167788
  // ../api-core/src/controllers/secrets.controller.ts
167650
167789
  var listKeys2, setSecrets, deleteSecret, diff, secrets;
167651
167790
  var init_secrets_controller = __esm(() => {
@@ -168808,6 +168947,7 @@ var init_controllers = __esm(() => {
168808
168947
  init_leaderboard_controller();
168809
168948
  init_logs_controller();
168810
168949
  init_lti_controller();
168950
+ init_releases_controller();
168811
168951
  init_secrets_controller();
168812
168952
  init_seed_controller();
168813
168953
  init_session_controller();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.10.1-beta.7",
3
+ "version": "0.10.1-beta.8",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {