fix: enforce rollout gates across backend entry points

This commit is contained in:
jb
2026-09-19 10:01:23 -04:00
parent 92272d4e9a
commit 99436225f6
16 changed files with 115 additions and 23 deletions
+15
View File
@@ -5,6 +5,7 @@ import {
} from "convex/server";
import { v } from "convex/values";
import { assetValidator } from "./schema";
import { capabilityEnabled, parseCapabilityAllowlist } from "../lib/server/release-gate";
const MAX_ASSET_CHUNK = 250;
const SYNC_LEASE_MS = 5 * 60 * 1000;
@@ -39,6 +40,16 @@ function assertBridge(secret: string): void {
}
}
function assertDeploymentCapability(capability: 'asset_sync'): void {
let enabled = false;
try {
enabled = capabilityEnabled(capability, parseCapabilityAllowlist(process.env.APP_CAPABILITY_ALLOWLIST));
} catch {
// Invalid backend rollout configuration fails closed.
}
if (!enabled) throw new Error(`The ${capability} capability is disabled by deployment configuration.`);
}
function constantTimeEqual(a: string, b: string): boolean {
let difference = a.length ^ b.length;
const length = Math.max(a.length, b.length);
@@ -412,6 +423,7 @@ export const bridgeBeginSync = mutationGeneric({
args: { ...bridgeArgs, ...characterArgs },
handler: async (ctx, args) => {
assertBridge(args.bridgeSecret);
assertDeploymentCapability('asset_sync');
const character = await findCharacter(ctx, args.userId, args.characterId);
if (!character) throw new Error("Character not found");
const timestamp = now();
@@ -446,6 +458,7 @@ export const bridgeStageAssets = mutationGeneric({
},
handler: async (ctx, args) => {
assertBridge(args.bridgeSecret);
assertDeploymentCapability('asset_sync');
if (!Number.isInteger(args.index) || args.index < 0 || args.assets.length > MAX_ASSET_CHUNK) {
throw new Error("Invalid asset chunk");
}
@@ -511,6 +524,7 @@ export const bridgeFinishSync = mutationGeneric({
},
handler: async (ctx, args) => {
assertBridge(args.bridgeSecret);
assertDeploymentCapability('asset_sync');
const timestamp = now();
if (
!Number.isFinite(args.syncedAt) ||
@@ -574,6 +588,7 @@ export const bridgeFailSync = mutationGeneric({
args: { ...bridgeArgs, ...characterArgs, generation: v.string(), error: v.string() },
handler: async (ctx, args) => {
assertBridge(args.bridgeSecret);
assertDeploymentCapability('asset_sync');
const character = await findCharacter(ctx, args.userId, args.characterId);
if (!character || character.syncGeneration !== args.generation) {
throw new Error("Synchronization generation is not current");