Revert "Merge pull request 'Complete Housekeeping migration and /ase cutover' (#52) from codex/housekeeping-complete into main"
CI / check (push) Successful in 27s
CI / release (push) Skipped
CI / deploy (push) Successful in 43s

This reverts commit 488b6e57c4, reversing
changes made to b506b4499a.
This commit is contained in:
Simo committed 2026-08-30 21:31:34 +02:00
1 parent 488b6e57c4
commit b1ddda66ff
802 files changed
+61370 -76659

No files matched your search

+1 -1
View File
@@ -156,7 +156,7 @@ async function emailStaff(input: SendAlertInput): Promise<boolean> {
? `<table style="border-collapse:collapse;font-size:13px">${contextRows}</table>`
: "") +
`<p style="margin-top:16px;color:#888;font-size:12px">` +
`Sent by ${escapeHtml(env.HOTEL_NAME)} · <a href="${env.APP_URL}/ase/system/operations/alerts">view alerts</a></p>`;
`Sent by ${escapeHtml(env.HOTEL_NAME)} · <a href="${env.APP_URL}/admin/alerts">view alerts</a></p>`;
try {
return await sendMail(to, subject, html);
-72
View File
@@ -87,61 +87,6 @@ describe("logAudit", () => {
expect(JSON.parse(data.diff).username).toEqual({ from: "foo", to: "bar" });
});
it("redacts sensitive keys recursively in nested objects and arrays", async () => {
insertValues.mockResolvedValue({ id: 1 });
await logAudit({
userId: 1,
action: "update",
target: "user",
before: {
profile: {
authTicket: "private-ticket",
credentials: { newPassword: "one-time-password" },
},
integrations: [{ api_key: "private-key" }],
},
});
const before = JSON.parse(insertValues.mock.calls[0][0].before);
expect(before.profile.authTicket).toBe("[Redacted]");
expect(before.profile.credentials.newPassword).toBe("[Redacted]");
expect(before.integrations[0].api_key).toBe("[Redacted]");
});
it("persists correlation, domain, outcome, reason, and IP evidence", async () => {
insertValues.mockResolvedValue({ id: 1 });
await logAudit({
userId: 1,
action: "ban",
target: "user",
correlationId: "corr-123",
domain: "people",
outcome: "denied",
reason: "Policy requirement was not met",
ipAddress: "127.0.0.1",
});
expect(insertValues.mock.calls[0][0]).toMatchObject({
correlationId: "corr-123",
domain: "people",
outcome: "denied",
reason: "Policy requirement was not met",
ipAddress: "127.0.0.1",
});
});
it("writes through the injected transaction when one is supplied", async () => {
const transactionValues = vi.fn().mockResolvedValue({ id: 1 });
const transactionInsert = vi.fn(() => ({ values: transactionValues }));
await logAudit({ userId: 1, action: "update", target: "settings" }, {
insert: transactionInsert,
} as never);
expect(transactionInsert).toHaveBeenCalledOnce();
expect(transactionValues).toHaveBeenCalledOnce();
expect(insertValues).not.toHaveBeenCalled();
});
it("omits diff when only before or after is missing", async () => {
insertValues.mockResolvedValue({ id: 1 });
await logAudit({
@@ -241,21 +186,4 @@ describe("getAuditLogs", () => {
const result = await getAuditLogs();
expect(result.rows[0].username).toBe("User #99");
});
it("fails closed beyond the redaction depth cap without mutating the input", async () => {
const sentinel = "raw-depth-secret";
const deep = {
a: { b: { c: { d: { e: { f: { g: { secret: sentinel } } } } } } },
};
insertValues.mockResolvedValue({ id: 1 });
await logAudit({
userId: 1,
action: "update",
target: "user",
before: { deep, state: "before" },
after: { deep, state: "after" },
});
const data = insertValues.mock.calls[0][0];
expect(`${data.before}${data.after}${data.diff}`).not.toContain(sentinel);
expect(deep.a.b.c.d.e.f.g.secret).toBe(sentinel);
});
});
+5 -34
View File
@@ -1,28 +1,13 @@
import { count, desc, inArray, like, or } from "drizzle-orm";
import type { HousekeepingDomainId } from "@/features/housekeeping/migration/types";
import { AdminAuditLog, type Db, db, User } from "@/lib/db";
import { AdminAuditLog, db, User } from "@/lib/db";
export interface AuditEntry {
interface AuditEntry {
userId: number;
action: string;
target: string;
targetId?: number;
before?: Record<string, unknown>;
after?: Record<string, unknown>;
correlationId?: string;
outcome?: "intent" | "success" | "failure" | "partial" | "denied";
reason?: string;
domain?: HousekeepingDomainId;
ipAddress?: string;
}
export type HousekeepingAuditTransaction = Pick<Db, "insert">;
export interface HousekeepingAuditWriter {
write(
entry: AuditEntry,
transaction?: HousekeepingAuditTransaction,
): Promise<void>;
}
const SENSITIVE_KEY_RE =
@@ -30,8 +15,7 @@ const SENSITIVE_KEY_RE =
const REDACTED = "[Redacted]";
function sanitizeAuditPayload(value: unknown, depth = 0): unknown {
if (depth > 6) return REDACTED;
if (value == null) return value;
if (depth > 6 || value == null) return value;
if (Array.isArray(value))
return value.map((v) => sanitizeAuditPayload(v, depth + 1));
if (typeof value !== "object") return value;
@@ -63,10 +47,7 @@ function computeDiff(
return Object.keys(diff).length > 0 ? diff : null;
}
export async function logAudit(
entry: AuditEntry,
transaction?: HousekeepingAuditTransaction,
): Promise<void> {
export async function logAudit(entry: AuditEntry): Promise<void> {
const sanitizedBefore = entry.before
? (sanitizeAuditPayload(entry.before) as Record<string, unknown>)
: undefined;
@@ -75,8 +56,7 @@ export async function logAudit(
: undefined;
const diff = computeDiff(sanitizedBefore, sanitizedAfter);
const auditWriter: HousekeepingAuditTransaction = transaction ?? db;
await auditWriter.insert(AdminAuditLog).values({
await db.insert(AdminAuditLog).values({
userId: entry.userId,
action: entry.action,
target: entry.target,
@@ -84,19 +64,10 @@ export async function logAudit(
before: sanitizedBefore ? JSON.stringify(sanitizedBefore) : null,
after: sanitizedAfter ? JSON.stringify(sanitizedAfter) : null,
diff: diff ? JSON.stringify(diff) : null,
correlationId: entry.correlationId,
outcome: entry.outcome,
reason: entry.reason,
domain: entry.domain,
ipAddress: entry.ipAddress,
createdAt: new Date().toISOString(),
});
}
export const housekeepingAuditWriter: HousekeepingAuditWriter = {
write: logAudit,
};
interface GetLogsOptions {
search?: string;
page?: number;
+3 -14
View File
@@ -134,16 +134,13 @@ async function fetchWithFallback(url: string): Promise<string> {
export async function fetchSourceFurnidata(
url: string,
now = Date.now(),
signal?: AbortSignal,
): Promise<SourceFurni[]> {
signal?.throwIfAborted();
const hit = cache.get(url);
if (hit && now && now - hit.ts < TTL) return hit.list;
let body: string;
try {
const timeout = AbortSignal.timeout(30000);
const res = await fetch(url, {
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
signal: AbortSignal.timeout(30000),
headers: browserHeaders(),
});
if (res.ok) {
@@ -162,8 +159,7 @@ export async function fetchSourceFurnidata(
} else {
body = await fetchWithFallback(url);
}
} catch (error) {
if (signal?.aborted) throw error;
} catch {
body = await fetchWithFallback(url);
}
if (body.trimStart().startsWith("<")) {
@@ -247,13 +243,11 @@ export async function cloneSingleFurni(params: {
source: CloneSource;
entry: SourceFurni;
onProgress?: (status: string) => void;
signal?: AbortSignal;
/** Skip the per-item FurnitureData.json write and return the entry instead,
* so batch callers can append everything in one lock + write cycle. */
deferFurniData?: boolean;
}): Promise<CloneResult> {
const { source, entry, onProgress, signal, deferFurniData } = params;
signal?.throwIfAborted();
const { source, entry, onProgress, deferFurniData } = params;
const { classname, itemType } = entry;
const warnings: string[] = [];
@@ -293,7 +287,6 @@ export async function cloneSingleFurni(params: {
// Download .nitro + icon directly from the source hotel.
onProgress?.("downloading");
signal?.throwIfAborted();
const nitroPath = getRuntimePath(
/*turbopackIgnore: true*/ nitroDir,
`${classname}.nitro`,
@@ -307,7 +300,6 @@ export async function cloneSingleFurni(params: {
nitroPath,
{
maxRetries: 2,
signal,
},
);
if (!dl.ok) {
@@ -342,7 +334,6 @@ export async function cloneSingleFurni(params: {
{
maxRetries: 1,
validate: "png",
signal,
},
);
if (!iconDl.ok) {
@@ -375,7 +366,6 @@ export async function cloneSingleFurni(params: {
}
onProgress?.("writing_db");
signal?.throwIfAborted();
const stackHeight =
entry.canlayon || entry.cansiton ? 1.0 : entry.canstandon ? 1.0 : 0.0;
// allow_stack: derived from stackHeight (mirrors furni-import.ts `dims.z > 0`).
@@ -432,7 +422,6 @@ export async function cloneSingleFurni(params: {
// AND our offerid. The source's offerid points at the source hotel's sprite
// ids, which would silently break the emulator's catalog offer lookup.
onProgress?.("writing_furnidata");
signal?.throwIfAborted();
const furniDataEntry = {
...entry,
id: newId,
+2 -10
View File
@@ -89,10 +89,8 @@ export async function importClothingSet(params: {
setType: string;
setId: number;
onProgress?: (status: string) => void;
signal?: AbortSignal;
}): Promise<ImportClothingSetResult> {
const { setType, setId, onProgress, signal } = params;
signal?.throwIfAborted();
const { setType, setId, onProgress } = params;
const warnings: string[] = [];
const [officialFd, officialLibs] = await Promise.all([
@@ -138,17 +136,12 @@ export async function importClothingSet(params: {
let hardFail = false;
for (const libId of requiredLibIds) {
signal?.throwIfAborted();
const officialLib = officialLibs.find((l) => l.id === libId);
if (!officialLib) continue;
if (!localLibIds.has(libId)) {
onProgress?.(`importing library ${libId}`);
const result = await importSingleFigure({
lib: libId,
onProgress,
signal,
});
const result = await importSingleFigure({ lib: libId, onProgress });
if (!result.ok) {
hardFail = true;
warnings.push(
@@ -176,7 +169,6 @@ export async function importClothingSet(params: {
}
const mappedSet = mapOfficialSet(set);
signal?.throwIfAborted();
await mergeSet(setType, st.paletteId, mappedSet as never);
if (palette) {
+2 -9
View File
@@ -76,10 +76,8 @@ export async function importSingleEffect(params: {
type: string;
revision: number;
onProgress?: (status: string) => void;
signal?: AbortSignal;
}): Promise<ImportEffectResult> {
const { id, lib, type, revision, onProgress, signal } = params;
signal?.throwIfAborted();
const { id, lib, type, revision, onProgress } = params;
const warnings: string[] = [];
await ensureEffectDirs();
@@ -102,10 +100,7 @@ export async function importSingleEffect(params: {
error: `Could not resolve SWF URL: ${(err as Error).message}`,
};
}
const dl = await downloadFile(swfUrl, swfPath, {
validate: "swf",
signal,
});
const dl = await downloadFile(swfUrl, swfPath, { validate: "swf" });
if (!dl.ok)
return {
ok: false,
@@ -116,7 +111,6 @@ export async function importSingleEffect(params: {
// Convert SWF → Nitro
onProgress?.("converting");
signal?.throwIfAborted();
try {
const swfBuffer = await fs.readFile(/*turbopackIgnore: true*/ swfPath);
const result = convertSwfToNitro(swfBuffer, lib);
@@ -133,7 +127,6 @@ export async function importSingleEffect(params: {
// Merge EffectMap.json (last step — no orphan entry if anything above failed)
onProgress?.("writing_map");
signal?.throwIfAborted();
try {
const entry: EffectMapEntry = { id, lib, type, revision };
await mergeEffect(entry);
+2 -8
View File
@@ -78,10 +78,8 @@ async function nitroPathFor(lib: string): Promise<string> {
export async function importSingleFigure(params: {
lib: string;
onProgress?: (status: string) => void;
signal?: AbortSignal;
}): Promise<ImportFigureResult> {
const { lib, onProgress, signal } = params;
signal?.throwIfAborted();
const { lib, onProgress } = params;
const warnings: string[] = [];
await ensureFigureDirs();
@@ -101,10 +99,7 @@ export async function importSingleFigure(params: {
error: `Could not resolve SWF URL: ${(err as Error).message}`,
};
}
const dl = await downloadFile(swfUrl, swfPath, {
validate: "swf",
signal,
});
const dl = await downloadFile(swfUrl, swfPath, { validate: "swf" });
if (!dl.ok)
return {
ok: false,
@@ -114,7 +109,6 @@ export async function importSingleFigure(params: {
};
onProgress?.("converting");
signal?.throwIfAborted();
try {
const swfBuffer = await fs.readFile(/*turbopackIgnore: true*/ swfPath);
const result = convertSwfToNitro(swfBuffer, lib);
+4 -18
View File
@@ -403,13 +403,10 @@ async function syncDirMissingFiles(
* so all imported furniture stays renderable in-game even when a mirror
* write was skipped.
*/
export async function syncAssetsToGamedataBundle(
signal?: AbortSignal,
): Promise<{
export async function syncAssetsToGamedataBundle(): Promise<{
copiedNitros: string[];
copiedIcons: string[];
}> {
signal?.throwIfAborted();
if (process.platform === "win32")
return { copiedNitros: [], copiedIcons: [] };
@@ -426,7 +423,6 @@ export async function syncAssetsToGamedataBundle(
"bundled/furniture",
);
if (nitroDir !== nitroTarget) {
signal?.throwIfAborted();
const copied = await syncDirMissingFiles(nitroDir, nitroTarget, (entry) =>
entry.endsWith(".nitro"),
);
@@ -438,7 +434,6 @@ export async function syncAssetsToGamedataBundle(
"icons",
);
if (iconDir !== iconTarget) {
signal?.throwIfAborted();
const copied = await syncDirMissingFiles(iconDir, iconTarget, (entry) =>
entry.endsWith("_icon.png"),
);
@@ -513,7 +508,6 @@ export async function importSingleFurni(params: {
skipFurniDataWrite?: boolean;
updateExisting?: boolean;
onProgress?: (status: string) => void;
signal?: AbortSignal;
/** Per-source SWF download base URL (e.g. "https://virtualc.nl/dcr"). */
sourceSwfBaseUrl?: string;
/** Per-source nitro bundle + icon base URL. */
@@ -531,12 +525,10 @@ export async function importSingleFurni(params: {
skipFurniDataWrite,
updateExisting,
onProgress,
signal,
sourceSwfBaseUrl,
nitroBaseUrl: sourceNitroBaseUrl,
iconBaseUrl: sourceIconBaseUrl,
} = params;
signal?.throwIfAborted();
const warnings: string[] = [];
// Check if already exists by classname OR by spriteId (primary key collision).
@@ -612,7 +604,6 @@ export async function importSingleFurni(params: {
// ── Insert into DB with enriched data ──────────────────────────────
onProgress?.("writing_db");
signal?.throwIfAborted();
try {
if (isUpdate) {
// NOTE: live Arcturus `items_base` has no description/revision/rare columns —
@@ -669,7 +660,6 @@ export async function importSingleFurni(params: {
// fall back to base-classname icon (Nitro reuses base for all colors
// when no color-specific icon exists).
onProgress?.("downloading");
signal?.throwIfAborted();
const iconColorAware = classname.replace(/\*/g, "_");
@@ -711,8 +701,8 @@ export async function importSingleFurni(params: {
// Download icon + SWF concurrently (independent assets). Each tries its
// candidate URLs in order until one succeeds.
const [iconOkResult, swfOk] = await Promise.all([
tryDownloadCandidates(iconUrls, iconPath, "png", signal),
tryDownloadCandidates(swfUrls, swfPath, "swf", signal),
tryDownloadCandidates(iconUrls, iconPath, "png"),
tryDownloadCandidates(swfUrls, swfPath, "swf"),
]);
let iconOk = iconOkResult;
@@ -725,10 +715,7 @@ export async function importSingleFurni(params: {
!existsSync(/*turbopackIgnore: true*/ nitroPath)
) {
const nitroUrl = `${sourceNitroBaseUrl}/${encodeURIComponent(safeNitroName)}.nitro`;
const dl = await downloadFile(nitroUrl, nitroPath, {
validate: "png",
signal,
});
const dl = await downloadFile(nitroUrl, nitroPath, { validate: "png" });
if (dl.ok) nitroDownloadOk = true;
}
@@ -757,7 +744,6 @@ export async function importSingleFurni(params: {
// ── Convert SWF to Nitro ──────────────────────────────────────────
onProgress?.("converting");
signal?.throwIfAborted();
let conversionResult: ConversionResult | null = null;
if (existsSync(/*turbopackIgnore: true*/ nitroPath)) {
+1 -8
View File
@@ -635,21 +635,14 @@ export interface FixAllResult {
/** Run every repair in a sensible order, then return before/after health. */
export async function fixEverything(
opts: { dedupePages?: boolean; signal?: AbortSignal } = {},
opts: { dedupePages?: boolean } = {},
): Promise<FixAllResult> {
opts.signal?.throwIfAborted();
const healthBefore = await getFurniHealth();
opts.signal?.throwIfAborted();
const sprite = await fixSpriteIds();
opts.signal?.throwIfAborted();
const offers = await fixCatalogOffers();
opts.signal?.throwIfAborted();
const reconcile = await reconcileIds();
opts.signal?.throwIfAborted();
const dedup = await removeDuplicates(opts.dedupePages ?? true);
opts.signal?.throwIfAborted();
const align = await forceItemsBaseIdsToFurnidata(true);
opts.signal?.throwIfAborted();
const healthAfter = await getFurniHealth();
return { healthBefore, sprite, offers, reconcile, dedup, align, healthAfter };
}
+7 -22
View File
@@ -17,9 +17,7 @@ export async function writeBadgeToExternalTexts(
code: string,
name: string,
description: string,
signal?: AbortSignal,
): Promise<ImportBadgeResult> {
signal?.throwIfAborted();
const gamedataRoot = await getGamedataRoot();
if (!gamedataRoot) {
return {
@@ -37,7 +35,6 @@ export async function writeBadgeToExternalTexts(
const raw = await fs.readFile(extPath, "utf-8");
texts = JSON.parse(raw) as Record<string, string>;
} catch (err) {
if (signal?.aborted) throw err;
// File not present yet (fresh deployment) — start with an empty object.
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
console.warn(
@@ -52,7 +49,6 @@ export async function writeBadgeToExternalTexts(
}
texts = {};
}
signal?.throwIfAborted();
if (texts[nameKey] !== undefined) {
return {
@@ -67,11 +63,9 @@ export async function writeBadgeToExternalTexts(
try {
await fs.mkdir(path.dirname(extPath), { recursive: true });
signal?.throwIfAborted();
await fs.writeFile(extPath, JSON.stringify(texts, null, 4), "utf-8");
return { ok: true };
} catch (err) {
if (signal?.aborted) throw err;
console.warn(
"[import-badges] Failed to update ExternalTexts.json:",
(err as Error).message,
@@ -89,9 +83,7 @@ export async function upsertWebsiteBadge(
code: string,
name: string,
description: string,
signal?: AbortSignal,
): Promise<void> {
signal?.throwIfAborted();
const now = new Date();
await db
.insert(WebsiteBadges)
@@ -115,28 +107,21 @@ export async function upsertWebsiteBadge(
* Single import path: ExternalTexts.json + WebsiteBadges.
* Callers should gate with ASSETS_IMPORT.
*/
export async function importBadgeSynced(
input: {
code: string;
name: string;
description: string;
},
signal?: AbortSignal,
): Promise<ImportBadgeResult> {
signal?.throwIfAborted();
export async function importBadgeSynced(input: {
code: string;
name: string;
description: string;
}): Promise<ImportBadgeResult> {
const texts = await writeBadgeToExternalTexts(
input.code,
input.name,
input.description,
signal,
);
if (!texts.ok) return texts;
try {
signal?.throwIfAborted();
await upsertWebsiteBadge(input.code, input.name, input.description, signal);
} catch (error) {
if (signal?.aborted) throw error;
await upsertWebsiteBadge(input.code, input.name, input.description);
} catch {
return { ok: false, error: "Failed to import badge", status: 500 };
}
+3 -14
View File
@@ -22,11 +22,9 @@ export async function tryDownloadCandidates(
urls: string[],
destPath: string,
kind: "swf" | "png",
signal?: AbortSignal,
): Promise<boolean> {
for (const url of urls) {
signal?.throwIfAborted();
const res = await downloadFile(url, destPath, { validate: kind, signal });
const res = await downloadFile(url, destPath, { validate: kind });
if (res.ok) return true;
}
return false;
@@ -35,26 +33,18 @@ export async function tryDownloadCandidates(
export async function downloadFile(
url: string,
destPath: string,
options?: {
maxRetries?: number;
validate?: "swf" | "png";
signal?: AbortSignal;
},
options?: { maxRetries?: number; validate?: "swf" | "png" },
): Promise<{ ok: boolean; size: number }> {
const maxRetries = options?.maxRetries ?? 3;
const baseDelay = 1000;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
options?.signal?.throwIfAborted();
if (attempt > 0) {
await new Promise((r) => setTimeout(r, baseDelay * 2 ** (attempt - 1)));
}
const timeout = AbortSignal.timeout(15000);
const res = await fetch(url, {
signal: options?.signal
? AbortSignal.any([options.signal, timeout])
: timeout,
signal: AbortSignal.timeout(15000),
headers: browserHeaders("image", {
Accept: "image/png,image/*,*/*;q=0.8",
}),
@@ -92,7 +82,6 @@ export async function downloadFile(
await fs.writeFile(/*turbopackIgnore: true*/ destPath, buffer);
return { ok: true, size: buffer.length };
} catch (err) {
if (options?.signal?.aborted) throw err;
if (attempt === maxRetries) {
console.warn(
`[import-download] Download error ${url}:`,
-47
View File
@@ -21,53 +21,6 @@ export interface ModerationResult {
reason?: string;
}
export type ModerationAction =
| { readonly action: "kick"; readonly userId: number }
| {
readonly action: "mute";
readonly userId: number;
readonly duration: number;
}
| { readonly action: "unmute"; readonly userId: number }
| {
readonly action: "alert";
readonly userId: number;
readonly message: string;
}
| { readonly action: "room-kick"; readonly roomId: number }
| {
readonly action: "broadcast";
readonly message: string;
readonly type: "hotel" | "staff";
};
export interface ModerationActionTransport {
disconnectUser(userId: number): Promise<boolean>;
muteUser(userId: number, duration: number): Promise<boolean>;
unmuteUser(userId: number): Promise<boolean>;
alertUser(userId: number, message: string): Promise<boolean>;
kickAll(roomId: number): Promise<boolean>;
hotelAlert(message: string): Promise<boolean>;
staffAlert(message: string): Promise<boolean>;
}
// Execute one already-authorized moderation effect and expose delivery truth.
export async function executeModerationAction(
transport: ModerationActionTransport,
input: ModerationAction,
): Promise<boolean> {
if (input.action === "kick") return transport.disconnectUser(input.userId);
if (input.action === "mute")
return transport.muteUser(input.userId, input.duration);
if (input.action === "unmute") return transport.unmuteUser(input.userId);
if (input.action === "alert")
return transport.alertUser(input.userId, input.message);
if (input.action === "room-kick") return transport.kickAll(input.roomId);
return input.type === "hotel"
? transport.hotelAlert(input.message)
: transport.staffAlert(input.message);
}
const OPENAI_MODERATIONS_URL = "https://api.openai.com/v1/moderations";
// Bound the AI call so a slow/hung endpoint can't stall a server action.
const OPENAI_TIMEOUT_MS = 5_000;
+2 -8
View File
@@ -45,10 +45,8 @@ function nitroPathFor(lib: string): string {
export async function importSinglePet(params: {
lib: string;
onProgress?: (status: string) => void;
signal?: AbortSignal;
}): Promise<ImportPetResult> {
const { lib, onProgress, signal } = params;
signal?.throwIfAborted();
const { lib, onProgress } = params;
const warnings: string[] = [];
await ensurePetDirs();
@@ -68,10 +66,7 @@ export async function importSinglePet(params: {
error: `Could not resolve SWF URL: ${(err as Error).message}`,
};
}
const dl = await downloadFile(swfUrl, swfPath, {
validate: "swf",
signal,
});
const dl = await downloadFile(swfUrl, swfPath, { validate: "swf" });
if (!dl.ok)
return {
ok: false,
@@ -81,7 +76,6 @@ export async function importSinglePet(params: {
};
onProgress?.("converting");
signal?.throwIfAborted();
try {
const swfBuffer = await fs.readFile(/*turbopackIgnore: true*/ swfPath);
const result = convertSwfToNitro(swfBuffer, lib);
+2 -6
View File
@@ -104,9 +104,7 @@ export interface RepairEvent {
export async function repairMissingIcons(
onEvent?: (evt: RepairEvent) => void,
options?: { signal?: AbortSignal },
): Promise<{ succeeded: number; failed: number; skipped: number }> {
options?.signal?.throwIfAborted();
let succeeded = 0;
let failed = 0;
let skipped = 0;
@@ -206,7 +204,6 @@ export async function repairMissingIcons(
onEvent?.({ type: "started", total });
const processItem = async (classname: string) => {
options?.signal?.throwIfAborted();
// Special items (badges, pets, effects, bots, sticky notes) don't have
// .png icon files to repair. Skip them.
if (dbSkipSet.has(classname)) {
@@ -315,7 +312,7 @@ export async function repairMissingIcons(
targets.iconDir,
fileName,
),
{ maxRetries: 1, validate: "png", signal: options?.signal },
{ maxRetries: 1, validate: "png" },
);
if (dl.ok) {
const icon = await fs.readFile(
@@ -353,7 +350,7 @@ export async function repairMissingIcons(
const dl = await downloadFile(
`${source.nitroBaseUrl}/${classname}.nitro`,
nitroTmp,
{ maxRetries: 1, signal: options?.signal },
{ maxRetries: 1 },
);
if (dl.ok) {
const icon = extractFurniIconPng(await fs.readFile(nitroTmp));
@@ -393,7 +390,6 @@ export async function repairMissingIcons(
};
await runPool(items, REPAIR_CONCURRENCY, async (item, i) => {
options?.signal?.throwIfAborted();
const classname = item.item_name;
const result = await processItem(classname);
if (result.status === "skipped") {
+2 -8
View File
@@ -106,9 +106,7 @@ export interface RepairNitroEvent {
export async function repairMissingNitros(
onEvent?: (evt: RepairNitroEvent) => void,
options?: { signal?: AbortSignal },
): Promise<{ succeeded: number; failed: number; skipped: number }> {
options?.signal?.throwIfAborted();
let succeeded = 0;
let failed = 0;
let skipped = 0;
@@ -203,7 +201,6 @@ export async function repairMissingNitros(
onEvent?.({ type: "started", total });
const processItem = async (classname: string) => {
options?.signal?.throwIfAborted();
// Special items (badges, pets, effects, bots, sticky notes) don't have
// furni .nitro bundles to repair. Skip them.
if (skipSet.has(classname)) {
@@ -279,7 +276,7 @@ export async function repairMissingNitros(
const dl = await downloadFile(
`${source.nitroBaseUrl}/${nitroName}`,
tmpPath,
{ maxRetries: 1, signal: options?.signal },
{ maxRetries: 1 },
);
if (dl.ok) {
// Validate it is a real Nitro bundle before writing it
@@ -318,7 +315,6 @@ export async function repairMissingNitros(
};
await runPool(items, REPAIR_CONCURRENCY, async (item, i) => {
options?.signal?.throwIfAborted();
const classname = item.item_name;
const result = await processItem(classname);
if (result.status === "skipped") {
@@ -388,14 +384,13 @@ export async function repairMissingNitros(
*/
export async function addSize32AllNitros(
onEvent?: (evt: RepairNitroEvent) => void,
opts?: { concurrency?: number; signal?: AbortSignal },
opts?: { concurrency?: number },
): Promise<{
succeeded: number;
failed: number;
skipped: number;
existing: number;
}> {
opts?.signal?.throwIfAborted();
let succeeded = 0;
let failed = 0;
let skipped = 0;
@@ -447,7 +442,6 @@ export async function addSize32AllNitros(
[...fileNames],
opts?.concurrency ?? 12,
async (fileName, i) => {
opts?.signal?.throwIfAborted();
let buf: Buffer | null = null;
for (const dir of nitroDirs) {
try {
-50
View File
@@ -19,56 +19,6 @@ export interface TicketReplyDb {
touchTicket(ticketId: bigint, updatedAt: Date): Promise<unknown>;
}
export const MAX_UNSIGNED_TICKET_ID = 18_446_744_073_709_551_615n;
function boundedDecimalPattern(maximum: string): RegExp {
const alternatives = [`[1-9]\\d{0,${maximum.length - 2}}`];
for (let index = 0; index < maximum.length; index += 1) {
const maximumDigit = Number(maximum[index]);
const minimumDigit = index === 0 ? 1 : 0;
const upperDigit = maximumDigit - 1;
if (upperDigit < minimumDigit) continue;
const digit =
upperDigit === minimumDigit
? String(minimumDigit)
: `[${minimumDigit}-${upperDigit}]`;
alternatives.push(
`${maximum.slice(0, index)}${digit}\\d{${maximum.length - index - 1}}`,
);
}
alternatives.push(maximum);
return new RegExp(`^(?:${alternatives.join("|")})$`, "u");
}
export const CANONICAL_TICKET_ID_PATTERN = boundedDecimalPattern(
MAX_UNSIGNED_TICKET_ID.toString(),
);
// Canonicalize a SQL BIGINT identifier without passing through Number.
export function canonicalTicketId(value: string | number | bigint): bigint {
let parsed: bigint;
try {
if (typeof value === "bigint") {
parsed = value;
} else if (typeof value === "string") {
if (!CANONICAL_TICKET_ID_PATTERN.test(value)) {
throw new Error("noncanonical identifier");
}
parsed = BigInt(value);
} else if (Number.isSafeInteger(value) && value > 0) {
parsed = BigInt(value);
} else {
throw new Error("unsafe identifier");
}
} catch {
throw new Error("invalid ticket identifier");
}
if (parsed <= 0n || parsed > MAX_UNSIGNED_TICKET_ID) {
throw new Error("invalid ticket identifier");
}
return parsed;
}
export async function createOwnedTicketReply(
db: TicketReplyDb,
input: { ticketId: bigint; userId: number; content: string },
-9
View File
@@ -234,7 +234,6 @@ export async function uploadSingleFurni(params: {
nitroBuffer: Buffer;
iconBuffer?: Buffer | null;
generateSql?: boolean;
signal?: AbortSignal;
}): Promise<UploadResult> {
const {
classname,
@@ -251,9 +250,7 @@ export async function uploadSingleFurni(params: {
nitroBuffer,
iconBuffer,
generateSql = false,
signal,
} = params;
signal?.throwIfAborted();
const warnings: string[] = [];
if (!/^[\w\-.*]+$/.test(classname)) {
@@ -283,7 +280,6 @@ export async function uploadSingleFurni(params: {
}
await ensureDirectories();
signal?.throwIfAborted();
const assetTargets = await getFurniAssetWriteTargets();
const { iconDir, nitroDir } = assetTargets;
@@ -301,7 +297,6 @@ export async function uploadSingleFurni(params: {
};
}
await fs.writeFile(nitroPath, nitroBuffer);
signal?.throwIfAborted();
let iconFileName: string | null = null;
let iconPath: string | null = null;
@@ -312,7 +307,6 @@ export async function uploadSingleFurni(params: {
}
const mirroredPaths: string[] = [];
signal?.throwIfAborted();
await mirrorUploadedAsset(
nitroPath,
nitroFileName,
@@ -352,7 +346,6 @@ export async function uploadSingleFurni(params: {
const interactionModesCount = autoInteraction.interactionModesCount;
let newId: number;
signal?.throwIfAborted();
try {
newId = await allocateItemsBaseId(async (nextId) => {
await db.execute(sql`
@@ -379,7 +372,6 @@ export async function uploadSingleFurni(params: {
}
try {
signal?.throwIfAborted();
const furniEntry = buildFurniEntry({
id: newId,
classname,
@@ -398,7 +390,6 @@ export async function uploadSingleFurni(params: {
let catalogItemId: number | null = null;
try {
signal?.throwIfAborted();
// Skip when the item already has a catalog row — re-uploads must never
// create a second row for the same item_ids.
const [existingCatalog] = (await db.execute(sql`