Release Privacy Tools 0.1.0

This commit is contained in:
2026-09-01 02:39:44 +02:00
commit bfd6422149
79 changed files with 13485 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
import {
encodeText,
sanitizeDownloadFilename,
stableStringify,
} from "@add-ideas/toolbox-helpers";
import { zipSync } from "fflate";
import { APP_VERSION } from "../version";
import {
assertLimit,
DEFAULT_PRIVACY_LIMITS,
PrivacyLimitError,
} from "./limits";
import type {
BatchReport,
ImageScanResult,
SanitizedAsset,
SanitizationReport,
} from "./model";
export function createBatchReport(
files: readonly ImageScanResult[],
assets: readonly SanitizedAsset[],
generatedAt = new Date().toISOString(),
): BatchReport {
return {
schemaVersion: 1,
generatedAt,
application: { name: "Privacy Tools", version: APP_VERSION },
files: [...files],
sanitizations: assets.map((asset) => asset.report),
warnings: [
"This report may itself contain sensitive source metadata and filenames; store and share it deliberately.",
"A successful re-scan is not an anonymity guarantee; see each sanitization disclaimer.",
],
};
}
export function serializeReport(
report: BatchReport | SanitizationReport,
): string {
return stableStringify(report, 2, {
maxDepth: 64,
maxNodes: 500_000,
maxTextChars: 2_000_000,
});
}
export async function createBatchArchive(
files: readonly ImageScanResult[],
assets: readonly SanitizedAsset[],
generatedAt = new Date().toISOString(),
): Promise<Blob> {
assertLimit(
files.length,
DEFAULT_PRIVACY_LIMITS.maxFiles,
"Report file count",
);
assertLimit(
assets.length,
DEFAULT_PRIVACY_LIMITS.maxFiles,
"Archive image count",
);
const entries: Record<string, Uint8Array> = Object.create(null) as Record<
string,
Uint8Array
>;
const names = new Set<string>();
let total = 0;
for (const asset of assets) {
const name = uniqueName(asset.report.outputName, names);
const bytes = new Uint8Array(await asset.blob.arrayBuffer());
total += bytes.byteLength;
if (total > DEFAULT_PRIVACY_LIMITS.maxZipBytes)
throw new PrivacyLimitError(
"Batch archive input size",
total,
DEFAULT_PRIVACY_LIMITS.maxZipBytes,
);
entries[`images/${name}`] = bytes;
}
const report = encodeText(
serializeReport(createBatchReport(files, assets, generatedAt)),
);
total += report.byteLength;
if (total > DEFAULT_PRIVACY_LIMITS.maxZipBytes)
throw new PrivacyLimitError(
"Batch archive input size",
total,
DEFAULT_PRIVACY_LIMITS.maxZipBytes,
);
entries["privacy-tools-report.json"] = report;
const archive = zipSync(entries, {
level: 0,
mtime: new Date("1980-01-01T00:00:00.000Z"),
});
if (archive.byteLength > DEFAULT_PRIVACY_LIMITS.maxZipBytes)
throw new PrivacyLimitError(
"Batch archive output size",
archive.byteLength,
DEFAULT_PRIVACY_LIMITS.maxZipBytes,
);
const ownedArchive = archive.slice().buffer as ArrayBuffer;
return new Blob([ownedArchive], { type: "application/zip" });
}
function uniqueName(input: string, used: Set<string>): string {
const safe = sanitizeDownloadFilename(input, "image.clean");
if (!used.has(safe)) {
used.add(safe);
return safe;
}
const dot = safe.lastIndexOf(".");
const stem = dot > 0 ? safe.slice(0, dot) : safe;
const extension = dot > 0 ? safe.slice(dot) : "";
let counter = 2;
while (used.has(`${stem}-${counter}${extension}`)) counter += 1;
const candidate = `${stem}-${counter}${extension}`;
used.add(candidate);
return candidate;
}