simplify v2 migration history

This commit is contained in:
ajnart committed 2026-07-29 20:19:47 +02:00
1 parent 6ae109e8f5
commit 7a2d6009f7
34 files changed
+725 -9091

No files matched your search

+6 -27
View File
@@ -97,34 +97,13 @@ for (const required of [
if (!migration.includes(required)) throw new Error(`Workshop rollback is missing state restoration: ${required}`);
}
const hardeningMigration = await read("apps/workshop/pb_migrations/1784240001_workshop_write_rate_limits.js");
for (const operation of ["create", "update", "delete"]) {
for (const collection of ["submissions", "votes", "comments", "reports"]) {
const label = `${collection}:${operation}`;
if (!hardeningMigration.includes(label)) throw new Error(`Workshop write rate limit is missing: ${label}`);
}
}
if (!hardeningMigration.includes("users:update")) throw new Error("Workshop user file updates must be rate limited");
for (const required of [
"writeRateLimits",
"managedRules(settings.rateLimits.rules)",
"settings.rateLimits.enabled = state.enabled",
"...cloneJson(state.managedRules)",
]) {
if (!hardeningMigration.includes(required)) {
throw new Error(`Workshop write rate-limit rollback is missing persistent state restoration: ${required}`);
}
}
const accessHardeningMigration = await read("apps/workshop/pb_migrations/1784240002_workshop_access_hardening.js");
const rateLimitCompatibilityMigration = await read(
"apps/workshop/pb_migrations/1784240003_workshop_rate_limit_rollback_compatibility.js",
);
for (const required of ["writeRateLimits", "legacyCreateRules", "rate-limit compatibility state is unavailable"]) {
if (!rateLimitCompatibilityMigration.includes(required)) {
throw new Error(`Workshop rate-limit compatibility migration is missing: ${required}`);
if (!migration.includes(label)) throw new Error(`Workshop write rate limit is missing: ${label}`);
}
}
if (!migration.includes("users:update")) throw new Error("Workshop user file updates must be rate limited");
for (const protectedField of [
"email:changed",
"displayName:changed",
@@ -132,11 +111,11 @@ for (const protectedField of [
"githubUsername:changed",
"githubProfileUrl:changed",
]) {
if (!accessHardeningMigration.includes(protectedField)) {
if (!migration.includes(protectedField)) {
throw new Error(`Workshop OAuth identity field remains caller-controlled: ${protectedField}`);
}
}
if (accessHardeningMigration.includes("@request.body.avatar:changed")) {
if (migration.includes("@request.body.avatar:changed")) {
throw new Error("Workshop access rules must not use the unsupported :changed modifier for file fields");
}
if (
@@ -153,12 +132,12 @@ for (const protectedField of [
"changelog:changed",
"screenshots:changed",
]) {
if (!accessHardeningMigration.includes(protectedField)) {
if (!migration.includes(protectedField)) {
throw new Error(`Workshop moderators can still rewrite submissions: ${protectedField}`);
}
}
for (const protectedField of ["category:changed", "explanation:changed"]) {
if (!accessHardeningMigration.includes(protectedField)) {
if (!migration.includes(protectedField)) {
throw new Error(`Workshop moderators can still rewrite reports: ${protectedField}`);
}
}
@@ -1,205 +0,0 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import vm from "node:vm";
const normalized = (value) => JSON.parse(JSON.stringify(value));
const managedLabels = [
"submissions:create",
"submissions:update",
"submissions:delete",
"votes:create",
"votes:update",
"votes:delete",
"comments:create",
"comments:update",
"comments:delete",
"reports:create",
"reports:update",
"reports:delete",
"users:update",
];
const originalManagedRules = managedLabels.map((label, index) => ({
label,
audience: index % 2 === 0 ? "@auth" : "",
duration: index + 1,
maxRequests: index + 10,
}));
const originalUnmanagedRule = {
label: "unrelated:create",
audience: "",
duration: 41,
maxRequests: 42,
};
const stateCollection = { id: "workshop_migration_state" };
let stateSnapshot = JSON.stringify({ marker: "retain-existing-migration-state" });
const stateRecord = {
getString(name) {
assert.equal(name, "snapshot");
return stateSnapshot;
},
set(name, value) {
assert.equal(name, "snapshot");
stateSnapshot = value;
},
};
const settings = {
rateLimits: {
enabled: false,
rules: [originalUnmanagedRule, ...originalManagedRules],
},
};
const app = {
findCollectionByNameOrId(name) {
if (name !== stateCollection.id) throw new Error(`Missing collection ${name}`);
return stateCollection;
},
findFirstRecordByFilter(collectionId) {
if (collectionId !== stateCollection.id) throw new Error(`Missing state record ${collectionId}`);
return stateRecord;
},
save() {},
settings() {
return settings;
},
};
const loadMigration = async (path) => {
let loaded;
const source = await readFile(path, "utf8");
vm.runInNewContext(source, {
console,
migrate: (up, down) => {
loaded = { up, down };
},
});
if (!loaded) throw new Error(`Workshop migration did not register: ${path}`);
return loaded;
};
const migration = await loadMigration("apps/workshop/pb_migrations/1784240001_workshop_write_rate_limits.js");
migration.up(app);
const savedState = JSON.parse(stateSnapshot);
assert.equal(savedState.marker, "retain-existing-migration-state");
assert.deepEqual(savedState.writeRateLimits, {
enabled: false,
managedRules: originalManagedRules,
});
assert.equal(settings.rateLimits.enabled, true);
assert.deepEqual(
normalized(settings.rateLimits.rules.filter((rule) => !managedLabels.includes(rule.label))),
[originalUnmanagedRule],
"Applying Workshop rate limits changed unrelated rules",
);
assert.deepEqual(
new Set(settings.rateLimits.rules.map((rule) => rule.label)),
new Set([...managedLabels, originalUnmanagedRule.label]),
"Applying Workshop rate limits did not replace every managed label",
);
const laterUnmanagedRule = {
label: "added-after-migration:update",
audience: "@auth",
duration: 51,
maxRequests: 52,
};
settings.rateLimits.rules = [
laterUnmanagedRule,
...settings.rateLimits.rules.filter((rule) => managedLabels.includes(rule.label)),
];
migration.down(app);
assert.equal(settings.rateLimits.enabled, false, "Rollback did not restore the original enabled flag");
assert.deepEqual(
normalized(settings.rateLimits.rules.filter((rule) => !managedLabels.includes(rule.label))),
[laterUnmanagedRule],
"Rollback changed unrelated rules added after the migration",
);
assert.deepEqual(
normalized(settings.rateLimits.rules.filter((rule) => managedLabels.includes(rule.label))),
originalManagedRules,
"Rollback did not restore every original managed-label rule",
);
assert.throws(
() =>
migration.up({
...app,
findCollectionByNameOrId() {
throw new Error("missing");
},
}),
/migration state is unavailable/u,
"Migration must fail closed when its persistent rollback snapshot is unavailable",
);
const legacyOriginalRules = [
{ label: "unrelated:create", audience: "", duration: 71, maxRequests: 72 },
{ label: "submissions:create", audience: "", duration: 73, maxRequests: 74 },
{ label: "comments:update", audience: "@auth", duration: 75, maxRequests: 76 },
];
const legacyStateCollection = { id: "workshop_migration_state" };
let legacyStateSnapshot = JSON.stringify({
marker: "already-applied-old-0001",
rateLimits: { enabled: false, rules: legacyOriginalRules },
});
const legacyStateRecord = {
getString() {
return legacyStateSnapshot;
},
set(_name, value) {
legacyStateSnapshot = value;
},
};
const legacySettings = {
rateLimits: {
enabled: true,
rules: [
{ label: "unrelated:create", audience: "", duration: 71, maxRequests: 72 },
...managedLabels.map((label) => ({ label, audience: "@auth", duration: 60, maxRequests: 30 })),
],
},
};
const legacyApp = {
findCollectionByNameOrId() {
return legacyStateCollection;
},
findFirstRecordByFilter() {
return legacyStateRecord;
},
save() {},
settings() {
return legacySettings;
},
};
const compatibilityMigration = await loadMigration(
"apps/workshop/pb_migrations/1784240003_workshop_rate_limit_rollback_compatibility.js",
);
compatibilityMigration.up(legacyApp);
compatibilityMigration.down(legacyApp);
const backfilledState = JSON.parse(legacyStateSnapshot);
assert.equal(backfilledState.marker, "already-applied-old-0001");
assert.deepEqual(backfilledState.writeRateLimits, {
enabled: true,
managedRules: [
{ label: "comments:update", audience: "@auth", duration: 75, maxRequests: 76 },
{ label: "submissions:create", audience: "@auth", duration: 60, maxRequests: 10 },
{ label: "votes:create", audience: "@auth", duration: 10, maxRequests: 20 },
{ label: "comments:create", audience: "@auth", duration: 60, maxRequests: 20 },
{ label: "reports:create", audience: "@auth", duration: 60, maxRequests: 5 },
],
});
migration.down(legacyApp);
assert.equal(legacySettings.rateLimits.enabled, true);
assert.deepEqual(
normalized(legacySettings.rateLimits.rules),
[
{ label: "unrelated:create", audience: "", duration: 71, maxRequests: 72 },
...backfilledState.writeRateLimits.managedRules,
],
"An already-applied legacy migration could not be rolled back after the compatibility backfill",
);
console.log("Workshop write rate-limit migration restoration passed");