blob: a43c9d1c04dd38d43ca4f4776de4e413dd890e17 [file]
import {
ARTIFACT_KINDS,
ARTIFACT_SOURCES,
ARTIFACT_STATUSES,
type ArtifactRecord,
type ArtifactBinaryReadFailureReason,
type ArtifactKind,
type ArtifactReadFailureReason,
type ArtifactSource,
type ArtifactStatus,
isArtifactTurnKey,
isCanonicalArtifactEntityId,
} from '@maka/core/artifacts';
import { MAX_ATTACHMENT_BYTES } from '@maka/core/attachments';
import { isCanonicalAttachmentRef, type AttachmentRef } from '@maka/core/events';
import { requireCount, requireEntityId, requireExactRecord, requireRecord } from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { defineOperation } from './operation-spec.js';
export const ARTIFACT_PAGE_MAX_ITEMS = 128;
export const ARTIFACT_RESULT_MAX_BYTES = 48 * 1024;
export const ARTIFACT_PREVIEW_MAX_BYTES = 32 * 1024;
export const ARTIFACT_READ_CHUNK_MAX_BYTES = 32 * 1024;
export const ARTIFACT_CURSOR_MAX_BYTES = 32;
export const ARTIFACT_NAME_MAX_BYTES = 512;
export const ARTIFACT_MIME_TYPE_MAX_BYTES = 512;
export const ARTIFACT_SUMMARY_MAX_BYTES = 8 * 1024;
export const ARTIFACT_INGEST_CHUNK_MAX_BYTES = 48 * 1024;
export const ARTIFACT_INGEST_MIME_TYPE_MAX_BYTES = 256;
const QUERY_ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'internal_failure',
'invalid_request',
'not_found',
'persistence_failed',
] as const;
const DELETE_ERRORS = [...QUERY_ERRORS, 'operation_conflict'] as const;
const ARTIFACT_REQUIRED_FIELDS = [
'id',
'sessionId',
'turnId',
'createdAt',
'name',
'kind',
'sizeBytes',
'status',
] as const;
const ARTIFACT_FIELDS = new Set([...ARTIFACT_REQUIRED_FIELDS, 'mimeType', 'source', 'summary']);
export type ArtifactRevision = `sha256:${string}`;
export interface ArtifactProjection {
readonly id: string;
readonly sessionId: string;
readonly turnId: string;
readonly createdAt: number;
readonly name: string;
readonly kind: ArtifactKind;
readonly sizeBytes: number;
readonly mimeType?: string;
readonly source?: ArtifactSource;
readonly summary?: string;
readonly status: ArtifactStatus;
}
export type ArtifactQueryInput =
| { readonly kind: 'list_start'; readonly sessionId: string }
| {
readonly kind: 'list_continue';
readonly sessionId: string;
readonly revision: ArtifactRevision;
readonly cursor: string;
}
| { readonly kind: 'get'; readonly sessionId: string; readonly artifactId: string }
| { readonly kind: 'read_text'; readonly sessionId: string; readonly artifactId: string }
| { readonly kind: 'read_binary'; readonly sessionId: string; readonly artifactId: string }
| {
readonly kind: 'read_chunk';
readonly sessionId: string;
readonly artifactId: string;
readonly offset: number;
};
export type ArtifactTextPreview =
| { readonly ok: true; readonly text: string }
| { readonly ok: false; readonly reason: ArtifactReadFailureReason };
export type ArtifactBinaryPreview =
| { readonly ok: true; readonly base64: string; readonly mimeType: string }
| { readonly ok: false; readonly reason: ArtifactBinaryReadFailureReason };
export type ArtifactQueryResult =
| {
readonly kind: 'page';
readonly sessionId: string;
readonly revision: ArtifactRevision;
readonly artifacts: readonly ArtifactProjection[];
readonly nextCursor: string | null;
}
| {
readonly kind: 'revision_changed';
readonly expected: ArtifactRevision;
readonly actual: ArtifactRevision;
}
| {
readonly kind: 'artifact';
readonly sessionId: string;
readonly revision: ArtifactRevision;
readonly artifact: ArtifactProjection | null;
}
| {
readonly kind: 'text';
readonly sessionId: string;
readonly artifactId: string;
readonly preview: ArtifactTextPreview;
}
| {
readonly kind: 'binary';
readonly sessionId: string;
readonly artifactId: string;
readonly preview: ArtifactBinaryPreview;
}
| {
readonly kind: 'chunk';
readonly sessionId: string;
readonly artifactId: string;
readonly offset: number;
readonly totalBytes: number;
readonly chunkBase64: string;
readonly nextOffset: number | null;
};
export interface ArtifactDeleteInput {
readonly sessionId: string;
readonly artifactId: string;
}
export interface ArtifactDeleteResult {
readonly kind: 'deleted';
readonly artifact: ArtifactProjection;
}
export type ArtifactIngestInput =
| {
readonly kind: 'begin';
readonly sessionId: string;
readonly uploadId: string;
readonly name: string;
readonly mimeType: string;
readonly totalBytes: number;
readonly contentSha256: `sha256:${string}`;
}
| {
readonly kind: 'chunk';
readonly sessionId: string;
readonly uploadId: string;
readonly offset: number;
readonly chunkBase64: string;
}
| { readonly kind: 'commit'; readonly sessionId: string; readonly uploadId: string }
| { readonly kind: 'abort'; readonly sessionId: string; readonly uploadId: string };
export type ArtifactIngestResult =
| { readonly kind: 'upload_opened'; readonly uploadId: string; readonly nextOffset: number }
| { readonly kind: 'chunk_accepted'; readonly uploadId: string; readonly nextOffset: number }
| { readonly kind: 'committed'; readonly uploadId: string; readonly attachment: AttachmentRef }
| { readonly kind: 'upload_aborted'; readonly uploadId: string };
export const ARTIFACT_OPERATION_SPECS = {
'artifact.ingest': defineOperation<
ArtifactIngestInput,
ArtifactIngestResult,
| 'host_not_ready'
| 'host_draining'
| 'operation_unavailable'
| 'invalid_request'
| 'not_found'
| 'operation_conflict'
| 'persistence_failed'
| 'internal_failure'
>({
mode: 'command',
availability: 'ready',
errors: [
'host_not_ready',
'host_draining',
'operation_unavailable',
'invalid_request',
'not_found',
'operation_conflict',
'persistence_failed',
'internal_failure',
],
decodeInput: decodeArtifactIngestInput,
decodeOutput: decodeArtifactIngestResult,
assertOutputForInput: (input, output) => {
if (input.uploadId !== output.uploadId) {
throw invalidProtocolFrame('Artifact ingest changed upload identity');
}
if (
output.kind === 'committed' &&
(output.attachment.ref.kind !== 'session_file' ||
output.attachment.ref.sessionId !== input.sessionId)
) {
throw invalidProtocolFrame('Artifact ingest changed Session identity');
}
},
}),
'artifact.query': defineOperation<
ArtifactQueryInput,
ArtifactQueryResult,
(typeof QUERY_ERRORS)[number]
>({
mode: 'query',
availability: 'ready',
errors: QUERY_ERRORS,
decodeInput: decodeArtifactQueryInput,
decodeOutput: decodeArtifactQueryResult,
}),
'artifact.delete': defineOperation<
ArtifactDeleteInput,
ArtifactDeleteResult,
(typeof DELETE_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: DELETE_ERRORS,
decodeInput: decodeArtifactDeleteInput,
decodeOutput: decodeArtifactDeleteResult,
}),
} as const;
export function decodeArtifactIngestInput(value: unknown): ArtifactIngestInput {
const input = requireRecord(value, 'artifact ingest input');
if (input.kind === 'begin') {
const exact = requireExactRecord(input, 'artifact ingest begin input', [
'kind',
'sessionId',
'uploadId',
'name',
'mimeType',
'totalBytes',
'contentSha256',
]);
return {
kind: 'begin',
sessionId: requireEntityId(exact.sessionId, 'sessionId'),
uploadId: requireEntityId(exact.uploadId, 'uploadId'),
name: boundedIngestText(exact.name, 'artifact ingest name', ARTIFACT_NAME_MAX_BYTES),
mimeType: boundedIngestText(
exact.mimeType,
'artifact ingest mime type',
ARTIFACT_INGEST_MIME_TYPE_MAX_BYTES,
),
totalBytes: boundedAttachmentBytes(exact.totalBytes),
contentSha256: contentDigest(exact.contentSha256),
};
}
if (input.kind === 'chunk') {
const exact = requireExactRecord(input, 'artifact ingest chunk input', [
'kind',
'sessionId',
'uploadId',
'offset',
'chunkBase64',
]);
const chunkBase64 = boundedText(
exact.chunkBase64,
'artifact ingest chunk',
Math.ceil((ARTIFACT_INGEST_CHUNK_MAX_BYTES * 4) / 3) + 4,
true,
);
if (!isCanonicalBase64(chunkBase64)) {
throw invalidProtocolFrame('Invalid artifact ingest chunk');
}
const chunk = Buffer.from(chunkBase64, 'base64');
if (chunk.byteLength === 0 || chunk.byteLength > ARTIFACT_INGEST_CHUNK_MAX_BYTES) {
throw invalidProtocolFrame('Invalid artifact ingest chunk');
}
return {
kind: 'chunk',
sessionId: requireEntityId(exact.sessionId, 'sessionId'),
uploadId: requireEntityId(exact.uploadId, 'uploadId'),
offset: requireCount(exact.offset, 'artifact ingest offset'),
chunkBase64,
};
}
if (input.kind === 'commit' || input.kind === 'abort') {
const exact = requireExactRecord(input, 'artifact ingest terminal input', [
'kind',
'sessionId',
'uploadId',
]);
return {
kind: input.kind,
sessionId: requireEntityId(exact.sessionId, 'sessionId'),
uploadId: requireEntityId(exact.uploadId, 'uploadId'),
};
}
throw invalidProtocolFrame('Invalid artifact ingest kind');
}
export function decodeArtifactIngestResult(value: unknown): ArtifactIngestResult {
const result = requireRecord(value, 'artifact ingest result');
if (result.kind === 'upload_opened' || result.kind === 'chunk_accepted') {
const exact = requireExactRecord(result, 'artifact ingest progress result', [
'kind',
'uploadId',
'nextOffset',
]);
return {
kind: result.kind,
uploadId: requireEntityId(exact.uploadId, 'uploadId'),
nextOffset: requireCount(exact.nextOffset, 'artifact ingest next offset'),
};
}
if (result.kind === 'upload_aborted') {
const exact = requireExactRecord(result, 'artifact ingest abort result', ['kind', 'uploadId']);
return { kind: 'upload_aborted', uploadId: requireEntityId(exact.uploadId, 'uploadId') };
}
if (result.kind === 'committed') {
const exact = requireExactRecord(result, 'artifact ingest committed result', [
'kind',
'uploadId',
'attachment',
]);
if (!isCanonicalAttachmentRef(exact.attachment)) {
throw invalidProtocolFrame('Invalid artifact ingest AttachmentRef');
}
if (exact.attachment.ref.kind !== 'session_file') {
throw invalidProtocolFrame('Invalid artifact ingest AttachmentRef');
}
return {
kind: 'committed',
uploadId: requireEntityId(exact.uploadId, 'uploadId'),
attachment: {
...exact.attachment,
ref: { ...exact.attachment.ref },
},
};
}
throw invalidProtocolFrame('Invalid artifact ingest result kind');
}
export function decodeArtifactQueryInput(value: unknown): ArtifactQueryInput {
const input = requireRecord(value, 'artifact query input');
if (input.kind === 'list_start') {
const exact = requireExactRecord(input, 'artifact list start input', ['kind', 'sessionId']);
return { kind: 'list_start', sessionId: artifactEntityId(exact.sessionId, 'sessionId') };
}
if (input.kind === 'list_continue') {
const exact = requireExactRecord(input, 'artifact list continuation input', [
'kind',
'sessionId',
'revision',
'cursor',
]);
return {
kind: 'list_continue',
sessionId: artifactEntityId(exact.sessionId, 'sessionId'),
revision: artifactRevision(exact.revision, 'artifact revision'),
cursor: boundedText(exact.cursor, 'artifact cursor', ARTIFACT_CURSOR_MAX_BYTES),
};
}
if (input.kind === 'get' || input.kind === 'read_text' || input.kind === 'read_binary') {
const exact = requireExactRecord(input, 'artifact item query input', [
'kind',
'sessionId',
'artifactId',
]);
return {
kind: input.kind,
sessionId: artifactEntityId(exact.sessionId, 'sessionId'),
artifactId: artifactEntityId(exact.artifactId, 'artifactId'),
};
}
if (input.kind === 'read_chunk') {
const exact = requireExactRecord(input, 'artifact chunk query input', [
'kind',
'sessionId',
'artifactId',
'offset',
]);
return {
kind: 'read_chunk',
sessionId: artifactEntityId(exact.sessionId, 'sessionId'),
artifactId: artifactEntityId(exact.artifactId, 'artifactId'),
offset: requireCount(exact.offset, 'artifact chunk offset'),
};
}
throw invalidProtocolFrame('Invalid artifact query kind');
}
export function decodeArtifactDeleteInput(value: unknown): ArtifactDeleteInput {
const input = requireExactRecord(value, 'artifact delete input', ['sessionId', 'artifactId']);
return {
sessionId: artifactEntityId(input.sessionId, 'sessionId'),
artifactId: artifactEntityId(input.artifactId, 'artifactId'),
};
}
export function decodeArtifactQueryResult(value: unknown): ArtifactQueryResult {
const result = requireRecord(value, 'artifact query result');
let decoded: ArtifactQueryResult;
if (result.kind === 'revision_changed') {
const exact = requireExactRecord(result, 'artifact revision changed result', [
'kind',
'expected',
'actual',
]);
decoded = {
kind: 'revision_changed',
expected: artifactRevision(exact.expected, 'expected artifact revision'),
actual: artifactRevision(exact.actual, 'actual artifact revision'),
};
} else if (result.kind === 'artifact') {
const exact = requireExactRecord(result, 'artifact item result', [
'kind',
'sessionId',
'revision',
'artifact',
]);
decoded = {
kind: 'artifact',
sessionId: artifactEntityId(exact.sessionId, 'sessionId'),
revision: artifactRevision(exact.revision, 'artifact revision'),
artifact: exact.artifact === null ? null : decodeArtifactProjection(exact.artifact),
};
} else if (result.kind === 'page') {
const exact = requireExactRecord(result, 'artifact page result', [
'kind',
'sessionId',
'revision',
'artifacts',
'nextCursor',
]);
if (!Array.isArray(exact.artifacts) || exact.artifacts.length > ARTIFACT_PAGE_MAX_ITEMS) {
throw invalidProtocolFrame('Artifact page exceeds item limit');
}
decoded = {
kind: 'page',
sessionId: artifactEntityId(exact.sessionId, 'sessionId'),
revision: artifactRevision(exact.revision, 'artifact revision'),
artifacts: exact.artifacts.map(decodeArtifactProjection),
nextCursor:
exact.nextCursor === null
? null
: boundedText(exact.nextCursor, 'artifact next cursor', ARTIFACT_CURSOR_MAX_BYTES),
};
} else if (result.kind === 'text') {
const exact = requireExactRecord(result, 'artifact text result', [
'kind',
'sessionId',
'artifactId',
'preview',
]);
decoded = {
kind: 'text',
sessionId: artifactEntityId(exact.sessionId, 'sessionId'),
artifactId: artifactEntityId(exact.artifactId, 'artifactId'),
preview: decodeTextPreview(exact.preview),
};
} else if (result.kind === 'binary') {
const exact = requireExactRecord(result, 'artifact binary result', [
'kind',
'sessionId',
'artifactId',
'preview',
]);
decoded = {
kind: 'binary',
sessionId: artifactEntityId(exact.sessionId, 'sessionId'),
artifactId: artifactEntityId(exact.artifactId, 'artifactId'),
preview: decodeBinaryPreview(exact.preview),
};
} else if (result.kind === 'chunk') {
const exact = requireExactRecord(result, 'artifact chunk result', [
'kind',
'sessionId',
'artifactId',
'offset',
'totalBytes',
'chunkBase64',
'nextOffset',
]);
const offset = requireCount(exact.offset, 'artifact chunk offset');
const totalBytes = requireCount(exact.totalBytes, 'artifact chunk total bytes');
const chunkBase64 = boundedText(
exact.chunkBase64,
'artifact chunk',
Math.ceil((ARTIFACT_READ_CHUNK_MAX_BYTES * 4) / 3) + 4,
true,
);
if (!isCanonicalBase64(chunkBase64)) {
throw invalidProtocolFrame('Invalid artifact chunk');
}
const chunkBytes = Buffer.from(chunkBase64, 'base64').byteLength;
if (chunkBytes > ARTIFACT_READ_CHUNK_MAX_BYTES || offset + chunkBytes > totalBytes) {
throw invalidProtocolFrame('Invalid artifact chunk bounds');
}
const nextOffset =
exact.nextOffset === null
? null
: requireCount(exact.nextOffset, 'artifact chunk next offset');
if (
(nextOffset === null && offset + chunkBytes !== totalBytes) ||
(nextOffset !== null &&
(chunkBytes === 0 || nextOffset !== offset + chunkBytes || nextOffset >= totalBytes))
) {
throw invalidProtocolFrame('Invalid artifact chunk continuation');
}
decoded = {
kind: 'chunk',
sessionId: artifactEntityId(exact.sessionId, 'sessionId'),
artifactId: artifactEntityId(exact.artifactId, 'artifactId'),
offset,
totalBytes,
chunkBase64,
nextOffset,
};
} else {
throw invalidProtocolFrame('Invalid artifact query result kind');
}
assertResultSize(decoded);
return decoded;
}
export const encodeArtifactQueryResult = decodeArtifactQueryResult;
export function decodeArtifactDeleteResult(value: unknown): ArtifactDeleteResult {
const result = requireExactRecord(value, 'artifact delete result', ['kind', 'artifact']);
if (result.kind !== 'deleted') throw invalidProtocolFrame('Invalid artifact delete result kind');
const decoded = { kind: 'deleted' as const, artifact: decodeArtifactProjection(result.artifact) };
assertResultSize(decoded);
return decoded;
}
export const encodeArtifactDeleteResult = decodeArtifactDeleteResult;
export function encodeArtifactProjection(record: ArtifactRecord): ArtifactProjection {
return {
id: record.id,
sessionId: record.sessionId,
turnId: record.turnId,
createdAt: record.createdAt,
name: projectArtifactText(record.name, ARTIFACT_NAME_MAX_BYTES),
kind: record.kind,
sizeBytes: record.sizeBytes,
...(record.mimeType === undefined
? {}
: { mimeType: projectArtifactText(record.mimeType, ARTIFACT_MIME_TYPE_MAX_BYTES) }),
...(record.source === undefined ? {} : { source: record.source }),
...(record.summary === undefined
? {}
: { summary: projectArtifactText(record.summary, ARTIFACT_SUMMARY_MAX_BYTES) }),
status: record.status,
};
}
function decodeArtifactProjection(value: unknown): ArtifactProjection {
const record = requireRecord(value, 'artifact projection');
if (Object.keys(record).some((key) => !ARTIFACT_FIELDS.has(key))) {
throw invalidProtocolFrame('Unknown artifact projection field');
}
if (ARTIFACT_REQUIRED_FIELDS.some((field) => !Object.hasOwn(record, field))) {
throw invalidProtocolFrame('Invalid artifact projection fields');
}
return {
id: artifactEntityId(record.id, 'artifact id'),
sessionId: artifactEntityId(record.sessionId, 'artifact sessionId'),
turnId: artifactTurnKey(record.turnId),
createdAt: requireCount(record.createdAt, 'artifact createdAt'),
name: boundedText(record.name, 'artifact name', ARTIFACT_NAME_MAX_BYTES),
kind: artifactKind(record.kind),
sizeBytes: requireCount(record.sizeBytes, 'artifact sizeBytes'),
status: artifactStatus(record.status),
...(Object.hasOwn(record, 'mimeType')
? {
mimeType: boundedText(record.mimeType, 'artifact mimeType', ARTIFACT_MIME_TYPE_MAX_BYTES),
}
: {}),
...(Object.hasOwn(record, 'source') ? { source: artifactSource(record.source) } : {}),
...(Object.hasOwn(record, 'summary')
? { summary: boundedText(record.summary, 'artifact summary', ARTIFACT_SUMMARY_MAX_BYTES) }
: {}),
};
}
function projectArtifactText(value: string, maxBytes: number): string {
let bytes = 0;
let projected = '';
for (const codePoint of value) {
const scalar = codePoint.codePointAt(0)!;
const canonical = scalar <= 0x1f || scalar === 0x7f ? '\ufffd' : codePoint;
const width = Buffer.byteLength(canonical, 'utf8');
if (bytes + width > maxBytes) break;
projected += canonical;
bytes += width;
}
return projected || 'artifact';
}
function decodeTextPreview(value: unknown): ArtifactTextPreview {
const preview = requireRecord(value, 'artifact text preview');
if (preview.ok === true) {
const exact = requireExactRecord(preview, 'artifact text preview', ['ok', 'text']);
return {
ok: true,
text: boundedText(exact.text, 'artifact text', ARTIFACT_PREVIEW_MAX_BYTES, true),
};
}
const exact = requireExactRecord(preview, 'artifact text unavailable', ['ok', 'reason']);
if (exact.ok !== false) throw invalidProtocolFrame('Invalid artifact text preview outcome');
return { ok: false, reason: readFailureReason(exact.reason) };
}
function decodeBinaryPreview(value: unknown): ArtifactBinaryPreview {
const preview = requireRecord(value, 'artifact binary preview');
if (preview.ok === true) {
const exact = requireExactRecord(preview, 'artifact binary preview', [
'ok',
'base64',
'mimeType',
]);
const base64 = boundedText(exact.base64, 'artifact binary base64', base64MaxBytes(), true);
if (
!isCanonicalBase64(base64) ||
Buffer.from(base64, 'base64').byteLength > ARTIFACT_PREVIEW_MAX_BYTES
) {
throw invalidProtocolFrame('Invalid artifact binary base64');
}
return {
ok: true,
base64,
mimeType: boundedText(
exact.mimeType,
'artifact binary mimeType',
ARTIFACT_MIME_TYPE_MAX_BYTES,
),
};
}
const exact = requireExactRecord(preview, 'artifact binary unavailable', ['ok', 'reason']);
if (exact.ok !== false) throw invalidProtocolFrame('Invalid artifact binary preview outcome');
return { ok: false, reason: binaryReadFailureReason(exact.reason) };
}
function boundedText(value: unknown, label: string, maxBytes: number, allowEmpty = false): string {
if (typeof value !== 'string' || (!allowEmpty && value.length === 0)) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
if (Buffer.byteLength(value, 'utf8') > maxBytes) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value;
}
function boundedIngestText(value: unknown, label: string, maxBytes: number): string {
const text = boundedText(value, label, maxBytes);
// eslint-disable-next-line no-control-regex
if (/[�-]/.test(text)) throw invalidProtocolFrame(`Invalid ${label}`);
return text;
}
function artifactEntityId(value: unknown, label: string): string {
if (!isCanonicalArtifactEntityId(value)) throw invalidProtocolFrame(`Invalid ${label}`);
return value;
}
function artifactTurnKey(value: unknown): string {
if (!isArtifactTurnKey(value)) throw invalidProtocolFrame('Invalid artifact turnId');
return value;
}
function artifactRevision(value: unknown, label: string): ArtifactRevision {
if (typeof value !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(value)) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value as ArtifactRevision;
}
function boundedAttachmentBytes(value: unknown): number {
const bytes = requireCount(value, 'artifact ingest total bytes');
if (bytes > MAX_ATTACHMENT_BYTES) {
throw invalidProtocolFrame('Artifact ingest exceeds attachment byte limit');
}
return bytes;
}
function contentDigest(value: unknown): `sha256:${string}` {
if (typeof value !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(value)) {
throw invalidProtocolFrame('Invalid artifact ingest content digest');
}
return value as `sha256:${string}`;
}
function artifactKind(value: unknown): ArtifactKind {
if (typeof value !== 'string' || !ARTIFACT_KINDS.includes(value as ArtifactKind)) {
throw invalidProtocolFrame('Invalid artifact kind');
}
return value as ArtifactKind;
}
function artifactSource(value: unknown): ArtifactSource {
if (typeof value !== 'string' || !ARTIFACT_SOURCES.includes(value as ArtifactSource)) {
throw invalidProtocolFrame('Invalid artifact source');
}
return value as ArtifactSource;
}
function artifactStatus(value: unknown): ArtifactStatus {
if (typeof value !== 'string' || !ARTIFACT_STATUSES.includes(value as ArtifactStatus)) {
throw invalidProtocolFrame('Invalid artifact status');
}
return value as ArtifactStatus;
}
function readFailureReason(value: unknown): ArtifactReadFailureReason {
if (
value === 'not_found' ||
value === 'too_large' ||
value === 'read_failed' ||
value === 'not_allowed' ||
value === 'deleted'
) {
return value;
}
throw invalidProtocolFrame('Invalid artifact read unavailable reason');
}
function binaryReadFailureReason(value: unknown): ArtifactBinaryReadFailureReason {
return value === 'unsupported_mime' ? value : readFailureReason(value);
}
function base64MaxBytes(): number {
return Math.ceil(ARTIFACT_PREVIEW_MAX_BYTES / 3) * 4;
}
function isCanonicalBase64(value: string): boolean {
return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
}
function assertResultSize(value: unknown): void {
if (Buffer.byteLength(JSON.stringify(value), 'utf8') > ARTIFACT_RESULT_MAX_BYTES) {
throw invalidProtocolFrame('Artifact result exceeds byte limit');
}
}