Complete visual configuration and secure authentication

This commit is contained in:
Victor Wiebe 2026-08-02 10:00:06 -04:00
parent 86ca42ad12
commit 26d0ef773a
62 changed files with 2775 additions and 1788 deletions

View File

@ -1,398 +0,0 @@
# ARCHITECTURE.md
# Conductor Architecture
## 1. Overview
Conductor is a web-based application builder for creating simple user interfaces backed by REST API endpoints.
The system consists of:
* React frontend
* Backend API service
* SQLite database
* Canonical JSON project definition
* Server-side REST API proxy
* External web server or reverse proxy
The MVP shall function without AI assistance.
---
## 2. Core Architectural Principle
The canonical source of truth for a Conductor application is a structured JSON project definition.
All editors and runtime views operate against this project definition.
This means:
* The Visual Editor modifies the JSON definition.
* The JSON Editor modifies the same JSON definition.
* Preview mode renders from the JSON definition.
* Saved projects persist the JSON definition.
* Future AI features will read and modify the JSON definition.
---
## 3. Recommended Stack
## Frontend
* React
* TypeScript
* Drag-and-drop canvas library
* Monaco Editor or similar JSON editor
* JSON schema validation
## Backend
* Node.js with Express/Fastify
**or**
* Python with FastAPI
Either is acceptable. Pick whichever is easiest for the team to support.
## Database
* SQLite for MVP
* PostgreSQL as a future migration target
## Web Server
Production deployments should run behind:
* NGINX
* Apache HTTP Server
* Caddy
* IBM-approved internal reverse proxy
The application should not implement its own production-grade web server.
---
## 4. High-Level Architecture
```text
Browser
|
|-- Visual Editor
|-- JSON Editor
|-- Preview Runtime
|
v
Backend API Service
|
|-- Project API
|-- Validation API
|-- REST Proxy API
|-- Secret Handling
|
v
SQLite Database
|
v
Stored Project Definitions
```
External API calls should flow through the backend proxy:
```text
Browser
|
v
Conductor Backend
|
v
External REST API / Concert / RIA Endpoint
```
---
## 5. Major Components
## 5.1 Visual Editor
The Visual Editor provides the drag-and-drop interface.
Responsibilities:
* Render canvas from project JSON
* Add components
* Move components
* Resize components
* Edit properties
* Configure events
* Configure bindings
* Update canonical project JSON
## 5.2 JSON Editor
The JSON Editor provides direct access to the canonical project definition.
Responsibilities:
* Display project JSON
* Validate against schema
* Show syntax errors
* Apply edits to project state
* Keep Visual Editor synchronized
## 5.3 Preview Runtime
Preview mode renders the project as an end user would experience it.
Responsibilities:
* Render components from JSON
* Execute configured events
* Call backend proxy for REST actions
* Apply response mappings
* Display success/error states
## 5.4 Backend API Service
Responsibilities:
* Save projects
* Load projects
* Validate project definitions
* Execute REST proxy requests
* Store non-secret metadata
* Manage secret references
* Provide basic execution logs
## 5.5 REST API Proxy
The backend shall proxy external REST API calls.
Reasons:
* Avoid exposing secrets in the browser
* Centralize authentication handling
* Support API keys and bearer tokens safely
* Normalize errors
* Capture sanitized execution logs
* Prepare for future endpoint allowlisting
## 5.6 Database Layer
SQLite stores:
* Project metadata
* Canonical project JSON
* Secret references
* Basic execution history
* Application settings
The backend should use a data access layer so SQLite can later be replaced with PostgreSQL.
---
## 6. Data Flow
## 6.1 Editing Flow
```text
User edits Visual Editor
-> Project JSON updated
-> Schema validation runs
-> UI rerenders
-> User saves project
-> Backend persists JSON to SQLite
```
## 6.2 JSON Editing Flow
```text
User edits JSON
-> JSON parsed
-> Schema validation runs
-> If valid, project state updates
-> Visual Editor rerenders
-> User saves project
-> Backend persists JSON to SQLite
```
## 6.3 API Execution Flow
```text
User clicks button
-> Event fires
-> Binding resolves input values
-> Backend REST proxy is called
-> Backend calls external API
-> Response returns to Preview Runtime
-> Response mapping updates target component
```
---
## 7. Project Definition
The project definition should include:
* Project metadata
* Pages
* Components
* Layout
* Component properties
* Events
* Actions
* Bindings
* Variables
* Settings
* Schema version
Example:
```json
{
"schemaVersion": "0.1.0",
"project": {
"id": "example-project",
"name": "Example Project",
"pages": [],
"actions": [],
"variables": {},
"settings": {}
}
}
```
---
## 8. Security Model
The MVP security model should assume:
* Secrets must not be exposed to the frontend.
* API calls requiring secrets must be executed by the backend.
* Logs must mask sensitive values.
* Project exports should exclude secrets by default.
* Anonymous endpoints may be called without stored credentials.
* Future deployments may require endpoint allowlisting.
Potential sensitive values:
* Authorization headers
* API keys
* Bearer tokens
* Basic auth passwords
* Session cookies
---
## 9. Deployment Model
The MVP deployment model should support:
```text
Reverse Proxy
-> Frontend static assets
-> Backend API service
-> SQLite database file
```
A simple Docker Compose deployment is recommended for local demos and early internal use.
Future deployment options may include:
* Kubernetes
* OpenShift
* IBM Cloud Code Engine
* Internal IBM hosting platform
---
## 10. Suggested Repository Structure
```text
conductor/
frontend/
src/
backend/
src/
docs/
REQUIREMENTS.md
NICE-TO-HAVE.md
ARCHITECTURE.md
examples/
project-definitions/
docker-compose.yml
README.md
```
---
## 11. Initial API Surface
Potential backend endpoints:
```text
GET /api/projects
POST /api/projects
GET /api/projects/:id
PUT /api/projects/:id
DELETE /api/projects/:id
POST /api/projects/:id/validate
POST /api/proxy/execute
GET /api/projects/:id/executions
```
---
## 12. Architectural Decisions
Initial decisions:
* Conductor is a web application.
* Conductor uses a React frontend.
* Conductor uses a backend API service.
* Conductor uses SQLite for MVP persistence.
* Conductor stores projects as canonical JSON documents.
* REST API calls flow through the backend proxy.
* Secrets are never intentionally exposed to the browser.
* Production deployments use an external reverse proxy.
* AI assistance is not required for MVP.
## Backend-Agnostic REST Integration
Conductor shall be designed as a backend-agnostic REST UI builder.
Although the initial target use case is IBM Concert Workflows / Rapid Infrastructure Automation, Conductor should not be tightly coupled to any single automation platform.
Any system that exposes reachable HTTP/REST endpoints may be used as an integration target.
Potential integration targets include:
* IBM Concert Workflows / Rapid Infrastructure Automation
* Node-RED HTTP endpoints
* Custom internal APIs
* FastAPI, Flask, Express, or similar backend services
* Other workflow or automation platforms with REST APIs
Conductor should treat external systems as REST action providers.
For the MVP, Conductor is responsible for:
* Rendering the user interface
* Collecting user input
* Calling configured REST endpoints through the backend proxy
* Passing request parameters
* Receiving responses
* Mapping responses back into UI components
External systems are responsible for:
* Workflow execution
* Automation logic
* Business logic
* External integrations
* Long-running task handling
Conductor should avoid implementing workflow orchestration internally unless required by a future enhancement.

View File

@ -241,7 +241,7 @@ Validation under the documented ephemeral Node 20/npm 10 toolchain passed:
- Backend build: not rerun because backend source and the shared schema were unchanged.
- Standalone frontend tsc --noEmit: retains the documented TypeScript 4.9 / @types/node 26 incompatibility and remains separate from the passing CRA compile.
Docker Compose rebuilt successfully on 2026-07-29. Both services started, the backend health endpoint returned status ok, and the frontend responded on port 3000. Proportional manual page-load acceptance passed on 2026-07-29. The accepted Table workflow exposed a non-blocking layout limitation: the white Preview page/canvas background does not grow with runtime-rendered rows beyond its configured or minimum dimensions. Responsive Preview output sizing and Table pagination design are tracked separately in TASKS.md. Final Slice 2 acceptance remains pending.
Docker Compose rebuilt successfully on 2026-07-29. Both services started, the backend health endpoint returned status ok, and the frontend responded on port 3000. Proportional manual page-load acceptance passed on 2026-07-29. The accepted Table workflow exposed a non-blocking layout limitation: the white Preview page/canvas background does not grow with runtime-rendered rows beyond its configured or minimum dimensions. Responsive Preview output sizing and post-MVP Table pagination are tracked in `ROADMAP.md`. Final Slice 2 acceptance remains pending.
## Slice 2 Component Deletion Safety Validation
@ -249,4 +249,68 @@ Manual baseline testing on 2026-07-29 through 2026-07-30 confirmed that deleting
The working tree now detects canonical binding source/target paths and action request-template references before component deletion. Referenced deletion requires explicit cancellation or confirmation, confirmed deletion preserves dangling configuration for diagnostics, and unreferenced deletion remains immediate.
Focused tests passed at 4 suites / 12 tests, the full frontend suite passed at 20 suites / 490 tests, and the frontend production build passed. The user manually confirmed every UI retest in `SAFETY_TEST_PLAN.md` passed on 2026-07-30.
Focused tests passed at 4 suites / 12 tests, the full frontend suite passed at 20 suites / 490 tests, and the frontend production build passed. The user manually confirmed every component-deletion safety retest passed on 2026-07-30.
## Slice 2 Final Workflow Manual Acceptance
On 2026-07-30, the user completed the final Slice 2 workflow-launcher and dependent-data manual acceptance. The launcher was configured entirely through visual controls with canonical component request templates, a Button onClick event, and an onSuccess JSON Viewer binding. Successful execution echoed current Dropdown and Text Input values; HTTP 503 handling was readable, suppressed onSuccess delivery, and cleared after a successful retry.
The dependent-data workflow used a page onLoad POST to https://httpbingo.org/anything with Content-Type application/json and a body template of ["dev","stage","prod"]. Mapping body.json to Dropdown.options populated the choices, [] produced a usable empty state without stale options, status/503 produced a readable failure without false success, and restoring the successful endpoint recovered. The loaded selection drove the later launcher request.
Save, load, and fresh Preview execution preserved page onLoad, Button onClick, request templates, and top-level bindings while resetting runtime state. Canonical JSON gained no inputMap, action.responseMapping, secrets, responses, loading flags, errors, selected runtime values, or loaded runtime options.
This is manual acceptance evidence, not an automated-validation result. Subsequent increments completed diagnostics confirmation, Preview sizing, remaining property/style controls, Actions & Bindings information architecture, the timeout decision, and consolidated validation. Explicit final Slice 2 sign-off remains pending.
## Slice 2 Page-Load Diagnostics Correction
The Actions & Bindings diagnostics now count page events as well as component events when determining whether an action can execute and a response binding can receive a response. The remediation wording names supported component events and page onLoad without prescribing JSON or a Button-only fix.
Focused ActionInspector coverage passed at 1 suite / 11 tests, including page onLoad suppression of both false diagnostics, revised messages for a genuinely untriggered action and binding, and the existing component-event trigger path. The complete frontend suite passed at 20 suites / 493 tests with 0 snapshots, and the frontend production build compiled successfully under the documented Node 20/npm 10 toolchain.
Automated validation is complete. On 2026-07-30, the user proportionally confirmed that the rebuilt Actions & Bindings view no longer shows either false diagnostic for the page onLoad action or its response binding. The initial browser view retained the old development bundle; a hard refresh loaded the rebuilt frontend and the check passed.
## Slice 2 Runtime-Aware Preview Canvas Sizing
Preview now observes the rendered dimensions of its absolutely positioned component wrappers and expands the white canvas beyond design-time bounds when runtime content grows. Measurements remain ephemeral and do not modify canonical component positions or sizes. The canvas can contract back to its design minimum after content becomes smaller or empty.
Focused Preview component validation passed at 1 suite / 9 tests, including expansion, unchanged in-bounds sizing, and contraction. The complete frontend suite passed at 20 suites / 496 tests with 0 snapshots, and the frontend production build compiled successfully under the documented Node 20/npm 10 toolchain.
Proportional manual confirmation passed on 2026-07-30. The user confirmed that populated runtime rows remain inside the expanded white Preview page, the expanded page remains scrollable, an empty runtime result contracts to a usable empty Table state, and canonical Table `size.height` remains unchanged.
## Slice 2 MVP Component Properties and Basic Appearance
The property audit added the missing JSON Viewer design-time default control, Table column-width authoring/rendering, required semantics for Text Input, Text Area, and Dropdown, and canonical per-component `properties.style` with font size, text color, and background color. Appearance overrides render in both the Visual Editor canvas and Preview; absence of an override preserves existing defaults.
The shared schema now validates `style.fontSize` from 8 through 72 and six-digit hexadecimal text/background colors. Runtime input values remain separate from these canonical configuration properties.
Focused Preview property coverage passed at 1 suite / 13 tests. The complete frontend suite passed at 20 suites / 500 tests with 0 snapshots, the frontend production build passed, the backend TypeScript build passed, and the schema fixture matrix passed at 13 valid / 2 expected-invalid / 2 diagnostic-invalid.
Proportional manual authoring, canvas, Preview, reset, and save/reload confirmation passed on 2026-07-31. The user accepted all five sections after Docker rebuild and hard refresh. This manual evidence is separate from automated validation.
## Slice 2 Actions & Bindings Information Architecture and Timeout Decision
Actions, response bindings, and variables now render as distinct bordered records with clearer section descriptions and record labels, attached edit forms, and improved variable/target summaries. Focused ActionInspector validation passes at 1 suite / 12 tests. Proportional manual acceptance passed on 2026-07-31, including record separation, editor ownership, canonical edit isolation, resolved variable-target labeling, and diagnostic recovery.
The v0.1.0 timeout decision retains the backend's fixed 30-second proxy timeout and documents that REST actions have no canonical timeout field. Configurable bounded timeouts are deferred to proxy-policy work.
The user explicitly granted final Slice 2 sign-off on 2026-07-31 after all implementation, consolidated automation, Docker health, documentation reconciliation, and proportional manual acceptance passed. Slice 2 is complete; Slice 2a is authorized as a separate usability increment.
## Slice 2a Request-Reference Usability Validation
Automated validation passed under Node 20/npm 10 at 4 focused suites / 23 tests and 21 complete frontend suites / 507 tests with 0 snapshots. The frontend production build passed. Backend build and schema validation were not rerun because this increment changes frontend authoring only and preserves the canonical document shape.
Proportional manual acceptance passed on 2026-08-01 in isolated Chromium against project `Slice 2 Final` (`#12`) at `http://localhost:3000/`. Testing used a 1440 x 1100 normal viewport and a 700 x 1000 responsive viewport; saved project `#12` was not updated. Contextual suggestions, ambiguous-name filtering and recovery, common and custom header names, guided-control order/alignment/responsive stacking, canonical insertion, replace/append behavior, path exclusion, continued free-form editing, and canonical/runtime-state separation all passed. Recursive canonical JSON inspection found none of the prohibited mapping or runtime-state properties. This is manual acceptance evidence, separate from automated validation. The user granted explicit final Slice 2a sign-off on 2026-08-01; Slice 2a is complete.
## Slice 3 Encrypted Secret Lifecycle Increment
On 2026-08-01, the user approved AES-256-GCM encryption in SQLite with a server-only 32-byte master key and opaque reference-only canonical JSON. The initial backend increment added an encrypted secret table, metadata-only create/list/lookup/update/delete APIs, internal-only resolution, and structured fail-closed key configuration behavior. `docs/SECRETS.md` records the threat assumptions and key lifecycle.
The backend TypeScript build passed. A controlled Docker check created a disposable Bearer credential and returned metadata without its token. A plaintext scan did not find the test token in SQLite. Metadata survived a backend restart using the same key. Deletion returned HTTP 204 and subsequent lookup returned HTTP 404. The disposable record was removed, and the disposable key was removed from the running configuration. Canonical references, authoring UI, credential injection, compatibility validation, comprehensive automated tests, and redaction remain pending.
The next execution increment added canonical `secretReferenceId`, protected-mode action authoring, all four server-side credential injection modes, structured missing/type-mismatch errors, and recursive response-body credential redaction. Controlled httpbingo checks returned HTTP 200 for Basic, Bearer, API-key header, and API-key query execution while reflected Authorization, header values, query values, and query-bearing URLs contained `[REDACTED]` instead of credential material. Missing references and type mismatches returned structured HTTP 400 responses. The full frontend suite passed at 21 suites / 507 tests, focused authoring suites passed at 2 suites / 32 tests, and frontend/backend production builds passed. Guided secret management UI, reference-aware deletion, durable automated backend security coverage, and final manual acceptance remain pending.
The management increment added guided create/replace/delete controls with masked credential entry, metadata-only records, compatible-secret selection, and deletion protection for both unsaved actions and references found in saved project JSON. Durable backend tests cover encrypted round-trip, missing-key failure, and wrong-key authentication failure. Backend tests/build, the full 21-suite / 507-test frontend run, focused 2-suite / 32-test authoring run, and frontend production build passed. Route-level automated injection/redaction coverage, persistent deployment-key validation, and final manual acceptance remain pending.
Final pre-manual Slice 3 validation extracted durable tests for all four protected injection modes, missing/not-found/type-mismatch failures, recursive reflected-value redaction, and conservative URL sanitization. A Docker credential remained usable after backend restart with the same key and its reflected Authorization header was `[REDACTED]`; the disposable record and key were removed afterward. Consolidated validation passed at 22 frontend suites / 509 tests, backend security tests and TypeScript build, frontend production build, the 13-valid / 2-expected-invalid / 2-diagnostic-invalid schema matrix, Docker health, and `git diff --check`. Proportional manual acceptance remains the next gate.
Proportional manual acceptance passed on 2026-08-01 in isolated Chromium using an unsaved browser copy of `Slice 2 Final` (`#12`). All four steps passed: masked lifecycle and metadata-only display; type-compatible choices and opaque-only canonical state; protected Preview execution with reflected credential redaction; and referenced deletion protection, cleanup, and missing-reference failure safety. The saved project was unchanged and the final server-side secret list was empty. The blocked-deletion warning remained visible after later successful cleanup; this was non-blocking and is tracked for stale-feedback cleanup in `ROADMAP.md`. The user granted explicit final Slice 3 sign-off on 2026-08-02; Slice 3 is complete.

View File

@ -18,7 +18,7 @@ Repository:
Use these files in this order:
1. Current source code and automated tests
2. `TASKS.md` for completed and remaining work
2. `ROADMAP.md` for completed and remaining work
3. `TESTING.md` for pending and accepted manual workflows
4. `docs/response-mapping-model.md` for response-binding decisions
5. `shared/schemas/conductor-project.schema.json` for the accepted project-document shape
@ -26,7 +26,7 @@ Use these files in this order:
7. `docs/ARCHITECTURE.md` for architectural direction
8. `docs/BUILD_AND_TEST_PLAN.md` for historical milestone and validation guidance
Some root-level documentation duplicates files under `docs/`. The `docs/` copies should become authoritative, but that cleanup has not yet been completed.
The `docs/` copies of requirements, architecture, and future-idea specifications are authoritative. Exact duplicate root copies were removed during the roadmap consolidation.
## Core Architectural Decisions
@ -134,15 +134,13 @@ The initial repository push was completed on 2026-07-18.
Unless the user chooses a different priority, proceed in this order:
1. Complete final Slice 2 workflow-launcher and dependent-data acceptance; request-input, response-binding/variable, and page-load acceptance passed by 2026-07-29.
2. Complete final Slice 2 workflow-launcher and dependent-data acceptance.
3. Add validation to every project save path.
4. Implement authentication and secure secret handling.
5. Harden the REST proxy and add sanitized execution logging.
6. Add backend, end-to-end, and security regression tests.
7. Reconcile and consolidate project documentation.
1. Continue Slice 3 with canonical secret references, action authoring, credential injection, compatibility validation, and redaction tests.
2. Add validation to every project save path.
3. Harden the REST proxy and add sanitized execution logging.
4. Add backend, end-to-end, and security regression tests.
5. Reconcile and consolidate project documentation.
See `TASKS.md` for the complete actionable checklist.
See `ROADMAP.md` for the complete actionable checklist.
## Working Conventions
@ -154,13 +152,13 @@ See `TASKS.md` for the complete actionable checklist.
- Do not commit secrets, environment files, SQLite databases, dependency directories, or build artifacts.
- Preserve backend-agnostic REST behavior; do not couple core runtime logic to IBM-specific services.
- Clearly distinguish implemented behavior from schema-only or planned behavior.
- Update `TASKS.md` when a task is completed, added, removed, or materially re-scoped.
- Update `ROADMAP.md` and the owning slice when a task is completed, added, removed, or materially re-scoped.
## Session Start Checklist
At the beginning of a new session:
- [ ] Read `CODEX.md` and `TASKS.md`.
- [ ] Read `CODEX.md`, `ROADMAP.md`, and the active slice plan.
- [ ] Run `git status --short --branch`.
- [ ] Inspect recent commits with `git log --oneline -5`.
- [ ] Verify relevant source files before relying on this handoff; this document may lag behind code.
@ -171,7 +169,7 @@ At the beginning of a new session:
After material work:
- [ ] Update completed and remaining items in `TASKS.md`.
- [ ] Update completed and remaining items in `ROADMAP.md` and the active slice plan.
- [ ] Update the implementation state, gaps, validation status, and next priority in `CODEX.md`.
- [ ] Record the exact commands run and whether they passed when validation results materially changed.
- [ ] Ensure documentation matches any schema or runtime decisions made during the session.
@ -211,7 +209,7 @@ Date: 2026-07-20
- Validation passes: 14 frontend suites / 467 tests, frontend production build, backend build, and the 13 / 2 / 2 schema matrix.
- Standalone frontend `tsc --noEmit` retains the recorded dependency failure.
- Manual request-input acceptance passed on 2026-07-28, including executed component and variable references, canonical/runtime-state separation, destination filtering, and raw-template regression coverage.
- `NEXT_SESSION_PROMPT.md` contains the continuation scope and manual request-input acceptance checklist.
- The request-input continuation checklist was completed; durable acceptance evidence remains in `TESTING.md` and `BASELINE.md`.
### Response Binding and Variable Addendum
@ -247,7 +245,7 @@ Date: 2026-07-20
- Page-load status, loading, errors, responses, component values, and runtime variable values remain ephemeral. No ComponentEvent.inputMap, deprecated action.responseMapping, authentication, secrets, or general orchestration was added.
- Updated valid-visual-rest-actions.json with representative page-load configuration. No schema, frontend model, or backend changes were required.
- Focused tests pass at 4 suites / 9 tests; the full frontend suite passes at 18 suites / 486 tests under Node 20/npm 10. The frontend production build and 13 valid / 2 expected-invalid / 2 diagnostic-invalid schema matrix pass.
- Docker Compose rebuilt successfully; both services started, backend health passed, and the frontend responded on port 3000. Proportional manual page-load acceptance passed on 2026-07-29. The accepted Table workflow exposed a non-blocking Preview background-sizing limitation; TASKS.md tracks content-aware Preview sizing and a separate Table-pagination design discussion. No commit or push was made.
- Docker Compose rebuilt successfully; both services started, backend health passed, and the frontend responded on port 3000. Proportional manual page-load acceptance passed on 2026-07-29. The accepted Table workflow exposed a non-blocking Preview background-sizing limitation; `ROADMAP.md` tracks content-aware Preview sizing and post-MVP Table pagination. No commit or push was made.
### Component Deletion Safety Addendum
@ -255,4 +253,62 @@ Date: 2026-07-20
- The warning names the affected binding or action and requires **Cancel** or **Delete anyway**. Confirmed deletion removes only the component so existing diagnostics can expose the dangling reference.
- Unreferenced components retain immediate deletion.
- Focused validation passed at 4 suites / 12 tests, the full frontend suite at 20 suites / 490 tests, and the production build passed.
- The user manually confirmed every component-deletion safety retest in `SAFETY_TEST_PLAN.md` passed on 2026-07-30. No commit or push was made.
- The user manually confirmed every component-deletion safety retest passed on 2026-07-30. No commit or push was made.
### Final Slice 2 Workflow Acceptance Addendum
- On 2026-07-30, the user manually accepted the complete workflow-launcher and dependent-data scenarios.
- The visually authored launcher used canonical component request templates, Button onClick, and an onSuccess JSON Viewer binding; success, readable HTTP 503 failure, onSuccess suppression, retry recovery, save/load, and fresh-Preview runtime reset passed.
- The dependent-data workflow used page onLoad to populate Dropdown.options, passed populated, empty, failure, and recovery states, and supplied the selected runtime value to the later launcher request.
- Canonical page events, component events, templates, and top-level bindings persisted. Runtime inputs, loaded options, loading, responses, errors, secrets, inputMap, and action.responseMapping did not.
- Manual acceptance is distinct from automated validation. Slice 2 remains open for diagnostics confirmation and the other hardening/presentation tasks listed in ROADMAP.md.
### Page-Load Diagnostics Correction Addendum
- Actions & Bindings now includes page events when computing triggered action IDs, so a valid page onLoad action suppresses the false untriggered-action information message and false response-binding warning.
- Diagnostic remediation now refers to supported component events or page onLoad rather than prescribing Button JSON.
- Focused ActionInspector validation passed at 1 suite / 11 tests; the complete frontend suite passed at 20 suites / 493 tests with 0 snapshots; the production build passed.
- Proportional manual confirmation passed on 2026-07-30: after a hard refresh loaded the rebuilt frontend, neither the page onLoad action nor its response binding showed the false untriggered diagnostic.
### Runtime-Aware Preview Canvas Sizing Addendum
- Preview observes rendered component-wrapper dimensions and grows the white canvas when runtime content exceeds configured design-time height.
- The measurement layer is runtime-only; canonical component positions and sizes are unchanged.
- Recalculation starts from design bounds so the page can contract after runtime content becomes smaller or empty.
- Focused Preview validation passed at 1 suite / 9 tests; the complete frontend suite passed at 20 suites / 496 tests with 0 snapshots; the production build passed.
### MVP Component Properties and Basic Appearance Addendum
- Added visual JSON Viewer default JSON, Table column width, and Dropdown required controls; existing Text Input/Text Area required controls now affect Preview semantics.
- Added canonical `properties.style` with font size, text color, and background color, applied in both canvas and Preview while preserving defaults when absent.
- The shared schema validates the style shape and bounds; no theme system or post-MVP styling features were added.
- Focused Preview property coverage passed at 1 suite / 13 tests; the complete frontend suite passed at 20 suites / 500 tests; frontend and backend builds passed; the 13 / 2 / 2 schema matrix passed.
- Proportional manual authoring and persistence confirmation passed on 2026-07-31 after Docker rebuild and hard refresh. Passing automation and manual acceptance remain separately reported.
- Proportional manual Table layout confirmation passed on 2026-07-30: populated rows remained inside the expanded white page, scrolling remained usable, an empty result contracted the page, and canonical Table size remained unchanged.
### 2026-07-31 Slice 2 Presentation Addendum
- The user manually accepted all five MVP component-property and basic-appearance sections. Configuration authoring, canvas/Preview agreement, canonical synchronization, reset behavior, save/reload persistence, and fresh-Preview runtime reset passed.
- Actions, response bindings, and variables now render as distinct summarized records with clearer section descriptions, hierarchy, spacing, editor attachment, and variable/target summaries. Focused ActionInspector validation passes at 1 suite / 12 tests; proportional manual acceptance passed on 2026-07-31.
- v0.1.0 retains the existing fixed 30-second backend proxy timeout. No canonical per-action timeout field or editor control was added; bounded configurability is deferred to proxy-policy work.
- The user manually accepted the Actions & Bindings presentation on 2026-07-31, including record separation, editor attachment, canonical edit isolation, resolved variable-target labeling, and diagnostic recovery.
- The user explicitly granted final Slice 2 sign-off on 2026-07-31. Slice 2 is complete, and Slice 2a is authorized as a separate request-reference usability increment.
### Slice 2a Request-Reference Usability Addendum
- Contextual component and variable suggestions now appear in URL, header-value, query-value, and body-template controls after an unfinished `{{` template opener.
- Suggestions insert canonical executed templates while preserving free-form prefixes; ambiguous duplicate component names are excluded.
- Header-name inputs suggest common names including Accept and Content-Type while retaining custom-name entry.
- Guided Request value reference controls now present request destination first and value source second for key/value alignment.
- Focused coverage passes at 4 suites / 23 tests; the complete frontend passes at 21 suites / 507 tests; the production build passes.
- Proportional manual acceptance passed on 2026-08-01 in isolated Chromium against project `Slice 2 Final` (`#12`) at 1440 x 1100 and 700 x 1000. Contextual suggestions, duplicate-name exclusion and recovery, common/custom headers, guided-control order and responsive alignment, canonical insertion, replace/append behavior, path exclusion, continued editing, and canonical/runtime separation all passed. Saved project `#12` was not updated. This manual evidence is separate from automation. The user granted explicit final Slice 2a sign-off on 2026-08-01; Slice 2a is complete.
### Slice 3 Encrypted Secret Lifecycle Addendum
- The user approved AES-256-GCM credential encryption in SQLite with a server-only 32-byte master key and opaque canonical references.
- The backend now has encrypted secret persistence, metadata-only lifecycle APIs, internal-only credential resolution, and fail-closed missing/malformed-key handling. The threat and lifecycle contract is in `docs/SECRETS.md`.
- Backend TypeScript build passed. A controlled Docker lifecycle check returned no credential value, found no plaintext token in SQLite, survived restart, and passed deletion/404 cleanup. The disposable test record and active disposable key were removed.
- Canonical `secretReferenceId`, protected-mode frontend authoring, runtime injection for all four credential-backed modes, structured compatibility errors, and reflected-response credential redaction are implemented. Controlled echo checks passed for every mode; full frontend coverage remains 21 suites / 507 tests, focused authoring coverage passes at 2 suites / 32 tests, and frontend/backend builds pass.
- Guided secret lifecycle UI, compatible action selection, reference-aware deletion, and durable encryption tests are implemented. Backend tests/build, full frontend coverage at 21 suites / 507 tests, focused authoring at 2 suites / 32 tests, and the frontend production build pass.
- Durable injection/failure/redaction/URL-sanitization tests, persistent-key Docker restart execution, consolidated 22-suite / 509-test frontend validation, backend tests/build, frontend production build, and the 13 / 2 / 2 schema matrix pass.
- Proportional Slice 3 manual acceptance passed on 2026-08-01 using an unsaved browser copy of project `#12`. Masked lifecycle, metadata-only records, compatible selection, opaque canonical references, protected execution/redaction, deletion safety, cleanup, and missing-reference failure behavior passed. The final secret list was empty and the saved project was unchanged. A non-blocking stale blocked-deletion warning is tracked in `ROADMAP.md`. The user granted explicit final Slice 3 sign-off on 2026-08-02; Slice 3 is complete.

