@playcademy/vite-plugin 1.1.3-beta.6 → 1.1.3-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +2720 -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.6",
24317
+ version: "1.1.3-beta.8",
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.1-beta.6",
25849
+ version: "0.6.1-beta.8",
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,374 @@ 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 expected = replayEvidence(evidence, claimedIndex);
52787
+ const live = { tables, indexes: indexes2, views };
52788
+ const verdicts = [];
52789
+ const unverified = [];
52790
+ evidence.forEach((entry2, index2) => {
52791
+ if (claimedIndex === -1 || index2 > claimedIndex) {
52792
+ verdicts.push(judgeBeyond(entry2, expected, live));
52793
+ return;
52794
+ }
52795
+ const judged = judgeClaimed(entry2, expected, live, lastDeployAt);
52796
+ verdicts.push(judged.verdict);
52797
+ if (judged.unverifiable) {
52798
+ unverified.push(judged.verdict);
52799
+ }
52800
+ });
52801
+ return {
52802
+ verdicts,
52803
+ contradictions: verdicts.filter((verdict) => verdict.verdict === "contradicted"),
52804
+ unverified,
52805
+ suggestedTag: suggestTag(evidence, tables)
52806
+ };
52079
52807
  }
52080
- var PREVIEW_STAGE_PATTERN;
52081
- var init_stages = __esm(() => {
52082
- PREVIEW_STAGE_PATTERN = /^pr-\d+$/;
52808
+ function replayEvidence(evidence, claimedIndex) {
52809
+ const state = { tables: new Map, indexes: new Map, views: new Map };
52810
+ for (let index2 = 0;index2 <= claimedIndex; index2++) {
52811
+ applyEvidenceEntry(state, evidence[index2]);
52812
+ }
52813
+ return state;
52814
+ }
52815
+ function applyEvidenceEntry(state, entry2) {
52816
+ for (const table8 of entry2.dropsTables) {
52817
+ state.tables.delete(table8);
52818
+ }
52819
+ for (const dropped of entry2.dropsColumns) {
52820
+ state.tables.get(dropped.table)?.columns.delete(dropped.column);
52821
+ }
52822
+ for (const droppedIndex of entry2.dropsIndexes) {
52823
+ state.indexes.delete(droppedIndex);
52824
+ }
52825
+ for (const view2 of entry2.dropsViews) {
52826
+ state.views.delete(view2);
52827
+ }
52828
+ for (const table8 of entry2.createsTables) {
52829
+ state.tables.set(table8.name, {
52830
+ creator: entry2.tag,
52831
+ columns: new Map(table8.columns.map((column2) => [column2, entry2.tag]))
52832
+ });
52833
+ }
52834
+ for (const added of entry2.addsColumns) {
52835
+ state.tables.get(added.table)?.columns.set(added.column, entry2.tag);
52836
+ }
52837
+ for (const createdIndex of entry2.createsIndexes) {
52838
+ state.indexes.set(createdIndex, entry2.tag);
52839
+ }
52840
+ for (const view2 of entry2.createsViews) {
52841
+ state.views.set(view2, entry2.tag);
52842
+ }
52843
+ }
52844
+ function judgeClaimed(entry2, expected, live, lastDeployAt) {
52845
+ let surviving = 0;
52846
+ for (const table8 of entry2.createsTables) {
52847
+ const expectation = expected.tables.get(table8.name);
52848
+ if (expectation?.creator === entry2.tag) {
52849
+ surviving++;
52850
+ const columns2 = live.tables.get(table8.name);
52851
+ if (!columns2) {
52852
+ return {
52853
+ verdict: {
52854
+ tag: entry2.tag,
52855
+ verdict: "contradicted",
52856
+ detail: `creates table \`${table8.name}\`, which is not in the live database`
52857
+ },
52858
+ unverifiable: false
52859
+ };
52860
+ }
52861
+ const missing = [...expectation.columns.entries()].filter(([, creator]) => creator === entry2.tag).map(([column2]) => column2).filter((column2) => !columns2.includes(column2));
52862
+ if (missing.length > 0) {
52863
+ return {
52864
+ verdict: {
52865
+ tag: entry2.tag,
52866
+ verdict: "contradicted",
52867
+ detail: `table \`${table8.name}\` exists but is missing column(s) ${missing.map((column2) => `\`${column2}\``).join(", ")} this migration defines`
52868
+ },
52869
+ unverifiable: false
52870
+ };
52871
+ }
52872
+ }
52873
+ }
52874
+ for (const added of entry2.addsColumns) {
52875
+ if (expected.tables.get(added.table)?.columns.get(added.column) === entry2.tag) {
52876
+ surviving++;
52877
+ const columns2 = live.tables.get(added.table);
52878
+ if (columns2 && !columns2.includes(added.column)) {
52879
+ return {
52880
+ verdict: {
52881
+ tag: entry2.tag,
52882
+ verdict: "contradicted",
52883
+ detail: `adds column \`${added.column}\` to \`${added.table}\`, which the live table does not have`
52884
+ },
52885
+ unverifiable: false
52886
+ };
52887
+ }
52888
+ }
52889
+ }
52890
+ for (const name2 of entry2.createsIndexes) {
52891
+ if (expected.indexes.get(name2) === entry2.tag && live.indexes.has(name2)) {
52892
+ surviving++;
52893
+ }
52894
+ }
52895
+ for (const name2 of entry2.createsViews) {
52896
+ if (expected.views.get(name2) === entry2.tag && live.views.has(name2)) {
52897
+ surviving++;
52898
+ }
52899
+ }
52900
+ if (surviving === 0) {
52901
+ const generated = new Date(entry2.generatedAt);
52902
+ if (lastDeployAt && generated > lastDeployAt) {
52903
+ return {
52904
+ verdict: {
52905
+ tag: entry2.tag,
52906
+ verdict: "no-signal",
52907
+ detail: `no schema footprint to verify, and it was generated after the last deploy (${lastDeployAt.toISOString()})`
52908
+ },
52909
+ unverifiable: true
52910
+ };
52911
+ }
52912
+ return { verdict: { tag: entry2.tag, verdict: "no-signal" }, unverifiable: false };
52913
+ }
52914
+ return { verdict: { tag: entry2.tag, verdict: "verified" }, unverifiable: false };
52915
+ }
52916
+ function judgeBeyond(entry2, expected, live) {
52917
+ for (const table8 of entry2.createsTables) {
52918
+ if (live.tables.has(table8.name) && !expected.tables.has(table8.name)) {
52919
+ return {
52920
+ tag: entry2.tag,
52921
+ verdict: "contradicted",
52922
+ detail: `is beyond the claim, but the table it creates (\`${table8.name}\`) already exists in the live database — the claim looks too old`
52923
+ };
52924
+ }
52925
+ }
52926
+ for (const added of entry2.addsColumns) {
52927
+ const columns2 = live.tables.get(added.table);
52928
+ const explained = expected.tables.get(added.table)?.columns.has(added.column);
52929
+ if (columns2?.includes(added.column) && !explained) {
52930
+ return {
52931
+ tag: entry2.tag,
52932
+ verdict: "contradicted",
52933
+ detail: `is beyond the claim, but the column it adds (\`${added.table}.${added.column}\`) already exists — the claim looks too old`
52934
+ };
52935
+ }
52936
+ }
52937
+ for (const index2 of entry2.createsIndexes) {
52938
+ if (live.indexes.has(index2) && !expected.indexes.has(index2)) {
52939
+ return {
52940
+ tag: entry2.tag,
52941
+ verdict: "contradicted",
52942
+ detail: `is beyond the claim, but the index it creates (\`${index2}\`) already exists — the claim looks too old`
52943
+ };
52944
+ }
52945
+ }
52946
+ for (const view2 of entry2.createsViews) {
52947
+ if (live.views.has(view2) && !expected.views.has(view2)) {
52948
+ return {
52949
+ tag: entry2.tag,
52950
+ verdict: "contradicted",
52951
+ detail: `is beyond the claim, but the view it creates (\`${view2}\`) already exists — the claim looks too old`
52952
+ };
52953
+ }
52954
+ }
52955
+ return { tag: entry2.tag, verdict: "verified" };
52956
+ }
52957
+ function suggestTag(evidence, tables) {
52958
+ const liveCreated = [
52959
+ ...new Set(evidence.flatMap((entry2) => entry2.createsTables.map((table8) => table8.name)))
52960
+ ].filter((name2) => tables.has(name2));
52961
+ const state = { tables: new Map, indexes: new Map, views: new Map };
52962
+ let best = null;
52963
+ for (const entry2 of evidence) {
52964
+ applyEvidenceEntry(state, entry2);
52965
+ const allPresent = [...state.tables.keys()].every((name2) => tables.has(name2));
52966
+ const noStray = liveCreated.every((name2) => state.tables.has(name2));
52967
+ if (allPresent && noStray) {
52968
+ best = entry2.tag;
52969
+ }
52970
+ }
52971
+ return best;
52972
+ }
52973
+ var init_baseline_validation_util = __esm(() => {
52974
+ init_spans();
52975
+ init_errors();
52083
52976
  });
