dist / imageSnapshots.js
dist / imageSnapshots.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImageSnapshots = void 0;
const node_crypto_1 = require("node:crypto");
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const images_1 = require("./images");
const hash = (bytes) => (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex");
// Snapshots belong to this chat's working directory, never a global image queue.
class ImageSnapshots {
getRoot;
directory;
constructor(getRoot) {
this.getRoot = getRoot;
}
root() {
return this.directory ??= (async () => {
const root = await (0, promises_1.realpath)(this.getRoot());
const directory = (0, node_path_1.join)(root, ".strata-image-cache");
await (0, promises_1.mkdir)(directory, { recursive: true });
const actual = await (0, promises_1.realpath)(directory);
const rel = (0, node_path_1.relative)(root, actual);
if (!rel || (0, node_path_1.isAbsolute)(rel) || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\"))
throw new Error("Image cache escapes the chat working directory.");
return actual;
})();
}
async atomic(path, data) {
const temp = `${path}.${(0, node_crypto_1.randomUUID)()}.tmp`;
try {
await (0, promises_1.writeFile)(temp, data, { flag: "wx" });
await (0, promises_1.rename)(temp, path);
}
finally {
await (0, promises_1.unlink)(temp).catch(() => { });
}
}
async remember(identity, bytes) {
if (bytes.length > images_1.IMAGE_LIMIT)
throw new Error("Image snapshot exceeds 20 MiB.");
const directory = await this.root();
const digest = hash(bytes);
await this.atomic((0, node_path_1.join)(directory, `${digest}.image`), bytes);
await this.atomic((0, node_path_1.join)(directory, `${hash(identity)}.json`), JSON.stringify({ version: 1, digest, size: bytes.length }));
}
async recover(identity, signal) {
signal.throwIfAborted();
const directory = await this.root();
let data;
try {
data = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(directory, `${hash(identity)}.json`), "utf8"));
}
catch (error) {
if (error.code === "ENOENT")
return;
throw error;
}
if (data.version !== 1 || !/^[a-f0-9]{64}$/.test(data.digest) || !Number.isInteger(data.size) || data.size > images_1.IMAGE_LIMIT || data.size < 1)
throw new Error("Invalid image snapshot index.");
const bytes = await (0, images_1.readImage)((0, node_path_1.join)(directory, `${data.digest}.image`), signal);
if (bytes.length !== data.size || hash(bytes) !== data.digest)
throw new Error("Image snapshot changed or is corrupt.");
(0, images_1.imageMime)(bytes);
return bytes;
}
}
exports.ImageSnapshots = ImageSnapshots;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImageSnapshots = void 0;
const node_crypto_1 = require("node:crypto");
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const images_1 = require("./images");
const hash = (bytes) => (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex");
// Snapshots belong to this chat's working directory, never a global image queue.
class ImageSnapshots {
getRoot;
directory;
constructor(getRoot) {
this.getRoot = getRoot;
}
root() {
return this.directory ??= (async () => {
const root = await (0, promises_1.realpath)(this.getRoot());
const directory = (0, node_path_1.join)(root, ".strata-image-cache");
await (0, promises_1.mkdir)(directory, { recursive: true });
const actual = await (0, promises_1.realpath)(directory);
const rel = (0, node_path_1.relative)(root, actual);
if (!rel || (0, node_path_1.isAbsolute)(rel) || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\"))
throw new Error("Image cache escapes the chat working directory.");
return actual;
})();
}
async atomic(path, data) {
const temp = `${path}.${(0, node_crypto_1.randomUUID)()}.tmp`;
try {
await (0, promises_1.writeFile)(temp, data, { flag: "wx" });
await (0, promises_1.rename)(temp, path);
}
finally {
await (0, promises_1.unlink)(temp).catch(() => { });
}
}
async remember(identity, bytes) {
if (bytes.length > images_1.IMAGE_LIMIT)
throw new Error("Image snapshot exceeds 20 MiB.");
const directory = await this.root();
const digest = hash(bytes);
await this.atomic((0, node_path_1.join)(directory, `${digest}.image`), bytes);
await this.atomic((0, node_path_1.join)(directory, `${hash(identity)}.json`), JSON.stringify({ version: 1, digest, size: bytes.length }));
}
async recover(identity, signal) {
signal.throwIfAborted();
const directory = await this.root();
let data;
try {
data = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(directory, `${hash(identity)}.json`), "utf8"));
}
catch (error) {
if (error.code === "ENOENT")
return;
throw error;
}
if (data.version !== 1 || !/^[a-f0-9]{64}$/.test(data.digest) || !Number.isInteger(data.size) || data.size > images_1.IMAGE_LIMIT || data.size < 1)
throw new Error("Invalid image snapshot index.");
const bytes = await (0, images_1.readImage)((0, node_path_1.join)(directory, `${data.digest}.image`), signal);
if (bytes.length !== data.size || hash(bytes) !== data.digest)
throw new Error("Image snapshot changed or is corrupt.");
(0, images_1.imageMime)(bytes);
return bytes;
}
}
exports.ImageSnapshots = ImageSnapshots;