@playcademy/vite-plugin 1.1.3-beta.5 → 1.1.3-beta.7

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 (2) hide show
  1. package/dist/index.js +2655 -184
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -24314,7 +24314,7 @@ import path2 from "node:path";
24314
24314
  // package.json
24315
24315
  var package_default = {
24316
24316
  name: "@playcademy/vite-plugin",
24317
- version: "1.1.3-beta.5",
24317
+ version: "1.1.3-beta.7",
24318
24318
  type: "module",
24319
24319
  exports: {
24320
24320
  ".": {
@@ -25056,6 +25056,7 @@ var WORKER_NAMING;
25056
25056
  var MAX_WORKER_NAME_LENGTH = 63;
25057
25057
  var SECRETS_PREFIX = "secrets_";
25058
25058
  var CLOUDFLARE_COMPATIBILITY_DATE = "2025-10-11";
25059
+ var D1_TIME_TRAVEL_RETENTION_DAYS = 30;
25059
25060
  var init_cloudflare = __esm(() => {
25060
25061
  WORKER_NAMING = {
25061
25062
  STAGING_PREFIX: "staging-",
@@ -25845,7 +25846,7 @@ var package_default2;
25845
25846
  var init_package = __esm(() => {
25846
25847
  package_default2 = {
25847
25848
  name: "@playcademy/sandbox",
25848
- version: "0.6.0",
25849
+ version: "0.6.1-beta.7",
25849
25850
  description: "Local development server for Playcademy game development",
25850
25851
  type: "module",
25851
25852
  exports: {
@@ -25912,6 +25913,16 @@ var init_package = __esm(() => {
25912
25913
  }
25913
25914
  };
25914
25915
  });
25916
+ function deployErrorCode(error) {
25917
+ if (!(error instanceof DomainError)) {
25918
+ return null;
25919
+ }
25920
+ const details = error.details;
25921
+ if (typeof details !== "object" || details === null || !("code" in details)) {
25922
+ return null;
25923
+ }
25924
+ return typeof details.code === "string" ? details.code : null;
25925
+ }
25915
25926
  var DomainError;
25916
25927
  var BadRequestError;
25917
25928
  var UnauthorizedError;
@@ -26004,6 +26015,171 @@ var init_domain_error = __esm(() => {
26004
26015
  }
26005
26016
  };
26006
26017
  });
26018
+ var DEPLOY_ERROR_CODES;
26019
+ var REFUSAL_CODES;
26020
+ var DEPLOY_REFUSAL_CODES;
26021
+ var DELETED_ACCOUNT_LABEL = "(deleted account)";
26022
+ var init_game2 = __esm(() => {
26023
+ DEPLOY_ERROR_CODES = {
26024
+ deployIdPayloadMismatch: "deploy-id-payload-mismatch",
26025
+ stateConflict: "deployment-state-conflict",
26026
+ stateDrift: "deployment-state-drift",
26027
+ destructiveSchema: "destructive-schema-changes",
26028
+ upgradeRequired: "deploy-upgrade-required",
26029
+ checksumMismatch: "migration-checksum-mismatch",
26030
+ journalDivergence: "migration-journal-divergence",
26031
+ outOfOrder: "migration-out-of-order",
26032
+ migrationFailed: "migration-failed",
26033
+ strategyMismatch: "strategy-mismatch",
26034
+ pushFailed: "push-failed",
26035
+ pruneUnmanagedSecrets: "secrets-prune-unmanaged",
26036
+ baselineLedgerNotEmpty: "baseline-ledger-not-empty",
26037
+ baselineDatabaseEmpty: "baseline-database-empty",
26038
+ baselineAlreadyAdopted: "baseline-already-adopted",
26039
+ baselineClaimRejected: "baseline-claim-rejected",
26040
+ restoreUnsupported: "restore-unsupported",
26041
+ restoreIncomplete: "restore-incomplete",
26042
+ restoreBlockedByDeploy: "restore-blocked-by-deploy",
26043
+ restoreExpired: "restore-expired"
26044
+ };
26045
+ REFUSAL_CODES = [
26046
+ DEPLOY_ERROR_CODES.deployIdPayloadMismatch,
26047
+ DEPLOY_ERROR_CODES.stateConflict,
26048
+ DEPLOY_ERROR_CODES.stateDrift,
26049
+ DEPLOY_ERROR_CODES.destructiveSchema,
26050
+ DEPLOY_ERROR_CODES.upgradeRequired,
26051
+ DEPLOY_ERROR_CODES.checksumMismatch,
26052
+ DEPLOY_ERROR_CODES.journalDivergence,
26053
+ DEPLOY_ERROR_CODES.outOfOrder,
26054
+ DEPLOY_ERROR_CODES.strategyMismatch,
26055
+ DEPLOY_ERROR_CODES.pruneUnmanagedSecrets,
26056
+ DEPLOY_ERROR_CODES.baselineLedgerNotEmpty,
26057
+ DEPLOY_ERROR_CODES.baselineDatabaseEmpty,
26058
+ DEPLOY_ERROR_CODES.baselineAlreadyAdopted,
26059
+ DEPLOY_ERROR_CODES.baselineClaimRejected
26060
+ ];
26061
+ DEPLOY_REFUSAL_CODES = new Set(REFUSAL_CODES);
26062
+ });
26063
+ var DeployIdConflictError;
26064
+ var DeploymentStateConflictError;
26065
+ var DeploymentStateDriftError;
26066
+ var LegacySchemaUpgradeRequiredError;
26067
+ var DestructiveSchemaError;
26068
+ var MigrationChecksumMismatchError;
26069
+ var MigrationJournalDivergenceError;
26070
+ var MigrationOrderError;
26071
+ var SecretsPruneUnmanagedError;
26072
+ var BaselineLedgerNotEmptyError;
26073
+ var BaselineDatabaseEmptyError;
26074
+ var BaselineAlreadyAdoptedError;
26075
+ var BaselineClaimRejectedError;
26076
+ var MigrationExecutionError;
26077
+ var PushExecutionError;
26078
+ var init_deploy_error = __esm(() => {
26079
+ init_game2();
26080
+ init_domain_error();
26081
+ DeployIdConflictError = class DeployIdConflictError2 extends ConflictError {
26082
+ constructor(deployId, existingJobId) {
26083
+ super(`Deploy '${deployId}' was already created with a different payload — ` + "generate a fresh deployId for a new deploy", { code: DEPLOY_ERROR_CODES.deployIdPayloadMismatch, deployId, existingJobId });
26084
+ this.name = "DeployIdConflictError";
26085
+ }
26086
+ };
26087
+ DeploymentStateConflictError = class DeploymentStateConflictError2 extends ConflictError {
26088
+ constructor(payload) {
26089
+ super("Deployment state conflict: the schema baseline changed since this deploy was " + "prepared — refetch the deployment state, re-diff, and retry", { code: DEPLOY_ERROR_CODES.stateConflict, ...payload });
26090
+ this.name = "DeploymentStateConflictError";
26091
+ }
26092
+ };
26093
+ DeploymentStateDriftError = class DeploymentStateDriftError2 extends ConflictError {
26094
+ constructor(payload) {
26095
+ super("Deployment state drift: the live database schema does not match the recorded " + "fingerprint (out-of-band change or partial prior apply) — repair the " + "database state before deploying", { code: DEPLOY_ERROR_CODES.stateDrift, ...payload });
26096
+ this.name = "DeploymentStateDriftError";
26097
+ }
26098
+ };
26099
+ LegacySchemaUpgradeRequiredError = class LegacySchemaUpgradeRequiredError2 extends ValidationError {
26100
+ constructor() {
26101
+ super("This game's database state is server-managed; the legacy schema payload is no " + "longer accepted — update the Playcademy CLI and redeploy", { code: DEPLOY_ERROR_CODES.upgradeRequired });
26102
+ this.name = "LegacySchemaUpgradeRequiredError";
26103
+ }
26104
+ };
26105
+ DestructiveSchemaError = class DestructiveSchemaError2 extends ValidationError {
26106
+ constructor(statements) {
26107
+ super(`Push contains ${statements.length} destructive statement(s) that can drop data — ` + "review them and re-run with --accept-data-loss to proceed", { code: DEPLOY_ERROR_CODES.destructiveSchema, statements });
26108
+ this.name = "DestructiveSchemaError";
26109
+ }
26110
+ };
26111
+ MigrationChecksumMismatchError = class MigrationChecksumMismatchError2 extends ConflictError {
26112
+ constructor(mismatches) {
26113
+ const tags = mismatches.map((mismatch) => mismatch.tag).join(", ");
26114
+ super(`Applied migration(s) ${tags} no longer match their recorded checksums.`, {
26115
+ code: DEPLOY_ERROR_CODES.checksumMismatch,
26116
+ mismatches
26117
+ });
26118
+ this.name = "MigrationChecksumMismatchError";
26119
+ }
26120
+ };
26121
+ MigrationJournalDivergenceError = class MigrationJournalDivergenceError2 extends ConflictError {
26122
+ constructor(tags) {
26123
+ super(`The database ledger records applied migration(s) missing from the deploy's ` + `journal: ${tags.join(", ")}. Restore the missing migration files (or resolve ` + "the ledger) before deploying", { code: DEPLOY_ERROR_CODES.journalDivergence, tags });
26124
+ this.name = "MigrationJournalDivergenceError";
26125
+ }
26126
+ };
26127
+ MigrationOrderError = class MigrationOrderError2 extends ValidationError {
26128
+ constructor(tags) {
26129
+ super(`Out-of-order migration(s): ${tags.join(", ")} are ordered before migrations that ` + "already applied. This usually means a branch merged with older-numbered " + "migrations — regenerate them after the applied ones and redeploy", { code: DEPLOY_ERROR_CODES.outOfOrder, tags });
26130
+ this.name = "MigrationOrderError";
26131
+ }
26132
+ };
26133
+ SecretsPruneUnmanagedError = class SecretsPruneUnmanagedError2 extends ValidationError {
26134
+ constructor(keys) {
26135
+ super(`Cannot prune secret(s) not managed by the platform: ${keys.join(", ")}. ` + "Only keys the platform previously pushed can be pruned", { code: DEPLOY_ERROR_CODES.pruneUnmanagedSecrets, keys });
26136
+ this.name = "SecretsPruneUnmanagedError";
26137
+ }
26138
+ };
26139
+ BaselineLedgerNotEmptyError = class BaselineLedgerNotEmptyError2 extends ConflictError {
26140
+ constructor(tags) {
26141
+ super(`Cannot baseline: the migration ledger already records ${tags.length} applied ` + "migration(s) — this database is already server-managed. Just deploy", { code: DEPLOY_ERROR_CODES.baselineLedgerNotEmpty, tags });
26142
+ this.name = "BaselineLedgerNotEmptyError";
26143
+ }
26144
+ };
26145
+ BaselineDatabaseEmptyError = class BaselineDatabaseEmptyError2 extends ValidationError {
26146
+ constructor() {
26147
+ super("Cannot baseline an empty database — there is no applied history to claim. " + "An empty database needs no baseline: deploy directly and the full " + "journal (or schema) applies fresh.", { code: DEPLOY_ERROR_CODES.baselineDatabaseEmpty });
26148
+ this.name = "BaselineDatabaseEmptyError";
26149
+ }
26150
+ };
26151
+ BaselineAlreadyAdoptedError = class BaselineAlreadyAdoptedError2 extends ConflictError {
26152
+ constructor(baselineSource) {
26153
+ super("Cannot baseline: deployment state already records a schema baseline" + `${baselineSource ? ` (source: ${baselineSource})` : ""} — ` + "use realign/resolve to repair drift instead", { code: DEPLOY_ERROR_CODES.baselineAlreadyAdopted, baselineSource });
26154
+ this.name = "BaselineAlreadyAdoptedError";
26155
+ }
26156
+ };
26157
+ BaselineClaimRejectedError = class BaselineClaimRejectedError2 extends ConflictError {
26158
+ constructor(details) {
26159
+ const [first] = details.contradictions;
26160
+ super(first ? `Cannot baseline: the claim contradicts the live database — ${first.tag} ${first.detail ?? ""}` : "Cannot baseline: the claim includes migrations that cannot be verified against the live database — " + "pass --allow-unverified to record them anyway", { code: DEPLOY_ERROR_CODES.baselineClaimRejected, ...details });
26161
+ this.name = "BaselineClaimRejectedError";
26162
+ }
26163
+ };
26164
+ MigrationExecutionError = class MigrationExecutionError2 extends ValidationError {
26165
+ tag;
26166
+ offset;
26167
+ constructor(input) {
26168
+ super(`Migration '${input.tag}' failed and rolled back: ${input.d1Message}. ` + "Fix the migration SQL and redeploy — the deploy resumes from this migration", { code: DEPLOY_ERROR_CODES.migrationFailed, ...input });
26169
+ this.name = "MigrationExecutionError";
26170
+ this.tag = input.tag;
26171
+ this.offset = input.offset;
26172
+ }
26173
+ };
26174
+ PushExecutionError = class PushExecutionError2 extends ValidationError {
26175
+ offset;
26176
+ constructor(input) {
26177
+ super(`Push schema changes failed and rolled back: ${input.d1Message}. ` + "Fix the schema and redeploy — nothing was applied", { code: DEPLOY_ERROR_CODES.pushFailed, ...input });
26178
+ this.name = "PushExecutionError";
26179
+ this.offset = input.offset;
26180
+ }
26181
+ };
26182
+ });
26007
26183
  var STATUS_MAP;
26008
26184
  var ApiError;
26009
26185
  var init_api_error = __esm(() => {
@@ -26101,6 +26277,7 @@ var init_api_error = __esm(() => {
26101
26277
  });
26102
26278
  var init_errors = __esm(() => {
26103
26279
  init_domain_error();
26280
+ init_deploy_error();
26104
26281
  init_api_error();
26105
26282
  });
26106
26283
  function isBrowser() {
@@ -30546,7 +30723,8 @@ var init_schema = __esm(() => {
30546
30723
  ltiTestMode: exports_external.boolean().default(false),
30547
30724
  platformServiceJwt: platformServiceJwtConfigSchema.optional(),
30548
30725
  uploadBucket: exports_external.string().optional(),
30549
- queueIngressSecret: exports_external.string().optional()
30726
+ queueIngressSecret: exports_external.string().optional(),
30727
+ secretsManifestPepper: exports_external.string().optional()
30550
30728
  }).superRefine((config2, ctx) => {
30551
30729
  if (config2.isLocal && !config2.baseUrl) {
30552
30730
  ctx.addIssue({
@@ -36005,6 +36183,8 @@ var deploymentTargetEnum;
36005
36183
  var deployJobStatusEnum;
36006
36184
  var gameDeployments;
36007
36185
  var gameDeployJobs;
36186
+ var gameDeployEvents;
36187
+ var gameDeploymentState;
36008
36188
  var customHostnameStatusEnum;
36009
36189
  var customHostnameSslStatusEnum;
36010
36190
  var customHostnameEnvironmentEnum;
@@ -36097,15 +36277,20 @@ var init_table5 = __esm(() => {
36097
36277
  target: deploymentTargetEnum("target").notNull().default("game"),
36098
36278
  url: text("url").notNull(),
36099
36279
  codeHash: text("code_hash"),
36280
+ schemaHash: text("schema_hash"),
36281
+ schemaFingerprint: text("schema_fingerprint"),
36282
+ timeTravelBookmark: text("time_travel_bookmark"),
36283
+ bookmarkCapturedAt: timestamp("bookmark_captured_at", { withTimezone: true }),
36100
36284
  isActive: boolean("is_active").notNull().default(false),
36101
36285
  resources: jsonb("resources").$type(),
36102
36286
  deployedAt: timestamp("deployed_at", { withTimezone: true }).notNull().defaultNow()
36103
- });
36287
+ }, (table3) => [index("game_deployments_game_target_idx").on(table3.gameId, table3.target)]);
36104
36288
  gameDeployJobs = pgTable("game_deploy_jobs", {
36105
36289
  id: uuid("id").primaryKey().defaultRandom(),
36106
36290
  gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
36107
- userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
36291
+ userId: text("user_id").references(() => users.id, { onDelete: "set null" }),
36108
36292
  status: deployJobStatusEnum("status").notNull().default("pending"),
36293
+ deployId: text("deploy_id"),
36109
36294
  request: jsonb("request").$type().notNull(),
36110
36295
  events: jsonb("events").$type().notNull().default([]),
36111
36296
  error: text("error"),
@@ -36119,6 +36304,27 @@ var init_table5 = __esm(() => {
36119
36304
  createdAt: timestamp("created_at", { mode: "date", withTimezone: true }).notNull().defaultNow(),
36120
36305
  startedAt: timestamp("started_at", { mode: "date", withTimezone: true }),
36121
36306
  completedAt: timestamp("completed_at", { mode: "date", withTimezone: true })
36307
+ }, (table3) => [uniqueIndex("game_deploy_jobs_game_deploy_id_idx").on(table3.gameId, table3.deployId)]);
36308
+ gameDeployEvents = pgTable("game_deploy_events", {
36309
+ id: uuid("id").primaryKey().defaultRandom(),
36310
+ gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
36311
+ userId: text("user_id").references(() => users.id, { onDelete: "set null" }),
36312
+ kind: text("kind").$type().notNull(),
36313
+ payload: jsonb("payload").$type().notNull(),
36314
+ createdAt: timestamp("created_at", { mode: "date", withTimezone: true }).notNull().defaultNow()
36315
+ }, (table3) => [index("game_deploy_events_game_idx").on(table3.gameId, table3.createdAt)]);
36316
+ gameDeploymentState = pgTable("game_deployment_state", {
36317
+ id: uuid("id").primaryKey().defaultRandom(),
36318
+ gameId: uuid("game_id").notNull().unique().references(() => games.id, { onDelete: "cascade" }),
36319
+ schemaHash: text("schema_hash"),
36320
+ schemaSnapshot: jsonb("schema_snapshot"),
36321
+ schemaFingerprint: text("schema_fingerprint"),
36322
+ secretsManifest: jsonb("secrets_manifest").$type(),
36323
+ integrationsHash: text("integrations_hash"),
36324
+ buildHash: text("build_hash"),
36325
+ compatibilityDate: text("compatibility_date"),
36326
+ baselineSource: text("baseline_source").$type(),
36327
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
36122
36328
  });
36123
36329
  customHostnameStatusEnum = pgEnum("custom_hostname_status", [
36124
36330
  "pending",
@@ -36280,7 +36486,9 @@ __export(exports_tables_index, {
36280
36486
  gameMembers: () => gameMembers,
36281
36487
  gameMemberRoleEnum: () => gameMemberRoleEnum,
36282
36488
  gameDeployments: () => gameDeployments,
36489
+ gameDeploymentState: () => gameDeploymentState,
36283
36490
  gameDeployJobs: () => gameDeployJobs,
36491
+ gameDeployEvents: () => gameDeployEvents,
36284
36492
  gameDashboardUsersRelations: () => gameDashboardUsersRelations,
36285
36493
  gameDashboardUsers: () => gameDashboardUsers,
36286
36494
  gameDashboardUserRoleEnum: () => gameDashboardUserRoleEnum,
@@ -47055,6 +47263,107 @@ var init_zip = __esm(() => {
47055
47263
  init_src2();
47056
47264
  import_jszip = __toESM2(require_lib3(), 1);
47057
47265
  });
47266
+ function isPreviewStage(stage) {
47267
+ return PREVIEW_STAGE_PATTERN.test(stage);
47268
+ }
47269
+ var PREVIEW_STAGE_PATTERN;
47270
+ var init_stages = __esm(() => {
47271
+ PREVIEW_STAGE_PATTERN = /^pr-\d+$/;
47272
+ });
47273
+ function deployJobInstant() {
47274
+ return sql`COALESCE(${gameDeployJobs.completedAt}, ${gameDeployJobs.createdAt})`;
47275
+ }
47276
+ async function findLastSuccessfulDeploy(db2, gameId) {
47277
+ const job = await db2.query.gameDeployJobs.findFirst({
47278
+ where: and(eq(gameDeployJobs.gameId, gameId), eq(gameDeployJobs.status, "succeeded")),
47279
+ orderBy: desc(deployJobInstant()),
47280
+ columns: { userId: true, createdAt: true, completedAt: true }
47281
+ });
47282
+ if (!job) {
47283
+ return null;
47284
+ }
47285
+ return { userId: job.userId, at: job.completedAt ?? job.createdAt };
47286
+ }
47287
+ async function findLastSuccessfulDeployWithEmail(db2, gameId) {
47288
+ const lastDeploy = await findLastSuccessfulDeploy(db2, gameId);
47289
+ if (!lastDeploy) {
47290
+ return null;
47291
+ }
47292
+ const deployer = lastDeploy.userId ? await db2.query.users.findFirst({
47293
+ where: eq(users.id, lastDeploy.userId),
47294
+ columns: { email: true }
47295
+ }) : null;
47296
+ return { ...lastDeploy, email: deployer?.email ?? null };
47297
+ }
47298
+ function getGameDeploymentId(gameSlug, sstStage) {
47299
+ if (sstStage === "production") {
47300
+ return gameSlug;
47301
+ }
47302
+ if (sstStage === "dev" || isPreviewStage(sstStage)) {
47303
+ return `${WORKER_NAMING.STAGING_PREFIX}${gameSlug}`;
47304
+ }
47305
+ return `${WORKER_NAMING.LOCAL_PREFIX}${sstStage}-${gameSlug}`;
47306
+ }
47307
+ function getDashboardDeploymentId(gameSlug, sstStage) {
47308
+ return `${getGameDeploymentId(gameSlug, sstStage)}${DASHBOARD_WORKER_SUFFIX}`;
47309
+ }
47310
+ function getGameWorkerApiKeyName(slug) {
47311
+ return `${GAME_WORKER_KEY_PREFIX}${slug}`.substring(0, 32);
47312
+ }
47313
+ function getDashboardWorkerApiKeyName(slug) {
47314
+ return `${DASHBOARD_WORKER_KEY_PREFIX}${slug}`;
47315
+ }
47316
+ function toBindingName(queueKey) {
47317
+ return `${queueKey.replace(/-/g, "_").toUpperCase()}_QUEUE`;
47318
+ }
47319
+ function isSchemaAdopted(state) {
47320
+ if (!state) {
47321
+ return false;
47322
+ }
47323
+ return state.schemaHash !== null || state.schemaFingerprint !== null || state.schemaSnapshot !== null;
47324
+ }
47325
+ function isPushAdopted(state) {
47326
+ return Boolean(state?.schemaHash);
47327
+ }
47328
+ function isMigrateManaged(state) {
47329
+ return Boolean(state?.schemaFingerprint) && !state?.schemaHash;
47330
+ }
47331
+ function generateDeploymentHash(code) {
47332
+ return sha256Hex(code);
47333
+ }
47334
+ function computeDeployPayloadFingerprint(payload) {
47335
+ return sha256Hex(canonicalJson(payload));
47336
+ }
47337
+ function canonicalJson(value) {
47338
+ if (value === null || typeof value !== "object") {
47339
+ return JSON.stringify(value) ?? "null";
47340
+ }
47341
+ if (Array.isArray(value)) {
47342
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
47343
+ }
47344
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([a], [b]) => compareKeys(a, b)).map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`);
47345
+ return `{${entries.join(",")}}`;
47346
+ }
47347
+ function compareKeys(a, b) {
47348
+ if (a < b) {
47349
+ return -1;
47350
+ }
47351
+ if (a > b) {
47352
+ return 1;
47353
+ }
47354
+ return 0;
47355
+ }
47356
+ var GAME_WORKER_KEY_PREFIX = "game-worker-";
47357
+ var DASHBOARD_WORKER_KEY_PREFIX = "dash-worker-";
47358
+ var init_deployment_util = __esm(() => {
47359
+ init_drizzle_orm();
47360
+ init_src();
47361
+ init_tables_index();
47362
+ init_stages();
47363
+ });
47364
+ function isEventDetails(value) {
47365
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47366
+ }
47058
47367
 
47059
47368
  class DeployJobService {
47060
47369
  deps;
@@ -47068,6 +47377,9 @@ class DeployJobService {
47068
47377
  if (leaseLost) {
47069
47378
  return "lease_lost";
47070
47379
  }
47380
+ if (DEPLOY_REFUSAL_CODES.has(deployErrorCode(error) ?? "")) {
47381
+ return "refused";
47382
+ }
47071
47383
  if (error instanceof DomainError) {
47072
47384
  return "domain_error";
47073
47385
  }
@@ -47147,13 +47459,28 @@ class DeployJobService {
47147
47459
  const bucketName = this.getUploadBucket();
47148
47460
  this.deps.storage.deleteObject(bucketName, codeUploadToken).catch(catchAttrs("deploy_job.temp_cleanup"));
47149
47461
  }
47150
- sanitizeRequestForPersistence(request) {
47151
- const sanitized = { ...request };
47462
+ async sanitizeRequestForPersistence(request) {
47463
+ const sanitized = {
47464
+ ...request
47465
+ };
47152
47466
  delete sanitized._headers;
47153
47467
  delete sanitized.code;
47154
47468
  delete sanitized.codeUploadToken;
47469
+ const codeFingerprint = await this.computeCodeFingerprint(request);
47470
+ if (codeFingerprint) {
47471
+ sanitized.codeFingerprint = codeFingerprint;
47472
+ }
47155
47473
  return sanitized;
47156
47474
  }
47475
+ async computeCodeFingerprint(request) {
47476
+ if (request.codeUploadToken) {
47477
+ return `upload:${request.codeUploadToken}`;
47478
+ }
47479
+ if (request.code) {
47480
+ return `sha256:${await generateDeploymentHash(request.code)}`;
47481
+ }
47482
+ return;
47483
+ }
47157
47484
  getLeaseExpiry() {
47158
47485
  return new Date(Date.now() + DEPLOY_JOB_LEASE_MS);
47159
47486
  }
@@ -47186,8 +47513,36 @@ class DeployJobService {
47186
47513
  heartbeatAt: null
47187
47514
  }).where(and(eq(gameDeployJobs.id, jobId), eq(gameDeployJobs.leaseId, leaseId)));
47188
47515
  }
47516
+ async findByDeployId(gameId, deployId) {
47517
+ const job = await this.deps.db.query.gameDeployJobs.findFirst({
47518
+ where: and(eq(gameDeployJobs.gameId, gameId), eq(gameDeployJobs.deployId, deployId))
47519
+ });
47520
+ return job ?? null;
47521
+ }
47522
+ async resolveIdempotentReplay(existing, request) {
47523
+ const [incoming, stored] = await Promise.all([
47524
+ this.sanitizeRequestForPersistence(request).then(computeDeployPayloadFingerprint),
47525
+ computeDeployPayloadFingerprint(existing.request)
47526
+ ]);
47527
+ if (incoming !== stored) {
47528
+ setAttribute("app.deploy_job.idempotency", "payload_mismatch");
47529
+ throw new DeployIdConflictError(request.deployId, existing.id);
47530
+ }
47531
+ setAttributes({
47532
+ "app.deploy_job.idempotency": "replayed",
47533
+ "app.deploy_job.id": existing.id,
47534
+ "app.deploy_job.status": existing.status
47535
+ });
47536
+ return this.toResponse(existing);
47537
+ }
47189
47538
  async create(slug, request, user) {
47190
47539
  const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
47540
+ if (request.deployId) {
47541
+ const existing = await this.findByDeployId(game2.id, request.deployId);
47542
+ if (existing) {
47543
+ return this.resolveIdempotentReplay(existing, request);
47544
+ }
47545
+ }
47191
47546
  const jobId = crypto.randomUUID();
47192
47547
  let codeSource = "none";
47193
47548
  if (request.code) {
@@ -47203,7 +47558,7 @@ class DeployJobService {
47203
47558
  request.code = await this.loadUploadedCode(request.codeUploadToken, game2.id);
47204
47559
  }
47205
47560
  setAttribute("app.deploy_job.code_bundle_size", request.code?.length ?? 0);
47206
- const sanitizedRequest = this.sanitizeRequestForPersistence(request);
47561
+ const sanitizedRequest = await this.sanitizeRequestForPersistence(request);
47207
47562
  if (request.code) {
47208
47563
  await this.storeCodeBundle(jobId, request.code);
47209
47564
  }
@@ -47213,6 +47568,7 @@ class DeployJobService {
47213
47568
  id: jobId,
47214
47569
  gameId: game2.id,
47215
47570
  userId: user.id,
47571
+ deployId: request.deployId ?? null,
47216
47572
  request: sanitizedRequest,
47217
47573
  events: [
47218
47574
  {
@@ -47224,6 +47580,12 @@ class DeployJobService {
47224
47580
  }).returning();
47225
47581
  } catch (error) {
47226
47582
  await this.deleteCodeBundle(jobId);
47583
+ if (request.deployId) {
47584
+ const winner = await this.findByDeployId(game2.id, request.deployId);
47585
+ if (winner) {
47586
+ return this.resolveIdempotentReplay(winner, request);
47587
+ }
47588
+ }
47227
47589
  throw error;
47228
47590
  }
47229
47591
  if (!job) {
@@ -47338,7 +47700,11 @@ class DeployJobService {
47338
47700
  "app.deploy_job.error_status": errorClassification?.errorStatus
47339
47701
  });
47340
47702
  if (!effectiveLeaseLost) {
47341
- await this.addStatusEvent(jobId, "Deployment failed", { error: message });
47703
+ const structuredDetails = error instanceof DomainError && isEventDetails(error.details) ? error.details : undefined;
47704
+ await this.addStatusEvent(jobId, "Deployment failed", {
47705
+ error: message,
47706
+ ...structuredDetails
47707
+ });
47342
47708
  const failed = await this.markFailed(jobId, leaseId, message, errorClassification);
47343
47709
  if (!failed) {
47344
47710
  await this.clearLease(jobId, leaseId);
@@ -47351,18 +47717,21 @@ class DeployJobService {
47351
47717
  displayName: game2.displayName,
47352
47718
  error: message,
47353
47719
  target,
47354
- developer: { id: user.id, email: user.email }
47720
+ developer: { id: user.id, email: user.email },
47721
+ errorCode: deployErrorCode(error)
47355
47722
  });
47356
47723
  }
47357
47724
  await this.deleteCodeBundle(jobId);
47358
47725
  }
47359
47726
  async loadJobActors(job, jobId, leaseId, onMissing) {
47360
- const game2 = await this.deps.db.query.games.findFirst({
47361
- where: eq(games.id, job.gameId)
47362
- });
47363
- const user = await this.deps.db.query.users.findFirst({
47364
- where: eq(users.id, job.userId)
47365
- });
47727
+ const [game2, user] = await Promise.all([
47728
+ this.deps.db.query.games.findFirst({
47729
+ where: eq(games.id, job.gameId)
47730
+ }),
47731
+ job.userId ? this.deps.db.query.users.findFirst({
47732
+ where: eq(users.id, job.userId)
47733
+ }) : undefined
47734
+ ]);
47366
47735
  if (!game2 || !user) {
47367
47736
  const message = !game2 ? "Deploy job game no longer exists" : "Deploy job user no longer exists";
47368
47737
  onMissing();
@@ -47448,7 +47817,8 @@ class DeployJobService {
47448
47817
  for await (const step of this.deps.runDeploy(game2.slug, request, user, uploadDeps, extractZipToDirectory)) {
47449
47818
  assertLease();
47450
47819
  if (step.type === "status" && "message" in step.data && typeof step.data.message === "string") {
47451
- await this.addStatusEvent(jobId, step.data.message);
47820
+ const details = "details" in step.data && isEventDetails(step.data.details) ? step.data.details : undefined;
47821
+ await this.addStatusEvent(jobId, step.data.message, details);
47452
47822
  }
47453
47823
  }
47454
47824
  assertLease();
@@ -47509,8 +47879,10 @@ var init_deploy_job_service = __esm(() => {
47509
47879
  init_helpers_index();
47510
47880
  init_tables_index();
47511
47881
  init_spans();
47882
+ init_game2();
47512
47883
  init_zip();
47513
47884
  init_errors();
47885
+ init_deployment_util();
47514
47886
  STATUS_MAP2 = {
47515
47887
  BAD_REQUEST: 400,
47516
47888
  UNAUTHORIZED: 401,
@@ -47532,7 +47904,154 @@ var init_deploy_job_service = __esm(() => {
47532
47904
  DEPLOY_JOB_LEASE_MS = 2 * 60 * 1000;
47533
47905
  DEPLOY_JOB_HEARTBEAT_MS = 30 * 1000;
47534
47906
  });
47535
- var init_d1 = () => {};
47907
+ function normalizeSqlForChecksum(sql4) {
47908
+ const unified = sql4.replace(/\r\n/g, `
47909
+ `).replace(/\r/g, `
47910
+ `);
47911
+ const stripped = stripSqlComments(unified);
47912
+ return stripped.split(`
47913
+ `).map((line3) => line3.replace(/\s+$/, "")).filter((line3) => line3 !== "").join(`
47914
+ `);
47915
+ }
47916
+ function stripSqlComments(sql4) {
47917
+ let output = "";
47918
+ let i2 = 0;
47919
+ while (i2 < sql4.length) {
47920
+ const char3 = sql4[i2];
47921
+ const next = sql4[i2 + 1];
47922
+ if (char3 === "-" && next === "-") {
47923
+ i2 = skipLineComment(sql4, i2);
47924
+ } else if (char3 === "/" && next === "*") {
47925
+ output += " ";
47926
+ i2 = skipBlockComment(sql4, i2);
47927
+ } else if (char3 === "'" || char3 === '"' || char3 === "`") {
47928
+ const quoted = copyQuoted(sql4, i2, char3);
47929
+ output += quoted.text;
47930
+ i2 = quoted.end;
47931
+ } else if (char3 === "[") {
47932
+ const bracketed = copyBracketed(sql4, i2);
47933
+ output += bracketed.text;
47934
+ i2 = bracketed.end;
47935
+ } else {
47936
+ output += char3;
47937
+ i2++;
47938
+ }
47939
+ }
47940
+ return output;
47941
+ }
47942
+ function skipLineComment(sql4, start2) {
47943
+ let i2 = start2 + 2;
47944
+ while (i2 < sql4.length && sql4[i2] !== `
47945
+ `) {
47946
+ i2++;
47947
+ }
47948
+ return i2;
47949
+ }
47950
+ function skipBlockComment(sql4, start2) {
47951
+ let i2 = start2 + 2;
47952
+ while (i2 < sql4.length && !(sql4[i2] === "*" && sql4[i2 + 1] === "/")) {
47953
+ i2++;
47954
+ }
47955
+ return i2 + 2;
47956
+ }
47957
+ function copyQuoted(sql4, start2, quote) {
47958
+ let text3 = quote;
47959
+ let i2 = start2 + 1;
47960
+ while (i2 < sql4.length) {
47961
+ text3 += sql4[i2];
47962
+ if (sql4[i2] !== quote) {
47963
+ i2++;
47964
+ } else if (sql4[i2 + 1] === quote) {
47965
+ text3 += quote;
47966
+ i2 += 2;
47967
+ } else {
47968
+ i2++;
47969
+ break;
47970
+ }
47971
+ }
47972
+ return { text: text3, end: i2 };
47973
+ }
47974
+ function copyBracketed(sql4, start2) {
47975
+ let text3 = "[";
47976
+ let i2 = start2 + 1;
47977
+ while (i2 < sql4.length) {
47978
+ text3 += sql4[i2];
47979
+ i2++;
47980
+ if (sql4[i2 - 1] === "]") {
47981
+ break;
47982
+ }
47983
+ }
47984
+ return { text: text3, end: i2 };
47985
+ }
47986
+ function findOversizedStatement(statements) {
47987
+ for (const [index2, statement] of statements.entries()) {
47988
+ const byteLength = Buffer.byteLength(statement, "utf8");
47989
+ if (byteLength > D1_MAX_STATEMENT_BYTES) {
47990
+ return { index: index2, byteLength };
47991
+ }
47992
+ }
47993
+ return null;
47994
+ }
47995
+ var MIGRATION_LEDGER_TABLE = "_playcademy_migrations";
47996
+ var MIGRATION_CHECKSUM_ALGO = "sha256-v1";
47997
+ var D1_MAX_STATEMENT_BYTES;
47998
+ var init_schema3 = __esm(() => {
47999
+ D1_MAX_STATEMENT_BYTES = 100 * 1024;
48000
+ });
48001
+ function parseD1ErrorOffset(message) {
48002
+ const match = message.match(/at offset (\d+)/);
48003
+ return match ? Number(match[1]) : null;
48004
+ }
48005
+ var MIGRATION_LEDGER_DDL;
48006
+ var D1StatementTooLargeError;
48007
+ var D1BatchError;
48008
+ var D1MigrationError;
48009
+ var init_d1 = __esm(() => {
48010
+ init_schema3();
48011
+ MIGRATION_LEDGER_DDL = `CREATE TABLE IF NOT EXISTS ${MIGRATION_LEDGER_TABLE} (
48012
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
48013
+ tag TEXT NOT NULL UNIQUE,
48014
+ checksum TEXT NOT NULL,
48015
+ checksum_algo TEXT NOT NULL DEFAULT 'sha256-v1',
48016
+ deploy_id TEXT NOT NULL,
48017
+ applied_by TEXT,
48018
+ applied_at TEXT NOT NULL,
48019
+ source TEXT NOT NULL DEFAULT 'deploy',
48020
+ statements_total INTEGER
48021
+ )`;
48022
+ D1StatementTooLargeError = class D1StatementTooLargeError2 extends Error {
48023
+ name = "D1StatementTooLargeError";
48024
+ statementIndex;
48025
+ byteLength;
48026
+ constructor(tag, statementIndex, byteLength) {
48027
+ super(`Migration '${tag}' statement ${statementIndex} is ${byteLength} bytes — ` + `over D1's ${D1_MAX_STATEMENT_BYTES / 1024} KB per-statement limit. ` + "Split the statement or reduce its size.");
48028
+ this.statementIndex = statementIndex;
48029
+ this.byteLength = byteLength;
48030
+ }
48031
+ };
48032
+ D1BatchError = class D1BatchError2 extends Error {
48033
+ name = "D1BatchError";
48034
+ d1Message;
48035
+ offset;
48036
+ constructor(d1Message, cause) {
48037
+ super(`Failed to execute batch: ${d1Message}`, { cause });
48038
+ this.d1Message = d1Message;
48039
+ this.offset = parseD1ErrorOffset(d1Message);
48040
+ }
48041
+ };
48042
+ D1MigrationError = class D1MigrationError2 extends Error {
48043
+ name = "D1MigrationError";
48044
+ tag;
48045
+ d1Message;
48046
+ offset;
48047
+ constructor(tag, d1Message, cause) {
48048
+ super(`Migration '${tag}' failed and rolled back: ${d1Message}`, { cause });
48049
+ this.tag = tag;
48050
+ this.d1Message = d1Message;
48051
+ this.offset = parseD1ErrorOffset(d1Message);
48052
+ }
48053
+ };
48054
+ });
47536
48055
  var init_kv = () => {};
47537
48056
  var RESERVED_SUBDOMAINS;
47538
48057
  var init_hostname = __esm(() => {
@@ -47542,6 +48061,159 @@ var init_mime = () => {};
47542
48061
  var init_assets = __esm(() => {
47543
48062
  init_mime();
47544
48063
  });
48064
+ function findOutOfOrderTags(orderedTags, applied) {
48065
+ let lastAppliedIndex = -1;
48066
+ orderedTags.forEach((tag, index2) => {
48067
+ if (applied.has(tag)) {
48068
+ lastAppliedIndex = index2;
48069
+ }
48070
+ });
48071
+ return orderedTags.filter((tag, index2) => index2 < lastAppliedIndex && !applied.has(tag));
48072
+ }
48073
+ function detectDestructiveStatements(statements) {
48074
+ return statements.filter((statement) => {
48075
+ const scannable = blankStringLiterals(normalizeSqlForChecksum(statement));
48076
+ return DESTRUCTIVE_SQL_PATTERNS.some((pattern) => pattern.test(scannable));
48077
+ });
48078
+ }
48079
+ function splitSqlStatements(sql4) {
48080
+ const statements = [];
48081
+ let current = "";
48082
+ let i2 = 0;
48083
+ while (i2 < sql4.length) {
48084
+ const char3 = sql4[i2];
48085
+ const next = sql4[i2 + 1];
48086
+ if (char3 === ";") {
48087
+ statements.push(current);
48088
+ current = "";
48089
+ i2++;
48090
+ } else if (char3 === "-" && next === "-") {
48091
+ const end = scanLineCommentEnd(sql4, i2);
48092
+ current += sql4.slice(i2, end);
48093
+ i2 = end;
48094
+ } else if (char3 === "/" && next === "*") {
48095
+ const end = scanBlockCommentEnd(sql4, i2);
48096
+ current += sql4.slice(i2, end);
48097
+ i2 = end;
48098
+ } else if (char3 === "'" || char3 === '"' || char3 === "`") {
48099
+ const end = scanQuoteEnd(sql4, i2, char3);
48100
+ current += sql4.slice(i2, end);
48101
+ i2 = end;
48102
+ } else if (char3 === "[") {
48103
+ const end = scanBracketEnd(sql4, i2);
48104
+ current += sql4.slice(i2, end);
48105
+ i2 = end;
48106
+ } else {
48107
+ current += char3;
48108
+ i2++;
48109
+ }
48110
+ }
48111
+ statements.push(current);
48112
+ return statements.map((statement) => statement.trim()).filter((statement) => normalizeSqlForChecksum(statement).trim() !== "");
48113
+ }
48114
+ function blankStringLiterals(sql4) {
48115
+ let output = "";
48116
+ let i2 = 0;
48117
+ while (i2 < sql4.length) {
48118
+ const char3 = sql4[i2];
48119
+ if (char3 === "'") {
48120
+ output += "''";
48121
+ i2 = scanQuoteEnd(sql4, i2, char3);
48122
+ } else if (char3 === '"' || char3 === "`") {
48123
+ const end = scanQuoteEnd(sql4, i2, char3);
48124
+ output += sql4.slice(i2, end);
48125
+ i2 = end;
48126
+ } else if (char3 === "[") {
48127
+ const end = scanBracketEnd(sql4, i2);
48128
+ output += sql4.slice(i2, end);
48129
+ i2 = end;
48130
+ } else {
48131
+ output += char3;
48132
+ i2++;
48133
+ }
48134
+ }
48135
+ return output;
48136
+ }
48137
+ function scanLineCommentEnd(sql4, start2) {
48138
+ let i2 = start2 + 2;
48139
+ while (i2 < sql4.length && sql4[i2] !== `
48140
+ `) {
48141
+ i2++;
48142
+ }
48143
+ return i2;
48144
+ }
48145
+ function scanBlockCommentEnd(sql4, start2) {
48146
+ let i2 = start2 + 2;
48147
+ while (i2 < sql4.length && !(sql4[i2] === "*" && sql4[i2 + 1] === "/")) {
48148
+ i2++;
48149
+ }
48150
+ return Math.min(i2 + 2, sql4.length);
48151
+ }
48152
+ function scanQuoteEnd(sql4, start2, quote) {
48153
+ let i2 = start2 + 1;
48154
+ while (i2 < sql4.length) {
48155
+ if (sql4[i2] !== quote) {
48156
+ i2++;
48157
+ } else if (sql4[i2 + 1] === quote) {
48158
+ i2 += 2;
48159
+ } else {
48160
+ return i2 + 1;
48161
+ }
48162
+ }
48163
+ return i2;
48164
+ }
48165
+ function scanBracketEnd(sql4, start2) {
48166
+ let i2 = start2 + 1;
48167
+ while (i2 < sql4.length) {
48168
+ if (sql4[i2] === "]") {
48169
+ return i2 + 1;
48170
+ }
48171
+ i2++;
48172
+ }
48173
+ return i2;
48174
+ }
48175
+ function isAlreadyExistsSqlError(message) {
48176
+ return /already exists|duplicate column/i.test(message);
48177
+ }
48178
+ function readIdentifier(groups) {
48179
+ return groups.find((group) => group !== undefined) ?? "";
48180
+ }
48181
+ function extractCreatedObjects(statements) {
48182
+ const tables = [];
48183
+ const columns2 = [];
48184
+ for (const statement of statements) {
48185
+ const scannable = blankStringLiterals(normalizeSqlForChecksum(statement));
48186
+ for (const match of scannable.matchAll(CREATE_TABLE_RE)) {
48187
+ const name2 = readIdentifier(match.slice(1, 5));
48188
+ if (name2 && !name2.startsWith("__new_")) {
48189
+ tables.push(name2);
48190
+ }
48191
+ }
48192
+ for (const match of scannable.matchAll(ADD_COLUMN_RE)) {
48193
+ const table8 = readIdentifier(match.slice(1, 5));
48194
+ const column2 = readIdentifier(match.slice(5, 9));
48195
+ if (table8 && column2 && !table8.startsWith("__new_")) {
48196
+ columns2.push({ table: table8, column: column2 });
48197
+ }
48198
+ }
48199
+ }
48200
+ return { tables, columns: columns2 };
48201
+ }
48202
+ var DESTRUCTIVE_SQL_PATTERNS;
48203
+ var IDENTIFIER_SOURCE;
48204
+ var CREATE_TABLE_RE;
48205
+ var ADD_COLUMN_RE;
48206
+ var init_sql3 = __esm(() => {
48207
+ init_schema3();
48208
+ DESTRUCTIVE_SQL_PATTERNS = [
48209
+ /\bDROP\s+TABLE\b/i,
48210
+ /\bALTER\s+TABLE\b[\s\S]*\bDROP\b/i,
48211
+ /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`[]?__new_/i
48212
+ ];
48213
+ IDENTIFIER_SOURCE = String.raw`(?:"([^"]+)"|\`([^\`]+)\`|\[([^\]]+)\]|([A-Za-z_][\w$]*))`;
48214
+ CREATE_TABLE_RE = new RegExp(String.raw`\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?${IDENTIFIER_SOURCE}`, "gi");
48215
+ ADD_COLUMN_RE = new RegExp(String.raw`\bALTER\s+TABLE\s+${IDENTIFIER_SOURCE}\s+ADD\s+(?:COLUMN\s+)?${IDENTIFIER_SOURCE}`, "gi");
48216
+ });
47545
48217
  var require_sbmh = __commonJS2((exports, module2) => {
47546
48218
  var { EventEmitter } = __require2("node:events");
47547
48219
  var { inherits } = __require2("node:util");
@@ -49444,6 +50116,8 @@ var init_multipart = __esm(() => {
49444
50116
  var init_utils5 = __esm(() => {
49445
50117
  init_hostname();
49446
50118
  init_assets();
50119
+ init_schema3();
50120
+ init_sql3();
49447
50121
  init_multipart();
49448
50122
  });
49449
50123
  var init_r2 = __esm(() => {
@@ -49584,6 +50258,12 @@ var init_client = __esm(() => {
49584
50258
  var init_core = __esm(() => {
49585
50259
  init_client();
49586
50260
  });
50261
+ var init_src4 = __esm(() => {
50262
+ init_core();
50263
+ init_namespaces();
50264
+ init_utils5();
50265
+ init_utils5();
50266
+ });
49587
50267
  var CUSTOM_DOMAINS_KV_NAME = "cademy-custom-domains";
49588
50268
  var QUEUE_NAME_PREFIX = "playcademy";
49589
50269
  var GAME_WORKER_DOMAIN_PRODUCTION;
@@ -52074,43 +52754,316 @@ var matchedPorts;
52074
52754
  var init_tunnel = __esm(() => {
52075
52755
  matchedPorts = new Map;
52076
52756
  });
52077
- function isPreviewStage(stage) {
52078
- return PREVIEW_STAGE_PATTERN.test(stage);
52757
+ function assertBaselineClaimValid(args2) {
52758
+ const validation = validateBaselineClaim(args2);
52759
+ const blocked = validation.contradictions.length > 0 || validation.unverified.length > 0 && !args2.allowUnverified;
52760
+ if (blocked) {
52761
+ addEvent("deployment_state.baseline_claim_rejected", {
52762
+ "app.game.id": args2.gameId,
52763
+ "app.deployment_state.baseline_claim_source": args2.source,
52764
+ "app.deployment_state.baseline_contradictions": validation.contradictions.length,
52765
+ "app.deployment_state.baseline_unverified": validation.unverified.length,
52766
+ "app.deployment_state.baseline_suggested_tag": validation.suggestedTag ?? "none"
52767
+ });
52768
+ throw new BaselineClaimRejectedError({
52769
+ contradictions: validation.contradictions,
52770
+ unverified: validation.unverified,
52771
+ suggestedTag: validation.suggestedTag,
52772
+ claimedTag: args2.claimedTag
52773
+ });
52774
+ }
52775
+ if (validation.unverified.length > 0) {
52776
+ addEvent("deployment_state.baseline_unverified_overridden", {
52777
+ "app.game.id": args2.gameId,
52778
+ "app.user.id": args2.userId,
52779
+ "app.deployment_state.baseline_overridden_tags": validation.unverified.map((entry2) => entry2.tag).join(",")
52780
+ });
52781
+ }
52782
+ }
52783
+ function validateBaselineClaim(args2) {
52784
+ const { claimedTag, evidence, tables, indexes: indexes2, views, lastDeployAt } = args2;
52785
+ const claimedIndex = evidence.findIndex((entry2) => entry2.tag === claimedTag);
52786
+ const verdicts = [];
52787
+ const unverified = [];
52788
+ evidence.forEach((entry2, index2) => {
52789
+ if (claimedIndex === -1 || index2 > claimedIndex) {
52790
+ verdicts.push(judgeBeyond(entry2, tables, indexes2, views));
52791
+ return;
52792
+ }
52793
+ const judged = judgeClaimed(entry2, tables, lastDeployAt);
52794
+ verdicts.push(judged.verdict);
52795
+ if (judged.unverifiable) {
52796
+ unverified.push(judged.verdict);
52797
+ }
52798
+ });
52799
+ return {
52800
+ verdicts,
52801
+ contradictions: verdicts.filter((verdict) => verdict.verdict === "contradicted"),
52802
+ unverified,
52803
+ suggestedTag: suggestTag(evidence, tables)
52804
+ };
52079
52805
  }
52080
- var PREVIEW_STAGE_PATTERN;
52081
- var init_stages = __esm(() => {
52082
- PREVIEW_STAGE_PATTERN = /^pr-\d+$/;
52806
+ function judgeClaimed(entry2, tables, lastDeployAt) {
52807
+ if (!hasSignal(entry2)) {
52808
+ const generated = new Date(entry2.generatedAt);
52809
+ if (lastDeployAt && generated > lastDeployAt) {
52810
+ return {
52811
+ verdict: {
52812
+ tag: entry2.tag,
52813
+ verdict: "no-signal",
52814
+ detail: `no schema footprint to verify, and it was generated after the last deploy (${lastDeployAt.toISOString()})`
52815
+ },
52816
+ unverifiable: true
52817
+ };
52818
+ }
52819
+ return { verdict: { tag: entry2.tag, verdict: "no-signal" }, unverifiable: false };
52820
+ }
52821
+ for (const table8 of entry2.createsTables) {
52822
+ const columns2 = tables.get(table8.name);
52823
+ if (!columns2) {
52824
+ return {
52825
+ verdict: {
52826
+ tag: entry2.tag,
52827
+ verdict: "contradicted",
52828
+ detail: `creates table \`${table8.name}\`, which is not in the live database`
52829
+ },
52830
+ unverifiable: false
52831
+ };
52832
+ }
52833
+ const missing = table8.columns.filter((column2) => !columns2.includes(column2));
52834
+ if (missing.length > 0) {
52835
+ return {
52836
+ verdict: {
52837
+ tag: entry2.tag,
52838
+ verdict: "contradicted",
52839
+ detail: `table \`${table8.name}\` exists but is missing column(s) ${missing.map((column2) => `\`${column2}\``).join(", ")} this migration defines`
52840
+ },
52841
+ unverifiable: false
52842
+ };
52843
+ }
52844
+ }
52845
+ for (const added of entry2.addsColumns) {
52846
+ const columns2 = tables.get(added.table);
52847
+ if (columns2 && !columns2.includes(added.column)) {
52848
+ return {
52849
+ verdict: {
52850
+ tag: entry2.tag,
52851
+ verdict: "contradicted",
52852
+ detail: `adds column \`${added.column}\` to \`${added.table}\`, which the live table does not have`
52853
+ },
52854
+ unverifiable: false
52855
+ };
52856
+ }
52857
+ }
52858
+ return { verdict: { tag: entry2.tag, verdict: "verified" }, unverifiable: false };
52859
+ }
52860
+ function judgeBeyond(entry2, tables, indexes2, views) {
52861
+ for (const table8 of entry2.createsTables) {
52862
+ if (tables.has(table8.name)) {
52863
+ return {
52864
+ tag: entry2.tag,
52865
+ verdict: "contradicted",
52866
+ detail: `is beyond the claim, but the table it creates (\`${table8.name}\`) already exists in the live database — the claim looks too old`
52867
+ };
52868
+ }
52869
+ }
52870
+ for (const added of entry2.addsColumns) {
52871
+ const columns2 = tables.get(added.table);
52872
+ if (columns2?.includes(added.column)) {
52873
+ return {
52874
+ tag: entry2.tag,
52875
+ verdict: "contradicted",
52876
+ detail: `is beyond the claim, but the column it adds (\`${added.table}.${added.column}\`) already exists — the claim looks too old`
52877
+ };
52878
+ }
52879
+ }
52880
+ for (const index2 of entry2.createsIndexes) {
52881
+ if (indexes2.has(index2)) {
52882
+ return {
52883
+ tag: entry2.tag,
52884
+ verdict: "contradicted",
52885
+ detail: `is beyond the claim, but the index it creates (\`${index2}\`) already exists — the claim looks too old`
52886
+ };
52887
+ }
52888
+ }
52889
+ for (const view2 of entry2.createsViews) {
52890
+ if (views.has(view2)) {
52891
+ return {
52892
+ tag: entry2.tag,
52893
+ verdict: "contradicted",
52894
+ detail: `is beyond the claim, but the view it creates (\`${view2}\`) already exists — the claim looks too old`
52895
+ };
52896
+ }
52897
+ }
52898
+ return { tag: entry2.tag, verdict: "verified" };
52899
+ }
52900
+ function suggestTag(evidence, tables) {
52901
+ let presentPrefix = 0;
52902
+ while (presentPrefix < evidence.length && evidence[presentPrefix].createsTables.every((table8) => tables.has(table8.name))) {
52903
+ presentPrefix++;
52904
+ }
52905
+ let absentFrom = evidence.length;
52906
+ while (absentFrom > 0 && evidence[absentFrom - 1].createsTables.every((table8) => !tables.has(table8.name))) {
52907
+ absentFrom--;
52908
+ }
52909
+ const candidate = presentPrefix - 1;
52910
+ return candidate >= 0 && candidate >= absentFrom - 1 ? evidence[candidate].tag : null;
52911
+ }
52912
+ function hasSignal(entry2) {
52913
+ return entry2.createsTables.length > 0 || entry2.addsColumns.length > 0 || entry2.createsIndexes.length > 0 || entry2.createsViews.length > 0;
52914
+ }
52915
+ var init_baseline_validation_util = __esm(() => {
52916
+ init_spans();
52917
+ init_errors();
52083
52918
  });
52084
- function getGameDeploymentId(gameSlug, sstStage) {
52085
- if (sstStage === "production") {
52086
- return gameSlug;
52919
+ function sliceJournalToTag(journal, lastAppliedMigrationTag) {
52920
+ const index2 = journal.findIndex((entry2) => entry2.tag === lastAppliedMigrationTag);
52921
+ return index2 === -1 ? null : journal.slice(0, index2 + 1);
52922
+ }
52923
+ function evaluateBaselineGuardrails(input) {
52924
+ if (input.ledgerTags.length > 0) {
52925
+ return "ledger-not-empty";
52087
52926
  }
52088
- if (sstStage === "dev" || isPreviewStage(sstStage)) {
52089
- return `${WORKER_NAMING.STAGING_PREFIX}${gameSlug}`;
52927
+ if (input.liveTables.length === 0) {
52928
+ return "database-empty";
52090
52929
  }
52091
- return `${WORKER_NAMING.LOCAL_PREFIX}${sstStage}-${gameSlug}`;
52930
+ return "ok";
52092
52931
  }
52093
- function getDashboardDeploymentId(gameSlug, sstStage) {
52094
- return `${getGameDeploymentId(gameSlug, sstStage)}${DASHBOARD_WORKER_SUFFIX}`;
52932
+ function planMigrations(journal, ledger) {
52933
+ const ledgerByTag = new Map(ledger.map((row) => [row.tag, row]));
52934
+ const journalTags = new Set(journal.map((entry2) => entry2.tag));
52935
+ const pendingTags = [];
52936
+ const checksumMismatches = [];
52937
+ for (const entry2 of journal) {
52938
+ const applied = ledgerByTag.get(entry2.tag);
52939
+ if (!applied) {
52940
+ pendingTags.push(entry2.tag);
52941
+ } else {
52942
+ const comparable = applied.checksumAlgo === MIGRATION_CHECKSUM_ALGO;
52943
+ if (comparable && applied.checksum !== entry2.checksum) {
52944
+ checksumMismatches.push({
52945
+ tag: entry2.tag,
52946
+ ledgerChecksum: applied.checksum,
52947
+ journalChecksum: entry2.checksum
52948
+ });
52949
+ }
52950
+ }
52951
+ }
52952
+ const outOfOrderTags = findOutOfOrderTags(journal.map((entry2) => entry2.tag), new Set(ledgerByTag.keys()));
52953
+ const missingFromJournalTags = ledger.filter((row) => !journalTags.has(row.tag)).map((row) => row.tag);
52954
+ return { pendingTags, checksumMismatches, outOfOrderTags, missingFromJournalTags };
52095
52955
  }
52096
- function getGameWorkerApiKeyName(slug) {
52097
- return `${GAME_WORKER_KEY_PREFIX}${slug}`.substring(0, 32);
52956
+ function assessMigrationAlreadyApplied(statements, liveTables) {
52957
+ const created = extractCreatedObjects(statements);
52958
+ const present = [];
52959
+ const missing = [];
52960
+ for (const table8 of created.tables) {
52961
+ (liveTables.has(table8) ? present : missing).push(`table ${table8}`);
52962
+ }
52963
+ for (const added of created.columns) {
52964
+ const live = Boolean(liveTables.get(added.table)?.includes(added.column));
52965
+ (live ? present : missing).push(`column ${added.table}.${added.column}`);
52966
+ }
52967
+ if (present.length === 0) {
52968
+ return { verdict: "no-signal", present, missing };
52969
+ }
52970
+ return {
52971
+ verdict: missing.length === 0 ? "all-present" : "partial",
52972
+ present,
52973
+ missing
52974
+ };
52098
52975
  }
52099
- function getDashboardWorkerApiKeyName(slug) {
52100
- return `${DASHBOARD_WORKER_KEY_PREFIX}${slug}`;
52976
+ function parseMigrationFailure(events) {
52977
+ if (!events) {
52978
+ return null;
52979
+ }
52980
+ for (let i2 = events.length - 1;i2 >= 0; i2--) {
52981
+ const event = events[i2];
52982
+ const failure = event ? parseFailureEvent(event) : null;
52983
+ if (failure) {
52984
+ return failure;
52985
+ }
52986
+ }
52987
+ return null;
52101
52988
  }
52102
- function toBindingName(queueKey) {
52103
- return `${queueKey.replace(/-/g, "_").toUpperCase()}_QUEUE`;
52989
+ function parseFailureEvent(event) {
52990
+ const details = event.details;
52991
+ if (!details) {
52992
+ return null;
52993
+ }
52994
+ const { code, tag, statementIndex, error, d1Message } = details;
52995
+ if (code !== DEPLOY_ERROR_CODES.migrationFailed || typeof tag !== "string") {
52996
+ return null;
52997
+ }
52998
+ return {
52999
+ tag,
53000
+ error: failureMessage(error, d1Message),
53001
+ statementIndex: typeof statementIndex === "number" ? statementIndex : null,
53002
+ at: event.createdAt
53003
+ };
52104
53004
  }
52105
- function generateDeploymentHash(code) {
52106
- return sha256Hex(code);
53005
+ function failureMessage(error, d1Message) {
53006
+ if (typeof error === "string") {
53007
+ return error;
53008
+ }
53009
+ if (typeof d1Message === "string") {
53010
+ return d1Message;
53011
+ }
53012
+ return "Migration failed";
52107
53013
  }
52108
- var GAME_WORKER_KEY_PREFIX = "game-worker-";
52109
- var DASHBOARD_WORKER_KEY_PREFIX = "dash-worker-";
52110
- var init_deployment_util = __esm(() => {
52111
- init_src();
52112
- init_stages();
53014
+ var init_migration_util = __esm(() => {
53015
+ init_src4();
53016
+ init_errors();
52113
53017
  });
53018
+ async function deriveSecretsManifestPepper(platformSecret) {
53019
+ const encoder = new TextEncoder;
53020
+ const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(platformSecret), "HKDF", false, ["deriveBits"]);
53021
+ const bits = await crypto.subtle.deriveBits({
53022
+ name: "HKDF",
53023
+ hash: "SHA-256",
53024
+ salt: new Uint8Array(32),
53025
+ info: encoder.encode(SECRETS_MANIFEST_HKDF_INFO)
53026
+ }, keyMaterial, 256);
53027
+ return new Uint8Array(bits);
53028
+ }
53029
+ async function computeSecretDigest(pepper, input) {
53030
+ const key = await crypto.subtle.importKey("raw", new Uint8Array(pepper), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
53031
+ const message = JSON.stringify([input.gameId, input.key, input.value]);
53032
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
53033
+ return [...new Uint8Array(signature)].map((b) => b.toString(16).padStart(2, "0")).join("");
53034
+ }
53035
+ function computeSecretsDiff(input) {
53036
+ const { localDigests, manifest, remoteKeys } = input;
53037
+ const added = [];
53038
+ const changed = [];
53039
+ const unchanged = [];
53040
+ for (const [key, digest] of Object.entries(localDigests)) {
53041
+ const recorded = manifest[key];
53042
+ if (recorded === undefined) {
53043
+ added.push(key);
53044
+ } else if (recorded === digest) {
53045
+ unchanged.push(key);
53046
+ } else {
53047
+ changed.push(key);
53048
+ }
53049
+ }
53050
+ const localKeys = new Set(Object.keys(localDigests));
53051
+ const managedKeys = new Set(Object.keys(manifest));
53052
+ const remoteOnlyManaged = [...managedKeys].filter((key) => !localKeys.has(key));
53053
+ const remoteOnlyUnmanaged = remoteKeys.filter((key) => !managedKeys.has(key) && !localKeys.has(key));
53054
+ return {
53055
+ added: added.toSorted(),
53056
+ changed: changed.toSorted(),
53057
+ unchanged: unchanged.toSorted(),
53058
+ remoteOnlyManaged: remoteOnlyManaged.toSorted(),
53059
+ remoteOnlyUnmanaged: remoteOnlyUnmanaged.toSorted()
53060
+ };
53061
+ }
53062
+ function findUnmanagedPruneKeys(pruneSecrets, manifest) {
53063
+ const managed = new Set(Object.keys(manifest ?? {}));
53064
+ return pruneSecrets.filter((key) => !managed.has(key));
53065
+ }
53066
+ var SECRETS_MANIFEST_HKDF_INFO = "playcademy:secrets-manifest:v1";
52114
53067
  async function sweepDashboardWorkerKeys(deleteApiKeysByName, slug) {
52115
53068
  try {
52116
53069
  await deleteApiKeysByName(getDashboardWorkerApiKeyName(slug));
@@ -52125,6 +53078,9 @@ var init_worker_keys_util = __esm(() => {
52125
53078
  init_spans();
52126
53079
  init_deployment_util();
52127
53080
  });
53081
+ function hasBinding(binding) {
53082
+ return binding === true || Array.isArray(binding) && binding.length > 0;
53083
+ }
52128
53084
  function readDashboardTheme(config2) {
52129
53085
  const theme = config2?.dashboard?.theme;
52130
53086
  return {
@@ -52192,7 +53148,7 @@ class DeployService {
52192
53148
  data: { message: hasBackend ? "Deploying backend code" : "Deploying to platform" }
52193
53149
  };
52194
53150
  const keepAssets = hasBackend && !hasFrontend;
52195
- const deploymentOptions = this.mapGameBindingsToOptions(deploymentId, request.bindings, request.schema);
53151
+ const deploymentOptions = this.mapGameBindingsToOptions(deploymentId, request.bindings);
52196
53152
  const bindings = deploymentOptions?.bindings;
52197
53153
  setAttributes({
52198
53154
  "app.deploy.has_d1": Boolean(bindings?.d1?.length),
@@ -52201,13 +53157,49 @@ class DeployService {
52201
53157
  "app.deploy.queue_count": bindings?.queues?.length ?? 0,
52202
53158
  "app.deploy.dead_letter_queue_count": DeployService.countDeadLetterQueues(bindings)
52203
53159
  });
52204
- const activeDeployment = await db2.query.gameDeployments.findFirst({
52205
- where: activeDeploymentWhere(game2.id, "game"),
52206
- columns: { resources: true }
52207
- });
52208
- if (deploymentOptions?.bindings?.d1?.length) {
53160
+ const [activeDeployment, deploymentState] = await Promise.all([
53161
+ db2.query.gameDeployments.findFirst({
53162
+ where: activeDeploymentWhere(game2.id, "game"),
53163
+ columns: { resources: true }
53164
+ }),
53165
+ db2.query.gameDeploymentState.findFirst({
53166
+ where: eq(gameDeploymentState.gameId, game2.id)
53167
+ })
53168
+ ]);
53169
+ if (request.pruneSecrets?.length) {
53170
+ const unmanaged = findUnmanagedPruneKeys(request.pruneSecrets, deploymentState?.secretsManifest);
53171
+ if (unmanaged.length > 0) {
53172
+ throw new SecretsPruneUnmanagedError(unmanaged);
53173
+ }
53174
+ }
53175
+ let state = deploymentState;
53176
+ if (request.baseline) {
53177
+ if (isSchemaAdopted(state)) {
53178
+ addEvent("deploy.baseline_skipped", {
53179
+ "app.deploy.baseline_source": state?.baselineSource ?? "unknown"
53180
+ });
53181
+ } else {
53182
+ state = yield* this.adoptClientBaseline({
53183
+ game: game2,
53184
+ request,
53185
+ user,
53186
+ deploymentId,
53187
+ baseline: request.baseline,
53188
+ existingResources: activeDeployment?.resources ?? undefined
53189
+ });
53190
+ }
53191
+ }
53192
+ if (deploymentOptions?.bindings?.d1?.length && !isSchemaAdopted(state)) {
52209
53193
  await this.cleanupOrphanD1Databases(cf, deploymentOptions.bindings.d1, slug, game2.id);
52210
53194
  }
53195
+ const databaseOutcome = yield* this.executeDatabaseStep({
53196
+ game: game2,
53197
+ request,
53198
+ user,
53199
+ deploymentId,
53200
+ state,
53201
+ existingResources: activeDeployment?.resources ?? undefined
53202
+ });
52211
53203
  const result = await this.deployToCloudflare({
52212
53204
  deploymentId,
52213
53205
  code: request.code,
@@ -52215,6 +53207,7 @@ class DeployService {
52215
53207
  tempDir,
52216
53208
  options: {
52217
53209
  ...deploymentOptions,
53210
+ ...databaseOutcome.legacySchema && { schema: databaseOutcome.legacySchema },
52218
53211
  compatibilityDate: request.compatibilityDate ?? CLOUDFLARE_COMPATIBILITY_DATE,
52219
53212
  compatibilityFlags: request.compatibilityFlags,
52220
53213
  existingResources: activeDeployment?.resources ?? undefined,
@@ -52230,8 +53223,12 @@ class DeployService {
52230
53223
  result,
52231
53224
  request,
52232
53225
  user,
52233
- flags: flags2
53226
+ flags: flags2,
53227
+ database: databaseOutcome
52234
53228
  });
53229
+ if (request.pruneSecrets?.length) {
53230
+ yield* this.pruneManagedSecretsStep(game2.id, result.deploymentId, request.pruneSecrets);
53231
+ }
52235
53232
  yield { type: "complete", data: updatedGame };
52236
53233
  }
52237
53234
  async* deployDashboard(context2) {
@@ -52358,7 +53355,10 @@ class DeployService {
52358
53355
  "app.deploy.code_size": request.code?.length ?? 0,
52359
53356
  "app.deploy.compatibility_date": request.compatibilityDate ?? CLOUDFLARE_COMPATIBILITY_DATE,
52360
53357
  "app.deploy.compatibility_flag_count": request.compatibilityFlags?.length ?? 0,
52361
- "app.deploy.has_schema": Boolean(request.schema)
53358
+ "app.deploy.has_schema": Boolean(request.schema),
53359
+ "app.deploy.has_database_payload": Boolean(request.database),
53360
+ "app.deploy.has_baseline": Boolean(request.baseline),
53361
+ "app.deploy.prune_secret_count": request.pruneSecrets?.length ?? 0
52362
53362
  });
52363
53363
  if (!hasBackend && !hasFrontend && !hasMetadata) {
52364
53364
  throw new ValidationError("Must provide at least one of: uploadToken (frontend), code (backend), or metadata");
@@ -52395,18 +53395,18 @@ class DeployService {
52395
53395
  }
52396
53396
  return "metadata_only";
52397
53397
  }
52398
- mapGameBindingsToOptions(deploymentId, bindings, schema2) {
52399
- if (!bindings && !schema2) {
53398
+ mapGameBindingsToOptions(deploymentId, bindings) {
53399
+ if (!bindings) {
52400
53400
  return;
52401
53401
  }
52402
53402
  const workerBindings = {};
52403
- if (bindings?.database) {
53403
+ if (hasBinding(bindings.database)) {
52404
53404
  workerBindings.d1 = [deploymentId];
52405
53405
  }
52406
- if (bindings?.keyValue) {
53406
+ if (hasBinding(bindings.keyValue)) {
52407
53407
  workerBindings.kv = [deploymentId];
52408
53408
  }
52409
- if (bindings?.bucket) {
53409
+ if (hasBinding(bindings.bucket)) {
52410
53410
  workerBindings.r2 = [deploymentId];
52411
53411
  }
52412
53412
  if (bindings?.queues) {
@@ -52435,10 +53435,7 @@ class DeployService {
52435
53435
  });
52436
53436
  }
52437
53437
  const hasBindings = workerBindings.d1?.length || workerBindings.kv?.length || workerBindings.r2?.length || workerBindings.queues?.length;
52438
- return {
52439
- ...hasBindings && { bindings: workerBindings },
52440
- ...schema2 && { schema: schema2 }
52441
- };
53438
+ return hasBindings ? { bindings: workerBindings } : undefined;
52442
53439
  }
52443
53440
  static countDeadLetterQueues(bindings) {
52444
53441
  return bindings?.queues?.filter((queue) => Boolean(queue.deadLetterQueue)).length ?? 0;
@@ -52462,6 +53459,473 @@ class DeployService {
52462
53459
  }
52463
53460
  }
52464
53461
  }
53462
+ async* executeDatabaseStep(context2) {
53463
+ const { game: game2, request, user, deploymentId, state } = context2;
53464
+ if (request.schema) {
53465
+ if (isSchemaAdopted(state)) {
53466
+ throw new LegacySchemaUpgradeRequiredError;
53467
+ }
53468
+ const legacyDbId = context2.existingResources?.d1?.find((db2) => db2.name === deploymentId)?.id;
53469
+ if (legacyDbId) {
53470
+ const ledger = await this.getCloudflare().d1.readMigrationLedger(legacyDbId);
53471
+ if (ledger.length > 0) {
53472
+ throw new LegacySchemaUpgradeRequiredError;
53473
+ }
53474
+ }
53475
+ setAttribute("app.deploy.db_mode", "legacy");
53476
+ return { ...NO_DATABASE_WORK, legacySchema: request.schema };
53477
+ }
53478
+ const database = request.database;
53479
+ if (!database) {
53480
+ return NO_DATABASE_WORK;
53481
+ }
53482
+ if (!hasBinding(request.bindings?.database)) {
53483
+ throw new ValidationError("The database payload requires a database binding");
53484
+ }
53485
+ if (!request.deployId) {
53486
+ throw new ValidationError("deployId is required when a database payload is present");
53487
+ }
53488
+ setAttribute("app.deploy.db_mode", database.mode);
53489
+ const cf = this.getCloudflare();
53490
+ const persistedId = context2.existingResources?.d1?.find((db2) => db2.name === deploymentId)?.id;
53491
+ const databaseId = persistedId ?? await cf.d1.create(deploymentId);
53492
+ if (database.mode === "migrate") {
53493
+ return yield* this.runMigrateMode({
53494
+ game: game2,
53495
+ user,
53496
+ deployId: request.deployId,
53497
+ databaseId,
53498
+ migrations: database.migrations,
53499
+ state
53500
+ });
53501
+ }
53502
+ return yield* this.runPushMode({ game: game2, databaseId, payload: database, state });
53503
+ }
53504
+ async* runMigrateMode(args2) {
53505
+ const { game: game2, user, deployId, databaseId, migrations, state } = args2;
53506
+ const cf = this.getCloudflare();
53507
+ yield { type: "status", data: { message: "Preparing database migrations" } };
53508
+ await this.assertNoSchemaDrift(databaseId, state);
53509
+ await cf.d1.ensureMigrationLedger(databaseId);
53510
+ const ledger = await cf.d1.readMigrationLedger(databaseId);
53511
+ const plan = planMigrations(migrations.map((migration) => ({ tag: migration.tag, checksum: migration.checksum })), ledger.map((row) => ({
53512
+ tag: row.tag,
53513
+ checksum: row.checksum,
53514
+ checksumAlgo: row.checksum_algo
53515
+ })));
53516
+ setAttributes({
53517
+ "app.deploy.migrations_total": migrations.length,
53518
+ "app.deploy.migrations_applied_before": ledger.length,
53519
+ "app.deploy.migrations_pending": plan.pendingTags.length
53520
+ });
53521
+ if (plan.checksumMismatches.length > 0) {
53522
+ throw new MigrationChecksumMismatchError(plan.checksumMismatches);
53523
+ }
53524
+ if (plan.missingFromJournalTags.length > 0) {
53525
+ throw new MigrationJournalDivergenceError(plan.missingFromJournalTags);
53526
+ }
53527
+ if (plan.outOfOrderTags.length > 0) {
53528
+ throw new MigrationOrderError(plan.outOfOrderTags);
53529
+ }
53530
+ if (plan.pendingTags.length === 0) {
53531
+ yield { type: "status", data: { message: "Database schema is up to date" } };
53532
+ if (ledger.length > 0 && !state?.schemaFingerprint) {
53533
+ const fingerprint2 = await cf.d1.fingerprintSchema(databaseId);
53534
+ await this.persistDeploymentState(game2.id, {
53535
+ schemaFingerprint: fingerprint2.fingerprint
53536
+ });
53537
+ return {
53538
+ ...NO_DATABASE_WORK,
53539
+ schemaFingerprint: fingerprint2.fingerprint
53540
+ };
53541
+ }
53542
+ return {
53543
+ ...NO_DATABASE_WORK,
53544
+ schemaFingerprint: state?.schemaFingerprint ?? null
53545
+ };
53546
+ }
53547
+ const capture = yield* this.captureBookmarkStep(databaseId);
53548
+ const migrationsByTag = new Map(migrations.map((migration) => [migration.tag, migration]));
53549
+ let appliedCount = 0;
53550
+ for (const tag of plan.pendingTags) {
53551
+ const migration = migrationsByTag.get(tag);
53552
+ const count = migration.statements.length;
53553
+ yield {
53554
+ type: "status",
53555
+ data: { message: `Applying ${tag} (${count} statement${count === 1 ? "" : "s"})` }
53556
+ };
53557
+ const startedAt = Date.now();
53558
+ try {
53559
+ await withSpan("deploy.apply_migration", () => cf.d1.applyMigration(databaseId, {
53560
+ tag,
53561
+ statements: migration.statements,
53562
+ checksum: migration.checksum,
53563
+ deployId,
53564
+ appliedBy: user.id
53565
+ }));
53566
+ } catch (error) {
53567
+ if (appliedCount > 0) {
53568
+ await this.recordAppliedPrefixFingerprint(game2.id, databaseId);
53569
+ }
53570
+ throw await this.toMigrationStepError(databaseId, error, migration);
53571
+ }
53572
+ appliedCount++;
53573
+ const seconds = ((Date.now() - startedAt) / 1000).toFixed(1);
53574
+ yield { type: "status", data: { message: `Applied ${tag} (${seconds}s)` } };
53575
+ }
53576
+ setAttribute("app.deploy.migrations_applied", plan.pendingTags.length);
53577
+ const fingerprint = await cf.d1.fingerprintSchema(databaseId);
53578
+ await this.persistDeploymentState(game2.id, {
53579
+ schemaFingerprint: fingerprint.fingerprint,
53580
+ schemaHash: null,
53581
+ schemaSnapshot: null
53582
+ });
53583
+ return {
53584
+ schemaHash: null,
53585
+ schemaFingerprint: fingerprint.fingerprint,
53586
+ timeTravelBookmark: capture?.bookmark ?? null,
53587
+ bookmarkCapturedAt: capture?.capturedAt ?? null
53588
+ };
53589
+ }
53590
+ async* runPushMode(args2) {
53591
+ const { game: game2, databaseId, payload, state } = args2;
53592
+ const cf = this.getCloudflare();
53593
+ yield { type: "status", data: { message: "Verifying database schema state" } };
53594
+ if (typeof payload.baselineHash !== "string" && payload.baselineHash !== null) {
53595
+ throw new ValidationError("Push deploys require baselineHash (null on first deploy)");
53596
+ }
53597
+ if (isMigrateManaged(state)) {
53598
+ const ledger = await cf.d1.readMigrationLedger(databaseId);
53599
+ throw new ValidationError(`This database is migrate-managed (${ledger.length} applied ` + "migration(s)) and does not accept push-mode SQL.", { code: DEPLOY_ERROR_CODES.strategyMismatch });
53600
+ }
53601
+ const storedHash = state?.schemaHash ?? null;
53602
+ if (payload.baselineHash !== storedHash) {
53603
+ throw await this.buildStateConflictError(game2.id, payload.baselineHash);
53604
+ }
53605
+ const statements = splitSqlStatements(payload.sql);
53606
+ const oversized = findOversizedStatement(statements);
53607
+ if (oversized) {
53608
+ throw new ValidationError(`Push statement ${oversized.index} is ${oversized.byteLength} bytes — over ` + `D1's ${D1_MAX_STATEMENT_BYTES / 1024} KB per-statement limit. Split the ` + "statement or reduce its size.", {
53609
+ code: DEPLOY_ERROR_CODES.pushFailed,
53610
+ statementIndex: oversized.index,
53611
+ offset: null
53612
+ });
53613
+ }
53614
+ const destructive = detectDestructiveStatements(statements);
53615
+ setAttributes({
53616
+ "app.deploy.push_statement_count": statements.length,
53617
+ "app.deploy.push_destructive_count": destructive.length,
53618
+ "app.deploy.push_accept_data_loss": Boolean(payload.acceptDataLoss)
53619
+ });
53620
+ if (destructive.length > 0 && !payload.acceptDataLoss) {
53621
+ throw new DestructiveSchemaError(destructive);
53622
+ }
53623
+ await this.assertNoSchemaDrift(databaseId, state);
53624
+ const reserved = await this.persistPushDeploymentState(game2.id, payload.baselineHash, {
53625
+ schemaFingerprint: PUSH_RESERVATION_FINGERPRINT,
53626
+ schemaHash: payload.nextHash,
53627
+ schemaSnapshot: payload.nextSnapshot
53628
+ });
53629
+ if (!reserved) {
53630
+ throw await this.buildStateConflictError(game2.id, payload.baselineHash);
53631
+ }
53632
+ let capture = null;
53633
+ if (statements.length > 0) {
53634
+ capture = yield* this.captureBookmarkStep(databaseId);
53635
+ yield {
53636
+ type: "status",
53637
+ data: {
53638
+ message: `Applying database schema changes (${statements.length} statements)`
53639
+ }
53640
+ };
53641
+ try {
53642
+ await cf.d1.batch(databaseId, [
53643
+ { sql: "PRAGMA defer_foreign_keys = on" },
53644
+ ...statements.map((statement) => ({ sql: statement })),
53645
+ { sql: "PRAGMA defer_foreign_keys = off" }
53646
+ ]);
53647
+ } catch (error) {
53648
+ await this.persistPushDeploymentState(game2.id, payload.nextHash, {
53649
+ schemaFingerprint: state?.schemaFingerprint ?? null,
53650
+ schemaHash: state?.schemaHash ?? null,
53651
+ schemaSnapshot: state?.schemaSnapshot ?? null
53652
+ });
53653
+ throw this.toPushStepError(error);
53654
+ }
53655
+ }
53656
+ const fingerprint = await cf.d1.fingerprintSchema(databaseId);
53657
+ await this.persistDeploymentState(game2.id, {
53658
+ schemaFingerprint: fingerprint.fingerprint
53659
+ });
53660
+ return {
53661
+ schemaHash: payload.nextHash,
53662
+ schemaFingerprint: fingerprint.fingerprint,
53663
+ timeTravelBookmark: capture?.bookmark ?? null,
53664
+ bookmarkCapturedAt: capture?.capturedAt ?? null
53665
+ };
53666
+ }
53667
+ async* captureBookmarkStep(databaseId) {
53668
+ const cf = this.getCloudflare();
53669
+ const bookmark = await cf.d1.captureBookmark(databaseId);
53670
+ const capturedAt = new Date;
53671
+ setAttribute("app.deploy.db_bookmark_captured", bookmark.captured);
53672
+ if (!bookmark.captured) {
53673
+ yield {
53674
+ type: "status",
53675
+ data: { message: `Time Travel bookmark unavailable (${bookmark.error})` }
53676
+ };
53677
+ return null;
53678
+ }
53679
+ yield {
53680
+ type: "status",
53681
+ data: {
53682
+ message: "Captured Time Travel bookmark",
53683
+ details: { bookmark: bookmark.bookmark }
53684
+ }
53685
+ };
53686
+ return { bookmark: bookmark.bookmark, capturedAt };
53687
+ }
53688
+ async toMigrationStepError(databaseId, error, migration) {
53689
+ if (error instanceof D1StatementTooLargeError) {
53690
+ return new ValidationError(error.message, {
53691
+ code: DEPLOY_ERROR_CODES.migrationFailed,
53692
+ tag: migration.tag,
53693
+ statementIndex: error.statementIndex,
53694
+ offset: null,
53695
+ d1Message: error.message
53696
+ });
53697
+ }
53698
+ if (error instanceof D1MigrationError) {
53699
+ addEvent("deploy.migration_failed", {
53700
+ "app.d1.migration_tag": migration.tag,
53701
+ "app.error.message": error.d1Message,
53702
+ ...error.offset !== null && { "app.d1.error_offset": error.offset }
53703
+ });
53704
+ const alreadyApplied = isAlreadyExistsSqlError(error.d1Message) ? await this.assessAlreadyApplied(databaseId, migration.statements) : null;
53705
+ return new MigrationExecutionError({
53706
+ tag: migration.tag,
53707
+ d1Message: error.d1Message,
53708
+ offset: error.offset,
53709
+ ...alreadyApplied ? { alreadyApplied } : {}
53710
+ });
53711
+ }
53712
+ return error;
53713
+ }
53714
+ async assessAlreadyApplied(databaseId, statements) {
53715
+ try {
53716
+ const cf = this.getCloudflare();
53717
+ const live = await cf.d1.fingerprintSchema(databaseId);
53718
+ const tables = await cf.d1.readTableColumns(databaseId, live.tables);
53719
+ const assessment = assessMigrationAlreadyApplied(statements, tables);
53720
+ addEvent("deploy.already_applied_assessment", {
53721
+ "app.deploy.already_applied_verdict": assessment.verdict,
53722
+ "app.deploy.already_applied_present": assessment.present.length,
53723
+ "app.deploy.already_applied_missing": assessment.missing.length
53724
+ });
53725
+ return assessment.verdict === "no-signal" ? null : assessment;
53726
+ } catch (assessError) {
53727
+ addEvent("deploy.already_applied_check_failed", {
53728
+ "app.error.message": errorMessage2(assessError)
53729
+ });
53730
+ return null;
53731
+ }
53732
+ }
53733
+ toPushStepError(error) {
53734
+ if (error instanceof D1BatchError) {
53735
+ addEvent("deploy.push_failed", {
53736
+ "app.error.message": error.d1Message,
53737
+ ...error.offset !== null && { "app.d1.error_offset": error.offset }
53738
+ });
53739
+ return new PushExecutionError({ d1Message: error.d1Message, offset: error.offset });
53740
+ }
53741
+ return error;
53742
+ }
53743
+ async assertNoSchemaDrift(databaseId, state) {
53744
+ if (!state?.schemaFingerprint) {
53745
+ return;
53746
+ }
53747
+ const live = await this.getCloudflare().d1.fingerprintSchema(databaseId);
53748
+ if (live.fingerprint !== state.schemaFingerprint) {
53749
+ addEvent("deploy.state_drift", {
53750
+ "app.deploy.expected_fingerprint": state.schemaFingerprint,
53751
+ "app.deploy.actual_fingerprint": live.fingerprint
53752
+ });
53753
+ throw new DeploymentStateDriftError({
53754
+ expectedFingerprint: state.schemaFingerprint,
53755
+ actualFingerprint: live.fingerprint
53756
+ });
53757
+ }
53758
+ }
53759
+ async buildStateConflictError(gameId, claimedHash) {
53760
+ const [row, lastDeploy] = await Promise.all([
53761
+ this.deps.db.query.gameDeploymentState.findFirst({
53762
+ where: eq(gameDeploymentState.gameId, gameId),
53763
+ columns: { schemaHash: true }
53764
+ }),
53765
+ findLastSuccessfulDeployWithEmail(this.deps.db, gameId)
53766
+ ]);
53767
+ const currentHash = row?.schemaHash ?? null;
53768
+ addEvent("deploy.state_conflict", {
53769
+ "app.deploy.baseline_hash": claimedHash ?? "null",
53770
+ "app.deploy.current_hash": currentHash ?? "null"
53771
+ });
53772
+ return new DeploymentStateConflictError({
53773
+ currentHash,
53774
+ lastDeployAt: lastDeploy?.at.toISOString() ?? null,
53775
+ lastDeployBy: lastDeploy?.email ?? lastDeploy?.userId ?? null
53776
+ });
53777
+ }
53778
+ async recordAppliedPrefixFingerprint(gameId, databaseId) {
53779
+ try {
53780
+ const fingerprint = await this.getCloudflare().d1.fingerprintSchema(databaseId);
53781
+ await this.persistDeploymentState(gameId, {
53782
+ schemaFingerprint: fingerprint.fingerprint
53783
+ });
53784
+ } catch (error) {
53785
+ addEvent("deploy.fingerprint_persist_failed", {
53786
+ "exception.type": errorType(error),
53787
+ "app.error.message": errorMessage2(error)
53788
+ });
53789
+ }
53790
+ }
53791
+ async persistDeploymentState(gameId, patch) {
53792
+ const set = {
53793
+ ...patch,
53794
+ updatedAt: new Date
53795
+ };
53796
+ await this.deps.db.insert(gameDeploymentState).values({ gameId, baselineSource: "deploy", ...set }).onConflictDoUpdate({ target: gameDeploymentState.gameId, set });
53797
+ }
53798
+ async persistArtifactHashes(gameId, patch) {
53799
+ const set = {
53800
+ ...patch.buildHash !== undefined && { buildHash: patch.buildHash },
53801
+ ...patch.integrationsHash !== undefined && {
53802
+ integrationsHash: patch.integrationsHash
53803
+ }
53804
+ };
53805
+ if (Object.keys(set).length === 0) {
53806
+ return;
53807
+ }
53808
+ await this.deps.db.insert(gameDeploymentState).values({ gameId, ...set, updatedAt: new Date }).onConflictDoUpdate({
53809
+ target: gameDeploymentState.gameId,
53810
+ set: { ...set, updatedAt: new Date }
53811
+ });
53812
+ }
53813
+ async persistPushDeploymentState(gameId, baselineHash, patch) {
53814
+ const set = { ...patch, updatedAt: new Date };
53815
+ if (baselineHash === null) {
53816
+ const claimed2 = await this.deps.db.insert(gameDeploymentState).values({ gameId, baselineSource: "deploy", ...set }).onConflictDoUpdate({
53817
+ target: gameDeploymentState.gameId,
53818
+ set,
53819
+ setWhere: isNull(gameDeploymentState.schemaHash)
53820
+ }).returning({ gameId: gameDeploymentState.gameId });
53821
+ return claimed2.length > 0;
53822
+ }
53823
+ const claimed = await this.deps.db.update(gameDeploymentState).set(set).where(and(eq(gameDeploymentState.gameId, gameId), eq(gameDeploymentState.schemaHash, baselineHash))).returning({ gameId: gameDeploymentState.gameId });
53824
+ return claimed.length > 0;
53825
+ }
53826
+ async* adoptClientBaseline(context2) {
53827
+ const { game: game2, request, user, deploymentId, baseline } = context2;
53828
+ const cf = this.getCloudflare();
53829
+ const persistedId = context2.existingResources?.d1?.find((db2) => db2.name === deploymentId)?.id;
53830
+ const databaseId = persistedId ?? (hasBinding(request.bindings?.database) ? await cf.d1.create(deploymentId) : null);
53831
+ if (baseline.lastAppliedMigrationTag && !databaseId) {
53832
+ throw new ValidationError("Baseline claims applied migrations, but the deploy has no database binding");
53833
+ }
53834
+ const live = databaseId ? await cf.d1.fingerprintSchema(databaseId) : null;
53835
+ let recordedRows = 0;
53836
+ if (databaseId && baseline.lastAppliedMigrationTag) {
53837
+ if (live.tables.length === 0) {
53838
+ throw new BaselineDatabaseEmptyError;
53839
+ }
53840
+ const slice = sliceJournalToTag(baseline.journal ?? [], baseline.lastAppliedMigrationTag);
53841
+ if (!slice) {
53842
+ throw new ValidationError(`Baseline lastAppliedMigrationTag '${baseline.lastAppliedMigrationTag}' ` + "is not in the submitted journal");
53843
+ }
53844
+ if (baseline.evidence?.length) {
53845
+ const [tables, lastDeploy] = await Promise.all([
53846
+ cf.d1.readTableColumns(databaseId, live.tables),
53847
+ findLastSuccessfulDeploy(this.deps.db, game2.id)
53848
+ ]);
53849
+ assertBaselineClaimValid({
53850
+ claimedTag: baseline.lastAppliedMigrationTag,
53851
+ evidence: baseline.evidence,
53852
+ tables,
53853
+ indexes: new Set(live.indexes),
53854
+ views: new Set(live.views),
53855
+ lastDeployAt: lastDeploy?.at ?? null,
53856
+ allowUnverified: false,
53857
+ source: "seed",
53858
+ gameId: game2.id,
53859
+ userId: user.id
53860
+ });
53861
+ }
53862
+ await cf.d1.ensureMigrationLedger(databaseId);
53863
+ const ledger = await cf.d1.readMigrationLedger(databaseId);
53864
+ const plan = planMigrations(slice, ledger.map((row) => ({
53865
+ tag: row.tag,
53866
+ checksum: row.checksum,
53867
+ checksumAlgo: row.checksum_algo
53868
+ })));
53869
+ if (plan.checksumMismatches.length > 0) {
53870
+ throw new MigrationChecksumMismatchError(plan.checksumMismatches);
53871
+ }
53872
+ if (plan.missingFromJournalTags.length > 0) {
53873
+ throw new MigrationJournalDivergenceError(plan.missingFromJournalTags);
53874
+ }
53875
+ if (plan.outOfOrderTags.length > 0) {
53876
+ throw new MigrationOrderError(plan.outOfOrderTags);
53877
+ }
53878
+ const checksumByTag = new Map(slice.map((entry2) => [entry2.tag, entry2.checksum]));
53879
+ const rows = plan.pendingTags.map((tag) => ({
53880
+ tag,
53881
+ checksum: checksumByTag.get(tag)
53882
+ }));
53883
+ if (rows.length > 0) {
53884
+ await cf.d1.recordLedgerRows(databaseId, {
53885
+ rows,
53886
+ deployId: `baseline:${request.deployId ?? crypto.randomUUID()}`,
53887
+ appliedBy: user.id,
53888
+ source: "baseline"
53889
+ });
53890
+ }
53891
+ recordedRows = rows.length;
53892
+ }
53893
+ const fingerprint = live?.fingerprint ?? null;
53894
+ const set = {
53895
+ ...baseline.schemaHash !== undefined && { schemaHash: baseline.schemaHash },
53896
+ ...baseline.schemaSnapshot !== undefined && {
53897
+ schemaSnapshot: baseline.schemaSnapshot
53898
+ },
53899
+ ...baseline.integrationsHash !== undefined && {
53900
+ integrationsHash: baseline.integrationsHash
53901
+ },
53902
+ ...baseline.buildHash !== undefined && { buildHash: baseline.buildHash },
53903
+ ...fingerprint !== null && { schemaFingerprint: fingerprint },
53904
+ baselineSource: "client-baseline",
53905
+ updatedAt: new Date
53906
+ };
53907
+ await this.deps.db.insert(gameDeploymentState).values({ gameId: game2.id, ...set }).onConflictDoUpdate({ target: gameDeploymentState.gameId, set });
53908
+ setAttributes({
53909
+ "app.deploy.baseline_adopted": true,
53910
+ "app.deploy.baseline_ledger_rows": recordedRows,
53911
+ "app.deploy.baseline_has_snapshot": baseline.schemaSnapshot !== undefined
53912
+ });
53913
+ yield {
53914
+ type: "status",
53915
+ data: {
53916
+ message: "Adopted deployment state from client baseline",
53917
+ details: {
53918
+ ledgerRows: recordedRows,
53919
+ ...baseline.lastAppliedMigrationTag && {
53920
+ lastAppliedMigrationTag: baseline.lastAppliedMigrationTag
53921
+ }
53922
+ }
53923
+ }
53924
+ };
53925
+ return this.deps.db.query.gameDeploymentState.findFirst({
53926
+ where: eq(gameDeploymentState.gameId, game2.id)
53927
+ });
53928
+ }
52465
53929
  async applyGameMetadata(gameId, request, hasFrontend, hasMetadata, deploymentUrl) {
52466
53930
  const updates = { updatedAt: new Date };
52467
53931
  if (hasFrontend) {
@@ -52487,7 +53951,8 @@ class DeployService {
52487
53951
  result,
52488
53952
  request,
52489
53953
  user,
52490
- flags: flags2
53954
+ flags: flags2,
53955
+ database
52491
53956
  }) {
52492
53957
  const { hasBackend, hasFrontend, hasMetadata } = flags2;
52493
53958
  const db2 = this.deps.db;
@@ -52497,9 +53962,17 @@ class DeployService {
52497
53962
  deploymentId: result.deploymentId,
52498
53963
  url: result.url,
52499
53964
  codeHash,
53965
+ schemaHash: database.schemaHash,
53966
+ schemaFingerprint: database.schemaFingerprint,
53967
+ timeTravelBookmark: database.timeTravelBookmark,
53968
+ bookmarkCapturedAt: database.bookmarkCapturedAt,
52500
53969
  resources: result.resources,
52501
53970
  target: "game"
52502
53971
  });
53972
+ await this.persistArtifactHashes(game2.id, {
53973
+ buildHash: hasFrontend ? request.buildHash : undefined,
53974
+ integrationsHash: request.integrationsHash
53975
+ });
52503
53976
  if (hasBackend) {
52504
53977
  await withSpan("deploy.configure_worker_secrets", async () => {
52505
53978
  await this.ensureWorkerApiKeyOnWorker(user, DeployService.gameWorkerKeySpec(slug), result.deploymentId);
@@ -52636,6 +54109,29 @@ class DeployService {
52636
54109
  const cf = this.getCloudflare();
52637
54110
  await cf.setSecrets(deploymentId, { QUEUE_INGRESS_SECRET: secret });
52638
54111
  }
54112
+ async* pruneManagedSecretsStep(gameId, deploymentId, pruneSecrets) {
54113
+ const cf = this.getCloudflare();
54114
+ const keys = [...new Set(pruneSecrets)];
54115
+ yield {
54116
+ type: "status",
54117
+ data: {
54118
+ message: `Pruning ${keys.length} managed secret(s)`,
54119
+ details: { keys }
54120
+ }
54121
+ };
54122
+ await withSpan("deploy.prune_secrets", async () => {
54123
+ const existing = await cf.listSecrets(deploymentId);
54124
+ for (const key of keys) {
54125
+ const prefixedKey = `${SECRETS_PREFIX}${key}`;
54126
+ if (existing.includes(prefixedKey)) {
54127
+ await cf.deleteSecret(deploymentId, prefixedKey);
54128
+ }
54129
+ }
54130
+ const pruned = keys.reduce((expr, key) => sql`${expr} - ${key}::text`, sql`coalesce(${gameDeploymentState.secretsManifest}, '{}'::jsonb)`);
54131
+ await this.deps.db.update(gameDeploymentState).set({ secretsManifest: pruned, updatedAt: new Date }).where(eq(gameDeploymentState.gameId, gameId));
54132
+ });
54133
+ setAttribute("app.deploy.pruned_secret_count", keys.length);
54134
+ }
52639
54135
  async ensureDashboardSessionSecret(deploymentId, existingSecrets, hasPriorDeployment) {
52640
54136
  if (existingSecrets === null && hasPriorDeployment) {
52641
54137
  setAttribute("app.deploy.session_secret_outcome", "check_failed_kept");
@@ -52740,6 +54236,10 @@ class DeployService {
52740
54236
  target: record.target,
52741
54237
  url: record.url,
52742
54238
  codeHash: record.codeHash,
54239
+ schemaHash: record.schemaHash ?? null,
54240
+ schemaFingerprint: record.schemaFingerprint ?? null,
54241
+ timeTravelBookmark: record.timeTravelBookmark ?? null,
54242
+ bookmarkCapturedAt: record.bookmarkCapturedAt ?? null,
52743
54243
  resources: record.resources,
52744
54244
  isActive: true
52745
54245
  });
@@ -52749,8 +54249,11 @@ class DeployService {
52749
54249
  await this.deps.alerts.notifyDeploymentFailure(failure).catch(catchAttrs("alerts.delivery", { "app.alerts.type": "deployment_failure" }));
52750
54250
  }
52751
54251
  }
54252
+ var PUSH_RESERVATION_FINGERPRINT = "reserved:push-in-flight";
54253
+ var NO_DATABASE_WORK;
52752
54254
  var init_deploy_service = __esm(() => {
52753
54255
  init_drizzle_orm();
54256
+ init_src4();
52754
54257
  init_playcademy();
52755
54258
  init_src();
52756
54259
  init_helpers_index();
@@ -52758,9 +54261,17 @@ var init_deploy_service = __esm(() => {
52758
54261
  init_spans();
52759
54262
  init_tunnel();
52760
54263
  init_errors();
54264
+ init_baseline_validation_util();
52761
54265
  init_dashboard_util();
52762
54266
  init_deployment_util();
54267
+ init_migration_util();
52763
54268
  init_worker_keys_util();
54269
+ NO_DATABASE_WORK = {
54270
+ schemaHash: null,
54271
+ schemaFingerprint: null,
54272
+ timeTravelBookmark: null,
54273
+ bookmarkCapturedAt: null
54274
+ };
52764
54275
  });
52765
54276
 
52766
54277
  class DeveloperService {
@@ -54205,7 +55716,7 @@ function createGameServices(deps) {
54205
55716
  }
54206
55717
  };
54207
55718
  }
54208
- var init_game2 = __esm(() => {
55719
+ var init_game3 = __esm(() => {
54209
55720
  init_dashboard_service();
54210
55721
  init_deploy_job_service();
54211
55722
  init_deploy_service();
@@ -54395,16 +55906,37 @@ class AlertsService {
54395
55906
  await this.sendAlert(discord, embed.build());
54396
55907
  }
54397
55908
  async notifyDeploymentFailure(failure) {
54398
- const discord = this.recordAlert("deployment_failure");
55909
+ const refused = DEPLOY_REFUSAL_CODES.has(failure.errorCode ?? "");
55910
+ const discord = this.recordAlert(refused ? "deployment_blocked" : "deployment_failure");
54399
55911
  if (!discord) {
54400
55912
  return;
54401
55913
  }
54402
- const [title, subject] = failure.target === "dashboard" ? ["Dashboard Deployment Failed", "Dashboard deployment"] : ["Deployment Failed", "Deployment"];
54403
- const embed = new DiscordEmbedBuilder().setTitle(title).setDescription(`${subject} failed for **${failure.displayName || failure.slug}** (**${this.getEnvironment()}**).`).setColor(DiscordColors.RED).addField("Slug", failure.slug, true);
55914
+ const [noun, subject] = failure.target === "dashboard" ? ["Dashboard Deployment", "Dashboard deployment"] : ["Deployment", "Deployment"];
55915
+ const title = refused ? `\uD83D\uDEA7 ${noun} Blocked` : `❌ ${noun} Failed`;
55916
+ const verb = refused ? "was blocked" : "failed";
55917
+ const embed = new DiscordEmbedBuilder().setTitle(title).setDescription(`${subject} ${verb} for **${failure.displayName || failure.slug}** (**${this.getEnvironment()}**).`).setColor(refused ? DiscordColors.YELLOW : DiscordColors.RED).addField("Slug", failure.slug, true);
54404
55918
  if (failure.developer) {
54405
55919
  embed.addField("Developer", failure.developer.email || failure.developer.id, true);
54406
55920
  }
54407
- embed.addField("Error", failure.error, false).setFooter("Playcademy Developer Platform").setTimestamp();
55921
+ embed.addField(refused ? "Reason" : "Error", failure.error, false);
55922
+ if (refused) {
55923
+ embed.addField("Next step", "The developer received repair guidance in their terminal", false);
55924
+ }
55925
+ embed.setFooter("Playcademy Developer Platform").setTimestamp();
55926
+ await this.sendAlert(discord, embed.build());
55927
+ }
55928
+ async notifyDeployBlocked(blocked) {
55929
+ const discord = this.recordAlert("deployment_blocked");
55930
+ if (!discord) {
55931
+ return;
55932
+ }
55933
+ const embed = new DiscordEmbedBuilder().setTitle("\uD83D\uDEA7 Deployment Blocked").setDescription(`Deployment was blocked for **${blocked.displayName || blocked.slug}** (**${this.getEnvironment()}**).`).setColor(DiscordColors.YELLOW).addField("Slug", blocked.slug, true);
55934
+ if (blocked.developer) {
55935
+ embed.addField("Developer", blocked.developer.email || blocked.developer.id, true);
55936
+ }
55937
+ embed.addField("Reason", blocked.reason, false);
55938
+ embed.addField("Next step", "The developer received repair guidance in their terminal", false);
55939
+ embed.setFooter("Playcademy Developer Platform").setTimestamp();
54408
55940
  await this.sendAlert(discord, embed.build());
54409
55941
  }
54410
55942
  async notifyGameDeletion(game2) {
@@ -54465,6 +55997,7 @@ var DISCORD_FIELD_LIMIT = 1024;
54465
55997
  var init_alerts_service = __esm(() => {
54466
55998
  init_discord();
54467
55999
  init_spans();
56000
+ init_game2();
54468
56001
  });
54469
56002
 
54470
56003
  class KVBackupService {
@@ -54893,6 +56426,15 @@ class DatabaseService {
54893
56426
  constructor(deps) {
54894
56427
  this.deps = deps;
54895
56428
  }
56429
+ static remapD1Resource(resources, d1ResourceName, databaseId) {
56430
+ return {
56431
+ resources: {
56432
+ ...resources,
56433
+ d1: resources.d1?.map((dbResource) => dbResource.name === d1ResourceName ? { ...dbResource, id: databaseId } : dbResource)
56434
+ },
56435
+ timeTravelBookmark: null
56436
+ };
56437
+ }
54896
56438
  getD1() {
54897
56439
  const d1 = this.deps.cloudflare?.d1;
54898
56440
  if (!d1) {
@@ -54911,11 +56453,7 @@ class DatabaseService {
54911
56453
  try {
54912
56454
  await this.deps.cloudflare.updateD1Binding(dashboardDeployment.deploymentId, databaseId);
54913
56455
  if (dashboardDeployment.resources?.d1?.length) {
54914
- const updatedResources = {
54915
- ...dashboardDeployment.resources,
54916
- d1: dashboardDeployment.resources.d1.map((dbResource) => dbResource.name === d1ResourceName ? { ...dbResource, id: databaseId } : dbResource)
54917
- };
54918
- await this.deps.db.update(gameDeployments).set({ resources: updatedResources }).where(eq(gameDeployments.id, dashboardDeployment.id));
56456
+ await this.deps.db.update(gameDeployments).set(DatabaseService.remapD1Resource(dashboardDeployment.resources, d1ResourceName, databaseId)).where(eq(gameDeployments.id, dashboardDeployment.id));
54919
56457
  }
54920
56458
  setAttribute("app.database.dashboard_binding", "updated");
54921
56459
  } catch (error) {
@@ -54926,20 +56464,36 @@ class DatabaseService {
54926
56464
  });
54927
56465
  }
54928
56466
  }
54929
- async reset(slug, user, schema2) {
56467
+ async reset(slug, user, request = {}) {
56468
+ const { schema: schema2, database } = request;
54930
56469
  setAttributes({
54931
56470
  "app.database.operation": "reset",
56471
+ "app.database.mode": database?.mode ?? (schema2 ? "legacy" : "none"),
54932
56472
  "app.database.schema_size": schema2?.sql.length,
54933
56473
  "app.database.schema_version": schema2?.hash
54934
56474
  });
54935
56475
  const d1 = this.getD1();
54936
56476
  const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56477
+ const state = await this.deps.db.query.gameDeploymentState.findFirst({
56478
+ where: eq(gameDeploymentState.gameId, game2.id)
56479
+ });
56480
+ if (isSchemaAdopted(state) && !database) {
56481
+ if (schema2) {
56482
+ throw new LegacySchemaUpgradeRequiredError;
56483
+ }
56484
+ throw new ValidationError("This game's database state is server-managed: resetting it requires the mode-aware rebuild payload so the recorded state matches the new database. Run the reset through the Playcademy CLI.");
56485
+ }
54937
56486
  const deploymentId = getGameDeploymentId(slug, this.deps.config.sstStage);
56487
+ let resetDatabaseId = null;
54938
56488
  try {
54939
56489
  const databaseId = await d1.reset(deploymentId);
56490
+ resetDatabaseId = databaseId;
54940
56491
  setAttribute("app.database.id", databaseId);
54941
56492
  let schemaPushed = false;
54942
- if (schema2?.sql) {
56493
+ if (database) {
56494
+ await this.rebuildDatabase(databaseId, database, game2.id, user);
56495
+ schemaPushed = true;
56496
+ } else if (schema2?.sql) {
54943
56497
  await d1.executeSchema(databaseId, schema2);
54944
56498
  schemaPushed = true;
54945
56499
  }
@@ -54955,11 +56509,7 @@ class DatabaseService {
54955
56509
  });
54956
56510
  setAttribute("app.database.active_deployment_found", Boolean(activeDeployment));
54957
56511
  if (activeDeployment?.resources?.d1?.length) {
54958
- const updatedResources = {
54959
- ...activeDeployment.resources,
54960
- d1: activeDeployment.resources.d1.map((dbResource) => dbResource.name === deploymentId ? { ...dbResource, id: databaseId } : dbResource)
54961
- };
54962
- await this.deps.db.update(gameDeployments).set({ resources: updatedResources }).where(eq(gameDeployments.id, activeDeployment.id));
56512
+ await this.deps.db.update(gameDeployments).set(DatabaseService.remapD1Resource(activeDeployment.resources, deploymentId, databaseId)).where(eq(gameDeployments.id, activeDeployment.id));
54963
56513
  }
54964
56514
  await this.syncDashboardD1Binding(game2.id, deploymentId, databaseId);
54965
56515
  setAttributes({
@@ -54973,24 +56523,604 @@ class DatabaseService {
54973
56523
  schemaPushed
54974
56524
  };
54975
56525
  } catch (error) {
56526
+ if (database && resetDatabaseId) {
56527
+ await this.recordLiveFingerprint(game2.id, resetDatabaseId);
56528
+ }
54976
56529
  this.deps.alerts.notifyDatabaseResetFailure({
54977
56530
  slug,
54978
56531
  displayName: game2.displayName,
54979
56532
  error: errorMessage2(error),
54980
56533
  developer: { id: user.id, email: user.email }
54981
56534
  }).catch(catchAttrs("alerts.delivery", { "app.alerts.type": "database_reset" }));
56535
+ if (error instanceof DomainError) {
56536
+ throw error;
56537
+ }
54982
56538
  throw new ValidationError(`Database reset failed: ${errorMessage2(error)}`);
54983
56539
  }
54984
56540
  }
56541
+ async rebuildDatabase(databaseId, payload, gameId, user) {
56542
+ const d1 = this.getD1();
56543
+ if (payload.mode === "migrate") {
56544
+ const deployId = `reset:${crypto.randomUUID()}`;
56545
+ await d1.ensureMigrationLedger(databaseId);
56546
+ for (const migration of payload.migrations) {
56547
+ await d1.applyMigration(databaseId, {
56548
+ tag: migration.tag,
56549
+ statements: migration.statements,
56550
+ checksum: migration.checksum,
56551
+ deployId,
56552
+ appliedBy: user.id
56553
+ });
56554
+ }
56555
+ setAttribute("app.database.migrations_replayed", payload.migrations.length);
56556
+ const fingerprint2 = await d1.fingerprintSchema(databaseId);
56557
+ await this.persistDeploymentState(gameId, {
56558
+ schemaFingerprint: fingerprint2.fingerprint,
56559
+ schemaHash: null,
56560
+ schemaSnapshot: null
56561
+ });
56562
+ return;
56563
+ }
56564
+ const statements = splitSqlStatements(payload.sql);
56565
+ if (statements.length > 0) {
56566
+ await d1.batch(databaseId, [
56567
+ { sql: "PRAGMA defer_foreign_keys = on" },
56568
+ ...statements.map((statement) => ({ sql: statement })),
56569
+ { sql: "PRAGMA defer_foreign_keys = off" }
56570
+ ]);
56571
+ }
56572
+ setAttribute("app.database.push_statement_count", statements.length);
56573
+ const fingerprint = await d1.fingerprintSchema(databaseId);
56574
+ await this.persistDeploymentState(gameId, {
56575
+ schemaFingerprint: fingerprint.fingerprint,
56576
+ schemaHash: payload.nextHash,
56577
+ schemaSnapshot: payload.nextSnapshot
56578
+ });
56579
+ }
56580
+ async persistDeploymentState(gameId, patch) {
56581
+ const set = { ...patch, updatedAt: new Date };
56582
+ await this.deps.db.insert(gameDeploymentState).values({ gameId, baselineSource: "deploy", ...set }).onConflictDoUpdate({ target: gameDeploymentState.gameId, set });
56583
+ }
56584
+ async recordLiveFingerprint(gameId, databaseId) {
56585
+ try {
56586
+ const fingerprint = await this.getD1().fingerprintSchema(databaseId);
56587
+ await this.persistDeploymentState(gameId, {
56588
+ schemaFingerprint: fingerprint.fingerprint
56589
+ });
56590
+ } catch (error) {
56591
+ addEvent("database.fingerprint_persist_failed", {
56592
+ "exception.type": errorType(error),
56593
+ "app.error.message": errorMessage2(error)
56594
+ });
56595
+ }
56596
+ }
54985
56597
  }
54986
56598
  var init_database_service = __esm(() => {
54987
56599
  init_drizzle_orm();
56600
+ init_src4();
54988
56601
  init_helpers_index();
54989
56602
  init_tables_index();
54990
56603
  init_spans();
54991
56604
  init_errors();
54992
56605
  init_deployment_util();
54993
56606
  });
56607
+ async function listUserSecretKeys(cloudflare2, deploymentId) {
56608
+ try {
56609
+ const allKeys = await cloudflare2.listSecrets(deploymentId);
56610
+ return allKeys.filter((key) => key.startsWith(SECRETS_PREFIX)).map((key) => key.slice(SECRETS_PREFIX.length));
56611
+ } catch (error) {
56612
+ const message = errorMessage2(error);
56613
+ if (message.includes("not found") || message.includes("10007")) {
56614
+ return null;
56615
+ }
56616
+ throw error;
56617
+ }
56618
+ }
56619
+ var init_secrets_util = __esm(() => {
56620
+ init_src();
56621
+ });
56622
+ function historyEventEntry(event) {
56623
+ const base = { at: event.createdAt.toISOString(), by: event.email ?? DELETED_ACCOUNT_LABEL };
56624
+ if (event.kind === "restore" && "restoredTo" in event.payload) {
56625
+ return [{ kind: "restore", ...base, restoredTo: event.payload.restoredTo }];
56626
+ }
56627
+ if (event.kind === "blocked" && "code" in event.payload) {
56628
+ return [
56629
+ { kind: "blocked", ...base, code: event.payload.code, reason: event.payload.reason }
56630
+ ];
56631
+ }
56632
+ return [];
56633
+ }
56634
+ function databaseIdFromResources(resources, deploymentId) {
56635
+ const database = resources?.d1?.find((db2) => db2.name === deploymentId) ?? resources?.d1?.[0];
56636
+ return database?.id ?? null;
56637
+ }
56638
+ function restorePointCapturedAt(row) {
56639
+ return row.bookmarkCapturedAt ?? row.deployedAt;
56640
+ }
56641
+
56642
+ class DeploymentStateService {
56643
+ deps;
56644
+ constructor(deps) {
56645
+ this.deps = deps;
56646
+ }
56647
+ async partitionSecretKeys(slug, manifest) {
56648
+ const managedKeys = Object.keys(manifest ?? {});
56649
+ if (!this.deps.cloudflare) {
56650
+ addEvent("deployment_state.secret_keys_unavailable", {
56651
+ "app.error.message": "Cloudflare provider not configured"
56652
+ });
56653
+ return { managedKeys, unmanagedKeys: null };
56654
+ }
56655
+ const deploymentId = getGameDeploymentId(slug, this.deps.config.sstStage);
56656
+ const remoteKeys = await listUserSecretKeys(this.deps.cloudflare, deploymentId);
56657
+ if (remoteKeys === null) {
56658
+ return { managedKeys, unmanagedKeys: [] };
56659
+ }
56660
+ const managed = new Set(managedKeys);
56661
+ return {
56662
+ managedKeys,
56663
+ unmanagedKeys: remoteKeys.filter((key) => !managed.has(key))
56664
+ };
56665
+ }
56666
+ async readAppliedMigrations(slug, resources) {
56667
+ if (!this.deps.cloudflare || !resources?.d1?.length) {
56668
+ return null;
56669
+ }
56670
+ const deploymentId = getGameDeploymentId(slug, this.deps.config.sstStage);
56671
+ const databaseId = databaseIdFromResources(resources, deploymentId);
56672
+ if (!databaseId) {
56673
+ return null;
56674
+ }
56675
+ const ledger = await this.deps.cloudflare.d1.readMigrationLedger(databaseId);
56676
+ setAttributes({ "app.deployment_state.applied_migrations": ledger.length });
56677
+ return ledger.map((row) => ({
56678
+ tag: row.tag,
56679
+ checksum: row.checksum,
56680
+ checksumAlgo: row.checksum_algo
56681
+ }));
56682
+ }
56683
+ async get(slug, user, options = {}) {
56684
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56685
+ const [state, gameDeployment, dashboardDeployment, lastSucceededJob, lastFailedJob] = await Promise.all([
56686
+ this.deps.db.query.gameDeploymentState.findFirst({
56687
+ where: eq(gameDeploymentState.gameId, game2.id)
56688
+ }),
56689
+ this.deps.db.query.gameDeployments.findFirst({
56690
+ where: activeDeploymentWhere(game2.id, "game"),
56691
+ columns: { codeHash: true, url: true, deployedAt: true, resources: true }
56692
+ }),
56693
+ this.deps.db.query.gameDeployments.findFirst({
56694
+ where: activeDeploymentWhere(game2.id, "dashboard"),
56695
+ columns: { url: true, deployedAt: true }
56696
+ }),
56697
+ findLastSuccessfulDeployWithEmail(this.deps.db, game2.id),
56698
+ this.deps.db.query.gameDeployJobs.findFirst({
56699
+ where: and(eq(gameDeployJobs.gameId, game2.id), eq(gameDeployJobs.status, "failed")),
56700
+ orderBy: desc(deployJobInstant()),
56701
+ columns: { events: true }
56702
+ })
56703
+ ]);
56704
+ const [secrets, appliedMigrations] = await Promise.all([
56705
+ this.partitionSecretKeys(slug, state?.secretsManifest ?? null),
56706
+ this.readAppliedMigrations(slug, gameDeployment?.resources ?? null)
56707
+ ]);
56708
+ setAttributes({
56709
+ "app.deployment_state.seeded": Boolean(state),
56710
+ "app.deployment_state.game_deployed": Boolean(gameDeployment),
56711
+ "app.deployment_state.dashboard_deployed": Boolean(dashboardDeployment)
56712
+ });
56713
+ return {
56714
+ gameId: game2.id,
56715
+ seeded: Boolean(state),
56716
+ game: gameDeployment ? {
56717
+ codeHash: gameDeployment.codeHash,
56718
+ buildHash: state?.buildHash ?? null,
56719
+ url: gameDeployment.url,
56720
+ deployedAt: gameDeployment.deployedAt.toISOString()
56721
+ } : null,
56722
+ dashboard: dashboardDeployment ? {
56723
+ url: dashboardDeployment.url,
56724
+ deployedAt: dashboardDeployment.deployedAt.toISOString()
56725
+ } : null,
56726
+ database: {
56727
+ appliedMigrations,
56728
+ lastFailure: parseMigrationFailure(lastFailedJob?.events ?? null),
56729
+ schemaHash: state?.schemaHash ?? null,
56730
+ schemaFingerprint: state?.schemaFingerprint ?? null,
56731
+ ...options.includeSchemaSnapshot && {
56732
+ schemaSnapshot: state?.schemaSnapshot ?? null
56733
+ }
56734
+ },
56735
+ secrets,
56736
+ integrationsHash: state?.integrationsHash ?? null,
56737
+ compatibilityDate: state?.compatibilityDate ?? null,
56738
+ lastDeploy: lastSucceededJob ? {
56739
+ at: lastSucceededJob.at.toISOString(),
56740
+ by: lastSucceededJob.email ?? DELETED_ACCOUNT_LABEL
56741
+ } : null
56742
+ };
56743
+ }
56744
+ requireCloudflare() {
56745
+ if (!this.deps.cloudflare) {
56746
+ throw new ValidationError("Deployment-state operations are not available in this environment");
56747
+ }
56748
+ return this.deps.cloudflare;
56749
+ }
56750
+ async resolveDatabaseId(slug, gameId) {
56751
+ const databaseId = await this.findDatabaseId(slug, gameId);
56752
+ if (!databaseId) {
56753
+ throw new ValidationError("Game has no deployed database — deploy with a database binding first");
56754
+ }
56755
+ return databaseId;
56756
+ }
56757
+ findSchemaHashState(gameId) {
56758
+ return this.deps.db.query.gameDeploymentState.findFirst({
56759
+ where: eq(gameDeploymentState.gameId, gameId),
56760
+ columns: { schemaHash: true }
56761
+ });
56762
+ }
56763
+ async findDatabaseId(slug, gameId) {
56764
+ const deployment = await this.deps.db.query.gameDeployments.findFirst({
56765
+ where: activeDeploymentWhere(gameId, "game"),
56766
+ columns: { resources: true }
56767
+ });
56768
+ return databaseIdFromResources(deployment?.resources, getGameDeploymentId(slug, this.deps.config.sstStage));
56769
+ }
56770
+ async baseline(slug, input, user) {
56771
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56772
+ const cf = this.requireCloudflare();
56773
+ const state = await this.deps.db.query.gameDeploymentState.findFirst({
56774
+ where: eq(gameDeploymentState.gameId, game2.id)
56775
+ });
56776
+ const schemaAdopted = isSchemaAdopted(state);
56777
+ const migrateOnlyClaim = Boolean(input.lastAppliedMigrationTag) && input.schemaHash === undefined && input.schemaSnapshot === undefined;
56778
+ if (schemaAdopted && !migrateOnlyClaim) {
56779
+ throw new BaselineAlreadyAdoptedError(state?.baselineSource ?? null);
56780
+ }
56781
+ const databaseId = await this.resolveDatabaseId(slug, game2.id);
56782
+ const [ledger, live] = await Promise.all([
56783
+ cf.d1.readMigrationLedger(databaseId),
56784
+ cf.d1.fingerprintSchema(databaseId)
56785
+ ]);
56786
+ const verdict = evaluateBaselineGuardrails({
56787
+ ledgerTags: ledger.map((row) => row.tag),
56788
+ liveTables: live.tables
56789
+ });
56790
+ if (verdict === "ledger-not-empty") {
56791
+ throw new BaselineLedgerNotEmptyError(ledger.map((row) => row.tag));
56792
+ }
56793
+ if (verdict === "database-empty") {
56794
+ throw new BaselineDatabaseEmptyError;
56795
+ }
56796
+ if (schemaAdopted && state?.schemaFingerprint && live.fingerprint !== state.schemaFingerprint) {
56797
+ throw new DeploymentStateDriftError({
56798
+ expectedFingerprint: state.schemaFingerprint,
56799
+ actualFingerprint: live.fingerprint
56800
+ });
56801
+ }
56802
+ if (input.lastAppliedMigrationTag && input.evidence?.length) {
56803
+ const [tables, lastDeploy] = await Promise.all([
56804
+ cf.d1.readTableColumns(databaseId, live.tables),
56805
+ findLastSuccessfulDeploy(this.deps.db, game2.id)
56806
+ ]);
56807
+ assertBaselineClaimValid({
56808
+ claimedTag: input.lastAppliedMigrationTag,
56809
+ evidence: input.evidence,
56810
+ tables,
56811
+ indexes: new Set(live.indexes),
56812
+ views: new Set(live.views),
56813
+ lastDeployAt: lastDeploy?.at ?? null,
56814
+ allowUnverified: Boolean(input.allowUnverified),
56815
+ source: "manual",
56816
+ gameId: game2.id,
56817
+ userId: user.id
56818
+ });
56819
+ }
56820
+ let recordedTags = [];
56821
+ if (input.lastAppliedMigrationTag) {
56822
+ const slice = sliceJournalToTag(input.journal ?? [], input.lastAppliedMigrationTag);
56823
+ if (!slice) {
56824
+ throw new ValidationError(`lastAppliedMigrationTag '${input.lastAppliedMigrationTag}' is not in the submitted journal`);
56825
+ }
56826
+ await cf.d1.ensureMigrationLedger(databaseId);
56827
+ await cf.d1.recordLedgerRows(databaseId, {
56828
+ rows: slice,
56829
+ deployId: `baseline:${crypto.randomUUID()}`,
56830
+ appliedBy: user.id,
56831
+ source: "baseline"
56832
+ });
56833
+ recordedTags = slice.map((entry2) => entry2.tag);
56834
+ }
56835
+ const graduating = migrateOnlyClaim && isPushAdopted(state);
56836
+ const set = {
56837
+ ...graduating ? { schemaHash: null, schemaSnapshot: null } : {
56838
+ ...input.schemaHash !== undefined && { schemaHash: input.schemaHash },
56839
+ ...input.schemaSnapshot !== undefined && {
56840
+ schemaSnapshot: input.schemaSnapshot
56841
+ }
56842
+ },
56843
+ schemaFingerprint: live.fingerprint,
56844
+ baselineSource: "manual-baseline",
56845
+ updatedAt: new Date
56846
+ };
56847
+ await this.persistDeploymentState(game2.id, set);
56848
+ addEvent("deployment_state.baseline_recorded", {
56849
+ "app.game.id": game2.id,
56850
+ "app.deployment_state.baseline_source": "manual-baseline",
56851
+ "app.deployment_state.baseline_ledger_rows": recordedTags.length,
56852
+ "app.deployment_state.baseline_has_snapshot": input.schemaSnapshot !== undefined,
56853
+ "app.deployment_state.baseline_graduated_from_push": graduating
56854
+ });
56855
+ return this.get(slug, user);
56856
+ }
56857
+ async realignMigration(slug, tag, checksum, user) {
56858
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56859
+ const cf = this.requireCloudflare();
56860
+ const databaseId = await this.resolveDatabaseId(slug, game2.id);
56861
+ const ledger = await cf.d1.readMigrationLedger(databaseId);
56862
+ if (!ledger.some((row) => row.tag === tag)) {
56863
+ throw new NotFoundError("Applied migration", tag);
56864
+ }
56865
+ const updated = await cf.d1.updateLedgerChecksum(databaseId, { tag, checksum });
56866
+ if (!updated) {
56867
+ throw new NotFoundError("Applied migration", tag);
56868
+ }
56869
+ addEvent("deployment_state.migration_realigned", {
56870
+ "app.game.id": game2.id,
56871
+ "app.d1.migration_tag": tag,
56872
+ "app.user.id": user.id
56873
+ });
56874
+ return { tag, checksum, checksumAlgo: MIGRATION_CHECKSUM_ALGO };
56875
+ }
56876
+ async persistDeploymentState(gameId, set) {
56877
+ await this.deps.db.insert(gameDeploymentState).values({ gameId, ...set }).onConflictDoUpdate({ target: gameDeploymentState.gameId, set });
56878
+ }
56879
+ async updateDeploymentState(gameId, set) {
56880
+ const updated = await this.deps.db.update(gameDeploymentState).set(set).where(eq(gameDeploymentState.gameId, gameId)).returning({ gameId: gameDeploymentState.gameId });
56881
+ return updated.length > 0;
56882
+ }
56883
+ async resolveMigration(slug, tag, input, user) {
56884
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56885
+ const cf = this.requireCloudflare();
56886
+ const databaseId = await this.resolveDatabaseId(slug, game2.id);
56887
+ const [ledger, live] = await Promise.all([
56888
+ cf.d1.readMigrationLedger(databaseId),
56889
+ cf.d1.fingerprintSchema(databaseId)
56890
+ ]);
56891
+ const exists2 = ledger.some((row) => row.tag === tag);
56892
+ if (input.resolution === "applied") {
56893
+ if (!input.checksum) {
56894
+ throw new ValidationError("Resolving a migration as 'applied' requires its checksum");
56895
+ }
56896
+ if (exists2) {
56897
+ throw new AlreadyExistsError(`Migration '${tag}' is already recorded as applied`);
56898
+ }
56899
+ await cf.d1.ensureMigrationLedger(databaseId);
56900
+ await cf.d1.recordLedgerRows(databaseId, {
56901
+ rows: [{ tag, checksum: input.checksum }],
56902
+ deployId: `resolve:${crypto.randomUUID()}`,
56903
+ appliedBy: user.id,
56904
+ source: "resolve"
56905
+ });
56906
+ } else {
56907
+ if (!exists2) {
56908
+ throw new NotFoundError("Migration ledger row", tag);
56909
+ }
56910
+ await cf.d1.deleteLedgerRow(databaseId, tag);
56911
+ }
56912
+ const fingerprintRecorded = await this.updateDeploymentState(game2.id, {
56913
+ schemaFingerprint: live.fingerprint,
56914
+ updatedAt: new Date
56915
+ });
56916
+ addEvent("deployment_state.migration_resolved", {
56917
+ "app.game.id": game2.id,
56918
+ "app.d1.migration_tag": tag,
56919
+ "app.deployment_state.resolution": input.resolution,
56920
+ "app.deployment_state.fingerprint_rerecorded": fingerprintRecorded,
56921
+ "app.user.id": user.id
56922
+ });
56923
+ return {
56924
+ tag,
56925
+ resolution: input.resolution,
56926
+ schemaFingerprint: live.fingerprint,
56927
+ fingerprintRecorded
56928
+ };
56929
+ }
56930
+ async history(slug, user, options = {}) {
56931
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56932
+ const limit = options.limit ?? 20;
56933
+ const [jobs, events] = await Promise.all([
56934
+ this.deps.db.select({
56935
+ status: gameDeployJobs.status,
56936
+ createdAt: gameDeployJobs.createdAt,
56937
+ completedAt: gameDeployJobs.completedAt,
56938
+ deployId: gameDeployJobs.deployId,
56939
+ error: gameDeployJobs.error,
56940
+ email: users.email
56941
+ }).from(gameDeployJobs).leftJoin(users, eq(gameDeployJobs.userId, users.id)).where(eq(gameDeployJobs.gameId, game2.id)).orderBy(desc(deployJobInstant())).limit(limit),
56942
+ this.deps.db.select({
56943
+ kind: gameDeployEvents.kind,
56944
+ payload: gameDeployEvents.payload,
56945
+ createdAt: gameDeployEvents.createdAt,
56946
+ email: users.email
56947
+ }).from(gameDeployEvents).leftJoin(users, eq(gameDeployEvents.userId, users.id)).where(eq(gameDeployEvents.gameId, game2.id)).orderBy(desc(gameDeployEvents.createdAt)).limit(limit)
56948
+ ]);
56949
+ setAttributes({
56950
+ "app.deployment_state.history_jobs": jobs.length,
56951
+ "app.deployment_state.history_events": events.length
56952
+ });
56953
+ const deploys = jobs.map((job) => ({
56954
+ kind: "deploy",
56955
+ status: job.status,
56956
+ at: (job.completedAt ?? job.createdAt).toISOString(),
56957
+ completedAt: job.completedAt?.toISOString() ?? null,
56958
+ by: job.email ?? DELETED_ACCOUNT_LABEL,
56959
+ deployId: job.deployId,
56960
+ error: job.error
56961
+ }));
56962
+ const merged = [...deploys, ...events.flatMap(historyEventEntry)].toSorted((a, b) => a.at < b.at ? 1 : -1).slice(0, limit);
56963
+ return { deploys: merged };
56964
+ }
56965
+ async restorePoints(slug, user) {
56966
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56967
+ const retentionCutoff = new Date(Date.now() - D1_TIME_TRAVEL_RETENTION_DAYS * 24 * 60 * 60 * 1000);
56968
+ const [currentDatabaseId, rows, state] = await Promise.all([
56969
+ this.findDatabaseId(slug, game2.id),
56970
+ this.deps.db.query.gameDeployments.findMany({
56971
+ where: and(eq(gameDeployments.gameId, game2.id), eq(gameDeployments.target, "game"), isNotNull(gameDeployments.timeTravelBookmark), gte(gameDeployments.deployedAt, retentionCutoff)),
56972
+ orderBy: desc(gameDeployments.deployedAt),
56973
+ limit: 100,
56974
+ columns: {
56975
+ id: true,
56976
+ deployedAt: true,
56977
+ bookmarkCapturedAt: true,
56978
+ isActive: true,
56979
+ resources: true
56980
+ }
56981
+ }),
56982
+ this.findSchemaHashState(game2.id)
56983
+ ]);
56984
+ if (isPushAdopted(state)) {
56985
+ return { restorePoints: [], restoreUnsupported: true };
56986
+ }
56987
+ if (!currentDatabaseId) {
56988
+ return { restorePoints: [], restoreUnsupported: false };
56989
+ }
56990
+ const deploymentId = getGameDeploymentId(slug, this.deps.config.sstStage);
56991
+ const restorable = rows.filter((row) => databaseIdFromResources(row.resources, deploymentId) === currentDatabaseId && restorePointCapturedAt(row) >= retentionCutoff).slice(0, 20);
56992
+ setAttributes({ "app.deployment_state.restore_points": restorable.length });
56993
+ return {
56994
+ restorePoints: restorable.map((row) => ({
56995
+ id: row.id,
56996
+ capturedAt: restorePointCapturedAt(row).toISOString(),
56997
+ active: row.isActive
56998
+ })),
56999
+ restoreUnsupported: false
57000
+ };
57001
+ }
57002
+ async restoreToBookmark(slug, input, user) {
57003
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
57004
+ const cf = this.requireCloudflare();
57005
+ const [row, state, currentDatabaseId, runningJob] = await Promise.all([
57006
+ this.deps.db.query.gameDeployments.findFirst({
57007
+ where: and(eq(gameDeployments.id, input.restorePointId), eq(gameDeployments.gameId, game2.id), eq(gameDeployments.target, "game")),
57008
+ columns: {
57009
+ id: true,
57010
+ timeTravelBookmark: true,
57011
+ deployedAt: true,
57012
+ bookmarkCapturedAt: true,
57013
+ resources: true
57014
+ }
57015
+ }),
57016
+ this.findSchemaHashState(game2.id),
57017
+ this.findDatabaseId(slug, game2.id),
57018
+ this.deps.db.query.gameDeployJobs.findFirst({
57019
+ where: and(eq(gameDeployJobs.gameId, game2.id), or(eq(gameDeployJobs.status, "pending"), and(eq(gameDeployJobs.status, "running"), gt(gameDeployJobs.leaseExpiresAt, new Date)))),
57020
+ columns: { id: true }
57021
+ })
57022
+ ]);
57023
+ if (!row?.timeTravelBookmark) {
57024
+ throw new NotFoundError("Bookmark", input.restorePointId);
57025
+ }
57026
+ if (isPushAdopted(state)) {
57027
+ throw new ValidationError("Restore is not supported for push-mode projects: rewinding the database would leave future push deploys diffing against the wrong schema.", { code: DEPLOY_ERROR_CODES.restoreUnsupported });
57028
+ }
57029
+ if (!currentDatabaseId) {
57030
+ throw new ValidationError("Game has no deployed database — deploy with a database binding first");
57031
+ }
57032
+ const databaseId = currentDatabaseId;
57033
+ const deploymentId = getGameDeploymentId(slug, this.deps.config.sstStage);
57034
+ if (databaseIdFromResources(row.resources, deploymentId) !== databaseId) {
57035
+ throw new ValidationError("This bookmark was captured on a previous database (a reset replaced it since) and can no longer be restored");
57036
+ }
57037
+ const retentionCutoff = new Date(Date.now() - D1_TIME_TRAVEL_RETENTION_DAYS * 24 * 60 * 60 * 1000);
57038
+ if (restorePointCapturedAt(row) < retentionCutoff) {
57039
+ throw new ValidationError(`This bookmark is older than Time Travel's ${D1_TIME_TRAVEL_RETENTION_DAYS}-day retention window and can no longer be restored.`, { code: DEPLOY_ERROR_CODES.restoreExpired });
57040
+ }
57041
+ if (runningJob) {
57042
+ throw new ValidationError("A deploy is currently running for this game. Wait for it to finish, then restore.", { code: DEPLOY_ERROR_CODES.restoreBlockedByDeploy });
57043
+ }
57044
+ const restored = await cf.d1.restoreBookmark(databaseId, row.timeTravelBookmark);
57045
+ const restoredTo = restorePointCapturedAt(row).toISOString();
57046
+ let live;
57047
+ let fingerprintRecorded;
57048
+ try {
57049
+ live = await cf.d1.fingerprintSchema(databaseId);
57050
+ fingerprintRecorded = await this.updateDeploymentState(game2.id, {
57051
+ schemaFingerprint: live.fingerprint,
57052
+ updatedAt: new Date
57053
+ });
57054
+ await this.deps.db.insert(gameDeployEvents).values({
57055
+ gameId: game2.id,
57056
+ userId: user.id,
57057
+ kind: "restore",
57058
+ payload: { restoredTo, previousBookmark: restored.previousBookmark }
57059
+ });
57060
+ } catch (error) {
57061
+ throw new ValidationError(`The database WAS restored, but re-recording its schema fingerprint failed (${errorMessage2(error)}). Run the same restore again to complete it.`, { code: DEPLOY_ERROR_CODES.restoreIncomplete });
57062
+ }
57063
+ addEvent("deployment_state.bookmark_restored", {
57064
+ "app.game.id": game2.id,
57065
+ "app.deployment_state.restore_point": row.id,
57066
+ "app.deployment_state.fingerprint_rerecorded": fingerprintRecorded,
57067
+ "app.user.id": user.id
57068
+ });
57069
+ return {
57070
+ restorePointId: row.id,
57071
+ restoredTo,
57072
+ schemaFingerprint: live.fingerprint,
57073
+ fingerprintRecorded,
57074
+ previousBookmark: restored.previousBookmark
57075
+ };
57076
+ }
57077
+ async reportDeployBlocked(slug, user, report) {
57078
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
57079
+ const recent = await this.deps.db.query.gameDeployEvents.findMany({
57080
+ where: and(eq(gameDeployEvents.gameId, game2.id), eq(gameDeployEvents.kind, "blocked"), gt(gameDeployEvents.createdAt, new Date(Date.now() - BLOCKED_ALERT_DEDUP_MS))),
57081
+ orderBy: desc(gameDeployEvents.createdAt),
57082
+ limit: 10,
57083
+ columns: { payload: true }
57084
+ });
57085
+ const duplicate = recent.some((event) => ("code" in event.payload) && event.payload.code === report.code);
57086
+ await this.deps.db.insert(gameDeployEvents).values({
57087
+ gameId: game2.id,
57088
+ userId: user.id,
57089
+ kind: "blocked",
57090
+ payload: { code: report.code, reason: report.reason }
57091
+ });
57092
+ addEvent("deployment_state.deploy_blocked", {
57093
+ "app.game.id": game2.id,
57094
+ "app.deploy.blocked_code": report.code,
57095
+ "app.user.id": user.id,
57096
+ "app.alerts.deduped": duplicate
57097
+ });
57098
+ if (!duplicate) {
57099
+ await this.deps.alerts.notifyDeployBlocked({
57100
+ slug,
57101
+ displayName: game2.displayName,
57102
+ reason: report.reason,
57103
+ developer: { id: user.id, email: user.email ?? null }
57104
+ });
57105
+ }
57106
+ }
57107
+ }
57108
+ var BLOCKED_ALERT_DEDUP_MS;
57109
+ var init_deployment_state_service = __esm(() => {
57110
+ init_drizzle_orm();
57111
+ init_src4();
57112
+ init_src();
57113
+ init_helpers_index();
57114
+ init_tables_index();
57115
+ init_spans();
57116
+ init_game2();
57117
+ init_errors();
57118
+ init_baseline_validation_util();
57119
+ init_deployment_util();
57120
+ init_migration_util();
57121
+ init_secrets_util();
57122
+ BLOCKED_ALERT_DEDUP_MS = 600000;
57123
+ });
54994
57124
 
54995
57125
  class DomainService {
54996
57126
  deps;
@@ -55355,6 +57485,7 @@ var init_kv_service = __esm(() => {
55355
57485
 
55356
57486
  class SecretsService {
55357
57487
  deps;
57488
+ pepperPromise = null;
55358
57489
  constructor(deps) {
55359
57490
  this.deps = deps;
55360
57491
  }
@@ -55367,36 +57498,69 @@ class SecretsService {
55367
57498
  getGameDeploymentId(slug) {
55368
57499
  return getGameDeploymentId(slug, this.deps.config.sstStage);
55369
57500
  }
55370
- async listKeys(slug, user) {
55371
- await this.deps.validateDeveloperAccessBySlug(user, slug);
55372
- const cf = this.getCloudflare();
55373
- const deploymentId = this.getGameDeploymentId(slug);
55374
- try {
55375
- const allKeys = await cf.listSecrets(deploymentId);
55376
- const userKeys = allKeys.filter((k) => k.startsWith(SECRETS_PREFIX)).map((k) => k.slice(SECRETS_PREFIX.length));
55377
- setAttributes({
55378
- "app.secrets.operation": "list",
55379
- "app.secrets.count": userKeys.length,
55380
- "app.secrets.game_deployed": true
55381
- });
55382
- return userKeys;
55383
- } catch (error) {
55384
- const message = errorMessage2(error);
55385
- if (message.includes("not found") || message.includes("10007")) {
57501
+ getPepper() {
57502
+ const pepperSecret = this.deps.config.secretsManifestPepper;
57503
+ if (!pepperSecret) {
57504
+ throw new ValidationError("Secrets manifest is not configured (missing manifest pepper secret)");
57505
+ }
57506
+ this.pepperPromise ??= deriveSecretsManifestPepper(pepperSecret);
57507
+ return this.pepperPromise;
57508
+ }
57509
+ async computeManifestEntries(gameId, secrets) {
57510
+ const pepper = await this.getPepper();
57511
+ const entries = {};
57512
+ for (const [key, value] of Object.entries(secrets)) {
57513
+ entries[key] = await computeSecretDigest(pepper, { gameId, key, value });
57514
+ }
57515
+ return entries;
57516
+ }
57517
+ async readManifest(gameId) {
57518
+ const state = await this.deps.db.query.gameDeploymentState.findFirst({
57519
+ where: eq(gameDeploymentState.gameId, gameId),
57520
+ columns: { secretsManifest: true }
57521
+ });
57522
+ return state?.secretsManifest ?? {};
57523
+ }
57524
+ async upsertManifestEntries(gameId, entries) {
57525
+ const merged = sql`coalesce(${gameDeploymentState.secretsManifest}, '{}'::jsonb) || ${JSON.stringify(entries)}::jsonb`;
57526
+ await this.deps.db.insert(gameDeploymentState).values({ gameId, secretsManifest: entries, updatedAt: new Date }).onConflictDoUpdate({
57527
+ target: gameDeploymentState.gameId,
57528
+ set: { secretsManifest: merged, updatedAt: new Date }
57529
+ });
57530
+ }
57531
+ async removeManifestKey(gameId, key) {
57532
+ await this.deps.db.update(gameDeploymentState).set({
57533
+ secretsManifest: sql`coalesce(${gameDeploymentState.secretsManifest}, '{}'::jsonb) - ${key}::text`,
57534
+ updatedAt: new Date
57535
+ }).where(eq(gameDeploymentState.gameId, gameId));
57536
+ }
57537
+ assertNoReservedKeys(keys, operation) {
57538
+ for (const key of keys) {
57539
+ if (INTERNAL_SECRET_KEYS.includes(key)) {
55386
57540
  setAttributes({
55387
- "app.secrets.operation": "list",
55388
- "app.secrets.count": 0,
55389
- "app.secrets.game_deployed": false
57541
+ "app.secrets.operation": operation,
57542
+ "app.secrets.reserved_key_rejected": true
55390
57543
  });
55391
- return [];
57544
+ throw new ValidationError(operation === "set" ? `Cannot set reserved secret "${key}"` : `Reserved secret "${key}" cannot be managed — remove it locally`);
55392
57545
  }
55393
- throw error;
55394
57546
  }
55395
57547
  }
55396
- async setSecrets(slug, newSecrets, user) {
57548
+ async listKeys(slug, user) {
55397
57549
  await this.deps.validateDeveloperAccessBySlug(user, slug);
55398
57550
  const cf = this.getCloudflare();
55399
57551
  const deploymentId = this.getGameDeploymentId(slug);
57552
+ const userKeys = await listUserSecretKeys(cf, deploymentId);
57553
+ setAttributes({
57554
+ "app.secrets.operation": "list",
57555
+ "app.secrets.count": userKeys?.length ?? 0,
57556
+ "app.secrets.game_deployed": userKeys !== null
57557
+ });
57558
+ return userKeys ?? [];
57559
+ }
57560
+ async setSecrets(slug, newSecrets, user) {
57561
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
57562
+ const cf = this.getCloudflare();
57563
+ const deploymentId = this.getGameDeploymentId(slug);
55400
57564
  const secretKeys = Object.keys(newSecrets);
55401
57565
  if (secretKeys.length === 0) {
55402
57566
  throw new ValidationError("At least one secret must be provided");
@@ -55405,14 +57569,9 @@ class SecretsService {
55405
57569
  if (typeof value !== "string") {
55406
57570
  throw new ValidationError(`Secret value for "${key}" must be a string`);
55407
57571
  }
55408
- if (INTERNAL_SECRET_KEYS.includes(key)) {
55409
- setAttributes({
55410
- "app.secrets.operation": "set",
55411
- "app.secrets.reserved_key_rejected": true
55412
- });
55413
- throw new ValidationError(`Cannot set reserved secret "${key}"`);
55414
- }
55415
57572
  }
57573
+ this.assertNoReservedKeys(secretKeys, "set");
57574
+ const manifestEntries = await this.computeManifestEntries(game2.id, newSecrets);
55416
57575
  try {
55417
57576
  const prefixedSecrets = {};
55418
57577
  for (const [key, value] of Object.entries(newSecrets)) {
@@ -55425,8 +57584,6 @@ class SecretsService {
55425
57584
  "app.secrets.game_deployed": true,
55426
57585
  "app.secrets.reserved_key_rejected": false
55427
57586
  });
55428
- const allKeys = await cf.listSecrets(deploymentId);
55429
- return allKeys.filter((k) => k.startsWith(SECRETS_PREFIX)).map((k) => k.slice(SECRETS_PREFIX.length));
55430
57587
  } catch (error) {
55431
57588
  const message = errorMessage2(error);
55432
57589
  if (message.includes("not found") || message.includes("10007")) {
@@ -55438,6 +57595,9 @@ class SecretsService {
55438
57595
  }
55439
57596
  throw error;
55440
57597
  }
57598
+ await this.upsertManifestEntries(game2.id, manifestEntries);
57599
+ const allKeys = await cf.listSecrets(deploymentId);
57600
+ return allKeys.filter((k) => k.startsWith(SECRETS_PREFIX)).map((k) => k.slice(SECRETS_PREFIX.length));
55441
57601
  }
55442
57602
  async deleteSecret(slug, key, user) {
55443
57603
  if (INTERNAL_SECRET_KEYS.includes(key)) {
@@ -55447,19 +57607,25 @@ class SecretsService {
55447
57607
  });
55448
57608
  throw new ValidationError(`Cannot delete reserved secret "${key}"`);
55449
57609
  }
55450
- await this.deps.validateDeveloperAccessBySlug(user, slug);
57610
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
55451
57611
  const cf = this.getCloudflare();
55452
57612
  const deploymentId = this.getGameDeploymentId(slug);
57613
+ const manifest = await this.readManifest(game2.id);
57614
+ const managed = key in manifest;
55453
57615
  try {
55454
57616
  const prefixedKey = `${SECRETS_PREFIX}${key}`;
55455
57617
  const existingKeys = await cf.listSecrets(deploymentId);
55456
- if (!existingKeys.includes(prefixedKey)) {
57618
+ const onWorker = existingKeys.includes(prefixedKey);
57619
+ if (!onWorker && !managed) {
55457
57620
  throw new NotFoundError("Secret", key);
55458
57621
  }
55459
- await cf.deleteSecret(deploymentId, prefixedKey);
57622
+ if (onWorker) {
57623
+ await cf.deleteSecret(deploymentId, prefixedKey);
57624
+ }
55460
57625
  setAttributes({
55461
57626
  "app.secrets.operation": "delete",
55462
57627
  "app.secrets.game_deployed": true,
57628
+ "app.secrets.managed": managed,
55463
57629
  "app.secrets.reserved_key_rejected": false
55464
57630
  });
55465
57631
  } catch (error) {
@@ -55476,14 +57642,45 @@ class SecretsService {
55476
57642
  }
55477
57643
  throw error;
55478
57644
  }
57645
+ if (managed) {
57646
+ await this.removeManifestKey(game2.id, key);
57647
+ }
57648
+ }
57649
+ async diff(slug, localSecrets, user) {
57650
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
57651
+ const deploymentId = this.getGameDeploymentId(slug);
57652
+ this.assertNoReservedKeys(Object.keys(localSecrets), "diff");
57653
+ const [manifest, remoteKeys, localDigests] = await Promise.all([
57654
+ this.readManifest(game2.id),
57655
+ this.deps.cloudflare ? listUserSecretKeys(this.deps.cloudflare, deploymentId) : null,
57656
+ this.computeManifestEntries(game2.id, localSecrets)
57657
+ ]);
57658
+ const verdicts = computeSecretsDiff({
57659
+ localDigests,
57660
+ manifest,
57661
+ remoteKeys: remoteKeys ?? []
57662
+ });
57663
+ setAttributes({
57664
+ "app.secrets.operation": "diff",
57665
+ "app.secrets.game_deployed": remoteKeys !== null,
57666
+ "app.secrets.diff_added": verdicts.added.length,
57667
+ "app.secrets.diff_changed": verdicts.changed.length,
57668
+ "app.secrets.diff_unchanged": verdicts.unchanged.length,
57669
+ "app.secrets.diff_remote_only_managed": verdicts.remoteOnlyManaged.length,
57670
+ "app.secrets.diff_remote_only_unmanaged": verdicts.remoteOnlyUnmanaged.length
57671
+ });
57672
+ return verdicts;
55479
57673
  }
55480
57674
  }
55481
57675
  var INTERNAL_SECRET_KEYS;
55482
57676
  var init_secrets_service = __esm(() => {
57677
+ init_drizzle_orm();
55483
57678
  init_src();
57679
+ init_tables_index();
55484
57680
  init_spans();
55485
57681
  init_errors();
55486
57682
  init_deployment_util();
57683
+ init_secrets_util();
55487
57684
  INTERNAL_SECRET_KEYS = ["PLAYCADEMY_API_KEY", "GAME_ID", "PLAYCADEMY_BASE_URL"];
55488
57685
  });
55489
57686
  function prefixSecrets(secrets) {
@@ -56019,6 +58216,9 @@ var SINGLE_EMOJI_REGEX;
56019
58216
  var init_emoji = __esm(() => {
56020
58217
  SINGLE_EMOJI_REGEX = /^(?:(?:\p{Regional_Indicator}{2})|(?:[#*0-9]\uFE0F?\u20E3)|(?:\p{Extended_Pictographic}(?:\uFE0F|\p{Emoji_Modifier})?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\p{Emoji_Modifier})?)*))$/u;
56021
58218
  });
58219
+ function requestsBinding(binding) {
58220
+ return binding === true || Array.isArray(binding) && binding.length > 0;
58221
+ }
56022
58222
  var HttpUrlSchema;
56023
58223
  var GameEmojiSchema;
56024
58224
  var GameMetadataRecordSchema;
@@ -56026,6 +58226,7 @@ var InsertGameSchema;
56026
58226
  var UpdateGameSchema;
56027
58227
  var InsertGameDeploymentSchema;
56028
58228
  var InsertGameDeployJobSchema;
58229
+ var InsertGameDeploymentStateSchema;
56029
58230
  var UpsertGameMetadataSchema;
56030
58231
  var PatchGameMetadataSchema;
56031
58232
  var AddGameMemberSchema;
@@ -56038,11 +58239,22 @@ var ALLOWED_UPLOAD_EXTENSIONS;
56038
58239
  var InitiateUploadSchema;
56039
58240
  var AddCustomHostnameSchema;
56040
58241
  var SetSecretsRequestSchema;
58242
+ var SecretsDiffRequestSchema;
56041
58243
  var SeedRequestSchema;
56042
58244
  var SchemaInfoSchema;
56043
- var DatabaseResetRequestSchema;
56044
58245
  var VerifyTokenSchema;
56045
58246
  var KVSeedRequestSchema;
58247
+ var DeployMigrationSchema;
58248
+ var DeployDatabaseSchema;
58249
+ var DatabaseResetDatabaseSchema;
58250
+ var DatabaseResetRequestSchema;
58251
+ var BaselineEvidenceSchema;
58252
+ var DeployBaselineSchema;
58253
+ var DeploymentStateBaselineSchema;
58254
+ var MigrationRealignSchema;
58255
+ var MigrationResolveSchema;
58256
+ var DatabaseRestoreSchema;
58257
+ var DeployBlockedReportSchema;
56046
58258
  var DeployRequestSchema;
56047
58259
  var init_schemas2 = __esm(() => {
56048
58260
  init_drizzle_zod();
@@ -56122,6 +58334,9 @@ var init_schemas2 = __esm(() => {
56122
58334
  InsertGameDeployJobSchema = createInsertSchema(gameDeployJobs, {
56123
58335
  status: exports_external.enum(deployJobStatusEnum.enumValues)
56124
58336
  });
58337
+ InsertGameDeploymentStateSchema = createInsertSchema(gameDeploymentState, {
58338
+ secretsManifest: exports_external.record(exports_external.string(), exports_external.string()).nullable().optional()
58339
+ });
56125
58340
  UpsertGameMetadataSchema = exports_external.object({
56126
58341
  displayName: exports_external.string().min(1),
56127
58342
  platform: exports_external.enum(gamePlatformEnum.enumValues),
@@ -56179,6 +58394,9 @@ var init_schemas2 = __esm(() => {
56179
58394
  hostname: exports_external.string().min(1).max(255)
56180
58395
  });
56181
58396
  SetSecretsRequestSchema = exports_external.record(exports_external.string().min(1), exports_external.string());
58397
+ SecretsDiffRequestSchema = exports_external.object({
58398
+ secrets: exports_external.record(exports_external.string().min(1), exports_external.string())
58399
+ });
56182
58400
  SeedRequestSchema = exports_external.object({
56183
58401
  code: exports_external.string().min(1, "Seed code is required"),
56184
58402
  secrets: exports_external.record(exports_external.string(), exports_external.string()).optional()
@@ -56187,9 +58405,6 @@ var init_schemas2 = __esm(() => {
56187
58405
  sql: exports_external.string(),
56188
58406
  hash: exports_external.string()
56189
58407
  });
56190
- DatabaseResetRequestSchema = exports_external.object({
56191
- schema: SchemaInfoSchema.optional()
56192
- });
56193
58408
  VerifyTokenSchema = exports_external.object({
56194
58409
  token: exports_external.string().min(1, "Token is required")
56195
58410
  });
@@ -56201,8 +58416,119 @@ var init_schemas2 = __esm(() => {
56201
58416
  metadata: exports_external.record(exports_external.unknown()).optional()
56202
58417
  }))
56203
58418
  });
58419
+ DeployMigrationSchema = exports_external.object({
58420
+ tag: exports_external.string().min(1),
58421
+ statements: exports_external.array(exports_external.string().min(1)).min(1),
58422
+ checksum: exports_external.string().min(1)
58423
+ });
58424
+ DeployDatabaseSchema = exports_external.discriminatedUnion("mode", [
58425
+ exports_external.object({
58426
+ mode: exports_external.literal("push"),
58427
+ sql: exports_external.string(),
58428
+ baselineHash: exports_external.string().nullable(),
58429
+ nextSnapshot: exports_external.record(exports_external.string(), exports_external.unknown()),
58430
+ nextHash: exports_external.string().min(1),
58431
+ acceptDataLoss: exports_external.boolean().optional()
58432
+ }),
58433
+ exports_external.object({
58434
+ mode: exports_external.literal("migrate"),
58435
+ migrations: exports_external.array(DeployMigrationSchema)
58436
+ })
58437
+ ]);
58438
+ DatabaseResetDatabaseSchema = exports_external.discriminatedUnion("mode", [
58439
+ exports_external.object({
58440
+ mode: exports_external.literal("push"),
58441
+ sql: exports_external.string(),
58442
+ nextSnapshot: exports_external.record(exports_external.string(), exports_external.unknown()),
58443
+ nextHash: exports_external.string().min(1)
58444
+ }),
58445
+ exports_external.object({
58446
+ mode: exports_external.literal("migrate"),
58447
+ migrations: exports_external.array(DeployMigrationSchema)
58448
+ })
58449
+ ]);
58450
+ DatabaseResetRequestSchema = exports_external.object({
58451
+ schema: SchemaInfoSchema.optional(),
58452
+ database: DatabaseResetDatabaseSchema.optional()
58453
+ }).refine((data) => !(data.schema && data.database), {
58454
+ message: "Send either the database payload or the legacy schema field, not both",
58455
+ path: ["database"]
58456
+ });
58457
+ BaselineEvidenceSchema = exports_external.array(exports_external.object({
58458
+ tag: exports_external.string().min(1),
58459
+ generatedAt: exports_external.string().min(1),
58460
+ createsTables: exports_external.array(exports_external.object({
58461
+ name: exports_external.string().min(1),
58462
+ columns: exports_external.array(exports_external.string())
58463
+ })),
58464
+ addsColumns: exports_external.array(exports_external.object({
58465
+ table: exports_external.string().min(1),
58466
+ column: exports_external.string().min(1)
58467
+ })),
58468
+ createsIndexes: exports_external.array(exports_external.string()),
58469
+ createsViews: exports_external.array(exports_external.string())
58470
+ })).optional();
58471
+ DeployBaselineSchema = exports_external.object({
58472
+ lastAppliedMigrationTag: exports_external.string().min(1).optional(),
58473
+ journal: exports_external.array(exports_external.object({
58474
+ tag: exports_external.string().min(1),
58475
+ checksum: exports_external.string().min(1)
58476
+ })).optional(),
58477
+ evidence: BaselineEvidenceSchema,
58478
+ schemaSnapshot: exports_external.unknown().optional(),
58479
+ schemaHash: exports_external.string().min(1).optional(),
58480
+ integrationsHash: exports_external.string().min(1).optional(),
58481
+ buildHash: exports_external.string().min(1).optional()
58482
+ }).refine((data) => !data.lastAppliedMigrationTag === !data.journal, {
58483
+ message: "lastAppliedMigrationTag and journal must be provided together",
58484
+ path: ["journal"]
58485
+ }).refine((data) => data.schemaSnapshot === undefined === !data.schemaHash, {
58486
+ message: "schemaSnapshot and schemaHash must be provided together",
58487
+ path: ["schemaHash"]
58488
+ }).refine((data) => Boolean(data.lastAppliedMigrationTag || data.schemaHash || data.integrationsHash || data.buildHash), {
58489
+ message: "A baseline must claim something — migration history, a schema snapshot, or artifact hashes",
58490
+ path: ["lastAppliedMigrationTag"]
58491
+ });
58492
+ DeploymentStateBaselineSchema = exports_external.object({
58493
+ lastAppliedMigrationTag: exports_external.string().min(1).optional(),
58494
+ journal: exports_external.array(exports_external.object({
58495
+ tag: exports_external.string().min(1),
58496
+ checksum: exports_external.string().min(1)
58497
+ })).optional(),
58498
+ schemaSnapshot: exports_external.unknown().optional(),
58499
+ schemaHash: exports_external.string().min(1).optional(),
58500
+ evidence: BaselineEvidenceSchema,
58501
+ allowUnverified: exports_external.boolean().optional()
58502
+ }).refine((data) => !data.lastAppliedMigrationTag === !data.journal, {
58503
+ message: "lastAppliedMigrationTag and journal must be provided together",
58504
+ path: ["journal"]
58505
+ }).refine((data) => data.schemaSnapshot === undefined === !data.schemaHash, {
58506
+ message: "schemaSnapshot and schemaHash must be provided together",
58507
+ path: ["schemaHash"]
58508
+ }).refine((data) => Boolean(data.lastAppliedMigrationTag) || Boolean(data.schemaHash), {
58509
+ message: "A baseline needs a migration claim, a schema snapshot, or both",
58510
+ path: ["lastAppliedMigrationTag"]
58511
+ });
58512
+ MigrationRealignSchema = exports_external.object({
58513
+ checksum: exports_external.string().min(1)
58514
+ });
58515
+ MigrationResolveSchema = exports_external.object({
58516
+ resolution: exports_external.enum(["applied", "rolled-back"]),
58517
+ checksum: exports_external.string().min(1).optional()
58518
+ }).refine((data) => data.resolution !== "applied" || Boolean(data.checksum), {
58519
+ message: "Resolving a migration as 'applied' requires its checksum",
58520
+ path: ["checksum"]
58521
+ });
58522
+ DatabaseRestoreSchema = exports_external.object({
58523
+ restorePointId: exports_external.string().uuid()
58524
+ });
58525
+ DeployBlockedReportSchema = exports_external.object({
58526
+ code: exports_external.string().regex(/^blocked:[a-z-]+$/).max(64),
58527
+ reason: exports_external.string().min(1).max(2000)
58528
+ });
56204
58529
  DeployRequestSchema = exports_external.object({
56205
58530
  target: exports_external.enum(deploymentTargetEnum.enumValues).optional().default("game"),
58531
+ deployId: exports_external.string().min(1).optional(),
56206
58532
  uploadToken: exports_external.string().optional(),
56207
58533
  code: exports_external.string().optional(),
56208
58534
  codeUploadToken: exports_external.string().optional(),
@@ -56229,6 +58555,11 @@ var init_schemas2 = __esm(() => {
56229
58555
  sql: exports_external.string(),
56230
58556
  hash: exports_external.string()
56231
58557
  }).optional(),
58558
+ database: DeployDatabaseSchema.optional(),
58559
+ baseline: DeployBaselineSchema.optional(),
58560
+ buildHash: exports_external.string().min(1).optional(),
58561
+ integrationsHash: exports_external.string().min(1).optional(),
58562
+ pruneSecrets: exports_external.array(exports_external.string().min(1)).optional(),
56232
58563
  metadata: exports_external.object({
56233
58564
  displayName: exports_external.string().optional(),
56234
58565
  description: exports_external.string().optional(),
@@ -56238,9 +58569,24 @@ var init_schemas2 = __esm(() => {
56238
58569
  }).refine((data) => !(data.code && data.codeUploadToken), {
56239
58570
  message: "Specify either code or codeUploadToken, not both",
56240
58571
  path: ["codeUploadToken"]
56241
- }).refine((data) => !(data.target === "dashboard" && (data.schema || data.bindings)), {
56242
- message: "Dashboard deployments cannot include schema or bindings — they attach to the game deployment’s existing resources",
58572
+ }).refine((data) => !(data.target === "dashboard" && (data.schema || data.bindings || data.database)), {
58573
+ message: "Dashboard deployments cannot include schema, database, or bindings — they attach to the game deployment’s existing resources",
56243
58574
  path: ["target"]
58575
+ }).refine((data) => !(data.target === "dashboard" && (data.baseline || data.pruneSecrets)), {
58576
+ message: "Dashboard deployments cannot adopt a baseline or prune secrets",
58577
+ path: ["target"]
58578
+ }).refine((data) => !(data.database && !data.deployId), {
58579
+ message: "deployId is required when a database payload is present",
58580
+ path: ["deployId"]
58581
+ }).refine((data) => !(data.database && data.schema), {
58582
+ message: "Send either the database payload or the legacy schema field, not both",
58583
+ path: ["database"]
58584
+ }).refine((data) => !(data.baseline && data.schema), {
58585
+ message: "The legacy schema field cannot ride a baseline-adopting deploy",
58586
+ path: ["baseline"]
58587
+ }).refine((data) => !(data.database && !requestsBinding(data.bindings?.database)), {
58588
+ message: "The database payload requires a database binding",
58589
+ path: ["database"]
56244
58590
  });
56245
58591
  });
56246
58592
  var LeaderboardQuerySchema;
@@ -101209,7 +103555,7 @@ var init_pure = __esm(() => {
101209
103555
  init_log();
101210
103556
  init_spinner();
101211
103557
  });
101212
- var init_src4 = __esm(() => {
103558
+ var init_src5 = __esm(() => {
101213
103559
  init_pure();
101214
103560
  });
101215
103561
  function createOneRosterUrls(baseUrl) {
@@ -103185,7 +105531,7 @@ var init_dist5 = __esm(async () => {
103185
105531
  init_spans();
103186
105532
  init_src();
103187
105533
  init_spans();
103188
- init_src4();
105534
+ init_src5();
103189
105535
  init_src();
103190
105536
  init_spans();
103191
105537
  init_spans();
@@ -103760,7 +106106,7 @@ function selectTimebackMetricDiscrepancyQueueItems(candidates, options) {
103760
106106
  }
103761
106107
  var DATE_INPUT_RE;
103762
106108
  var init_timeback_discrepancy_queue_util = __esm(() => {
103763
- init_src4();
106109
+ init_src5();
103764
106110
  init_timeback_util();
103765
106111
  DATE_INPUT_RE = /^\d{4}-\d{2}-\d{2}$/;
103766
106112
  });
@@ -106273,7 +108619,7 @@ var init_timeback_admin_service = __esm(async () => {
106273
108619
  init_constants3();
106274
108620
  init_types2();
106275
108621
  init_utils6();
106276
- init_src4();
108622
+ init_src5();
106277
108623
  init_timeback3();
106278
108624
  init_errors();
106279
108625
  init_timeback_admin_metrics_util();
@@ -108707,8 +111053,15 @@ function createPlatformServices(deps) {
108707
111053
  validateDeveloperAccess
108708
111054
  });
108709
111055
  const kv = new KVService({ db: db2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
108710
- const secrets = new SecretsService({ config: config4, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
111056
+ const secrets = new SecretsService({ db: db2, config: config4, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
108711
111057
  const domain3 = new DomainService({ db: db2, cloudflare: cloudflare2, corsKvs, validateDeveloperAccessBySlug });
111058
+ const deploymentState = new DeploymentStateService({
111059
+ db: db2,
111060
+ config: config4,
111061
+ cloudflare: cloudflare2,
111062
+ alerts,
111063
+ validateDeveloperAccessBySlug
111064
+ });
108712
111065
  const database = new DatabaseService({
108713
111066
  db: db2,
108714
111067
  config: config4,
@@ -108747,6 +111100,7 @@ function createPlatformServices(deps) {
108747
111100
  kv,
108748
111101
  secrets,
108749
111102
  domain: domain3,
111103
+ deploymentState,
108750
111104
  database,
108751
111105
  seed,
108752
111106
  timeback: timeback2,
@@ -108757,6 +111111,7 @@ function createPlatformServices(deps) {
108757
111111
  var init_platform2 = __esm(async () => {
108758
111112
  init_bucket_service();
108759
111113
  init_database_service();
111114
+ init_deployment_state_service();
108760
111115
  init_domain_service();
108761
111116
  init_kv_service();
108762
111117
  init_secrets_service();
@@ -109474,7 +111829,7 @@ function createServices(ctx) {
109474
111829
  };
109475
111830
  }
109476
111831
  var init_factory = __esm(async () => {
109477
- init_game2();
111832
+ init_game3();
109478
111833
  init_infra2();
109479
111834
  init_player();
109480
111835
  init_standalone();
@@ -109767,6 +112122,7 @@ function buildConfig(options) {
109767
112122
  gameDomain: "localhost",
109768
112123
  uploadBucket: "sandbox-uploads",
109769
112124
  ltiTestMode: true,
112125
+ secretsManifestPepper: "sandbox-secrets-manifest-pepper",
109770
112126
  ...options.config
109771
112127
  });
109772
112128
  }
@@ -127811,7 +130167,7 @@ var _a27;
127811
130167
  var _b12;
127812
130168
  var _c2;
127813
130169
  var View3;
127814
- var init_sql3;
130170
+ var init_sql4;
127815
130171
  var _a28;
127816
130172
  var ColumnAliasProxyHandler2;
127817
130173
  var _a29;
@@ -128304,7 +130660,7 @@ var PgSequence;
128304
130660
  var init_sequence2;
128305
130661
  var _a164;
128306
130662
  var PgSchema5;
128307
- var init_schema3;
130663
+ var init_schema4;
128308
130664
  var _a165;
128309
130665
  var Cache;
128310
130666
  var _a166;
@@ -149783,7 +152139,7 @@ globstar while`, file3, fr, pattern, pr2, swallowee);
149783
152139
  });
149784
152140
  }
149785
152141
  });
149786
- init_sql3 = __esm2({
152142
+ init_sql4 = __esm2({
149787
152143
  "../drizzle-orm/dist/sql/sql.js"() {
149788
152144
  init_entity2();
149789
152145
  init_enum2();
@@ -150136,7 +152492,7 @@ globstar while`, file3, fr, pattern, pr2, swallowee);
150136
152492
  "../drizzle-orm/dist/alias.js"() {
150137
152493
  init_column2();
150138
152494
  init_entity2();
150139
- init_sql3();
152495
+ init_sql4();
150140
152496
  init_table8();
150141
152497
  init_view_common3();
150142
152498
  _a28 = entityKind2;
@@ -150310,7 +152666,7 @@ params: ${params}`);
150310
152666
  "../drizzle-orm/dist/utils.js"() {
150311
152667
  init_column2();
150312
152668
  init_entity2();
150313
- init_sql3();
152669
+ init_sql4();
150314
152670
  init_subquery2();
150315
152671
  init_table8();
150316
152672
  init_view_common3();
@@ -150569,7 +152925,7 @@ params: ${params}`);
150569
152925
  init_date_common2 = __esm2({
150570
152926
  "../drizzle-orm/dist/pg-core/columns/date.common.js"() {
150571
152927
  init_entity2();
150572
- init_sql3();
152928
+ init_sql4();
150573
152929
  init_common22();
150574
152930
  PgDateColumnBaseBuilder2 = class extends (_b33 = PgColumnBuilder2, _a54 = entityKind2, _b33) {
150575
152931
  defaultNow() {
@@ -151354,7 +153710,7 @@ params: ${params}`);
151354
153710
  init_uuid3 = __esm2({
151355
153711
  "../drizzle-orm/dist/pg-core/columns/uuid.js"() {
151356
153712
  init_entity2();
151357
- init_sql3();
153713
+ init_sql4();
151358
153714
  init_common22();
151359
153715
  PgUUIDBuilder2 = class extends (_b88 = PgColumnBuilder2, _a109 = entityKind2, _b88) {
151360
153716
  constructor(name22) {
@@ -151625,7 +153981,7 @@ params: ${params}`);
151625
153981
  init_column2();
151626
153982
  init_entity2();
151627
153983
  init_table8();
151628
- init_sql3();
153984
+ init_sql4();
151629
153985
  eq2 = (left, right) => {
151630
153986
  return sql4`${left} = ${bindIfParam2(right, left)}`;
151631
153987
  };
@@ -151648,7 +154004,7 @@ params: ${params}`);
151648
154004
  });
151649
154005
  init_select3 = __esm2({
151650
154006
  "../drizzle-orm/dist/sql/expressions/select.js"() {
151651
- init_sql3();
154007
+ init_sql4();
151652
154008
  }
151653
154009
  });
151654
154010
  init_expressions2 = __esm2({
@@ -151664,7 +154020,7 @@ params: ${params}`);
151664
154020
  init_entity2();
151665
154021
  init_primary_keys2();
151666
154022
  init_expressions2();
151667
- init_sql3();
154023
+ init_sql4();
151668
154024
  _a124 = entityKind2;
151669
154025
  Relation2 = class {
151670
154026
  constructor(sourceTable, referencedTable, relationName) {
@@ -151718,12 +154074,12 @@ params: ${params}`);
151718
154074
  "../drizzle-orm/dist/sql/functions/aggregate.js"() {
151719
154075
  init_column2();
151720
154076
  init_entity2();
151721
- init_sql3();
154077
+ init_sql4();
151722
154078
  }
151723
154079
  });
151724
154080
  init_vector22 = __esm2({
151725
154081
  "../drizzle-orm/dist/sql/functions/vector.js"() {
151726
- init_sql3();
154082
+ init_sql4();
151727
154083
  }
151728
154084
  });
151729
154085
  init_functions2 = __esm2({
@@ -151736,7 +154092,7 @@ params: ${params}`);
151736
154092
  "../drizzle-orm/dist/sql/index.js"() {
151737
154093
  init_expressions2();
151738
154094
  init_functions2();
151739
- init_sql3();
154095
+ init_sql4();
151740
154096
  }
151741
154097
  });
151742
154098
  dist_exports = {};
@@ -151953,7 +154309,7 @@ params: ${params}`);
151953
154309
  init_alias3();
151954
154310
  init_column2();
151955
154311
  init_entity2();
151956
- init_sql3();
154312
+ init_sql4();
151957
154313
  init_subquery2();
151958
154314
  init_view_common3();
151959
154315
  _a130 = entityKind2;
@@ -152012,7 +154368,7 @@ params: ${params}`);
152012
154368
  });
152013
154369
  init_indexes2 = __esm2({
152014
154370
  "../drizzle-orm/dist/pg-core/indexes.js"() {
152015
- init_sql3();
154371
+ init_sql4();
152016
154372
  init_entity2();
152017
154373
  init_columns2();
152018
154374
  _a131 = entityKind2;
@@ -152175,7 +154531,7 @@ params: ${params}`);
152175
154531
  init_view_base2 = __esm2({
152176
154532
  "../drizzle-orm/dist/pg-core/view-base.js"() {
152177
154533
  init_entity2();
152178
- init_sql3();
154534
+ init_sql4();
152179
154535
  PgViewBase2 = class extends (_b103 = View3, _a136 = entityKind2, _b103) {
152180
154536
  };
152181
154537
  __publicField(PgViewBase2, _a136, "PgViewBase");
@@ -152192,7 +154548,7 @@ params: ${params}`);
152192
154548
  init_table22();
152193
154549
  init_relations2();
152194
154550
  init_sql22();
152195
- init_sql3();
154551
+ init_sql4();
152196
154552
  init_subquery2();
152197
154553
  init_table8();
152198
154554
  init_utils22();
@@ -152776,7 +155132,7 @@ params: ${params}`);
152776
155132
  init_query_builder3();
152777
155133
  init_query_promise2();
152778
155134
  init_selection_proxy2();
152779
- init_sql3();
155135
+ init_sql4();
152780
155136
  init_subquery2();
152781
155137
  init_table8();
152782
155138
  init_tracing2();
@@ -153397,7 +155753,7 @@ params: ${params}`);
153397
155753
  "../drizzle-orm/dist/pg-core/utils.js"() {
153398
155754
  init_entity2();
153399
155755
  init_table22();
153400
- init_sql3();
155756
+ init_sql4();
153401
155757
  init_subquery2();
153402
155758
  init_table8();
153403
155759
  init_view_common3();
@@ -153485,7 +155841,7 @@ params: ${params}`);
153485
155841
  init_entity2();
153486
155842
  init_query_promise2();
153487
155843
  init_selection_proxy2();
153488
- init_sql3();
155844
+ init_sql4();
153489
155845
  init_table8();
153490
155846
  init_tracing2();
153491
155847
  init_utils22();
@@ -153679,7 +156035,7 @@ params: ${params}`);
153679
156035
  init_table22();
153680
156036
  init_query_promise2();
153681
156037
  init_selection_proxy2();
153682
- init_sql3();
156038
+ init_sql4();
153683
156039
  init_subquery2();
153684
156040
  init_table8();
153685
156041
  init_utils22();
@@ -153853,7 +156209,7 @@ params: ${params}`);
153853
156209
  init_count2 = __esm2({
153854
156210
  "../drizzle-orm/dist/pg-core/query-builders/count.js"() {
153855
156211
  init_entity2();
153856
- init_sql3();
156212
+ init_sql4();
153857
156213
  _PgCountBuilder = class _PgCountBuilder2 extends (_c6 = SQL2, _b116 = entityKind2, _a157 = Symbol.toStringTag, _c6) {
153858
156214
  constructor(params) {
153859
156215
  super(_PgCountBuilder2.buildEmbeddedCount(params.source, params.filters).queryChunks);
@@ -154021,7 +156377,7 @@ params: ${params}`);
154021
156377
  init_entity2();
154022
156378
  init_query_builders2();
154023
156379
  init_selection_proxy2();
154024
- init_sql3();
156380
+ init_sql4();
154025
156381
  init_subquery2();
154026
156382
  init_count2();
154027
156383
  init_query2();
@@ -154193,10 +156549,10 @@ params: ${params}`);
154193
156549
  __publicField(PgSequence, _a163, "PgSequence");
154194
156550
  }
154195
156551
  });
154196
- init_schema3 = __esm2({
156552
+ init_schema4 = __esm2({
154197
156553
  "../drizzle-orm/dist/pg-core/schema.js"() {
154198
156554
  init_entity2();
154199
- init_sql3();
156555
+ init_sql4();
154200
156556
  init_enum2();
154201
156557
  init_sequence2();
154202
156558
  init_table22();
@@ -154412,7 +156768,7 @@ params: ${params}`);
154412
156768
  init_primary_keys2();
154413
156769
  init_query_builders2();
154414
156770
  init_roles2();
154415
- init_schema3();
156771
+ init_schema4();
154416
156772
  init_sequence2();
154417
156773
  init_session3();
154418
156774
  init_subquery22();
@@ -156220,7 +158576,7 @@ ORDER BY
156220
158576
  init_integer22 = __esm2({
156221
158577
  "../drizzle-orm/dist/sqlite-core/columns/integer.js"() {
156222
158578
  init_entity2();
156223
- init_sql3();
158579
+ init_sql4();
156224
158580
  init_utils22();
156225
158581
  init_common3();
156226
158582
  SQLiteBaseIntegerBuilder = class extends (_b131 = SQLiteColumnBuilder, _a187 = entityKind2, _b131) {
@@ -156583,7 +158939,7 @@ ORDER BY
156583
158939
  init_utils72 = __esm2({
156584
158940
  "../drizzle-orm/dist/sqlite-core/utils.js"() {
156585
158941
  init_entity2();
156586
- init_sql3();
158942
+ init_sql4();
156587
158943
  init_subquery2();
156588
158944
  init_table8();
156589
158945
  init_view_common3();
@@ -156677,7 +159033,7 @@ ORDER BY
156677
159033
  init_view_base22 = __esm2({
156678
159034
  "../drizzle-orm/dist/sqlite-core/view-base.js"() {
156679
159035
  init_entity2();
156680
- init_sql3();
159036
+ init_sql4();
156681
159037
  SQLiteViewBase = class extends (_b153 = View3, _a214 = entityKind2, _b153) {
156682
159038
  };
156683
159039
  __publicField(SQLiteViewBase, _a214, "SQLiteViewBase");
@@ -156692,7 +159048,7 @@ ORDER BY
156692
159048
  init_errors22();
156693
159049
  init_relations2();
156694
159050
  init_sql22();
156695
- init_sql3();
159051
+ init_sql4();
156696
159052
  init_columns22();
156697
159053
  init_table32();
156698
159054
  init_subquery2();
@@ -157272,7 +159628,7 @@ ORDER BY
157272
159628
  init_query_builder3();
157273
159629
  init_query_promise2();
157274
159630
  init_selection_proxy2();
157275
- init_sql3();
159631
+ init_sql4();
157276
159632
  init_subquery2();
157277
159633
  init_table8();
157278
159634
  init_utils22();
@@ -157635,7 +159991,7 @@ ORDER BY
157635
159991
  "../drizzle-orm/dist/sqlite-core/query-builders/insert.js"() {
157636
159992
  init_entity2();
157637
159993
  init_query_promise2();
157638
- init_sql3();
159994
+ init_sql4();
157639
159995
  init_table32();
157640
159996
  init_table8();
157641
159997
  init_utils22();
@@ -157882,7 +160238,7 @@ ORDER BY
157882
160238
  init_count22 = __esm2({
157883
160239
  "../drizzle-orm/dist/sqlite-core/query-builders/count.js"() {
157884
160240
  init_entity2();
157885
- init_sql3();
160241
+ init_sql4();
157886
160242
  _SQLiteCountBuilder = class _SQLiteCountBuilder2 extends (_c8 = SQL2, _b160 = entityKind2, _a226 = Symbol.toStringTag, _c8) {
157887
160243
  constructor(params) {
157888
160244
  super(_SQLiteCountBuilder2.buildEmbeddedCount(params.source, params.filters).queryChunks);
@@ -158051,7 +160407,7 @@ ORDER BY
158051
160407
  "../drizzle-orm/dist/sqlite-core/db.js"() {
158052
160408
  init_entity2();
158053
160409
  init_selection_proxy2();
158054
- init_sql3();
160410
+ init_sql4();
158055
160411
  init_query_builders22();
158056
160412
  init_subquery2();
158057
160413
  init_count22();
@@ -160022,7 +162378,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
160022
162378
  init_date_common22 = __esm2({
160023
162379
  "../drizzle-orm/dist/mysql-core/columns/date.common.js"() {
160024
162380
  init_entity2();
160025
- init_sql3();
162381
+ init_sql4();
160026
162382
  init_common4();
160027
162383
  MySqlDateColumnBaseBuilder = class extends (_b223 = MySqlColumnBuilder, _a301 = entityKind2, _b223) {
160028
162384
  defaultNow() {
@@ -160248,7 +162604,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
160248
162604
  init_count3 = __esm2({
160249
162605
  "../drizzle-orm/dist/mysql-core/query-builders/count.js"() {
160250
162606
  init_entity2();
160251
- init_sql3();
162607
+ init_sql4();
160252
162608
  _MySqlCountBuilder = class _MySqlCountBuilder2 extends (_c9 = SQL2, _b237 = entityKind2, _a315 = Symbol.toStringTag, _c9) {
160253
162609
  constructor(params) {
160254
162610
  super(_MySqlCountBuilder2.buildEmbeddedCount(params.source, params.filters).queryChunks);
@@ -160510,7 +162866,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
160510
162866
  init_view_base3 = __esm2({
160511
162867
  "../drizzle-orm/dist/mysql-core/view-base.js"() {
160512
162868
  init_entity2();
160513
- init_sql3();
162869
+ init_sql4();
160514
162870
  MySqlViewBase = class extends (_b240 = View3, _a323 = entityKind2, _b240) {
160515
162871
  };
160516
162872
  __publicField(MySqlViewBase, _a323, "MySqlViewBase");
@@ -160525,7 +162881,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
160525
162881
  init_errors22();
160526
162882
  init_relations2();
160527
162883
  init_expressions2();
160528
- init_sql3();
162884
+ init_sql4();
160529
162885
  init_subquery2();
160530
162886
  init_table8();
160531
162887
  init_utils22();
@@ -161291,7 +163647,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
161291
163647
  init_query_builder3();
161292
163648
  init_query_promise2();
161293
163649
  init_selection_proxy2();
161294
- init_sql3();
163650
+ init_sql4();
161295
163651
  init_subquery2();
161296
163652
  init_table8();
161297
163653
  init_utils22();
@@ -161692,7 +164048,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
161692
164048
  "../drizzle-orm/dist/mysql-core/query-builders/insert.js"() {
161693
164049
  init_entity2();
161694
164050
  init_query_promise2();
161695
- init_sql3();
164051
+ init_sql4();
161696
164052
  init_table8();
161697
164053
  init_utils22();
161698
164054
  init_utils8();
@@ -161972,7 +164328,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
161972
164328
  "../drizzle-orm/dist/mysql-core/db.js"() {
161973
164329
  init_entity2();
161974
164330
  init_selection_proxy2();
161975
- init_sql3();
164331
+ init_sql4();
161976
164332
  init_subquery2();
161977
164333
  init_count3();
161978
164334
  init_query_builders3();
@@ -162201,7 +164557,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
162201
164557
  init_cache();
162202
164558
  init_entity2();
162203
164559
  init_errors22();
162204
- init_sql3();
164560
+ init_sql4();
162205
164561
  init_db3();
162206
164562
  _a341 = entityKind2;
162207
164563
  MySqlPreparedQuery = class {
@@ -164224,7 +166580,7 @@ AND
164224
166580
  init_date_common3 = __esm2({
164225
166581
  "../drizzle-orm/dist/singlestore-core/columns/date.common.js"() {
164226
166582
  init_entity2();
164227
- init_sql3();
166583
+ init_sql4();
164228
166584
  init_common5();
164229
166585
  SingleStoreDateColumnBaseBuilder = class extends (_b302 = SingleStoreColumnBuilder, _a399 = entityKind2, _b302) {
164230
166586
  defaultNow() {
@@ -164249,7 +166605,7 @@ AND
164249
166605
  init_timestamp3 = __esm2({
164250
166606
  "../drizzle-orm/dist/singlestore-core/columns/timestamp.js"() {
164251
166607
  init_entity2();
164252
- init_sql3();
166608
+ init_sql4();
164253
166609
  init_utils22();
164254
166610
  init_date_common3();
164255
166611
  SingleStoreTimestampBuilder = class extends (_b304 = SingleStoreDateColumnBaseBuilder, _a401 = entityKind2, _b304) {
@@ -164484,7 +166840,7 @@ AND
164484
166840
  init_count4 = __esm2({
164485
166841
  "../drizzle-orm/dist/singlestore-core/query-builders/count.js"() {
164486
166842
  init_entity2();
164487
- init_sql3();
166843
+ init_sql4();
164488
166844
  _SingleStoreCountBuilder = class _SingleStoreCountBuilder2 extends (_c12 = SQL2, _b318 = entityKind2, _a415 = Symbol.toStringTag, _c12) {
164489
166845
  constructor(params) {
164490
166846
  super(_SingleStoreCountBuilder2.buildEmbeddedCount(params.source, params.filters).queryChunks);
@@ -164654,7 +167010,7 @@ AND
164654
167010
  init_utils10 = __esm2({
164655
167011
  "../drizzle-orm/dist/singlestore-core/utils.js"() {
164656
167012
  init_entity2();
164657
- init_sql3();
167013
+ init_sql4();
164658
167014
  init_subquery2();
164659
167015
  init_table8();
164660
167016
  init_indexes4();
@@ -164732,7 +167088,7 @@ AND
164732
167088
  "../drizzle-orm/dist/singlestore-core/query-builders/insert.js"() {
164733
167089
  init_entity2();
164734
167090
  init_query_promise2();
164735
- init_sql3();
167091
+ init_sql4();
164736
167092
  init_table8();
164737
167093
  init_utils22();
164738
167094
  init_utils10();
@@ -164829,7 +167185,7 @@ AND
164829
167185
  init_errors22();
164830
167186
  init_relations2();
164831
167187
  init_expressions2();
164832
- init_sql3();
167188
+ init_sql4();
164833
167189
  init_subquery2();
164834
167190
  init_table8();
164835
167191
  init_utils22();
@@ -165360,7 +167716,7 @@ AND
165360
167716
  init_query_builder3();
165361
167717
  init_query_promise2();
165362
167718
  init_selection_proxy2();
165363
- init_sql3();
167719
+ init_sql4();
165364
167720
  init_subquery2();
165365
167721
  init_table8();
165366
167722
  init_utils22();
@@ -165818,7 +168174,7 @@ AND
165818
168174
  "../drizzle-orm/dist/singlestore-core/db.js"() {
165819
168175
  init_entity2();
165820
168176
  init_selection_proxy2();
165821
- init_sql3();
168177
+ init_sql4();
165822
168178
  init_subquery2();
165823
168179
  init_count4();
165824
168180
  init_query_builders4();
@@ -165932,7 +168288,7 @@ AND
165932
168288
  init_cache();
165933
168289
  init_entity2();
165934
168290
  init_errors22();
165935
- init_sql3();
168291
+ init_sql4();
165936
168292
  init_db4();
165937
168293
  _a434 = entityKind2;
165938
168294
  SingleStorePreparedQuery = class {
@@ -168168,7 +170524,7 @@ function requireUserId(userId) {
168168
170524
  return userId;
168169
170525
  }
168170
170526
  var init_params_util = __esm(() => {
168171
- init_src4();
170527
+ init_src5();
168172
170528
  init_errors();
168173
170529
  });
168174
170530
  function formatZodError(error89) {
@@ -168218,6 +170574,7 @@ var init_utils11 = __esm(() => {
168218
170574
  init_lti_util();
168219
170575
  init_lti_provisioning();
168220
170576
  init_params_util();
170577
+ init_secrets_util();
168221
170578
  init_timeback_util();
168222
170579
  init_validation_util();
168223
170580
  });
@@ -168414,7 +170771,7 @@ var init_database_controller = __esm(() => {
168414
170771
  throw ApiError.unprocessableEntity("Validation failed", details);
168415
170772
  }
168416
170773
  }
168417
- return ctx.services.database.reset(slug2, ctx.user, body2.schema);
170774
+ return ctx.services.database.reset(slug2, ctx.user, body2);
168418
170775
  });
168419
170776
  database = defineControllerNames("database", {
168420
170777
  reset
@@ -168464,6 +170821,91 @@ var init_deploy_controller = __esm(() => {
168464
170821
  getJob: requireDeveloper(getJob)
168465
170822
  });
168466
170823
  });
170824
+ var get;
170825
+ var baseline;
170826
+ var realignMigration;
170827
+ var resolveMigration;
170828
+ var history;
170829
+ var restorePoints;
170830
+ var restore;
170831
+ var reportBlocked;
170832
+ var deploymentState;
170833
+ var init_deployment_state_controller = __esm(() => {
170834
+ init_schemas_index();
170835
+ init_errors();
170836
+ init_utils11();
170837
+ get = requireDeveloper(async (ctx) => {
170838
+ const slug2 = requireSlug(ctx.params.slug);
170839
+ const include = ctx.url.searchParams.getAll("include").flatMap((value) => value.split(",")).filter(Boolean);
170840
+ const unsupported = include.filter((value) => value !== "schemaSnapshot");
170841
+ if (unsupported.length > 0) {
170842
+ throw ApiError.badRequest(`Unsupported include value(s): ${unsupported.join(", ")}`);
170843
+ }
170844
+ return ctx.services.deploymentState.get(slug2, ctx.user, {
170845
+ includeSchemaSnapshot: include.includes("schemaSnapshot")
170846
+ });
170847
+ });
170848
+ baseline = requireDeveloper(async (ctx) => {
170849
+ const slug2 = requireSlug(ctx.params.slug);
170850
+ const body2 = await parseRequestBody(ctx.request, DeploymentStateBaselineSchema);
170851
+ return ctx.services.deploymentState.baseline(slug2, body2, ctx.user);
170852
+ });
170853
+ realignMigration = requireDeveloper(async (ctx) => {
170854
+ const slug2 = requireSlug(ctx.params.slug);
170855
+ const tag = ctx.params.tag;
170856
+ if (!tag) {
170857
+ throw ApiError.badRequest("Missing migration tag");
170858
+ }
170859
+ const body2 = await parseRequestBody(ctx.request, MigrationRealignSchema);
170860
+ return ctx.services.deploymentState.realignMigration(slug2, tag, body2.checksum, ctx.user);
170861
+ });
170862
+ resolveMigration = requireDeveloper(async (ctx) => {
170863
+ const slug2 = requireSlug(ctx.params.slug);
170864
+ const tag = ctx.params.tag;
170865
+ if (!tag) {
170866
+ throw ApiError.badRequest("Missing migration tag");
170867
+ }
170868
+ const body2 = await parseRequestBody(ctx.request, MigrationResolveSchema);
170869
+ return ctx.services.deploymentState.resolveMigration(slug2, tag, body2, ctx.user);
170870
+ });
170871
+ history = requireDeveloper(async (ctx) => {
170872
+ const slug2 = requireSlug(ctx.params.slug);
170873
+ const limitParam = ctx.url.searchParams.get("limit");
170874
+ let limit;
170875
+ if (limitParam !== null) {
170876
+ limit = Number(limitParam);
170877
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
170878
+ throw ApiError.badRequest("limit must be an integer between 1 and 100");
170879
+ }
170880
+ }
170881
+ return ctx.services.deploymentState.history(slug2, ctx.user, { limit });
170882
+ });
170883
+ restorePoints = requireDeveloper(async (ctx) => {
170884
+ const slug2 = requireSlug(ctx.params.slug);
170885
+ return ctx.services.deploymentState.restorePoints(slug2, ctx.user);
170886
+ });
170887
+ restore = requireDeveloper(async (ctx) => {
170888
+ const slug2 = requireSlug(ctx.params.slug);
170889
+ const body2 = await parseRequestBody(ctx.request, DatabaseRestoreSchema);
170890
+ return ctx.services.deploymentState.restoreToBookmark(slug2, body2, ctx.user);
170891
+ });
170892
+ reportBlocked = requireDeveloper(async (ctx) => {
170893
+ const slug2 = requireSlug(ctx.params.slug);
170894
+ const body2 = await parseRequestBody(ctx.request, DeployBlockedReportSchema);
170895
+ await ctx.services.deploymentState.reportDeployBlocked(slug2, ctx.user, body2);
170896
+ return { recorded: true };
170897
+ });
170898
+ deploymentState = defineControllerNames("deploymentState", {
170899
+ get,
170900
+ baseline,
170901
+ realignMigration,
170902
+ resolveMigration,
170903
+ history,
170904
+ restorePoints,
170905
+ restore,
170906
+ reportBlocked
170907
+ });
170908
+ });
168467
170909
  var apply;
168468
170910
  var getStatus;
168469
170911
  var developer;
@@ -168953,6 +171395,7 @@ var init_lti_controller = __esm(() => {
168953
171395
  var listKeys2;
168954
171396
  var setSecrets;
168955
171397
  var deleteSecret;
171398
+ var diff;
168956
171399
  var secrets;
168957
171400
  var init_secrets_controller = __esm(() => {
168958
171401
  init_esm();
@@ -168999,10 +171442,19 @@ var init_secrets_controller = __esm(() => {
168999
171442
  await ctx.services.secrets.deleteSecret(slug2, key, ctx.user);
169000
171443
  return { success: true };
169001
171444
  });
171445
+ diff = requireDeveloper(async (ctx) => {
171446
+ const slug2 = ctx.params.slug;
171447
+ if (!slug2) {
171448
+ throw ApiError.badRequest("Missing game slug");
171449
+ }
171450
+ const body2 = await parseRequestBody(ctx.request, SecretsDiffRequestSchema);
171451
+ return ctx.services.secrets.diff(slug2, body2.secrets, ctx.user);
171452
+ });
169002
171453
  secrets = defineControllerNames("secrets", {
169003
171454
  listKeys: listKeys2,
169004
171455
  setSecrets,
169005
- deleteSecret
171456
+ deleteSecret,
171457
+ diff
169006
171458
  });
169007
171459
  });
169008
171460
  var seed2;
@@ -169126,7 +171578,7 @@ var timeback2;
169126
171578
  var init_timeback_controller = __esm(() => {
169127
171579
  init_esm();
169128
171580
  init_schemas_index();
169129
- init_src4();
171581
+ init_src5();
169130
171582
  init_timeback3();
169131
171583
  init_errors();
169132
171584
  init_utils11();
@@ -169889,6 +172341,7 @@ var init_controllers = __esm(() => {
169889
172341
  init_dashboard_controller();
169890
172342
  init_database_controller();
169891
172343
  init_deploy_controller();
172344
+ init_deployment_state_controller();
169892
172345
  init_developer_controller();
169893
172346
  init_domain_controller();
169894
172347
  init_game_member_controller();
@@ -170409,6 +172862,21 @@ var init_deploy = __esm(async () => {
170409
172862
  });
170410
172863
  });
170411
172864
  });
172865
+ var gameDeploymentStateRouter;
172866
+ var init_deployment_state = __esm(async () => {
172867
+ init_dist7();
172868
+ init_controllers();
172869
+ await init_api3();
172870
+ gameDeploymentStateRouter = new Hono2;
172871
+ gameDeploymentStateRouter.get("/:slug/deployment-state", handle2(deploymentState.get));
172872
+ gameDeploymentStateRouter.post("/:slug/deployment-state/baseline", handle2(deploymentState.baseline));
172873
+ gameDeploymentStateRouter.post("/:slug/deployment-state/migrations/:tag/realign", handle2(deploymentState.realignMigration));
172874
+ gameDeploymentStateRouter.post("/:slug/deployment-state/migrations/:tag/resolve", handle2(deploymentState.resolveMigration));
172875
+ gameDeploymentStateRouter.get("/:slug/deployments", handle2(deploymentState.history));
172876
+ gameDeploymentStateRouter.post("/:slug/deployments/blocked", handle2(deploymentState.reportBlocked));
172877
+ gameDeploymentStateRouter.get("/:slug/database/restore-points", handle2(deploymentState.restorePoints));
172878
+ gameDeploymentStateRouter.post("/:slug/database/restore", handle2(deploymentState.restore));
172879
+ });
170412
172880
  var gameDomainsRouter;
170413
172881
  var init_domains2 = __esm(async () => {
170414
172882
  init_dist7();
@@ -170447,6 +172915,7 @@ var init_secrets = __esm(async () => {
170447
172915
  gameSecretsRouter = new Hono2;
170448
172916
  gameSecretsRouter.get("/:slug/secrets", handle2(secrets.listKeys));
170449
172917
  gameSecretsRouter.post("/:slug/secrets", handle2(secrets.setSecrets));
172918
+ gameSecretsRouter.post("/:slug/secrets/diff", handle2(secrets.diff));
170450
172919
  gameSecretsRouter.delete("/:slug/secrets/:key", handle2(secrets.deleteSecret));
170451
172920
  });
170452
172921
  var gameSeedRouter;
@@ -170641,6 +173110,7 @@ var init_games2 = __esm(async () => {
170641
173110
  await __promiseAll([
170642
173111
  init_crud(),
170643
173112
  init_deploy(),
173113
+ init_deployment_state(),
170644
173114
  init_domains2(),
170645
173115
  init_logs(),
170646
173116
  init_scores(),
@@ -170655,6 +173125,7 @@ var init_games2 = __esm(async () => {
170655
173125
  gamesRouter.route("/", gameVerifyRouter);
170656
173126
  gamesRouter.route("/", gameUploadsRouter);
170657
173127
  gamesRouter.route("/", gameDeployRouter);
173128
+ gamesRouter.route("/", gameDeploymentStateRouter);
170658
173129
  gamesRouter.route("/", gameDomainsRouter);
170659
173130
  gamesRouter.route("/", gameLogsRouter);
170660
173131
  gamesRouter.route("/", gameScoresRouter);
@@ -171586,7 +174057,7 @@ var init_domains3 = __esm3(() => {
171586
174057
  });
171587
174058
  var init_env_vars2 = () => {};
171588
174059
  var GAME_MEMBER_ROLES2;
171589
- var init_game3 = __esm3(() => {
174060
+ var init_game4 = __esm3(() => {
171590
174061
  GAME_MEMBER_ROLES2 = {
171591
174062
  OWNER: "owner",
171592
174063
  COLLABORATOR: "collaborator"
@@ -171661,13 +174132,13 @@ var init_cloudflare2 = __esm3(() => {
171661
174132
  LOCAL_PREFIX: "local-"
171662
174133
  };
171663
174134
  });
171664
- var init_src5 = __esm3(() => {
174135
+ var init_src6 = __esm3(() => {
171665
174136
  init_auth3();
171666
174137
  init_dashboard2();
171667
174138
  init_typescript2();
171668
174139
  init_domains3();
171669
174140
  init_env_vars2();
171670
- init_game3();
174141
+ init_game4();
171671
174142
  init_platform3();
171672
174143
  init_timeback8();
171673
174144
  init_cloudflare2();
@@ -171677,7 +174148,7 @@ var DEMO_USER_IDS2;
171677
174148
  var DEMO_USERS2;
171678
174149
  var DEMO_USER2;
171679
174150
  var init_demo_users2 = __esm3(() => {
171680
- init_src5();
174151
+ init_src6();
171681
174152
  now2 = new Date;
171682
174153
  DEMO_USER_IDS2 = {
171683
174154
  player: "00000000-0000-0000-0000-000000000001",