52084
- function getGameDeploymentId(gameSlug, sstStage) {
52085
- if (sstStage === "production") {
52086
- return gameSlug;
52977
+ function sliceJournalToTag(journal, lastAppliedMigrationTag) {
52978
+ const index2 = journal.findIndex((entry2) => entry2.tag === lastAppliedMigrationTag);
52979
+ return index2 === -1 ? null : journal.slice(0, index2 + 1);
52980
+ }
52981
+ function evaluateBaselineGuardrails(input) {
52982
+ if (input.ledgerTags.length > 0) {
52983
+ return "ledger-not-empty";
52087
52984
  }
52088
- if (sstStage === "dev" || isPreviewStage(sstStage)) {
52089
- return `${WORKER_NAMING.STAGING_PREFIX}${gameSlug}`;
52985
+ if (input.liveTables.length === 0) {
52986
+ return "database-empty";
52090
52987
  }
52091
- return `${WORKER_NAMING.LOCAL_PREFIX}${sstStage}-${gameSlug}`;
52988
+ return "ok";
52092
52989
  }
52093
- function getDashboardDeploymentId(gameSlug, sstStage) {
52094
- return `${getGameDeploymentId(gameSlug, sstStage)}${DASHBOARD_WORKER_SUFFIX}`;
52990
+ function planMigrations(journal, ledger) {
52991
+ const ledgerByTag = new Map(ledger.map((row) => [row.tag, row]));
52992
+ const journalTags = new Set(journal.map((entry2) => entry2.tag));
52993
+ const pendingTags = [];
52994
+ const checksumMismatches = [];
52995
+ for (const entry2 of journal) {
52996
+ const applied = ledgerByTag.get(entry2.tag);
52997
+ if (!applied) {
52998
+ pendingTags.push(entry2.tag);
52999
+ } else {
53000
+ const comparable = applied.checksumAlgo === MIGRATION_CHECKSUM_ALGO;
53001
+ if (comparable && applied.checksum !== entry2.checksum) {
53002
+ checksumMismatches.push({
53003
+ tag: entry2.tag,
53004
+ ledgerChecksum: applied.checksum,
53005
+ journalChecksum: entry2.checksum
53006
+ });
53007
+ }
53008
+ }
53009
+ }
53010
+ const outOfOrderTags = findOutOfOrderTags(journal.map((entry2) => entry2.tag), new Set(ledgerByTag.keys()));
53011
+ const missingFromJournalTags = ledger.filter((row) => !journalTags.has(row.tag)).map((row) => row.tag);
53012
+ return { pendingTags, checksumMismatches, outOfOrderTags, missingFromJournalTags };
52095
53013
  }
52096
- function getGameWorkerApiKeyName(slug) {
52097
- return `${GAME_WORKER_KEY_PREFIX}${slug}`.substring(0, 32);
53014
+ function assessMigrationAlreadyApplied(statements, liveTables) {
53015
+ const created = extractCreatedObjects(statements);
53016
+ const present = [];
53017
+ const missing = [];
53018
+ for (const table8 of created.tables) {
53019
+ (liveTables.has(table8) ? present : missing).push(`table ${table8}`);
53020
+ }
53021
+ for (const added of created.columns) {
53022
+ const live = Boolean(liveTables.get(added.table)?.includes(added.column));
53023
+ (live ? present : missing).push(`column ${added.table}.${added.column}`);
53024
+ }
53025
+ if (present.length === 0) {
53026
+ return { verdict: "no-signal", present, missing };
53027
+ }
53028
+ return {
53029
+ verdict: missing.length === 0 ? "all-present" : "partial",
53030
+ present,
53031
+ missing
53032
+ };
52098
53033
  }
52099
- function getDashboardWorkerApiKeyName(slug) {
52100
- return `${DASHBOARD_WORKER_KEY_PREFIX}${slug}`;
53034
+ function parseMigrationFailure(events) {
53035
+ if (!events) {
53036
+ return null;
53037
+ }
53038
+ for (let i2 = events.length - 1;i2 >= 0; i2--) {
53039
+ const event = events[i2];
53040
+ const failure = event ? parseFailureEvent(event) : null;
53041
+ if (failure) {
53042
+ return failure;
53043
+ }
53044
+ }
53045
+ return null;
52101
53046
  }
52102
- function toBindingName(queueKey) {
52103
- return `${queueKey.replace(/-/g, "_").toUpperCase()}_QUEUE`;
53047
+ function parseFailureEvent(event) {
53048
+ const details = event.details;
53049
+ if (!details) {
53050
+ return null;
53051
+ }
53052
+ const { code, tag, statementIndex, error, d1Message } = details;
53053
+ if (code !== DEPLOY_ERROR_CODES.migrationFailed || typeof tag !== "string") {
53054
+ return null;
53055
+ }
53056
+ return {
53057
+ tag,
53058
+ error: failureMessage(error, d1Message),
53059
+ statementIndex: typeof statementIndex === "number" ? statementIndex : null,
53060
+ at: event.createdAt
53061
+ };
52104
53062
  }
52105
- function generateDeploymentHash(code) {
52106
- return sha256Hex(code);
53063
+ function failureMessage(error, d1Message) {
53064
+ if (typeof error === "string") {
53065
+ return error;
53066
+ }
53067
+ if (typeof d1Message === "string") {
53068
+ return d1Message;
53069
+ }
53070
+ return "Migration failed";
52107
53071
  }
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();
53072
+ var init_migration_util = __esm(() => {
53073
+ init_src4();
53074
+ init_errors();
52113
53075
  });
53076
+ async function deriveSecretsManifestPepper(platformSecret) {
53077
+ const encoder = new TextEncoder;
53078
+ const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(platformSecret), "HKDF", false, ["deriveBits"]);
53079
+ const bits = await crypto.subtle.deriveBits({
53080
+ name: "HKDF",
53081
+ hash: "SHA-256",
53082
+ salt: new Uint8Array(32),
53083
+ info: encoder.encode(SECRETS_MANIFEST_HKDF_INFO)
53084
+ }, keyMaterial, 256);
53085
+ return new Uint8Array(bits);
53086
+ }
53087
+ async function computeSecretDigest(pepper, input) {
53088
+ const key = await crypto.subtle.importKey("raw", new Uint8Array(pepper), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
53089
+ const message = JSON.stringify([input.gameId, input.key, input.value]);
53090
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
53091
+ return [...new Uint8Array(signature)].map((b) => b.toString(16).padStart(2, "0")).join("");
53092
+ }
53093
+ function computeSecretsDiff(input) {
53094
+ const { localDigests, manifest, remoteKeys } = input;
53095
+ const added = [];
53096
+ const changed = [];
53097
+ const unchanged = [];
53098
+ for (const [key, digest] of Object.entries(localDigests)) {
53099
+ const recorded = manifest[key];
53100
+ if (recorded === undefined) {
53101
+ added.push(key);
53102
+ } else if (recorded === digest) {
53103
+ unchanged.push(key);
53104
+ } else {
53105
+ changed.push(key);
53106
+ }
53107
+ }
53108
+ const localKeys = new Set(Object.keys(localDigests));
53109
+ const managedKeys = new Set(Object.keys(manifest));
53110
+ const remoteOnlyManaged = [...managedKeys].filter((key) => !localKeys.has(key));
53111
+ const remoteOnlyUnmanaged = remoteKeys.filter((key) => !managedKeys.has(key) && !localKeys.has(key));
53112
+ return {
53113
+ added: added.toSorted(),
53114
+ changed: changed.toSorted(),
53115
+ unchanged: unchanged.toSorted(),
53116
+ remoteOnlyManaged: remoteOnlyManaged.toSorted(),
53117
+ remoteOnlyUnmanaged: remoteOnlyUnmanaged.toSorted()
53118
+ };
53119
+ }
53120
+ function findUnmanagedPruneKeys(pruneSecrets, manifest) {
53121
+ const managed = new Set(Object.keys(manifest ?? {}));
53122
+ return pruneSecrets.filter((key) => !managed.has(key));
53123
+ }
53124
+ var SECRETS_MANIFEST_HKDF_INFO = "playcademy:secrets-manifest:v1";
52114
53125
  async function sweepDashboardWorkerKeys(deleteApiKeysByName, slug) {
52115
53126
  try {
52116
53127
  await deleteApiKeysByName(getDashboardWorkerApiKeyName(slug));
@@ -52125,6 +53136,9 @@ var init_worker_keys_util = __esm(() => {
52125
53136
  init_spans();
52126
53137
  init_deployment_util();
52127
53138
  });
53139
+ function hasBinding(binding) {
53140
+ return binding === true || Array.isArray(binding) && binding.length > 0;
53141
+ }
52128
53142
  function readDashboardTheme(config2) {
52129
53143
  const theme = config2?.dashboard?.theme;
52130
53144
  return {
@@ -52192,7 +53206,7 @@ class DeployService {
52192
53206
  data: { message: hasBackend ? "Deploying backend code" : "Deploying to platform" }
52193
53207
  };
52194
53208
  const keepAssets = hasBackend && !hasFrontend;
52195
- const deploymentOptions = this.mapGameBindingsToOptions(deploymentId, request.bindings, request.schema);
53209
+ const deploymentOptions = this.mapGameBindingsToOptions(deploymentId, request.bindings);
52196
53210
  const bindings = deploymentOptions?.bindings;
52197
53211
  setAttributes({
52198
53212
  "app.deploy.has_d1": Boolean(bindings?.d1?.length),
@@ -52201,13 +53215,49 @@ class DeployService {
52201
53215
  "app.deploy.queue_count": bindings?.queues?.length ?? 0,
52202
53216
  "app.deploy.dead_letter_queue_count": DeployService.countDeadLetterQueues(bindings)
52203
53217
  });
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) {
53218
+ const [activeDeployment, deploymentState] = await Promise.all([
53219
+ db2.query.gameDeployments.findFirst({
53220
+ where: activeDeploymentWhere(game2.id, "game"),
53221
+ columns: { resources: true }
53222
+ }),
53223
+ db2.query.gameDeploymentState.findFirst({
53224
+ where: eq(gameDeploymentState.gameId, game2.id)
53225
+ })
53226
+ ]);
53227
+ if (request.pruneSecrets?.length) {
53228
+ const unmanaged = findUnmanagedPruneKeys(request.pruneSecrets, deploymentState?.secretsManifest);
53229
+ if (unmanaged.length > 0) {
53230
+ throw new SecretsPruneUnmanagedError(unmanaged);
53231
+ }
53232
+ }
53233
+ let state = deploymentState;
53234
+ if (request.baseline) {
53235
+ if (isSchemaAdopted(state)) {
53236
+ addEvent("deploy.baseline_skipped", {
53237
+ "app.deploy.baseline_source": state?.baselineSource ?? "unknown"
53238
+ });
53239
+ } else {
53240
+ state = yield* this.adoptClientBaseline({
53241
+ game: game2,
53242
+ request,
53243
+ user,
53244
+ deploymentId,
53245
+ baseline: request.baseline,
53246
+ existingResources: activeDeployment?.resources ?? undefined
53247
+ });
53248
+ }
53249
+ }
53250
+ if (deploymentOptions?.bindings?.d1?.length && !isSchemaAdopted(state)) {
52209
53251
  await this.cleanupOrphanD1Databases(cf, deploymentOptions.bindings.d1, slug, game2.id);
52210
53252
  }
53253
+ const databaseOutcome = yield* this.executeDatabaseStep({
53254
+ game: game2,
53255
+ request,
53256
+ user,
53257
+ deploymentId,
53258
+ state,
53259
+ existingResources: activeDeployment?.resources ?? undefined
53260
+ });
52211
53261
  const result = await this.deployToCloudflare({
52212
53262
  deploymentId,
52213
53263
  code: request.code,
@@ -52215,6 +53265,7 @@ class DeployService {
52215
53265
  tempDir,
52216
53266
  options: {
52217
53267
  ...deploymentOptions,
53268
+ ...databaseOutcome.legacySchema && { schema: databaseOutcome.legacySchema },
52218
53269
  compatibilityDate: request.compatibilityDate ?? CLOUDFLARE_COMPATIBILITY_DATE,
52219
53270
  compatibilityFlags: request.compatibilityFlags,
52220
53271
  existingResources: activeDeployment?.resources ?? undefined,
@@ -52230,8 +53281,12 @@ class DeployService {
52230
53281
  result,
52231
53282
  request,
52232
53283
  user,
52233
- flags: flags2
53284
+ flags: flags2,
53285
+ database: databaseOutcome
52234
53286
  });
53287
+ if (request.pruneSecrets?.length) {
53288
+ yield* this.pruneManagedSecretsStep(game2.id, result.deploymentId, request.pruneSecrets);
53289
+ }
52235
53290
  yield { type: "complete", data: updatedGame };
52236
53291
  }
52237
53292
  async* deployDashboard(context2) {
@@ -52358,7 +53413,10 @@ class DeployService {
52358
53413
  "app.deploy.code_size": request.code?.length ?? 0,
52359
53414
  "app.deploy.compatibility_date": request.compatibilityDate ?? CLOUDFLARE_COMPATIBILITY_DATE,
52360
53415
  "app.deploy.compatibility_flag_count": request.compatibilityFlags?.length ?? 0,
52361
- "app.deploy.has_schema": Boolean(request.schema)
53416
+ "app.deploy.has_schema": Boolean(request.schema),
53417
+ "app.deploy.has_database_payload": Boolean(request.database),
53418
+ "app.deploy.has_baseline": Boolean(request.baseline),
53419
+ "app.deploy.prune_secret_count": request.pruneSecrets?.length ?? 0
52362
53420
  });
52363
53421
  if (!hasBackend && !hasFrontend && !hasMetadata) {
52364
53422
  throw new ValidationError("Must provide at least one of: uploadToken (frontend), code (backend), or metadata");
@@ -52395,18 +53453,18 @@ class DeployService {
52395
53453
  }
52396
53454
  return "metadata_only";
52397
53455
  }
52398
- mapGameBindingsToOptions(deploymentId, bindings, schema2) {
52399
- if (!bindings && !schema2) {
53456
+ mapGameBindingsToOptions(deploymentId, bindings) {
53457
+ if (!bindings) {
52400
53458
  return;
52401
53459
  }
52402
53460
  const workerBindings = {};
52403
- if (bindings?.database) {
53461
+ if (hasBinding(bindings.database)) {
52404
53462
  workerBindings.d1 = [deploymentId];
52405
53463
  }
52406
- if (bindings?.keyValue) {
53464
+ if (hasBinding(bindings.keyValue)) {
52407
53465
  workerBindings.kv = [deploymentId];
52408
53466
  }
52409
- if (bindings?.bucket) {
53467
+ if (hasBinding(bindings.bucket)) {
52410
53468
  workerBindings.r2 = [deploymentId];
52411
53469
  }
52412
53470
  if (bindings?.queues) {
@@ -52435,10 +53493,7 @@ class DeployService {
52435
53493
  });
52436
53494
  }
52437
53495
  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
- };
53496
+ return hasBindings ? { bindings: workerBindings } : undefined;
52442
53497
  }
52443
53498
  static countDeadLetterQueues(bindings) {
52444
53499
  return bindings?.queues?.filter((queue) => Boolean(queue.deadLetterQueue)).length ?? 0;
@@ -52462,6 +53517,473 @@ class DeployService {
52462
53517
  }
52463
53518
  }
52464
53519
  }
53520
+ async* executeDatabaseStep(context2) {
53521
+ const { game: game2, request, user, deploymentId, state } = context2;
53522
+ if (request.schema) {
53523
+ if (isSchemaAdopted(state)) {
53524
+ throw new LegacySchemaUpgradeRequiredError;
53525
+ }
53526
+ const legacyDbId = context2.existingResources?.d1?.find((db2) => db2.name === deploymentId)?.id;
53527
+ if (legacyDbId) {
53528
+ const ledger = await this.getCloudflare().d1.readMigrationLedger(legacyDbId);
53529
+ if (ledger.length > 0) {
53530
+ throw new LegacySchemaUpgradeRequiredError;
53531
+ }
53532
+ }
53533
+ setAttribute("app.deploy.db_mode", "legacy");
53534
+ return { ...NO_DATABASE_WORK, legacySchema: request.schema };
53535
+ }
53536
+ const database = request.database;
53537
+ if (!database) {
53538
+ return NO_DATABASE_WORK;
53539
+ }
53540
+ if (!hasBinding(request.bindings?.database)) {
53541
+ throw new ValidationError("The database payload requires a database binding");
53542
+ }
53543
+ if (!request.deployId) {
53544
+ throw new ValidationError("deployId is required when a database payload is present");
53545
+ }
53546
+ setAttribute("app.deploy.db_mode", database.mode);
53547
+ const cf = this.getCloudflare();
53548
+ const persistedId = context2.existingResources?.d1?.find((db2) => db2.name === deploymentId)?.id;
53549
+ const databaseId = persistedId ?? await cf.d1.create(deploymentId);
53550
+ if (database.mode === "migrate") {
53551
+ return yield* this.runMigrateMode({
53552
+ game: game2,
53553
+ user,
53554
+ deployId: request.deployId,
53555
+ databaseId,
53556
+ migrations: database.migrations,
53557
+ state
53558
+ });
53559
+ }
53560
+ return yield* this.runPushMode({ game: game2, databaseId, payload: database, state });
53561
+ }
53562
+ async* runMigrateMode(args2) {
53563
+ const { game: game2, user, deployId, databaseId, migrations, state } = args2;
53564
+ const cf = this.getCloudflare();
53565
+ yield { type: "status", data: { message: "Preparing database migrations" } };
53566
+ await this.assertNoSchemaDrift(databaseId, state);
53567
+ await cf.d1.ensureMigrationLedger(databaseId);
53568
+ const ledger = await cf.d1.readMigrationLedger(databaseId);
53569
+ const plan = planMigrations(migrations.map((migration) => ({ tag: migration.tag, checksum: migration.checksum })), ledger.map((row) => ({
53570
+ tag: row.tag,
53571
+ checksum: row.checksum,
53572
+ checksumAlgo: row.checksum_algo
53573
+ })));
53574
+ setAttributes({
53575
+ "app.deploy.migrations_total": migrations.length,
53576
+ "app.deploy.migrations_applied_before": ledger.length,
53577
+ "app.deploy.migrations_pending": plan.pendingTags.length
53578
+ });
53579
+ if (plan.checksumMismatches.length > 0) {
53580
+ throw new MigrationChecksumMismatchError(plan.checksumMismatches);
53581
+ }
53582
+ if (plan.missingFromJournalTags.length > 0) {
53583
+ throw new MigrationJournalDivergenceError(plan.missingFromJournalTags);
53584
+ }
53585
+ if (plan.outOfOrderTags.length > 0) {
53586
+ throw new MigrationOrderError(plan.outOfOrderTags);
53587
+ }
53588
+ if (plan.pendingTags.length === 0) {
53589
+ yield { type: "status", data: { message: "Database schema is up to date" } };
53590
+ if (ledger.length > 0 && !state?.schemaFingerprint) {
53591
+ const fingerprint2 = await cf.d1.fingerprintSchema(databaseId);
53592
+ await this.persistDeploymentState(game2.id, {
53593
+ schemaFingerprint: fingerprint2.fingerprint
53594
+ });
53595
+ return {
53596
+ ...NO_DATABASE_WORK,
53597
+ schemaFingerprint: fingerprint2.fingerprint
53598
+ };
53599
+ }
53600
+ return {
53601
+ ...NO_DATABASE_WORK,
53602
+ schemaFingerprint: state?.schemaFingerprint ?? null
53603
+ };
53604
+ }
53605
+ const capture = yield* this.captureBookmarkStep(databaseId);
53606
+ const migrationsByTag = new Map(migrations.map((migration) => [migration.tag, migration]));
53607
+ let appliedCount = 0;
53608
+ for (const tag of plan.pendingTags) {
53609
+ const migration = migrationsByTag.get(tag);
53610
+ const count = migration.statements.length;
53611
+ yield {
53612
+ type: "status",
53613
+ data: { message: `Applying ${tag} (${count} statement${count === 1 ? "" : "s"})` }
53614
+ };
53615
+ const startedAt = Date.now();
53616
+ try {
53617
+ await withSpan("deploy.apply_migration", () => cf.d1.applyMigration(databaseId, {
53618
+ tag,
53619
+ statements: migration.statements,
53620
+ checksum: migration.checksum,
53621
+ deployId,
53622
+ appliedBy: user.id
53623
+ }));
53624
+ } catch (error) {
53625
+ if (appliedCount > 0) {
53626
+ await this.recordAppliedPrefixFingerprint(game2.id, databaseId);
53627
+ }
53628
+ throw await this.toMigrationStepError(databaseId, error, migration);
53629
+ }
53630
+ appliedCount++;
53631
+ const seconds = ((Date.now() - startedAt) / 1000).toFixed(1);
53632
+ yield { type: "status", data: { message: `Applied ${tag} (${seconds}s)` } };
53633
+ }
53634
+ setAttribute("app.deploy.migrations_applied", plan.pendingTags.length);
53635
+ const fingerprint = await cf.d1.fingerprintSchema(databaseId);
53636
+ await this.persistDeploymentState(game2.id, {
53637
+ schemaFingerprint: fingerprint.fingerprint,
53638
+ schemaHash: null,
53639
+ schemaSnapshot: null
53640
+ });
53641
+ return {
53642
+ schemaHash: null,
53643
+ schemaFingerprint: fingerprint.fingerprint,
53644
+ timeTravelBookmark: capture?.bookmark ?? null,
53645
+ bookmarkCapturedAt: capture?.capturedAt ?? null
53646
+ };
53647
+ }
53648
+ async* runPushMode(args2) {
53649
+ const { game: game2, databaseId, payload, state } = args2;
53650
+ const cf = this.getCloudflare();
53651
+ yield { type: "status", data: { message: "Verifying database schema state" } };
53652
+ if (typeof payload.baselineHash !== "string" && payload.baselineHash !== null) {
53653
+ throw new ValidationError("Push deploys require baselineHash (null on first deploy)");
53654
+ }
53655
+ if (isMigrateManaged(state)) {
53656
+ const ledger = await cf.d1.readMigrationLedger(databaseId);
53657
+ 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 });
53658
+ }
53659
+ const storedHash = state?.schemaHash ?? null;
53660
+ if (payload.baselineHash !== storedHash) {
53661
+ throw await this.buildStateConflictError(game2.id, payload.baselineHash);
53662
+ }
53663
+ const statements = splitSqlStatements(payload.sql);
53664
+ const oversized = findOversizedStatement(statements);
53665
+ if (oversized) {
53666
+ 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.", {
53667
+ code: DEPLOY_ERROR_CODES.pushFailed,
53668
+ statementIndex: oversized.index,
53669
+ offset: null
53670
+ });
53671
+ }
53672
+ const destructive = detectDestructiveStatements(statements);
53673
+ setAttributes({
53674
+ "app.deploy.push_statement_count": statements.length,
53675
+ "app.deploy.push_destructive_count": destructive.length,
53676
+ "app.deploy.push_accept_data_loss": Boolean(payload.acceptDataLoss)
53677
+ });
53678
+ if (destructive.length > 0 && !payload.acceptDataLoss) {
53679
+ throw new DestructiveSchemaError(destructive);
53680
+ }
53681
+ await this.assertNoSchemaDrift(databaseId, state);
53682
+ const reserved = await this.persistPushDeploymentState(game2.id, payload.baselineHash, {
53683
+ schemaFingerprint: PUSH_RESERVATION_FINGERPRINT,
53684
+ schemaHash: payload.nextHash,
53685
+ schemaSnapshot: payload.nextSnapshot
53686
+ });
53687
+ if (!reserved) {
53688
+ throw await this.buildStateConflictError(game2.id, payload.baselineHash);
53689
+ }
53690
+ let capture = null;
53691
+ if (statements.length > 0) {
53692
+ capture = yield* this.captureBookmarkStep(databaseId);
53693
+ yield {
53694
+ type: "status",
53695
+ data: {
53696
+ message: `Applying database schema changes (${statements.length} statements)`
53697
+ }
53698
+ };
53699
+ try {
53700
+ await cf.d1.batch(databaseId, [
53701
+ { sql: "PRAGMA defer_foreign_keys = on" },
53702
+ ...statements.map((statement) => ({ sql: statement })),
53703
+ { sql: "PRAGMA defer_foreign_keys = off" }
53704
+ ]);
53705
+ } catch (error) {
53706
+ await this.persistPushDeploymentState(game2.id, payload.nextHash, {
53707
+ schemaFingerprint: state?.schemaFingerprint ?? null,
53708
+ schemaHash: state?.schemaHash ?? null,
53709
+ schemaSnapshot: state?.schemaSnapshot ?? null
53710
+ });
53711
+ throw this.toPushStepError(error);
53712
+ }
53713
+ }
53714
+ const fingerprint = await cf.d1.fingerprintSchema(databaseId);
53715
+ await this.persistDeploymentState(game2.id, {
53716
+ schemaFingerprint: fingerprint.fingerprint
53717
+ });
53718
+ return {
53719
+ schemaHash: payload.nextHash,
53720
+ schemaFingerprint: fingerprint.fingerprint,
53721
+ timeTravelBookmark: capture?.bookmark ?? null,
53722
+ bookmarkCapturedAt: capture?.capturedAt ?? null
53723
+ };
53724
+ }
53725
+ async* captureBookmarkStep(databaseId) {
53726
+ const cf = this.getCloudflare();
53727
+ const bookmark = await cf.d1.captureBookmark(databaseId);
53728
+ const capturedAt = new Date;
53729
+ setAttribute("app.deploy.db_bookmark_captured", bookmark.captured);
53730
+ if (!bookmark.captured) {
53731
+ yield {
53732
+ type: "status",
53733
+ data: { message: `Time Travel bookmark unavailable (${bookmark.error})` }
53734
+ };
53735
+ return null;
53736
+ }
53737
+ yield {
53738
+ type: "status",
53739
+ data: {
53740
+ message: "Captured Time Travel bookmark",
53741
+ details: { bookmark: bookmark.bookmark }
53742
+ }
53743
+ };
53744
+ return { bookmark: bookmark.bookmark, capturedAt };
53745
+ }
53746
+ async toMigrationStepError(databaseId, error, migration) {
53747
+ if (error instanceof D1StatementTooLargeError) {
53748
+ return new ValidationError(error.message, {
53749
+ code: DEPLOY_ERROR_CODES.migrationFailed,
53750
+ tag: migration.tag,
53751
+ statementIndex: error.statementIndex,
53752
+ offset: null,
53753
+ d1Message: error.message
53754
+ });
53755
+ }
53756
+ if (error instanceof D1MigrationError) {
53757
+ addEvent("deploy.migration_failed", {
53758
+ "app.d1.migration_tag": migration.tag,
53759
+ "app.error.message": error.d1Message,
53760
+ ...error.offset !== null && { "app.d1.error_offset": error.offset }
53761
+ });
53762
+ const alreadyApplied = isAlreadyExistsSqlError(error.d1Message) ? await this.assessAlreadyApplied(databaseId, migration.statements) : null;
53763
+ return new MigrationExecutionError({
53764
+ tag: migration.tag,
53765
+ d1Message: error.d1Message,
53766
+ offset: error.offset,
53767
+ ...alreadyApplied ? { alreadyApplied } : {}
53768
+ });
53769
+ }
53770
+ return error;
53771
+ }
53772
+ async assessAlreadyApplied(databaseId, statements) {
53773
+ try {
53774
+ const cf = this.getCloudflare();
53775
+ const live = await cf.d1.fingerprintSchema(databaseId);
53776
+ const tables = await cf.d1.readTableColumns(databaseId, live.tables);
53777
+ const assessment = assessMigrationAlreadyApplied(statements, tables);
53778
+ addEvent("deploy.already_applied_assessment", {
53779
+ "app.deploy.already_applied_verdict": assessment.verdict,
53780
+ "app.deploy.already_applied_present": assessment.present.length,
53781
+ "app.deploy.already_applied_missing": assessment.missing.length
53782
+ });
53783
+ return assessment.verdict === "no-signal" ? null : assessment;
53784
+ } catch (assessError) {
53785
+ addEvent("deploy.already_applied_check_failed", {
53786
+ "app.error.message": errorMessage2(assessError)
53787
+ });
53788
+ return null;
53789
+ }
53790
+ }
53791
+ toPushStepError(error) {
53792
+ if (error instanceof D1BatchError) {
53793
+ addEvent("deploy.push_failed", {
53794
+ "app.error.message": error.d1Message,
53795
+ ...error.offset !== null && { "app.d1.error_offset": error.offset }
53796
+ });
53797
+ return new PushExecutionError({ d1Message: error.d1Message, offset: error.offset });
53798
+ }
53799
+ return error;
53800
+ }
53801
+ async assertNoSchemaDrift(databaseId, state) {
53802
+ if (!state?.schemaFingerprint) {
53803
+ return;
53804
+ }
53805
+ const live = await this.getCloudflare().d1.fingerprintSchema(databaseId);
53806
+ if (live.fingerprint !== state.schemaFingerprint) {
53807
+ addEvent("deploy.state_drift", {
53808
+ "app.deploy.expected_fingerprint": state.schemaFingerprint,
53809
+ "app.deploy.actual_fingerprint": live.fingerprint
53810
+ });
53811
+ throw new DeploymentStateDriftError({
53812
+ expectedFingerprint: state.schemaFingerprint,
53813
+ actualFingerprint: live.fingerprint
53814
+ });
53815
+ }
53816
+ }
53817
+ async buildStateConflictError(gameId, claimedHash) {
53818
+ const [row, lastDeploy] = await Promise.all([
53819
+ this.deps.db.query.gameDeploymentState.findFirst({
53820
+ where: eq(gameDeploymentState.gameId, gameId),
53821
+ columns: { schemaHash: true }
53822
+ }),
53823
+ findLastSuccessfulDeployWithEmail(this.deps.db, gameId)
53824
+ ]);
53825
+ const currentHash = row?.schemaHash ?? null;
53826
+ addEvent("deploy.state_conflict", {
53827
+ "app.deploy.baseline_hash": claimedHash ?? "null",
53828
+ "app.deploy.current_hash": currentHash ?? "null"
53829
+ });
53830
+ return new DeploymentStateConflictError({
53831
+ currentHash,
53832
+ lastDeployAt: lastDeploy?.at.toISOString() ?? null,
53833
+ lastDeployBy: lastDeploy?.email ?? lastDeploy?.userId ?? null
53834
+ });
53835
+ }
53836
+ async recordAppliedPrefixFingerprint(gameId, databaseId) {
53837
+ try {
53838
+ const fingerprint = await this.getCloudflare().d1.fingerprintSchema(databaseId);
53839
+ await this.persistDeploymentState(gameId, {
53840
+ schemaFingerprint: fingerprint.fingerprint
53841
+ });
53842
+ } catch (error) {
53843
+ addEvent("deploy.fingerprint_persist_failed", {
53844
+ "exception.type": errorType(error),
53845
+ "app.error.message": errorMessage2(error)
53846
+ });
53847
+ }
53848
+ }
53849
+ async persistDeploymentState(gameId, patch) {
53850
+ const set = {
53851
+ ...patch,
53852
+ updatedAt: new Date
53853
+ };
53854
+ await this.deps.db.insert(gameDeploymentState).values({ gameId, baselineSource: "deploy", ...set }).onConflictDoUpdate({ target: gameDeploymentState.gameId, set });
53855
+ }
53856
+ async persistArtifactHashes(gameId, patch) {
53857
+ const set = {
53858
+ ...patch.buildHash !== undefined && { buildHash: patch.buildHash },
53859
+ ...patch.integrationsHash !== undefined && {
53860
+ integrationsHash: patch.integrationsHash
53861
+ }
53862
+ };
53863
+ if (Object.keys(set).length === 0) {
53864
+ return;
53865
+ }
53866
+ await this.deps.db.insert(gameDeploymentState).values({ gameId, ...set, updatedAt: new Date }).onConflictDoUpdate({
53867
+ target: gameDeploymentState.gameId,
53868
+ set: { ...set, updatedAt: new Date }
53869
+ });
53870
+ }
53871
+ async persistPushDeploymentState(gameId, baselineHash, patch) {
53872
+ const set = { ...patch, updatedAt: new Date };
53873
+ if (baselineHash === null) {
53874
+ const claimed2 = await this.deps.db.insert(gameDeploymentState).values({ gameId, baselineSource: "deploy", ...set }).onConflictDoUpdate({
53875
+ target: gameDeploymentState.gameId,
53876
+ set,
53877
+ setWhere: isNull(gameDeploymentState.schemaHash)
53878
+ }).returning({ gameId: gameDeploymentState.gameId });
53879
+ return claimed2.length > 0;
53880
+ }
53881
+ const claimed = await this.deps.db.update(gameDeploymentState).set(set).where(and(eq(gameDeploymentState.gameId, gameId), eq(gameDeploymentState.schemaHash, baselineHash))).returning({ gameId: gameDeploymentState.gameId });
53882
+ return claimed.length > 0;
53883
+ }
53884
+ async* adoptClientBaseline(context2) {
53885
+ const { game: game2, request, user, deploymentId, baseline } = context2;
53886
+ const cf = this.getCloudflare();
53887
+ const persistedId = context2.existingResources?.d1?.find((db2) => db2.name === deploymentId)?.id;
53888
+ const databaseId = persistedId ?? (hasBinding(request.bindings?.database) ? await cf.d1.create(deploymentId) : null);
53889
+ if (baseline.lastAppliedMigrationTag && !databaseId) {
53890
+ throw new ValidationError("Baseline claims applied migrations, but the deploy has no database binding");
53891
+ }
53892
+ const live = databaseId ? await cf.d1.fingerprintSchema(databaseId) : null;
53893
+ let recordedRows = 0;
53894
+ if (databaseId && baseline.lastAppliedMigrationTag) {
53895
+ if (live.tables.length === 0) {
53896
+ throw new BaselineDatabaseEmptyError;
53897
+ }
53898
+ const slice = sliceJournalToTag(baseline.journal ?? [], baseline.lastAppliedMigrationTag);
53899
+ if (!slice) {
53900
+ throw new ValidationError(`Baseline lastAppliedMigrationTag '${baseline.lastAppliedMigrationTag}' ` + "is not in the submitted journal");
53901
+ }
53902
+ if (baseline.evidence?.length) {
53903
+ const [tables, lastDeploy] = await Promise.all([
53904
+ cf.d1.readTableColumns(databaseId, live.tables),
53905
+ findLastSuccessfulDeploy(this.deps.db, game2.id)
53906
+ ]);
53907
+ assertBaselineClaimValid({
53908
+ claimedTag: baseline.lastAppliedMigrationTag,
53909
+ evidence: baseline.evidence,
53910
+ tables,
53911
+ indexes: new Set(live.indexes),
53912
+ views: new Set(live.views),
53913
+ lastDeployAt: lastDeploy?.at ?? null,
53914
+ allowUnverified: false,
53915
+ source: "seed",
53916
+ gameId: game2.id,
53917
+ userId: user.id
53918
+ });
53919
+ }
53920
+ await cf.d1.ensureMigrationLedger(databaseId);
53921
+ const ledger = await cf.d1.readMigrationLedger(databaseId);
53922
+ const plan = planMigrations(slice, ledger.map((row) => ({
53923
+ tag: row.tag,
53924
+ checksum: row.checksum,
53925
+ checksumAlgo: row.checksum_algo
53926
+ })));
53927
+ if (plan.checksumMismatches.length > 0) {
53928
+ throw new MigrationChecksumMismatchError(plan.checksumMismatches);
53929
+ }
53930
+ if (plan.missingFromJournalTags.length > 0) {
53931
+ throw new MigrationJournalDivergenceError(plan.missingFromJournalTags);
53932
+ }
53933
+ if (plan.outOfOrderTags.length > 0) {
53934
+ throw new MigrationOrderError(plan.outOfOrderTags);
53935
+ }
53936
+ const checksumByTag = new Map(slice.map((entry2) => [entry2.tag, entry2.checksum]));
53937
+ const rows = plan.pendingTags.map((tag) => ({
53938
+ tag,
53939
+ checksum: checksumByTag.get(tag)
53940
+ }));
53941
+ if (rows.length > 0) {
53942
+ await cf.d1.recordLedgerRows(databaseId, {
53943
+ rows,
53944
+ deployId: `baseline:${request.deployId ?? crypto.randomUUID()}`,
53945
+ appliedBy: user.id,
53946
+ source: "baseline"
53947
+ });
53948
+ }
53949
+ recordedRows = rows.length;
53950
+ }
53951
+ const fingerprint = live?.fingerprint ?? null;
53952
+ const set = {
53953
+ ...baseline.schemaHash !== undefined && { schemaHash: baseline.schemaHash },
53954
+ ...baseline.schemaSnapshot !== undefined && {
53955
+ schemaSnapshot: baseline.schemaSnapshot
53956
+ },
53957
+ ...baseline.integrationsHash !== undefined && {
53958
+ integrationsHash: baseline.integrationsHash
53959
+ },
53960
+ ...baseline.buildHash !== undefined && { buildHash: baseline.buildHash },
53961
+ ...fingerprint !== null && { schemaFingerprint: fingerprint },
53962
+ baselineSource: "client-baseline",
53963
+ updatedAt: new Date
53964
+ };
53965
+ await this.deps.db.insert(gameDeploymentState).values({ gameId: game2.id, ...set }).onConflictDoUpdate({ target: gameDeploymentState.gameId, set });
53966
+ setAttributes({
53967
+ "app.deploy.baseline_adopted": true,
53968
+ "app.deploy.baseline_ledger_rows": recordedRows,
53969
+ "app.deploy.baseline_has_snapshot": baseline.schemaSnapshot !== undefined
53970
+ });
53971
+ yield {
53972
+ type: "status",
53973
+ data: {
53974
+ message: "Adopted deployment state from client baseline",
53975
+ details: {
53976
+ ledgerRows: recordedRows,
53977
+ ...baseline.lastAppliedMigrationTag && {
53978
+ lastAppliedMigrationTag: baseline.lastAppliedMigrationTag
53979
+ }
53980
+ }
53981
+ }
53982
+ };
53983
+ return this.deps.db.query.gameDeploymentState.findFirst({
53984
+ where: eq(gameDeploymentState.gameId, game2.id)
53985
+ });
53986
+ }
52465
53987
  async applyGameMetadata(gameId, request, hasFrontend, hasMetadata, deploymentUrl) {
52466
53988
  const updates = { updatedAt: new Date };
52467
53989
  if (hasFrontend) {
@@ -52487,7 +54009,8 @@ class DeployService {
52487
54009
  result,
52488
54010
  request,
52489
54011
  user,
52490
- flags: flags2
54012
+ flags: flags2,
54013
+ database
52491
54014
  }) {
52492
54015
  const { hasBackend, hasFrontend, hasMetadata } = flags2;
52493
54016
  const db2 = this.deps.db;
@@ -52497,9 +54020,17 @@ class DeployService {
52497
54020
  deploymentId: result.deploymentId,
52498
54021
  url: result.url,
52499
54022
  codeHash,
54023
+ schemaHash: database.schemaHash,
54024
+ schemaFingerprint: database.schemaFingerprint,
54025
+ timeTravelBookmark: database.timeTravelBookmark,
54026
+ bookmarkCapturedAt: database.bookmarkCapturedAt,
52500
54027
  resources: result.resources,
52501
54028
  target: "game"
52502
54029
  });
54030
+ await this.persistArtifactHashes(game2.id, {
54031
+ buildHash: hasFrontend ? request.buildHash : undefined,
54032
+ integrationsHash: request.integrationsHash
54033
+ });
52503
54034
  if (hasBackend) {
52504
54035
  await withSpan("deploy.configure_worker_secrets", async () => {
52505
54036
  await this.ensureWorkerApiKeyOnWorker(user, DeployService.gameWorkerKeySpec(slug), result.deploymentId);
@@ -52636,6 +54167,29 @@ class DeployService {
52636
54167
  const cf = this.getCloudflare();
52637
54168
  await cf.setSecrets(deploymentId, { QUEUE_INGRESS_SECRET: secret });
52638
54169
  }
54170
+ async* pruneManagedSecretsStep(gameId, deploymentId, pruneSecrets) {
54171
+ const cf = this.getCloudflare();
54172
+ const keys = [...new Set(pruneSecrets)];
54173
+ yield {
54174
+ type: "status",
54175
+ data: {
54176
+ message: `Pruning ${keys.length} managed secret(s)`,
54177
+ details: { keys }
54178
+ }
54179
+ };
54180
+ await withSpan("deploy.prune_secrets", async () => {
54181
+ const existing = await cf.listSecrets(deploymentId);
54182
+ for (const key of keys) {
54183
+ const prefixedKey = `${SECRETS_PREFIX}${key}`;
54184
+ if (existing.includes(prefixedKey)) {
54185
+ await cf.deleteSecret(deploymentId, prefixedKey);
54186
+ }
54187
+ }
54188
+ const pruned = keys.reduce((expr, key) => sql`${expr} - ${key}::text`, sql`coalesce(${gameDeploymentState.secretsManifest}, '{}'::jsonb)`);
54189
+ await this.deps.db.update(gameDeploymentState).set({ secretsManifest: pruned, updatedAt: new Date }).where(eq(gameDeploymentState.gameId, gameId));
54190
+ });
54191
+ setAttribute("app.deploy.pruned_secret_count", keys.length);
54192
+ }
52639
54193
  async ensureDashboardSessionSecret(deploymentId, existingSecrets, hasPriorDeployment) {
52640
54194
  if (existingSecrets === null && hasPriorDeployment) {
52641
54195
  setAttribute("app.deploy.session_secret_outcome", "check_failed_kept");
@@ -52740,6 +54294,10 @@ class DeployService {
52740
54294
  target: record.target,
52741
54295
  url: record.url,
52742
54296
  codeHash: record.codeHash,
54297
+ schemaHash: record.schemaHash ?? null,
54298
+ schemaFingerprint: record.schemaFingerprint ?? null,
54299
+ timeTravelBookmark: record.timeTravelBookmark ?? null,
54300
+ bookmarkCapturedAt: record.bookmarkCapturedAt ?? null,
52743
54301
  resources: record.resources,
52744
54302
  isActive: true
52745
54303
  });
@@ -52749,8 +54307,11 @@ class DeployService {
52749
54307
  await this.deps.alerts.notifyDeploymentFailure(failure).catch(catchAttrs("alerts.delivery", { "app.alerts.type": "deployment_failure" }));
52750
54308
  }
52751
54309
  }
54310
+ var PUSH_RESERVATION_FINGERPRINT = "reserved:push-in-flight";
54311
+ var NO_DATABASE_WORK;
52752
54312
  var init_deploy_service = __esm(() => {
52753
54313
  init_drizzle_orm();
54314
+ init_src4();
52754
54315
  init_playcademy();
52755
54316
  init_src();
52756
54317
  init_helpers_index();
@@ -52758,9 +54319,17 @@ var init_deploy_service = __esm(() => {
52758
54319
  init_spans();
52759
54320
  init_tunnel();
52760
54321
  init_errors();
54322
+ init_baseline_validation_util();
52761
54323
  init_dashboard_util();
52762
54324
  init_deployment_util();
54325
+ init_migration_util();
52763
54326
  init_worker_keys_util();
54327
+ NO_DATABASE_WORK = {
54328
+ schemaHash: null,
54329
+ schemaFingerprint: null,
54330
+ timeTravelBookmark: null,
54331
+ bookmarkCapturedAt: null
54332
+ };
52764
54333
  });
52765
54334
 
52766
54335
  class DeveloperService {
@@ -54205,7 +55774,7 @@ function createGameServices(deps) {
54205
55774
  }
54206
55775
  };
54207
55776
  }
54208
- var init_game2 = __esm(() => {
55777
+ var init_game3 = __esm(() => {
54209
55778
  init_dashboard_service();
54210
55779
  init_deploy_job_service();
54211
55780
  init_deploy_service();
@@ -54395,16 +55964,37 @@ class AlertsService {
54395
55964
  await this.sendAlert(discord, embed.build());
54396
55965
  }
54397
55966
  async notifyDeploymentFailure(failure) {
54398
- const discord = this.recordAlert("deployment_failure");
55967
+ const refused = DEPLOY_REFUSAL_CODES.has(failure.errorCode ?? "");
55968
+ const discord = this.recordAlert(refused ? "deployment_blocked" : "deployment_failure");
54399
55969
  if (!discord) {
54400
55970
  return;
54401
55971
  }
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);
55972
+ const [noun, subject] = failure.target === "dashboard" ? ["Dashboard Deployment", "Dashboard deployment"] : ["Deployment", "Deployment"];
55973
+ const title = refused ? `\uD83D\uDEA7 ${noun} Blocked` : `❌ ${noun} Failed`;
55974
+ const verb = refused ? "was blocked" : "failed";
55975
+ 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
55976
  if (failure.developer) {
54405
55977
  embed.addField("Developer", failure.developer.email || failure.developer.id, true);
54406
55978
  }
54407
- embed.addField("Error", failure.error, false).setFooter("Playcademy Developer Platform").setTimestamp();
55979
+ embed.addField(refused ? "Reason" : "Error", failure.error, false);
55980
+ if (refused) {
55981
+ embed.addField("Next step", "The developer received repair guidance in their terminal", false);
55982
+ }
55983
+ embed.setFooter("Playcademy Developer Platform").setTimestamp();
55984
+ await this.sendAlert(discord, embed.build());
55985
+ }
55986
+ async notifyDeployBlocked(blocked) {
55987
+ const discord = this.recordAlert("deployment_blocked");
55988
+ if (!discord) {
55989
+ return;
55990
+ }
55991
+ 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);
55992
+ if (blocked.developer) {
55993
+ embed.addField("Developer", blocked.developer.email || blocked.developer.id, true);
55994
+ }
55995
+ embed.addField("Reason", blocked.reason, false);
55996
+ embed.addField("Next step", "The developer received repair guidance in their terminal", false);
55997
+ embed.setFooter("Playcademy Developer Platform").setTimestamp();
54408
55998
  await this.sendAlert(discord, embed.build());
54409
55999
  }
54410
56000
  async notifyGameDeletion(game2) {
@@ -54465,6 +56055,7 @@ var DISCORD_FIELD_LIMIT = 1024;
54465
56055
  var init_alerts_service = __esm(() => {
54466
56056
  init_discord();
54467
56057
  init_spans();
56058
+ init_game2();
54468
56059
  });
54469
56060
 
54470
56061
  class KVBackupService {
@@ -54893,6 +56484,15 @@ class DatabaseService {
54893
56484
  constructor(deps) {
54894
56485
  this.deps = deps;
54895
56486
  }
56487
+ static remapD1Resource(resources, d1ResourceName, databaseId) {
56488
+ return {
56489
+ resources: {
56490
+ ...resources,
56491
+ d1: resources.d1?.map((dbResource) => dbResource.name === d1ResourceName ? { ...dbResource, id: databaseId } : dbResource)
56492
+ },
56493
+ timeTravelBookmark: null
56494
+ };
56495
+ }
54896
56496
  getD1() {
54897
56497
  const d1 = this.deps.cloudflare?.d1;
54898
56498
  if (!d1) {
@@ -54911,11 +56511,7 @@ class DatabaseService {
54911
56511
  try {
54912
56512
  await this.deps.cloudflare.updateD1Binding(dashboardDeployment.deploymentId, databaseId);
54913
56513
  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));
56514
+ await this.deps.db.update(gameDeployments).set(DatabaseService.remapD1Resource(dashboardDeployment.resources, d1ResourceName, databaseId)).where(eq(gameDeployments.id, dashboardDeployment.id));
54919
56515
  }
54920
56516
  setAttribute("app.database.dashboard_binding", "updated");
54921
56517
  } catch (error) {
@@ -54926,20 +56522,36 @@ class DatabaseService {
54926
56522
  });
54927
56523
  }
54928
56524
  }
54929
- async reset(slug, user, schema2) {
56525
+ async reset(slug, user, request = {}) {
56526
+ const { schema: schema2, database } = request;
54930
56527
  setAttributes({
54931
56528
  "app.database.operation": "reset",
56529
+ "app.database.mode": database?.mode ?? (schema2 ? "legacy" : "none"),
54932
56530
  "app.database.schema_size": schema2?.sql.length,
54933
56531
  "app.database.schema_version": schema2?.hash
54934
56532
  });
54935
56533
  const d1 = this.getD1();
54936
56534
  const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56535
+ const state = await this.deps.db.query.gameDeploymentState.findFirst({
56536
+ where: eq(gameDeploymentState.gameId, game2.id)
56537
+ });
56538
+ if (isSchemaAdopted(state) && !database) {
56539
+ if (schema2) {
56540
+ throw new LegacySchemaUpgradeRequiredError;
56541
+ }
56542
+ 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.");
56543
+ }
54937
56544
  const deploymentId = getGameDeploymentId(slug, this.deps.config.sstStage);
56545
+ let resetDatabaseId = null;
54938
56546
  try {
54939
56547
  const databaseId = await d1.reset(deploymentId);
56548
+ resetDatabaseId = databaseId;
54940
56549
  setAttribute("app.database.id", databaseId);
54941
56550
  let schemaPushed = false;
54942
- if (schema2?.sql) {
56551
+ if (database) {
56552
+ await this.rebuildDatabase(databaseId, database, game2.id, user);
56553
+ schemaPushed = true;
56554
+ } else if (schema2?.sql) {
54943
56555
  await d1.executeSchema(databaseId, schema2);
54944
56556
  schemaPushed = true;
54945
56557
  }
@@ -54955,11 +56567,7 @@ class DatabaseService {
54955
56567
  });
54956
56568
  setAttribute("app.database.active_deployment_found", Boolean(activeDeployment));
54957
56569
  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));
56570
+ await this.deps.db.update(gameDeployments).set(DatabaseService.remapD1Resource(activeDeployment.resources, deploymentId, databaseId)).where(eq(gameDeployments.id, activeDeployment.id));
54963
56571
  }
54964
56572
  await this.syncDashboardD1Binding(game2.id, deploymentId, databaseId);
54965
56573
  setAttributes({
@@ -54973,24 +56581,604 @@ class DatabaseService {
54973
56581
  schemaPushed
54974
56582
  };
54975
56583
  } catch (error) {
56584
+ if (database && resetDatabaseId) {
56585
+ await this.recordLiveFingerprint(game2.id, resetDatabaseId);
56586
+ }
54976
56587
  this.deps.alerts.notifyDatabaseResetFailure({
54977
56588
  slug,
54978
56589
  displayName: game2.displayName,
54979
56590
  error: errorMessage2(error),
54980
56591
  developer: { id: user.id, email: user.email }
54981
56592
  }).catch(catchAttrs("alerts.delivery", { "app.alerts.type": "database_reset" }));
56593
+ if (error instanceof DomainError) {
56594
+ throw error;
56595
+ }
54982
56596
  throw new ValidationError(`Database reset failed: ${errorMessage2(error)}`);
54983
56597
  }
54984
56598
  }
56599
+ async rebuildDatabase(databaseId, payload, gameId, user) {
56600
+ const d1 = this.getD1();
56601
+ if (payload.mode === "migrate") {
56602
+ const deployId = `reset:${crypto.randomUUID()}`;
56603
+ await d1.ensureMigrationLedger(databaseId);
56604
+ for (const migration of payload.migrations) {
56605
+ await d1.applyMigration(databaseId, {
56606
+ tag: migration.tag,
56607
+ statements: migration.statements,
56608
+ checksum: migration.checksum,
56609
+ deployId,
56610
+ appliedBy: user.id
56611
+ });
56612
+ }
56613
+ setAttribute("app.database.migrations_replayed", payload.migrations.length);
56614
+ const fingerprint2 = await d1.fingerprintSchema(databaseId);
56615
+ await this.persistDeploymentState(gameId, {
56616
+ schemaFingerprint: fingerprint2.fingerprint,
56617
+ schemaHash: null,
56618
+ schemaSnapshot: null
56619
+ });
56620
+ return;
56621
+ }
56622
+ const statements = splitSqlStatements(payload.sql);
56623
+ if (statements.length > 0) {
56624
+ await d1.batch(databaseId, [
56625
+ { sql: "PRAGMA defer_foreign_keys = on" },
56626
+ ...statements.map((statement) => ({ sql: statement })),
56627
+ { sql: "PRAGMA defer_foreign_keys = off" }
56628
+ ]);
56629
+ }
56630
+ setAttribute("app.database.push_statement_count", statements.length);
56631
+ const fingerprint = await d1.fingerprintSchema(databaseId);
56632
+ await this.persistDeploymentState(gameId, {
56633
+ schemaFingerprint: fingerprint.fingerprint,
56634
+ schemaHash: payload.nextHash,
56635
+ schemaSnapshot: payload.nextSnapshot
56636
+ });
56637
+ }
56638
+ async persistDeploymentState(gameId, patch) {
56639
+ const set = { ...patch, updatedAt: new Date };
56640
+ await this.deps.db.insert(gameDeploymentState).values({ gameId, baselineSource: "deploy", ...set }).onConflictDoUpdate({ target: gameDeploymentState.gameId, set });
56641
+ }
56642
+ async recordLiveFingerprint(gameId, databaseId) {
56643
+ try {
56644
+ const fingerprint = await this.getD1().fingerprintSchema(databaseId);
56645
+ await this.persistDeploymentState(gameId, {
56646
+ schemaFingerprint: fingerprint.fingerprint
56647
+ });
56648
+ } catch (error) {
56649
+ addEvent("database.fingerprint_persist_failed", {
56650
+ "exception.type": errorType(error),
56651
+ "app.error.message": errorMessage2(error)
56652
+ });
56653
+ }
56654
+ }
54985
56655
  }
54986
56656
  var init_database_service = __esm(() => {
54987
56657
  init_drizzle_orm();
56658
+ init_src4();
54988
56659
  init_helpers_index();
54989
56660
  init_tables_index();
54990
56661
  init_spans();
54991
56662
  init_errors();
54992
56663
  init_deployment_util();
54993
56664
  });
56665
+ async function listUserSecretKeys(cloudflare2, deploymentId) {
56666
+ try {
56667
+ const allKeys = await cloudflare2.listSecrets(deploymentId);
56668
+ return allKeys.filter((key) => key.startsWith(SECRETS_PREFIX)).map((key) => key.slice(SECRETS_PREFIX.length));
56669
+ } catch (error) {
56670
+ const message = errorMessage2(error);
56671
+ if (message.includes("not found") || message.includes("10007")) {
56672
+ return null;
56673
+ }
56674
+ throw error;
56675
+ }
56676
+ }
56677
+ var init_secrets_util = __esm(() => {
56678
+ init_src();
56679
+ });
56680
+ function historyEventEntry(event) {
56681
+ const base = { at: event.createdAt.toISOString(), by: event.email ?? DELETED_ACCOUNT_LABEL };
56682
+ if (event.kind === "restore" && "restoredTo" in event.payload) {
56683
+ return [{ kind: "restore", ...base, restoredTo: event.payload.restoredTo }];
56684
+ }
56685
+ if (event.kind === "blocked" && "code" in event.payload) {
56686
+ return [
56687
+ { kind: "blocked", ...base, code: event.payload.code, reason: event.payload.reason }
56688
+ ];
56689
+ }
56690
+ return [];
56691
+ }
56692
+ function databaseIdFromResources(resources, deploymentId) {
56693
+ const database = resources?.d1?.find((db2) => db2.name === deploymentId) ?? resources?.d1?.[0];
56694
+ return database?.id ?? null;
56695
+ }
56696
+ function restorePointCapturedAt(row) {
56697
+ return row.bookmarkCapturedAt ?? row.deployedAt;
56698
+ }
56699
+
56700
+ class DeploymentStateService {
56701
+ deps;
56702
+ constructor(deps) {
56703
+ this.deps = deps;
56704
+ }
56705
+ async partitionSecretKeys(slug, manifest) {
56706
+ const managedKeys = Object.keys(manifest ?? {});
56707
+ if (!this.deps.cloudflare) {
56708
+ addEvent("deployment_state.secret_keys_unavailable", {
56709
+ "app.error.message": "Cloudflare provider not configured"
56710
+ });
56711
+ return { managedKeys, unmanagedKeys: null };
56712
+ }
56713
+ const deploymentId = getGameDeploymentId(slug, this.deps.config.sstStage);
56714
+ const remoteKeys = await listUserSecretKeys(this.deps.cloudflare, deploymentId);
56715
+ if (remoteKeys === null) {
56716
+ return { managedKeys, unmanagedKeys: [] };
56717
+ }
56718
+ const managed = new Set(managedKeys);
56719
+ return {
56720
+ managedKeys,
56721
+ unmanagedKeys: remoteKeys.filter((key) => !managed.has(key))
56722
+ };
56723
+ }
56724
+ async readAppliedMigrations(slug, resources) {
56725
+ if (!this.deps.cloudflare || !resources?.d1?.length) {
56726
+ return null;
56727
+ }
56728
+ const deploymentId = getGameDeploymentId(slug, this.deps.config.sstStage);
56729
+ const databaseId = databaseIdFromResources(resources, deploymentId);
56730
+ if (!databaseId) {
56731
+ return null;
56732
+ }
56733
+ const ledger = await this.deps.cloudflare.d1.readMigrationLedger(databaseId);
56734
+ setAttributes({ "app.deployment_state.applied_migrations": ledger.length });
56735
+ return ledger.map((row) => ({
56736
+ tag: row.tag,
56737
+ checksum: row.checksum,
56738
+ checksumAlgo: row.checksum_algo
56739
+ }));
56740
+ }
56741
+ async get(slug, user, options = {}) {
56742
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56743
+ const [state, gameDeployment, dashboardDeployment, lastSucceededJob, lastFailedJob] = await Promise.all([
56744
+ this.deps.db.query.gameDeploymentState.findFirst({
56745
+ where: eq(gameDeploymentState.gameId, game2.id)
56746
+ }),
56747
+ this.deps.db.query.gameDeployments.findFirst({
56748
+ where: activeDeploymentWhere(game2.id, "game"),
56749
+ columns: { codeHash: true, url: true, deployedAt: true, resources: true }
56750
+ }),
56751
+ this.deps.db.query.gameDeployments.findFirst({
56752
+ where: activeDeploymentWhere(game2.id, "dashboard"),
56753
+ columns: { url: true, deployedAt: true }
56754
+ }),
56755
+ findLastSuccessfulDeployWithEmail(this.deps.db, game2.id),
56756
+ this.deps.db.query.gameDeployJobs.findFirst({
56757
+ where: and(eq(gameDeployJobs.gameId, game2.id), eq(gameDeployJobs.status, "failed")),
56758
+ orderBy: desc(deployJobInstant()),
56759
+ columns: { events: true }
56760
+ })
56761
+ ]);
56762
+ const [secrets, appliedMigrations] = await Promise.all([
56763
+ this.partitionSecretKeys(slug, state?.secretsManifest ?? null),
56764
+ this.readAppliedMigrations(slug, gameDeployment?.resources ?? null)
56765
+ ]);
56766
+ setAttributes({
56767
+ "app.deployment_state.seeded": Boolean(state),
56768
+ "app.deployment_state.game_deployed": Boolean(gameDeployment),
56769
+ "app.deployment_state.dashboard_deployed": Boolean(dashboardDeployment)
56770
+ });
56771
+ return {
56772
+ gameId: game2.id,
56773
+ seeded: Boolean(state),
56774
+ game: gameDeployment ? {
56775
+ codeHash: gameDeployment.codeHash,
56776
+ buildHash: state?.buildHash ?? null,
56777
+ url: gameDeployment.url,
56778
+ deployedAt: gameDeployment.deployedAt.toISOString()
56779
+ } : null,
56780
+ dashboard: dashboardDeployment ? {
56781
+ url: dashboardDeployment.url,
56782
+ deployedAt: dashboardDeployment.deployedAt.toISOString()
56783
+ } : null,
56784
+ database: {
56785
+ appliedMigrations,
56786
+ lastFailure: parseMigrationFailure(lastFailedJob?.events ?? null),
56787
+ schemaHash: state?.schemaHash ?? null,
56788
+ schemaFingerprint: state?.schemaFingerprint ?? null,
56789
+ ...options.includeSchemaSnapshot && {
56790
+ schemaSnapshot: state?.schemaSnapshot ?? null
56791
+ }
56792
+ },
56793
+ secrets,
56794
+ integrationsHash: state?.integrationsHash ?? null,
56795
+ compatibilityDate: state?.compatibilityDate ?? null,
56796
+ lastDeploy: lastSucceededJob ? {
56797
+ at: lastSucceededJob.at.toISOString(),
56798
+ by: lastSucceededJob.email ?? DELETED_ACCOUNT_LABEL
56799
+ } : null
56800
+ };
56801
+ }
56802
+ requireCloudflare() {
56803
+ if (!this.deps.cloudflare) {
56804
+ throw new ValidationError("Deployment-state operations are not available in this environment");
56805
+ }
56806
+ return this.deps.cloudflare;
56807
+ }
56808
+ async resolveDatabaseId(slug, gameId) {
56809
+ const databaseId = await this.findDatabaseId(slug, gameId);
56810
+ if (!databaseId) {
56811
+ throw new ValidationError("Game has no deployed database — deploy with a database binding first");
56812
+ }
56813
+ return databaseId;
56814
+ }
56815
+ findSchemaHashState(gameId) {
56816
+ return this.deps.db.query.gameDeploymentState.findFirst({
56817
+ where: eq(gameDeploymentState.gameId, gameId),
56818
+ columns: { schemaHash: true }
56819
+ });
56820
+ }
56821
+ async findDatabaseId(slug, gameId) {
56822
+ const deployment = await this.deps.db.query.gameDeployments.findFirst({
56823
+ where: activeDeploymentWhere(gameId, "game"),
56824
+ columns: { resources: true }
56825
+ });
56826
+ return databaseIdFromResources(deployment?.resources, getGameDeploymentId(slug, this.deps.config.sstStage));
56827
+ }
56828
+ async baseline(slug, input, user) {
56829
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56830
+ const cf = this.requireCloudflare();
56831
+ const state = await this.deps.db.query.gameDeploymentState.findFirst({
56832
+ where: eq(gameDeploymentState.gameId, game2.id)
56833
+ });
56834
+ const schemaAdopted = isSchemaAdopted(state);
56835
+ const migrateOnlyClaim = Boolean(input.lastAppliedMigrationTag) && input.schemaHash === undefined && input.schemaSnapshot === undefined;
56836
+ if (schemaAdopted && !migrateOnlyClaim) {
56837
+ throw new BaselineAlreadyAdoptedError(state?.baselineSource ?? null);
56838
+ }
56839
+ const databaseId = await this.resolveDatabaseId(slug, game2.id);
56840
+ const [ledger, live] = await Promise.all([
56841
+ cf.d1.readMigrationLedger(databaseId),
56842
+ cf.d1.fingerprintSchema(databaseId)
56843
+ ]);
56844
+ const verdict = evaluateBaselineGuardrails({
56845
+ ledgerTags: ledger.map((row) => row.tag),
56846
+ liveTables: live.tables
56847
+ });
56848
+ if (verdict === "ledger-not-empty") {
56849
+ throw new BaselineLedgerNotEmptyError(ledger.map((row) => row.tag));
56850
+ }
56851
+ if (verdict === "database-empty") {
56852
+ throw new BaselineDatabaseEmptyError;
56853
+ }
56854
+ if (schemaAdopted && state?.schemaFingerprint && live.fingerprint !== state.schemaFingerprint) {
56855
+ throw new DeploymentStateDriftError({
56856
+ expectedFingerprint: state.schemaFingerprint,
56857
+ actualFingerprint: live.fingerprint
56858
+ });
56859
+ }
56860
+ if (input.lastAppliedMigrationTag && input.evidence?.length) {
56861
+ const [tables, lastDeploy] = await Promise.all([
56862
+ cf.d1.readTableColumns(databaseId, live.tables),
56863
+ findLastSuccessfulDeploy(this.deps.db, game2.id)
56864
+ ]);
56865
+ assertBaselineClaimValid({
56866
+ claimedTag: input.lastAppliedMigrationTag,
56867
+ evidence: input.evidence,
56868
+ tables,
56869
+ indexes: new Set(live.indexes),
56870
+ views: new Set(live.views),
56871
+ lastDeployAt: lastDeploy?.at ?? null,
56872
+ allowUnverified: Boolean(input.allowUnverified),
56873
+ source: "manual",
56874
+ gameId: game2.id,
56875
+ userId: user.id
56876
+ });
56877
+ }
56878
+ let recordedTags = [];
56879
+ if (input.lastAppliedMigrationTag) {
56880
+ const slice = sliceJournalToTag(input.journal ?? [], input.lastAppliedMigrationTag);
56881
+ if (!slice) {
56882
+ throw new ValidationError(`lastAppliedMigrationTag '${input.lastAppliedMigrationTag}' is not in the submitted journal`);
56883
+ }
56884
+ await cf.d1.ensureMigrationLedger(databaseId);
56885
+ await cf.d1.recordLedgerRows(databaseId, {
56886
+ rows: slice,
56887
+ deployId: `baseline:${crypto.randomUUID()}`,
56888
+ appliedBy: user.id,
56889
+ source: "baseline"
56890
+ });
56891
+ recordedTags = slice.map((entry2) => entry2.tag);
56892
+ }
56893
+ const graduating = migrateOnlyClaim && isPushAdopted(state);
56894
+ const set = {
56895
+ ...graduating ? { schemaHash: null, schemaSnapshot: null } : {
56896
+ ...input.schemaHash !== undefined && { schemaHash: input.schemaHash },
56897
+ ...input.schemaSnapshot !== undefined && {
56898
+ schemaSnapshot: input.schemaSnapshot
56899
+ }
56900
+ },
56901
+ schemaFingerprint: live.fingerprint,
56902
+ baselineSource: "manual-baseline",
56903
+ updatedAt: new Date
56904
+ };
56905
+ await this.persistDeploymentState(game2.id, set);
56906
+ addEvent("deployment_state.baseline_recorded", {
56907
+ "app.game.id": game2.id,
56908
+ "app.deployment_state.baseline_source": "manual-baseline",
56909
+ "app.deployment_state.baseline_ledger_rows": recordedTags.length,
56910
+ "app.deployment_state.baseline_has_snapshot": input.schemaSnapshot !== undefined,
56911
+ "app.deployment_state.baseline_graduated_from_push": graduating
56912
+ });
56913
+ return this.get(slug, user);
56914
+ }
56915
+ async realignMigration(slug, tag, checksum, user) {
56916
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56917
+ const cf = this.requireCloudflare();
56918
+ const databaseId = await this.resolveDatabaseId(slug, game2.id);
56919
+ const ledger = await cf.d1.readMigrationLedger(databaseId);
56920
+ if (!ledger.some((row) => row.tag === tag)) {
56921
+ throw new NotFoundError("Applied migration", tag);
56922
+ }
56923
+ const updated = await cf.d1.updateLedgerChecksum(databaseId, { tag, checksum });
56924
+ if (!updated) {
56925
+ throw new NotFoundError("Applied migration", tag);
56926
+ }
56927
+ addEvent("deployment_state.migration_realigned", {
56928
+ "app.game.id": game2.id,
56929
+ "app.d1.migration_tag": tag,
56930
+ "app.user.id": user.id
56931
+ });
56932
+ return { tag, checksum, checksumAlgo: MIGRATION_CHECKSUM_ALGO };
56933
+ }
56934
+ async persistDeploymentState(gameId, set) {
56935
+ await this.deps.db.insert(gameDeploymentState).values({ gameId, ...set }).onConflictDoUpdate({ target: gameDeploymentState.gameId, set });
56936
+ }
56937
+ async updateDeploymentState(gameId, set) {
56938
+ const updated = await this.deps.db.update(gameDeploymentState).set(set).where(eq(gameDeploymentState.gameId, gameId)).returning({ gameId: gameDeploymentState.gameId });
56939
+ return updated.length > 0;
56940
+ }
56941
+ async resolveMigration(slug, tag, input, user) {
56942
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56943
+ const cf = this.requireCloudflare();
56944
+ const databaseId = await this.resolveDatabaseId(slug, game2.id);
56945
+ const [ledger, live] = await Promise.all([
56946
+ cf.d1.readMigrationLedger(databaseId),
56947
+ cf.d1.fingerprintSchema(databaseId)
56948
+ ]);
56949
+ const exists2 = ledger.some((row) => row.tag === tag);
56950
+ if (input.resolution === "applied") {
56951
+ if (!input.checksum) {
56952
+ throw new ValidationError("Resolving a migration as 'applied' requires its checksum");
56953
+ }
56954
+ if (exists2) {
56955
+ throw new AlreadyExistsError(`Migration '${tag}' is already recorded as applied`);
56956
+ }
56957
+ await cf.d1.ensureMigrationLedger(databaseId);
56958
+ await cf.d1.recordLedgerRows(databaseId, {
56959
+ rows: [{ tag, checksum: input.checksum }],
56960
+ deployId: `resolve:${crypto.randomUUID()}`,
56961
+ appliedBy: user.id,
56962
+ source: "resolve"
56963
+ });
56964
+ } else {
56965
+ if (!exists2) {
56966
+ throw new NotFoundError("Migration ledger row", tag);
56967
+ }
56968
+ await cf.d1.deleteLedgerRow(databaseId, tag);
56969
+ }
56970
+ const fingerprintRecorded = await this.updateDeploymentState(game2.id, {
56971
+ schemaFingerprint: live.fingerprint,
56972
+ updatedAt: new Date
56973
+ });
56974
+ addEvent("deployment_state.migration_resolved", {
56975
+ "app.game.id": game2.id,
56976
+ "app.d1.migration_tag": tag,
56977
+ "app.deployment_state.resolution": input.resolution,
56978
+ "app.deployment_state.fingerprint_rerecorded": fingerprintRecorded,
56979
+ "app.user.id": user.id
56980
+ });
56981
+ return {
56982
+ tag,
56983
+ resolution: input.resolution,
56984
+ schemaFingerprint: live.fingerprint,
56985
+ fingerprintRecorded
56986
+ };
56987
+ }
56988
+ async history(slug, user, options = {}) {
56989
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
56990
+ const limit = options.limit ?? 20;
56991
+ const [jobs, events] = await Promise.all([
56992
+ this.deps.db.select({
56993
+ status: gameDeployJobs.status,
56994
+ createdAt: gameDeployJobs.createdAt,
56995
+ completedAt: gameDeployJobs.completedAt,
56996
+ deployId: gameDeployJobs.deployId,
56997
+ error: gameDeployJobs.error,
56998
+ email: users.email
56999
+ }).from(gameDeployJobs).leftJoin(users, eq(gameDeployJobs.userId, users.id)).where(eq(gameDeployJobs.gameId, game2.id)).orderBy(desc(deployJobInstant())).limit(limit),
57000
+ this.deps.db.select({
57001
+ kind: gameDeployEvents.kind,
57002
+ payload: gameDeployEvents.payload,
57003
+ createdAt: gameDeployEvents.createdAt,
57004
+ email: users.email
57005
+ }).from(gameDeployEvents).leftJoin(users, eq(gameDeployEvents.userId, users.id)).where(eq(gameDeployEvents.gameId, game2.id)).orderBy(desc(gameDeployEvents.createdAt)).limit(limit)
57006
+ ]);
57007
+ setAttributes({
57008
+ "app.deployment_state.history_jobs": jobs.length,
57009
+ "app.deployment_state.history_events": events.length
57010
+ });
57011
+ const deploys = jobs.map((job) => ({
57012
+ kind: "deploy",
57013
+ status: job.status,
57014
+ at: (job.completedAt ?? job.createdAt).toISOString(),
57015
+ completedAt: job.completedAt?.toISOString() ?? null,
57016
+ by: job.email ?? DELETED_ACCOUNT_LABEL,
57017
+ deployId: job.deployId,
57018
+ error: job.error
57019
+ }));
57020
+ const merged = [...deploys, ...events.flatMap(historyEventEntry)].toSorted((a, b) => a.at < b.at ? 1 : -1).slice(0, limit);
57021
+ return { deploys: merged };
57022
+ }
57023
+ async restorePoints(slug, user) {
57024
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
57025
+ const retentionCutoff = new Date(Date.now() - D1_TIME_TRAVEL_RETENTION_DAYS * 24 * 60 * 60 * 1000);
57026
+ const [currentDatabaseId, rows, state] = await Promise.all([
57027
+ this.findDatabaseId(slug, game2.id),
57028
+ this.deps.db.query.gameDeployments.findMany({
57029
+ where: and(eq(gameDeployments.gameId, game2.id), eq(gameDeployments.target, "game"), isNotNull(gameDeployments.timeTravelBookmark), gte(gameDeployments.deployedAt, retentionCutoff)),
57030
+ orderBy: desc(gameDeployments.deployedAt),
57031
+ limit: 100,
57032
+ columns: {
57033
+ id: true,
57034
+ deployedAt: true,
57035
+ bookmarkCapturedAt: true,
57036
+ isActive: true,
57037
+ resources: true
57038
+ }
57039
+ }),
57040
+ this.findSchemaHashState(game2.id)
57041
+ ]);
57042
+ if (isPushAdopted(state)) {
57043
+ return { restorePoints: [], restoreUnsupported: true };
57044
+ }
57045
+ if (!currentDatabaseId) {
57046
+ return { restorePoints: [], restoreUnsupported: false };
57047
+ }
57048
+ const deploymentId = getGameDeploymentId(slug, this.deps.config.sstStage);
57049
+ const restorable = rows.filter((row) => databaseIdFromResources(row.resources, deploymentId) === currentDatabaseId && restorePointCapturedAt(row) >= retentionCutoff).slice(0, 20);
57050
+ setAttributes({ "app.deployment_state.restore_points": restorable.length });
57051
+ return {
57052
+ restorePoints: restorable.map((row) => ({
57053
+ id: row.id,
57054
+ capturedAt: restorePointCapturedAt(row).toISOString(),
57055
+ active: row.isActive
57056
+ })),
57057
+ restoreUnsupported: false
57058
+ };
57059
+ }
57060
+ async restoreToBookmark(slug, input, user) {
57061
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
57062
+ const cf = this.requireCloudflare();
57063
+ const [row, state, currentDatabaseId, runningJob] = await Promise.all([
57064
+ this.deps.db.query.gameDeployments.findFirst({
57065
+ where: and(eq(gameDeployments.id, input.restorePointId), eq(gameDeployments.gameId, game2.id), eq(gameDeployments.target, "game")),
57066
+ columns: {
57067
+ id: true,
57068
+ timeTravelBookmark: true,
57069
+ deployedAt: true,
57070
+ bookmarkCapturedAt: true,
57071
+ resources: true
57072
+ }
57073
+ }),
57074
+ this.findSchemaHashState(game2.id),
57075
+ this.findDatabaseId(slug, game2.id),
57076
+ this.deps.db.query.gameDeployJobs.findFirst({
57077
+ where: and(eq(gameDeployJobs.gameId, game2.id), or(eq(gameDeployJobs.status, "pending"), and(eq(gameDeployJobs.status, "running"), gt(gameDeployJobs.leaseExpiresAt, new Date)))),
57078
+ columns: { id: true }
57079
+ })
57080
+ ]);
57081
+ if (!row?.timeTravelBookmark) {
57082
+ throw new NotFoundError("Bookmark", input.restorePointId);
57083
+ }
57084
+ if (isPushAdopted(state)) {
57085
+ 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 });
57086
+ }
57087
+ if (!currentDatabaseId) {
57088
+ throw new ValidationError("Game has no deployed database — deploy with a database binding first");
57089
+ }
57090
+ const databaseId = currentDatabaseId;
57091
+ const deploymentId = getGameDeploymentId(slug, this.deps.config.sstStage);
57092
+ if (databaseIdFromResources(row.resources, deploymentId) !== databaseId) {
57093
+ throw new ValidationError("This bookmark was captured on a previous database (a reset replaced it since) and can no longer be restored");
57094
+ }
57095
+ const retentionCutoff = new Date(Date.now() - D1_TIME_TRAVEL_RETENTION_DAYS * 24 * 60 * 60 * 1000);
57096
+ if (restorePointCapturedAt(row) < retentionCutoff) {
57097
+ 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 });
57098
+ }
57099
+ if (runningJob) {
57100
+ throw new ValidationError("A deploy is currently running for this game. Wait for it to finish, then restore.", { code: DEPLOY_ERROR_CODES.restoreBlockedByDeploy });
57101
+ }
57102
+ const restored = await cf.d1.restoreBookmark(databaseId, row.timeTravelBookmark);
57103
+ const restoredTo = restorePointCapturedAt(row).toISOString();
57104
+ let live;
57105
+ let fingerprintRecorded;
57106
+ try {
57107
+ live = await cf.d1.fingerprintSchema(databaseId);
57108
+ fingerprintRecorded = await this.updateDeploymentState(game2.id, {
57109
+ schemaFingerprint: live.fingerprint,
57110
+ updatedAt: new Date
57111
+ });
57112
+ await this.deps.db.insert(gameDeployEvents).values({
57113
+ gameId: game2.id,
57114
+ userId: user.id,
57115
+ kind: "restore",
57116
+ payload: { restoredTo, previousBookmark: restored.previousBookmark }
57117
+ });
57118
+ } catch (error) {
57119
+ 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 });
57120
+ }
57121
+ addEvent("deployment_state.bookmark_restored", {
57122
+ "app.game.id": game2.id,
57123
+ "app.deployment_state.restore_point": row.id,
57124
+ "app.deployment_state.fingerprint_rerecorded": fingerprintRecorded,
57125
+ "app.user.id": user.id
57126
+ });
57127
+ return {
57128
+ restorePointId: row.id,
57129
+ restoredTo,
57130
+ schemaFingerprint: live.fingerprint,
57131
+ fingerprintRecorded,
57132
+ previousBookmark: restored.previousBookmark
57133
+ };
57134
+ }
57135
+ async reportDeployBlocked(slug, user, report) {
57136
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
57137
+ const recent = await this.deps.db.query.gameDeployEvents.findMany({
57138
+ where: and(eq(gameDeployEvents.gameId, game2.id), eq(gameDeployEvents.kind, "blocked"), gt(gameDeployEvents.createdAt, new Date(Date.now() - BLOCKED_ALERT_DEDUP_MS))),
57139
+ orderBy: desc(gameDeployEvents.createdAt),
57140
+ limit: 10,
57141
+ columns: { payload: true }
57142
+ });
57143
+ const duplicate = recent.some((event) => ("code" in event.payload) && event.payload.code === report.code);
57144
+ await this.deps.db.insert(gameDeployEvents).values({
57145
+ gameId: game2.id,
57146
+ userId: user.id,
57147
+ kind: "blocked",
57148
+ payload: { code: report.code, reason: report.reason }
57149
+ });
57150
+ addEvent("deployment_state.deploy_blocked", {
57151
+ "app.game.id": game2.id,
57152
+ "app.deploy.blocked_code": report.code,
57153
+ "app.user.id": user.id,
57154
+ "app.alerts.deduped": duplicate
57155
+ });
57156
+ if (!duplicate) {
57157
+ await this.deps.alerts.notifyDeployBlocked({
57158
+ slug,
57159
+ displayName: game2.displayName,
57160
+ reason: report.reason,
57161
+ developer: { id: user.id, email: user.email ?? null }
57162
+ });
57163
+ }
57164
+ }
57165
+ }
57166
+ var BLOCKED_ALERT_DEDUP_MS;
57167
+ var init_deployment_state_service = __esm(() => {
57168
+ init_drizzle_orm();
57169
+ init_src4();
57170
+ init_src();
57171
+ init_helpers_index();
57172
+ init_tables_index();
57173
+ init_spans();
57174
+ init_game2();
57175
+ init_errors();
57176
+ init_baseline_validation_util();
57177
+ init_deployment_util();
57178
+ init_migration_util();
57179
+ init_secrets_util();
57180
+ BLOCKED_ALERT_DEDUP_MS = 600000;
57181
+ });
54994
57182
 
54995
57183
  class DomainService {
54996
57184
  deps;
@@ -55355,6 +57543,7 @@ var init_kv_service = __esm(() => {
55355
57543
 
55356
57544
  class SecretsService {
55357
57545
  deps;
57546
+ pepperPromise = null;
55358
57547
  constructor(deps) {
55359
57548
  this.deps = deps;
55360
57549
  }
@@ -55367,36 +57556,69 @@ class SecretsService {
55367
57556
  getGameDeploymentId(slug) {
55368
57557
  return getGameDeploymentId(slug, this.deps.config.sstStage);
55369
57558
  }
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")) {
57559
+ getPepper() {
57560
+ const pepperSecret = this.deps.config.secretsManifestPepper;
57561
+ if (!pepperSecret) {
57562
+ throw new ValidationError("Secrets manifest is not configured (missing manifest pepper secret)");
57563
+ }
57564
+ this.pepperPromise ??= deriveSecretsManifestPepper(pepperSecret);
57565
+ return this.pepperPromise;
57566
+ }
57567
+ async computeManifestEntries(gameId, secrets) {
57568
+ const pepper = await this.getPepper();
57569
+ const entries = {};
57570
+ for (const [key, value] of Object.entries(secrets)) {
57571
+ entries[key] = await computeSecretDigest(pepper, { gameId, key, value });
57572
+ }
57573
+ return entries;
57574
+ }
57575
+ async readManifest(gameId) {
57576
+ const state = await this.deps.db.query.gameDeploymentState.findFirst({
57577
+ where: eq(gameDeploymentState.gameId, gameId),
57578
+ columns: { secretsManifest: true }
57579
+ });
57580
+ return state?.secretsManifest ?? {};
57581
+ }
57582
+ async upsertManifestEntries(gameId, entries) {
57583
+ const merged = sql`coalesce(${gameDeploymentState.secretsManifest}, '{}'::jsonb) || ${JSON.stringify(entries)}::jsonb`;
57584
+ await this.deps.db.insert(gameDeploymentState).values({ gameId, secretsManifest: entries, updatedAt: new Date }).onConflictDoUpdate({
57585
+ target: gameDeploymentState.gameId,
57586
+ set: { secretsManifest: merged, updatedAt: new Date }
57587
+ });
57588
+ }
57589
+ async removeManifestKey(gameId, key) {
57590
+ await this.deps.db.update(gameDeploymentState).set({
57591
+ secretsManifest: sql`coalesce(${gameDeploymentState.secretsManifest}, '{}'::jsonb) - ${key}::text`,
57592
+ updatedAt: new Date
57593
+ }).where(eq(gameDeploymentState.gameId, gameId));
57594
+ }
57595
+ assertNoReservedKeys(keys, operation) {
57596
+ for (const key of keys) {
57597
+ if (INTERNAL_SECRET_KEYS.includes(key)) {
55386
57598
  setAttributes({
55387
- "app.secrets.operation": "list",
55388
- "app.secrets.count": 0,
55389
- "app.secrets.game_deployed": false
57599
+ "app.secrets.operation": operation,
57600
+ "app.secrets.reserved_key_rejected": true
55390
57601
  });
55391
- return [];
57602
+ throw new ValidationError(operation === "set" ? `Cannot set reserved secret "${key}"` : `Reserved secret "${key}" cannot be managed — remove it locally`);
55392
57603
  }
55393
- throw error;
55394
57604
  }
55395
57605
  }
55396
- async setSecrets(slug, newSecrets, user) {
57606
+ async listKeys(slug, user) {
55397
57607
  await this.deps.validateDeveloperAccessBySlug(user, slug);
55398
57608
  const cf = this.getCloudflare();
55399
57609
  const deploymentId = this.getGameDeploymentId(slug);
57610
+ const userKeys = await listUserSecretKeys(cf, deploymentId);
57611
+ setAttributes({
57612
+ "app.secrets.operation": "list",
57613
+ "app.secrets.count": userKeys?.length ?? 0,
57614
+ "app.secrets.game_deployed": userKeys !== null
57615
+ });
57616
+ return userKeys ?? [];
57617
+ }
57618
+ async setSecrets(slug, newSecrets, user) {
57619
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
57620
+ const cf = this.getCloudflare();
57621
+ const deploymentId = this.getGameDeploymentId(slug);
55400
57622
  const secretKeys = Object.keys(newSecrets);
55401
57623
  if (secretKeys.length === 0) {
55402
57624
  throw new ValidationError("At least one secret must be provided");
@@ -55405,14 +57627,9 @@ class SecretsService {
55405
57627
  if (typeof value !== "string") {
55406
57628
  throw new ValidationError(`Secret value for "${key}" must be a string`);
55407
57629
  }
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
57630
  }
57631
+ this.assertNoReservedKeys(secretKeys, "set");
57632
+ const manifestEntries = await this.computeManifestEntries(game2.id, newSecrets);
55416
57633
  try {
55417
57634
  const prefixedSecrets = {};
55418
57635
  for (const [key, value] of Object.entries(newSecrets)) {
@@ -55425,8 +57642,6 @@ class SecretsService {
55425
57642
  "app.secrets.game_deployed": true,
55426
57643
  "app.secrets.reserved_key_rejected": false
55427
57644
  });
55428
- const allKeys = await cf.listSecrets(deploymentId);
55429
- return allKeys.filter((k) => k.startsWith(SECRETS_PREFIX)).map((k) => k.slice(SECRETS_PREFIX.length));
55430
57645
  } catch (error) {
55431
57646
  const message = errorMessage2(error);
55432
57647
  if (message.includes("not found") || message.includes("10007")) {
@@ -55438,6 +57653,9 @@ class SecretsService {
55438
57653
  }
55439
57654
  throw error;
55440
57655
  }
57656
+ await this.upsertManifestEntries(game2.id, manifestEntries);
57657
+ const allKeys = await cf.listSecrets(deploymentId);
57658
+ return allKeys.filter((k) => k.startsWith(SECRETS_PREFIX)).map((k) => k.slice(SECRETS_PREFIX.length));
55441
57659
  }
55442
57660
  async deleteSecret(slug, key, user) {
55443
57661
  if (INTERNAL_SECRET_KEYS.includes(key)) {
@@ -55447,19 +57665,25 @@ class SecretsService {
55447
57665
  });
55448
57666
  throw new ValidationError(`Cannot delete reserved secret "${key}"`);
55449
57667
  }
55450
- await this.deps.validateDeveloperAccessBySlug(user, slug);
57668
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
55451
57669
  const cf = this.getCloudflare();
55452
57670
  const deploymentId = this.getGameDeploymentId(slug);
57671
+ const manifest = await this.readManifest(game2.id);
57672
+ const managed = key in manifest;
55453
57673
  try {
55454
57674
  const prefixedKey = `${SECRETS_PREFIX}${key}`;
55455
57675
  const existingKeys = await cf.listSecrets(deploymentId);
55456
- if (!existingKeys.includes(prefixedKey)) {
57676
+ const onWorker = existingKeys.includes(prefixedKey);
57677
+ if (!onWorker && !managed) {
55457
57678
  throw new NotFoundError("Secret", key);
55458
57679
  }
55459
- await cf.deleteSecret(deploymentId, prefixedKey);
57680
+ if (onWorker) {
57681
+ await cf.deleteSecret(deploymentId, prefixedKey);
57682
+ }
55460
57683
  setAttributes({
55461
57684
  "app.secrets.operation": "delete",
55462
57685
  "app.secrets.game_deployed": true,
57686
+ "app.secrets.managed": managed,
55463
57687
  "app.secrets.reserved_key_rejected": false
55464
57688
  });
55465
57689
  } catch (error) {
@@ -55476,14 +57700,45 @@ class SecretsService {
55476
57700
  }
55477
57701
  throw error;
55478
57702
  }
57703
+ if (managed) {
57704
+ await this.removeManifestKey(game2.id, key);
57705
+ }
57706
+ }
57707
+ async diff(slug, localSecrets, user) {
57708
+ const game2 = await this.deps.validateDeveloperAccessBySlug(user, slug);
57709
+ const deploymentId = this.getGameDeploymentId(slug);
57710
+ this.assertNoReservedKeys(Object.keys(localSecrets), "diff");
57711
+ const [manifest, remoteKeys, localDigests] = await Promise.all([
57712
+ this.readManifest(game2.id),
57713
+ this.deps.cloudflare ? listUserSecretKeys(this.deps.cloudflare, deploymentId) : null,
57714
+ this.computeManifestEntries(game2.id, localSecrets)
57715
+ ]);
57716
+ const verdicts = computeSecretsDiff({
57717
+ localDigests,
57718
+ manifest,
57719
+ remoteKeys: remoteKeys ?? []
57720
+ });
57721
+ setAttributes({
57722
+ "app.secrets.operation": "diff",
57723
+ "app.secrets.game_deployed": remoteKeys !== null,
57724
+ "app.secrets.diff_added": verdicts.added.length,
57725
+ "app.secrets.diff_changed": verdicts.changed.length,
57726
+ "app.secrets.diff_unchanged": verdicts.unchanged.length,
57727
+ "app.secrets.diff_remote_only_managed": verdicts.remoteOnlyManaged.length,
57728
+ "app.secrets.diff_remote_only_unmanaged": verdicts.remoteOnlyUnmanaged.length
57729
+ });
57730
+ return verdicts;
55479
57731
  }
55480
57732
  }
55481
57733
  var INTERNAL_SECRET_KEYS;
55482
57734
  var init_secrets_service = __esm(() => {
57735
+ init_drizzle_orm();
55483
57736
  init_src();
57737
+ init_tables_index();
55484
57738
  init_spans();
55485
57739
  init_errors();
55486
57740
  init_deployment_util();
57741
+ init_secrets_util();
55487
57742
  INTERNAL_SECRET_KEYS = ["PLAYCADEMY_API_KEY", "GAME_ID", "PLAYCADEMY_BASE_URL"];
55488
57743
  });
55489
57744
  function prefixSecrets(secrets) {
@@ -56019,6 +58274,9 @@ var SINGLE_EMOJI_REGEX;
56019
58274
  var init_emoji = __esm(() => {
56020
58275
  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
58276
  });
58277
+ function requestsBinding(binding) {
58278
+ return binding === true || Array.isArray(binding) && binding.length > 0;
58279
+ }
56022
58280
  var HttpUrlSchema;
56023
58281
  var GameEmojiSchema;
56024
58282
  var GameMetadataRecordSchema;
@@ -56026,6 +58284,7 @@ var InsertGameSchema;
56026
58284
  var UpdateGameSchema;
56027
58285
  var InsertGameDeploymentSchema;
56028
58286
  var InsertGameDeployJobSchema;
58287
+ var InsertGameDeploymentStateSchema;
56029
58288
  var UpsertGameMetadataSchema;
56030
58289
  var PatchGameMetadataSchema;
56031
58290
  var AddGameMemberSchema;
@@ -56038,11 +58297,22 @@ var ALLOWED_UPLOAD_EXTENSIONS;
56038
58297
  var InitiateUploadSchema;
56039
58298
  var AddCustomHostnameSchema;
56040
58299
  var SetSecretsRequestSchema;
58300
+ var SecretsDiffRequestSchema;
56041
58301
  var SeedRequestSchema;
56042
58302
  var SchemaInfoSchema;
56043
- var DatabaseResetRequestSchema;
56044
58303
  var VerifyTokenSchema;
56045
58304
  var KVSeedRequestSchema;
58305
+ var DeployMigrationSchema;
58306
+ var DeployDatabaseSchema;
58307
+ var DatabaseResetDatabaseSchema;
58308
+ var DatabaseResetRequestSchema;
58309
+ var BaselineEvidenceSchema;
58310
+ var DeployBaselineSchema;
58311
+ var DeploymentStateBaselineSchema;
58312
+ var MigrationRealignSchema;
58313
+ var MigrationResolveSchema;
58314
+ var DatabaseRestoreSchema;
58315
+ var DeployBlockedReportSchema;
56046
58316
  var DeployRequestSchema;
56047
58317
  var init_schemas2 = __esm(() => {
56048
58318
  init_drizzle_zod();
@@ -56122,6 +58392,9 @@ var init_schemas2 = __esm(() => {
56122
58392
  InsertGameDeployJobSchema = createInsertSchema(gameDeployJobs, {
56123
58393
  status: exports_external.enum(deployJobStatusEnum.enumValues)
56124
58394
  });
58395
+ InsertGameDeploymentStateSchema = createInsertSchema(gameDeploymentState, {
58396
+ secretsManifest: exports_external.record(exports_external.string(), exports_external.string()).nullable().optional()
58397
+ });
56125
58398
  UpsertGameMetadataSchema = exports_external.object({
56126
58399
  displayName: exports_external.string().min(1),
56127
58400
  platform: exports_external.enum(gamePlatformEnum.enumValues),
@@ -56179,6 +58452,9 @@ var init_schemas2 = __esm(() => {
56179
58452
  hostname: exports_external.string().min(1).max(255)
56180
58453
  });
56181
58454
  SetSecretsRequestSchema = exports_external.record(exports_external.string().min(1), exports_external.string());
58455
+ SecretsDiffRequestSchema = exports_external.object({
58456
+ secrets: exports_external.record(exports_external.string().min(1), exports_external.string())
58457
+ });
56182
58458
  SeedRequestSchema = exports_external.object({
56183
58459
  code: exports_external.string().min(1, "Seed code is required"),
56184
58460
  secrets: exports_external.record(exports_external.string(), exports_external.string()).optional()
@@ -56187,9 +58463,6 @@ var init_schemas2 = __esm(() => {
56187
58463
  sql: exports_external.string(),
56188
58464
  hash: exports_external.string()
56189
58465
  });
56190
- DatabaseResetRequestSchema = exports_external.object({
56191
- schema: SchemaInfoSchema.optional()
56192
- });
56193
58466
  VerifyTokenSchema = exports_external.object({
56194
58467
  token: exports_external.string().min(1, "Token is required")
56195
58468
  });
@@ -56201,8 +58474,126 @@ var init_schemas2 = __esm(() => {
56201
58474
  metadata: exports_external.record(exports_external.unknown()).optional()
56202
58475
  }))
56203
58476
  });
58477
+ DeployMigrationSchema = exports_external.object({
58478
+ tag: exports_external.string().min(1),
58479
+ statements: exports_external.array(exports_external.string().min(1)).min(1),
58480
+ checksum: exports_external.string().min(1)
58481
+ });
58482
+ DeployDatabaseSchema = exports_external.discriminatedUnion("mode", [
58483
+ exports_external.object({
58484
+ mode: exports_external.literal("push"),
58485
+ sql: exports_external.string(),
58486
+ baselineHash: exports_external.string().nullable(),
58487
+ nextSnapshot: exports_external.record(exports_external.string(), exports_external.unknown()),
58488
+ nextHash: exports_external.string().min(1),
58489
+ acceptDataLoss: exports_external.boolean().optional()
58490
+ }),
58491
+ exports_external.object({
58492
+ mode: exports_external.literal("migrate"),
58493
+ migrations: exports_external.array(DeployMigrationSchema)
58494
+ })
58495
+ ]);
58496
+ DatabaseResetDatabaseSchema = exports_external.discriminatedUnion("mode", [
58497
+ exports_external.object({
58498
+ mode: exports_external.literal("push"),
58499
+ sql: exports_external.string(),
58500
+ nextSnapshot: exports_external.record(exports_external.string(), exports_external.unknown()),
58501
+ nextHash: exports_external.string().min(1)
58502
+ }),
58503
+ exports_external.object({
58504
+ mode: exports_external.literal("migrate"),
58505
+ migrations: exports_external.array(DeployMigrationSchema)
58506
+ })
58507
+ ]);
58508
+ DatabaseResetRequestSchema = exports_external.object({
58509
+ schema: SchemaInfoSchema.optional(),
58510
+ database: DatabaseResetDatabaseSchema.optional()
58511
+ }).refine((data) => !(data.schema && data.database), {
58512
+ message: "Send either the database payload or the legacy schema field, not both",
58513
+ path: ["database"]
58514
+ });
58515
+ BaselineEvidenceSchema = exports_external.array(exports_external.object({
58516
+ tag: exports_external.string().min(1),
58517
+ generatedAt: exports_external.string().min(1),
58518
+ createsTables: exports_external.array(exports_external.object({
58519
+ name: exports_external.string().min(1),
58520
+ columns: exports_external.array(exports_external.string())
58521
+ })),
58522
+ addsColumns: exports_external.array(exports_external.object({
58523
+ table: exports_external.string().min(1),
58524
+ column: exports_external.string().min(1)
58525
+ })),
58526
+ createsIndexes: exports_external.array(exports_external.string()),
58527
+ createsViews: exports_external.array(exports_external.string()),
58528
+ dropsTables: exports_external.array(exports_external.string()),
58529
+ dropsColumns: exports_external.array(exports_external.object({
58530
+ table: exports_external.string().min(1),
58531
+ column: exports_external.string().min(1)
58532
+ })),
58533
+ dropsIndexes: exports_external.array(exports_external.string()),
58534
+ dropsViews: exports_external.array(exports_external.string())
58535
+ })).optional();
58536
+ DeployBaselineSchema = exports_external.object({
58537
+ lastAppliedMigrationTag: exports_external.string().min(1).optional(),
58538
+ journal: exports_external.array(exports_external.object({
58539
+ tag: exports_external.string().min(1),
58540
+ checksum: exports_external.string().min(1)
58541
+ })).optional(),
58542
+ evidence: BaselineEvidenceSchema,
58543
+ schemaSnapshot: exports_external.unknown().optional(),
58544
+ schemaHash: exports_external.string().min(1).optional(),
58545
+ integrationsHash: exports_external.string().min(1).optional(),
58546
+ buildHash: exports_external.string().min(1).optional()
58547
+ }).refine((data) => !data.lastAppliedMigrationTag === !data.journal, {
58548
+ message: "lastAppliedMigrationTag and journal must be provided together",
58549
+ path: ["journal"]
58550
+ }).refine((data) => data.schemaSnapshot === undefined === !data.schemaHash, {
58551
+ message: "schemaSnapshot and schemaHash must be provided together",
58552
+ path: ["schemaHash"]
58553
+ }).refine((data) => Boolean(data.lastAppliedMigrationTag || data.schemaHash || data.integrationsHash || data.buildHash), {
58554
+ message: "A baseline must claim something — migration history, a schema snapshot, or artifact hashes",
58555
+ path: ["lastAppliedMigrationTag"]
58556
+ });
58557
+ DeploymentStateBaselineSchema = exports_external.object({
58558
+ lastAppliedMigrationTag: exports_external.string().min(1).optional(),
58559
+ journal: exports_external.array(exports_external.object({
58560
+ tag: exports_external.string().min(1),
58561
+ checksum: exports_external.string().min(1)
58562
+ })).optional(),
58563
+ schemaSnapshot: exports_external.unknown().optional(),
58564
+ schemaHash: exports_external.string().min(1).optional(),
58565
+ evidence: BaselineEvidenceSchema,
58566
+ allowUnverified: exports_external.boolean().optional()
58567
+ }).refine((data) => !data.lastAppliedMigrationTag === !data.journal, {
58568
+ message: "lastAppliedMigrationTag and journal must be provided together",
58569
+ path: ["journal"]
58570
+ }).refine((data) => data.schemaSnapshot === undefined === !data.schemaHash, {
58571
+ message: "schemaSnapshot and schemaHash must be provided together",
58572
+ path: ["schemaHash"]
58573
+ }).refine((data) => Boolean(data.lastAppliedMigrationTag) || Boolean(data.schemaHash), {
58574
+ message: "A baseline needs a migration claim, a schema snapshot, or both",
58575
+ path: ["lastAppliedMigrationTag"]
58576
+ });
58577
+ MigrationRealignSchema = exports_external.object({
58578
+ checksum: exports_external.string().min(1)
58579
+ });
58580
+ MigrationResolveSchema = exports_external.object({
58581
+ resolution: exports_external.enum(["applied", "rolled-back"]),
58582
+ checksum: exports_external.string().min(1).optional()
58583
+ }).refine((data) => data.resolution !== "applied" || Boolean(data.checksum), {
58584
+ message: "Resolving a migration as 'applied' requires its checksum",
58585
+ path: ["checksum"]
58586
+ });
58587
+ DatabaseRestoreSchema = exports_external.object({
58588
+ restorePointId: exports_external.string().uuid()
58589
+ });
58590
+ DeployBlockedReportSchema = exports_external.object({
58591
+ code: exports_external.string().regex(/^blocked:[a-z-]+$/).max(64),
58592
+ reason: exports_external.string().min(1).max(2000)
58593
+ });
56204
58594
  DeployRequestSchema = exports_external.object({
56205
58595
  target: exports_external.enum(deploymentTargetEnum.enumValues).optional().default("game"),
58596
+ deployId: exports_external.string().min(1).optional(),
56206
58597
  uploadToken: exports_external.string().optional(),
56207
58598
  code: exports_external.string().optional(),
56208
58599
  codeUploadToken: exports_external.string().optional(),
@@ -56229,6 +58620,11 @@ var init_schemas2 = __esm(() => {
56229
58620
  sql: exports_external.string(),
56230
58621
  hash: exports_external.string()
56231
58622
  }).optional(),
58623
+ database: DeployDatabaseSchema.optional(),
58624
+ baseline: DeployBaselineSchema.optional(),
58625
+ buildHash: exports_external.string().min(1).optional(),
58626
+ integrationsHash: exports_external.string().min(1).optional(),
58627
+ pruneSecrets: exports_external.array(exports_external.string().min(1)).optional(),
56232
58628
  metadata: exports_external.object({
56233
58629
  displayName: exports_external.string().optional(),
56234
58630
  description: exports_external.string().optional(),
@@ -56238,9 +58634,24 @@ var init_schemas2 = __esm(() => {
56238
58634
  }).refine((data) => !(data.code && data.codeUploadToken), {
56239
58635
  message: "Specify either code or codeUploadToken, not both",
56240
58636
  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",
58637
+ }).refine((data) => !(data.target === "dashboard" && (data.schema || data.bindings || data.database)), {
58638
+ message: "Dashboard deployments cannot include schema, database, or bindings — they attach to the game deployment’s existing resources",
56243
58639
  path: ["target"]
58640
+ }).refine((data) => !(data.target === "dashboard" && (data.baseline || data.pruneSecrets)), {
58641
+ message: "Dashboard deployments cannot adopt a baseline or prune secrets",
58642
+ path: ["target"]
58643
+ }).refine((data) => !(data.database && !data.deployId), {
58644
+ message: "deployId is required when a database payload is present",
58645
+ path: ["deployId"]
58646
+ }).refine((data) => !(data.database && data.schema), {
58647
+ message: "Send either the database payload or the legacy schema field, not both",
58648
+ path: ["database"]
58649
+ }).refine((data) => !(data.baseline && data.schema), {
58650
+ message: "The legacy schema field cannot ride a baseline-adopting deploy",
58651
+ path: ["baseline"]
58652
+ }).refine((data) => !(data.database && !requestsBinding(data.bindings?.database)), {
58653
+ message: "The database payload requires a database binding",
58654
+ path: ["database"]
56244
58655
  });
56245
58656
  });
56246
58657
  var LeaderboardQuerySchema;
@@ -101209,7 +103620,7 @@ var init_pure = __esm(() => {
101209
103620
  init_log();
101210
103621
  init_spinner();
101211
103622
  });
101212
- var init_src4 = __esm(() => {
103623
+ var init_src5 = __esm(() => {
101213
103624
  init_pure();
101214
103625
  });
101215
103626
  function createOneRosterUrls(baseUrl) {
@@ -103185,7 +105596,7 @@ var init_dist5 = __esm(async () => {
103185
105596
  init_spans();
103186
105597
  init_src();
103187
105598
  init_spans();
103188
- init_src4();
105599
+ init_src5();
103189
105600
  init_src();
103190
105601
  init_spans();
103191
105602
  init_spans();
@@ -103760,7 +106171,7 @@ function selectTimebackMetricDiscrepancyQueueItems(candidates, options) {
103760
106171
  }
103761
106172
  var DATE_INPUT_RE;
103762
106173
  var init_timeback_discrepancy_queue_util = __esm(() => {
103763
- init_src4();
106174
+ init_src5();
103764
106175
  init_timeback_util();
103765
106176
  DATE_INPUT_RE = /^\d{4}-\d{2}-\d{2}$/;
103766
106177
  });
@@ -106273,7 +108684,7 @@ var init_timeback_admin_service = __esm(async () => {
106273
108684
  init_constants3();
106274
108685
  init_types2();
106275
108686
  init_utils6();
106276
- init_src4();
108687
+ init_src5();
106277
108688
  init_timeback3();
106278
108689
  init_errors();
106279
108690
  init_timeback_admin_metrics_util();
@@ -108707,8 +111118,15 @@ function createPlatformServices(deps) {
108707
111118
  validateDeveloperAccess
108708
111119
  });
108709
111120
  const kv = new KVService({ db: db2, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
108710
- const secrets = new SecretsService({ config: config4, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
111121
+ const secrets = new SecretsService({ db: db2, config: config4, cloudflare: cloudflare2, validateDeveloperAccessBySlug });
108711
111122
  const domain3 = new DomainService({ db: db2, cloudflare: cloudflare2, corsKvs, validateDeveloperAccessBySlug });
111123
+ const deploymentState = new DeploymentStateService({
111124
+ db: db2,
111125
+ config: config4,
111126
+ cloudflare: cloudflare2,
111127
+ alerts,
111128
+ validateDeveloperAccessBySlug
111129
+ });
108712
111130
  const database = new DatabaseService({
108713
111131
  db: db2,
108714
111132
  config: config4,
@@ -108747,6 +111165,7 @@ function createPlatformServices(deps) {
108747
111165
  kv,
108748
111166
  secrets,
108749
111167
  domain: domain3,
111168
+ deploymentState,
108750
111169
  database,
108751
111170
  seed,
108752
111171
  timeback: timeback2,
@@ -108757,6 +111176,7 @@ function createPlatformServices(deps) {
108757
111176
  var init_platform2 = __esm(async () => {
108758
111177
  init_bucket_service();
108759
111178
  init_database_service();
111179
+ init_deployment_state_service();
108760
111180
  init_domain_service();
108761
111181
  init_kv_service();
108762
111182
  init_secrets_service();
@@ -109474,7 +111894,7 @@ function createServices(ctx) {
109474
111894
  };
109475
111895
  }
109476
111896
  var init_factory = __esm(async () => {
109477
- init_game2();
111897
+ init_game3();
109478
111898
  init_infra2();
109479
111899
  init_player();
109480
111900
  init_standalone();
@@ -109767,6 +112187,7 @@ function buildConfig(options) {
109767
112187
  gameDomain: "localhost",
109768
112188
  uploadBucket: "sandbox-uploads",
109769
112189
  ltiTestMode: true,
112190
+ secretsManifestPepper: "sandbox-secrets-manifest-pepper",
109770
112191
  ...options.config
109771
112192
  });
109772
112193
  }
@@ -127811,7 +130232,7 @@ var _a27;
127811
130232
  var _b12;
127812
130233
  var _c2;
127813
130234
  var View3;
127814
- var init_sql3;
130235
+ var init_sql4;
127815
130236
  var _a28;
127816
130237
  var ColumnAliasProxyHandler2;
127817
130238
  var _a29;
@@ -128304,7 +130725,7 @@ var PgSequence;
128304
130725
  var init_sequence2;
128305
130726
  var _a164;
128306
130727
  var PgSchema5;
128307
- var init_schema3;
130728
+ var init_schema4;
128308
130729
  var _a165;
128309
130730
  var Cache;
128310
130731
  var _a166;
@@ -149783,7 +152204,7 @@ globstar while`, file3, fr, pattern, pr2, swallowee);
149783
152204
  });
149784
152205
  }
149785
152206
  });
149786
- init_sql3 = __esm2({
152207
+ init_sql4 = __esm2({
149787
152208
  "../drizzle-orm/dist/sql/sql.js"() {
149788
152209
  init_entity2();
149789
152210
  init_enum2();
@@ -150136,7 +152557,7 @@ globstar while`, file3, fr, pattern, pr2, swallowee);
150136
152557
  "../drizzle-orm/dist/alias.js"() {
150137
152558
  init_column2();
150138
152559
  init_entity2();
150139
- init_sql3();
152560
+ init_sql4();
150140
152561
  init_table8();
150141
152562
  init_view_common3();
150142
152563
  _a28 = entityKind2;
@@ -150310,7 +152731,7 @@ params: ${params}`);
150310
152731
  "../drizzle-orm/dist/utils.js"() {
150311
152732
  init_column2();
150312
152733
  init_entity2();
150313
- init_sql3();
152734
+ init_sql4();
150314
152735
  init_subquery2();
150315
152736
  init_table8();
150316
152737
  init_view_common3();
@@ -150569,7 +152990,7 @@ params: ${params}`);
150569
152990
  init_date_common2 = __esm2({
150570
152991
  "../drizzle-orm/dist/pg-core/columns/date.common.js"() {
150571
152992
  init_entity2();
150572
- init_sql3();
152993
+ init_sql4();
150573
152994
  init_common22();
150574
152995
  PgDateColumnBaseBuilder2 = class extends (_b33 = PgColumnBuilder2, _a54 = entityKind2, _b33) {
150575
152996
  defaultNow() {
@@ -151354,7 +153775,7 @@ params: ${params}`);
151354
153775
  init_uuid3 = __esm2({
151355
153776
  "../drizzle-orm/dist/pg-core/columns/uuid.js"() {
151356
153777
  init_entity2();
151357
- init_sql3();
153778
+ init_sql4();
151358
153779
  init_common22();
151359
153780
  PgUUIDBuilder2 = class extends (_b88 = PgColumnBuilder2, _a109 = entityKind2, _b88) {
151360
153781
  constructor(name22) {
@@ -151625,7 +154046,7 @@ params: ${params}`);
151625
154046
  init_column2();
151626
154047
  init_entity2();
151627
154048
  init_table8();
151628
- init_sql3();
154049
+ init_sql4();
151629
154050
  eq2 = (left, right) => {
151630
154051
  return sql4`${left} = ${bindIfParam2(right, left)}`;
151631
154052
  };
@@ -151648,7 +154069,7 @@ params: ${params}`);
151648
154069
  });
151649
154070
  init_select3 = __esm2({
151650
154071
  "../drizzle-orm/dist/sql/expressions/select.js"() {
151651
- init_sql3();
154072
+ init_sql4();
151652
154073
  }
151653
154074
  });
151654
154075
  init_expressions2 = __esm2({
@@ -151664,7 +154085,7 @@ params: ${params}`);
151664
154085
  init_entity2();
151665
154086
  init_primary_keys2();
151666
154087
  init_expressions2();
151667
- init_sql3();
154088
+ init_sql4();
151668
154089
  _a124 = entityKind2;
151669
154090
  Relation2 = class {
151670
154091
  constructor(sourceTable, referencedTable, relationName) {
@@ -151718,12 +154139,12 @@ params: ${params}`);
151718
154139
  "../drizzle-orm/dist/sql/functions/aggregate.js"() {
151719
154140
  init_column2();
151720
154141
  init_entity2();
151721
- init_sql3();
154142
+ init_sql4();
151722
154143
  }
151723
154144
  });
151724
154145
  init_vector22 = __esm2({
151725
154146
  "../drizzle-orm/dist/sql/functions/vector.js"() {
151726
- init_sql3();
154147
+ init_sql4();
151727
154148
  }
151728
154149
  });
151729
154150
  init_functions2 = __esm2({
@@ -151736,7 +154157,7 @@ params: ${params}`);
151736
154157
  "../drizzle-orm/dist/sql/index.js"() {
151737
154158
  init_expressions2();
151738
154159
  init_functions2();
151739
- init_sql3();
154160
+ init_sql4();
151740
154161
  }
151741
154162
  });
151742
154163
  dist_exports = {};
@@ -151953,7 +154374,7 @@ params: ${params}`);
151953
154374
  init_alias3();
151954
154375
  init_column2();
151955
154376
  init_entity2();
151956
- init_sql3();
154377
+ init_sql4();
151957
154378
  init_subquery2();
151958
154379
  init_view_common3();
151959
154380
  _a130 = entityKind2;
@@ -152012,7 +154433,7 @@ params: ${params}`);
152012
154433
  });
152013
154434
  init_indexes2 = __esm2({
152014
154435
  "../drizzle-orm/dist/pg-core/indexes.js"() {
152015
- init_sql3();
154436
+ init_sql4();
152016
154437
  init_entity2();
152017
154438
  init_columns2();
152018
154439
  _a131 = entityKind2;
@@ -152175,7 +154596,7 @@ params: ${params}`);
152175
154596
  init_view_base2 = __esm2({
152176
154597
  "../drizzle-orm/dist/pg-core/view-base.js"() {
152177
154598
  init_entity2();
152178
- init_sql3();
154599
+ init_sql4();
152179
154600
  PgViewBase2 = class extends (_b103 = View3, _a136 = entityKind2, _b103) {
152180
154601
  };
152181
154602
  __publicField(PgViewBase2, _a136, "PgViewBase");
@@ -152192,7 +154613,7 @@ params: ${params}`);
152192
154613
  init_table22();
152193
154614
  init_relations2();
152194
154615
  init_sql22();
152195
- init_sql3();
154616
+ init_sql4();
152196
154617
  init_subquery2();
152197
154618
  init_table8();
152198
154619
  init_utils22();
@@ -152776,7 +155197,7 @@ params: ${params}`);
152776
155197
  init_query_builder3();
152777
155198
  init_query_promise2();
152778
155199
  init_selection_proxy2();
152779
- init_sql3();
155200
+ init_sql4();
152780
155201
  init_subquery2();
152781
155202
  init_table8();
152782
155203
  init_tracing2();
@@ -153397,7 +155818,7 @@ params: ${params}`);
153397
155818
  "../drizzle-orm/dist/pg-core/utils.js"() {
153398
155819
  init_entity2();
153399
155820
  init_table22();
153400
- init_sql3();
155821
+ init_sql4();
153401
155822
  init_subquery2();
153402
155823
  init_table8();
153403
155824
  init_view_common3();
@@ -153485,7 +155906,7 @@ params: ${params}`);
153485
155906
  init_entity2();
153486
155907
  init_query_promise2();
153487
155908
  init_selection_proxy2();
153488
- init_sql3();
155909
+ init_sql4();
153489
155910
  init_table8();
153490
155911
  init_tracing2();
153491
155912
  init_utils22();
@@ -153679,7 +156100,7 @@ params: ${params}`);
153679
156100
  init_table22();
153680
156101
  init_query_promise2();
153681
156102
  init_selection_proxy2();
153682
- init_sql3();
156103
+ init_sql4();
153683
156104
  init_subquery2();
153684
156105
  init_table8();
153685
156106
  init_utils22();
@@ -153853,7 +156274,7 @@ params: ${params}`);
153853
156274
  init_count2 = __esm2({
153854
156275
  "../drizzle-orm/dist/pg-core/query-builders/count.js"() {
153855
156276
  init_entity2();
153856
- init_sql3();
156277
+ init_sql4();
153857
156278
  _PgCountBuilder = class _PgCountBuilder2 extends (_c6 = SQL2, _b116 = entityKind2, _a157 = Symbol.toStringTag, _c6) {
153858
156279
  constructor(params) {
153859
156280
  super(_PgCountBuilder2.buildEmbeddedCount(params.source, params.filters).queryChunks);
@@ -154021,7 +156442,7 @@ params: ${params}`);
154021
156442
  init_entity2();
154022
156443
  init_query_builders2();
154023
156444
  init_selection_proxy2();
154024
- init_sql3();
156445
+ init_sql4();
154025
156446
  init_subquery2();
154026
156447
  init_count2();
154027
156448
  init_query2();
@@ -154193,10 +156614,10 @@ params: ${params}`);
154193
156614
  __publicField(PgSequence, _a163, "PgSequence");
154194
156615
  }
154195
156616
  });
154196
- init_schema3 = __esm2({
156617
+ init_schema4 = __esm2({
154197
156618
  "../drizzle-orm/dist/pg-core/schema.js"() {
154198
156619
  init_entity2();
154199
- init_sql3();
156620
+ init_sql4();
154200
156621
  init_enum2();
154201
156622
  init_sequence2();
154202
156623
  init_table22();
@@ -154412,7 +156833,7 @@ params: ${params}`);
154412
156833
  init_primary_keys2();
154413
156834
  init_query_builders2();
154414
156835
  init_roles2();
154415
- init_schema3();
156836
+ init_schema4();
154416
156837
  init_sequence2();
154417
156838
  init_session3();
154418
156839
  init_subquery22();
@@ -156220,7 +158641,7 @@ ORDER BY
156220
158641
  init_integer22 = __esm2({
156221
158642
  "../drizzle-orm/dist/sqlite-core/columns/integer.js"() {
156222
158643
  init_entity2();
156223
- init_sql3();
158644
+ init_sql4();
156224
158645
  init_utils22();
156225
158646
  init_common3();
156226
158647
  SQLiteBaseIntegerBuilder = class extends (_b131 = SQLiteColumnBuilder, _a187 = entityKind2, _b131) {
@@ -156583,7 +159004,7 @@ ORDER BY
156583
159004
  init_utils72 = __esm2({
156584
159005
  "../drizzle-orm/dist/sqlite-core/utils.js"() {
156585
159006
  init_entity2();
156586
- init_sql3();
159007
+ init_sql4();
156587
159008
  init_subquery2();
156588
159009
  init_table8();
156589
159010
  init_view_common3();
@@ -156677,7 +159098,7 @@ ORDER BY
156677
159098
  init_view_base22 = __esm2({
156678
159099
  "../drizzle-orm/dist/sqlite-core/view-base.js"() {
156679
159100
  init_entity2();
156680
- init_sql3();
159101
+ init_sql4();
156681
159102
  SQLiteViewBase = class extends (_b153 = View3, _a214 = entityKind2, _b153) {
156682
159103
  };
156683
159104
  __publicField(SQLiteViewBase, _a214, "SQLiteViewBase");
@@ -156692,7 +159113,7 @@ ORDER BY
156692
159113
  init_errors22();
156693
159114
  init_relations2();
156694
159115
  init_sql22();
156695
- init_sql3();
159116
+ init_sql4();
156696
159117
  init_columns22();
156697
159118
  init_table32();
156698
159119
  init_subquery2();
@@ -157272,7 +159693,7 @@ ORDER BY
157272
159693
  init_query_builder3();
157273
159694
  init_query_promise2();
157274
159695
  init_selection_proxy2();
157275
- init_sql3();
159696
+ init_sql4();
157276
159697
  init_subquery2();
157277
159698
  init_table8();
157278
159699
  init_utils22();
@@ -157635,7 +160056,7 @@ ORDER BY
157635
160056
  "../drizzle-orm/dist/sqlite-core/query-builders/insert.js"() {
157636
160057
  init_entity2();
157637
160058
  init_query_promise2();
157638
- init_sql3();
160059
+ init_sql4();
157639
160060
  init_table32();
157640
160061
  init_table8();
157641
160062
  init_utils22();
@@ -157882,7 +160303,7 @@ ORDER BY
157882
160303
  init_count22 = __esm2({
157883
160304
  "../drizzle-orm/dist/sqlite-core/query-builders/count.js"() {
157884
160305
  init_entity2();
157885
- init_sql3();
160306
+ init_sql4();
157886
160307
  _SQLiteCountBuilder = class _SQLiteCountBuilder2 extends (_c8 = SQL2, _b160 = entityKind2, _a226 = Symbol.toStringTag, _c8) {
157887
160308
  constructor(params) {
157888
160309
  super(_SQLiteCountBuilder2.buildEmbeddedCount(params.source, params.filters).queryChunks);
@@ -158051,7 +160472,7 @@ ORDER BY
158051
160472
  "../drizzle-orm/dist/sqlite-core/db.js"() {
158052
160473
  init_entity2();
158053
160474
  init_selection_proxy2();
158054
- init_sql3();
160475
+ init_sql4();
158055
160476
  init_query_builders22();
158056
160477
  init_subquery2();
158057
160478
  init_count22();
@@ -160022,7 +162443,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
160022
162443
  init_date_common22 = __esm2({
160023
162444
  "../drizzle-orm/dist/mysql-core/columns/date.common.js"() {
160024
162445
  init_entity2();
160025
- init_sql3();
162446
+ init_sql4();
160026
162447
  init_common4();
160027
162448
  MySqlDateColumnBaseBuilder = class extends (_b223 = MySqlColumnBuilder, _a301 = entityKind2, _b223) {
160028
162449
  defaultNow() {
@@ -160248,7 +162669,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
160248
162669
  init_count3 = __esm2({
160249
162670
  "../drizzle-orm/dist/mysql-core/query-builders/count.js"() {
160250
162671
  init_entity2();
160251
- init_sql3();
162672
+ init_sql4();
160252
162673
  _MySqlCountBuilder = class _MySqlCountBuilder2 extends (_c9 = SQL2, _b237 = entityKind2, _a315 = Symbol.toStringTag, _c9) {
160253
162674
  constructor(params) {
160254
162675
  super(_MySqlCountBuilder2.buildEmbeddedCount(params.source, params.filters).queryChunks);
@@ -160510,7 +162931,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
160510
162931
  init_view_base3 = __esm2({
160511
162932
  "../drizzle-orm/dist/mysql-core/view-base.js"() {
160512
162933
  init_entity2();
160513
- init_sql3();
162934
+ init_sql4();
160514
162935
  MySqlViewBase = class extends (_b240 = View3, _a323 = entityKind2, _b240) {
160515
162936
  };
160516
162937
  __publicField(MySqlViewBase, _a323, "MySqlViewBase");
@@ -160525,7 +162946,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
160525
162946
  init_errors22();
160526
162947
  init_relations2();
160527
162948
  init_expressions2();
160528
- init_sql3();
162949
+ init_sql4();
160529
162950
  init_subquery2();
160530
162951
  init_table8();
160531
162952
  init_utils22();
@@ -161291,7 +163712,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
161291
163712
  init_query_builder3();
161292
163713
  init_query_promise2();
161293
163714
  init_selection_proxy2();
161294
- init_sql3();
163715
+ init_sql4();
161295
163716
  init_subquery2();
161296
163717
  init_table8();
161297
163718
  init_utils22();
@@ -161692,7 +164113,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
161692
164113
  "../drizzle-orm/dist/mysql-core/query-builders/insert.js"() {
161693
164114
  init_entity2();
161694
164115
  init_query_promise2();
161695
- init_sql3();
164116
+ init_sql4();
161696
164117
  init_table8();
161697
164118
  init_utils22();
161698
164119
  init_utils8();
@@ -161972,7 +164393,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
161972
164393
  "../drizzle-orm/dist/mysql-core/db.js"() {
161973
164394
  init_entity2();
161974
164395
  init_selection_proxy2();
161975
- init_sql3();
164396
+ init_sql4();
161976
164397
  init_subquery2();
161977
164398
  init_count3();
161978
164399
  init_query_builders3();
@@ -162201,7 +164622,7 @@ ${withStyle.errorWarning(`We've found duplicated view name across ${source_defau
162201
164622
  init_cache();
162202
164623
  init_entity2();
162203
164624
  init_errors22();
162204
- init_sql3();
164625
+ init_sql4();
162205
164626
  init_db3();
162206
164627
  _a341 = entityKind2;
162207
164628
  MySqlPreparedQuery = class {
@@ -164224,7 +166645,7 @@ AND
164224
166645
  init_date_common3 = __esm2({
164225
166646
  "../drizzle-orm/dist/singlestore-core/columns/date.common.js"() {
164226
166647
  init_entity2();
164227
- init_sql3();
166648
+ init_sql4();
164228
166649
  init_common5();
164229
166650
  SingleStoreDateColumnBaseBuilder = class extends (_b302 = SingleStoreColumnBuilder, _a399 = entityKind2, _b302) {
164230
166651
  defaultNow() {
@@ -164249,7 +166670,7 @@ AND
164249
166670
  init_timestamp3 = __esm2({
164250
166671
  "../drizzle-orm/dist/singlestore-core/columns/timestamp.js"() {
164251
166672
  init_entity2();
164252
- init_sql3();
166673
+ init_sql4();
164253
166674
  init_utils22();
164254
166675
  init_date_common3();
164255
166676
  SingleStoreTimestampBuilder = class extends (_b304 = SingleStoreDateColumnBaseBuilder, _a401 = entityKind2, _b304) {
@@ -164484,7 +166905,7 @@ AND
164484
166905
  init_count4 = __esm2({
164485
166906
  "../drizzle-orm/dist/singlestore-core/query-builders/count.js"() {
164486
166907
  init_entity2();
164487
- init_sql3();
166908
+ init_sql4();
164488
166909
  _SingleStoreCountBuilder = class _SingleStoreCountBuilder2 extends (_c12 = SQL2, _b318 = entityKind2, _a415 = Symbol.toStringTag, _c12) {
164489
166910
  constructor(params) {
164490
166911
  super(_SingleStoreCountBuilder2.buildEmbeddedCount(params.source, params.filters).queryChunks);
@@ -164654,7 +167075,7 @@ AND
164654
167075
  init_utils10 = __esm2({
164655
167076
  "../drizzle-orm/dist/singlestore-core/utils.js"() {
164656
167077
  init_entity2();
164657
- init_sql3();
167078
+ init_sql4();
164658
167079
  init_subquery2();
164659
167080
  init_table8();
164660
167081
  init_indexes4();
@@ -164732,7 +167153,7 @@ AND
164732
167153
  "../drizzle-orm/dist/singlestore-core/query-builders/insert.js"() {
164733
167154
  init_entity2();
164734
167155
  init_query_promise2();
164735
- init_sql3();
167156
+ init_sql4();
164736
167157
  init_table8();
164737
167158
  init_utils22();
164738
167159
  init_utils10();
@@ -164829,7 +167250,7 @@ AND
164829
167250
  init_errors22();
164830
167251
  init_relations2();
164831
167252
  init_expressions2();
164832
- init_sql3();
167253
+ init_sql4();
164833
167254
  init_subquery2();
164834
167255
  init_table8();
164835
167256
  init_utils22();
@@ -165360,7 +167781,7 @@ AND
165360
167781
  init_query_builder3();
165361
167782
  init_query_promise2();
165362
167783
  init_selection_proxy2();
165363
- init_sql3();
167784
+ init_sql4();
165364
167785
  init_subquery2();
165365
167786
  init_table8();
165366
167787
  init_utils22();
@@ -165818,7 +168239,7 @@ AND
165818
168239
  "../drizzle-orm/dist/singlestore-core/db.js"() {
165819
168240
  init_entity2();
165820
168241
  init_selection_proxy2();
165821
- init_sql3();
168242
+ init_sql4();
165822
168243
  init_subquery2();
165823
168244
  init_count4();
165824
168245
  init_query_builders4();
@@ -165932,7 +168353,7 @@ AND
165932
168353
  init_cache();
165933
168354
  init_entity2();
165934
168355
  init_errors22();
165935
- init_sql3();
168356
+ init_sql4();
165936
168357
  init_db4();
165937
168358
  _a434 = entityKind2;
165938
168359
  SingleStorePreparedQuery = class {
@@ -168168,7 +170589,7 @@ function requireUserId(userId) {
168168
170589
  return userId;
168169
170590
  }
168170
170591
  var init_params_util = __esm(() => {
168171
- init_src4();
170592
+ init_src5();
168172
170593
  init_errors();
168173
170594
  });
168174
170595
  function formatZodError(error89) {
@@ -168218,6 +170639,7 @@ var init_utils11 = __esm(() => {
168218
170639
  init_lti_util();
168219
170640
  init_lti_provisioning();
168220
170641
  init_params_util();
170642
+ init_secrets_util();
168221
170643
  init_timeback_util();
168222
170644
  init_validation_util();
168223
170645
  });
@@ -168414,7 +170836,7 @@ var init_database_controller = __esm(() => {
168414
170836
  throw ApiError.unprocessableEntity("Validation failed", details);
168415
170837
  }
168416
170838
  }
168417
- return ctx.services.database.reset(slug2, ctx.user, body2.schema);
170839
+ return ctx.services.database.reset(slug2, ctx.user, body2);
168418
170840
  });
168419
170841
  database = defineControllerNames("database", {
168420
170842
  reset
@@ -168464,6 +170886,91 @@ var init_deploy_controller = __esm(() => {
168464
170886
  getJob: requireDeveloper(getJob)
168465
170887
  });
168466
170888
  });
170889
+ var get;
170890
+ var baseline;
170891
+ var realignMigration;
170892
+ var resolveMigration;
170893
+ var history;
170894
+ var restorePoints;
170895
+ var restore;
170896
+ var reportBlocked;
170897
+ var deploymentState;
170898
+ var init_deployment_state_controller = __esm(() => {
170899
+ init_schemas_index();
170900
+ init_errors();
170901
+ init_utils11();
170902
+ get = requireDeveloper(async (ctx) => {
170903
+ const slug2 = requireSlug(ctx.params.slug);
170904
+ const include = ctx.url.searchParams.getAll("include").flatMap((value) => value.split(",")).filter(Boolean);
170905
+ const unsupported = include.filter((value) => value !== "schemaSnapshot");
170906
+ if (unsupported.length > 0) {
170907
+ throw ApiError.badRequest(`Unsupported include value(s): ${unsupported.join(", ")}`);
170908
+ }
170909
+ return ctx.services.deploymentState.get(slug2, ctx.user, {
170910
+ includeSchemaSnapshot: include.includes("schemaSnapshot")
170911
+ });
170912
+ });
170913
+ baseline = requireDeveloper(async (ctx) => {
170914
+ const slug2 = requireSlug(ctx.params.slug);
170915
+ const body2 = await parseRequestBody(ctx.request, DeploymentStateBaselineSchema);
170916
+ return ctx.services.deploymentState.baseline(slug2, body2, ctx.user);
170917
+ });
170918
+ realignMigration = requireDeveloper(async (ctx) => {
170919
+ const slug2 = requireSlug(ctx.params.slug);
170920
+ const tag = ctx.params.tag;
170921
+ if (!tag) {
170922
+ throw ApiError.badRequest("Missing migration tag");
170923
+ }
170924
+ const body2 = await parseRequestBody(ctx.request, MigrationRealignSchema);
170925
+ return ctx.services.deploymentState.realignMigration(slug2, tag, body2.checksum, ctx.user);
170926
+ });
170927
+ resolveMigration = requireDeveloper(async (ctx) => {
170928
+ const slug2 = requireSlug(ctx.params.slug);
170929
+ const tag = ctx.params.tag;
170930
+ if (!tag) {
170931
+ throw ApiError.badRequest("Missing migration tag");
170932
+ }
170933
+ const body2 = await parseRequestBody(ctx.request, MigrationResolveSchema);
170934
+ return ctx.services.deploymentState.resolveMigration(slug2, tag, body2, ctx.user);
170935
+ });
170936
+ history = requireDeveloper(async (ctx) => {
170937
+ const slug2 = requireSlug(ctx.params.slug);
170938
+ const limitParam = ctx.url.searchParams.get("limit");
170939
+ let limit;
170940
+ if (limitParam !== null) {
170941
+ limit = Number(limitParam);
170942
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
170943
+ throw ApiError.badRequest("limit must be an integer between 1 and 100");
170944
+ }
170945
+ }
170946
+ return ctx.services.deploymentState.history(slug2, ctx.user, { limit });
170947
+ });
170948
+ restorePoints = requireDeveloper(async (ctx) => {
170949
+ const slug2 = requireSlug(ctx.params.slug);
170950
+ return ctx.services.deploymentState.restorePoints(slug2, ctx.user);
170951
+ });
170952
+ restore = requireDeveloper(async (ctx) => {
170953
+ const slug2 = requireSlug(ctx.params.slug);
170954
+ const body2 = await parseRequestBody(ctx.request, DatabaseRestoreSchema);
170955
+ return ctx.services.deploymentState.restoreToBookmark(slug2, body2, ctx.user);
170956
+ });
170957
+ reportBlocked = requireDeveloper(async (ctx) => {
170958
+ const slug2 = requireSlug(ctx.params.slug);
170959
+ const body2 = await parseRequestBody(ctx.request, DeployBlockedReportSchema);
170960
+ await ctx.services.deploymentState.reportDeployBlocked(slug2, ctx.user, body2);
170961
+ return { recorded: true };
170962
+ });
170963
+ deploymentState = defineControllerNames("deploymentState", {
170964
+ get,
170965
+ baseline,
170966
+ realignMigration,
170967
+ resolveMigration,
170968
+ history,
170969
+ restorePoints,
170970
+ restore,
170971
+ reportBlocked
170972
+ });
170973
+ });
168467
170974
  var apply;
168468
170975
  var getStatus;
168469
170976
  var developer;
@@ -168953,6 +171460,7 @@ var init_lti_controller = __esm(() => {
168953
171460
  var listKeys2;
168954
171461
  var setSecrets;
168955
171462
  var deleteSecret;
171463
+ var diff;
168956
171464
  var secrets;
168957
171465
  var init_secrets_controller = __esm(() => {
168958
171466
  init_esm();
@@ -168999,10 +171507,19 @@ var init_secrets_controller = __esm(() => {
168999
171507
  await ctx.services.secrets.deleteSecret(slug2, key, ctx.user);
169000
171508
  return { success: true };
169001
171509
  });
171510
+ diff = requireDeveloper(async (ctx) => {
171511
+ const slug2 = ctx.params.slug;
171512
+ if (!slug2) {
171513
+ throw ApiError.badRequest("Missing game slug");
171514
+ }
171515
+ const body2 = await parseRequestBody(ctx.request, SecretsDiffRequestSchema);
171516
+ return ctx.services.secrets.diff(slug2, body2.secrets, ctx.user);
171517
+ });
169002
171518
  secrets = defineControllerNames("secrets", {
169003
171519
  listKeys: listKeys2,
169004
171520
  setSecrets,
169005
- deleteSecret
171521
+ deleteSecret,
171522
+ diff
169006
171523
  });
169007
171524
  });
169008
171525
  var seed2;
@@ -169126,7 +171643,7 @@ var timeback2;
169126
171643
  var init_timeback_controller = __esm(() => {
169127
171644
  init_esm();
169128
171645
  init_schemas_index();
169129
- init_src4();
171646
+ init_src5();
169130
171647
  init_timeback3();
169131
171648
  init_errors();
169132
171649
  init_utils11();
@@ -169889,6 +172406,7 @@ var init_controllers = __esm(() => {
169889
172406
  init_dashboard_controller();
169890
172407
  init_database_controller();
169891
172408
  init_deploy_controller();
172409
+ init_deployment_state_controller();
169892
172410
  init_developer_controller();
169893
172411
  init_domain_controller();
169894
172412
  init_game_member_controller();
@@ -170409,6 +172927,21 @@ var init_deploy = __esm(async () => {
170409
172927
  });
170410
172928
  });
170411
172929
  });
172930
+ var gameDeploymentStateRouter;
172931
+ var init_deployment_state = __esm(async () => {
172932
+ init_dist7();
172933
+ init_controllers();
172934
+ await init_api3();
172935
+ gameDeploymentStateRouter = new Hono2;
172936
+ gameDeploymentStateRouter.get("/:slug/deployment-state", handle2(deploymentState.get));
172937
+ gameDeploymentStateRouter.post("/:slug/deployment-state/baseline", handle2(deploymentState.baseline));
172938
+ gameDeploymentStateRouter.post("/:slug/deployment-state/migrations/:tag/realign", handle2(deploymentState.realignMigration));
172939
+ gameDeploymentStateRouter.post("/:slug/deployment-state/migrations/:tag/resolve", handle2(deploymentState.resolveMigration));
172940
+ gameDeploymentStateRouter.get("/:slug/deployments", handle2(deploymentState.history));
172941
+ gameDeploymentStateRouter.post("/:slug/deployments/blocked", handle2(deploymentState.reportBlocked));
172942
+ gameDeploymentStateRouter.get("/:slug/database/restore-points", handle2(deploymentState.restorePoints));
172943
+ gameDeploymentStateRouter.post("/:slug/database/restore", handle2(deploymentState.restore));
172944
+ });
170412
172945
  var gameDomainsRouter;
170413
172946
  var init_domains2 = __esm(async () => {
170414
172947
  init_dist7();
@@ -170447,6 +172980,7 @@ var init_secrets = __esm(async () => {
170447
172980
  gameSecretsRouter = new Hono2;
170448
172981
  gameSecretsRouter.get("/:slug/secrets", handle2(secrets.listKeys));
170449
172982
  gameSecretsRouter.post("/:slug/secrets", handle2(secrets.setSecrets));
172983
+ gameSecretsRouter.post("/:slug/secrets/diff", handle2(secrets.diff));
170450
172984
  gameSecretsRouter.delete("/:slug/secrets/:key", handle2(secrets.deleteSecret));
170451
172985
  });
170452
172986
  var gameSeedRouter;
@@ -170641,6 +173175,7 @@ var init_games2 = __esm(async () => {
170641
173175
  await __promiseAll([
170642
173176
  init_crud(),
170643
173177
  init_deploy(),
173178
+ init_deployment_state(),
170644
173179
  init_domains2(),
170645
173180
  init_logs(),
170646
173181
  init_scores(),
@@ -170655,6 +173190,7 @@ var init_games2 = __esm(async () => {
170655
173190
  gamesRouter.route("/", gameVerifyRouter);
170656
173191
  gamesRouter.route("/", gameUploadsRouter);
170657
173192
  gamesRouter.route("/", gameDeployRouter);
173193
+ gamesRouter.route("/", gameDeploymentStateRouter);
170658
173194
  gamesRouter.route("/", gameDomainsRouter);
170659
173195
  gamesRouter.route("/", gameLogsRouter);
170660
173196
  gamesRouter.route("/", gameScoresRouter);
@@ -171586,7 +174122,7 @@ var init_domains3 = __esm3(() => {
171586
174122
  });
171587
174123
  var init_env_vars2 = () => {};
171588
174124
  var GAME_MEMBER_ROLES2;
171589
- var init_game3 = __esm3(() => {
174125
+ var init_game4 = __esm3(() => {
171590
174126
  GAME_MEMBER_ROLES2 = {
171591
174127
  OWNER: "owner",
171592
174128
  COLLABORATOR: "collaborator"
@@ -171661,13 +174197,13 @@ var init_cloudflare2 = __esm3(() => {
171661
174197
  LOCAL_PREFIX: "local-"
171662
174198
  };
171663
174199
  });
171664
- var init_src5 = __esm3(() => {
174200
+ var init_src6 = __esm3(() => {
171665
174201
  init_auth3();
171666
174202
  init_dashboard2();
171667
174203
  init_typescript2();
171668
174204
  init_domains3();
171669
174205
  init_env_vars2();
171670
- init_game3();
174206
+ init_game4();
171671
174207
  init_platform3();
171672
174208
  init_timeback8();
171673
174209
  init_cloudflare2();
@@ -171677,7 +174213,7 @@ var DEMO_USER_IDS2;
171677
174213
  var DEMO_USERS2;
171678
174214
  var DEMO_USER2;
171679
174215
  var init_demo_users2 = __esm3(() => {
171680
- init_src5();
174216
+ init_src6();
171681
174217
  now2 = new Date;
171682
174218
  DEMO_USER_IDS2 = {
171683
174219
  player: "00000000-0000-0000-0000-000000000001",