@playcademy/sandbox 0.10.1-beta.6 → 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 +170 -15
  2. package/dist/server.js +170 -15
  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.6",
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;
@@ -47590,7 +47691,7 @@ var init_emoji = __esm(() => {
47590
47691
  function requestsBinding(binding) {
47591
47692
  return binding === true || Array.isArray(binding) && binding.length > 0;
47592
47693
  }
47593
- var HttpUrlSchema, GameEmojiSchema, GamePermissionsSchema, GameMetadataRecordSchema, InsertGameSchema, UpdateGameSchema, InsertGameDeploymentSchema, InsertGameDeployJobSchema, InsertGameDeploymentStateSchema, UpsertGameMetadataSchema, PatchGameMetadataSchema, AddGameMemberSchema, UpdateGameMemberRoleSchema, AddGameDashboardUserSchema, VerifyGameDashboardLoginSchema, AcceptGameDashboardInviteSchema, AcceptGameDashboardResetSchema, ALLOWED_UPLOAD_EXTENSIONS, InitiateUploadSchema, AddCustomHostnameSchema, SetSecretsRequestSchema, SecretsDiffRequestSchema, SeedRequestSchema, SchemaInfoSchema, VerifyTokenSchema, KVSeedRequestSchema, DeployMigrationSchema, DeployDatabaseSchema, DatabaseResetDatabaseSchema, DatabaseResetRequestSchema, BaselineEvidenceSchema, DeployBaselineSchema, DeploymentStateBaselineSchema, MigrationRealignSchema, MigrationResolveSchema, DatabaseRestoreSchema, DeployBlockedReportSchema, DeployRequestSchema;
47694
+ var HttpUrlSchema, GameEmojiSchema, GamePermissionsSchema, GameMetadataRecordSchema, InsertGameSchema, UpdateGameSchema, InsertGameDeploymentSchema, InsertGameDeployJobSchema, InsertGameDeploymentStateSchema, UpsertGameMetadataSchema, PatchGameMetadataSchema, AddGameMemberSchema, UpdateGameMemberRoleSchema, AddGameDashboardUserSchema, VerifyGameDashboardLoginSchema, AcceptGameDashboardInviteSchema, AcceptGameDashboardResetSchema, ALLOWED_UPLOAD_EXTENSIONS, InitiateUploadSchema, AddCustomHostnameSchema, SetSecretsRequestSchema, SecretsDiffRequestSchema, SeedRequestSchema, SchemaInfoSchema, VerifyTokenSchema, KVSeedRequestSchema, DeployMigrationSchema, DeployDatabaseSchema, DatabaseResetDatabaseSchema, DatabaseResetRequestSchema, BaselineEvidenceSchema, DeployBaselineSchema, DeploySourceSchema, DeploymentStateBaselineSchema, MigrationRealignSchema, MigrationResolveSchema, DatabaseRestoreSchema, DeployBlockedReportSchema, DeployRequestSchema;
47594
47695
  var init_schemas2 = __esm(() => {
47595
47696
  init_drizzle_zod();
47596
47697
  init_esm();
@@ -47836,6 +47937,13 @@ var init_schemas2 = __esm(() => {
47836
47937
  message: "A baseline must claim something — migration history, a schema snapshot, or artifact hashes",
47837
47938
  path: ["lastAppliedMigrationTag"]
47838
47939
  });
47940
+ DeploySourceSchema = exports_external.object({
47941
+ repository: exports_external.string().url().nullable().transform((value) => value === null ? null : publishableRepositoryUrl(value)),
47942
+ commit: exports_external.string().regex(/^[0-9a-f]{40}$/, "commit must be a full 40-character lowercase SHA").nullable(),
47943
+ branch: exports_external.string().min(1).nullable(),
47944
+ dirty: exports_external.boolean(),
47945
+ externalBuild: exports_external.boolean().default(false)
47946
+ });
47839
47947
  DeploymentStateBaselineSchema = exports_external.object({
47840
47948
  lastAppliedMigrationTag: exports_external.string().min(1).optional(),
47841
47949
  journal: exports_external.array(exports_external.object({
@@ -47907,6 +48015,7 @@ var init_schemas2 = __esm(() => {
47907
48015
  buildHash: exports_external.string().min(1).optional(),
47908
48016
  integrationsHash: exports_external.string().min(1).optional(),
47909
48017
  pruneSecrets: exports_external.array(exports_external.string().min(1)).optional(),
48018
+ source: DeploySourceSchema.optional(),
47910
48019
  metadata: exports_external.object({
47911
48020
  displayName: exports_external.string().optional(),
47912
48021
  description: exports_external.string().optional(),
@@ -109825,6 +109934,7 @@ function createPlatformServices(deps) {
109825
109934
  const kv = new KVService({ db: db2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
109826
109935
  const secrets = new SecretsService({ db: db2, config: config5, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
109827
109936
  const domain3 = new DomainService({ db: db2, cloudflare: cloudflare2, corsKvs, validateDeveloperAccessBySlug });
109937
+ const releases = new ReleasesService({ db: db2 });
109828
109938
  const deploymentState = new DeploymentStateService({
109829
109939
  db: db2,
109830
109940
  config: config5,
@@ -109884,6 +109994,7 @@ function createPlatformServices(deps) {
109884
109994
  kv,
109885
109995
  secrets,
109886
109996
  domain: domain3,
109997
+ releases,
109887
109998
  deploymentState,
109888
109999
  database,
109889
110000
  seed,
@@ -109902,6 +110013,7 @@ var init_platform2 = __esm(async () => {
109902
110013
  init_deployment_state_service();
109903
110014
  init_domain_service();
109904
110015
  init_kv_service();
110016
+ init_releases_service();
109905
110017
  init_secrets_service();
109906
110018
  init_seed_service();
109907
110019
  init_upload_service();
@@ -167633,6 +167745,47 @@ var init_lti_controller = __esm(() => {
167633
167745
  });
167634
167746
  });
167635
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
+
167636
167789
  // ../api-core/src/controllers/secrets.controller.ts
167637
167790
  var listKeys2, setSecrets, deleteSecret, diff, secrets;
167638
167791
  var init_secrets_controller = __esm(() => {
@@ -168795,6 +168948,7 @@ var init_controllers = __esm(() => {
168795
168948
  init_leaderboard_controller();
168796
168949
  init_logs_controller();
168797
168950
  init_lti_controller();
168951
+ init_releases_controller();
168798
168952
  init_secrets_controller();
168799
168953
  init_seed_controller();
168800
168954
  init_session_controller();
@@ -169246,7 +169400,8 @@ var init_deploy = __esm(async () => {
169246
169400
  config: body2.config,
169247
169401
  bindings: body2.bindings,
169248
169402
  schema: body2.schema,
169249
- metadata: body2.metadata
169403
+ metadata: body2.metadata,
169404
+ source: body2.source
169250
169405
  },
169251
169406
  events,
169252
169407
  result: {
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.6",
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;
@@ -47589,7 +47690,7 @@ var init_emoji = __esm(() => {
47589
47690
  function requestsBinding(binding) {
47590
47691
  return binding === true || Array.isArray(binding) && binding.length > 0;
47591
47692
  }
47592
- var HttpUrlSchema, GameEmojiSchema, GamePermissionsSchema, GameMetadataRecordSchema, InsertGameSchema, UpdateGameSchema, InsertGameDeploymentSchema, InsertGameDeployJobSchema, InsertGameDeploymentStateSchema, UpsertGameMetadataSchema, PatchGameMetadataSchema, AddGameMemberSchema, UpdateGameMemberRoleSchema, AddGameDashboardUserSchema, VerifyGameDashboardLoginSchema, AcceptGameDashboardInviteSchema, AcceptGameDashboardResetSchema, ALLOWED_UPLOAD_EXTENSIONS, InitiateUploadSchema, AddCustomHostnameSchema, SetSecretsRequestSchema, SecretsDiffRequestSchema, SeedRequestSchema, SchemaInfoSchema, VerifyTokenSchema, KVSeedRequestSchema, DeployMigrationSchema, DeployDatabaseSchema, DatabaseResetDatabaseSchema, DatabaseResetRequestSchema, BaselineEvidenceSchema, DeployBaselineSchema, DeploymentStateBaselineSchema, MigrationRealignSchema, MigrationResolveSchema, DatabaseRestoreSchema, DeployBlockedReportSchema, DeployRequestSchema;
47693
+ var HttpUrlSchema, GameEmojiSchema, GamePermissionsSchema, GameMetadataRecordSchema, InsertGameSchema, UpdateGameSchema, InsertGameDeploymentSchema, InsertGameDeployJobSchema, InsertGameDeploymentStateSchema, UpsertGameMetadataSchema, PatchGameMetadataSchema, AddGameMemberSchema, UpdateGameMemberRoleSchema, AddGameDashboardUserSchema, VerifyGameDashboardLoginSchema, AcceptGameDashboardInviteSchema, AcceptGameDashboardResetSchema, ALLOWED_UPLOAD_EXTENSIONS, InitiateUploadSchema, AddCustomHostnameSchema, SetSecretsRequestSchema, SecretsDiffRequestSchema, SeedRequestSchema, SchemaInfoSchema, VerifyTokenSchema, KVSeedRequestSchema, DeployMigrationSchema, DeployDatabaseSchema, DatabaseResetDatabaseSchema, DatabaseResetRequestSchema, BaselineEvidenceSchema, DeployBaselineSchema, DeploySourceSchema, DeploymentStateBaselineSchema, MigrationRealignSchema, MigrationResolveSchema, DatabaseRestoreSchema, DeployBlockedReportSchema, DeployRequestSchema;
47593
47694
  var init_schemas2 = __esm(() => {
47594
47695
  init_drizzle_zod();
47595
47696
  init_esm();
@@ -47835,6 +47936,13 @@ var init_schemas2 = __esm(() => {
47835
47936
  message: "A baseline must claim something — migration history, a schema snapshot, or artifact hashes",
47836
47937
  path: ["lastAppliedMigrationTag"]
47837
47938
  });
47939
+ DeploySourceSchema = exports_external.object({
47940
+ repository: exports_external.string().url().nullable().transform((value) => value === null ? null : publishableRepositoryUrl(value)),
47941
+ commit: exports_external.string().regex(/^[0-9a-f]{40}$/, "commit must be a full 40-character lowercase SHA").nullable(),
47942
+ branch: exports_external.string().min(1).nullable(),
47943
+ dirty: exports_external.boolean(),
47944
+ externalBuild: exports_external.boolean().default(false)
47945
+ });
47838
47946
  DeploymentStateBaselineSchema = exports_external.object({
47839
47947
  lastAppliedMigrationTag: exports_external.string().min(1).optional(),
47840
47948
  journal: exports_external.array(exports_external.object({
@@ -47906,6 +48014,7 @@ var init_schemas2 = __esm(() => {
47906
48014
  buildHash: exports_external.string().min(1).optional(),
47907
48015
  integrationsHash: exports_external.string().min(1).optional(),
47908
48016
  pruneSecrets: exports_external.array(exports_external.string().min(1)).optional(),
48017
+ source: DeploySourceSchema.optional(),
47909
48018
  metadata: exports_external.object({
47910
48019
  displayName: exports_external.string().optional(),
47911
48020
  description: exports_external.string().optional(),
@@ -109824,6 +109933,7 @@ function createPlatformServices(deps) {
109824
109933
  const kv = new KVService({ db: db2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
109825
109934
  const secrets = new SecretsService({ db: db2, config: config5, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
109826
109935
  const domain3 = new DomainService({ db: db2, cloudflare: cloudflare2, corsKvs, validateDeveloperAccessBySlug });
109936
+ const releases = new ReleasesService({ db: db2 });
109827
109937
  const deploymentState = new DeploymentStateService({
109828
109938
  db: db2,
109829
109939
  config: config5,
@@ -109883,6 +109993,7 @@ function createPlatformServices(deps) {
109883
109993
  kv,
109884
109994
  secrets,
109885
109995
  domain: domain3,
109996
+ releases,
109886
109997
  deploymentState,
109887
109998
  database,
109888
109999
  seed,
@@ -109901,6 +110012,7 @@ var init_platform2 = __esm(async () => {
109901
110012
  init_deployment_state_service();
109902
110013
  init_domain_service();
109903
110014
  init_kv_service();
110015
+ init_releases_service();
109904
110016
  init_secrets_service();
109905
110017
  init_seed_service();
109906
110018
  init_upload_service();
@@ -167632,6 +167744,47 @@ var init_lti_controller = __esm(() => {
167632
167744
  });
167633
167745
  });
167634
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
+
167635
167788
  // ../api-core/src/controllers/secrets.controller.ts
167636
167789
  var listKeys2, setSecrets, deleteSecret, diff, secrets;
167637
167790
  var init_secrets_controller = __esm(() => {
@@ -168794,6 +168947,7 @@ var init_controllers = __esm(() => {
168794
168947
  init_leaderboard_controller();
168795
168948
  init_logs_controller();
168796
168949
  init_lti_controller();
168950
+ init_releases_controller();
168797
168951
  init_secrets_controller();
168798
168952
  init_seed_controller();
168799
168953
  init_session_controller();
@@ -169245,7 +169399,8 @@ var init_deploy = __esm(async () => {
169245
169399
  config: body2.config,
169246
169400
  bindings: body2.bindings,
169247
169401
  schema: body2.schema,
169248
- metadata: body2.metadata
169402
+ metadata: body2.metadata,
169403
+ source: body2.source
169249
169404
  },
169250
169405
  events,
169251
169406
  result: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.10.1-beta.6",
3
+ "version": "0.10.1-beta.8",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {