refactor: move workflow definitions behind storage (#1195)

* refactor: move workflow definitions behind storage

* fix: normalize workflow metadata descriptions

* test: cover workflow storage failure paths
This commit is contained in:
Brad Groux 2026-08-23 16:57:49 -05:00 committed by GitHub
parent 14b6d591a5
commit 06d7fa61f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 399 additions and 129 deletions

View file

@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"maximumEntries": 49,
"maximumEntries": 48,
"entries": [
{
"path": "server/src/services/agent-health-service.ts",
@ -284,12 +284,6 @@
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/workflow-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/workflow-step-executor.ts",
"category": "authoritative-persistence",

View file

@ -83,7 +83,9 @@
"src/__tests__/task-service-sqlite.test.ts",
"src/__tests__/work-product-run-launch-manifest.test.ts",
"src/__tests__/work-product-schemas.test.ts",
"src/__tests__/workflow-definition-repository.test.ts",
"src/__tests__/workflow-run-service.test.ts",
"src/__tests__/workflow-service.test.ts",
"src/__tests__/workflow-step-executor-codex.test.ts",
"src/__tests__/workflow-step-executor-openclaw.test.ts",
"src/storage/sqlite/database.test.ts",

View file

@ -0,0 +1,146 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdir, mkdtemp, readFile, rm, symlink, truncate, writeFile } from 'node:fs/promises';
import path from 'node:path';
import type { WorkflowACL, WorkflowAuditEvent, WorkflowDefinition } from '../types/workflow.js';
import { FileWorkflowDefinitionRepository } from '../storage/workflow-definition-repository.js';
function workflow(id: string): WorkflowDefinition {
return {
id,
name: `Workflow ${id}`,
version: 1,
agents: [{ id: 'agent-1', name: 'Agent One', role: 'developer' }],
steps: [{ id: 'step-1', type: 'agent', agent: 'agent-1', input: 'Do the work.' }],
};
}
function acl(workflowId: string): WorkflowACL {
return {
workflowId,
owner: 'brad',
editors: ['brad'],
viewers: [],
executors: [],
isPublic: false,
};
}
describe('FileWorkflowDefinitionRepository', () => {
let root: string;
let workflowsDir: string;
let repository: FileWorkflowDefinitionRepository;
beforeEach(async () => {
root = await mkdtemp(path.join(process.cwd(), '.veritas-workflow-repository-'));
workflowsDir = path.join(root, 'workflows');
repository = new FileWorkflowDefinitionRepository(workflowsDir);
});
afterEach(async () => {
await rm(root, { recursive: true, force: true });
});
it('atomically persists, lists, and deletes workflow definitions', async () => {
await expect(repository.get('missing')).resolves.toBeNull();
await expect(repository.getAcl('missing')).resolves.toBeNull();
await repository.save(workflow('alpha'));
await expect(repository.get('alpha')).resolves.toMatchObject({ id: 'alpha' });
await expect(repository.count()).resolves.toBe(1);
await expect(repository.listMetadata()).resolves.toEqual([
{ id: 'alpha', name: 'Workflow alpha', version: 1, description: '' },
]);
await expect(repository.delete('alpha')).resolves.toBe(true);
await expect(repository.delete('alpha')).resolves.toBe(false);
});
it('loads both supported YAML extensions and skips invalid metadata', async () => {
await mkdir(workflowsDir, { recursive: true });
await writeFile(
path.join(workflowsDir, 'legacy.yaml'),
[
'id: legacy',
'name: Legacy Workflow',
'version: 1',
'agents:',
' - id: agent-1',
' name: Agent One',
'steps:',
' - id: step-1',
' type: agent',
' agent: agent-1',
].join('\n'),
'utf8'
);
await writeFile(path.join(workflowsDir, 'broken.yml'), ': invalid', 'utf8');
await expect(repository.listMetadata()).resolves.toEqual([
{ id: 'legacy', name: 'Legacy Workflow', version: 1, description: '' },
]);
});
it('serializes concurrent ACL updates and appends audit events', async () => {
await Promise.all([repository.saveAcl(acl('alpha')), repository.saveAcl(acl('beta'))]);
await expect(repository.getAcl('alpha')).resolves.toMatchObject({ workflowId: 'alpha' });
await expect(repository.getAcl('beta')).resolves.toMatchObject({ workflowId: 'beta' });
const event: WorkflowAuditEvent = {
timestamp: '2026-08-23T21:00:00.000Z',
userId: 'brad',
action: 'edit',
workflowId: 'alpha',
workflowVersion: 1,
};
await repository.appendAuditEvent(event);
await expect(readFile(path.join(workflowsDir, '.audit.jsonl'), 'utf8')).resolves.toBe(
`${JSON.stringify(event)}\n`
);
});
it('rejects symbolic-link files and parent directories', async () => {
await mkdir(workflowsDir, { recursive: true });
const target = path.join(root, 'outside.yml');
await writeFile(target, 'id: linked', 'utf8');
await symlink(target, path.join(workflowsDir, 'linked.yml'));
await expect(repository.get('linked')).rejects.toThrow(/symbolic link/i);
const realDirectory = path.join(root, 'real-workflows');
const linkedDirectory = path.join(root, 'linked-workflows');
await mkdir(realDirectory);
await symlink(realDirectory, linkedDirectory, 'dir');
const linkedRepository = new FileWorkflowDefinitionRepository(linkedDirectory);
await expect(linkedRepository.save(workflow('unsafe'))).rejects.toThrow(/regular directory/i);
});
it('rejects non-file, oversized, and linked persistence targets', async () => {
await mkdir(workflowsDir, { recursive: true });
const invalidWorkflowPath = path.join(workflowsDir, 'invalid.yml');
await mkdir(invalidWorkflowPath);
await expect(repository.get('invalid')).rejects.toThrow(/bounded regular file/i);
await expect(repository.delete('invalid')).rejects.toThrow();
await rm(invalidWorkflowPath, { recursive: true });
await expect(
repository.save({ ...workflow('oversized'), description: 'x'.repeat(2 * 1024 * 1024) })
).rejects.toThrow(/storage limit/i);
const auditPath = path.join(workflowsDir, '.audit.jsonl');
await writeFile(auditPath, '', 'utf8');
await truncate(auditPath, 64 * 1024 * 1024);
const event: WorkflowAuditEvent = {
timestamp: '2026-08-23T21:00:00.000Z',
userId: 'brad',
action: 'edit',
workflowId: 'alpha',
workflowVersion: 1,
};
await expect(repository.appendAuditEvent(event)).rejects.toThrow(/bounded regular file/i);
await rm(auditPath);
const auditTarget = path.join(root, 'outside-audit.jsonl');
await writeFile(auditTarget, '', 'utf8');
await symlink(auditTarget, auditPath);
await expect(repository.appendAuditEvent(event)).rejects.toThrow(/symbolic link/i);
});
});

View file

@ -1,6 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import { WorkflowService } from '../services/workflow-service.js';
@ -29,7 +28,7 @@ describe('WorkflowService', () => {
};
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'workflow-service-'));
tmpDir = await fs.mkdtemp(path.join(process.cwd(), '.veritas-workflow-service-'));
service = new WorkflowService(tmpDir);
originalCodexEnv = {
VERITAS_CODEX_EXECUTABLE: process.env.VERITAS_CODEX_EXECUTABLE,

View file

@ -3,9 +3,6 @@
* Phase 1: Core Engine
*/
import fs from 'fs/promises';
import path from 'path';
import yaml from 'yaml';
import { PHASE_NAMES, type PhaseName } from '@veritas-kanban/shared';
import type { WorkflowDefinition, WorkflowACL, WorkflowAuditEvent } from '../types/workflow.js';
import { ValidationError } from '../types/workflow.js';
@ -13,6 +10,7 @@ import { getWorkflowsDir } from '../utils/paths.js';
import { createLogger } from '../lib/logger.js';
import { SqliteDatabase, type SqliteConnectionOptions } from '../storage/sqlite/database.js';
import { SqliteWorkflowDefinitionRepository } from '../storage/sqlite/workflow-repositories.js';
import { FileWorkflowDefinitionRepository } from '../storage/workflow-definition-repository.js';
import { evaluateCodexCommandPolicy, isCodexWorkflowAgent } from '../utils/codex-command-policy.js';
const log = createLogger('workflow-service');
@ -28,16 +26,15 @@ const MAX_TOOLS_PER_AGENT = 50;
const MAX_RETRY_DELAY_MS = 300000; // 5 minutes max delay
export class WorkflowService {
private workflowsDir: string;
private cache: Map<string, WorkflowDefinition> = new Map();
private readonly fileRepository: FileWorkflowDefinitionRepository | null = null;
private readonly repository: SqliteWorkflowDefinitionRepository | null = null;
private readonly sqliteDatabase: SqliteDatabase | null = null;
private readonly ownsSqliteDatabase: boolean = false;
private readonly directoriesReady: Promise<void>;
constructor(options: string | WorkflowServiceOptions = {}) {
const resolvedOptions = typeof options === 'string' ? { workflowsDir: options } : options;
this.workflowsDir = resolvedOptions.workflowsDir || getWorkflowsDir();
const workflowsDir = resolvedOptions.workflowsDir || getWorkflowsDir();
const storageType =
resolvedOptions.storageType ?? (process.env.VERITAS_STORAGE === 'sqlite' ? 'sqlite' : 'file');
@ -51,16 +48,10 @@ export class WorkflowService {
}
if (!this.repository) {
this.directoriesReady = this.ensureDirectories();
} else {
this.directoriesReady = Promise.resolve();
this.fileRepository = new FileWorkflowDefinitionRepository(workflowsDir);
}
}
private async ensureDirectories(): Promise<void> {
await fs.mkdir(this.workflowsDir, { recursive: true });
}
private normalizeWorkflowId(id: string): string {
const trimmed = (id ?? '').trim();
if (!trimmed) {
@ -101,13 +92,12 @@ export class WorkflowService {
return workflow;
}
await this.directoriesReady;
const filePath = path.join(this.workflowsDir, `${normalizedId}.yml`);
try {
const content = await fs.readFile(filePath, 'utf-8');
const workflow = yaml.parse(content) as WorkflowDefinition;
const workflow = await this.getFileRepository().get(normalizedId);
if (!workflow) {
log.debug({ workflowId: normalizedId }, 'Workflow not found');
return null;
}
// Validate schema
this.validateWorkflow(workflow);
@ -118,10 +108,6 @@ export class WorkflowService {
log.info({ workflowId: normalizedId, version: workflow.version }, 'Workflow loaded');
return workflow;
} catch (err: unknown) {
if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {
log.debug({ workflowId: normalizedId }, 'Workflow not found');
return null;
}
log.error({ workflowId: normalizedId, err }, 'Failed to load workflow');
const message = err instanceof Error ? err.message : 'Unknown error';
throw new ValidationError(`Invalid workflow YAML: ${message}`);
@ -141,19 +127,10 @@ export class WorkflowService {
return workflows;
}
await this.directoriesReady;
const files = await fs.readdir(this.workflowsDir).catch(() => []);
const workflows: WorkflowDefinition[] = [];
for (const file of files) {
if (!file.endsWith('.yml') && !file.endsWith('.yaml')) continue;
const id = file.replace(/\.(yml|yaml)$/, '');
const workflow = await this.loadWorkflow(id);
if (workflow) {
workflows.push(workflow);
}
const workflows = await this.getFileRepository().list();
for (const workflow of workflows) {
this.validateWorkflow(workflow);
this.cache.set(workflow.id, workflow);
}
log.info({ count: workflows.length }, 'Listed workflows');
@ -173,32 +150,7 @@ export class WorkflowService {
return metadata;
}
await this.directoriesReady;
const files = await fs.readdir(this.workflowsDir).catch(() => []);
const metadata: Array<Pick<WorkflowDefinition, 'id' | 'name' | 'version' | 'description'>> = [];
for (const file of files) {
if (!file.endsWith('.yml') && !file.endsWith('.yaml')) continue;
const id = file.replace(/\.(yml|yaml)$/, '');
const filePath = path.join(this.workflowsDir, file);
try {
const content = await fs.readFile(filePath, 'utf-8');
const workflow = yaml.parse(content) as WorkflowDefinition;
metadata.push({
id: workflow.id,
name: workflow.name,
version: workflow.version,
description: workflow.description,
});
} catch (err: unknown) {
log.warn({ workflowId: id, err }, 'Failed to read workflow metadata');
continue;
}
}
const metadata = await this.getFileRepository().listMetadata();
log.info({ count: metadata.length }, 'Listed workflow metadata');
return metadata;
@ -225,28 +177,16 @@ export class WorkflowService {
return;
}
await this.directoriesReady;
const filePath = path.join(this.workflowsDir, `${normalizedId}.yml`);
// Check if this is a new workflow (not an update)
try {
await fs.access(filePath);
// File exists, this is an update
} catch {
// New workflow - check count limit
const files = await fs.readdir(this.workflowsDir).catch(() => []);
const workflowCount = files.filter((f) => f.endsWith('.yml') || f.endsWith('.yaml')).length;
if (workflowCount >= MAX_WORKFLOWS) {
throw new ValidationError(
`Maximum workflow limit (${MAX_WORKFLOWS}) reached. Delete unused workflows before creating new ones.`
);
}
const fileRepository = this.getFileRepository();
if (
!(await fileRepository.get(normalizedId)) &&
(await fileRepository.count()) >= MAX_WORKFLOWS
) {
throw new ValidationError(
`Maximum workflow limit (${MAX_WORKFLOWS}) reached. Delete unused workflows before creating new ones.`
);
}
const content = yaml.stringify(workflow);
await fs.writeFile(filePath, content, 'utf-8');
await fileRepository.save(workflow);
// Update cache
this.cache.set(normalizedId, workflow);
@ -266,10 +206,7 @@ export class WorkflowService {
return;
}
await this.directoriesReady;
const filePath = path.join(this.workflowsDir, `${normalizedId}.yml`);
await fs.unlink(filePath);
await this.getFileRepository().delete(normalizedId);
this.cache.delete(normalizedId);
log.info({ workflowId: normalizedId }, 'Workflow deleted');
@ -411,18 +348,7 @@ export class WorkflowService {
return this.repository.getAcl(workflowId);
}
await this.directoriesReady;
const aclPath = path.join(this.workflowsDir, '.acl.json');
try {
const content = await fs.readFile(aclPath, 'utf-8');
const acls = JSON.parse(content) as Record<string, WorkflowACL>;
return acls[workflowId] || null;
} catch (err: unknown) {
if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') return null;
throw err;
}
return this.getFileRepository().getAcl(workflowId);
}
/**
@ -435,22 +361,7 @@ export class WorkflowService {
return;
}
await this.directoriesReady;
const aclPath = path.join(this.workflowsDir, '.acl.json');
let acls: Record<string, WorkflowACL> = {};
try {
const content = await fs.readFile(aclPath, 'utf-8');
acls = JSON.parse(content);
} catch (err: unknown) {
if (!(err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT')) throw err;
}
acls[acl.workflowId] = acl;
await fs.writeFile(aclPath, JSON.stringify(acls, null, 2), 'utf-8');
await this.getFileRepository().saveAcl(acl);
log.info({ workflowId: acl.workflowId }, 'Workflow ACL saved');
}
@ -465,11 +376,7 @@ export class WorkflowService {
return;
}
await this.directoriesReady;
const auditPath = path.join(this.workflowsDir, '.audit.jsonl');
const line = JSON.stringify(event) + '\n';
await fs.appendFile(auditPath, line, 'utf-8');
await this.getFileRepository().appendAuditEvent(event);
log.info({ event }, 'Workflow audit event logged');
}
@ -481,6 +388,13 @@ export class WorkflowService {
this.cache.clear();
}
private getFileRepository(): FileWorkflowDefinitionRepository {
if (!this.fileRepository) {
throw new Error('File workflow repository is not configured');
}
return this.fileRepository;
}
dispose(): void {
if (this.ownsSqliteDatabase) {
this.sqliteDatabase?.close();

View file

@ -42,6 +42,7 @@ export { LocalWorkspaceFileRepository } from './workspace-file-repository.js';
export { FileProgressRepository, type ProgressRepository } from './progress-repository.js';
export { FileStatusHistoryStore } from './status-history-repository.js';
export { FileScheduledDeliverablesStore } from './scheduled-deliverables-repository.js';
export { FileWorkflowDefinitionRepository } from './workflow-definition-repository.js';
export {
FileWorkspaceExecutionTrustRepository,
InMemoryWorkspaceExecutionTrustRepository,

View file

@ -0,0 +1,214 @@
import { constants } from 'node:fs';
import { lstat, mkdir, open, readdir, unlink } from 'node:fs/promises';
import path from 'node:path';
import yaml from 'yaml';
import { withFileLock } from '../services/file-lock.js';
import type { WorkflowACL, WorkflowAuditEvent, WorkflowDefinition } from '../types/workflow.js';
import { ensureWithinBase, validatePathSegment } from '../utils/sanitize.js';
import { atomicWriteFile } from './fs-helpers.js';
const MAX_WORKFLOW_BYTES = 2 * 1024 * 1024;
const MAX_ACL_BYTES = 2 * 1024 * 1024;
const MAX_AUDIT_BYTES = 64 * 1024 * 1024;
type WorkflowMetadata = Pick<WorkflowDefinition, 'id' | 'name' | 'version' | 'description'>;
export class FileWorkflowDefinitionRepository {
private readonly workflowsDir: string;
constructor(workflowsDir: string) {
this.workflowsDir = path.resolve(workflowsDir);
ensureWithinBase(path.dirname(this.workflowsDir), this.workflowsDir);
}
async count(): Promise<number> {
return (await this.listDefinitionFiles()).length;
}
async get(id: string): Promise<WorkflowDefinition | null> {
const content = await this.readOptionalBoundedFile(
this.getWorkflowPath(id),
MAX_WORKFLOW_BYTES
);
return content === null ? null : (yaml.parse(content) as WorkflowDefinition);
}
async list(): Promise<WorkflowDefinition[]> {
const workflows: WorkflowDefinition[] = [];
for (const file of await this.listDefinitionFiles()) {
const content = await this.readBoundedFile(
ensureWithinBase(this.workflowsDir, path.join(this.workflowsDir, file)),
MAX_WORKFLOW_BYTES
);
workflows.push(yaml.parse(content) as WorkflowDefinition);
}
return workflows;
}
async listMetadata(): Promise<WorkflowMetadata[]> {
const metadata: WorkflowMetadata[] = [];
for (const file of await this.listDefinitionFiles()) {
try {
const content = await this.readBoundedFile(
ensureWithinBase(this.workflowsDir, path.join(this.workflowsDir, file)),
MAX_WORKFLOW_BYTES
);
const workflow = yaml.parse(content) as Partial<WorkflowDefinition> | null;
if (
!workflow ||
typeof workflow.id !== 'string' ||
typeof workflow.name !== 'string' ||
typeof workflow.version !== 'number'
) {
continue;
}
metadata.push({
id: workflow.id,
name: workflow.name,
version: workflow.version,
description: workflow.description ?? '',
});
} catch {
// Preserve the legacy metadata behavior by skipping unreadable definitions.
}
}
return metadata;
}
async save(workflow: WorkflowDefinition): Promise<void> {
const workflowPath = this.getWorkflowPath(workflow.id);
const content = yaml.stringify(workflow);
this.assertBounded(content, MAX_WORKFLOW_BYTES, 'Workflow definition');
await this.prepareDirectory();
await withFileLock(workflowPath, () => atomicWriteFile(workflowPath, content, 'utf8'));
}
async delete(id: string): Promise<boolean> {
const workflowPath = this.getWorkflowPath(id);
await this.prepareDirectory();
return withFileLock(workflowPath, async () => {
try {
await unlink(workflowPath);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
throw error;
}
});
}
async getAcl(workflowId: string): Promise<WorkflowACL | null> {
const content = await this.readOptionalBoundedFile(this.getAclPath(), MAX_ACL_BYTES);
if (content === null) return null;
const acls = JSON.parse(content) as Record<string, WorkflowACL>;
return acls[workflowId] ?? null;
}
async saveAcl(acl: WorkflowACL): Promise<void> {
const aclPath = this.getAclPath();
await this.prepareDirectory();
await withFileLock(aclPath, async () => {
const current = await this.readOptionalBoundedFile(aclPath, MAX_ACL_BYTES);
const acls = current === null ? {} : (JSON.parse(current) as Record<string, WorkflowACL>);
acls[acl.workflowId] = acl;
const content = JSON.stringify(acls, null, 2);
this.assertBounded(content, MAX_ACL_BYTES, 'Workflow ACL state');
await atomicWriteFile(aclPath, content, 'utf8');
});
}
async appendAuditEvent(event: WorkflowAuditEvent): Promise<void> {
const auditPath = this.getAuditPath();
const line = `${JSON.stringify(event)}\n`;
this.assertBounded(line, MAX_AUDIT_BYTES, 'Workflow audit event');
await this.prepareDirectory();
await withFileLock(auditPath, async () => {
let handle: Awaited<ReturnType<typeof open>> | undefined;
try {
handle = await open(
auditPath,
constants.O_APPEND | constants.O_CREAT | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0),
0o600
);
const stats = await handle.stat();
if (!stats.isFile() || stats.size + Buffer.byteLength(line, 'utf8') > MAX_AUDIT_BYTES) {
throw new Error('Workflow audit log must use a bounded regular file');
}
await handle.writeFile(line, { encoding: 'utf8' });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ELOOP') {
throw new Error('Workflow audit log must not be a symbolic link', { cause: error });
}
throw error;
} finally {
await handle?.close();
}
});
}
private getWorkflowPath(id: string): string {
const safeId = validatePathSegment(id);
return ensureWithinBase(this.workflowsDir, path.join(this.workflowsDir, `${safeId}.yml`));
}
private getAclPath(): string {
return ensureWithinBase(this.workflowsDir, path.join(this.workflowsDir, '.acl.json'));
}
private getAuditPath(): string {
return ensureWithinBase(this.workflowsDir, path.join(this.workflowsDir, '.audit.jsonl'));
}
private async listDefinitionFiles(): Promise<string[]> {
await this.prepareDirectory();
const entries = await readdir(this.workflowsDir);
return entries.filter((entry) => entry.endsWith('.yml') || entry.endsWith('.yaml'));
}
private async prepareDirectory(): Promise<void> {
await mkdir(this.workflowsDir, { recursive: true, mode: 0o700 });
const stats = await lstat(this.workflowsDir);
if (!stats.isDirectory() || stats.isSymbolicLink()) {
throw new Error('Workflow storage path must use a regular directory');
}
}
private async readBoundedFile(filePath: string, maximumBytes: number): Promise<string> {
let handle: Awaited<ReturnType<typeof open>> | undefined;
try {
handle = await open(filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
const stats = await handle.stat();
if (!stats.isFile() || stats.size > maximumBytes) {
throw new Error('Workflow storage must use a bounded regular file');
}
return await handle.readFile({ encoding: 'utf8' });
} catch (error) {
const errorCode = (error as NodeJS.ErrnoException).code;
if (errorCode === 'ELOOP') {
throw new Error('Workflow storage must not use symbolic links', { cause: error });
}
throw error;
} finally {
await handle?.close();
}
}
private async readOptionalBoundedFile(
filePath: string,
maximumBytes: number
): Promise<string | null> {
try {
return await this.readBoundedFile(filePath, maximumBytes);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
throw error;
}
}
private assertBounded(content: string, maximumBytes: number, label: string): void {
if (Buffer.byteLength(content, 'utf8') > maximumBytes) {
throw new Error(`${label} exceeds the ${maximumBytes}-byte storage limit`);
}
}
}