fix(artifacts): enforce recovery content limit

This commit is contained in:
Jinwoo-H
2026-08-31 16:23:15 -04:00
parent 3ab9ea0253
commit dc3a8452c6
2 changed files with 31 additions and 5 deletions
@@ -3,7 +3,11 @@ import { mkdtemp, readFile, readdir, rm, stat, truncate, writeFile } from 'node:
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { ARTIFACT_MAX_REQUEST_BYTES, artifactWriteRequestByteLength } from '../../shared/artifacts'
import {
ARTIFACT_MAX_CONTENT_BYTES,
ARTIFACT_MAX_REQUEST_BYTES,
artifactWriteRequestByteLength
} from '../../shared/artifacts'
import {
MAX_ARTIFACT_CREATE_INTENT_BYTES,
MAX_PENDING_ARTIFACT_CREATES,
@@ -270,9 +274,9 @@ describe('artifact create intent store', () => {
).toThrow(/unsupported format/)
})
it('persists a valid artifact request near the recovery limit', async () => {
it('persists a 5 MiB escaped artifact within the recovery limit', async () => {
const userDataPath = await createUserDataPath()
const nearLimitBody = { ...body, content: 'x'.repeat(ARTIFACT_MAX_REQUEST_BYTES - 200) }
const nearLimitBody = { ...body, content: '"'.repeat(ARTIFACT_MAX_CONTENT_BYTES) }
expect(
artifactWriteRequestByteLength({ sourceKey: '/repo/report.html', ...nearLimitBody })
).toBeLessThanOrEqual(ARTIFACT_MAX_REQUEST_BYTES)
@@ -289,7 +293,21 @@ describe('artifact create intent store', () => {
).not.toThrow()
const directory = join(userDataPath, 'profiles', 'local-profile', 'artifact-create-intents')
const [fileName] = await readdir(directory)
expect((await stat(join(directory, fileName))).size).toBeGreaterThan(ARTIFACT_MAX_REQUEST_BYTES)
expect((await stat(join(directory, fileName))).size).toBeGreaterThan(ARTIFACT_MAX_CONTENT_BYTES)
})
it('rejects oversized artifact content before creating a recovery record', async () => {
const userDataPath = await createUserDataPath()
expect(() =>
getOrCreateArtifactCreateIntent(
'local-profile',
userDataPath,
'/repo/report.html',
scope,
'key-a',
{ ...body, content: 'x'.repeat(ARTIFACT_MAX_CONTENT_BYTES + 1) }
)
).toThrow(/5 MiB limit/)
})
it('rejects an oversized recovery record before reading it', async () => {
@@ -10,7 +10,11 @@ import {
writeFileSync
} from 'node:fs'
import { join } from 'node:path'
import { ARTIFACT_MAX_REQUEST_BYTES } from '../../shared/artifacts'
import {
ARTIFACT_MAX_CONTENT_BYTES,
ARTIFACT_MAX_REQUEST_BYTES,
artifactContentByteLength
} from '../../shared/artifacts'
import {
bestEffortFsyncDirectorySync,
fsyncFileSync,
@@ -116,6 +120,7 @@ function isWriteBody(value: unknown): value is ArtifactWriteBody {
const body = value as Partial<ArtifactWriteBody>
return (
typeof body.content === 'string' &&
artifactContentByteLength(body.content) <= ARTIFACT_MAX_CONTENT_BYTES &&
(body.contentType === 'text/html' || body.contentType === 'text/markdown') &&
typeof body.fileName === 'string' &&
(body.title === undefined || typeof body.title === 'string')
@@ -193,6 +198,9 @@ export function getOrCreateArtifactCreateIntent(
idempotencyKey: string,
body: ArtifactWriteBody
): ArtifactCreateIntent {
if (artifactContentByteLength(body.content) > ARTIFACT_MAX_CONTENT_BYTES) {
throw new Error('Artifact content exceeds the 5 MiB limit.')
}
const existing = getArtifactCreateIntent(profileId, userDataPath, sourceKey, scope)
if (existing) {
return existing