@ttt-productions/media-processing-core 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/local-input.test.d.ts +2 -0
- package/dist/__tests__/local-input.test.d.ts.map +1 -0
- package/dist/__tests__/local-input.test.js +92 -0
- package/dist/__tests__/local-input.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/io/local-input.d.ts +39 -0
- package/dist/io/local-input.d.ts.map +1 -0
- package/dist/io/local-input.js +84 -0
- package/dist/io/local-input.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-input.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/local-input.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { readFile, stat, writeFile, mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { streamToTempFile, hashFileSha256, readFileHeader, ByteLimitExceededError, FILE_HEADER_BYTES, } from "../io/local-input.js";
|
|
8
|
+
const chunked = (buf, chunkSize) => {
|
|
9
|
+
const chunks = [];
|
|
10
|
+
for (let i = 0; i < buf.length; i += chunkSize)
|
|
11
|
+
chunks.push(buf.subarray(i, i + chunkSize));
|
|
12
|
+
return Readable.from(chunks);
|
|
13
|
+
};
|
|
14
|
+
describe("streamToTempFile", () => {
|
|
15
|
+
it("writes the exact bytes and reports bytesWritten; cleanup removes the file", async () => {
|
|
16
|
+
const data = randomBytes(256 * 1024 + 17);
|
|
17
|
+
const input = await streamToTempFile(chunked(data, 64 * 1024), { maxBytes: 1024 * 1024 });
|
|
18
|
+
try {
|
|
19
|
+
expect(input.bytesWritten).toBe(data.length);
|
|
20
|
+
expect(Buffer.compare(await readFile(input.localPath), data)).toBe(0);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await input.cleanup();
|
|
24
|
+
}
|
|
25
|
+
await expect(stat(input.localPath)).rejects.toThrow();
|
|
26
|
+
});
|
|
27
|
+
it("HARD CAP: throws ByteLimitExceededError and leaves no temp file behind", async () => {
|
|
28
|
+
const data = randomBytes(100_000);
|
|
29
|
+
let caught;
|
|
30
|
+
try {
|
|
31
|
+
await streamToTempFile(chunked(data, 10_000), { maxBytes: 50_000 });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
caught = err;
|
|
35
|
+
}
|
|
36
|
+
expect(caught).toBeInstanceOf(ByteLimitExceededError);
|
|
37
|
+
expect(caught.maxBytes).toBe(50_000);
|
|
38
|
+
});
|
|
39
|
+
it("a source stream error cleans up and propagates", async () => {
|
|
40
|
+
const bad = new Readable({
|
|
41
|
+
read() {
|
|
42
|
+
this.emit("error", new Error("source died"));
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
await expect(streamToTempFile(bad, { maxBytes: 1000 })).rejects.toThrow("source died");
|
|
46
|
+
});
|
|
47
|
+
it("rejects a non-positive maxBytes", async () => {
|
|
48
|
+
await expect(streamToTempFile(Readable.from([]), { maxBytes: 0 })).rejects.toThrow(/positive finite maxBytes/);
|
|
49
|
+
});
|
|
50
|
+
it("cleanup is idempotent", async () => {
|
|
51
|
+
const input = await streamToTempFile(Readable.from([Buffer.from("x")]), { maxBytes: 10 });
|
|
52
|
+
await input.cleanup();
|
|
53
|
+
await expect(input.cleanup()).resolves.toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("hashFileSha256", () => {
|
|
57
|
+
it("matches crypto's whole-buffer digest", async () => {
|
|
58
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "local-input-test-"));
|
|
59
|
+
const p = path.join(dir, "f");
|
|
60
|
+
const data = randomBytes(300_000);
|
|
61
|
+
try {
|
|
62
|
+
await writeFile(p, data);
|
|
63
|
+
const expected = createHash("sha256").update(data).digest("hex");
|
|
64
|
+
await expect(hashFileSha256(p)).resolves.toBe(expected);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await rm(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("readFileHeader", () => {
|
|
72
|
+
it("reads at most the requested window and the whole file when smaller", async () => {
|
|
73
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "local-input-test-"));
|
|
74
|
+
const p = path.join(dir, "f");
|
|
75
|
+
const data = randomBytes(FILE_HEADER_BYTES + 500);
|
|
76
|
+
try {
|
|
77
|
+
await writeFile(p, data);
|
|
78
|
+
const header = await readFileHeader(p);
|
|
79
|
+
expect(header.byteLength).toBe(FILE_HEADER_BYTES);
|
|
80
|
+
expect(Buffer.compare(Buffer.from(header), data.subarray(0, FILE_HEADER_BYTES))).toBe(0);
|
|
81
|
+
const small = await readFileHeader(p, 16);
|
|
82
|
+
expect(small.byteLength).toBe(16);
|
|
83
|
+
await writeFile(p, Buffer.from("tiny"));
|
|
84
|
+
const whole = await readFileHeader(p);
|
|
85
|
+
expect(Buffer.from(whole).toString()).toBe("tiny");
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await rm(dir, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
//# sourceMappingURL=local-input.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-input.test.js","sourceRoot":"","sources":["../../src/__tests__/local-input.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { Readable } from \"node:stream\";\nimport { readFile, stat, writeFile, mkdtemp, rm } from \"node:fs/promises\";\nimport { createHash, randomBytes } from \"node:crypto\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport {\n streamToTempFile,\n hashFileSha256,\n readFileHeader,\n ByteLimitExceededError,\n FILE_HEADER_BYTES,\n} from \"../io/local-input.js\";\n\nconst chunked = (buf: Buffer, chunkSize: number): Readable => {\n const chunks: Buffer[] = [];\n for (let i = 0; i < buf.length; i += chunkSize) chunks.push(buf.subarray(i, i + chunkSize));\n return Readable.from(chunks);\n};\n\ndescribe(\"streamToTempFile\", () => {\n it(\"writes the exact bytes and reports bytesWritten; cleanup removes the file\", async () => {\n const data = randomBytes(256 * 1024 + 17);\n const input = await streamToTempFile(chunked(data, 64 * 1024), { maxBytes: 1024 * 1024 });\n try {\n expect(input.bytesWritten).toBe(data.length);\n expect(Buffer.compare(await readFile(input.localPath), data)).toBe(0);\n } finally {\n await input.cleanup();\n }\n await expect(stat(input.localPath)).rejects.toThrow();\n });\n\n it(\"HARD CAP: throws ByteLimitExceededError and leaves no temp file behind\", async () => {\n const data = randomBytes(100_000);\n let caught: unknown;\n try {\n await streamToTempFile(chunked(data, 10_000), { maxBytes: 50_000 });\n } catch (err) {\n caught = err;\n }\n expect(caught).toBeInstanceOf(ByteLimitExceededError);\n expect((caught as ByteLimitExceededError).maxBytes).toBe(50_000);\n });\n\n it(\"a source stream error cleans up and propagates\", async () => {\n const bad = new Readable({\n read() {\n this.emit(\"error\", new Error(\"source died\"));\n },\n });\n await expect(streamToTempFile(bad, { maxBytes: 1000 })).rejects.toThrow(\"source died\");\n });\n\n it(\"rejects a non-positive maxBytes\", async () => {\n await expect(streamToTempFile(Readable.from([]), { maxBytes: 0 })).rejects.toThrow(\n /positive finite maxBytes/,\n );\n });\n\n it(\"cleanup is idempotent\", async () => {\n const input = await streamToTempFile(Readable.from([Buffer.from(\"x\")]), { maxBytes: 10 });\n await input.cleanup();\n await expect(input.cleanup()).resolves.toBeUndefined();\n });\n});\n\ndescribe(\"hashFileSha256\", () => {\n it(\"matches crypto's whole-buffer digest\", async () => {\n const dir = await mkdtemp(path.join(os.tmpdir(), \"local-input-test-\"));\n const p = path.join(dir, \"f\");\n const data = randomBytes(300_000);\n try {\n await writeFile(p, data);\n const expected = createHash(\"sha256\").update(data).digest(\"hex\");\n await expect(hashFileSha256(p)).resolves.toBe(expected);\n } finally {\n await rm(dir, { recursive: true, force: true });\n }\n });\n});\n\ndescribe(\"readFileHeader\", () => {\n it(\"reads at most the requested window and the whole file when smaller\", async () => {\n const dir = await mkdtemp(path.join(os.tmpdir(), \"local-input-test-\"));\n const p = path.join(dir, \"f\");\n const data = randomBytes(FILE_HEADER_BYTES + 500);\n try {\n await writeFile(p, data);\n const header = await readFileHeader(p);\n expect(header.byteLength).toBe(FILE_HEADER_BYTES);\n expect(Buffer.compare(Buffer.from(header), data.subarray(0, FILE_HEADER_BYTES))).toBe(0);\n\n const small = await readFileHeader(p, 16);\n expect(small.byteLength).toBe(16);\n\n await writeFile(p, Buffer.from(\"tiny\"));\n const whole = await readFileHeader(p);\n expect(Buffer.from(whole).toString()).toBe(\"tiny\");\n } finally {\n await rm(dir, { recursive: true, force: true });\n }\n });\n});\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./io/types.js";
|
|
|
10
10
|
export * from "./run-pipeline.js";
|
|
11
11
|
export * from "./inspection/inspect-media.js";
|
|
12
12
|
export * from "./io/fs.js";
|
|
13
|
+
export * from "./io/local-input.js";
|
|
13
14
|
export * from "./moderation/types.js";
|
|
14
15
|
export * from "./moderation/merge.js";
|
|
15
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAElC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAElC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AAEpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./io/types.js";
|
|
|
10
10
|
export * from "./run-pipeline.js";
|
|
11
11
|
export * from "./inspection/inspect-media.js";
|
|
12
12
|
export * from "./io/fs.js";
|
|
13
|
+
export * from "./io/local-input.js";
|
|
13
14
|
export * from "./moderation/types.js";
|
|
14
15
|
export * from "./moderation/merge.js";
|
|
15
16
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAElC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAElC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC;AAEpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC","sourcesContent":["export * from \"./image/index.js\"; // Not ./image.js\nexport * from \"./image/image-processor.js\";\n\nexport * from \"./video/index.js\"; // Not ./video.js\nexport * from \"./video/video-processor.js\";\n\nexport * from \"./process-media.js\";\nexport * from \"./workspace/temp.js\";\n\nexport * from \"./audio/index.js\"; // Not ./audio.js\nexport * from \"./audio/audio-processor.js\";\n\nexport * from \"./io/types.js\";\nexport * from \"./run-pipeline.js\";\n\nexport * from \"./inspection/inspect-media.js\";\n\nexport * from \"./io/fs.js\";\nexport * from \"./io/local-input.js\";\n\nexport * from \"./moderation/types.js\";\nexport * from \"./moderation/merge.js\";"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
/** Thrown by {@link streamToTempFile} when the source exceeds `maxBytes`. */
|
|
3
|
+
export declare class ByteLimitExceededError extends Error {
|
|
4
|
+
readonly maxBytes: number;
|
|
5
|
+
constructor(maxBytes: number);
|
|
6
|
+
}
|
|
7
|
+
export interface TempFileInput {
|
|
8
|
+
/** Absolute path of the fully-written temp file. */
|
|
9
|
+
localPath: string;
|
|
10
|
+
/** Total bytes written (always <= maxBytes). */
|
|
11
|
+
bytesWritten: number;
|
|
12
|
+
/** Removes the temp file and its private directory. Idempotent; never throws. */
|
|
13
|
+
cleanup: () => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface StreamToTempFileOptions {
|
|
16
|
+
/** HARD cap: the stream is destroyed and the temp file removed the moment the
|
|
17
|
+
* cap is crossed, so an oversized source never fully lands on disk. */
|
|
18
|
+
maxBytes: number;
|
|
19
|
+
/** Temp-dir prefix (default "media-input-"). */
|
|
20
|
+
prefix?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Streams a Readable into a private temp file with a hard byte cap and
|
|
24
|
+
* constant memory. On success the caller MUST invoke `cleanup()` in a
|
|
25
|
+
* `finally`. On any failure (cap exceeded, stream error) the temp file is
|
|
26
|
+
* removed before the error propagates.
|
|
27
|
+
*/
|
|
28
|
+
export declare function streamToTempFile(source: Readable, options: StreamToTempFileOptions): Promise<TempFileInput>;
|
|
29
|
+
/** Streams a file through SHA-256 (constant memory) and returns the lowercase hex digest. */
|
|
30
|
+
export declare function hashFileSha256(localPath: string): Promise<string>;
|
|
31
|
+
/** Default header window for signature-family checks (matches the inspector's sniff window). */
|
|
32
|
+
export declare const FILE_HEADER_BYTES: number;
|
|
33
|
+
/**
|
|
34
|
+
* Reads at most `maxBytes` (default 64 KiB) from the start of a file — the
|
|
35
|
+
* bounded input for {@link detectSignatureFamily}-style checks without loading
|
|
36
|
+
* the whole source.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readFileHeader(localPath: string, maxBytes?: number): Promise<Uint8Array>;
|
|
39
|
+
//# sourceMappingURL=local-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-input.d.ts","sourceRoot":"","sources":["../../src/io/local-input.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,6EAA6E;AAC7E,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACd,QAAQ,EAAE,MAAM;CAK7B;AAED,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC;4EACwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,aAAa,CAAC,CA2BxB;AAED,6FAA6F;AAC7F,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMvE;AAED,gGAAgG;AAChG,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,QAAQ,GAAE,MAA0B,GACnC,OAAO,CAAC,UAAU,CAAC,CASrB"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Bounded-memory local-input primitives (canonical-upload-content-classification,
|
|
2
|
+
// bounded-memory unit). Safety scanning and hashing must never hold a full media
|
|
3
|
+
// source in JS memory: sources stream to a generation-pinned temp file with a
|
|
4
|
+
// hard byte cap, hashes stream from disk, and signature checks read only a
|
|
5
|
+
// bounded header. These primitives are generic (no storage/vendor deps) — the
|
|
6
|
+
// caller supplies the Readable and owns generation pinning.
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
9
|
+
import { mkdtemp, open, rm } from "node:fs/promises";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { pipeline } from "node:stream/promises";
|
|
13
|
+
/** Thrown by {@link streamToTempFile} when the source exceeds `maxBytes`. */
|
|
14
|
+
export class ByteLimitExceededError extends Error {
|
|
15
|
+
maxBytes;
|
|
16
|
+
constructor(maxBytes) {
|
|
17
|
+
super(`source exceeded the ${maxBytes}-byte limit`);
|
|
18
|
+
this.name = "ByteLimitExceededError";
|
|
19
|
+
this.maxBytes = maxBytes;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Streams a Readable into a private temp file with a hard byte cap and
|
|
24
|
+
* constant memory. On success the caller MUST invoke `cleanup()` in a
|
|
25
|
+
* `finally`. On any failure (cap exceeded, stream error) the temp file is
|
|
26
|
+
* removed before the error propagates.
|
|
27
|
+
*/
|
|
28
|
+
export async function streamToTempFile(source, options) {
|
|
29
|
+
const { maxBytes, prefix = "media-input-" } = options;
|
|
30
|
+
if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
|
|
31
|
+
throw new Error("streamToTempFile requires a positive finite maxBytes");
|
|
32
|
+
}
|
|
33
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), prefix));
|
|
34
|
+
const localPath = path.join(dir, "input");
|
|
35
|
+
const cleanup = async () => {
|
|
36
|
+
await rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
37
|
+
};
|
|
38
|
+
let bytesWritten = 0;
|
|
39
|
+
try {
|
|
40
|
+
const sink = createWriteStream(localPath, { flags: "wx" });
|
|
41
|
+
await pipeline(source, async function* (chunks) {
|
|
42
|
+
for await (const chunk of chunks) {
|
|
43
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
44
|
+
bytesWritten += buf.byteLength;
|
|
45
|
+
if (bytesWritten > maxBytes)
|
|
46
|
+
throw new ByteLimitExceededError(maxBytes);
|
|
47
|
+
yield buf;
|
|
48
|
+
}
|
|
49
|
+
}, sink);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
await cleanup();
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
return { localPath, bytesWritten, cleanup };
|
|
56
|
+
}
|
|
57
|
+
/** Streams a file through SHA-256 (constant memory) and returns the lowercase hex digest. */
|
|
58
|
+
export async function hashFileSha256(localPath) {
|
|
59
|
+
const hash = createHash("sha256");
|
|
60
|
+
await pipeline(createReadStream(localPath), async (chunks) => {
|
|
61
|
+
for await (const chunk of chunks)
|
|
62
|
+
hash.update(chunk);
|
|
63
|
+
});
|
|
64
|
+
return hash.digest("hex");
|
|
65
|
+
}
|
|
66
|
+
/** Default header window for signature-family checks (matches the inspector's sniff window). */
|
|
67
|
+
export const FILE_HEADER_BYTES = 64 * 1024;
|
|
68
|
+
/**
|
|
69
|
+
* Reads at most `maxBytes` (default 64 KiB) from the start of a file — the
|
|
70
|
+
* bounded input for {@link detectSignatureFamily}-style checks without loading
|
|
71
|
+
* the whole source.
|
|
72
|
+
*/
|
|
73
|
+
export async function readFileHeader(localPath, maxBytes = FILE_HEADER_BYTES) {
|
|
74
|
+
const handle = await open(localPath, "r");
|
|
75
|
+
try {
|
|
76
|
+
const buf = Buffer.alloc(maxBytes);
|
|
77
|
+
const { bytesRead } = await handle.read(buf, 0, maxBytes, 0);
|
|
78
|
+
return buf.subarray(0, bytesRead);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await handle.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=local-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-input.js","sourceRoot":"","sources":["../../src/io/local-input.ts"],"names":[],"mappings":"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","sourcesContent":["// Bounded-memory local-input primitives (canonical-upload-content-classification,\n// bounded-memory unit). Safety scanning and hashing must never hold a full media\n// source in JS memory: sources stream to a generation-pinned temp file with a\n// hard byte cap, hashes stream from disk, and signature checks read only a\n// bounded header. These primitives are generic (no storage/vendor deps) — the\n// caller supplies the Readable and owns generation pinning.\n\nimport { createHash } from \"node:crypto\";\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { mkdtemp, open, rm } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { Readable } from \"node:stream\";\n\n/** Thrown by {@link streamToTempFile} when the source exceeds `maxBytes`. */\nexport class ByteLimitExceededError extends Error {\n readonly maxBytes: number;\n constructor(maxBytes: number) {\n super(`source exceeded the ${maxBytes}-byte limit`);\n this.name = \"ByteLimitExceededError\";\n this.maxBytes = maxBytes;\n }\n}\n\nexport interface TempFileInput {\n /** Absolute path of the fully-written temp file. */\n localPath: string;\n /** Total bytes written (always <= maxBytes). */\n bytesWritten: number;\n /** Removes the temp file and its private directory. Idempotent; never throws. */\n cleanup: () => Promise<void>;\n}\n\nexport interface StreamToTempFileOptions {\n /** HARD cap: the stream is destroyed and the temp file removed the moment the\n * cap is crossed, so an oversized source never fully lands on disk. */\n maxBytes: number;\n /** Temp-dir prefix (default \"media-input-\"). */\n prefix?: string;\n}\n\n/**\n * Streams a Readable into a private temp file with a hard byte cap and\n * constant memory. On success the caller MUST invoke `cleanup()` in a\n * `finally`. On any failure (cap exceeded, stream error) the temp file is\n * removed before the error propagates.\n */\nexport async function streamToTempFile(\n source: Readable,\n options: StreamToTempFileOptions,\n): Promise<TempFileInput> {\n const { maxBytes, prefix = \"media-input-\" } = options;\n if (!Number.isFinite(maxBytes) || maxBytes <= 0) {\n throw new Error(\"streamToTempFile requires a positive finite maxBytes\");\n }\n const dir = await mkdtemp(path.join(os.tmpdir(), prefix));\n const localPath = path.join(dir, \"input\");\n const cleanup = async () => {\n await rm(dir, { recursive: true, force: true }).catch(() => {});\n };\n\n let bytesWritten = 0;\n try {\n const sink = createWriteStream(localPath, { flags: \"wx\" });\n await pipeline(source, async function* (chunks: AsyncIterable<Buffer | string>) {\n for await (const chunk of chunks) {\n const buf = typeof chunk === \"string\" ? Buffer.from(chunk) : chunk;\n bytesWritten += buf.byteLength;\n if (bytesWritten > maxBytes) throw new ByteLimitExceededError(maxBytes);\n yield buf;\n }\n }, sink);\n } catch (err) {\n await cleanup();\n throw err;\n }\n return { localPath, bytesWritten, cleanup };\n}\n\n/** Streams a file through SHA-256 (constant memory) and returns the lowercase hex digest. */\nexport async function hashFileSha256(localPath: string): Promise<string> {\n const hash = createHash(\"sha256\");\n await pipeline(createReadStream(localPath), async (chunks: AsyncIterable<Buffer>) => {\n for await (const chunk of chunks) hash.update(chunk);\n });\n return hash.digest(\"hex\");\n}\n\n/** Default header window for signature-family checks (matches the inspector's sniff window). */\nexport const FILE_HEADER_BYTES = 64 * 1024;\n\n/**\n * Reads at most `maxBytes` (default 64 KiB) from the start of a file — the\n * bounded input for {@link detectSignatureFamily}-style checks without loading\n * the whole source.\n */\nexport async function readFileHeader(\n localPath: string,\n maxBytes: number = FILE_HEADER_BYTES,\n): Promise<Uint8Array> {\n const handle = await open(localPath, \"r\");\n try {\n const buf = Buffer.alloc(maxBytes);\n const { bytesRead } = await handle.read(buf, 0, maxBytes, 0);\n return buf.subarray(0, bytesRead);\n } finally {\n await handle.close();\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/media-processing-core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.7",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/ttt-productions/ttt-packages.git",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^22.10.0",
|
|
48
|
-
"firebase-admin": "^
|
|
48
|
+
"firebase-admin": "^14.0.0",
|
|
49
49
|
"typescript": "^5.8.3"
|
|
50
50
|
},
|
|
51
51
|
"author": "DJ (TTT Productions)",
|