Detailed implementation plan derived from auto-claude ideation analysis (2026-01-31). 35 items across 6 categories, organized into prioritized execution phases.
| Phase | Focus | Items | Priority |
|---|---|---|---|
| Phase 1 | Security hardening | sec-002, sec-001, sec-003, sec-004, sec-005 | Critical/High |
| Phase 2 | Performance + code quality | perf-002, perf-004, perf-001, cq-001, cq-003 | High |
| Phase 3 | Feature wiring + schedulers | ci-001, ci-003, ci-004, ci-005, ci-002 | Medium |
| Phase 4 | Rendering perf + UI/UX | perf-003, uiux-006, uiux-008, uiux-009, uiux-007 | Medium |
| Phase 5 | Architecture cleanup | cq-002, cq-004, perf-005, uiux-010 | Medium |
| Phase 6 | Test coverage + docs | cq-005, doc-001 through doc-005 | Medium/Low |
Problem: GET /intake/:intakeId/submissions/:submissionId (src/app.ts:340-354) returns resumeToken in the response body with zero authentication. The resume token is the sole authorization mechanism for all write operations (setFields, submit, approve, reject). An attacker who knows a submission ID can retrieve the token and gain full write access. Tokens also leak in event payloads via GET /submissions/:id/events.
Affected files:
src/app.ts(lines 340-354)src/core/submission-manager.ts(event payload construction)src/routes/hono-events.ts
Implementation:
- Create a
SubmissionViewDTO that excludesresumeToken— use it in all GET responses - Only return
resumeTokenin POST create and POST handoff responses (to the creator/initiator) - Redact resume tokens from event payloads before returning via the events API — store internally but strip from query responses
- As defense-in-depth, require resume token as a query parameter for GET submission so only token holders can read
- Add integration tests verifying resume tokens never leak in read-only responses
Vulnerability: CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor)
Problem: WebhookManager (src/core/webhook-manager.ts:226) calls fetch() against user-supplied destination URLs with zero SSRF protection. IntakeRegistry validates URL syntax but doesn't block private IP ranges (127.0.0.1, 10.x, 192.168.x, 169.254.169.254 AWS IMDS), internal hostnames, or dangerous schemes (file://, gopher://). Destination headers are spread directly into fetch, allowing header injection. Retry logic amplifies attacks.
Affected files:
src/core/webhook-manager.tssrc/core/intake-registry.ts
Implementation:
- Add URL validation layer in
IntakeRegistry.validateDestination()— resolve hostname to IP, block RFC 1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local (169.254.0.0/16), loopback (127.0.0.0/8), and IPv6 equivalents - Restrict allowed schemes to
https://only (or configurable allowlist) - In
WebhookManager, re-validate destination URL at delivery time to prevent DNS rebinding - Sanitize
destination.headers— block overwriting Content-Type, Host, Authorization, and X-FormBridge-* headers via allowlist - Add configurable URL allowlist/blocklist for webhook destinations
Vulnerability: CWE-918 (SSRF)
Problem: No security response headers are set (no CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy). Missing Referrer-Policy means resume tokens in URL query parameters leak via the Referer header. POST/PATCH endpoints have no body size limit — a multi-GB JSON payload can exhaust memory. Demo Vite binds to 0.0.0.0.
Affected files:
src/app.tssrc/middleware/cors.tspackages/demo/vite.config.ts
Implementation:
- Add
app.use('*', secureHeaders())fromhono/secure-headers— sets X-Content-Type-Options, X-Frame-Options, CSP, HSTS, Referrer-Policy - Add
app.use('*', bodyLimit({ maxSize: 1024 * 1024 }))fromhono/body-limit— 1MB default with route-specific overrides for uploads - Change demo
vite.config.tshost from0.0.0.0tolocalhost - Environment-aware header presets (strict for production, relaxed for dev)
Vulnerability: CWE-693 (Protection Mechanism Failure), CWE-770 (Resource Allocation Without Limits)
Problem: HTTP routes store user-supplied fields via SubmissionManager.setFields() without validating against the intake's JSON Schema. The AJV Validator class exists at src/core/validator.ts but is never invoked from HTTP handlers. MCP path validates with Zod, creating inconsistency. The internal __uploads field name can be overwritten via fields: {"__uploads": {}}. Actor validation on HTTP routes only checks truthy kind/id with no type/length enforcement.
Affected files:
src/app.ts(POST lines 209-299, PATCH lines 357-446)src/core/submission-manager.tssrc/core/validator.ts
Implementation:
- Wire
Validatorinto HTTP route handlers — callvalidator.validate(intakeSchema, body.fields)in both POST and PATCH beforesetFields(), return 400 with errors on failure - Add reserved field name blocklist in
setFields()— reject__proto__,constructor,prototype,__uploads - Standardize actor validation using the Zod
actorSchemafromhono-submissions.tswith.strict()and.max(255) - Add max field value size limits (64KB per field, 1MB per submission)
- Add tests verifying HTTP API rejects invalid data matching MCP rules
Vulnerability: CWE-20 (Improper Input Validation)
Problem: .gitignore has no exclusion for .env files. Webhook signing secret defaults to undefined — webhooks are unsigned with no startup warning. Manual XOR comparison in webhook-manager.ts (lines 88-93) instead of crypto.timingSafeEqual() (which IS used correctly in local-storage.ts).
Affected files:
.gitignoresrc/app.tssrc/core/webhook-manager.tspackage.json
Implementation:
- Add
.env,.env.*,.env.local,.env.production,.env.*.localto.gitignore - Create
.env.exampledocumenting expected variables - Emit startup warning when webhook signing secret is not configured
- Replace manual XOR loop in
verifySignature()withcrypto.timingSafeEqual() - Consider adding pre-commit hook via husky + gitleaks
Vulnerability: CWE-540 (Sensitive Information in Source Code)
Problem: Every mutating operation calls store.save() explicitly, then calls recordEvent() which also calls store.save() — 2x write amplification. recordEvent() runs emitter, eventStore, and save sequentially when emit and appendEvent are independent. BridgingEventEmitter.emit() uses Promise.all() blocking on the slowest listener (webhook delivery blocks API response). POST /submissions with initial fields produces 4 saves and 2 triple-write cycles.
Current metric: 2x writes per operation; 8+ async ops for create-with-fields
Affected files:
src/core/submission-manager.ts(recordEvent at line 136; save calls at lines 204, 318, 436, 546, 656, 701)src/app.ts(BridgingEventEmitter.emit lines 76-86)
Implementation:
- Remove explicit
store.save()calls beforerecordEvent()— recordEvent already saves - Parallelize emit and appendEvent in
recordEvent():await Promise.all([emit, appendEvent]); await store.save() - Use
Promise.allSettled()inBridgingEventEmitter.emit()for error isolation - Separate listeners into sync (must complete before response) and async (fire-and-forget) so webhook delivery doesn't block API responses
- Support initial fields in
createSubmission()natively to avoid separatesetFieldsroundtrip
Expected improvement: 50% reduction in save calls; ~40% latency reduction in recordEvent; 30-50% faster mutation endpoints
Problem: InMemorySubmissionStore in src/app.ts does O(n) linear scan for getByResumeToken(). MCP store does O(n) for getByIdempotencyKey(). memory-storage.ts wastes O(T) scanning all tokens on every save.
Current metric: O(n) per resumeToken lookup (critical handoff path)
Affected files:
src/app.ts(InMemorySubmissionStore.getByResumeToken lines 53-59)src/mcp/submission-store.tssrc/storage/memory-storage.ts
Implementation:
- Add
resumeTokenIndex: Map<string, string>to InMemorySubmissionStore, maintained on save() - Add
idempotencyKeyIndex: Map<string, string>to MCP store - Replace O(T) stale token scan in memory-storage.ts with reverse index:
submissionTokens: Map<string, string> - Consider consolidating three store implementations to prevent future drift
Expected improvement: All lookups drop to O(1). Handoff latency becomes constant.
Problem: AnalyticsDataProvider in src/app.ts (lines 162-192) performs 4+ separate full-table scans on every /analytics/summary request. getRecentEvents() collects ALL events from ALL submissions, sorts the entire array, then takes only 20 items.
Current metric: O(4n + nmlog(n*m)) per /analytics/summary
Affected files:
src/app.ts(analyticsProvider lines 162-192)src/core/event-store.ts
Implementation:
- Add incremental counters to InMemorySubmissionStore:
totalCount,stateCountMap<string,number>,pendingApprovalCount— updated insave()by diffing old vs new state - Maintain bounded max-heap or sorted deque of last N events in EventStore, updated on
appendEvent() - Add type-indexed
Map<eventType, IntakeEvent[]>to EventStore forgetEventsByTypeO(1) lookup - Wire AnalyticsDataProvider to read pre-computed structures
- Periodic reconciliation to prevent counter drift
Expected improvement: O(1) reads for counters; O(k) for recent events. ~100x faster for 1000+ submissions.
Problem: src/core/condition-evaluator.ts (306 lines) and packages/form-renderer/src/core/condition-evaluator.ts (305 lines) are near-identical copies. Same for step-validator.ts (147 vs 146 lines). Any bug fix must be applied in two places.
Affected files:
src/core/condition-evaluator.tspackages/form-renderer/src/core/condition-evaluator.tssrc/core/step-validator.tspackages/form-renderer/src/core/step-validator.ts
Implementation:
- Create
packages/shared(orpackages/core-logic) as a new workspace package - Move condition-evaluator and step-validator as zero-dependency isomorphic modules
- Both root
src/andpackages/form-rendererimport from@formbridge/shared - Consolidate the 5 independent nested-value accessor implementations (getFieldValue in both evaluators, schemaParser.ts, FormBridgeForm.tsx getNestedValue/setNestedValue) into a single utility in the shared package
- Ensure existing tests pass against the unified module
Metrics: 451 duplicate lines eliminated
Problem: SubmissionNotFoundError and InvalidResumeTokenError are defined identically in both submission-manager.ts and approval-manager.ts (instanceof checks can fail unexpectedly). actorSchema + parseActor() are copy-pasted between hono-submissions.ts and hono-approvals.ts (20 identical lines each).
Affected files:
src/core/submission-manager.ts,src/core/approval-manager.tssrc/routes/hono-submissions.ts,src/routes/hono-approvals.ts
Implementation:
- Extract shared errors to
src/core/errors.ts— import from both managers - Extract
actorSchema+parseActortosrc/routes/shared/actor-validation.ts - Extract shared interfaces (SubmissionStore, EventEmitter) to
src/core/interfaces.ts - Verify instanceof checks still work after consolidation
Metrics: ~85 duplicate lines eliminated
Problem: The WebhookNotifier interface is defined, ApprovalManager accepts it optionally, and notifyReviewers() builds the full payload — but silently returns when no notifier configured. A MockWebhookNotifier in tests confirms the expected contract. Only the production implementation is missing.
Affected files:
src/core/approval-manager.tssrc/app.tssrc/core/webhook-manager.ts
Implementation:
- Create
WebhookNotifierImplclass implementingWebhookNotifierby wrappingWebhookManager notifyReviewers()formatsReviewerNotificationinto webhook payload, callswebhookManager.deliver()for each configured reviewer URL- Wire in
app.ts— pass new instance as 3rd arg toApprovalManagerconstructor - Gets HMAC signing, exponential backoff retry, and queue tracking from existing WebhookManager infrastructure
Effort: Small — pure wiring between existing subsystems
Problem: Submissions have ttlMs and expiresAt fields, and the state machine supports expired as a terminal state, but expiry is only checked reactively when setFields or confirmUpload is called. Submissions can sit expired indefinitely.
Affected files:
src/core/submission-manager.tssrc/app.ts
Implementation:
- Add
expireStaleSubmissions()to SubmissionManager — iterate store, filter non-terminal withexpiresAt < now, transition each toexpiredvia triple-write - Create
ExpiryScheduler(or methods on SubmissionManager) usingsetIntervalat configurable interval (default 60s) - Wire in
app.tsduringcreateFormBridgeAppWithIntakes() - Include
stop()for graceful shutdown in tests - Follow the pattern of
EventStore.cleanupOld()
Problem: DeliveryQueue defines getPendingRetries(), calculateRetryDelay() exists with exponential backoff, WebhookManager.processDelivery() handles individual attempts — but nothing connects them for periodic retry.
Affected files:
src/core/webhook-manager.tssrc/core/delivery-queue.tssrc/app.ts
Implementation:
- Add
startRetryScheduler(intervalMs?)andstopRetryScheduler()to WebhookManager - Scheduler uses
setInterval→queue.getPendingRetries()→ filter by elapsed delay →processDelivery()each - Default 30s interval, configurable via
WebhookManagerOptions - Wire in
app.tsafter WebhookManager creation
Problem: Analytics only computes aggregate metrics across all intakes. All per-intake data is already stored on submissions (intakeId, state, createdAt, updatedAt).
Affected files:
src/routes/hono-analytics.tssrc/app.tspackages/admin-dashboard/src/api/client.ts
Implementation:
- Add
getSubmissionsByIntake()andgetCompletionRates()toAnalyticsDataProvider - Implement by grouping
store.getAll()byintakeId - Add
GET /analytics/intakes→[{ intakeId, total, byState, completionRate }] - Add
GET /analytics/funnel→ state-transition funnel data - Add matching methods to admin dashboard API client
Problem: DataTable declares sortable?: boolean on ColumnDef but ignores it in render logic. @tanstack/react-table is a dependency but unused.
Affected files:
packages/admin-dashboard/src/components/DataTable.ts
Implementation:
- Add
useState<{column: string, direction: 'asc'|'desc'} | null>for sort state - Make
<th>clickable for columns withsortable=true, toggle sort on click - Sort data array using active column's
accessorbefore rendering - Add CSS classes for visual indicators (arrow via
::afterpseudo-element)
Problem: validateField() validates the entire form schema against all data just to check a single field (called on every blur/change). handleFieldChange is recreated on every keystroke due to localFields dependency, causing entire form re-render. No field components use React.memo.
Current metric: O(total_fields) validation per blur; O(N) re-renders per keystroke
Affected files:
packages/form-renderer/src/utils/validation.ts(lines 248-272)packages/form-renderer/src/components/FormBridgeForm.tsxpackages/form-renderer/src/components/fields/*
Implementation:
- Use functional setState:
setLocalFields(prev => ...)to remove localFields from useCallback deps — stable callback reference - Wrap FieldWrapper, ArrayField, FileField with
React.memo - Extract sub-schemas per field at form init; compile per-field AJV validators
- Validate only target field's data in
validateField() - Assign stable
$idto schemas to eliminateJSON.stringifycache keys - Keep full-form
validateForm()for submit-time
Expected improvement: N-fold validation reduction; O(1) re-renders per keystroke; <1ms per field validation
Problem: ApprovalActions uses window.prompt() for rejection reasons (can't be styled, no textarea, not accessible, blocked by some browsers). Admin dashboard fires approve/reject immediately with no confirmation and hardcodes rejection reason as 'Rejected by admin'.
Affected files:
packages/form-renderer/src/components/ApprovalActions.tsxpackages/admin-dashboard/src/pages/SubmissionDetailPage.tspackages/admin-dashboard/src/main.ts
Implementation:
- Create
ConfirmationDialogcomponent for each package — inline modal overlay with focus trap, Escape-to-close, backdrop click-to-close,aria-modal/role='dialog' - Replace
prompt()calls with dialog containing<textarea>for reason/feedback - Add simple confirmation variant for approve: "Are you sure?"
- Admin reject dialog must include required reason textarea passed to
client.rejectSubmission() - Show success/error feedback before navigating away
Problem: Layout uses strict equality (currentPath === item.path) — child routes like /submissions/abc-123 don't highlight the parent "Submissions" nav item. aria-current='page' also fails.
Affected files:
packages/admin-dashboard/src/components/Layout.ts
Implementation:
- Replace strict equality with:
itemPath === '/' ? currentPath === '/' : (currentPath === itemPath || currentPath.startsWith(itemPath + '/')) - Optionally add
matchMode: 'exact' | 'prefix'toNavItem - Ensure
aria-current='page'uses same logic - Consider adding breadcrumb trail on detail pages
Problem: Clickable rows have onClick + cursor:pointer but lack tabIndex, keyboard handlers (onKeyDown for Enter/Space), and ARIA attributes. Keyboard-only users can't navigate. Pagination buttons lack descriptive aria-labels.
Affected files:
packages/admin-dashboard/src/components/DataTable.ts
Implementation:
- Add
tabIndex={0}to clickable<tr>elements - Add
onKeyDown: Enter/Space triggersonRowClick(row) - Add
aria-labelvia newrowLabelprop - Add
.fb-table__row--clickable:focus-visibleoutline style - Pagination buttons:
aria-label={Go to page ${N}}
Problem: No focus management after: ArrayField add/remove, WizardForm step nav, ResumeFormPage load/submit, FormBridgeForm validation errors. Keyboard and screen reader users lose position.
Affected files:
packages/form-renderer/src/components/fields/ArrayField.tsxpackages/form-renderer/src/components/WizardForm.tsxpackages/form-renderer/src/components/ResumeFormPage.tsxpackages/form-renderer/src/components/FormBridgeForm.tsx
Implementation:
- ArrayField: After add, focus new item's first input via ref + useEffect. After remove, focus previous item or Add button
- WizardForm: After step nav, focus first focusable element in new step via useEffect watching
state.currentStep - ResumeFormPage: After load, focus form heading. After submit success/error, focus result container (
tabIndex={-1}) - FormBridgeForm: After validation errors, focus first
[aria-invalid="true"]element - All focus ops use
requestAnimationFrame()for DOM timing. Respectprefers-reduced-motion
Problem: src/mcp/server.ts handles MCP lifecycle, tool registration, 6 tool handler types, submission state, token handling, and response formatting. Contains 14 nearly-identical error construction blocks (~168 duplicate lines).
Affected files:
src/mcp/server.tssrc/mcp/tool-generator.ts
Implementation:
- Extract tool handlers to
src/mcp/handlers/— one file per action:create-handler.ts,set-handler.ts,validate-handler.ts,submit-handler.ts,upload-handlers.ts - Extract common error construction to
src/mcp/response-builder.tswith factory functions - Extract common token-lookup-and-validate to shared helper
- Main
server.tsbecomes thin orchestrator (~250 lines)
Problem: 153 instances of as any in production code. approval-manager.ts uses (submission as any).reviewDecisions in 9 places. s3-storage.ts has any for AWS SDK client. validator.ts casts AJV params to any.
Affected files:
src/core/approval-manager.ts(9 instances)src/storage/s3-storage.ts(5 instances)src/core/validator.ts(4 instances)packages/form-renderer/src/api/client.ts
Implementation:
- Add
reviewDecisions?: ReviewDecision[]toSubmissiontype — eliminates 9 casts - Create typed interface for S3 client operations
- Type AJV
ErrorObjectparams properly - Add proper return type to
getSubmissionByResumeTokenin form-renderer client
Problem: No eviction policies, size limits, or TTL cleanup in any in-memory store. Memory grows monotonically. Events stored 3x simultaneously. appendEvent() sorts entire array O(n*log(n)) on every call despite events arriving in order.
Affected files:
src/app.ts(InMemorySubmissionStore)src/core/event-store.tssrc/core/delivery-queue.tssrc/types.ts(Submission.events)
Implementation:
- Periodic cleanup sweep in InMemorySubmissionStore — remove past
expiresAt - Configurable
maxEntrieswith LRU eviction for terminal-state submissions - Auto-purge succeeded deliveries after configurable retention (default 24h)
- Replace full sort in
appendEventwith check-and-insert (skip sort if in order) - Cap
submission.eventsto last N events (e.g., 50) - Wire
EventStore.cleanup()to periodic schedule - Add memory metric to
/healthendpoint
Problem: Step transitions are silent — no aria-live announcements, no "Step X of Y" text. Completed steps are non-interactive <span> elements despite goTo(stepId) existing in the hook.
Affected files:
packages/form-renderer/src/components/WizardForm.tsxpackages/form-renderer/src/components/StepIndicator.tsxpackages/form-renderer/src/hooks/useWizardNavigation.ts
Implementation:
- Add visually-hidden
aria-live='polite'div that announces "Step N of M: [Title]" on step change - Change completed step
<span>to<button>witharia-label='Go to step N: [Title] (completed)' - Wire
onGoToStepcallback through toactions.goTo() - Add visible "Step N of M" text for all users
Coverage gaps:
packages/admin-dashboard/— 14 source files, 0 tests (0% coverage)src/middleware/error-handler.ts— 279 lines, 0 testssrc/auth/middleware.ts— 232 lines, 0 testssrc/core/intake-registry.ts— 371 lines, no dedicated unit test
Implementation priority:
tests/middleware/error-handler.test.ts— test each error type → HTTP status mappingtests/auth/middleware.test.ts— test auth pipeline (API key, permissions, rate limiting)src/core/__tests__/intake-registry.test.ts— test registration, validation, lookuppackages/admin-dashboard/src/__tests__/client.test.ts— test API client with mocked fetch
Current state: docs-site API pages are skeleton stubs. 0/18 endpoints fully documented. No analytics page.
Scope: For each of 18+ endpoints: full request/response JSON, required/optional fields table, status codes, error format, curl example. Create analytics.md page. Document handoff and upload endpoints.
Current state: 25-line stub despite 30+ exports.
Scope: Expand into: overview/install, component props tables, hooks API (useFormState, useValidation, useFormSubmission, useResumeSubmission), WizardForm guide, ResumeFormPage integration, ReviewerView/ApprovalActions, and package README.
Current state: 41 lines, happy-path-only state diagram.
Scope: Full state machine Mermaid diagram with all states/transitions, triple-write pattern explanation, field attribution JSON examples, resume token lifecycle, approval gates, idempotency.
Missing for: form-renderer, admin-dashboard, create-formbridge, templates (4 of 6 packages).
Scope: 50-150 lines each following schema-normalizer's pattern: overview, install, quick start, API surface, links to docs-site.
Current state: ~17% JSDoc coverage. 11 interfaces and 13+ methods undocumented.
Scope: Add field descriptions to all interfaces, @param/@returns/@throws to all methods, @example for high-use methods.
Phase 1 (security) — no dependencies, can start immediately
sec-005 → independent
sec-002 → independent
sec-001 → independent
sec-003 → independent
sec-004 → independent
Phase 2 (perf + quality) — after Phase 1 security baseline
perf-002 → independent
perf-004 → independent
perf-001 → after perf-004 (uses same store, avoid conflicts)
cq-001 → independent (shared package creation)
cq-003 → independent
Phase 3 (features) — after Phase 2 core fixes
ci-001 → after cq-003 (uses consolidated error classes)
ci-003 → after perf-002 (depends on clean save pattern)
ci-004 → independent
ci-005 → after perf-001 (extends analytics provider)
ci-002 → independent
Phase 4 (UI) — after Phase 2 shared package
perf-003 → after cq-001 (uses shared validation utils)
uiux-006 → independent
uiux-008 → independent
uiux-009 → independent (can combine with ci-002)
uiux-007 → independent
Phase 5 (cleanup) — after Phases 2-3
cq-002 → after ci-001 (MCP handlers may change)
cq-004 → independent
perf-005 → after ci-003 (TTL scheduler interacts with eviction)
uiux-010 → after uiux-007 (builds on focus management)
Phase 6 (tests + docs) — ongoing, can run in parallel
cq-005 → after Phases 1-3 (test against final implementations)
doc-* → independent, can start anytime
After each phase:
npm run buildandnpm run build --workspacespassnpm run test:run— all tests pass, no regressionsnpm run lint— no new warnings- Manual smoke test: start backend (
npx tsx src/test-server.ts), verify affected endpoints - For UI changes: start demo/admin-dashboard dev servers, verify in browser