70
MANUAL_TEST_REQUEST.md Normal file
View File

@ -0,0 +1,70 @@
# Slice 2a Manual Test Request
## Current status
Manual Step 1, contextual suggestions, has already passed and does not need to be repeated unless a regression is suspected.
Complete the four remaining tests in order. Perform one step at a time and record the result before continuing.
## Step 2 — Ambiguous duplicate component names
1. In **Visual Editor**, ensure one Text Input is named `hostname`.
2. Add a second Text Input and also name it `hostname`.
3. Return to **Actions & Bindings** and edit the REST action.
4. In a supported request value field, replace the value with exactly `{{`.
5. Confirm `Component: hostname` is absent from the contextual suggestion panel.
6. Open **Value source** under **Request value reference** and confirm `Component: hostname` is absent there too.
7. Confirm a valid declared variable such as `Variable: environment` remains offered.
8. Rename the second component to `hostnameSecondary`.
9. Return to the action, type `{{` again, and confirm both unique components are offered.
### Expected result
The ambiguous `hostname` component name is withheld from both contextual suggestions and the guided source list. The declared variable remains available. After the duplicate is renamed, both uniquely named components are offered again.
## Step 3 — Common and custom header names
1. Edit a REST action and open **Request parameters**.
2. Under **Headers**, click **+ Add row**.
3. Focus the header-name field or begin typing.
4. Confirm `Accept` and `Content-Type` are offered by the header-name suggestions.
5. Enter the custom name `X-Custom-Vendor-Header` instead.
6. Enter any value, such as `test`.
7. Inspect canonical JSON and confirm the header is stored exactly as entered.
### Expected result
Common header names including `Accept` and `Content-Type` are suggested, arbitrary custom header names remain accepted, and the canonical header object retains its existing shape with `X-Custom-Vendor-Header` stored exactly as entered.
## Step 4 — Guided-control order and alignment
1. Edit a REST action containing a query parameter such as `environment`.
2. Locate **Request value reference**.
3. Confirm the visible order is **Request destination**, **Value source**, **Insert reference**.
4. In **Request destination**, select `Query: environment`.
5. In **Value source**, select `Variable: environment` or a unique component.
6. Confirm the destination control aligns with the request-key side of the query row and the value-source control aligns with the request-value side at the current viewport.
7. Click **Insert reference** and confirm the expected canonical template appears in the query value.
8. Narrow the browser window to a supported small width and confirm the controls stack clearly without overlap.
### Expected result
The controls appear in the required order, their alignment clearly represents request key and value at the normal viewport, and they stack without overlap at a supported small width. Inserting the selected source produces the expected canonical template in the query value.
## Step 5 — Existing behavior regression
1. Use guided insertion into an existing header value and confirm the old value is replaced.
2. Use guided insertion into an existing query value and confirm the old value is replaced.
3. Use guided insertion into an Endpoint URL and confirm the template is appended.
4. Use guided insertion into a non-empty body template and confirm the template is appended.
5. Confirm path parameters are not offered as guided destinations and do not show contextual template suggestions.
6. After any insertion, type additional free-form text and confirm editing remains possible.
7. Inspect canonical JSON and confirm no `inputMap`, `action.responseMapping`, runtime values, responses, loading flags, or errors were added.
### Expected result
Guided insertion replaces existing header and query values, while it appends to Endpoint URL and body-template values. Path parameters remain excluded from guided and contextual insertion. Fields remain freely editable, and canonical JSON contains none of the prohibited mapping or runtime-state properties.
## Completion gate
If a step fails, record the exact step and observed behavior before making any changes. If Steps 25 pass, reconcile the Slice 2a status and evidence documents, run final integrity and Docker health checks, and request explicit Slice 2a sign-off. Do not commit or push without explicit authorization.

49
MANUAL_TEST_SLICE3.md Normal file
View File

@ -0,0 +1,49 @@
# Slice 3 Manual Verification
Run one step at a time against `http://localhost:3000/`. Use a disposable project or avoid saving changes to an existing project. Do not use real credentials; use the example values below.
## Step 1 — Secret lifecycle and browser exposure
1. Open **Actions & Bindings** and locate **Secrets**.
2. Create a Bearer token secret named `Manual Bearer` with token `manual-bearer-value`.
3. Confirm the saved record shows only its name, authentication type, opaque ID, and lifecycle controls—not the token.
4. Confirm the token input was masked while entering it and cleared after creation.
5. Click **Replace value**, enter `manual-bearer-replaced`, and confirm the old or new token is not displayed afterward.
Expected: credential entry is masked, create/replace succeeds, and stored values are never redisplayed.
## Step 2 — Compatible selection and canonical isolation
1. Create one secret for each remaining mode using disposable values: Basic, API-key header, and API-key query.
2. Edit a REST action and select each authentication mode in turn.
3. Confirm **Stored secret** offers only secrets matching the selected mode.
4. Select `Manual Bearer` for Bearer mode.
5. Inspect canonical JSON and confirm the action contains `authenticationType: "bearerToken"` and an opaque `secretReferenceId` only.
6. Confirm canonical JSON contains none of the usernames, passwords, tokens, API-key names, or API-key values entered above.
Expected: guided choices are type-compatible and canonical state contains only the opaque reference.
## Step 3 — Execution and redaction
1. Configure the referenced action as `GET https://httpbingo.org/anything`.
2. Use **Test Action** or Preview execution.
3. Confirm HTTP 200.
4. Inspect the response and confirm the reflected Authorization value is `[REDACTED]`, with neither Bearer token value present.
5. Repeat proportionally with API-key header and query secrets. Confirm reflected values and the query-bearing reflected URL contain `[REDACTED]`.
Expected: all credential modes execute server-side and no stored value returns to the browser.
## Step 4 — Failure safety and deletion protection
1. With an action still referencing `Manual Bearer`, try deleting that secret.
2. Confirm deletion is blocked and names the referencing action.
3. Change the action to Anonymous, which clears the secret reference.
4. Delete `Manual Bearer` and confirm it disappears.
5. Delete all other disposable secrets.
6. Select a protected mode without a secret and confirm the diagnostic requests a server-side secret reference; execution must fail clearly rather than execute anonymously.
Expected: referenced deletion is blocked, unreferenced deletion succeeds, missing credentials fail safely, and no protected action silently executes anonymously.
## Completion gate
Record each step as PASS or FAIL with observed behavior. Do not save disposable secret references into a valued project. If all four steps pass, reconcile final Slice 3 evidence and request explicit Slice 3 sign-off.

View File

@ -70,6 +70,6 @@ These are end-to-end acceptance scenarios. They demonstrate that Conductor can b
## Release Boundary
The MVP is complete only when all required component, configuration, authentication, security, validation, persistence, and deployment tasks in `TASKS.md` are complete and all six workflows above pass the Slice 6 release-validation process.
The MVP is complete only when all required component, configuration, authentication, security, validation, persistence, and deployment tasks in `ROADMAP.md` are complete and all six workflows above pass the Slice 6 release-validation process.
Post-MVP scope includes AI assistance, OAuth 2.0, IBM Cloud IAM, mTLS, advanced orchestration, and the future GUI components listed in `TASKS.md`.
Post-MVP scope includes AI assistance, OAuth 2.0, IBM Cloud IAM, mTLS, advanced orchestration, and the future capabilities listed in `ROADMAP.md`.

View File

@ -1,45 +0,0 @@
# Next Session Prompt
Continue Conductor Slice 2: Visual Configuration from the latest `main` branch.
First read `CODEX.md`, `TASKS.md`, `MVP_SCOPE.md`, `SLICE2.md`, and `BASELINE.md`. Verify the current branch, working tree, recent commits, and relevant diffs before changing anything.
Completed and pushed work includes:
- Visual anonymous REST action creation, editing, duplication, and reference-aware deletion.
- Visual Button `onClick` action assignment with add/change/clear, no-action state, diagnostics, canonical synchronization, automated Preview execution coverage, and manual acceptance.
- Visual request-input authoring from existing component and variable values into executed REST URL, header, query, and body templates.
- `ComponentEvent.inputMap` is compatibility-only and must not be used by new UI.
Begin the next coherent vertical increment: visual response bindings and variable authoring.
Requirements:
- Create, edit, and delete canonical top-level `project.bindings` visually.
- Default new action-response bindings to `trigger: "onSuccess"`.
- Select an existing REST action response source and supported component or variable target.
- Preserve compatibility with legacy `onClick` response bindings without creating new ones.
- Do not use deprecated `action.responseMapping`.
- Add variable declaration/default-value editing with reference-aware diagnostics.
- Keep runtime values, loading state, responses, and errors ephemeral.
- Preserve anonymous-only REST configuration; authentication/secrets remain Slice 3.
- Leave page-load actions for the following Slice 2 increment unless a minimal supporting change is unavoidable.
- Add focused tests, update representative fixtures and durable state documents, and run the Node 20/npm 10 focused/full frontend tests, frontend build, backend build, schema matrix, and proportional manual workflow.
- Report the standalone TypeScript 4.9 / `@types/node` incompatibility separately from the passing CRA production compile.
Before implementing the next increment, complete or coordinate manual acceptance of the most recently completed request-input step:
1. Start Docker Desktop and run Conductor with Docker Compose; verify both services and the health endpoint.
2. Open or create a project containing a Text Input or Dropdown, a Button, and an anonymous REST action.
3. Edit the REST action in Actions & Bindings and add a query parameter named `item`.
4. In Request value reference, select the input component, select `Query: item`, and click Insert reference.
5. Inspect canonical JSON and confirm the query value immediately becomes `{{components.<componentName>.value}}`.
6. Confirm no `ComponentEvent.inputMap` data was added and path parameters are not offered as destinations.
7. Assign the action to the Button, enter or select a runtime value in Preview, and execute the Button.
8. Confirm the outgoing request contains the runtime component value and runtime loading, response, and error state do not leak into canonical JSON.
9. Confirm raw URL/header/query/body template editing still works after guided insertion.
10. If the project already declares a variable through JSON, repeat insertion with it and verify `{{variables.<name>}}` resolution.
Record manual acceptance separately from automated coverage in `CODEX.md`, `SLICE2.md`, and `BASELINE.md`. Do not mark it accepted without user confirmation.
Do not commit or push unless explicitly requested.

View File

@ -1,155 +0,0 @@
# NICE-TO-HAVE.md
# Future Enhancements for Conductor
The following features are considered desirable enhancements but are not required for the initial MVP.
---
# AI-Assisted Development
Conductor should be designed so that AI capabilities can be integrated without changing the underlying project architecture.
AI assistance should operate by reading and modifying the project's canonical JSON definition.
The application shall remain fully functional when AI services are unavailable.
---
## AI Chat
Provide an integrated conversational interface for interacting with a project.
Example requests:
* Build a login page.
* Add a table below the dropdown.
* Connect this button to a REST endpoint.
* Explain what this page does.
* Rename all references to "Environment" as "Target Environment."
* Improve the layout.
* Add validation to required fields.
* Document this application.
The AI should generate proposed project changes rather than modifying the project without user approval.
---
## AI Project Generation
Allow users to create an application from a natural language description.
Example:
> Build a form that accepts a hostname, environment, and owner, then calls a Rapid Infrastructure Automation workflow and displays the results.
The AI should generate:
* Pages
* Components
* Layout
* REST actions
* Bindings
* Default styling
The generated project should immediately open in the Visual Editor for refinement.
---
## AI-Assisted REST Configuration
The AI may assist users by:
* Creating REST action definitions
* Suggesting request bodies
* Generating headers
* Creating authentication configurations
* Mapping API responses to UI components
* Suggesting validation rules
---
## AI Documentation
Generate documentation from an existing project.
Potential outputs include:
* Markdown documentation
* API documentation
* End-user documentation
* Administrator documentation
* Project summaries
---
## AI Refactoring
Allow the AI to improve an existing project.
Examples include:
* Simplifying layouts
* Removing unused components
* Consolidating duplicate REST actions
* Improving naming consistency
* Reorganizing pages
* Suggesting accessibility improvements
---
## AI Validation
The AI may analyze projects for potential issues, including:
* Missing bindings
* Invalid REST configurations
* Unused components
* Circular dependencies
* Missing required inputs
* Security concerns
* Inconsistent naming
---
## AI Explainability
Allow users to ask questions about an existing project.
Examples include:
* What happens when this button is clicked?
* Which components call REST APIs?
* Which workflow launches this action?
* Where is this value used?
* Why is this field disabled?
---
## OpenAPI Integration
Allow users to import an OpenAPI specification and automatically generate:
* REST action definitions
* Forms
* CRUD pages
* Tables
* Documentation
---
## Additional Future Enhancements
* Multi-page applications
* Reusable component libraries
* Themes and styling templates
* Workflow templates
* Project version history
* Git integration
* Team collaboration
* Role-based access control
* Plugin architecture
* Additional authentication providers
* Internationalization
* Accessibility auditing
* Application packaging and deployment

View File

@ -81,6 +81,7 @@ docker-compose up --build
| Document | Description |
|---|---|
| [ROADMAP.md](ROADMAP.md) | Unified v0.1.0 work plan grouped by function and slice |
| [TESTING.md](TESTING.md) | Detailed manual test workflows and acceptance records |
| [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md) | Full product requirements |
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Technical architecture |

View File

@ -1,692 +0,0 @@
# REQUIREMENTS.md
# Project: AI-Assisted REST UI Builder
## 1. Purpose
Build a lightweight UI builder that allows users to create simple frontend applications backed by REST API endpoints, with a focus on IBM Concert Workflows / Rapid Infrastructure Automation use cases.
The tool should allow users to drag and drop UI elements onto a canvas, configure those elements, connect them to REST API calls, and allow one UI element to update another based on API responses or user interaction.
IBM Bob / watsonx should assist with generating, configuring, documenting, and refining these UI applications.
---
## 2. Goals
The system should:
* Allow users to visually build simple UI workflows.
* Support drag-and-drop placement of UI components on a canvas.
* Allow UI components to call REST API endpoints.
* Support multiple authentication types, including anonymous access.
* Allow API responses to update other UI components.
* Make it easier for CSMs, architects, and technical users to build working demos or operational tools without hand-coding every frontend.
* Use AI assistance to speed up configuration, explanation, and generation of UI/API bindings.
---
## 3. Non-Goals
The system is not intended to be a full Appsmith replacement.
The MVP will not include:
* Full database integrations.
* Complex multi-user application publishing.
* Advanced permissions or RBAC.
* Marketplace-style widget plugins.
* Pixel-perfect design tooling.
* Full workflow orchestration.
* Public SaaS hosting.
* Complex version control or branching.
* Enterprise-grade audit logging.
---
## 4. Primary Users
### 4.1 Customer Success Managers
CSMs may use the tool to build demos, internal utilities, workflow launchers, and simple operational dashboards.
### 4.2 Technical Sellers / Architects
Technical users may use the tool to demonstrate how REST APIs, Concert Workflows, or Rapid Infrastructure Automation endpoints can be exposed through a simple UI.
### 4.3 Internal Operators
Internal IBM teams may use the tool to create small utilities for repeatable operational tasks.
---
## 5. Core Use Cases
### 5.1 Build a Simple Workflow Launcher
A user creates a page with:
* A dropdown to select an environment.
* A text input for parameters.
* A button to call a REST endpoint.
* A status/output panel showing the response.
### 5.2 Build a Dependent Dropdown UI
A user creates:
* A dropdown that calls an API to retrieve available environments.
* A second dropdown that updates based on the selected environment.
* A button that launches a workflow using both selected values.
### 5.3 Build a Read-Only API Dashboard
A user creates:
* A table connected to a REST endpoint.
* A refresh button.
* A details panel that updates when a table row is selected.
### 5.4 Build a Concert/RIA Workflow Form
A user creates:
* Input fields for required workflow parameters.
* Authentication configuration.
* A submit button that calls the workflow endpoint.
* A response display showing success, failure, or returned data.
---
## 6. Functional Requirements
## 6.1 Canvas Builder
The system shall provide a visual canvas where users can place UI elements.
The canvas shall support:
* Drag-and-drop widget placement.
* Selecting widgets.
* Moving widgets.
* Resizing widgets.
* Deleting widgets.
* Basic alignment or snapping.
* Saving the canvas layout.
## 6.2 UI Components
The MVP shall support the following UI components:
* Button
* Text input
* Text area
* Dropdown/select
* Checkbox
* Radio group
* Static text/label
* Table
* JSON response viewer
* Status/message panel
* Container/card
Future components may include:
* Tabs
* Modal dialog
* Date picker
* File upload
* Chart
* Progress indicator
## 6.3 Component Configuration
Each component shall have configurable properties.
Common properties should include:
* Component name / ID
* Label
* Placeholder text
* Default value
* Visibility
* Disabled state
* Required state
* Styling basics
Component-specific properties may include:
* Dropdown options
* Table columns
* Button action
* API binding
* Response mapping
* Validation rules
## 6.4 REST API Configuration
The system shall allow users to define REST API actions.
Each REST API action shall support:
* Name
* Description
* HTTP method: GET, POST, PUT, PATCH, DELETE
* Endpoint URL
* Headers
* Query parameters
* Path parameters
* Request body
* Authentication type
* Timeout setting
* Expected response format
The system should support JSON request and response bodies in the MVP.
## 6.5 Authentication Support
The system shall support multiple authentication types for REST API calls.
MVP authentication types:
* Anonymous / no authentication
* Basic authentication
* Bearer token
* API key in header
* API key in query parameter
Future authentication types may include:
* OAuth 2.0
* mTLS
* IBM Cloud IAM token flow
* Custom authentication script or pre-request action
## 6.6 UI-to-API Binding
The system shall allow a UI component to trigger a REST API action.
Examples:
* Button click calls an API endpoint.
* Dropdown selection calls an API endpoint.
* Page load calls an API endpoint.
* Table row selection updates another component.
The system shall allow component values to be used in API requests.
Examples:
* Text input value used in a POST body.
* Dropdown value used as a query parameter.
* Table row value used in a path parameter.
## 6.7 Component-to-Component Updates
The system shall allow one component to update another component.
Examples:
* Dropdown A updates the options in Dropdown B.
* Button response updates a JSON viewer.
* Table selection updates a details panel.
* API response updates a status/message component.
The MVP should support simple response mapping using JSON paths.
Example:
```json
{
"source": "apiResponse.environments",
"target": "environmentDropdown.options"
}
```
## 6.8 AI Assistance
IBM Bob / watsonx should assist users by:
* Suggesting UI layouts from a natural language prompt.
* Generating REST API action definitions.
* Explaining API responses.
* Suggesting response mappings.
* Helping generate sample request bodies.
* Helping troubleshoot API errors.
* Producing documentation for a generated UI.
* Suggesting improvements to usability.
Example prompt:
> Build a form that lets me select an environment, enter a hostname, and call a Rapid Infrastructure Automation workflow to provision monitoring.
The AI assistant should produce a proposed page layout, components, API bindings, and configuration steps.
## 6.9 Save and Load
The system shall allow users to save a project.
A saved project should include:
* Pages
* Canvas layout
* Components
* API action definitions
* Component bindings
* Response mappings
* Non-secret configuration
Secrets should not be stored directly in exported project files unless encrypted or intentionally allowed by policy.
## 6.10 Preview Mode
The system shall provide a preview mode.
Preview mode shall allow users to:
* Interact with the UI as an end user.
* Trigger API calls.
* View API responses.
* Validate bindings.
* Test component-to-component updates.
## 6.11 Export
The system should support exporting the project definition as JSON.
Future export options may include:
* React application export
* Static frontend bundle
* Documentation export
* Markdown summary
* Deployment package
## 6.12 Project Editors
Conductor shall support multiple methods for creating and editing a project.
### Visual Editor
The Visual Editor shall be the primary user interface for creating applications.
The Visual Editor shall provide:
* Drag-and-drop placement of UI components
* Component selection
* Component movement and resizing
* Property editing
* Event configuration
* Component binding configuration
* Visual indication of selected components
* Canvas zoom and pan
* Undo and redo operations (future enhancement)
Changes made through the Visual Editor shall immediately update the project's canonical JSON definition.
### JSON Editor
Conductor shall provide a JSON Editor for advanced users.
The JSON Editor shall allow users to directly edit the project's canonical JSON document.
The JSON Editor should provide:
* Syntax highlighting
* Automatic formatting
* Schema validation
* Error reporting
* Search and replace
* Read-only and editable modes
Changes made through the JSON Editor shall immediately update the Visual Editor.
The Visual Editor and JSON Editor shall always represent the same project state.
### Canonical Project Model
Both editors shall operate on the same underlying project definition.
Neither editor shall maintain an independent representation of the application.
All modifications shall update the project's canonical JSON document, which serves as the authoritative representation of the application.
---
## 7. Data Model
## 7.1 Project
A project represents a UI application.
A project contains:
* Project ID
* Name
* Description
* Pages
* API actions
* Global variables
* Metadata
## 7.2 Page
A page contains:
* Page ID
* Name
* Layout
* Components
* Page-level events
## 7.3 Component
A component contains:
* Component ID
* Type
* Name
* Position
* Size
* Properties
* Events
* Bindings
## 7.4 API Action
An API action contains:
* Action ID
* Name
* Method
* URL
* Headers
* Parameters
* Body template
* Authentication configuration
* Response mapping
## 7.5 Binding
A binding defines how data moves between components and actions.
A binding contains:
* Source
* Target
* Trigger event
* Transformation rule
* Error handling behavior
## 7.6 Project Definition Document
The canonical representation of a Conductor project shall be a single structured JSON document.
This document represents the complete application definition and serves as the authoritative source of truth for the project.
The project definition shall include:
* Project metadata
* Pages
* Canvas layout
* UI components
* Component properties
* Events
* REST API action definitions
* Component bindings
* Global variables
* Application settings
* Theme information (future)
* Version information
The backend shall persist this project definition as JSON.
The backend may extract selected metadata into relational database fields for indexing, searching, reporting, or performance optimization, but the JSON project definition remains the canonical representation.
A project definition should be portable between Conductor installations and suitable for export, import, version control, and AI-assisted modification.
Example capabilities enabled by this approach include:
* Exporting a complete application as a single file.
* Importing existing applications.
* Versioning projects in Git.
* Comparing changes between revisions.
* AI-assisted editing of complete applications.
* Generating documentation from the project definition.
* Generating frontend code from the project definition.
Future versions of Conductor may define a published JSON schema describing the project definition format to support validation, tooling, and interoperability.
---
## 8. Example MVP Workflow
1. User creates a new project.
2. User drags a dropdown onto the canvas.
3. User configures the dropdown to call an API endpoint on page load.
4. API response populates the dropdown options.
5. User drags a text input onto the canvas.
6. User drags a button onto the canvas.
7. User configures the button to call a POST endpoint.
8. The POST body uses values from the dropdown and text input.
9. User drags a JSON viewer onto the canvas.
10. Button response updates the JSON viewer.
11. User tests the flow in preview mode.
12. User saves the project.
---
## 9. Technical Requirements
## 9.1 Frontend
Recommended frontend stack:
* React
* TypeScript
* Drag-and-drop library
* Component state management
* JSON schema-driven configuration panels
## 9.2 Backend
Recommended backend capabilities:
* REST API proxy
* Project persistence
* Secret handling
* Authentication configuration storage
* API execution logging for troubleshooting
* AI assistant integration
## 9.3 Security
The system must:
* Avoid exposing secrets in the frontend.
* Store secrets securely.
* Mask sensitive values in logs.
* Support anonymous API calls where appropriate.
* Prevent arbitrary unsafe code execution in user-defined mappings.
* Validate URLs and headers before execution.
* Consider allowlists for internal endpoint access.
## 9.4 Observability
The system should provide basic troubleshooting details:
* API request timestamp
* Method and endpoint
* Response status
* Response duration
* Error message
* Sanitized request/response preview
---
## 10. MVP Scope
The MVP should include:
* Single-page project builder.
* Basic canvas.
* Core widgets.
* REST action configuration.
* Anonymous, Basic, Bearer, and API key authentication.
* Component-to-API binding.
* API response-to-component binding.
* Preview mode.
* Save/load project as JSON.
* IBM Bob/watsonx-assisted generation of project configuration.
---
## 11. Future Enhancements
Potential future enhancements:
* Multi-page applications.
* Role-based access control.
* OAuth support.
* IBM Cloud IAM integration.
* Workflow execution history.
* Generated React code export.
* Import from OpenAPI specification.
* Visual JSON path mapper.
* Charts and dashboards.
* Reusable templates.
* Version history.
* Approval workflows.
* Deployment to internal hosting.
* Git integration.
* Team collaboration.
---
## 12. Open Questions
* Should this be a standalone internal tool or embedded into another IBM workflow?
* Will API calls execute directly from the browser or through a backend proxy?
* How should secrets be stored and managed?
* Which Concert/RIA authentication methods are required first?
* Should OpenAPI import be part of MVP or future scope?
* Is the primary output a working hosted UI, a generated React app, or a reusable project definition?
* What level of audit logging is required for internal IBM use?
* Should users be allowed to call arbitrary URLs?
* Will the tool need approval before calling production endpoints?
* Should IBM Bob generate only suggestions, or should it directly modify the canvas?
---
## 13. Success Criteria
The MVP is successful if a user can:
* Create a simple UI without writing frontend code.
* Configure at least one REST API endpoint.
* Pass values from UI components into the API request.
* Display the API response in another UI component.
* Save and reload the project.
* Use IBM Bob/watsonx to generate or improve part of the UI/API configuration.
* Demonstrate a working Concert/RIA workflow launcher.
## Web Application Requirement
The system shall be delivered as a web-based application.
Users shall access the builder through a browser. No desktop client shall be required.
The application shall include:
* A browser-based frontend UI
* A backend API service
* Persistent project storage
* A secure server-side API proxy for calling external REST endpoints
## Web Server / Hosting Requirement
The system should not implement its own production-grade web server.
Instead, it should run behind a standard web server or reverse proxy such as:
* NGINX
* Apache HTTP Server
* Caddy
* IBM-approved internal hosting infrastructure
The application backend may include an embedded development server for local testing, but production deployment should use an external reverse proxy.
The reverse proxy should handle:
* HTTPS termination
* Static frontend asset delivery
* Routing requests to the backend service
* Request size limits
* Basic security headers
* Optional access restrictions
Recommended deployment model:
```text
Browser
NGINX / Apache / Caddy / IBM-approved reverse proxy
Frontend static assets + Backend API service
REST API endpoints / Concert / RIA / other systems
```
The backend service should focus on application logic, project storage, authentication handling, secret management, REST API proxying, and AI integration.
## Backend Persistence Requirement
The backend shall include persistent storage for project definitions and runtime metadata.
For the MVP, SQLite is the preferred database.
SQLite should store:
* Projects
* Pages
* UI components
* Component layout data
* REST API action definitions
* Component-to-component bindings
* Component-to-API bindings
* Non-secret configuration
* Basic execution history
* Error/debug logs
Secrets should not be stored directly in plain text in SQLite.
The database should be treated as local application state for the MVP, not as an enterprise shared data platform.
## Recommended MVP Database
The MVP should use SQLite because it is:
* Simple to deploy
* Easy to back up
* Suitable for single-instance usage
* Lightweight
* Good enough for project metadata and configuration storage
* Easier to package with a demo or proof-of-concept
## Future Database Options
If the project grows beyond MVP, the backend should be designed so SQLite can later be replaced by a full RDBMS such as PostgreSQL.
A full RDBMS may be required if the system needs:
* Multiple concurrent users
* Team collaboration
* Role-based access control
* High availability
* Centralized deployment
* Enterprise backup/restore
* Larger execution history
* Reporting or analytics
* Strict audit retention

300
ROADMAP.md Normal file
View File

@ -0,0 +1,300 @@
# Conductor Unified Roadmap to v0.1.0
## Purpose
This is the master index of completed and remaining work for Conductor v0.1.0. It supersedes the older numbered-step roadmap and legacy task inventory while reconciling `MVP_SCOPE.md` and `SLICE1.md` through `SLICE8.md`.
The slice files remain the detailed implementation plans. When an older list conflicts with the approved MVP boundary or current acceptance evidence, this document follows `MVP_SCOPE.md`, the slice files, and recorded manual acceptance in that order.
## Status Legend
- [x] Implemented and accepted, or a completed product decision.
- [ ] Remaining v0.1.0 work.
- **Partial** means useful work has passed, but the slice-level release gate is not complete.
- **Post-MVP** means deliberately excluded from v0.1.0.
## Slice Summary
| Slice | Function | Status | Remaining gate |
| --- | --- | --- | --- |
| [Slice 1](SLICE1.md) | MVP GUI components | Complete | None |
| [Slice 2](SLICE2.md) | Visual configuration | Complete | None |
| [Slice 3](SLICE3.md) | Authentication and secrets | Not started | Entire slice |
| [Slice 4](SLICE4.md) | Proxy security and observability | Not started | Entire slice |
| [Slice 5](SLICE5.md) | Validation and error handling | Not started | Entire slice |
| [Slice 6](SLICE6.md) | Testing and release validation | Partial | Release-critical coverage, end-to-end workflows, security regression, and final evidence |
| [Slice 7](SLICE7.md) | MVP scope decision | Complete | Cross-document reconciliation is carried by Slices 6 and 8 |
| [Slice 8](SLICE8.md) | Documentation and release packaging | Partial | Unified roadmap complete; broader documentation ownership, reconciliation, guides, and release packaging remain |
## 1. MVP GUI Components
### Slice 1 — Complete
- [x] Implement all eleven schema-supported MVP components in the palette, canvas, property editor, Preview, bindings, diagnostics, examples, and tests.
- [x] Keep interactive values in ephemeral runtime state and canonical properties in project JSON.
- [x] Complete manual canvas, Preview, JSON synchronization, save/load, and regression acceptance.
### Slice 2 — Remaining property and presentation work
- [x] Complete the audited component-specific property controls and basic styling controls required by the MVP specification.
- [x] Manually confirm JSON Viewer defaults, Table column widths, required inputs, and basic appearance authoring in Visual Editor and Preview.
- [x] Make the white Preview page/canvas background grow with runtime-rendered output, including Tables that exceed their configured design-time height.
- [x] Manually confirm the rebuilt Preview background expands and contracts with runtime-populated Table content.
### Post-MVP
- [ ] Add tabs, modal dialogs, date pickers, file uploads, charts, and progress indicators.
- [ ] Add reusable themes and component libraries.
- [ ] Add editable Table cells, sorting, filtering, pagination, CSV import/export, row actions, and multi-selection.
Table pagination requires a separate client-side versus server-side design discussion and is not a v0.1.0 release requirement.
## 2. Visual Configuration Workflows
### Slice 2 — Implemented and accepted increments
- [x] Create, edit, duplicate, test, and reference-safely delete anonymous REST actions through the UI.
- [x] Edit action method, URL, headers, query parameters, path parameters, and body templates in canonical `project.actions`.
- [x] Configure Button `onClick` action events visually.
- [x] Insert component and variable request values into executed request templates.
- [x] Create, edit, and delete canonical top-level response bindings.
- [x] Target supported component properties and declared runtime variables.
- [x] Create and edit typed variable declarations and defaults.
- [x] Configure page `onLoad` actions and execute them once per Preview initialization.
- [x] Warn before deleting referenced actions, variables, and components while preserving intentionally broken references for diagnostics.
- [x] Keep Visual Editor, JSON Editor, persistence, and Preview runtime state separated correctly for the accepted increments.
These items replace the older roadmap's pending Steps 19 and 20. They are not new remaining work.
### Slice 2 — Remaining release work
- [x] Run and record the final workflow-launcher acceptance without hand-editing JSON.
- [x] Run and record the final dependent-data acceptance, including initial Dropdown population.
- [x] Mark the five Slice 2 acceptance criteria complete after those workflows pass.
- [x] Retain and document the fixed 30-second backend request timeout for v0.1.0; defer a bounded canonical per-action timeout to proxy-policy work.
- [x] Correct action and response-binding diagnostics so page `onLoad` counts as a valid trigger and the UI uses clear component-or-page-event wording.
- [x] Manually confirm the corrected Actions & Bindings view no longer reports false untriggered-action or binding warnings for page `onLoad`.
- [x] Manually accept the implemented Actions & Bindings information-architecture improvements for clearer action, binding, and variable cards, hierarchy, spacing, and summaries.
- [x] Finish Slice 2's consolidated tests, examples, documentation reconciliation, and explicit sign-off.
### Slice 2a — Request-reference usability
- [x] Implement contextual component and variable suggestions in template-capable request fields, including after typing `{{`.
- [x] Suggest common REST header names while preserving arbitrary custom header names.
- [x] Align the guided request destination with request keys and the value source with request values.
- [x] Complete proportional manual acceptance.
- [x] Obtain explicit final Slice 2a sign-off.
### Post-MVP
- [ ] Add controlled action chaining or orchestration only after a new canonical design and scope decision.
- [ ] Add advanced transforms, JSONPath or another expression language, conditions, branches, parallel actions, loops, retries, and general workflow graphs.
The older roadmap placed controlled action orchestration in v0.1.0. That conflicts with `MVP_SCOPE.md` and `SLICE2.md`, which explicitly avoid turning Conductor into a workflow engine. It is therefore post-MVP unless the product owner deliberately changes the approved release boundary.
## 3. Authentication and Secrets
### Slice 3 — Design and storage
- [x] Define the MVP threat assumptions and secret-reference model.
- [x] Select and document the server-side secret storage and deployment mechanism.
- [ ] Define secret ownership, creation, replacement, deletion, missing-reference, and restart behavior.
- [x] Add safe secret create, update, lookup, and delete APIs.
- [x] Ensure canonical project JSON, exports, project CRUD responses, and browser state use opaque references rather than credential values.
### Slice 3 — Authentication execution
- [x] Preserve Anonymous execution.
- [x] Implement Basic authentication injection.
- [x] Implement Bearer token injection.
- [x] Implement API-key header injection.
- [x] Implement API-key query-parameter injection.
- [x] Reject missing or incompatible credential references with structured errors.
### Slice 3 — Redaction and validation
- [x] Redact credentials from logs, errors, diagnostics, history, Inspector output, proxy responses, and URLs.
- [x] Test all five modeled authentication modes with controlled endpoints.
- [x] Confirm secrets never appear in saved JSON, exports, browser-visible traffic, frontend state, or unsanitized logs.
- [x] Verify Docker secret provisioning and restart behavior.
- [x] Complete proportional manual secret lifecycle, compatible-selection, execution, redaction, and deletion-safety acceptance.
- [x] Obtain explicit final Slice 3 sign-off.
OAuth 2.0, IBM Cloud IAM, mTLS, and arbitrary authentication scripts remain post-MVP.
## 4. Proxy Security and Observability
### Slice 4 — Destination and request policy
- [ ] Document the proxy threat model and default-deny boundaries.
- [ ] Define permitted schemes, origins, hosts, ports, and explicit internal-host exceptions.
- [ ] Block unsafe local, link-local, metadata-service, and unapproved destinations.
- [ ] Revalidate DNS results and redirect targets at every boundary.
- [ ] Permit only HTTP and HTTPS.
- [ ] Allowlist forwarded headers and strip dangerous or hop-by-hop headers.
- [ ] Add request-size, response-size, redirect, and execution-time limits.
- [ ] Standardize safe proxy errors and malformed-response behavior.
### Slice 4 — Sanitized execution history
- [ ] Persist timestamp, project/action identity, method, sanitized URL, status, duration, outcome, sanitized error, and bounded response-preview metadata.
- [ ] Mask authorization headers, passwords, tokens, API keys, cookies, credential-resolution details, and sensitive query values.
- [ ] Add an execution-history view with recent executions, action/status filtering, useful errors, and no backend stack traces.
- [ ] Decide retention, persistence, restart, and clear-history behavior.
- [ ] Add successful, failed, redirect, timeout, size-limit, SSRF, redaction, and regression tests.
- [ ] Verify proxy policy through Docker Compose.
Enterprise SIEM integration, RBAC, and enterprise audit retention remain post-MVP.
## 5. Validation and Error Handling
### Slice 5 — Canonical validation and persistence
- [ ] Inventory validation behavior across JSON Apply, create, update, load, save, and Preview.
- [ ] Centralize backend schema validation and use it at every persistence boundary.
- [ ] Reject invalid projects without partial database updates.
- [ ] Add supported schema-version and compatibility behavior.
- [ ] Decide how invalid stored JSON is handled and whether v0.1.0 needs migrations.
- [ ] Verify explicit `null`, missing values, empty arrays, and empty objects round-trip correctly.
- [ ] Confirm backend default projects are canonical and schema-valid.
### Slice 5 — Semantic validation and diagnostics
- [ ] Validate duplicate IDs/names and dangling action, component, variable, event, and binding references.
- [ ] Validate component-specific properties and response-binding target compatibility.
- [ ] Standardize backend error codes, paths, severities, and response shapes.
- [ ] Surface actionable errors consistently in Visual Editor, Actions & Bindings, JSON Editor, Inspector, and Preview.
- [ ] Add Preview loading, empty, upstream-error, mapping-error, retry/recovery, and stale-error behavior for all supported targets.
- [ ] Preserve unsaved edits when validation or network operations fail.
- [ ] Clear stale Secrets lifecycle error feedback after a later successful create, replace, or delete operation.
- [ ] Reconcile schema descriptions and examples with canonical top-level `project.bindings`.
- [ ] Reconcile free-form binding triggers with the triggers the runtime actually supports.
- [ ] Unify or explicitly document the path-parameter interpolation contract.
- [ ] Prevent new uses of deprecated `action.responseMapping`, inert `ComponentEvent.inputMap`, and unused component-level binding shapes.
- [ ] Resolve, remove, document, or explicitly defer every modeled-but-unexecuted field, including `Binding.transform`.
- [ ] Strengthen `project.settings` TypeScript typing to match the schema.
- [ ] Decide whether backend-supported project deletion requires a v0.1.0 UI control.
- [ ] Review deletion confirmations, keyboard accessibility, component disabled/hidden behavior, and Inspector/runtime diagnostic agreement.
- [ ] Add malformed, semantic, version, rollback, and recovery regression tests.
## 6. Testing and Release Validation
### Slice 6 — Completed baseline work
- [x] Document and validate the Node 20/npm 10 toolchain.
- [x] Establish frontend, backend, schema, build, and Docker validation commands.
- [x] Validate all current valid and intentionally invalid examples.
- [x] Produce successful frontend and backend builds.
- [x] Verify Docker Compose startup, health, restart, CRUD, and persistence at recorded checkpoints.
### Slice 6 — Remaining automated coverage
- [ ] Define the release-critical test layers and coverage boundary.
- [ ] Complete frontend component, property-editor, action, event, binding, variable, and Preview tests.
- [ ] Add backend health, CRUD, schema-validation, proxy-input, authentication, security, and persistence tests.
- [ ] Add proxy integration tests for success, upstream errors, malformed responses, redirects, limits, and timeouts.
- [ ] Add canonical-document persistence round-trip tests.
- [ ] Add maintainable end-to-end tests for project creation, visual configuration, execution, response mapping, save, reload, and failure recovery.
- [ ] Resolve the TypeScript 4.9 / `@types/node` incompatibility so the standalone frontend TypeScript check passes, or update the supported toolchain deliberately.
- [ ] Run the final security regression suite after Slices 35.
### Slice 6 — Six required MVP workflows
- [ ] Workflow Launcher: visually build, execute, save, reload, and rerun a request/response UI.
- [ ] Dependent Data: populate a component from an API and use its selected value to update another component with usable loading, empty, and failure states.
- [ ] Read-Only Dashboard: populate a Table, select a row, show details, refresh, and handle errors.
- [ ] Authenticated Request: execute all five authentication modes without exposing credentials.
- [ ] JSON Editing and Persistence: validate/apply JSON, synchronize the Visual Editor, and round-trip without structural or behavioral loss.
- [ ] Failure Handling: show actionable request/mapping errors without corrupting canonical state or losing unsaved work.
### Slice 6 — Deterministic demonstration and release gate
- [ ] Build a deterministic local demonstration project using Label, Text Input, Dropdown, Button, JSON Viewer, and Table.
- [ ] Populate Dropdown and Table data, use selected/runtime values in later requests, and display both focused and full responses.
- [ ] Use a controlled local mock server with known success, error, Dropdown, and Table responses; do not depend on public HTTPBin-style services.
- [ ] Demonstrate save, backend restart, reload, and rerun.
- [ ] Run clean-checkout installation, all automated suites, schema matrix, production builds, Docker integration, restart, backup, and restoration checks.
- [ ] Confirm runtime state is never persisted and the repository contains no credentials or generated data.
- [ ] Record commands, counts, artifacts, limitations, defects, and accepted risks.
- [ ] Confirm no unresolved critical or high-severity defects remain.
- [ ] Publish and pass the final acceptance checklist.
Controlled orchestration is not required in the v0.1.0 demonstration. Execution history is included only if Slice 4 retains it as a release requirement.
## 7. MVP Scope and Requirement Governance
### Slice 7 — Completed decisions
- [x] Exclude IBM Bob, watsonx, and other AI dependencies from v0.1.0.
- [x] Require all five modeled authentication modes for v0.1.0.
- [x] Approve the six acceptance workflows in `MVP_SCOPE.md`.
- [x] Keep advanced orchestration and future UI capabilities outside the MVP boundary.
### Slices 6 and 8 — Remaining consistency work
- [ ] Make `MVP_SCOPE.md`, requirements, architecture, tasks, roadmap, and slice terminology consistent.
- [ ] Ensure every v0.1.0 requirement maps to a slice and a validation criterion.
- [ ] Clearly label all deferred capabilities post-MVP.
- [ ] Remove or resolve every open question that could materially change the release boundary.
- [ ] Produce a definitive Slice 6 release checklist and record product-owner approval.
## 8. Documentation, Packaging, and Release
### Slice 8 — Documentation ownership and reconciliation
- [x] Create and link a unified v0.1.0 roadmap grouped by function and slice.
- [x] Designate `docs/` as the authoritative specification location.
- [x] Remove exact duplicate root requirements, architecture, and future-idea documents after verifying links and replacements.
- [ ] Reconcile requirements and architecture with the approved MVP scope.
- [ ] Update schema and response-mapping documentation to match canonical runtime behavior.
- [ ] Reconcile `ROADMAP.md`, `CODEX.md`, and all slice handoffs as implementation progresses.
### Slice 8 — User and operator documentation
- [ ] Update README prerequisites, commands, and documentation index.
- [ ] Publish installation, Docker Compose, first-project, Visual Editor, JSON Editor, component, REST Action, template, variable, binding/event, and troubleshooting guides.
- [ ] Publish authentication, secret provisioning, redaction, proxy policy, security, known-limitations, and deployment guidance.
- [ ] Document SQLite backup, persistence, recovery, upgrade, and schema-version policy.
### Slice 8 — Developer documentation
- [ ] Publish architecture, canonical model, frontend state, runtime state, binding flow, proxy flow, database, test structure, mock-server, and contribution documentation.
- [ ] Publish verified build, test, Docker, and release commands.
- [ ] Publish the final acceptance checklist with links to evidence.
- [ ] Check internal links, paths, examples, and documented commands.
### Slices 6 and 8 — v0.1.0 packaging
- [ ] Review examples, demo, open issues, deferred work, and release criteria.
- [ ] Complete fresh-clone validation and final Docker images.
- [ ] Prepare release notes and update version references.
- [ ] Commit the release documentation and code.
- [ ] Tag `v0.1.0` and push the release commit and tag.
## Post-MVP Backlog
- [ ] Multi-page application authoring and advanced page management.
- [ ] OAuth 2.0, IBM Cloud IAM, mTLS, and custom authentication scripts.
- [ ] OpenAPI import and generated forms/actions.
- [ ] Reusable templates, themes, and component libraries.
- [ ] Version history, Git integration, and team collaboration.
- [ ] Role-based access control and enterprise audit retention.
- [ ] Tabs, modals, date pickers, file uploads, charts, and progress indicators.
- [ ] Table sorting, filtering, pagination, editing, CSV import/export, row actions, and multi-selection.
- [ ] Advanced response transforms and expression languages.
- [ ] Action chaining, conditions, branches, parallelism, loops, retries, and workflow graphs.
- [ ] Provider-neutral AI editing or chat assistance.
- [ ] Richer variable-management UI beyond the MVP declaration/default editor.
## Reconciliation Notes
- Older Step 19 (REST Action Manager) and Step 20 (Binding and Event Editor) are represented by accepted Slice 2 work, not remaining milestones.
- Older Step 21 maps to Slice 3.
- Older Step 23 maps to Slice 4.
- Older Step 24 maps to Slice 6, minus the conflicting orchestration requirement.
- Older Step 25 is divided among Slices 4, 5, and 6.
- Older Step 26 maps primarily to Slice 8, with the final validation gate in Slice 6.
- Older audit findings that are already resolved—page `onLoad` execution and the schema/runtime component mismatch—must not remain listed as pending.
- The older roadmap's completed persistence checkpoint records working functional round trips; comprehensive automated persistence round-trip coverage remains a Slice 6 release task.
- Slice 7's status is complete because its product decisions were approved. Its unchecked cross-document consistency criteria are carried forward under Slices 6 and 8 rather than treated as a new scope decision.
- Passing an incremental automated suite does not complete a slice. Manual acceptance and final release gates remain separately recorded.

View File

@ -1,144 +0,0 @@
# Component Deletion Safety Test Plan
## Purpose
Verify that deleting a referenced component is safe, warns before damage, and leaves useful diagnostics when deletion is confirmed.
Use a disposable project. Save it before each deletion test so the working version can be restored without rebuilding it.
## Test 1: Table Referenced by a Response Binding
### Setup
1. Confirm the project contains a Table named `postsTable`.
2. In **Actions & Bindings**, confirm a response binding targets `postsTable (Table.rows)`.
3. Save the project.
Why: deleting `postsTable` would break a real canonical response-binding target.
### Attempt deletion
1. Open **Visual Editor**.
2. Select `postsTable`.
3. Select the Table's red **x**.
Expected warning:
- The UI says `postsTable` is referenced.
- It identifies the affected response binding.
- It offers **Cancel** and **Delete anyway**.
### Stop condition
If the Table disappears immediately without a warning, stop this test and record:
> Referenced Table deleted without a warning.
This confirms the deletion-warning implementation gap. Continue only with **Post-deletion diagnostic** below.
### Cancel path
If a warning appears:
1. Select **Cancel**.
2. Confirm `postsTable` remains on the canvas.
3. Confirm the response binding remains unchanged.
Why: cancellation must not partially modify the component or binding.
### Confirmed deletion path
1. Attempt the deletion again.
2. Select **Delete anyway**.
3. Confirm only `postsTable` was removed.
Why: intentional deletion must remain possible without changing unrelated records.
### Post-deletion diagnostic
1. Open **Actions & Bindings**.
2. Find the binding that targeted `postsTable`.
3. Confirm it reports that target component `postsTable` is missing.
4. Open **JSON Editor** and confirm:
- `postsTable` is absent from `page.components`.
- The binding still targets `components.postsTable.rows`.
- Unrelated actions, bindings, variables, and components are unchanged.
Why: broken references must remain visible and diagnosable rather than being silently removed.
### Restore
1. Do not save the broken project.
2. Use **Load** to reopen the saved project.
3. Confirm `postsTable` and its binding return.
4. Enter **Preview** and confirm page-load population still works.
## Test 2: Text Input Referenced by a Request Template
### Setup
1. Add a Text Input named `itemInput`.
2. In **Actions & Bindings**, edit or create a disposable GET action.
3. Add this query parameter:
- Key: `item`
- Value: `{{components.itemInput.value}}`
4. Save the action and project.
Why: the action now depends on `itemInput` when rendering its request.
### Attempt deletion
1. Open **Visual Editor**.
2. Select `itemInput`.
3. Select its red **x**.
Expected warning:
- The UI says `itemInput` is referenced by an action request template.
- It offers **Cancel** and **Delete anyway**.
If no warning appears, record:
> Referenced Text Input deleted without a warning.
### Diagnostic after confirmed deletion
1. Open **Actions & Bindings**.
2. Inspect the affected action.
3. Confirm it reports that the template references missing component `itemInput`.
4. Confirm unrelated configuration remains unchanged.
5. Reload the saved project to restore the working version.
## Results
- [ ] Referenced Table deletion produced a warning. **Failed: the Table was deleted immediately without a warning.**
- [ ] The Table warning identified the affected binding. **Not applicable: no warning appeared.**
- [ ] Cancel preserved the Table and binding. **Not applicable: no warning appeared.**
- [ ] Delete anyway removed only the Table. **Not applicable: deletion was immediate.**
- [x] The broken binding produced a missing-component diagnostic.
- [ ] Referenced Text Input deletion produced a warning. **Failed: itemInput was deleted immediately without a warning.**
- [ ] The Text Input warning identified the affected request template. **Not applicable: no warning appeared.**
- [x] The broken template produced a missing-component diagnostic.
- [x] Reload restored the saved working project.
Tester/date: User, 2026-07-29 through 2026-07-30
Notes or defects: Deleting postsTable did not show a reference warning even though binding_response_1 targeted components.postsTable.rows. After deletion, Actions & Bindings correctly retained the binding and reported component postsTable not found / Target references component postsTable which does not exist on any page. Deleting itemInput also produced no warning, but the retained action correctly diagnosed the missing component template afterward. After restoration, the query row correctly placed the template in the value field; queryParameters.key identifies the query parameter named key rather than the editor's key field. The separate action-trigger warning remains a false positive because it ignores the valid page onLoad event.
## Implementation Retest
The missing warning is now implemented and automated-tested. Repeat these checks in the rebuilt app:
- [x] Delete referenced `postsTable`; the dialog lists its response binding.
- [x] Choose **Cancel**; the Table and binding remain.
- [x] Try again and choose **Delete anyway**; only the Table is removed, and the retained binding reports the missing component.
- [x] Delete referenced `itemInput`; the dialog lists the action request-template reference.
- [x] Choose **Cancel**; the Text Input and action remain.
- [x] Try again and choose **Delete anyway**; only the Text Input is removed, and the retained action reports the missing component.
- [x] Delete an unreferenced disposable component; it is removed immediately without an unnecessary dialog.
Why: this proves both safe interruption and intentional deletion work, while broken references remain visible instead of being silently erased.
Automated checkpoint on 2026-07-30: focused coverage passed at 4 suites / 12 tests, the full frontend suite passed at 20 suites / 490 tests, and the production build passed. The user manually confirmed every implementation-retest check passed on 2026-07-30.

271
SAVE_073026.md Normal file
View File

@ -0,0 +1,271 @@
# Conductor Save Point — 2026-07-30
## Purpose
Resume Conductor from the current working tree at `/home/vwiebe/projects/conductor` and finish Slice 2 before starting Slice 2a or Slice 3.
Do not commit or push without explicit authorization. Do not restore the deliberately removed root documents or overwrite unrelated documentation-consolidation changes. Automated validation and manual acceptance must remain separately reported.
## Resume Checklist
Read, in order:
1. `SAVE_073026.md`
2. `ROADMAP.md`
3. `SLICE2.md`
4. `SLICE2a.md`
5. `CODEX.md`
6. `TESTING.md`
7. `BASELINE.md`
8. `MVP_SCOPE.md`
Then run:
```bash
git status --short --branch
git log --oneline -5
git diff --check
```
Expected pushed baseline:
- Branch: `main`
- `main` and `origin/main`: `86ca42a Add page-load actions and component deletion safeguards`
- Everything described below is intentionally uncommitted and unpushed.
## Working-Tree Boundaries
Preserve the intentional documentation consolidation already in the tree:
- `ROADMAP.md` is the master work index.
- `SLICE2a.md` is an untracked deferred-usability plan.
- Root duplicate specifications were removed in favor of authoritative `docs/` copies.
- `TASKS.md`, `NEXT_SESSION_PROMPT.md`, and `SAFETY_TEST_PLAN.md` were deliberately removed.
- Do not restore deleted files.
- Preserve all unrelated modified slice, README, baseline, scope, testing, and handoff documents.
`SLICE2a.md` now records, but does not implement:
- compatible component/variable suggestions in template-capable request fields, including after typing `{{`;
- improved source/destination alignment in Request value reference;
- common REST header-name suggestions such as `Accept` and `Content-Type`, while preserving custom header-name entry.
Do not begin Slice 2a until Slice 2 has final manual sign-off.
## Completed and Manually Accepted This Session
### Final workflow-launcher and dependent-data acceptance
Manual acceptance passed on 2026-07-30 and is recorded in `TESTING.md`, `SLICE2.md`, `BASELINE.md`, `CODEX.md`, and `ROADMAP.md`.
- The workflow launcher was built visually with an environment Dropdown, hostname Text Input, Submit Button, JSON Viewer, canonical component request templates, Button `onClick`, and an `onSuccess` response binding.
- Success echoed both runtime values.
- HTTP 503 handling was readable, withheld `onSuccess`, and cleared stale errors after retry.
- Page `onLoad` populated Dropdown options through `https://httpbingo.org/anything`.
- Populated, empty, failed, and recovered dependent-data states passed.
- Save/load and fresh Preview preserved canonical configuration and reset runtime state.
- No `inputMap`, `action.responseMapping`, secrets, responses, loading flags, errors, selected runtime values, or loaded runtime options were persisted.
### Page-onLoad diagnostics correction
Implemented and manually accepted:
- `frontend/src/components/ActionInspector/ActionInspector.tsx` now passes pages into diagnostics.
- Page-event action IDs and component-event action IDs both count as valid triggers.
- Wording now says “component or page event” and recommends supported component events or page `onLoad`.
- Page `onLoad` suppresses both the false untriggered-action information diagnostic and false binding warning.
- A genuinely untriggered action still reports the revised diagnostic.
- Existing component-trigger behavior remains valid.
Automated evidence:
- Focused ActionInspector: 1 suite / 11 tests passed.
- Complete frontend at that checkpoint: 20 suites / 493 tests passed.
- Production build passed.
The first browser check showed the old bundle; a hard refresh loaded the rebuilt frontend and manual acceptance passed.
### Runtime-aware Preview canvas sizing
Implemented and manually accepted:
- `frontend/src/components/Preview/Preview.tsx` observes rendered component-wrapper dimensions.
- The white Preview page grows when runtime content, especially a populated Table, exceeds design-time bounds.
- It can contract back to design bounds after content becomes empty or smaller.
- Measurements remain runtime-only and do not change canonical component positions or sizes.
Manual acceptance confirmed populated rows stay inside the white page, the expanded page remains scrollable, an empty result contracts to a usable empty state, and canonical Table `size.height` remains unchanged.
## Current Implemented Increment — Manual Acceptance Pending
The remaining audited MVP component-property and basic-appearance controls are implemented, automated validation passes, Docker has been rebuilt, and proportional manual acceptance is the current gate.
### Implemented behavior
- JSON Viewer exposes **Default JSON** in the Visual Editor. Preview formats and displays the configured default before a runtime response arrives.
- Table column definitions expose an optional numeric **Width**. Canvas and Preview render configured column widths.
- Dropdown now exposes **Required** alongside Text Input and Text Area; Preview renders native required semantics for those three controls.
- Canonical `properties.style` supports:
```json
{
"fontSize": 20,
"textColor": "#112233",
"backgroundColor": "#ddeeff"
}
```
- The shared schema permits font sizes from 8 through 72 and six-digit hexadecimal colors.
- **Basic appearance** authoring is available for selected components and applies to Visual Editor component content and Preview.
- Reset controls remove individual color overrides; selecting the default font size removes the font-size override.
- Absence of style overrides preserves existing component rendering.
- This is per-component basic styling only. It does not introduce themes or post-MVP styling systems.
### Files materially changed by this increment
- `frontend/src/components/VisualEditor/VisualEditor.tsx`
- `frontend/src/components/VisualEditor/Canvas/CanvasComponent.tsx`
- `frontend/src/components/VisualEditor/Canvas/CanvasComponent.module.css`
- `frontend/src/components/Preview/PreviewComponent.tsx`
- `frontend/src/components/Preview/PreviewComponent.module.css`
- `frontend/src/components/Preview/mvpComponents.test.tsx`
- `frontend/src/types/project.ts`
- `shared/schemas/conductor-project.schema.json`
- Status/evidence documents listed above
### Current automated validation
All passed under the documented Node 20/npm 10 toolchain:
- Focused Preview property suite: 1 suite / 13 tests.
- Complete frontend suite: 20 suites / 500 tests, 0 snapshots.
- Frontend production build.
- Backend TypeScript build.
- Schema fixture matrix: 13 valid, 2 expected-invalid, 2 diagnostic-invalid.
- `git diff --check`.
- Docker Compose rebuild.
- Both Docker services running.
- Backend health returned `status: ok`.
Standalone frontend `tsc --noEmit` retains the existing TypeScript 4.9 / `@types/node@26.1.0` dependency incompatibility and is separate from the passing CRA production compile.
## Current Manual Acceptance Test
Hard-refresh `http://localhost:3000` before testing so the rebuilt development bundle is loaded.
Use a disposable project or the existing acceptance project. Do not hand-edit JSON to create the configuration being tested; JSON inspection is allowed to verify canonical synchronization.
### 1. JSON Viewer default
1. Add or select a JSON Viewer.
2. Enter valid JSON in **Default JSON**, for example:
```json
{"status":"ready","count":2}
```
3. Open Preview before any action populates the viewer.
4. Confirm Preview displays formatted configured JSON.
5. Inspect canonical JSON and confirm the value is stored only as the configured `defaultValue`.
Expected: configured default persists; no runtime response/loading/error fields are added.
### 2. Table column width
1. Add or select a Table with at least one column and row.
2. Set one columns **Width** to a recognizable value such as `180`.
3. Confirm the Visual Editor canvas uses that width.
4. Open Preview and confirm the same column width is used.
5. Inspect canonical JSON for the columns numeric `width`.
Expected: canvas, Preview, and canonical column configuration agree.
### 3. Required inputs
1. Add or select a Text Input, Text Area, and Dropdown.
2. Enable **Required** on each.
3. Open Preview and inspect or exercise the controls.
4. Confirm the rendered controls carry native required semantics.
5. Confirm no current input/selection value is persisted into canonical JSON.
Expected: required is canonical configuration; runtime values remain ephemeral.
### 4. Basic appearance
Test representative components: Label, Button, one input, Table, Status Panel, and Card.
For each representative component:
1. Set **Font size** to `20px`.
2. Set **Text** to a recognizable color.
3. Set **Background** to a contrasting recognizable color.
4. Confirm the Visual Editor canvas updates without changing the editors type badge/delete-control styling.
5. Open Preview and confirm the component content uses the same appearance.
6. Inspect canonical JSON and confirm only the selected component has the expected `properties.style` values.
Expected: style changes synchronize immediately across canonical JSON, canvas, and Preview without changing runtime state.
### 5. Reset and persistence
1. Reset text and background colors.
2. Select the default font size.
3. Confirm default rendering returns and the individual keys are removed from `properties.style`.
4. Save the project and reload it.
5. Confirm non-reset property and appearance configuration persists.
6. Open a fresh Preview and confirm runtime values, responses, loading, and errors are reset.
Expected: configuration persists; runtime state does not.
### Manual result boundary
Do not mark this increment accepted unless all five sections pass. Record partial results and defects separately if only some sections pass.
## How to Proceed After Power-On
1. Run the resume checklist and confirm no unexpected working-tree drift.
2. Run `docker compose ps` and verify backend health:
```bash
docker compose exec -T backend wget -qO- http://localhost:4000/api/health
```
3. If services are missing or source changed, run:
```bash
docker compose up --build -d
```
4. Hard-refresh the browser.
5. Execute the complete current manual acceptance test above.
6. If a defect is found:
- reproduce it narrowly;
- fix only the affected property/editor/canvas/Preview path;
- add focused automated coverage;
- run focused tests, the complete frontend suite, frontend production build, backend build if schema/types/backend-relevant code changed, and the 13/2/2 schema matrix;
- rebuild Docker and rerun the failed manual section plus proportional regressions.
7. If every section passes, update `ROADMAP.md`, `SLICE2.md`, `TESTING.md`, `BASELINE.md`, and `CODEX.md`, keeping manual acceptance distinct from automation.
8. Continue Slice 2 in this order:
1. Improve Actions & Bindings information architecture and readability with clearer cards, hierarchy, spacing, and summaries.
2. Decide whether v0.1.0 needs a canonical configurable per-action timeout or should document the existing fixed backend timeout.
3. Run consolidated Slice 2 automated validation.
4. Run final proportional manual acceptance for remaining changes.
5. Reconcile all Slice 2 status/evidence documents.
6. Stop for explicit manual sign-off before starting Slice 2a or Slice 3.
Do not add Table pagination or action orchestration; both are post-MVP. Do not begin broad visual redesign outside the scoped Actions & Bindings information-architecture task while functional Slice 2 work remains.
## Final Safety Checks Before Ending the Next Session
```bash
git diff --check
git status --short --branch
```
Report separately:
- implemented changes;
- automated validation;
- manual acceptance status;
- partial checkpoints or blockers;
- remaining Slice 2 work;
- commit/push state.

245
SAVE_073126.md Normal file
View File

@ -0,0 +1,245 @@
# Conductor Save Point — 2026-07-31
## Purpose
Resume Conductor at `/home/vwiebe/projects/conductor`, finish the remaining Slice 2a proportional manual step tests, record the results separately from automation, and obtain explicit Slice 2a sign-off before moving to another slice.
Do not commit or push without explicit authorization. Preserve the intentional documentation consolidation and all unrelated working-tree changes. Do not restore deliberately deleted root documents. Runtime state must remain separate from canonical project JSON.
## Resume Checklist
Read, in order:
1. `SAVE_073126.md`
2. `ROADMAP.md`
3. `SLICE2a.md`
4. `SLICE2.md`
5. `CODEX.md`
6. `TESTING.md`
7. `BASELINE.md`
8. `MVP_SCOPE.md`
Then run:
```bash
git status --short --branch
git log --oneline -5
git diff --check
docker compose ps
docker compose exec -T backend wget -qO- http://localhost:4000/api/health
```
Expected pushed baseline:
- Branch: `main`
- `main` and `origin/main`: `86ca42a Add page-load actions and component deletion safeguards`
- Everything described below remains intentionally uncommitted and unpushed.
## Working-Tree Boundaries
Preserve the existing documentation consolidation:
- `ROADMAP.md` is the master work index.
- `SLICE2a.md` owns the current request-reference usability increment.
- Authoritative requirement, architecture, and future-idea documents are under `docs/`.
- Root duplicate specifications and `TASKS.md`, `NEXT_SESSION_PROMPT.md`, and `SAFETY_TEST_PLAN.md` were deliberately removed.
- Do not restore deleted files or overwrite unrelated modified documentation.
Do not add path-parameter reference insertion, authentication/secret references, new template syntax, transforms, orchestration, or Table pagination. These remain outside Slice 2a.
## Slice 2 Status
Slice 2 is complete and explicitly signed off by the user on 2026-07-31.
Completed Slice 2 evidence includes:
- final workflow-launcher and dependent-data acceptance;
- page `onLoad` diagnostics correction;
- runtime-aware Preview canvas sizing;
- JSON Viewer defaults, Table column widths, required inputs, and basic appearance;
- Actions & Bindings information-architecture improvements;
- the documented fixed 30-second backend timeout contract;
- consolidated automated validation, Docker health, documentation reconciliation, and proportional manual acceptance.
Do not reopen Slice 2 unless a concrete regression is found.
## Current Slice 2a Implementation
Slice 2a is implemented, automated validation passes, Docker has been rebuilt, and proportional manual acceptance is in progress.
Implemented behavior:
- Endpoint URL, header-value, query-value, and body-template fields show compatible component and variable suggestions after an unfinished `{{` opener.
- Selecting a suggestion replaces the unfinished opener/partial expression while preserving preceding free-form text.
- Suggestions insert only the canonical executed forms:
- `{{components.<componentName>.value}}`
- `{{variables.<variableName>}}`
- Components with ambiguous duplicate names are excluded from contextual suggestions and the guided source list.
- Header-name inputs suggest common names including `Accept` and `Content-Type` while accepting arbitrary custom names.
- **Request value reference** now presents controls in this order:
1. **Request destination**
2. **Value source**
3. **Insert reference**
- Existing guided semantics remain unchanged: header/query destinations replace the selected value, while URL/body destinations append.
- Path parameters remain excluded from guided and contextual template insertion.
- Canonical request shapes and Preview runtime behavior are unchanged.
Material Slice 2a files:
- `frontend/src/components/ActionInspector/TemplateSuggestions.tsx`
- `frontend/src/components/ActionInspector/TemplateSuggestions.test.tsx`
- `frontend/src/components/ActionInspector/RestActionEditor.tsx`
- `frontend/src/components/ActionInspector/RequestInputEditor.tsx`
- `frontend/src/components/ActionInspector/RequestInputEditor.test.tsx`
- `frontend/src/components/ActionInspector/requestInputUtils.ts`
- `frontend/src/components/ActionInspector/requestInputUtils.test.ts`
- `frontend/src/components/ActionInspector/ActionInspector.module.css`
- `frontend/src/components/ActionInspector/ActionInspector.test.tsx`
- `SLICE2a.md`, `ROADMAP.md`, `TESTING.md`, `CODEX.md`, and other status/evidence documents
## Current Automated Validation
All passed under the documented Node 20/npm 10 toolchain:
- Focused Slice 2a request-authoring coverage: 4 suites / 23 tests.
- Complete frontend coverage: 21 suites / 507 tests, 0 snapshots.
- Frontend production build.
- `git diff --check`.
- Docker Compose rebuild.
- Both Docker services running.
- Backend health returned `status: ok`.
- Frontend returned HTTP 200.
Backend build and schema validation were not rerun for Slice 2a because this increment changes frontend authoring behavior only and preserves the canonical document shape. The earlier Slice 2 consolidated backend build and 13-valid / 2-expected-invalid / 2-diagnostic-invalid schema matrix passed.
Standalone frontend `tsc --noEmit` retains the existing TypeScript 4.9 / `@types/node@26.1.0` incompatibility and remains separate from the passing CRA production compile.
## Manual Acceptance Status
### Step 1 — Contextual suggestions: PASSED
The user confirmed the detailed contextual-suggestion procedure on 2026-07-31.
The accepted check used:
- a uniquely named Text Input such as `hostname`;
- a declared string variable such as `environment`;
- an Endpoint URL ending in an unfinished `{{` opener;
- component and variable suggestion visibility;
- selection of canonical component/variable templates;
- preservation of the text preceding `{{`;
- equivalent suggestion behavior in URL, header value, query value, and body template fields.
Do not repeat Step 1 unless a regression is suspected.
### Remaining manual tests
Four step tests remain. Give the user exact point-and-click instructions one step at a time. Do not combine them into a vague checklist. Wait for confirmation after each step.
#### Step 2 — Ambiguous duplicate component names
1. In **Visual Editor**, ensure one Text Input is named `hostname`.
2. Add a second Text Input and also name it `hostname`.
3. Return to **Actions & Bindings** and edit the REST action.
4. In a supported request value field, replace the value with exactly `{{`.
5. Confirm `Component: hostname` is absent from the contextual suggestion panel.
6. Open **Value source** under **Request value reference** and confirm `Component: hostname` is absent there too.
7. Confirm a valid declared variable such as `Variable: environment` remains offered.
8. Rename the second component to `hostnameSecondary`.
9. Return to the action, type `{{` again, and confirm both unique components are offered.
Pass: the ambiguous name is withheld in both suggestion mechanisms and reappears only after names become unique.
#### Step 3 — Common and custom header names
1. Edit a REST action and open **Request parameters**.
2. Under **Headers**, click **+ Add row**.
3. Focus the header-name field or begin typing.
4. Confirm `Accept` and `Content-Type` are offered by the header-name suggestions.
5. Enter the custom name `X-Custom-Vendor-Header` instead.
6. Enter any value, such as `test`.
7. Inspect canonical JSON and confirm the header is stored exactly as entered.
Pass: common names are suggested, custom entry remains possible, and the canonical header object is unchanged in shape.
#### Step 4 — Guided-control order and alignment
1. Edit a REST action containing a query parameter such as `environment`.
2. Locate **Request value reference**.
3. Confirm the visible order is **Request destination**, **Value source**, **Insert reference**.
4. In **Request destination**, select `Query: environment`.
5. In **Value source**, select `Variable: environment` or a unique component.
6. Confirm the destination control aligns with the request-key side of the query row and the value-source control aligns with the request-value side at the current viewport.
7. Click **Insert reference** and confirm the expected canonical template appears in the query value.
8. Narrow the browser window to a supported small width and confirm the controls stack clearly without overlap.
Pass: order, conceptual key/value alignment, responsive stacking, and insertion are clear.
#### Step 5 — Existing behavior regression
1. Use guided insertion into an existing header value and confirm the old value is replaced.
2. Use guided insertion into an existing query value and confirm the old value is replaced.
3. Use guided insertion into an Endpoint URL and confirm the template is appended.
4. Use guided insertion into a non-empty body template and confirm the template is appended.
5. Confirm path parameters are not offered as guided destinations and do not show contextual template suggestions.
6. After any insertion, type additional free-form text and confirm editing remains possible.
7. Inspect canonical JSON and confirm no `inputMap`, `action.responseMapping`, runtime values, responses, loading flags, or errors were added.
Pass: all previous replace/append behavior and canonical/runtime separation remain intact.
## After Manual Testing
If a defect is found:
1. Record the exact failed step and observed behavior.
2. Reproduce it narrowly.
3. Fix only the affected Slice 2a authoring path.
4. Add focused coverage.
5. Rerun the focused suites, complete frontend suite, frontend production build, `git diff --check`, Docker rebuild, and the failed manual step plus proportional regressions.
If Steps 25 pass:
1. Update `SLICE2a.md`, `ROADMAP.md`, `TESTING.md`, `BASELINE.md`, and `CODEX.md` with manual evidence separate from automation.
2. Run final `git diff --check`, `docker compose ps`, and backend health.
3. Ask the user for explicit final Slice 2a sign-off.
4. Do not commit or push without explicit authorization.
## Current Commit and Service State
- Branch: `main`, aligned with `origin/main` at `86ca42a`.
- The working tree is intentionally dirty with the documented consolidation, completed Slice 2 work, and current Slice 2a increment.
- Nothing from these increments has been committed or pushed.
- Docker frontend and backend services were running and healthy when this save point was created.
- `git diff --check` passed.
## Complete Continuation Prompt
Copy and paste the following prompt into a future session:
```text
Please resume Conductor from /home/vwiebe/projects/conductor using SAVE_073126.md as the authoritative current-session handoff.
Read SAVE_073126.md first, then follow its resume checklist. Preserve all intentional documentation consolidation and unrelated working-tree changes. Do not restore deliberately deleted root documents. Do not commit or push without my explicit authorization.
Slice 2 is complete and explicitly signed off. Slice 2a is implemented and automated validation passes. Manual Slice 2a Step 1 (contextual suggestions) has passed. Continue with the four remaining proportional manual tests, beginning with Step 2 (ambiguous duplicate component names).
Please give me exact, detailed, point-and-click instructions for only one manual step at a time, including concrete field names, example values, and precise expected results. Wait for my confirmation after each step before presenting the next one. If a test fails, reproduce and fix only the affected Slice 2a path, add focused coverage, and rerun proportional validation. If Steps 2 through 5 pass, reconcile SLICE2a.md, ROADMAP.md, TESTING.md, BASELINE.md, and CODEX.md, run final integrity and Docker health checks, and ask me for explicit Slice 2a sign-off.
```
## Final Safety Checks
Before ending the resumed session, report separately:
- implemented changes;
- automated validation;
- each manual step result;
- remaining blockers or gates;
- Docker/service state;
- commit/push state.
Always run:
```bash
git diff --check
git status --short --branch
```

View File

@ -4,6 +4,10 @@
Complete
## Roadmap Alignment
This slice owns the **MVP GUI Components** function in `ROADMAP.md`. It is complete and has no remaining v0.1.0 gate. Styling follow-ups and Preview output sizing belong to Slice 2; additional component types and advanced Table capabilities are post-MVP.
## Objective
Implement every GUI component required by the MVP so each schema-supported component can be added, configured, rendered, used in Preview, and tested.
@ -34,7 +38,7 @@ Implement every GUI component required by the MVP so each schema-supported compo
- [x] Add component-specific binding support and diagnostics.
- [x] Add or update example projects for all five components.
- [x] Remove the schema/runtime mismatch for supported component types.
- [x] Update `TASKS.md`, `CODEX.md`, and relevant documentation.
- [x] Update the roadmap, `CODEX.md`, and relevant documentation.
## Acceptance Criteria

View File

@ -2,7 +2,11 @@
## Status
In progress - visual configuration and page-load manual acceptance are complete; final Slice 2 acceptance remains
Complete - explicitly signed off on 2026-07-31
## Roadmap Alignment
This slice owns the **Visual Configuration Workflows** function and the remaining MVP component-property/presentation follow-ups in `ROADMAP.md`. The older roadmap's Steps 19 and 20 are implemented Slice 2 increments, not future milestones. Controlled action orchestration and Table pagination are post-MVP unless the approved boundary changes.
## Objective
@ -24,13 +28,14 @@ Allow an MVP project to be configured through the GUI without routine hand-editi
- AI-generated configuration
- Chained actions, general orchestration, and advanced JSONPath authoring
- Table pagination, sorting, filtering, editing, and other advanced Table behavior
## Tasks
- [x] Design a consistent, progressively disclosed workflow for editing actions, events, bindings, variables, and page-load behavior.
- [x] Implement REST Action list, create, edit, duplicate, and reference-aware delete operations.
- [x] Add method, URL, headers, query, path, and body controls for anonymous REST actions.
- [ ] Decide and implement the canonical timeout field before exposing a timeout control; the current schema and runtime use a fixed backend timeout.
- [x] Keep the fixed 30-second backend timeout as the documented v0.1.0 contract; defer a bounded canonical per-action timeout to proxy-policy work.
- [x] Keep Slice 2 authentication authoring anonymous-only and defer credential-backed authentication controls to Slice 3.
- [x] Implement component event configuration for Button `onClick`, including add/change/clear and dangling-reference diagnostics.
- [x] Implement request input authoring through executed REST request templates; do not write inert `ComponentEvent.inputMap`.
@ -40,7 +45,14 @@ Allow an MVP project to be configured through the GUI without routine hand-editi
- [x] Add variable declaration and typed default-value editing.
- [x] Warn before deleting referenced actions, variables, or components. Referenced component deletion requires explicit confirmation and preserves dangling references for diagnostics; manual acceptance passed on 2026-07-30.
- [x] Ensure every REST action visual edit immediately updates canonical JSON while invalid drafts remain local.
- [ ] Add slice-wide tests, examples, and documentation updates. Coverage, the representative fixture, and handoff docs are complete through page-load actions; final manual slice acceptance remains.
- [x] Correct action and response-binding diagnostics so page `onLoad` counts as a valid trigger and uses clear component-or-page-event wording.
- [x] Manually confirm the page `onLoad` action and binding no longer show false untriggered diagnostics.
- [x] Manually accept the implemented Actions & Bindings information architecture with clearer record boundaries, hierarchy, spacing, and summaries.
- [x] Make the Preview page background grow with runtime output that exceeds design-time component height.
- [x] Manually confirm runtime-populated Table rows remain inside the white Preview background and an empty result contracts it again.
- [x] Complete audited MVP component-specific properties: JSON Viewer default JSON, Table column widths, input required semantics, and canonical basic appearance.
- [x] Manually confirm those controls synchronize with JSON and render consistently on the canvas and in Preview.
- [x] Add slice-wide tests, examples, and documentation updates; reconcile evidence and receive explicit final Slice 2 sign-off.
## Implementation Order
@ -54,20 +66,20 @@ Each increment writes only canonical configuration into the shared project docum
## Acceptance Criteria
- [ ] A user can build the workflow launcher without editing JSON.
- [ ] Actions, inputs, triggers, and response mappings can be configured visually.
- [ ] Initial dropdown/table population can be configured visually.
- [ ] Dangling references are reported before Preview execution.
- [ ] Visual and JSON editors remain synchronized and save/load preserves configuration.
- [x] A user can build the workflow launcher without editing JSON.
- [x] Actions, inputs, triggers, and response mappings can be configured visually.
- [x] Initial dropdown/table population can be configured visually.
- [x] Dangling references are reported before Preview execution.
- [x] Visual and JSON editors remain synchronized and save/load preserves configuration.
## Validation
- [ ] Standalone frontend TypeScript check passes. TypeScript 4.9 currently cannot parse the resolved `@types/node@26.1.0`; the CRA production compile passes.
- [x] Frontend production build passes through the page-load increment.
- [x] Editor interaction and Preview dispatch tests pass through the page-load increment.
- [x] Frontend production build passes through the Actions & Bindings information-architecture increment.
- [x] Editor interaction and Preview dispatch tests pass through the information-architecture increment (20 suites / 501 tests; focused ActionInspector suite 12 tests; focused Preview component suite 13 tests).
- [x] The representative canonical JSON validates through the page-load increment.
- [x] Increment 1 manual REST action authoring and referenced-action deletion workflow passes.
- [ ] Manual workflow-launcher and dependent-data scenarios pass.
- [x] Manual workflow-launcher and dependent-data scenarios pass.
## Risks and Open Questions
@ -77,7 +89,7 @@ Each increment writes only canonical configuration into the shared project docum
## Progress Log
- 2026-07-18: Restored project state from `CODEX.md`, `TASKS.md`, `MVP_SCOPE.md`, and this plan.
- 2026-07-18: Restored project state from the then-current handoff and task documents, `MVP_SCOPE.md`, and this plan; `ROADMAP.md` now supersedes the legacy task inventory.
- Verified the clean `main` branch against a freshly fetched `origin/main`; both point to `b9697cc8b11bce806887d6e7a29098a308b0846a`.
- Re-ran the pre-change frontend baseline successfully: 7 suites and 433 tests passed.
- Audited the canonical action/event/binding model, Visual Editor, read-only Actions & Bindings Inspector, Preview execution path, schema validation, examples, and existing tests.
@ -92,11 +104,11 @@ Each increment writes only canonical configuration into the shared project docum
## Handoff
- Last completed: Visual page-load action authoring and Preview initialization, with automated validation.
- Next action: Complete final Slice 2 workflow-launcher and dependent-data acceptance. Track responsive Preview output sizing and Table pagination as explicit follow-ups.
- Last completed: Actions & Bindings information architecture and the fixed-timeout decision are implemented, validated, documented, and manually accepted.
- Next action: Begin the separately scoped Slice 2a request-reference usability work authorized after final Slice 2 sign-off.
- Known blockers: Standalone frontend `tsc` retains its recorded tooling limitation.
- Current pushed baseline: `9e4eb33 Add visual response bindings and variables`.
- Continuation and manual-test instructions: `NEXT_SESSION_PROMPT.md`.
- Current pushed baseline: `86ca42a Add page-load actions and component deletion safeguards`.
- Durable manual-test instructions and acceptance records: `TESTING.md`.
## Response Binding and Variable Increment
@ -135,4 +147,4 @@ Implemented in the working tree on 2026-07-29:
- Focused validation passes at 4 suites / 9 tests. The full frontend suite passes at 18 suites / 486 tests under Node 20/npm 10, the CRA production build passes, and the 13 / 2 / 2 schema matrix remains green.
- Backend code and schema were not changed. Standalone tsc --noEmit retains the documented TypeScript 4.9 / @types/node 26 incompatibility.
Manual page-load acceptance passed on 2026-07-29 and is recorded in TESTING.md. The screenshot exposed a non-blocking Preview page-sizing limitation for runtime-expanded Tables; responsive Preview output sizing and a separately designed Table pagination capability are tracked in TASKS.md. Final Slice 2 acceptance remains pending.
Manual page-load acceptance passed on 2026-07-29 and is recorded in TESTING.md. The screenshot exposed a non-blocking Preview page-sizing limitation for runtime-expanded Tables; responsive Preview output sizing and post-MVP Table pagination are tracked in `ROADMAP.md`. Final Slice 2 acceptance remains pending.

89
SLICE2a.md Normal file
View File

@ -0,0 +1,89 @@
# Slice 2a: Request Reference Usability
## Status
Complete - automated validation, proportional manual acceptance, and explicit final sign-off passed
## Objective
Make REST request-template authoring easier to discover and visually map while
preserving the existing canonical template format and guided insertion workflow.
## Compatible Reference Suggestions
- [x] Make template-capable request fields aware of compatible component and
declared-variable references.
- [x] Offer suggestions when a user begins entering a template, including after
typing `{{`, without requiring exact template syntax from memory.
- [x] Apply suggestions to endpoint URLs, header values, query-parameter values,
and request body templates where the existing runtime supports references.
- [x] Insert the canonical executed forms
`{{components.<componentName>.value}}` and `{{variables.<variableName>}}`.
- [x] Filter suggestions to references supported by the selected field and
current project configuration.
- [x] Preserve free-form template editing and the existing **Request value
reference** controls.
## Header Name Suggestions
- [x] Offer a pulldown of common HTTP header names, including **Accept** and
**Content-Type**, when authoring REST Action headers.
- [x] Keep free-form custom header-name entry available for backend-agnostic,
vendor-specific, and less common headers.
- [x] Preserve the existing canonical header object and runtime behavior.
## Source and Destination Alignment
- [x] Reorder the **Request value reference** controls so request destinations
align vertically with request keys and value sources align vertically with
request values.
- [x] Present the controls in this order: request destination, component or
variable source, then **Insert reference**.
- [x] Preserve the source and destination terminology and existing insertion
behavior.
- [x] Manually verify the alignment remains clear for URL, header, query, and body
destinations at supported viewport widths.
## Acceptance Criteria
- [x] Typing `{{` in a supported request field offers compatible component and
declared-variable suggestions.
- [x] Choosing a suggestion inserts valid canonical template syntax without
preventing subsequent free-form editing.
- [x] Unsupported or ambiguous references are not offered.
- [x] Header-name authoring suggests common HTTP headers while still accepting
arbitrary custom names.
- [x] A destination such as `Query: environment` is vertically aligned with the
`environment` request key below it.
- [x] A source such as `Component: environment` is vertically aligned with the
resulting `{{components.environment.value}}` request value below it.
- [x] Existing request-template execution and canonical/runtime-state separation
continue to pass automated and manual regression checks.
## Boundaries
- This work improves authoring only; it does not add new runtime template
destinations, expression syntax, transforms, or orchestration.
- Path-parameter reference insertion remains excluded until its runtime contract
is deliberately unified.
- Authentication and secret references remain owned by Slice 3.
## Automated Validation
- Focused request-authoring coverage: 4 suites / 23 tests passed.
- Complete frontend coverage: 21 suites / 507 tests passed, 0 snapshots.
- Frontend production build passed under the documented Node 20/npm 10 toolchain.
- Backend and schema validation were not rerun because Slice 2a changes only frontend authoring behavior and preserves the canonical document shape.
## Manual Acceptance
Proportional manual acceptance passed on 2026-08-01 in an isolated Chromium browser against project `Slice 2 Final` (`#12`) at `http://localhost:3000/`. The normal viewport was 1440 x 1100 and the responsive viewport was 700 x 1000. The saved project was not updated.
- Contextual component and variable suggestions passed across URL, header-value, query-value, and body-template fields.
- Duplicate `hostname` component names were withheld from contextual and guided suggestions while `Variable: statusText` remained available; `hostname` and `hostnameSecondary` became eligible after uniqueness was restored.
- Common header suggestions included `Accept` and `Content-Type`; `X-Custom-Vendor-Header` was preserved exactly with value `test` in the canonical `headers` object.
- Guided controls appeared as **Request destination**, **Value source**, and **Insert reference**, aligned semantically at 1440 x 1100 and stacked clearly without overlap at 700 x 1000.
- Header/query insertion replaced existing values, URL/body insertion appended, path parameters remained excluded, free-form editing remained available, and canonical JSON remained free of mapping and runtime state.
This evidence is manual acceptance, separate from the automated validation above. The user granted explicit final Slice 2a sign-off on 2026-08-01.

View File

@ -2,7 +2,11 @@
## Status
Not started
Complete - automated validation, proportional manual acceptance, and explicit final sign-off passed
## Roadmap Alignment
This slice owns the **Authentication and Secrets** function in `ROADMAP.md` and replaces the older roadmap's Step 21. Anonymous execution already exists; credential-backed modes, storage, lifecycle, redaction, and security validation remain.
## Objective
@ -25,47 +29,54 @@ Execute every MVP authentication type securely without exposing credentials to t
## Tasks
- [ ] Define the MVP secret-reference model and threat assumptions.
- [ ] Select and document the server-side secret storage mechanism.
- [ ] Ensure project JSON contains references and metadata only.
- [ ] Implement safe secret create, update, lookup, and delete APIs.
- [ ] Implement Basic authentication injection.
- [ ] Implement Bearer token injection.
- [ ] Implement API-key header injection.
- [ ] Implement API-key query-parameter injection.
- [ ] Reject missing or incompatible references with structured errors.
- [ ] Redact credentials from logs, errors, history, and frontend responses.
- [ ] Exclude secrets from export and project CRUD responses.
- [ ] Add authentication, failure, and redaction tests.
- [ ] Update state, deployment, and security documentation.
- [x] Define the MVP secret-reference model and threat assumptions.
- [x] Select and document the server-side secret storage mechanism.
- [x] Ensure project JSON contains references and metadata only.
- [x] Implement safe secret create, update, lookup, and delete APIs.
- [x] Implement Basic authentication injection.
- [x] Implement Bearer token injection.
- [x] Implement API-key header injection.
- [x] Implement API-key query-parameter injection.
- [x] Reject missing or incompatible references with structured errors.
- [x] Redact credentials from logs, errors, history, and frontend responses.
- [x] Exclude secrets from export and project CRUD responses.
- [x] Add authentication, failure, and redaction tests.
- [x] Update state, deployment, and security documentation.
## Acceptance Criteria
- [ ] All five modeled authentication modes execute as documented.
- [ ] Browser-visible traffic and exported JSON never contain stored credentials.
- [ ] Logs and errors redact sensitive values.
- [ ] Missing or invalid secret references fail safely and clearly.
- [ ] Secrets survive the intended MVP deployment lifecycle.
- [x] All five modeled authentication modes execute as documented.
- [x] Browser-visible traffic and exported JSON never contain stored credentials.
- [x] Logs and errors redact sensitive values.
- [x] Missing or invalid secret references fail safely and clearly.
- [x] Secrets survive the intended MVP deployment lifecycle.
## Validation
- [ ] Backend TypeScript check passes.
- [ ] Authentication and redaction tests pass.
- [ ] Controlled mock-endpoint checks pass.
- [ ] Docker Compose secret provisioning and restart checks pass.
- [x] Backend TypeScript check passes.
- [x] Authentication and redaction tests pass.
- [x] Controlled mock-endpoint checks pass.
- [x] Docker Compose secret provisioning and restart checks pass.
## Risks and Open Questions
- Plaintext secrets in SQLite require an explicit and acceptable MVP policy.
- The approved store uses AES-256-GCM rather than plaintext SQLite values; master-key loss makes credentials unrecoverable and rotation remains future work.
- Query API keys require special URL redaction.
- Secret ownership is limited in a single-user MVP.
## Progress Log
No work recorded yet.
- 2026-08-01: The user approved AES-256-GCM encryption in SQLite using a server-only 32-byte master key supplied through Docker/environment configuration. Canonical JSON will carry opaque secret references only.
- 2026-08-01: Added the encrypted secret table, metadata-only CRUD API, internal-only resolution path, structured fail-closed key configuration behavior, and `docs/SECRETS.md` threat/lifecycle contract.
- 2026-08-01: Backend TypeScript build passed. A controlled Docker lifecycle check created a disposable Bearer credential, returned metadata only, found no plaintext token in SQLite, preserved metadata across backend restart, deleted the record with HTTP 204, and confirmed HTTP 404 afterward. The disposable master key was removed from the running configuration after the check.
- 2026-08-01: Added canonical `secretReferenceId`, protected-mode authoring, server-side Basic/Bearer/API-key header/API-key query injection, type compatibility checks, and recursive response-body credential redaction. Controlled httpbingo checks returned HTTP 200 for all four modes while reflected Authorization, header-key, query-key, and URL values were `[REDACTED]`. Missing references and type mismatches returned structured HTTP 400 errors. Full frontend coverage passed at 21 suites / 507 tests, focused authoring coverage passed at 2 suites / 32 tests, and frontend/backend production builds passed.
- 2026-08-01: Added guided secret creation and replacement with masked credential inputs, metadata-only listing, compatible-secret selection for actions, and deletion protection for unsaved and saved project references. Added durable AES-256-GCM round-trip, missing-key, and wrong-key backend tests. Backend tests/build, 21 frontend suites / 507 tests, focused 2 suites / 32 tests, and the frontend production build passed.
- 2026-08-01: Extracted and tested all four injection modes, missing/not-found/type-mismatch failures, recursive reflected-value redaction, and conservative URL sanitization. Persistent-key Docker restart execution passed with reflected Authorization redacted. Consolidated validation passed at 22 frontend suites / 509 tests, backend security tests/build, frontend production build, and the 13 / 2 / 2 schema matrix. Proportional manual acceptance remains pending.
- 2026-08-01: Proportional manual acceptance passed Steps 1-4 in order in isolated Chromium using an unsaved browser copy of `Slice 2 Final` (`#12`). Masked create/replace lifecycle, metadata-only display, compatible guided selection, opaque-only canonical state, Bearer/header-key/query-key execution and redaction, referenced deletion protection, unreferenced cleanup, and missing-reference failure safety all passed. Saved project `#12` was unchanged and the final server-side secret list was empty. A stale blocked-deletion warning remained visible after successful cleanup; it was non-blocking and is tracked in `ROADMAP.md` for error-feedback cleanup.
- 2026-08-02: The user granted explicit final Slice 3 sign-off. Slice 3 is complete.
## Handoff
- Last completed: Slice plan created.
- Next action: Write the threat model and select the MVP secret store.
- Known blockers: Secret storage and deployment policy need an explicit decision.
- Last completed: Explicit final Slice 3 sign-off.
- Next action: Begin the next authorized roadmap slice.
- Known blockers: None for the approved design; master-key rotation is explicitly deferred.

View File

@ -4,6 +4,10 @@
Not started
## Roadmap Alignment
This slice owns **Proxy Security and Observability** in `ROADMAP.md`. It absorbs the older roadmap's Step 23 and the proxy-security portions of Step 25. Execution history is an MVP requirement here; orchestration history is not required because orchestration is post-MVP.
## Objective
Harden the REST proxy against unsafe destinations, header abuse, credential leakage, and resource exhaustion while retaining backend-agnostic REST support.

View File

@ -4,6 +4,10 @@
Not started
## Roadmap Alignment
This slice owns **Validation and Error Handling** in `ROADMAP.md` and the canonical-model/frontend-hardening portions of the older Step 25. Page `onLoad` execution and the schema/runtime component mismatch are already resolved and must not return as pending audit findings.
## Objective
Reject invalid project definitions consistently and explain editor, persistence, and Preview failures clearly without losing user work.
@ -38,6 +42,12 @@ Reject invalid project definitions consistently and explain editor, persistence,
- [ ] Add Preview loading, empty, upstream-error, mapping-error, and recovery states.
- [ ] Preserve unsaved edits when validation or network operations fail.
- [ ] Reconcile examples with canonical binding behavior.
- [ ] Reconcile free-form binding triggers with runtime-supported triggers.
- [ ] Unify or explicitly document path-parameter interpolation.
- [ ] Resolve, remove, document, or defer modeled-but-unexecuted fields such as `Binding.transform`, `ComponentEvent.inputMap`, and component-level bindings.
- [ ] Strengthen `project.settings` TypeScript typing to match the schema.
- [ ] Decide whether backend-supported project deletion requires a v0.1.0 UI control.
- [ ] Review deletion confirmations, keyboard accessibility, disabled/hidden component behavior, and Inspector/runtime agreement.
- [ ] Add malformed, semantic, version, and recovery regression tests.
- [ ] Update state and validation documentation.

View File

@ -4,6 +4,10 @@
In progress
## Roadmap Alignment
This slice owns **Testing and Release Validation** in `ROADMAP.md`, including the deterministic MVP demonstration and final release gate. It absorbs the older roadmap's Step 24 and validation portions of Steps 25-26. Functional persistence has passed manual checkpoints; comprehensive automated persistence round-trip coverage remains pending.
## Objective
Produce repeatable automated and manual evidence that the complete MVP builds, runs, and satisfies its acceptance workflows.
@ -31,12 +35,14 @@ Produce repeatable automated and manual evidence that the complete MVP builds, r
- [ ] Complete backend CRUD, validation, authentication, proxy, security, and persistence tests.
- [ ] Add canonical-document persistence round-trip tests.
- [ ] Add workflow-launcher, dependent-dropdown, and dashboard end-to-end scenarios.
- [ ] Build and validate a deterministic local MVP demonstration project and controlled mock endpoints; do not rely on public HTTPBin-style services.
- [x] Validate all valid and invalid examples.
- [x] Produce successful frontend and backend production builds.
- [x] Verify full-stack Docker Compose startup, health, restart, and persistence.
- [ ] Run the final security regression suite.
- [ ] Record commands, test counts, artifacts, limitations, and defects.
- [ ] Update state and the final acceptance checklist.
- [ ] Run clean-checkout, backend restart, Docker restart, database backup, and database restoration validation.
## Acceptance Criteria
@ -73,5 +79,5 @@ Produce repeatable automated and manual evidence that the complete MVP builds, r
## Handoff
- Last completed: Initial technical and Docker validation baseline.
- Next action: Continue release validation as each implementation slice completes.
- Next action: Complete Slice 2 workflow acceptance, then continue release validation as Slices 3-5 complete.
- Known blockers: Final validation depends on Slices 1-5; frontend audit findings remain open.

View File

@ -4,6 +4,10 @@
Complete
## Roadmap Alignment
This slice owns **MVP Scope and Requirement Governance** in `ROADMAP.md`. Its product decisions are complete. Remaining cross-document consistency and final release-checklist work is assigned to Slices 6 and 8 rather than reopening the scope decision.
## Objective
Define one testable MVP release boundary, especially whether IBM Bob/watsonx integration is required for the initial release.
@ -36,7 +40,7 @@ Define one testable MVP release boundary, especially whether IBM Bob/watsonx int
## Acceptance Criteria
- [ ] One authoritative MVP definition exists without contradictory release requirements.
- [ ] AI has an explicit status and, if included, a bounded acceptance test.
- [x] AI has an explicit post-MVP status and no v0.1.0 acceptance dependency.
- [ ] Every requirement maps to a slice and validation criterion.
- [ ] Deferred features are clearly labeled post-MVP.
- [ ] Slice 6 has a definitive release checklist.
@ -45,7 +49,7 @@ Define one testable MVP release boundary, especially whether IBM Bob/watsonx int
- [ ] Requirements, architecture, tasks, and slices use consistent language.
- [ ] No open question can materially change MVP completion.
- [ ] The product owner approves the release boundary.
- [x] The product owner approves the release boundary.
## Risks and Open Questions

View File

@ -2,7 +2,11 @@
## Status
Not started
In progress - unified roadmap created and linked; broader documentation reconciliation remains
## Roadmap Alignment
This slice owns **Documentation, Packaging, and Release** in `ROADMAP.md`, while the final executable release gate remains in Slice 6. `ROADMAP.md` is the master work index; this slice must eventually reconcile or redirect duplicate planning and specification documents without erasing useful history.
## Objective
@ -26,8 +30,9 @@ Make documentation accurate, non-duplicative, and sufficient for development, de
## Tasks
- [ ] Designate `docs/` as the authoritative specification location.
- [ ] Remove, redirect, or clearly mark duplicate root documents.
- [x] Create and link a unified v0.1.0 roadmap grouped by function and slice.
- [x] Designate `docs/` as the authoritative specification location.
- [x] Remove exact duplicate root requirements, architecture, and future-idea documents.
- [ ] Update README prerequisites, commands, and documentation index.
- [ ] Reconcile requirements and architecture with Slice 7.
- [ ] Update schema docs and remove new deprecated `action.responseMapping` examples.
@ -37,7 +42,7 @@ Make documentation accurate, non-duplicative, and sufficient for development, de
- [ ] Document SQLite backup, persistence, and recovery.
- [ ] Publish verified build, test, Docker, and release commands.
- [ ] Publish the final acceptance checklist and known limitations.
- [ ] Reconcile `TASKS.md`, `CODEX.md`, and all slice handoffs.
- [ ] Reconcile `ROADMAP.md`, `CODEX.md`, and all slice handoffs as implementation progresses.
## Acceptance Criteria
@ -64,10 +69,11 @@ Make documentation accurate, non-duplicative, and sufficient for development, de
## Progress Log
No work recorded yet.
- Created `ROADMAP.md` as the unified work index, reconciled the older numbered-step plan and legacy task inventory with `MVP_SCOPE.md` and Slices 1-8, and linked it from the README.
- Designated the `docs/` specifications as authoritative and removed byte-identical root duplicates plus stale transient planning documents.
## Handoff
- Last completed: Slice plan created.
- Next action: Inventory document ownership after the Slice 7 decision.
- Last completed: Unified roadmap creation and cross-slice alignment.
- Next action: Inventory documentation ownership and reconcile duplicate root and `docs/` specifications.
- Known blockers: Final reconciliation depends on completed behavior and validation evidence.

147
TASKS.md
View File

@ -1,147 +0,0 @@
# Conductor MVP Tasks
This document summarizes the current implementation and the remaining work required to complete the initial Conductor MVP. Completed items are based on the current workspace source code; the complete application has not yet been revalidated through a fresh full-stack test run.
## Completed
### Application foundation
- [x] Create the React and TypeScript frontend application.
- [x] Create the Node.js, Express, and TypeScript backend service.
- [x] Add SQLite project persistence.
- [x] Add Dockerfiles and Docker Compose configuration.
- [x] Add application navigation and a shared project context.
- [x] Add the backend health endpoint.
- [x] Add project create, list, read, update, and delete endpoints.
- [x] Add frontend project creation, save, update, and load controls.
### Canonical project model
- [x] Define the canonical JSON project document.
- [x] Add the versioned `0.1.0` JSON schema.
- [x] Add shared frontend TypeScript project types.
- [x] Add backend schema validation.
- [x] Add valid and intentionally invalid example project definitions.
- [x] Add a JSON editor that validates and applies changes to shared project state.
- [x] Keep the Visual Editor and JSON Editor synchronized through the canonical document.
### Visual Editor
- [x] Add the component palette and canvas.
- [x] Support component selection, movement, resizing, and deletion.
- [x] Add common property editing for component name, label, placeholder, default value, visibility, disabled state, required state, position, and size where applicable.
- [x] Detect duplicate component names in the editor.
- [x] Implement the Label component.
- [x] Implement the Button component.
- [x] Implement the Text Input component.
- [x] Implement the Dropdown component, including static option editing.
- [x] Implement the Table component, including column and row editing.
- [x] Implement the JSON Viewer component.
### REST actions and Preview runtime
- [x] Define REST actions in the project schema and TypeScript model.
- [x] Support GET, POST, PUT, PATCH, and DELETE action definitions.
- [x] Add the server-side REST proxy.
- [x] Add URL, path, query, header, and body template handling.
- [x] Add normalized proxy response envelopes and basic error handling.
- [x] Add Preview mode.
- [x] Execute button-bound REST actions in Preview mode.
- [x] Resolve component and variable values in request templates.
- [x] Store action, component, and variable runtime state separately from canonical project JSON.
- [x] Implement response bindings to Label and JSON Viewer values.
- [x] Implement response bindings to Dropdown options.
- [x] Implement response bindings to Table rows.
- [x] Implement response bindings to project variables.
- [x] Support Table row selection at runtime.
- [x] Add binding and action diagnostics in the Inspector.
- [x] Add frontend unit tests for request templates, bindings, variables, dropdowns, and tables.
## Remaining MVP Tasks
### Missing MVP GUI components
- [x] Implement the Text Area component in the palette, canvas, property editor, Preview runtime, and tests.
- [x] Implement the Checkbox component in the palette, canvas, property editor, Preview runtime, and tests.
- [x] Implement the Radio Group component in the palette, canvas, property editor, Preview runtime, and tests.
- [x] Implement the Status/Message Panel component in the palette, canvas, property editor, Preview runtime, response bindings, and tests.
- [x] Implement the Container/Card component in the palette, canvas, property editor, Preview runtime, and tests.
- [x] Remove the schema/runtime component-type mismatch; all eleven schema-supported types now render in the editor and Preview.
### Visual configuration workflows
- [x] Implement a visual REST Action editor for creating, editing, duplicating, and deleting anonymous actions without hand-editing JSON.
- [x] Implement visual Button `onClick` configuration with add/change/clear, missing-reference diagnostics, canonical synchronization, and Preview execution.
- [x] Implement visual component/variable request-input references through executed REST request templates.
- [x] Implement visual canonical response-binding CRUD with REST response sources, supported component/variable targets, `onSuccess` defaults, and legacy `onClick` compatibility.
- [x] Implement visual variable declaration and typed default-value editing with reference-aware diagnostics.
- [ ] Revisit Actions and Bindings information architecture after Slice 2 acceptance: visually separate action, binding, and variable records with clearer card boundaries, hierarchy, spacing, and scannable summaries so the page does not read like a continuous encyclopedia-style text stream.
- [x] Implement visual page onLoad action selection, canonical synchronization, missing-reference diagnostics, once-per-Preview initialization, and onSuccess delivery to component and variable targets.
- [x] Warn before deleting components referenced by response bindings or action request templates, with explicit Cancel and Delete anyway choices while preserving dangling references for diagnostics. Manual UI acceptance passed on 2026-07-30.
- [ ] Update action and response-binding diagnostics to treat page onLoad events as valid action triggers, eliminating the false no-component-event / binding-will-never-receive-a-response warnings and using clear component-or-page-event wording.
- [ ] Make the white Preview page/canvas background grow with runtime-rendered output, including Tables whose bound row content exceeds the configured design-time component or canvas height.
- [ ] Add Table pagination after a dedicated design discussion covering client-side versus server-side pagination, data volume, request/response contracts, page-size controls, loading/error behavior, and canonical configuration.
- [ ] Complete component-specific property controls and basic styling controls required by the MVP specification.
### Authentication and secrets
- [ ] Implement Basic authentication during proxy execution.
- [ ] Implement Bearer token authentication during proxy execution.
- [ ] Implement API-key header authentication during proxy execution.
- [ ] Implement API-key query-parameter authentication during proxy execution.
- [ ] Add secure server-side credential storage and secret references.
- [ ] Ensure exported project JSON excludes secret values.
- [ ] Mask credentials and sensitive values in logs and error messages.
### Proxy security and observability
- [ ] Add permitted-origin or endpoint allowlisting for proxied requests.
- [ ] Add SSRF protections and stricter URL validation.
- [ ] Validate and sanitize forwarded request headers.
- [ ] Persist sanitized API execution history, including timestamp, method, endpoint, status, duration, and error details.
- [ ] Add a way to inspect basic execution history for troubleshooting.
### Validation and error handling
- [ ] Validate the canonical project document on every save operation.
- [ ] Prevent invalid project documents from being persisted.
- [ ] Add user-facing Preview error states for all supported component targets.
- [ ] Reconcile schema descriptions and examples with the canonical `project.bindings` response-mapping model.
- [ ] Remove new uses of the deprecated `action.responseMapping` field.
### Testing and release validation
- [ ] Add backend tests for health, project CRUD, schema validation, and proxy input validation.
- [ ] Add proxy integration tests for successful responses, upstream errors, malformed responses, and timeouts.
- [ ] Add persistence round-trip tests for complete canonical project documents.
- [ ] Add editor tests for every MVP component and its property controls.
- [ ] Add end-to-end tests covering project creation, visual editing, action execution, response mapping, saving, and loading.
- [ ] Run frontend and backend TypeScript checks successfully.
- [ ] Run all frontend and backend automated tests successfully.
- [ ] Run schema validation against every valid and invalid example successfully.
- [ ] Produce successful frontend and backend production builds.
- [ ] Run the full application through Docker Compose and complete the documented integration checklist.
### MVP scope decision
- [x] Decide that IBM Bob/watsonx and other AI integrations are not part of the initial MVP.
- [x] Confirm that all five modeled authentication modes are required for MVP.
- [x] Define the six end-to-end MVP acceptance workflows in `MVP_SCOPE.md`.
### Documentation
- [ ] Designate the `docs/` copies of project documentation as authoritative and remove or redirect duplicate root-level copies.
- [ ] Update the README documentation index to include the schema and response-mapping documents.
- [ ] Update the build plan with the current implementation status and remaining milestone order.
- [ ] Remove resolved questions from the requirements document or record their architectural decisions.
- [ ] Publish a final MVP acceptance checklist tied to tested user workflows.
## Post-MVP Ideas
- [ ] Add multi-page application support.
- [ ] Add OAuth 2.0 and IBM Cloud IAM authentication.
- [ ] Add OpenAPI import and generated forms/actions.
- [ ] Add reusable templates, themes, and component libraries.
- [ ] Add version history, Git integration, and team collaboration.
- [ ] Add role-based access control and enterprise audit retention.
- [ ] Add tabs, modal dialogs, date pickers, file uploads, charts, and progress indicators.

View File

@ -18,7 +18,14 @@ tester, environment, and any observations in the result block for that workflow.
| Complete request-input execution | Accepted 2026-07-28 |
| Response bindings and variable authoring | Accepted 2026-07-29 |
| Page-load actions | Accepted 2026-07-29 |
| Final Slice 2 end-to-end acceptance | Pending |
| Final Slice 2 workflow-launcher and dependent-data acceptance | Accepted 2026-07-30 |
| Page-load Actions & Bindings diagnostics | Accepted 2026-07-30 |
| Runtime-expanded Preview background | Accepted 2026-07-30 |
| MVP component properties and basic appearance | Accepted 2026-07-31 |
| Actions & Bindings information architecture | Accepted 2026-07-31 |
| Final Slice 2 sign-off | Accepted 2026-07-31 |
| Slice 2a request-reference usability | Complete and signed off |
| Slice 3 authentication and secrets | Complete and signed off 2026-08-02 |
## Test Environment Setup
@ -467,7 +474,7 @@ declaring the page-load increment or Slice 2 accepted:
Tester/date: User, 2026-07-29
Notes or defects: Test 3 passed. The populated Table exposed a non-blocking Preview layout limitation: the white page/canvas background retains its configured or minimum size instead of growing with runtime-rendered Table output. TASKS.md now tracks content-aware Preview sizing. It also tracks Table pagination as a separate design item that requires discussion of client-side versus server-side pagination before implementation.
Notes or defects: Test 3 passed. The populated Table exposed a non-blocking Preview layout limitation: the white page/canvas background retains its configured or minimum size instead of growing with runtime-rendered Table output. `ROADMAP.md` tracks content-aware Preview sizing and places Table pagination post-MVP pending a client-side versus server-side design discussion.
Automated checkpoint: 4 focused suites / 9 tests, 18 full frontend suites / 486 tests, frontend production build, and the 13 / 2 / 2 schema matrix passed under Node 20/npm 10 on 2026-07-29. Docker Compose rebuilt, both services started, backend health passed, and the frontend responded on port 3000. Automation and smoke checks do not mark this workflow accepted.
@ -512,24 +519,113 @@ complete.
### Final result
- [ ] Request-input workflow accepted.
- [ ] Response-binding and variable workflow accepted.
- [ ] Page-load workflow accepted.
- [ ] Workflow launcher passed without routine JSON editing.
- [ ] Dependent-data workflow passed.
- [x] Request-input workflow accepted.
- [x] Response-binding and variable workflow accepted.
- [x] Page-load workflow accepted.
- [x] Workflow launcher passed without routine JSON editing.
- [x] Dependent-data workflow passed.
- [ ] Read-only dashboard workflow passed.
- [ ] Failure handling passed.
- [ ] Canonical synchronization and save/reload passed.
- [x] Failure handling passed.
- [x] Canonical synchronization and save/reload passed.
Tester/date:
Tester/date: User, 2026-07-30
Notes or defects:
Notes or defects: The workflow launcher was authored visually with an environment Dropdown, hostname Text Input, Submit Button, JSON Viewer, canonical component templates, Button onClick, and an onSuccess response binding. Successful execution echoed both runtime values; HTTP 503 handling was readable, withheld onSuccess delivery, and cleared its stale error after a successful retry. Dependent-data acceptance used a page onLoad POST to https://httpbingo.org/anything, mapped body.json to Dropdown.options, passed populated and empty states, drove the later launcher request from the selected option, exposed readable failure behavior, and recovered after restoring the successful endpoint. Save/load and fresh Preview preserved canonical configuration while resetting runtime state. No inputMap, action.responseMapping, secrets, responses, loading flags, errors, selected values, or loaded runtime options were persisted. The read-only-dashboard workflow remains a separate pending Slice 6 release workflow.
### Page-load diagnostics result
- [x] The page `onLoad` action no longer reports that it lacks a component or page event.
- [x] Its response binding no longer reports that no component or page event fires the action.
Tester/date: User, 2026-07-30
Notes or defects: The first check displayed the old diagnostics because the browser retained a stale development bundle. A hard refresh loaded the rebuilt frontend, after which both false diagnostics disappeared.
### Runtime-expanded Preview background
Purpose: confirm the white Preview page follows runtime content without changing canonical component dimensions.
1. Open the accepted page-load Table project, or configure a page onLoad response binding that supplies enough Table rows to exceed the Table's design-time height.
2. Enter Preview and confirm the white page background extends below all rendered rows with its normal bottom padding.
3. Confirm the outer Preview area remains scrollable when the expanded page exceeds the viewport.
4. Change the successful response to an empty array and re-enter Preview.
5. Confirm the Table shows its usable empty state and the white page contracts to its design-time minimum.
6. Inspect canonical JSON and confirm the Table's configured `size.height` did not change.
Result: Accepted
- [x] Runtime-populated rows remained inside the white Preview page.
- [x] The expanded page remained scrollable.
- [x] Empty runtime rows produced a usable empty Table and contracted the page.
- [x] Canonical Table `size.height` remained unchanged.
Tester/date: User, 2026-07-30
### MVP component properties and basic appearance
Purpose: confirm the remaining schema-backed properties can be authored visually and render without hand-editing JSON.
1. Select a JSON Viewer, enter valid JSON in **Default JSON**, and confirm the canvas JSON remains a design placeholder while Preview displays the formatted configured default before any response arrives.
2. Select a Table, set a numeric width on one column, and confirm both the canvas and Preview use that width.
3. Select Text Input, Text Area, and Dropdown components, enable **Required**, and confirm Preview renders the corresponding controls as required without persisting runtime values.
4. On representative Label, Button, input, Table, Status Panel, and Card components, set **Font size**, **Text**, and **Background** under **Basic appearance**.
5. Confirm each appearance change immediately updates canonical `properties.style`, the Visual Editor canvas, and Preview.
6. Reset text and background colors and select the default font size; confirm the overrides are removed from `properties.style` and default rendering returns.
7. Save and reload, then confirm configured properties persist while runtime state remains reset.
Expected canonical style shape:
```json
{ "fontSize": 20, "textColor": "#112233", "backgroundColor": "#ddeeff" }
```
Result: Passed on 2026-07-31. The user confirmed all five saved manual sections after Docker rebuild and hard refresh. JSON Viewer defaults, Table widths, required semantics, representative appearance authoring, reset behavior, canonical synchronization, save/reload persistence, and fresh-Preview runtime reset passed. This manual evidence is separate from automated validation.
## Slice 2 Actions & Bindings Information Architecture
Purpose: proportionally confirm that the implemented presentation changes make records easier to distinguish without changing configuration behavior.
1. Hard-refresh the rebuilt frontend and open **Actions & Bindings** in a project with multiple actions, response bindings, and variables.
2. Confirm each record has a distinct boundary and record-type label, and section descriptions clearly distinguish requests, response routing, and runtime variables.
3. Confirm response-binding source/target flow and variable default summaries remain readable.
4. Open action, binding, and variable editors and confirm each editor remains visually attached to the selected record.
5. Edit one record of each type and confirm canonical JSON synchronization and existing diagnostics are unchanged.
Result: Passed on 2026-07-31. The user confirmed distinct section and record hierarchy, readable action/binding/variable summaries, correct editor attachment, canonical edit isolation, and diagnostic placement/recovery. The variable-target check confirmed the canonical `variables.statusText` target alongside the resolved **Variable statusText** label.
## Slice 2a Request-Reference Usability
Purpose: confirm contextual reference and header-name suggestions improve authoring without changing canonical request behavior.
1. Use a project with one uniquely named Text Input and one declared variable. Edit a REST action and type `{{` at the end of the Endpoint URL. Confirm both references appear; choose the component and confirm the canonical `{{components.<name>.value}}` form is inserted while the URL prefix remains.
2. Repeat after typing `{{` in a header value, query-parameter value, and body template. Choose the variable and confirm `{{variables.<name>}}` is inserted in each field.
3. Create two components with the same name. Confirm that ambiguous component name is absent from contextual suggestions and the guided source list. Restore unique names afterward.
4. Add a header row. Confirm the header-name control suggests **Accept** and **Content-Type**, then enter a custom name such as `X-Custom-Vendor-Header` and confirm it remains accepted in canonical JSON.
5. In **Request value reference**, confirm the first control is **Request destination**, the second is **Value source**, and **Insert reference** remains last. Confirm a selected `Query: <key>` aligns conceptually with the query key and the selected component/variable aligns with the request value.
6. Insert one guided reference and confirm existing replace/append behavior remains: header/query values replace; URL/body values append. Confirm path parameters are still excluded.
Result: Passed on 2026-08-01 in isolated Chromium against project `Slice 2 Final` (`#12`) at `http://localhost:3000/`, using 1440 x 1100 and 700 x 1000 viewports. The saved project was not updated. Step 1 contextual suggestions and Steps 2-5 all passed in order. Duplicate component names were excluded until unique, common and custom headers behaved correctly with exact canonical capitalization, guided controls aligned and stacked responsively, canonical component/variable templates were inserted with the required replace/append semantics, path parameters remained excluded, fields remained freely editable, and recursive canonical JSON inspection found no `inputMap`, `responseMapping`, `runtimeValues`, `responses`, `loading`, `isLoading`, `errors`, or `error`. This manual evidence is separate from automated validation. The user granted explicit final Slice 2a sign-off on 2026-08-01.
## Slice 3 Authentication and Secrets
Purpose: confirm encrypted credential lifecycle authoring, compatible action selection, reference-only canonical state, protected execution, redaction, and deletion safety.
Result: Passed on 2026-08-01 in isolated Chromium against an unsaved browser copy of `Slice 2 Final` (`#12`) at `http://localhost:3000/`. Saved project `#12` was not updated.
1. Masked Bearer creation and value replacement passed. Records displayed only name, authentication type, opaque ID, and lifecycle controls; submitted values were cleared and never redisplayed.
2. Basic, Bearer, API-key header, and API-key query choices offered exactly type-compatible secrets. Canonical action state contained only `authenticationType` and opaque `secretReferenceId`, with no credential names or values.
3. Preview execution returned HTTP 200 for Bearer, header-key, and query-key checks. Reflected Authorization, header value, query value, and query-bearing URL used `[REDACTED]`; no stored value reached the browser response.
4. Referenced deletion was blocked with the referencing action named. Changing the action to Anonymous cleared the reference, all disposable secrets were deleted, the server-side list ended empty, and protected mode without a secret did not send a proxy request or execute anonymously.
The tester observed that the earlier blocked-deletion warning remained visible after successful cleanup. This did not affect lifecycle operations or execution safety and is tracked as a non-blocking error-feedback follow-up in `ROADMAP.md`. This manual evidence is separate from automated validation. The user granted explicit final Slice 3 sign-off on 2026-08-02.
## Recording Results
After a manual workflow is completed:
1. Update its checkboxes in this document.
2. Record the tester, date, environment, and any deviations.
3. Record manual acceptance separately from automated coverage in:
@ -538,4 +634,4 @@ After a manual workflow is completed:
- `BASELINE.md`
4. Do not mark a broader workflow accepted when only one checkpoint passed.
5. File unresolved defects in `TASKS.md` with enough detail to reproduce them.
5. File unresolved defects in `ROADMAP.md` and the owning slice with enough detail to reproduce them.

View File

