Claude c17445874d
style: use top-level type imports in the media changes
AGENTS.md asks for top-level `import type` instead of inline `type`
specifiers; this applies it to the import statements the media work added
or changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 14:50:16 +00:00

240 lines
7.1 KiB
TypeScript

import { createHash, randomUUID } from "node:crypto";
import { constants } from "node:fs";
import type { FileHandle } from "node:fs/promises";
import { lstat, mkdir, open, rm } from "node:fs/promises";
import path from "node:path";
import type {
AdapterContext,
ArtifactByteRange,
ArtifactPut,
ArtifactStore,
ArtifactStreamPut,
NotificationMessage,
NotificationProvider,
StoredArtifactStream,
} from "@rakazo/adapter-kit";
import { ArtifactTooLargeError } from "@rakazo/adapter-kit";
const O_NOFOLLOW = constants.O_NOFOLLOW ?? 0;
const READ_CHUNK_BYTES = 256 * 1024;
/** Space ids and artifact keys are generated ids; anything else could address another path. */
const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
export class LocalArtifactStore implements ArtifactStore {
constructor(private readonly root: string) {}
describe() {
return {
id: "local-artifacts",
contractVersion: "1",
adapterVersion: "0.1.0",
capabilities: { stream: true },
};
}
private spaceDir(context: AdapterContext) {
if (!SAFE_SEGMENT.test(context.spaceId)) throw new Error("Invalid artifact space");
return path.join(this.root, "artifacts", context.spaceId);
}
private file(key: string, context: AdapterContext) {
if (!SAFE_SEGMENT.test(key)) throw new Error("Invalid artifact key");
return path.join(this.spaceDir(context), key);
}
/** Creates a new owner-only file under a fresh key; never follows or replaces an existing path. */
private async create(context: AdapterContext) {
const dir = this.spaceDir(context);
await mkdir(dir, { recursive: true });
const key = randomUUID();
const file = path.join(dir, key);
const handle = await open(
file,
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | O_NOFOLLOW,
0o600,
);
return { key, file, handle };
}
private openForRead(key: string, context: AdapterContext) {
return open(this.file(key, context), constants.O_RDONLY | O_NOFOLLOW);
}
async put(artifact: ArtifactPut, context: AdapterContext) {
const { key, handle } = await this.create(context);
try {
await handle.writeFile(artifact.bytes);
} finally {
await handle.close();
}
return { id: key, hash: String(artifact.bytes.byteLength) };
}
async putStream(
artifact: ArtifactStreamPut,
context: AdapterContext,
): Promise<StoredArtifactStream> {
if (!Number.isSafeInteger(artifact.maxBytes) || artifact.maxBytes < 0) {
throw new Error("maxBytes must be a non-negative integer");
}
const { key, file, handle } = await this.create(context);
const hash = createHash("sha256");
let size = 0;
try {
try {
for await (const chunk of byteChunks(artifact.stream)) {
context.signal.throwIfAborted();
size += chunk.byteLength;
if (size > artifact.maxBytes) throw new ArtifactTooLargeError(artifact.maxBytes);
hash.update(chunk);
await writeFully(handle, chunk);
}
} finally {
await handle.close();
}
} catch (error) {
await rm(file, { force: true }).catch(() => undefined);
throw error;
}
return { key, size, sha256: hash.digest("hex") };
}
async get(id: string, context: AdapterContext) {
const handle = await this.openForRead(id, context);
try {
return new Uint8Array(await handle.readFile());
} finally {
await handle.close();
}
}
async stat(key: string, context: AdapterContext) {
const handle = await this.openForRead(key, context);
try {
const info = await handle.stat();
if (!info.isFile()) throw new Error("Artifact is not a file");
return { size: info.size };
} finally {
await handle.close();
}
}
async createReadStream(key: string, context: AdapterContext, range?: ArtifactByteRange) {
const handle = await this.openForRead(key, context);
try {
const info = await handle.stat();
if (!info.isFile()) throw new Error("Artifact is not a file");
const start = range?.start ?? 0;
const end = range?.end ?? info.size - 1;
if (
range &&
(!Number.isSafeInteger(start) ||
!Number.isSafeInteger(end) ||
start < 0 ||
end < start ||
end >= info.size)
) {
throw new RangeError("Range does not fit the artifact");
}
return fileByteStream(handle, start, end);
} catch (error) {
await handle.close();
throw error;
}
}
async localPath(key: string, context: AdapterContext) {
const file = this.file(key, context);
const info = await lstat(file);
if (!info.isFile()) throw new Error("Artifact is not a file");
return file;
}
async remove(id: string, context: AdapterContext) {
await rm(this.file(id, context), { force: true });
}
}
async function* byteChunks(
source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
): AsyncIterable<Uint8Array> {
if (!("getReader" in source)) {
yield* source;
return;
}
const reader = source.getReader();
try {
for (;;) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
} finally {
// Stop the producer (e.g. an upload body) when the store gives up early, without waiting on
// a producer that may never confirm.
void reader.cancel().catch(() => undefined);
reader.releaseLock();
}
}
async function writeFully(handle: FileHandle, chunk: Uint8Array) {
let offset = 0;
while (offset < chunk.byteLength) {
const { bytesWritten } = await handle.write(chunk, offset, chunk.byteLength - offset);
offset += bytesWritten;
}
}
/** Streams [start, end] of an open file and closes it when done, failed or cancelled. */
function fileByteStream(handle: FileHandle, start: number, end: number) {
let position = start;
let closed = false;
const close = async () => {
if (closed) return;
closed = true;
await handle.close().catch(() => undefined);
};
return new ReadableStream<Uint8Array>({
async pull(controller) {
try {
const length = Math.min(end - position + 1, READ_CHUNK_BYTES);
if (length <= 0) {
await close();
controller.close();
return;
}
const buffer = new Uint8Array(length);
const { bytesRead } = await handle.read(buffer, 0, length, position);
if (bytesRead === 0) throw new Error("Artifact ended before the requested range");
position += bytesRead;
controller.enqueue(bytesRead === length ? buffer : buffer.subarray(0, bytesRead));
if (position > end) {
await close();
controller.close();
}
} catch (error) {
await close();
controller.error(error);
}
},
cancel: close,
});
}
export class CapturingNotificationProvider implements NotificationProvider {
readonly sent: NotificationMessage[] = [];
describe() {
return {
id: "capturing",
contractVersion: "1",
adapterVersion: "0.1.0",
capabilities: { push: false, email: false },
};
}
async send(message: NotificationMessage, _context: AdapterContext): Promise<void> {
this.sent.push(message);
}
}