diff --git a/apps/server/package.json b/apps/server/package.json index 328f55a..351ae6f 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -37,6 +37,7 @@ "@tanstack/react-start": "catalog:", "drizzle-orm": "catalog:", "jszip": "catalog:", + "lossless-json": "catalog:", "react": "catalog:", "react-dom": "catalog:", "systeminformation": "catalog:", diff --git a/apps/server/src/server/api/routers/crypto.router.ts b/apps/server/src/server/api/routers/crypto.router.ts index fdd7982..900ea28 100644 --- a/apps/server/src/server/api/routers/crypto.router.ts +++ b/apps/server/src/server/api/routers/crypto.router.ts @@ -10,12 +10,28 @@ import { } from '@furtherverse/crypto' import { ORPCError } from '@orpc/server' import JSZip from 'jszip' +import { + isInteger, + isSafeNumber, + LosslessNumber, + parse as losslessParse, + stringify as losslessStringify, +} from 'lossless-json' import { z } from 'zod' import { extractSafeZipFiles, ZipValidationError } from '@/server/safe-zip' import { getUxConfig } from '@/server/ux-config' import { db } from '../middlewares' import { os } from '../server' +const safeNumberParser = (value: string): number | string => { + if (isSafeNumber(value)) return Number(value) + if (isInteger(value)) return value + return Number(value) +} + +const toLosslessNumber = (value: string): LosslessNumber | string => + value !== '' && /^-?\d+$/.test(value) ? new LosslessNumber(value) : value + const summaryPayloadSchema = z .object({ taskId: z.string().min(1, 'summary.json must contain a non-empty taskId'), @@ -94,7 +110,7 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy let rawJson: unknown try { - rawJson = JSON.parse(Buffer.from(summaryFile.bytes).toString('utf-8')) + rawJson = losslessParse(Buffer.from(summaryFile.bytes).toString('utf-8'), undefined, safeNumberParser) } catch { throw new ORPCError('BAD_REQUEST', { message: 'summary.json in the ZIP is not valid JSON', @@ -138,15 +154,21 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy // Build final summary.json with flat structure (matching Kotlin reference) const finalSummary = { - orgId, - checkId, + orgId: toLosslessNumber(String(orgId)), + checkId: toLosslessNumber(checkId), taskId: summaryPayload.taskId, licence: config.licence, fingerprint: config.fingerprint, deviceSignature, summary: summaryPayload.summary ?? '', } - const summaryBytes = Buffer.from(JSON.stringify(finalSummary), 'utf-8') + const summaryJson = losslessStringify(finalSummary) + if (!summaryJson) { + throw new ORPCError('INTERNAL_SERVER_ERROR', { + message: 'Failed to serialize summary.json', + }) + } + const summaryBytes = Buffer.from(summaryJson, 'utf-8') // Build manifest.json (fixed file list, matching Kotlin reference) const manifestFiles: Record = { diff --git a/bun.lock b/bun.lock index 0ce52ec..d4b6754 100644 --- a/bun.lock +++ b/bun.lock @@ -49,6 +49,7 @@ "@tanstack/react-start": "catalog:", "drizzle-orm": "catalog:", "jszip": "catalog:", + "lossless-json": "^4.3.0", "react": "catalog:", "react-dom": "catalog:", "systeminformation": "catalog:", @@ -1078,6 +1079,8 @@ "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "lossless-json": ["lossless-json@4.3.0", "", {}, "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g=="], + "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], diff --git a/package.json b/package.json index 8574243..2990792 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "electron-builder": "^26.8.1", "electron-vite": "^5.0.0", "jszip": "^3.10.1", + "lossless-json": "^4.3.0", "motion": "^12.35.0", "nitro": "npm:nitro-nightly@3.0.1-20260227-181935-bfbb207c", "openpgp": "^6.0.1",