@ -5,6 +5,7 @@
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"test": "npm run build && node --test dist/lib/secretCrypto.test.js dist/routes/proxy.test.js",
"start": "node dist/index.js"
},
"dependencies": {

View File

@ -5,6 +5,7 @@ import healthRouter from './routes/health';
import projectsRouter from './routes/projects';
import validateRouter from './routes/validate';
import proxyRouter from './routes/proxy';
import secretsRouter from './routes/secrets';
const app: Application = express();
@ -18,6 +19,7 @@ app.use(morgan('dev'));
app.use('/api/health', healthRouter);
app.use('/api/projects/validate', validateRouter);
app.use('/api/projects', projectsRouter);
app.use('/api/secrets', secretsRouter);
app.use('/api/proxy/execute', proxyRouter);
// ── Catch-all 404 ─────────────────────────────────────────────────────────────

View File

@ -14,6 +14,17 @@ export function initDatabase(): void {
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE TABLE IF NOT EXISTS secrets (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
authentication_type TEXT NOT NULL,
encrypted_value TEXT NOT NULL,
iv TEXT NOT NULL,
auth_tag TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
`);
console.log('Database initialised');

89
backend/src/db/secrets.ts Normal file
View File

@ -0,0 +1,89 @@
import { randomUUID } from 'crypto';
import db from './database';
import { decryptSecret, encryptSecret } from '../lib/secretCrypto';
export type CredentialType = 'basicAuth' | 'bearerToken' | 'apiKeyHeader' | 'apiKeyQueryParameter';
export type CredentialValue =
| { username: string; password: string }
| { token: string }
| { parameterName: string; value: string };
export interface SecretMetadata {
id: string;
name: string;
authenticationType: CredentialType;
createdAt: string;
updatedAt: string;
}
interface SecretRow {
id: string;
name: string;
authentication_type: CredentialType;
encrypted_value: string;
iv: string;
auth_tag: string;
created_at: string;
updated_at: string;
}
const metadataColumns = `id, name, authentication_type, created_at, updated_at`;
const toMetadata = (row: Omit<SecretRow, 'encrypted_value' | 'iv' | 'auth_tag'>): SecretMetadata => ({
id: row.id,
name: row.name,
authenticationType: row.authentication_type,
createdAt: row.created_at,
updatedAt: row.updated_at,
});
export function listSecrets(): SecretMetadata[] {
const rows = db.prepare(`SELECT ${metadataColumns} FROM secrets ORDER BY name, id`).all() as Array<Omit<SecretRow, 'encrypted_value' | 'iv' | 'auth_tag'>>;
return rows.map(toMetadata);
}
export function getSecretMetadata(id: string): SecretMetadata | undefined {
const row = db.prepare(`SELECT ${metadataColumns} FROM secrets WHERE id = ?`).get(id) as Omit<SecretRow, 'encrypted_value' | 'iv' | 'auth_tag'> | undefined;
return row ? toMetadata(row) : undefined;
}
export function createSecret(name: string, authenticationType: CredentialType, value: CredentialValue): SecretMetadata {
const id = randomUUID();
const encrypted = encryptSecret(value);
db.prepare(`INSERT INTO secrets (id, name, authentication_type, encrypted_value, iv, auth_tag) VALUES (?, ?, ?, ?, ?, ?)`)
.run(id, name, authenticationType, encrypted.encryptedValue, encrypted.iv, encrypted.authTag);
return getSecretMetadata(id)!;
}
export function updateSecret(id: string, name: string, authenticationType: CredentialType, value: CredentialValue): SecretMetadata | undefined {
const encrypted = encryptSecret(value);
const result = db.prepare(`UPDATE secrets SET name = ?, authentication_type = ?, encrypted_value = ?, iv = ?, auth_tag = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`)
.run(name, authenticationType, encrypted.encryptedValue, encrypted.iv, encrypted.authTag, id);
return result.changes ? getSecretMetadata(id) : undefined;
}
export function deleteSecret(id: string): boolean {
return db.prepare('DELETE FROM secrets WHERE id = ?').run(id).changes > 0;
}
export function findSecretReferences(id: string): string[] {
const rows = db.prepare('SELECT id, name, project_json FROM projects').all() as Array<{ id: number; name: string; project_json: string }>;
const references: string[] = [];
for (const row of rows) {
try {
const document = JSON.parse(row.project_json) as { project?: { actions?: Array<{ name?: string; secretReferenceId?: string }> } };
for (const action of document.project?.actions ?? []) {
if (action.secretReferenceId === id) references.push(`Project ${row.name} (#${row.id}), action ${action.name ?? '(unnamed)'}`);
}
} catch { /* Invalid stored project handling belongs to Slice 5. */ }
}
return references;
}
/** Server-internal credential resolution. Never expose this result through a route. */
export function resolveSecret(id: string): { authenticationType: CredentialType; value: CredentialValue } | undefined {
const row = db.prepare('SELECT * FROM secrets WHERE id = ?').get(id) as SecretRow | undefined;
return row ? {
authenticationType: row.authentication_type,
value: decryptSecret<CredentialValue>(row.encrypted_value, row.iv, row.auth_tag),
} : undefined;
}

View File

@ -0,0 +1,45 @@
import type { CredentialType, CredentialValue } from '../db/secrets';
import { SecretConfigurationError } from './secretCrypto';
export type AuthenticationInput = { authenticationType?: string; secretReferenceId?: string };
export type ResolvedSecret = { authenticationType: CredentialType; value: CredentialValue };
export type SecretResolver = (id: string) => ResolvedSecret | undefined;
export class AuthenticationError extends Error {
constructor(public readonly code: string, public readonly status: number, message: string) { super(message); this.name = 'AuthenticationError'; }
}
export function applyAuthentication(action: AuthenticationInput, headers: Record<string, string>, queryParameters: Record<string, string>, resolver: SecretResolver): string[] {
const authenticationType = action.authenticationType ?? 'anonymous';
if (authenticationType === 'anonymous') return [];
if (!['basicAuth', 'bearerToken', 'apiKeyHeader', 'apiKeyQueryParameter'].includes(authenticationType)) throw new AuthenticationError('UNSUPPORTED_AUTHENTICATION_TYPE', 400, 'The action authentication type is unsupported.');
if (!action.secretReferenceId) throw new AuthenticationError('SECRET_REFERENCE_REQUIRED', 400, 'A secret reference is required for credential-backed authentication.');
let secret;
try { secret = resolver(action.secretReferenceId); }
catch (error) { if (error instanceof SecretConfigurationError) throw new AuthenticationError('SECRET_STORE_UNAVAILABLE', 503, error.message); throw error; }
if (!secret) throw new AuthenticationError('SECRET_NOT_FOUND', 404, 'The referenced credential was not found.');
if (secret.authenticationType !== authenticationType) throw new AuthenticationError('SECRET_TYPE_MISMATCH', 400, 'The referenced credential is incompatible with the action authentication type.');
const value = secret.value as unknown as Record<string, string>;
switch (secret.authenticationType) {
case 'basicAuth': headers.Authorization = `Basic ${Buffer.from(`${value.username}:${value.password}`, 'utf8').toString('base64')}`; return [value.username, value.password, headers.Authorization];
case 'bearerToken': headers.Authorization = `Bearer ${value.token}`; return [value.token, headers.Authorization];
case 'apiKeyHeader': headers[value.parameterName] = value.value; return [value.value];
case 'apiKeyQueryParameter': queryParameters[value.parameterName] = value.value; return [value.value];
}
}
export function redactCredentialValues(value: unknown, sensitiveValues: string[]): unknown {
const secrets = sensitiveValues.filter(Boolean).sort((a, b) => b.length - a.length);
if (typeof value === 'string') return secrets.reduce((redacted, secret) => redacted.split(secret).join('[REDACTED]'), value);
if (Array.isArray(value)) return value.map((item) => redactCredentialValues(item, secrets));
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactCredentialValues(item, secrets)]));
return value;
}
export function sanitizeUrlForOutput(rawUrl: string): string {
try {
const url = new URL(rawUrl); if (url.username) url.username = '[REDACTED]'; if (url.password) url.password = '[REDACTED]';
for (const key of Array.from(url.searchParams.keys())) url.searchParams.set(key, '[REDACTED]');
return url.toString();
} catch { return '[invalid or redacted URL]'; }
}

View File

@ -0,0 +1,42 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { decryptSecret, encryptSecret, SecretConfigurationError } from './secretCrypto';
const validKey = '22'.repeat(32);
test('AES-256-GCM round-trips structured credentials without plaintext storage', () => {
const previous = process.env.CONDUCTOR_SECRET_KEY;
process.env.CONDUCTOR_SECRET_KEY = validKey;
try {
const credential = { username: 'alice', password: 'correct-horse-battery-staple' };
const encrypted = encryptSecret(credential);
assert.equal(encrypted.encryptedValue.includes(credential.password), false);
assert.deepEqual(decryptSecret(encrypted.encryptedValue, encrypted.iv, encrypted.authTag), credential);
} finally {
if (previous === undefined) delete process.env.CONDUCTOR_SECRET_KEY;
else process.env.CONDUCTOR_SECRET_KEY = previous;
}
});
test('secret operations fail closed without a configured master key', () => {
const previous = process.env.CONDUCTOR_SECRET_KEY;
delete process.env.CONDUCTOR_SECRET_KEY;
try {
assert.throws(() => encryptSecret({ token: 'never-written' }), SecretConfigurationError);
} finally {
if (previous !== undefined) process.env.CONDUCTOR_SECRET_KEY = previous;
}
});
test('authenticated encryption rejects a different master key', () => {
const previous = process.env.CONDUCTOR_SECRET_KEY;
process.env.CONDUCTOR_SECRET_KEY = validKey;
const encrypted = encryptSecret({ token: 'protected' });
process.env.CONDUCTOR_SECRET_KEY = '33'.repeat(32);
try {
assert.throws(() => decryptSecret(encrypted.encryptedValue, encrypted.iv, encrypted.authTag));
} finally {
if (previous === undefined) delete process.env.CONDUCTOR_SECRET_KEY;
else process.env.CONDUCTOR_SECRET_KEY = previous;
}
});

View File

@ -0,0 +1,53 @@
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
const ALGORITHM = 'aes-256-gcm';
export class SecretConfigurationError extends Error {
constructor(message: string) {
super(message);
this.name = 'SecretConfigurationError';
}
}
function masterKey(): Buffer {
const configured = process.env.CONDUCTOR_SECRET_KEY;
if (!configured) {
throw new SecretConfigurationError('Secret storage is unavailable because CONDUCTOR_SECRET_KEY is not configured.');
}
const key = /^[0-9a-fA-F]{64}$/.test(configured)
? Buffer.from(configured, 'hex')
: Buffer.from(configured, 'base64');
if (key.length !== 32) {
throw new SecretConfigurationError('CONDUCTOR_SECRET_KEY must encode exactly 32 bytes as base64 or 64 hexadecimal characters.');
}
return key;
}
export interface EncryptedSecret {
encryptedValue: string;
iv: string;
authTag: string;
}
export function encryptSecret(value: unknown): EncryptedSecret {
const iv = randomBytes(12);
const cipher = createCipheriv(ALGORITHM, masterKey(), iv);
const plaintext = JSON.stringify(value);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
return {
encryptedValue: encrypted.toString('base64'),
iv: iv.toString('base64'),
authTag: cipher.getAuthTag().toString('base64'),
};
}
export function decryptSecret<T>(encryptedValue: string, iv: string, authTag: string): T {
const decipher = createDecipheriv(ALGORITHM, masterKey(), Buffer.from(iv, 'base64'));
decipher.setAuthTag(Buffer.from(authTag, 'base64'));
const decrypted = Buffer.concat([
decipher.update(Buffer.from(encryptedValue, 'base64')),
decipher.final(),
]);
return JSON.parse(decrypted.toString('utf8')) as T;
}

View File

@ -0,0 +1,38 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAuthentication, redactCredentialValues, sanitizeUrlForOutput } from '../lib/authentication';
import type { CredentialType, CredentialValue } from '../db/secrets';
const resolver = (authenticationType: CredentialType, value: CredentialValue) => () => ({ authenticationType, value });
test('injects Basic authentication and returns values needed for response redaction', () => {
const headers: Record<string, string> = {}; const query: Record<string, string> = {};
const sensitive = applyAuthentication({ authenticationType: 'basicAuth', secretReferenceId: 'basic' }, headers, query, resolver('basicAuth', { username: 'alice', password: 'secret' }));
assert.equal(headers.Authorization, `Basic ${Buffer.from('alice:secret').toString('base64')}`);
assert.ok(sensitive.includes('secret'));
});
test('injects Bearer and both API-key modes without mutating unrelated inputs', () => {
const bearerHeaders: Record<string, string> = { Accept: 'application/json' }; const bearerQuery = { page: '1' };
applyAuthentication({ authenticationType: 'bearerToken', secretReferenceId: 'bearer' }, bearerHeaders, bearerQuery, resolver('bearerToken', { token: 'token-value' }));
assert.equal(bearerHeaders.Authorization, 'Bearer token-value'); assert.equal(bearerQuery.page, '1');
const headerHeaders: Record<string, string> = {}; applyAuthentication({ authenticationType: 'apiKeyHeader', secretReferenceId: 'header' }, headerHeaders, {}, resolver('apiKeyHeader', { parameterName: 'X-API-Key', value: 'header-value' }));
assert.equal(headerHeaders['X-API-Key'], 'header-value');
const query: Record<string, string> = {}; applyAuthentication({ authenticationType: 'apiKeyQueryParameter', secretReferenceId: 'query' }, {}, query, resolver('apiKeyQueryParameter', { parameterName: 'api_key', value: 'query-value' }));
assert.equal(query.api_key, 'query-value');
});
test('rejects missing, unknown, and incompatible secret references', () => {
assert.throws(() => applyAuthentication({ authenticationType: 'bearerToken' }, {}, {}, resolver('bearerToken', { token: 'x' })), (error: Error & { code?: string }) => error.code === 'SECRET_REFERENCE_REQUIRED');
assert.throws(() => applyAuthentication({ authenticationType: 'bearerToken', secretReferenceId: 'missing' }, {}, {}, () => undefined), (error: Error & { code?: string }) => error.code === 'SECRET_NOT_FOUND');
assert.throws(() => applyAuthentication({ authenticationType: 'basicAuth', secretReferenceId: 'bearer' }, {}, {}, resolver('bearerToken', { token: 'x' })), (error: Error & { code?: string }) => error.code === 'SECRET_TYPE_MISMATCH');
});
test('redacts reflected credential values recursively and sanitizes output URLs', () => {
const redacted = redactCredentialValues({ authorization: 'Bearer token-value', nested: ['token-value'], url: 'https://example.test/?api_key=query-value' }, ['token-value', 'query-value']);
assert.deepEqual(redacted, { authorization: 'Bearer [REDACTED]', nested: ['[REDACTED]'], url: 'https://example.test/?api_key=[REDACTED]' });
assert.equal(sanitizeUrlForOutput('https://user:pass@example.test/items?api_key=value&safe=also-hidden'), 'https://%5BREDACTED%5D:%5BREDACTED%5D@example.test/items?api_key=%5BREDACTED%5D&safe=%5BREDACTED%5D');
assert.equal(sanitizeUrlForOutput('not a URL'), '[invalid or redacted URL]');
});

View File

@ -18,6 +18,8 @@
*/
import { Router, Request, Response } from 'express';
import { resolveSecret } from '../db/secrets';
import { applyAuthentication, AuthenticationError, redactCredentialValues, sanitizeUrlForOutput } from '../lib/authentication';
const router = Router();
@ -60,6 +62,7 @@ interface RestActionInput {
pathParameters?: Record<string, string>;
bodyTemplate?: string;
authenticationType?: string;
secretReferenceId?: string;
}
interface ProxySuccessResponse {
@ -119,17 +122,19 @@ async function executeRequest(action: RestActionInput): Promise<ProxyResponse> {
const method = (action.method ?? '').toUpperCase();
const urlTemplate = action.url ?? '';
const pathParameters = action.pathParameters ?? {};
const queryParameters = action.queryParameters ?? {};
const headers = action.headers ?? {};
const queryParameters = { ...(action.queryParameters ?? {}) };
const headers = { ...(action.headers ?? {}) };
const bodyTemplate = action.bodyTemplate ?? '';
const sensitiveValues = applyAuthentication(action, headers, queryParameters, resolveSecret);
// Build URL
const finalUrl = buildUrl(urlTemplate, pathParameters, queryParameters);
// Build request init
const init: RequestInit = {
method,
headers: { ...headers },
headers,
// Attach body only for methods that semantically support it
body: method !== 'GET' && method !== 'DELETE' && bodyTemplate.trim()
? bodyTemplate
@ -137,10 +142,6 @@ async function executeRequest(action: RestActionInput): Promise<ProxyResponse> {
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
};
// TODO(auth): inject credentials here based on action.authenticationType
// For now only 'anonymous' is executed; other types are accepted by the model
// but treated as anonymous until secret storage is implemented in a later step.
const startMs = Date.now();
const upstream = await fetch(finalUrl, init);
const durationMs = Date.now() - startMs;
@ -171,7 +172,7 @@ async function executeRequest(action: RestActionInput): Promise<ProxyResponse> {
status: upstream.status,
statusText: upstream.statusText,
headers: responseHeaders,
body,
body: redactCredentialValues(body, sensitiveValues),
durationMs,
};
}
@ -212,20 +213,24 @@ router.post('/', async (req: Request, res: Response): Promise<void> => {
try {
result = await executeRequest({ ...action, method });
} catch (err) {
if (err instanceof AuthenticationError) {
res.status(err.status).json({ code: err.code, error: err.message });
return;
}
// Network-level failure (DNS, ECONNREFUSED, timeout, etc.)
const message = err instanceof Error ? err.message : String(err);
const isTimeout = err instanceof Error && err.name === 'TimeoutError';
console.error('[proxy] Network error executing action:', {
url: action.url,
url: sanitizeUrlForOutput(action.url),
method,
error: message,
});
res.status(502).json({
error: isTimeout
? `Request to "${action.url}" timed out after ${REQUEST_TIMEOUT_MS / 1000}s.`
: `Network error reaching "${action.url}": ${message}`,
? `Request to "${sanitizeUrlForOutput(action.url)}" timed out after ${REQUEST_TIMEOUT_MS / 1000}s.`
: `Network error reaching "${sanitizeUrlForOutput(action.url)}": ${message}`,
});
return;
}

View File

@ -0,0 +1,55 @@
import { Router, Request, Response } from 'express';
import { CredentialType, CredentialValue, createSecret, deleteSecret, findSecretReferences, getSecretMetadata, listSecrets, updateSecret } from '../db/secrets';
import { SecretConfigurationError } from '../lib/secretCrypto';
const router = Router();
const credentialTypes = new Set<CredentialType>(['basicAuth', 'bearerToken', 'apiKeyHeader', 'apiKeyQueryParameter']);
function parseInput(body: unknown): { name: string; authenticationType: CredentialType; value: CredentialValue } | string {
if (!body || typeof body !== 'object') return 'Request body must be a JSON object.';
const candidate = body as Record<string, unknown>;
if (typeof candidate.name !== 'string' || !candidate.name.trim()) return '`name` is required.';
if (typeof candidate.authenticationType !== 'string' || !credentialTypes.has(candidate.authenticationType as CredentialType)) return '`authenticationType` must be a credential-backed authentication type.';
if (!candidate.value || typeof candidate.value !== 'object') return '`value` is required.';
const value = candidate.value as Record<string, unknown>;
const type = candidate.authenticationType as CredentialType;
const required = type === 'basicAuth' ? ['username', 'password'] : type === 'bearerToken' ? ['token'] : ['parameterName', 'value'];
if (required.some((field) => typeof value[field] !== 'string' || !(value[field] as string))) return `Credential value requires non-empty ${required.join(' and ')} fields.`;
return { name: candidate.name.trim(), authenticationType: type, value: value as CredentialValue };
}
function configurationFailure(error: unknown, res: Response): boolean {
if (!(error instanceof SecretConfigurationError)) return false;
res.status(503).json({ code: 'SECRET_STORE_UNAVAILABLE', error: error.message });
return true;
}
router.get('/', (_req, res) => res.json(listSecrets()));
router.get('/:id', (req, res) => {
const secret = getSecretMetadata(req.params.id);
secret ? res.json(secret) : res.status(404).json({ code: 'SECRET_NOT_FOUND', error: 'Secret not found.' });
});
router.post('/', (req: Request, res: Response) => {
const input = parseInput(req.body);
if (typeof input === 'string') { res.status(400).json({ code: 'INVALID_SECRET', error: input }); return; }
try { res.status(201).json(createSecret(input.name, input.authenticationType, input.value)); }
catch (error) { if (!configurationFailure(error, res)) throw error; }
});
router.put('/:id', (req: Request, res: Response) => {
const input = parseInput(req.body);
if (typeof input === 'string') { res.status(400).json({ code: 'INVALID_SECRET', error: input }); return; }
try {
const secret = updateSecret(req.params.id, input.name, input.authenticationType, input.value);
secret ? res.json(secret) : res.status(404).json({ code: 'SECRET_NOT_FOUND', error: 'Secret not found.' });
} catch (error) { if (!configurationFailure(error, res)) throw error; }
});
router.delete('/:id', (req, res) => {
const references = findSecretReferences(req.params.id);
if (references.length) {
res.status(409).json({ code: 'SECRET_IN_USE', error: 'Secret is referenced by saved project actions and cannot be deleted.', references });
return;
}
deleteSecret(req.params.id) ? res.status(204).send() : res.status(404).json({ code: 'SECRET_NOT_FOUND', error: 'Secret not found.' });
});
export default router;

View File

@ -8,6 +8,7 @@ services:
environment:
- PORT=4000
- NODE_ENV=development
- CONDUCTOR_SECRET_KEY=${CONDUCTOR_SECRET_KEY:-}
# Data directory is mounted so the SQLite file survives container restarts.
# shared/ is mounted read-only at /shared so the backend can resolve the
# schema via path.resolve(__dirname, '../../../shared/schemas/...').

View File

@ -128,7 +128,7 @@ A REST API call definition. Required fields: `id`, `name`, `method`, `url`, `aut
Allowed `method` values: `GET` · `POST` · `PUT` · `PATCH` · `DELETE`
Allowed `authenticationType` values: `anonymous` · `bearerToken` · `basicAuth` · `apiKeyHeader` · `apiKeyQueryParameter`
Allowed `authenticationType` values: `anonymous` · `bearerToken` · `basicAuth` · `apiKeyHeader` · `apiKeyQueryParameter`. Credential-backed actions use `secretReferenceId`, an opaque ID resolved only by the backend; anonymous actions omit it.
```json
{
@ -181,6 +181,11 @@ New response mappings belong in top-level `project.bindings`, not inside the act
| `authenticationType` | `string` | ✅ | Authentication strategy (see allowed values above). Credentials are never stored here. |
| `responseMapping` | `array` | — | Deprecated legacy action-local mappings. New mappings use top-level `project.bindings`. |
REST actions do not have a configurable timeout field in schema version
`0.1.0`. The backend applies a fixed 30-second execution timeout to every
proxied request. A configurable timeout is deferred until the proxy-policy work
can define safe minimum and maximum limits consistently.
### `Binding`
Declares data flow between a source and a target. Required fields: `id`, `source`, `target`.

35
docs/SECRETS.md Normal file
View File

@ -0,0 +1,35 @@
# Secrets and Authentication Security Model
## Approved MVP policy
Conductor stores credential values server-side in SQLite encrypted with AES-256-GCM. A persistent 32-byte master key is supplied to the backend as `CONDUCTOR_SECRET_KEY`, encoded as base64 or 64 hexadecimal characters. The key is never stored in SQLite, project JSON, frontend state, exports, logs, or source control.
Canonical actions will identify credentials by an opaque secret-reference ID. Credential values are never embedded in the canonical project document. Anonymous actions require no secret.
## Stored credential shapes
- `basicAuth`: username and password
- `bearerToken`: token
- `apiKeyHeader`: header name and API-key value
- `apiKeyQueryParameter`: query-parameter name and API-key value
Secret names and authentication types are non-secret metadata. List and lookup APIs return only ID, name, authentication type, and timestamps. There is no API that returns a stored credential value. Creation and replacement accept credential values but return metadata only.
## Threat assumptions and boundaries
- Conductor v0.1.0 is a single-user deployment; accounts, tenant isolation, and RBAC are out of scope.
- TLS termination and host access controls are deployment responsibilities. Credential submission must use HTTPS outside trusted local development.
- SQLite disclosure alone must not reveal plaintext. An attacker holding both the database and master key can decrypt credentials.
- Process or browser compromise, memory inspection, hostile administrators, and compromised destinations are not prevented by encryption at rest.
- Authentication injection and error/log/URL redaction occur server-side. Query API keys require URL sanitization before logging.
## Key and lifecycle contract
- Generate the key outside Conductor and inject it through a Docker secret or environment variable.
- Retain the same key across restarts and back it up separately from SQLite.
- Losing the key makes stored credentials unrecoverable.
- Key rotation requires a deliberate future procedure and is not part of this increment.
- If the key is absent or malformed, anonymous behavior remains available while secret creation, replacement, and resolution fail closed.
- Secret deletion is permanent. Reference-aware deletion safeguards will accompany canonical action references.
Generate a development key with `openssl rand -base64 32`, then set `CONDUCTOR_SECRET_KEY` before starting Docker Compose. Never commit the value.

View File

@ -0,0 +1,27 @@
import type { AuthenticationType } from '../types/project';
export type CredentialType = Exclude<AuthenticationType, 'anonymous'>;
export type SecretMetadata = {
id: string;
name: string;
authenticationType: CredentialType;
createdAt: string;
updatedAt: string;
};
export type SecretValue =
| { username: string; password: string }
| { token: string }
| { parameterName: string; value: string };
const BASE = '/api/secrets';
async function response<T>(res: Response): Promise<T> {
if (!res.ok) {
const body = await res.json().catch(() => ({})) as { error?: string };
throw new Error(body.error ?? `HTTP ${res.status}`);
}
return res.status === 204 ? undefined as T : res.json() as Promise<T>;
}
export const listSecrets = async (): Promise<SecretMetadata[]> => response(await fetch(BASE));
export const createSecret = async (name: string, authenticationType: CredentialType, value: SecretValue): Promise<SecretMetadata> => response(await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, authenticationType, value }) }));
export const replaceSecret = async (id: string, name: string, authenticationType: CredentialType, value: SecretValue): Promise<SecretMetadata> => response(await fetch(`${BASE}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, authenticationType, value }) }));
export const deleteSecret = async (id: string): Promise<void> => response(await fetch(`${BASE}/${id}`, { method: 'DELETE' }));

View File

@ -53,6 +53,13 @@
color: #1f2328;
}
.sectionDescription {
margin-top: 2px;
color: #6b7280;
font-size: 11px;
line-height: 1.4;
}
.sectionCount {
font-size: 11px;
font-weight: 600;
@ -74,14 +81,35 @@
.actionCard {
padding: 14px 16px;
border-bottom: 1px solid #e5e7eb;
border: 1px solid #d8dee4;
border-radius: 6px;
background: #ffffff;
display: flex;
flex-direction: column;
gap: 8px;
}
.actionCard:last-child {
border-bottom: none;
.recordList {
display: flex;
flex-direction: column;
gap: 12px;
padding: 14px;
background: #f6f8fa;
}
.recordGroup {
min-width: 0;
overflow: hidden;
border-radius: 6px;
box-shadow: 0 1px 2px rgb(31 35 40 / 6%);
}
.recordType {
color: #6b7280;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.actionCardHeader {
@ -227,16 +255,14 @@
.bindingCard {
padding: 14px 16px;
border-bottom: 1px solid #e5e7eb;
border: 1px solid #d8dee4;
border-radius: 6px;
background: #ffffff;
display: flex;
flex-direction: column;
gap: 8px;
}
.bindingCard:last-child {
border-bottom: none;
}
.bindingCardHeader {
display: flex;
align-items: center;
@ -525,7 +551,8 @@
flex-direction: column;
gap: 16px;
padding: 18px;
border-bottom: 1px solid #bfdbfe;
border: 1px solid #bfdbfe;
border-top: 0;
background: #f8fbff;
box-shadow: inset 3px 0 0 #2563eb;
}
@ -535,19 +562,35 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
padding: 16px;
border-bottom: 1px solid #bfdbfe;
border: 1px solid #bfdbfe;
border-top: 0;
background: #f8fbff;
box-shadow: inset 3px 0 0 #2563eb;
}
.variableCard {
padding: 14px 16px;
border-bottom: 1px solid #e5e7eb;
border: 1px solid #d8dee4;
border-radius: 6px;
background: #ffffff;
display: flex;
flex-direction: column;
gap: 8px;
}
.variableName {
color: #1f2328;
font-size: 13px;
font-weight: 600;
}
.variableDefault {
display: flex;
align-items: baseline;
gap: 8px;
flex-wrap: wrap;
}
.actionEditorHeader {
display: flex;
align-items: flex-start;
@ -733,10 +776,62 @@
margin-top: 8px;
}
.templateField {
min-width: 0;
}
.templateSuggestions {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 4px;
padding: 6px;
border: 1px solid #bfdbfe;
border-radius: 5px;
background: #eff6ff;
}
.templateSuggestionsLabel {
color: #1e40af;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.templateSuggestion {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
width: 100%;
padding: 5px 7px;
border: 1px solid #dbeafe;
border-radius: 4px;
background: #ffffff;
color: #1f2328;
cursor: pointer;
font-family: inherit;
font-size: 11px;
text-align: left;
}
.templateSuggestion:hover {
border-color: #60a5fa;
background: #f8fbff;
}
.templateSuggestion code {
color: #57606a;
font-size: 10px;
word-break: break-all;
}
@media (max-width: 720px) {
.formGrid,
.endpointRow,
.keyValueRow {
.keyValueRow,
.requestInputRow {
grid-template-columns: 1fr;
}

View File

@ -33,6 +33,31 @@ function ProjectSeed({ legacyBinding = false }: { legacyBinding?: boolean }): nu
return null;
}
type DiagnosticTrigger = 'page' | 'component' | 'none';
function DiagnosticSeed({ trigger }: { trigger: DiagnosticTrigger }): null {
const { setDoc } = useProject();
useEffect(() => setDoc((doc) => ({
...doc,
project: {
...doc.project,
pages: [{
...doc.project.pages[0],
events: trigger === 'page'
? [{ event: 'onLoad', actionId: 'action_lookup' }]
: [],
components: [
{ id: 'cmp_result', type: 'JsonViewer', name: 'result', position: { x: 0, y: 0 }, size: { width: 200, height: 100 }, properties: {} },
{ id: 'cmp_button', type: 'Button', name: 'submit', position: { x: 0, y: 120 }, size: { width: 120, height: 40 }, properties: {}, events: trigger === 'component' ? [{ event: 'onClick', actionId: 'action_lookup' }] : [] },
],
}],
actions: [{ id: 'action_lookup', name: 'Lookup', method: 'GET', url: 'https://example.com', headers: {}, queryParameters: {}, pathParameters: {}, bodyTemplate: '', authenticationType: 'anonymous' }],
bindings: [{ id: 'binding_lookup', source: 'actions.action_lookup.response.body', target: 'components.result.value', trigger: 'onSuccess' }],
},
})), [setDoc, trigger]);
return null;
}
function currentDocument(container: HTMLElement): ProjectDocument {
const probe = container.querySelector('[data-testid="project-probe"]');
if (!probe?.textContent) throw new Error('Project probe was not rendered.');
@ -119,7 +144,7 @@ describe('Actions & Bindings visual REST action authoring', () => {
'bearerToken',
);
expect(currentDocument(container).project.actions[0].authenticationType)
.toBe('anonymous');
.toBe('bearerToken');
setControlValue(
container.querySelector<HTMLInputElement>('[data-testid="action-name"]'),
@ -155,10 +180,63 @@ describe('Actions & Bindings visual REST action authoring', () => {
url: 'https://api.example.com/items',
queryParameters: { limit: '25' },
bodyTemplate: '{"name":"{{components.nameInput.value}}"}',
authenticationType: 'anonymous',
authenticationType: 'bearerToken',
});
});
test('presents actions and response bindings as distinct summarized records', () => {
act(() => {
root.render(
<ProjectProvider>
<ProjectSeed legacyBinding />
<ActionInspector />
<ProjectProbe />
</ProjectProvider>,
);
});
expect(container.textContent).toContain('Requests sent through the backend proxy.');
expect(container.textContent).toContain(
'Routes successful action responses into components or runtime variables.',
);
expect(container.textContent).toContain('REST action');
expect(container.textContent).toContain('Response binding');
});
test('suggests canonical request references and common header names while preserving custom headers', () => {
act(() => {
root.render(
<ProjectProvider>
<ProjectSeed />
<ActionInspector />
<ProjectProbe />
</ProjectProvider>,
);
});
click(container.querySelector('button[aria-label="Edit REST action Lookup"]'));
setControlValue(
container.querySelector<HTMLInputElement>('[data-testid="action-url"]'),
'https://example.com/{{',
);
const urlSuggestions = container.querySelector('[data-testid="action-url-suggestions"]');
expect(urlSuggestions?.textContent).toContain('Component: result');
click(urlSuggestions?.querySelector('button') ?? null);
expect(currentDocument(container).project.actions[0].url)
.toBe('https://example.com/{{components.result.value}}');
click(container.querySelector('button[aria-label="Add headers row"]'));
const headerKey = container.querySelector<HTMLInputElement>('input[aria-label="Headers key"]');
const listId = headerKey?.getAttribute('list');
expect(listId).toBeTruthy();
const headerOptions = Array.from(container.querySelectorAll(`#${listId} option`))
.map((option) => option.getAttribute('value'));
expect(headerOptions).toEqual(expect.arrayContaining(['Accept', 'Content-Type']));
setControlValue(headerKey, 'X-Custom-Vendor-Header');
expect(currentDocument(container).project.actions[0].headers)
.toEqual({ 'X-Custom-Vendor-Header': '' });
});
test('keeps invalid drafts local until they are corrected', () => {
click(container.querySelector('[data-testid="add-rest-action"]'));
@ -324,4 +402,39 @@ describe('Actions & Bindings visual REST action authoring', () => {
expect(currentDocument(container).project.bindings[0].trigger).toBe('onSuccess');
expect(Array.from(container.querySelectorAll<HTMLSelectElement>('select[aria-label="Binding trigger"] option')).map((option) => option.value)).toEqual(['onSuccess']);
});
test('treats page onLoad as a valid action and binding trigger', () => {
act(() => {
root.render(<ProjectProvider><DiagnosticSeed trigger="page" /><ActionInspector /></ProjectProvider>);
});
expect(container.textContent).not.toContain(
'Action "Lookup" is not triggered by any component or page event.',
);
expect(container.textContent).not.toContain(
'No component or page event fires action "action_lookup".',
);
});
test('reports revised diagnostics for a genuinely untriggered action', () => {
act(() => {
root.render(<ProjectProvider><DiagnosticSeed trigger="none" /><ActionInspector /></ProjectProvider>);
});
expect(container.textContent).toContain(
'Action "Lookup" is not triggered by any component or page event.',
);
expect(container.textContent).toContain(
'No component or page event fires action "action_lookup".',
);
});
test('continues to treat component events as valid triggers', () => {
act(() => {
root.render(<ProjectProvider><DiagnosticSeed trigger="component" /><ActionInspector /></ProjectProvider>);
});
expect(container.textContent).not.toContain('not triggered by any component or page event');
expect(container.textContent).not.toContain('No component or page event fires action');
});
});

View File

@ -6,7 +6,7 @@
*
* Diagnostics computed
* Per action (Step 15.6):
* Action is not triggered by any component event (info)
* Action is not triggered by any component or page event (info)
* Per action (Step 16.5 / 18.1):
* Template references a component that does not exist (warning)
* Template uses unsupported property (not "value") (warning)
@ -22,7 +22,7 @@
* (.value JsonViewer/Label; .options Dropdown; .rows Table)
* Target is a variable that is not declared (warning) 18.1
* Trigger is unsupported for variable-target bindings (warning) 18.1
* No component has an event that fires the source action (warning)
* No component or page event fires the source action (warning)
* Trigger is unsupported for response bindings (warning)
* Trigger "onClick" is legacy recommend "onSuccess" (info)
*
@ -39,7 +39,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useProject } from '../../context/ProjectContext';
import { executeAction } from '../../api/proxyApi';
import type { ProxyResponse } from '../../api/proxyApi';
import type { RestAction, Binding, CanvasComponent, DropdownOption, TableColumn, Variable } from '../../types/project';
import type { RestAction, Binding, CanvasComponent, DropdownOption, Page, TableColumn, Variable } from '../../types/project';
import { extractTemplates, classifyVariableExpression } from '../Preview/templateUtils';
import {
isTargetPropertySupported,
@ -52,6 +52,9 @@ import styles from './ActionInspector.module.css';
import RestActionEditor from './RestActionEditor';
import ResponseBindingEditor from './ResponseBindingEditor';
import VariableEditor from './VariableEditor';
import SecretManager from './SecretManager';
import { listSecrets } from '../../api/secretsApi';
import type { SecretMetadata } from '../../api/secretsApi';
import {
createResponseBinding,
findVariableReferences,
@ -105,6 +108,7 @@ function actionDiagnosticKey(actionIndex: number): string {
function computeDiagnostics(
actions: RestAction[],
bindings: Binding[],
pages: Page[],
allComponents: CanvasComponent[],
variables: Record<string, Variable>,
): DiagMap {
@ -119,8 +123,13 @@ function computeDiagnostics(
const actionIds = new Set(actions.map((a) => a.id));
const componentsByName = new Map(allComponents.map((c) => [c.name, c]));
// ── Index: which actionIds are triggered by at least one component event ──
// ── Index: which actionIds are triggered by a component or page event ─────
const triggeredActionIds = new Set<string>();
for (const page of pages) {
for (const ev of page.events ?? []) {
triggeredActionIds.add(ev.actionId);
}
}
for (const comp of allComponents) {
for (const ev of comp.events ?? []) {
triggeredActionIds.add(ev.actionId);
@ -153,8 +162,8 @@ function computeDiagnostics(
add(
diagnosticKey,
'info',
`Action "${action.name}" is not triggered by any component event. ` +
`Add an events entry on a component: { "event": "onClick", "actionId": "${action.id}" }`,
`Action "${action.name}" is not triggered by any component or page event. ` +
`Assign it to a supported component event or to a page onLoad event.`,
);
}
@ -371,14 +380,14 @@ function computeDiagnostics(
}
}
// 2e. No triggering component fires the source action at all
// 2e. No component or page event fires the source action at all
if (sourceActionId && actionIds.has(sourceActionId) && !triggeredActionIds.has(sourceActionId)) {
add(
binding.id,
'warn',
`No component has an event that fires action "${sourceActionId}". ` +
`No component or page event fires action "${sourceActionId}". ` +
`The binding will never receive a response. ` +
`Add events: [{ "event": "onClick", "actionId": "${sourceActionId}" }] to a Button.`,
`Assign the action to a supported component event or to a page onLoad event.`,
);
}
@ -658,6 +667,7 @@ function ActionCard({
return (
<div className={styles.actionCard}>
<div className={styles.recordType}>REST action</div>
{/* ── Header: method + name + id ── */}
<div className={styles.actionCardHeader}>
<span className={[styles.methodBadge, methodClass(action.method)].join(' ')}>
@ -822,11 +832,13 @@ function BindingCard({
const targetComponent = targetComponentName
? allComponents.find((c) => c.name === targetComponentName)
: null;
const targetVariableName = parseVariableTargetPath(binding.target)?.variableName ?? null;
const trigger = binding.trigger ?? 'onChange';
return (
<div className={styles.bindingCard}>
<div className={styles.recordType}>Response binding</div>
{/* ── Header: id + trigger ── */}
<div className={styles.bindingCardHeader}>
<span className={styles.bindingId}>{binding.id}</span>
@ -873,6 +885,10 @@ function BindingCard({
<span className={styles.bindingUnresolved}>
component &ldquo;{targetComponentName}&rdquo; not found
</span>
) : targetVariableName ? (
<span className={styles.bindingResolved}>
Variable {targetVariableName}
</span>
) : null}
</div>
</div>
@ -902,6 +918,13 @@ function ActionInspector(): React.ReactElement {
const [editingBindingIndex, setEditingBindingIndex] = useState<number | null>(null);
const [editingVariableName, setEditingVariableName] = useState<string | null>(null);
const [addingVariable, setAddingVariable] = useState(false);
const [secrets, setSecrets] = useState<SecretMetadata[]>([]);
const refreshSecrets = useCallback(async () => {
try { setSecrets(await listSecrets()); } catch { setSecrets([]); }
}, []);
useEffect(() => { void refreshSecrets(); }, [refreshSecrets]);
useEffect(() => {
if (
@ -984,8 +1007,8 @@ function ActionInspector(): React.ReactElement {
// Compute all diagnostics once per render cycle
const diagMap = useMemo(
() => computeDiagnostics(actions, bindings, allComponents, variables),
[actions, bindings, allComponents, variables],
() => computeDiagnostics(actions, bindings, pages, allComponents, variables),
[actions, bindings, pages, allComponents, variables],
);
// Count total warnings/infos for section summary banners
@ -1023,10 +1046,15 @@ function ActionInspector(): React.ReactElement {
</div>
</div>
<SecretManager secrets={secrets} actions={actions} onChanged={refreshSecrets} />
{/* ══ Actions section ══════════════════════════════════════════ */}
<div className={styles.section}>
<div className={styles.sectionHeader}>
<span className={styles.sectionTitle}>REST Actions</span>
<div>
<div className={styles.sectionTitle}>REST Actions</div>
<div className={styles.sectionDescription}>Requests sent through the backend proxy.</div>
</div>
<div className={styles.sectionHeaderMeta}>
<span className={styles.sectionCount}>{actions.length}</span>
<button
@ -1062,8 +1090,9 @@ function ActionInspector(): React.ReactElement {
project document visually.
</div>
) : (
actions.map((action, actionIndex) => (
<React.Fragment key={`${action.id}-${actionIndex}`}>
<div className={styles.recordList}>
{actions.map((action, actionIndex) => (
<div className={styles.recordGroup} key={`${action.id}-${actionIndex}`}>
<ActionCard
action={action}
diags={diagMap[actionDiagnosticKey(actionIndex)] ?? []}
@ -1081,21 +1110,26 @@ function ActionInspector(): React.ReactElement {
actions={actions}
components={allComponents}
variables={variables}
secrets={secrets}
onChange={(updatedAction) =>
handleUpdateAction(actionIndex, updatedAction)
}
onDone={() => setEditingActionIndex(null)}
/>
)}
</React.Fragment>
))
</div>
))}
</div>
)}
</div>
{/* ══ Bindings section ═════════════════════════════════════════ */}
<div className={styles.section}>
<div className={styles.sectionHeader}>
<span className={styles.sectionTitle}>Bindings</span>
<div>
<div className={styles.sectionTitle}>Response Bindings</div>
<div className={styles.sectionDescription}>Routes successful action responses into components or runtime variables.</div>
</div>
<div className={styles.sectionHeaderMeta}>
<span className={styles.sectionCount}>{bindings.length}</span>
<button type="button" className={styles.primaryButton} data-testid="add-response-binding" onClick={handleAddBinding} disabled={actions.length === 0 || targetOptions.length === 0 || editingBindingIndex !== null} title={actions.length === 0 ? 'Add a REST action first.' : targetOptions.length === 0 ? 'Add a supported target component or variable first.' : undefined}>+ Add response binding</button>
@ -1120,8 +1154,9 @@ function ActionInspector(): React.ReactElement {
component or variable target to configure one visually.
</div>
) : (
bindings.map((binding, bindingIndex) => (
<React.Fragment key={`${binding.id}-${bindingIndex}`}>
<div className={styles.recordList}>
{bindings.map((binding, bindingIndex) => (
<div className={styles.recordGroup} key={`${binding.id}-${bindingIndex}`}>
<BindingCard binding={binding} allComponents={allComponents} allActions={actions} diags={diagMap[binding.id] ?? []} onEdit={() => setEditingBindingIndex(bindingIndex)} onDelete={() => {
if (window.confirm(`Delete binding "${binding.id}"?`)) {
setDoc((current) => removeBindingAt(current, bindingIndex));
@ -1132,28 +1167,33 @@ function ActionInspector(): React.ReactElement {
}
}} />
{editingBindingIndex === bindingIndex && <ResponseBindingEditor binding={binding} actions={actions} components={allComponents} variables={variables} onChange={(updated) => setDoc((current) => replaceBindingAt(current, bindingIndex, updated))} onDone={() => setEditingBindingIndex(null)} />}
</React.Fragment>
))
</div>
))}
</div>
)}
</div>
<div className={styles.section}>
<div className={styles.sectionHeader}>
<span className={styles.sectionTitle}>Variables</span>
<div>
<div className={styles.sectionTitle}>Variables</div>
<div className={styles.sectionDescription}>Typed defaults copied into ephemeral Preview runtime state.</div>
</div>
<div className={styles.sectionHeaderMeta}><span className={styles.sectionCount}>{Object.keys(variables).length}</span><button type="button" className={styles.primaryButton} data-testid="add-variable" disabled={addingVariable || editingVariableName !== null} onClick={() => setAddingVariable(true)}>+ Add variable</button></div>
</div>
{addingVariable && <VariableEditor name="" variable={{ type: 'string' }} variables={variables} isNew onSave={(name, variable) => { setDoc((current) => setVariable(current, name, variable)); setAddingVariable(false); }} onCancel={() => setAddingVariable(false)} />}
{Object.keys(variables).length === 0 && !addingVariable ? <div className={styles.empty}>No variables declared.</div> : Object.entries(variables).map(([name, variable]) => (
<React.Fragment key={name}>
{Object.keys(variables).length === 0 && !addingVariable ? <div className={styles.empty}>No variables declared.</div> : <div className={styles.recordList}>{Object.entries(variables).map(([name, variable]) => (
<div className={styles.recordGroup} key={name}>
<div className={styles.variableCard}>
<div><span className={styles.bindingId}>{name}</span> <span className={styles.triggerBadge}>{variable.type}</span></div>
<code className={styles.bindingExpr}>{variable.defaultValue === undefined ? 'No default value' : JSON.stringify(variable.defaultValue)}</code>
<div className={styles.recordType}>Runtime variable</div>
<div className={styles.bindingCardHeader}><span className={styles.variableName}>{name}</span> <span className={styles.triggerBadge}>{variable.type}</span></div>
<div className={styles.variableDefault}><span className={styles.bindingLabel}>Default</span><code className={styles.bindingExpr}>{variable.defaultValue === undefined ? 'No default value' : JSON.stringify(variable.defaultValue)}</code></div>
{variable.description && <span className={styles.actionDescription}>{variable.description}</span>}
<div className={styles.actionControls}><button type="button" className={styles.smallButton} aria-label={`Edit variable ${name}`} onClick={() => setEditingVariableName(name)}>Edit</button><button type="button" className={styles.dangerButton} aria-label={`Delete variable ${name}`} onClick={() => handleDeleteVariable(name)}>Delete</button></div>
</div>
{editingVariableName === name && <VariableEditor name={name} variable={variable} variables={variables} onSave={(nextName, nextVariable) => { setDoc((current) => setVariable(current, nextName, nextVariable, name)); setEditingVariableName(null); }} onCancel={() => setEditingVariableName(null)} />}
</React.Fragment>
))}
</div>
))}</div>}
</div>
{/* ══ Overall summary (only shown when there are issues) ═══════ */}

View File

@ -57,4 +57,15 @@ describe('RequestInputEditor', () => {
));
expect(container.textContent).not.toContain('Path');
});
test('places the request destination before the value source', () => {
act(() => root.render(
<RequestInputEditor action={action} components={[]} variables={{ environment: { type: 'string' } }} onChange={jest.fn()} />,
));
const selects = Array.from(container.querySelectorAll('select'));
expect(selects[0].getAttribute('data-testid')).toBe('request-input-target');
expect(selects[1].getAttribute('data-testid')).toBe('request-input-source');
expect(container.textContent).toContain('Request destination');
expect(container.textContent).toContain('Value source');
});
});

View File

@ -47,10 +47,8 @@ export default function RequestInputEditor({ action, components, variables, onCh
Insert an executed component or variable template. Header and query destinations replace that value; URL and body destinations append.
</div>
<div className={styles.requestInputRow}>
<select className={styles.formSelect} data-testid="request-input-source" value={sourceId} onChange={(event) => setSourceId(event.target.value)}>
<option value="">Select component or variable</option>
{sources.map((candidate) => <option key={candidate.id} value={candidate.id}>{candidate.label}</option>)}
</select>
<label className={styles.formField}>
<span className={styles.formLabel}>Request destination</span>
<select className={styles.formSelect} data-testid="request-input-target" value={targetValue} onChange={(event) => setTargetValue(event.target.value)}>
<option value="">Select request destination</option>
{targets.map((candidate) => {
@ -63,6 +61,14 @@ export default function RequestInputEditor({ action, components, variables, onCh
return <option key={value} value={value}>{label}</option>;
})}
</select>
</label>
<label className={styles.formField}>
<span className={styles.formLabel}>Value source</span>
<select className={styles.formSelect} data-testid="request-input-source" value={sourceId} onChange={(event) => setSourceId(event.target.value)}>
<option value="">Select component or variable</option>
{sources.map((candidate) => <option key={candidate.id} value={candidate.id}>{candidate.label}</option>)}
</select>
</label>
<button type="button" className={styles.smallButton} disabled={!source || !target} onClick={() => {
if (source && target) onChange(insertRequestInput(action, target, source.template));
}}>Insert reference</button>

View File

@ -19,16 +19,29 @@ import type {
} from './actionEditorUtils';
import styles from './ActionInspector.module.css';
import RequestInputEditor from './RequestInputEditor';
import TemplateSuggestions from './TemplateSuggestions';
import { requestInputSources } from './requestInputUtils';
import type { RequestInputSource } from './requestInputUtils';
import type { SecretMetadata } from '../../api/secretsApi';
const COMMON_HEADER_NAMES = [
'Accept',
'Content-Type',
'If-Match',
'If-None-Match',
'User-Agent',
'X-Request-ID',
];
const AUTHENTICATION_OPTIONS: Array<{
value: AuthenticationType;
label: string;
}> = [
{ value: 'anonymous', label: 'Anonymous' },
{ value: 'bearerToken', label: 'Bearer token (Slice 3)' },
{ value: 'basicAuth', label: 'Basic authentication (Slice 3)' },
{ value: 'apiKeyHeader', label: 'API key header (Slice 3)' },
{ value: 'apiKeyQueryParameter', label: 'API key query parameter (Slice 3)' },
{ value: 'bearerToken', label: 'Bearer token' },
{ value: 'basicAuth', label: 'Basic authentication' },
{ value: 'apiKeyHeader', label: 'API key header' },
{ value: 'apiKeyQueryParameter', label: 'API key query parameter' },
];
type KeyValueEditorProps = {
@ -40,6 +53,7 @@ type KeyValueEditorProps = {
value: Record<string, string>;
onChange: (value: Record<string, string>) => void;
onValidityChange: (valid: boolean) => void;
templateSources?: RequestInputSource[];
};
function makeRows(value: Record<string, string>): KeyValueRow[] {
@ -59,6 +73,7 @@ function KeyValueEditor({
value,
onChange,
onValidityChange,
templateSources = [],
}: KeyValueEditorProps): React.ReactElement {
const [rows, setRows] = useState<KeyValueRow[]>(() => makeRows(value));
const [error, setError] = useState<string | null>(null);
@ -145,9 +160,11 @@ function KeyValueEditor({
className={styles.formInput}
aria-label={`${title} key`}
placeholder={keyPlaceholder}
list={keyKind === 'header' ? `${fieldName}-common-header-names` : undefined}
value={row.key}
onChange={(event) => handleChange(row.id, 'key', event.target.value)}
/>
<div className={styles.templateField}>
<input
className={styles.formInput}
aria-label={`${title} value`}
@ -155,6 +172,13 @@ function KeyValueEditor({
value={row.value}
onChange={(event) => handleChange(row.id, 'value', event.target.value)}
/>
<TemplateSuggestions
value={row.value}
sources={templateSources}
onChange={(nextValue) => handleChange(row.id, 'value', nextValue)}
testId={`${fieldName}-value-suggestions-${row.id}`}
/>
</div>
<button
type="button"
className={styles.removeRowButton}
@ -166,6 +190,12 @@ function KeyValueEditor({
</div>
))}
{keyKind === 'header' && (
<datalist id={`${fieldName}-common-header-names`}>
{COMMON_HEADER_NAMES.map((name) => <option key={name} value={name} />)}
</datalist>
)}
{error && (
<div className={styles.inlineError} role="alert">
{error} Invalid rows remain local until corrected.
@ -180,6 +210,7 @@ export type RestActionEditorProps = {
actions: RestAction[];
components: CanvasComponent[];
variables: Record<string, Variable>;
secrets?: SecretMetadata[];
onChange: (action: RestAction) => void;
onDone: () => void;
};
@ -189,6 +220,7 @@ function RestActionEditor({
actions,
components,
variables,
secrets = [],
onChange,
onDone,
}: RestActionEditorProps): React.ReactElement {
@ -206,6 +238,10 @@ function RestActionEditor({
() => validateRestAction(action, actions),
[action, actions],
);
const templateSources = useMemo(
() => requestInputSources(components, variables),
[components, variables],
);
useEffect(() => {
if (action === lastEmittedAction.current) {
@ -365,21 +401,28 @@ function RestActionEditor({
</select>
</label>
<label className={styles.urlField}>
<span className={styles.formLabel}>Endpoint URL</span>
<div className={styles.urlField}>
<label className={styles.formLabel} htmlFor="action-url">Endpoint URL</label>
<input
id="action-url"
className={styles.formInput}
data-testid="action-url"
value={urlDraft}
placeholder="https://api.example.com/items/{{itemId}}"
onChange={(event) => handleUrlDraftChange(event.target.value)}
/>
<TemplateSuggestions
value={urlDraft}
sources={templateSources}
onChange={handleUrlDraftChange}
testId="action-url-suggestions"
/>
{!urlDraftValid && (
<span className={styles.inlineError} role="alert">
Enter absolute HTTP or HTTPS without spaces or embedded credentials.
</span>
)}
</label>
</div>
</div>
<label className={styles.formField}>
@ -391,29 +434,48 @@ function RestActionEditor({
onChange={(event) => {
const nextAuthenticationType =
event.target.value as AuthenticationType;
if (nextAuthenticationType === 'anonymous') {
update('authenticationType', nextAuthenticationType);
} else {
event.currentTarget.value = action.authenticationType;
}
onChange({
...action,
authenticationType: nextAuthenticationType,
secretReferenceId: nextAuthenticationType === 'anonymous'
? undefined
: action.secretReferenceId,
});
}}
>
{AUTHENTICATION_OPTIONS.map((option) => (
<option
key={option.value}
value={option.value}
disabled={option.value !== 'anonymous'}
>
{option.label}
</option>
))}
</select>
<span className={styles.formHint}>
Slice 2 creates anonymous actions only. Authentication and secrets are
configured in Slice 3.
Credential-backed modes resolve an encrypted server-side secret at execution time.
</span>
</label>
{action.authenticationType !== 'anonymous' && (
<label className={styles.formField}>
<span className={styles.formLabel}>Stored secret</span>
<select
className={styles.formSelect}
data-testid="action-secret-reference"
value={action.secretReferenceId ?? ''}
onChange={(event) => update('secretReferenceId', event.target.value || undefined)}
>
<option value="">Select a compatible secret</option>
{secrets.filter((secret) => secret.authenticationType === action.authenticationType).map((secret) => <option key={secret.id} value={secret.id}>{secret.name}</option>)}
{action.secretReferenceId && !secrets.some((secret) => secret.id === action.secretReferenceId) && <option value={action.secretReferenceId}>Unavailable reference ({action.secretReferenceId})</option>}
</select>
<span className={styles.formHint}>
Only compatible encrypted server-side secrets are offered.
</span>
</label>
)}
<details className={styles.actionEditorDetails} open>
<summary>Request parameters</summary>
<div className={styles.detailsBody}>
@ -434,6 +496,7 @@ function RestActionEditor({
onValidityChange={(valid) =>
handleMapValidityChange('headers', valid)
}
templateSources={templateSources}
/>
<KeyValueEditor
key={`${action.id}-query`}
@ -445,6 +508,7 @@ function RestActionEditor({
onValidityChange={(valid) =>
handleMapValidityChange('queryParameters', valid)
}
templateSources={templateSources}
/>
<KeyValueEditor
key={`${action.id}-path`}
@ -465,9 +529,10 @@ function RestActionEditor({
<details className={styles.actionEditorDetails} open>
<summary>Request body</summary>
<div className={styles.detailsBody}>
<label className={styles.formField}>
<span className={styles.formLabel}>Body template</span>
<div className={styles.formField}>
<label className={styles.formLabel} htmlFor="action-body">Body template</label>
<textarea
id="action-body"
className={[styles.formTextarea, styles.bodyTemplate].join(' ')}
data-testid="action-body"
value={action.bodyTemplate ?? ''}
@ -476,13 +541,19 @@ function RestActionEditor({
rows={7}
spellCheck={false}
/>
<TemplateSuggestions
value={action.bodyTemplate ?? ''}
sources={templateSources}
onChange={(nextValue) => update('bodyTemplate', nextValue)}
testId="action-body-suggestions"
/>
{(action.method === 'GET' || action.method === 'DELETE') && (
<span className={styles.formHint}>
{action.method} requests retain this configuration but do not
send a request body.
</span>
)}
</label>
</div>
</div>
</details>
</form>

View File

@ -0,0 +1,41 @@
import React, { act } from 'react';
import { createRoot, Root } from 'react-dom/client';
import SecretManager from './SecretManager';
import * as api from '../../api/secretsApi';
jest.mock('../../api/secretsApi');
const mockedApi = api as jest.Mocked<typeof api>;
const metadata = { id: 'secret_1', name: 'Production token', authenticationType: 'bearerToken' as const, createdAt: 'now', updatedAt: 'now' };
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
function setInput(element: HTMLInputElement, value: string): void {
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set!;
act(() => { setter.call(element, value); element.dispatchEvent(new Event('input', { bubbles: true })); });
}
describe('SecretManager', () => {
let container: HTMLDivElement; let root: Root;
beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); root = createRoot(container); });
afterEach(() => { act(() => root.unmount()); container.remove(); jest.resetAllMocks(); });
test('submits a masked credential and refreshes metadata without displaying its value', async () => {
mockedApi.createSecret.mockResolvedValue(metadata);
const changed = jest.fn().mockResolvedValue(undefined);
await act(async () => { root.render(<SecretManager secrets={[]} actions={[]} onChanged={changed} />); });
const inputs = container.querySelectorAll<HTMLInputElement>('input');
setInput(inputs[0], 'Production token'); setInput(inputs[1], 'stored-token-value');
expect(inputs[1].type).toBe('password');
await act(async () => { container.querySelector<HTMLButtonElement>('button')!.click(); });
expect(mockedApi.createSecret).toHaveBeenCalledWith('Production token', 'bearerToken', { token: 'stored-token-value' });
expect(changed).toHaveBeenCalled();
expect(container.textContent).not.toContain('stored-token-value');
});
test('blocks deletion when an unsaved action references the secret', async () => {
await act(async () => { root.render(<SecretManager secrets={[metadata]} actions={[{ id: 'a', name: 'Protected request', method: 'GET', url: 'https://example.test', authenticationType: 'bearerToken', secretReferenceId: metadata.id }]} onChanged={jest.fn()} />); });
const deleteButton = Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'Delete')!;
await act(async () => { deleteButton.click(); });
expect(mockedApi.deleteSecret).not.toHaveBeenCalled();
expect(container.textContent).toContain('referenced by Protected request');
});
});

View File

@ -0,0 +1,40 @@
import React, { useState } from 'react';
import type { RestAction } from '../../types/project';
import * as api from '../../api/secretsApi';
import type { CredentialType, SecretMetadata, SecretValue } from '../../api/secretsApi';
import styles from './ActionInspector.module.css';
type Props = { secrets: SecretMetadata[]; actions: RestAction[]; onChanged: () => Promise<void> };
const types: Array<{ value: CredentialType; label: string }> = [
{ value: 'bearerToken', label: 'Bearer token' }, { value: 'basicAuth', label: 'Basic authentication' },
{ value: 'apiKeyHeader', label: 'API key header' }, { value: 'apiKeyQueryParameter', label: 'API key query parameter' },
];
export default function SecretManager({ secrets, actions, onChanged }: Props): React.ReactElement {
const [editing, setEditing] = useState<SecretMetadata | null>(null);
const [name, setName] = useState(''); const [type, setType] = useState<CredentialType>('bearerToken');
const [first, setFirst] = useState(''); const [second, setSecond] = useState('');
const [error, setError] = useState(''); const [busy, setBusy] = useState(false);
const reset = () => { setEditing(null); setName(''); setFirst(''); setSecond(''); setError(''); };
const value = (): SecretValue => type === 'basicAuth' ? { username: first, password: second } : type === 'bearerToken' ? { token: first } : { parameterName: first, value: second };
const save = async () => {
setBusy(true); setError('');
try { editing ? await api.replaceSecret(editing.id, name, type, value()) : await api.createSecret(name, type, value()); await onChanged(); reset(); }
catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); }
};
const remove = async (secret: SecretMetadata) => {
const refs = actions.filter((a) => a.secretReferenceId === secret.id);
if (refs.length) { setError(`Cannot delete “${secret.name}”; referenced by ${refs.map((a) => a.name).join(', ')}.`); return; }
if (!window.confirm(`Delete secret “${secret.name}”? This cannot be undone.`)) return;
try { await api.deleteSecret(secret.id); await onChanged(); } catch (e) { setError(e instanceof Error ? e.message : String(e)); }
};
return <div className={styles.section}>
<div className={styles.sectionHeader}><div><div className={styles.sectionTitle}>Secrets</div><div className={styles.sectionDescription}>Encrypted server-side credentials. Stored values are never displayed after submission.</div></div><span className={styles.sectionCount}>{secrets.length}</span></div>
{secrets.map((secret) => <div className={styles.recordGroup} key={secret.id}><div className={styles.actionCard}><strong>{secret.name}</strong> <span className={styles.authBadge}>{types.find((item) => item.value === secret.authenticationType)?.label}</span><div className={styles.actionId}>{secret.id}</div><div className={styles.actionControls}><button className={styles.smallButton} onClick={() => { setEditing(secret); setName(secret.name); setType(secret.authenticationType); setFirst(''); setSecond(''); }}>Replace value</button><button className={styles.dangerButton} onClick={() => void remove(secret)}>Delete</button></div></div></div>)}
<div className={styles.editorPanel}>
<div className={styles.formGrid}><label className={styles.formField}><span className={styles.formLabel}>Secret name</span><input className={styles.formInput} value={name} onChange={(e) => setName(e.target.value)} /></label><label className={styles.formField}><span className={styles.formLabel}>Authentication type</span><select className={styles.formSelect} value={type} disabled={!!editing} onChange={(e) => setType(e.target.value as CredentialType)}>{types.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</select></label></div>
<div className={styles.formGrid}><label className={styles.formField}><span className={styles.formLabel}>{type === 'basicAuth' ? 'Username' : type === 'bearerToken' ? 'Token' : type === 'apiKeyHeader' ? 'Header name' : 'Query parameter name'}</span><input className={styles.formInput} type={type === 'bearerToken' ? 'password' : 'text'} value={first} onChange={(e) => setFirst(e.target.value)} autoComplete="off" /></label>{type !== 'bearerToken' && <label className={styles.formField}><span className={styles.formLabel}>{type === 'basicAuth' ? 'Password' : 'API key'}</span><input className={styles.formInput} type="password" value={second} onChange={(e) => setSecond(e.target.value)} autoComplete="new-password" /></label>}</div>
{error && <div className={styles.inlineError} role="alert">{error}</div>}<div className={styles.editorActions}><button className={styles.primaryButton} disabled={busy || !name.trim() || !first || (type !== 'bearerToken' && !second)} onClick={() => void save()}>{editing ? 'Replace secret' : 'Add secret'}</button>{editing && <button className={styles.smallButton} onClick={reset}>Cancel</button>}</div>
</div>
</div>;
}

View File

@ -0,0 +1,50 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import type { Root } from 'react-dom/client';
import TemplateSuggestions from './TemplateSuggestions';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
describe('TemplateSuggestions', () => {
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
container = document.createElement('div');
root = createRoot(container);
});
afterEach(() => act(() => root.unmount()));
test('offers canonical references after an opening template and preserves free-form prefix text', () => {
const onChange = jest.fn();
act(() => root.render(
<TemplateSuggestions
value="prefix {{"
sources={[
{ id: 'component:input', label: 'Component: itemInput', template: '{{components.itemInput.value}}' },
{ id: 'variable:environment', label: 'Variable: environment', template: '{{variables.environment}}' },
]}
onChange={onChange}
testId="suggestions"
/>,
));
expect(container.textContent).toContain('Component: itemInput');
expect(container.textContent).toContain('Variable: environment');
act(() => container.querySelectorAll('button')[1].click());
expect(onChange).toHaveBeenCalledWith('prefix {{variables.environment}}');
});
test('stays hidden when the template is complete', () => {
act(() => root.render(
<TemplateSuggestions
value="{{variables.environment}}"
sources={[{ id: 'variable:environment', label: 'Variable: environment', template: '{{variables.environment}}' }]}
onChange={jest.fn()}
testId="suggestions"
/>,
));
expect(container.querySelector('[data-testid="suggestions"]')).toBeNull();
});
});

View File

@ -0,0 +1,32 @@
import React from 'react';
import type { RequestInputSource } from './requestInputUtils';
import { applyTemplateSuggestion, hasOpenTemplate } from './requestInputUtils';
import styles from './ActionInspector.module.css';
type Props = {
value: string;
sources: RequestInputSource[];
onChange: (value: string) => void;
testId: string;
};
export default function TemplateSuggestions({ value, sources, onChange, testId }: Props): React.ReactElement | null {
if (!hasOpenTemplate(value) || sources.length === 0) return null;
return (
<div className={styles.templateSuggestions} data-testid={testId}>
<span className={styles.templateSuggestionsLabel}>Insert reference</span>
{sources.map((source) => (
<button
key={source.id}
type="button"
className={styles.templateSuggestion}
onClick={() => onChange(applyTemplateSuggestion(value, source.template))}
>
<span>{source.label}</span>
<code>{source.template}</code>
</button>
))}
</div>
);
}

View File

@ -257,7 +257,7 @@ describe('REST action validation', () => {
expect(issues.some((issue) => issue.message.includes('duplicated'))).toBe(true);
});
test('reports method/body and deferred authentication behavior without mutation', () => {
test('reports method/body and missing secret-reference behavior without mutation', () => {
const action = makeAction({
method: 'GET',
bodyTemplate: '{"test":true}',
@ -272,8 +272,8 @@ describe('REST action validation', () => {
severity: 'info',
}),
expect.objectContaining({
field: 'authenticationType',
severity: 'info',
field: 'secretReferenceId',
severity: 'warn',
}),
]));
expect(action.authenticationType).toBe('bearerToken');

View File

@ -386,10 +386,11 @@ export function validateRestAction(
if (action.authenticationType !== 'anonymous') {
issues.push({
field: 'authenticationType',
severity: 'info',
message:
'Credential-backed authentication is configured but executes anonymously until Slice 3.',
field: 'secretReferenceId',
severity: action.secretReferenceId?.trim() ? 'info' : 'warn',
message: action.secretReferenceId?.trim()
? 'Credential material is resolved and injected by the backend at execution time.'
: 'Select or enter a server-side secret reference before executing this action.',
});
}

View File

@ -1,5 +1,5 @@
import type { RestAction } from '../../types/project';
import { insertRequestInput, requestInputSources } from './requestInputUtils';
import { applyTemplateSuggestion, hasOpenTemplate, insertRequestInput, requestInputSources } from './requestInputUtils';
const action: RestAction = {
id: 'action',
@ -40,4 +40,22 @@ describe('visual request input authoring', () => {
insertRequestInput(action, { location: 'headers', key: 'X-Item' }, '{{variables.id}}');
expect(action.headers).toEqual({ 'X-Item': 'static' });
});
test('detects an unfinished template and replaces it with a canonical suggestion', () => {
expect(hasOpenTemplate('prefix {{')).toBe(true);
expect(hasOpenTemplate('prefix {{variables.id}}')).toBe(false);
expect(applyTemplateSuggestion('prefix {{var', '{{variables.id}}'))
.toBe('prefix {{variables.id}}');
});
test('excludes ambiguous duplicate component names from reference suggestions', () => {
const duplicate = {
id: 'first', name: 'duplicate', type: 'TextInput' as const,
position: { x: 0, y: 0 }, size: { width: 100, height: 40 }, properties: {},
};
expect(requestInputSources([
duplicate,
{ ...duplicate, id: 'second' },
], {})).toEqual([]);
});
});

View File

@ -11,8 +11,12 @@ export function requestInputSources(
components: CanvasComponent[],
variables: Record<string, Variable>,
): RequestInputSource[] {
const componentNameCounts = components.reduce<Record<string, number>>((counts, component) => {
counts[component.name] = (counts[component.name] ?? 0) + 1;
return counts;
}, {});
return [
...components.map((component) => ({
...components.filter((component) => componentNameCounts[component.name] === 1).map((component) => ({
id: `component:${component.id}`,
label: `Component: ${component.name}`,
template: `{{components.${component.name}.value}}`,
@ -38,3 +42,14 @@ export function insertRequestInput(action: RestAction, target: RequestInputTarge
},
};
}
export function hasOpenTemplate(value: string): boolean {
return value.lastIndexOf('{{') > value.lastIndexOf('}}');
}
export function applyTemplateSuggestion(value: string, template: string): string {
const templateStart = value.lastIndexOf('{{');
return templateStart >= 0
? `${value.slice(0, templateStart)}${template}`
: `${value}${template}`;
}

View File

@ -6,8 +6,16 @@ import styles from './Preview.module.css';
// ── Helpers ───────────────────────────────────────────────────────────────────
export type CanvasDimensions = { width: number; height: number };
export type RenderedComponentBounds = {
left: number;
top: number;
width: number;
height: number;
};
/** Compute the minimum canvas dimensions needed to fit all components. */
function canvasBounds(components: { position: { x: number; y: number }; size: { width: number; height: number } }[]): { width: number; height: number } {
export function canvasBounds(components: { position: { x: number; y: number }; size: { width: number; height: number } }[]): CanvasDimensions {
if (components.length === 0) return { width: 800, height: 600 };
let maxX = 0;
let maxY = 0;
@ -18,6 +26,23 @@ function canvasBounds(components: { position: { x: number; y: number }; size: {
return { width: Math.max(maxX + 48, 800), height: Math.max(maxY + 48, 400) };
}
/** Grow design-time bounds to contain wrappers that expand at runtime. */
export function expandCanvasBounds(
designBounds: CanvasDimensions,
renderedComponents: RenderedComponentBounds[],
): CanvasDimensions {
let maxX = designBounds.width - 48;
let maxY = designBounds.height - 48;
for (const component of renderedComponents) {
maxX = Math.max(maxX, component.left + component.width);
maxY = Math.max(maxY, component.top + component.height);
}
return {
width: Math.max(designBounds.width, maxX + 48),
height: Math.max(designBounds.height, maxY + 48),
};
}
// ── Component ─────────────────────────────────────────────────────────────────
function Preview(): React.ReactElement {
@ -28,7 +53,46 @@ function Preview(): React.ReactElement {
const [activePageIndex, setActivePageIndex] = React.useState(0);
const activePage = pages[activePageIndex] ?? null;
const bounds = activePage ? canvasBounds(activePage.components) : { width: 800, height: 400 };
const bounds = React.useMemo(
() => activePage ? canvasBounds(activePage.components) : { width: 800, height: 400 },
[activePage],
);
const canvasRef = React.useRef<HTMLDivElement | null>(null);
const [runtimeBounds, setRuntimeBounds] = React.useState<CanvasDimensions>(bounds);
React.useLayoutEffect(() => {
const canvas = canvasRef.current;
if (!canvas || !activePage) {
setRuntimeBounds(bounds);
return;
}
const wrappers = Array.from(canvas.children)
.filter((child): child is HTMLElement => child instanceof HTMLElement);
const measure = () => {
const rendered = wrappers.map((wrapper) => ({
left: wrapper.offsetLeft,
top: wrapper.offsetTop,
width: Math.max(wrapper.offsetWidth, wrapper.scrollWidth),
height: Math.max(wrapper.offsetHeight, wrapper.scrollHeight),
}));
const next = expandCanvasBounds(bounds, rendered);
setRuntimeBounds((current) => (
current.width === next.width && current.height === next.height ? current : next
));
};
measure();
if (typeof ResizeObserver === 'undefined') return;
const observer = new ResizeObserver(measure);
wrappers.forEach((wrapper) => observer.observe(wrapper));
return () => observer.disconnect();
}, [activePage, bounds]);
const displayBounds = {
width: Math.max(bounds.width, runtimeBounds.width),
height: Math.max(bounds.height, runtimeBounds.height),
};
// ── Preview runtime (binding execution, component state) ─────────────────
const runtime = usePreviewRuntime(doc);
@ -87,7 +151,8 @@ function Preview(): React.ReactElement {
) : (
<div
className={styles.canvas}
style={{ width: bounds.width, height: bounds.height }}
ref={canvasRef}
style={{ width: displayBounds.width, height: displayBounds.height }}
>
{activePage.components.map((c) => (
<PreviewComponent

View File

@ -5,6 +5,18 @@
box-sizing: border-box;
}
.wrapper[data-custom-font-size='true'] * {
font-size: inherit !important;
}
.wrapper[data-custom-text-color='true'] * {
color: inherit !important;
}
.wrapper[data-custom-background-color='true'] * {
background-color: inherit !important;
}
/* ── Label ────────────────────────────────────────────────────────── */
.label {

View File

@ -1,5 +1,5 @@
import React from 'react';
import type { CanvasComponent, DropdownOption, TableColumn, TableRow } from '../../types/project';
import type { CanvasComponent, ComponentStyle, DropdownOption, TableColumn, TableRow } from '../../types/project';
import type { ComponentRuntimeState } from './usePreviewRuntime';
import { labelDisplayValue } from './bindingUtils';
import styles from './PreviewComponent.module.css';
@ -58,6 +58,7 @@ function TextInputRenderer({
placeholder,
value,
disabled,
required,
onChange,
}: {
label: string;
@ -65,6 +66,7 @@ function TextInputRenderer({
value: string;
disabled: boolean;
onChange: (value: string) => void;
required: boolean;
}): React.ReactElement {
return (
<div className={styles.inputWrapper}>
@ -75,17 +77,19 @@ function TextInputRenderer({
value={value}
placeholder={placeholder}
disabled={disabled}
required={required}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
}
function TextAreaRenderer({ label, placeholder, value, disabled, onChange }: {
function TextAreaRenderer({ label, placeholder, value, disabled, required, onChange }: {
label: string;
placeholder: string;
value: string;
disabled: boolean;
required: boolean;
onChange: (value: string) => void;
}): React.ReactElement {
return (
@ -96,6 +100,7 @@ function TextAreaRenderer({ label, placeholder, value, disabled, onChange }: {
value={value}
placeholder={placeholder}
disabled={disabled}
required={required}
onChange={(e) => onChange(e.target.value)}
/>
</div>
@ -167,6 +172,7 @@ function ContainerRenderer({ label, body }: { label: string; body: string }): Re
type DropdownRendererProps = {
label: string;
placeholder: string;
required: boolean;
options: DropdownOption[];
/** Current selection: runtimeState.value if set, else configured properties.value */
selectedValue: string;
@ -177,6 +183,7 @@ type DropdownRendererProps = {
function DropdownRenderer({
label,
placeholder,
required,
options,
selectedValue,
disabled,
@ -190,6 +197,7 @@ function DropdownRenderer({
value={selectedValue}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
required={required}
>
{/* Disabled placeholder option — shown when no value selected */}
<option value="" disabled>
@ -255,7 +263,7 @@ function TableRenderer({
<thead>
<tr>
{effectiveCols.map((col) => (
<th key={col.key} className={styles.tableTh}>{col.header}</th>
<th key={col.key} className={styles.tableTh} style={col.width ? { width: col.width, minWidth: col.width } : undefined}>{col.header}</th>
))}
</tr>
</thead>
@ -282,7 +290,7 @@ function TableRenderer({
onClick={() => { if (!disabled) onRowSelect(ri, row); }}
>
{effectiveCols.map((col) => (
<td key={col.key} className={styles.tableTd}>
<td key={col.key} className={styles.tableTd} style={col.width ? { width: col.width, minWidth: col.width } : undefined}>
{renderTableCellValue(row[col.key])}
</td>
))}
@ -395,9 +403,17 @@ function PreviewComponent({
const defaultValue = typeof properties.defaultValue === 'string' ? properties.defaultValue : '';
const disabled = properties.disabled === true;
const appearance =
properties.style && typeof properties.style === 'object'
? properties.style as ComponentStyle
: {};
return (
<div
className={styles.wrapper}
data-custom-font-size={appearance.fontSize !== undefined ? 'true' : undefined}
data-custom-text-color={appearance.textColor ? 'true' : undefined}
data-custom-background-color={appearance.backgroundColor ? 'true' : undefined}
style={{
left: position.x,
top: position.y,
@ -405,6 +421,9 @@ function PreviewComponent({
...(type === 'TextInput' || type === 'TextArea' || type === 'JsonViewer' || type === 'Dropdown' || type === 'Table'
? { minHeight: size.height }
: { height: size.height }),
...(appearance.fontSize !== undefined ? { fontSize: appearance.fontSize } : {}),
...(appearance.textColor ? { color: appearance.textColor } : {}),
...(appearance.backgroundColor ? { backgroundColor: appearance.backgroundColor } : {}),
}}
>
{type === 'Label' && (
@ -426,6 +445,7 @@ function PreviewComponent({
placeholder={placeholder}
value={runtimeState?.textValue ?? defaultValue}
disabled={disabled}
required={properties.required === true}
onChange={(val) => onTextInputChange(component.id, val)}
/>
)}
@ -443,6 +463,7 @@ function PreviewComponent({
placeholder={placeholder}
value={runtimeState?.textValue ?? (runtimeState?.value !== undefined ? labelDisplayValue(runtimeState.value) : defaultValue)}
disabled={disabled}
required={properties.required === true}
onChange={(val) => onTextInputChange(component.id, val)}
/>
)}
@ -540,6 +561,7 @@ function PreviewComponent({
options={effectiveOptions}
selectedValue={selectedValue}
disabled={disabled}
required={properties.required === true}
onChange={(val) => onDropdownChange(component.id, val)}
/>
);

View File

@ -3,6 +3,7 @@ import { renderToStaticMarkup } from 'react-dom/server';
import type { CanvasComponent, ComponentType } from '../../types/project';
import PreviewComponent from './PreviewComponent';
import type { ComponentRuntimeState } from './usePreviewRuntime';
import { expandCanvasBounds } from './Preview';
function render(type: ComponentType, properties: CanvasComponent['properties'], runtimeState?: ComponentRuntimeState): string {
const component: CanvasComponent = {
@ -66,3 +67,78 @@ describe('remaining MVP preview components', () => {
expect(markup).toContain('Card body');
});
});
describe('Preview runtime canvas sizing', () => {
const designBounds = { width: 800, height: 400 };
test('grows the white canvas to contain runtime-expanded content', () => {
expect(expandCanvasBounds(designBounds, [{
left: 24,
top: 300,
width: 500,
height: 360,
}])).toEqual({ width: 800, height: 708 });
});
test('retains design bounds when rendered content fits', () => {
expect(expandCanvasBounds(designBounds, [{
left: 24,
top: 40,
width: 320,
height: 120,
}])).toEqual(designBounds);
});
test('shrinks back to design bounds after runtime content contracts', () => {
const expanded = expandCanvasBounds(
designBounds,
[{ left: 0, top: 300, width: 320, height: 300 }],
);
expect(expanded.height).toBe(648);
expect(expandCanvasBounds(
designBounds,
[{ left: 0, top: 300, width: 320, height: 40 }],
)).toEqual(designBounds);
});
});
describe('MVP component property rendering', () => {
test('renders canonical basic appearance overrides', () => {
const markup = render('Label', {
label: 'Styled',
style: {
fontSize: 20,
textColor: '#112233',
backgroundColor: '#ddeeff',
},
});
expect(markup).toContain('font-size:20px');
expect(markup).toContain('color:#112233');
expect(markup).toContain('background-color:#ddeeff');
expect(markup).toContain('data-custom-font-size="true"');
});
test('renders required semantics for text and choice inputs', () => {
expect(render('TextInput', { required: true })).toContain('required=""');
expect(render('TextArea', { required: true })).toContain('required=""');
expect(render('Dropdown', {
required: true,
options: [{ label: 'Dev', value: 'dev' }],
})).toContain('required=""');
});
test('renders a JSON Viewer design-time default', () => {
const markup = render('JsonViewer', { defaultValue: '{"status":"ready"}' });
expect(markup).toContain('&quot;status&quot;');
expect(markup).toContain('&quot;ready&quot;');
});
test('renders configured Table column widths', () => {
const markup = render('Table', {
columns: [{ key: 'name', header: 'Name', width: 180 }],
rows: [{ name: 'alpha' }],
});
expect(markup).toContain('width:180px');
expect(markup).toContain('min-width:180px');
});
});

View File

@ -9,6 +9,28 @@
touch-action: none; /* required for pointer capture */
}
.wrapper[data-custom-font-size='true'] .content,
.wrapper[data-custom-font-size='true'] .content *,
.wrapper[data-custom-font-size='true'] .tableContent,
.wrapper[data-custom-font-size='true'] .tableContent * {
font-size: inherit !important;
}
.wrapper[data-custom-text-color='true'] .content,
.wrapper[data-custom-text-color='true'] .content *,
.wrapper[data-custom-text-color='true'] .tableContent,
.wrapper[data-custom-text-color='true'] .tableContent * {
color: inherit !important;
}
.wrapper[data-custom-background-color='true'] .content,
.wrapper[data-custom-background-color='true'] .content *,
.wrapper[data-custom-background-color='true'] .tableContent,
.wrapper[data-custom-background-color='true'] .tableContent * {
background-color: inherit !important;
}
.wrapper:hover {
border-color: #3b82d4;
}

View File

@ -1,5 +1,5 @@
import React, { useRef } from 'react';
import type { CanvasComponent as CanvasComponentType, DropdownOption, TableColumn, TableRow } from '../../../types/project';
import type { CanvasComponent as CanvasComponentType, ComponentStyle, DropdownOption, TableColumn, TableRow } from '../../../types/project';
import styles from './CanvasComponent.module.css';
// ── Props ─────────────────────────────────────────────────────────────────────
@ -144,7 +144,7 @@ function TableRenderer({
<thead>
<tr>
{effectiveCols.map((col) => (
<th key={col.key} className={styles.tableTh}>{col.header}</th>
<th key={col.key} className={styles.tableTh} style={col.width ? { width: col.width, minWidth: col.width } : undefined}>{col.header}</th>
))}
</tr>
</thead>
@ -163,7 +163,7 @@ function TableRenderer({
rows.map((row, ri) => (
<tr key={ri}>
{effectiveCols.map((col) => (
<td key={col.key} className={styles.tableTd}>
<td key={col.key} className={styles.tableTd} style={col.width ? { width: col.width, minWidth: col.width } : undefined}>
{renderCellValue(row[col.key])}
</td>
))}
@ -211,6 +211,10 @@ function CanvasComponent({
const label = typeof properties.label === 'string' ? properties.label : '';
const placeholder = typeof properties.placeholder === 'string' ? properties.placeholder : undefined;
const appearance = properties.style && typeof properties.style === 'object'
? properties.style as ComponentStyle
: {};
const dragState = useRef<DragState | null>(null);
// ── Pointer-based drag-to-move ────────────────────────────────────────────
@ -255,12 +259,18 @@ function CanvasComponent({
return (
<div
className={[styles.wrapper, selected ? styles.selected : ''].join(' ')}
data-custom-font-size={appearance.fontSize !== undefined ? 'true' : undefined}
data-custom-text-color={appearance.textColor ? 'true' : undefined}
data-custom-background-color={appearance.backgroundColor ? 'true' : undefined}
style={{
left: position.x,
top: position.y,
width: size.width,
height: type === 'JsonViewer' || type === 'TextInput' || type === 'Table' ? 'auto' : size.height,
minHeight: size.height,
...(appearance.fontSize !== undefined ? { fontSize: appearance.fontSize } : {}),
...(appearance.textColor ? { color: appearance.textColor } : {}),
...(appearance.backgroundColor ? { backgroundColor: appearance.backgroundColor } : {}),
}}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}

View File

@ -4,7 +4,7 @@ import Canvas from './Canvas/Canvas';
import ProjectToolbar from '../ProjectToolbar/ProjectToolbar';
import { useProject } from '../../context/ProjectContext';
import styles from './VisualEditor.module.css';
import type { ComponentType, DropdownOption, TableColumn, TableRow } from '../../types/project';
import type { ComponentStyle, ComponentType, DropdownOption, TableColumn, TableRow } from '../../types/project';
import ButtonEventEditor from './ButtonEventEditor';
import PageEventEditor from './PageEventEditor';
import ComponentDeleteDialog from './ComponentDeleteDialog';
@ -174,6 +174,25 @@ function TableColumnsEditor({ columns, onColumnsChange }: TableColumnsEditorProp
onChange={(e) => handleChange(i, 'key', e.target.value)}
title={!col.key ? 'Key must not be empty' : undefined}
/>
<input
className={styles.infoInput}
style={{ width: 58 }}
type="number"
min={1}
placeholder="Width"
aria-label={`Column ${col.header || col.key || i + 1} width`}
value={col.width ?? ''}
onChange={(event) => onColumnsChange(columns.map((current, columnIndex) =>
columnIndex === i
? {
...current,
width: event.target.value === ''
? undefined
: Math.max(1, Number(event.target.value)),
}
: current
))}
/>
<button
style={{ fontSize: 10, padding: '2px 5px', cursor: 'pointer', background: 'none',
border: '1px solid #d0d7de', borderRadius: 3, color: '#b91c1c', flexShrink: 0 }}
@ -275,7 +294,74 @@ function TableRowsEditor({ rows, onRowsChange }: TableRowsEditorProps): React.Re
);
}
// ── Component ─────────────────────────────────────────────────────────────────
// Basic appearance editor
type AppearanceEditorProps = {
value: ComponentStyle;
onChange: (value: ComponentStyle) => void;
};
function AppearanceEditor({ value, onChange }: AppearanceEditorProps): React.ReactElement {
const setValue = (key: keyof ComponentStyle, next: number | string | undefined) => {
const updated = { ...value };
if (next === undefined) delete updated[key];
else Object.assign(updated, { [key]: next });
onChange(updated);
};
return (
<div style={{ marginTop: 10 }}>
<div className={styles.infoKey} style={{ marginBottom: 4, display: 'block' }}>
Basic appearance
</div>
<div className={styles.infoRow}>
<label className={styles.infoKey} htmlFor="prop-font-size">Font size</label>
<select
id="prop-font-size"
className={styles.infoInput}
value={value.fontSize ?? ''}
onChange={(event) => setValue(
'fontSize',
event.target.value === '' ? undefined : Number(event.target.value),
)}
>
<option value="">Default</option>
{[12, 14, 16, 18, 20, 24, 32].map((size) => (
<option key={size} value={size}>{size}px</option>
))}
</select>
</div>
<div className={styles.infoRow}>
<label className={styles.infoKey} htmlFor="prop-text-color">Text</label>
<input
id="prop-text-color"
type="color"
value={value.textColor ?? '#1f2328'}
onChange={(event) => setValue('textColor', event.target.value)}
aria-label="Component text color"
/>
{value.textColor && (
<button type="button" onClick={() => setValue('textColor', undefined)}>Reset</button>
)}
</div>
<div className={styles.infoRow}>
<label className={styles.infoKey} htmlFor="prop-background-color">Background</label>
<input
id="prop-background-color"
type="color"
value={value.backgroundColor ?? '#ffffff'}
onChange={(event) => setValue('backgroundColor', event.target.value)}
aria-label="Component background color"
/>
{value.backgroundColor && (
<button type="button" onClick={() => setValue('backgroundColor', undefined)}>Reset</button>
)}
</div>
</div>
);
}
// Main component
function VisualEditor(): React.ReactElement {
const {
@ -546,7 +632,7 @@ function VisualEditor(): React.ReactElement {
}
/>
</div>
{['TextInput', 'TextArea', 'Checkbox', 'RadioGroup'].includes(selectedComponent.type) && (
{['TextInput', 'TextArea', 'Checkbox', 'RadioGroup', 'Dropdown'].includes(selectedComponent.type) && (
<div className={styles.infoRow}>
<label className={styles.infoKey} htmlFor="prop-required">Required</label>
<input
@ -622,10 +708,10 @@ function VisualEditor(): React.ReactElement {
/>
</div>
)}
{['TextInput', 'TextArea', 'StatusPanel', 'Container'].includes(selectedComponent.type) && (
{['TextInput', 'TextArea', 'JsonViewer', 'StatusPanel', 'Container'].includes(selectedComponent.type) && (
<div className={styles.infoRow}>
<label className={styles.infoKey} htmlFor="prop-default-value">
{selectedComponent.type === 'Container' ? 'Body' : selectedComponent.type === 'StatusPanel' ? 'Message' : 'Default'}
{selectedComponent.type === 'Container' ? 'Body' : selectedComponent.type === 'StatusPanel' ? 'Message' : selectedComponent.type === 'JsonViewer' ? 'Default JSON' : 'Default'}
</label>
<textarea
id="prop-default-value"
@ -686,6 +772,17 @@ function VisualEditor(): React.ReactElement {
onChange={(events) => updateComponentEvents(selectedComponent.id, events)}
/>
)}
<AppearanceEditor
value={
selectedComponent.properties.style &&
typeof selectedComponent.properties.style === 'object'
? selectedComponent.properties.style as ComponentStyle
: {}
}
onChange={(appearance) =>
updateComponentProperty(selectedComponent.id, 'style', appearance)
}
/>
</div>
) : (
<p className={styles.infoEmpty}>No component selected</p>

View File

@ -231,6 +231,8 @@ export type RestAction = {
bodyTemplate?: string;
/** Authentication strategy. Credentials are resolved by the backend at runtime. */
authenticationType: AuthenticationType;
/** Opaque server-side credential reference. Never contains credential material. */
secretReferenceId?: string;
/**
* @deprecated Use `project.bindings` for response data movement.
* This field is retained for backward compatibility but is not executed
@ -245,6 +247,12 @@ export type Position = { x: number; y: number };
export type Size = { width: number; height: number };
export type ComponentStyle = {
fontSize?: number;
textColor?: string;
backgroundColor?: string;
};
export type ComponentProperties = {
label?: string;
placeholder?: string;
@ -252,6 +260,7 @@ export type ComponentProperties = {
visible?: boolean;
disabled?: boolean;
required?: boolean;
style?: ComponentStyle;
[key: string]: unknown;
};

View File

@ -221,6 +221,16 @@
"visible": { "type": "boolean", "description": "Whether the component is visible at runtime.", "default": true },
"disabled": { "type": "boolean", "description": "Whether the component is disabled at runtime.", "default": false },
"required": { "type": "boolean", "description": "Whether the component requires a value before form submission.", "default": false },
"style": {
"type": "object",
"description": "Basic per-component appearance overrides.",
"additionalProperties": false,
"properties": {
"fontSize": { "type": "number", "minimum": 8, "maximum": 72 },
"textColor": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" },
"backgroundColor": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" }
}
},
"options": {
"type": "array",
"description": "Static option list for Dropdown and RadioGroup components.",
@ -327,6 +337,11 @@
"apiKeyQueryParameter"
]
},
"secretReferenceId": {
"type": "string",
"minLength": 1,
"description": "Opaque reference to a server-side encrypted credential. Required at execution time for credential-backed authentication and omitted for anonymous actions."
},
"responseMapping": {
"type": "array",
"deprecated": true,