Ash API Contract Endpoint Inventory
This inventory is the implementation starting point for Ash API contract unification. It maps externally accessible routes to their handlers, consumers, baseline tests, existing contract docs, classification, and migration decision. A route is not converted solely for uniformity: Ash-backed routes benefit from a generated contract, retained routes have an explicit protocol rationale, and deferred routes have no approved migration in this feature.
Classifications:
ash-backed: externally consumed resource/action contract using Ash and generated OpenAPI as the canonical contract. A compatibility wrapper may remain when it preserves an existing client wire format.retained-controller: infrastructure or workflow protocol intentionally kept in a Phoenix controller with explicit contract documentation and rationale.deferred: externally useful contract whose future need or migration boundary is not established; unchanged in this feature.ui-only: browser/LiveView-only behavior with no durable external contract.
Generated OpenAPI And Ash JSON:API Surface
| Route | Current handler | Consumer | Baseline tests | Contract docs | Classification | Migration decision | Rationale |
|---|---|---|---|---|---|---|---|
GET /openapi.yaml | Plug.Static via NixstasisWeb.Endpoint and NixstasisWeb.static_paths/0 | API readers, docs tooling | Add schema/static contract check if changed | packages/server/priv/static/openapi.yaml, website/docs/reference/contracts.md | ash-backed | Keep generated static artifact | Static generated OpenAPI artifact is the durable file referenced by docs. |
GET /api/json/swaggerui | OpenApiSpex.Plug.SwaggerUI | API readers, developers | Add route smoke test if changed | website/docs/reference/contracts.md | ash-backed | Keep generated UI | Developer UI for the Ash generated API docs; not a product data endpoint. |
GET /api/json/open_api | NixstasisWeb.AshJsonApiRouter | API readers, developers | Add route smoke test if changed | packages/server/priv/static/openapi.yaml, website/docs/reference/contracts.md | ash-backed | Keep generated router endpoint | Machine-readable OpenAPI endpoint served through the Ash JSON:API router. |
/api/json/devices | NixstasisWeb.AshJsonApiRouter / Nixstasis.Devices.Device | Operator/developer API automation, internal docs | packages/server/test/nixstasis_web/controllers/builder_contract_json_api_test.exs | packages/server/priv/static/openapi.yaml | ash-backed | Keep generated with operator role gate | Existing Ash resource route group for device CRUD; separate from device runtime /api/v1/devices protocol and gated by Phoenix JSON:API permissions. |
/api/json/pending_commands | NixstasisWeb.AshJsonApiRouter / Nixstasis.Devices.PendingCommand | Operator/developer API automation, internal docs | Add Ash JSON:API route tests when expanded | packages/server/priv/static/openapi.yaml | ash-backed | Keep generated with operator role gate | Existing Ash resource route group for pending command CRUD; collection writes require unscoped device manage permission. |
/api/json/alerts | NixstasisWeb.AshJsonApiRouter / Nixstasis.Monitoring.Alert | Operator/developer API automation, internal docs | Add Ash JSON:API route tests when expanded | packages/server/priv/static/openapi.yaml | ash-backed | Keep generated with operator role gate | Existing Ash resource route group for alert CRUD. |
/api/json/alert_rules | NixstasisWeb.AshJsonApiRouter / Nixstasis.Monitoring.AlertRule | Operator/developer API automation, internal docs | packages/server/test/nixstasis_web/controllers/builder_contract_json_api_test.exs | packages/server/priv/static/openapi.yaml | ash-backed | Keep generated with operator role gate | Existing Ash resource route group for alert-rule CRUD called out by the feature plan. |
/api/json/telemetry_events | NixstasisWeb.AshJsonApiRouter / Nixstasis.Monitoring.Telemetry | Operator/developer API automation, internal docs | Add Ash JSON:API route tests when expanded | packages/server/priv/static/openapi.yaml | ash-backed | Keep generated with operator role gate | Existing Ash resource route group for telemetry event create/read; collection writes require unscoped device manage permission. |
/api/json/custom_reports | NixstasisWeb.AshJsonApiRouter / Nixstasis.Reporting.CustomReport | Operator/developer API automation, internal docs | Add Ash JSON:API route tests when expanded | packages/server/priv/static/openapi.yaml, website/docs/modules/server-reporting.md | ash-backed | Keep generated with operator role gate | Existing Ash resource route group for custom report CRUD. |
/api/json/system_settings | NixstasisWeb.AshJsonApiRouter / Nixstasis.SystemSetting | Admin API automation, internal docs | packages/server/test/nixstasis_web/controllers/builder_contract_json_api_test.exs | packages/server/priv/static/openapi.yaml | ash-backed | Keep generated with admin role gate | Existing Ash resource route group for system settings. |
Device Runtime Generated Target
The five /api/v1 device rows below are the ash-backed migration
group. Their canonical generated target is the additive route family below; the
/api/v1
paths remain compatibility wrappers for the Go client and existing automation.
The existing /api/json/devices operator CRUD family is not reused for these
workflow actions.
| Generated route | Contract owner | Security and compatibility boundary |
|---|---|---|
GET /api/json/device_runtime/devices | :list_runtime_devices, adapting Devices.list_devices/1 | Operator bearer/device-view permission; preserves product, account_number, approval_status, connectivity_status, ipv4_address, and active-filter metadata. |
POST /api/json/device_runtime/devices/register | :register_runtime_device, calling Device.register through public normalization and token issuance | No application device credential; generated route is additive while /api/v1/devices/register remains the application/json 201 wrapper. |
POST /api/json/device_runtime/devices/{device_id}/heartbeat | :heartbeat, delegating to Monitoring.heartbeat/2 | deviceBearer HTTP bearer scheme; generated route and wrapper preserve 30/60-second heartbeat limiting. |
POST /api/json/device_runtime/devices/{device_id}/command_results | :acknowledge_command_results plus script/policy ingestion | deviceBearer HTTP bearer scheme; /api/v1 retains 202, acknowledged-count, replay/duplicate, and malformed-list behavior. |
GET /api/json/device_runtime/devices/{device_id}/command_payloads/{ref} | :fetch_command_payload, backed by Devices.get_command_payload/2 | deviceBearer HTTP bearer scheme; /api/v1 retains the raw payload and 404 behavior. |
Generated OpenAPI uses route-level security: deviceBearer is an HTTP bearer scheme using the
Authorization header; it applies only to the last three operations, and query-string device credentials are rejected. Registration
has security: [], and the generated list uses the operator bearer boundary. The
raw token is assigned by the device-runtime permission plug and is not an Ash action
argument. Unknown devices remain 404, missing or invalid bearer tokens 401, and
unapproved devices 403. The device_runtime dispatch in the existing
JsonApiPermissions pipeline sets the authenticated device actor and enables the
list/registration routes. The generated heartbeat action preserves its 200 response,
telemetry/inventory side effects, command delivery, probe, remote-access, and
heartbeat-rate-limit behavior; the command-result acknowledgement and payload-fetch
actions reuse the same branch. Generated action schemas and the compatibility
device-api.yaml sections remain side by side because they document distinct
generated and /api/v1 transport contracts. Runtime tests prove complete generated coverage in
packages/server/priv/static/openapi.yaml.
Excluded UI-Only Ash Resources
The script workbench persists its records through Ash resources, but those
resources are not currently supported HTTP contracts. The LiveView uses
Nixstasis.Domain directly, and exposing generic CRUD would bypass the
workbench's validation, command-dispatch, and audit boundaries. Their
AshJsonApi route declarations are therefore intentionally absent from
Nixstasis.Domain and the generated OpenAPI artifact.
| Resource | Internal consumer | Baseline boundary test | Classification | Decision |
|---|---|---|---|---|
Nixstasis.Scripts.ScriptDraft | Script workbench LiveView/domain | packages/server/test/nixstasis_web/controllers/script_json_api_test.exs | ui-only | Keep Ash ownership; do not expose generic JSON:API CRUD. |
Nixstasis.Scripts.ScriptVersion | Script workbench LiveView/domain | packages/server/test/nixstasis_web/controllers/script_json_api_test.exs | ui-only | Keep Ash ownership; do not expose generic JSON:API CRUD. |
Nixstasis.Scripts.ScriptValidationRun | Script workbench LiveView/domain | packages/server/test/nixstasis_web/controllers/script_json_api_test.exs | ui-only | Keep Ash ownership; do not expose generic JSON:API CRUD. |
Nixstasis.Scripts.ScriptTestRun | Script workbench LiveView/domain | packages/server/test/nixstasis_web/controllers/script_json_api_test.exs | ui-only | Keep Ash ownership; do not expose generic JSON:API CRUD. |
Nixstasis.Scripts.ScriptDeploymentRun | Script workbench LiveView/domain | packages/server/test/nixstasis_web/controllers/script_json_api_test.exs | ui-only | Keep Ash ownership; do not expose generic JSON:API CRUD. |
Nixstasis.Scripts.ScriptClientAction | Script workbench LiveView/domain | packages/server/test/nixstasis_web/controllers/script_json_api_test.exs | ui-only | Keep Ash ownership; do not expose generic JSON:API CRUD. |
Bespoke /api/v1 Routes
| Route | Current handler | Consumer | Baseline tests | Contract docs | Classification | Migration decision | Rationale |
|---|---|---|---|---|---|---|---|
GET /api/v1/builder-schemas | NixstasisWeb.BuilderSchemaController.index/2 | Builder UI, external automation | packages/server/test/nixstasis_web/controllers/builder_schema_controller_test.exs | website/docs/reference/openapi/builder-api.yaml, website/docs/modules/server-web.md, packages/server/priv/static/openapi.yaml | retained-controller | Keep wrapper, route canonical generated action separately | /api/v1 remains a compatibility wrapper; canonical generated Ash action contract is GET /api/json/builder_contract/schema_references. |
GET /api/v1/builder-schemas/:schema_id/versions/:schema_version/options | NixstasisWeb.BuilderSchemaController.options/2 | Builder UI, external automation | packages/server/test/nixstasis_web/controllers/builder_schema_controller_test.exs | website/docs/reference/openapi/builder-api.yaml, website/docs/modules/server-web.md, packages/server/priv/static/openapi.yaml | retained-controller | Keep wrapper, route canonical generated action separately | /api/v1 remains a compatibility wrapper; canonical generated Ash action contract is GET /api/json/builder_contract/schemas/:schema_id/versions/:schema_version/options. The generated route's bearer/report-view authorization and the wrapper's compatibility authorization are separate contracts; preserve not-found behavior. |
POST /api/v1/builder-configurations/validate | NixstasisWeb.BuilderConfigValidationController.create/2 | Builder UI, external automation | packages/server/test/nixstasis_web/controllers/builder_config_validation_controller_test.exs | website/docs/reference/openapi/builder-api.yaml, website/docs/modules/server-web.md, packages/server/priv/static/openapi.yaml | retained-controller | Keep wrapper, route canonical generated action separately | /api/v1 remains a compatibility wrapper; canonical generated Ash action contract is POST /api/json/builder_contract/builder_configurations/validate. |
GET /api/v1/devices | NixstasisWeb.DeviceController.index/2 | Device list API consumers, possible UI helpers | packages/server/test/nixstasis_web/controllers/device_controller_test.exs | website/docs/reference/openapi/device-api.yaml, website/docs/modules/server-web.md | ash-backed | Retain compatibility wrapper; generated list action is canonical | Resource-oriented device list overlaps Ash Device routes; preserve runtime filters and status semantics. |
POST /api/v1/devices/register | NixstasisWeb.DeviceController.register/2 | Go client | packages/server/test/nixstasis_web/controllers/device_controller_test.exs | website/docs/reference/openapi/device-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-devices.md | ash-backed | Retain compatibility wrapper; generated registration action is canonical | Durable credential issuance contract; preserve pending/approved token behavior exactly. |
POST /api/v1/devices/:device_id/heartbeat | NixstasisWeb.HeartbeatController.create/2 | Go client | packages/server/test/nixstasis_web/controllers/heartbeat_controller_test.exs | website/docs/reference/openapi/device-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-devices.md | ash-backed | Retain compatibility wrapper; generated heartbeat action is canonical | Durable protocol endpoint with telemetry, command polling, rate limiting, and remote-access token behavior. |
POST /api/v1/devices/:device_id/command_results | NixstasisWeb.DeviceCommandController.command_results/2 | Go client | packages/server/test/nixstasis_web/controllers/device_command_controller_test.exs | website/docs/reference/openapi/device-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-devices.md | ash-backed | Retain compatibility wrapper; generated command-result action is canonical | Preserve replay/duplicate acknowledgement behavior, malformed-list 400, and command-policy side effects. |
GET /api/v1/devices/:device_id/command_payloads/:ref | NixstasisWeb.DeviceCommandController.command_payload/2 | Go client | packages/server/test/nixstasis_web/controllers/device_command_controller_test.exs | website/docs/reference/openapi/device-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-devices.md | ash-backed | Retain compatibility wrapper; generated payload-fetch action is canonical | Preserve payload shape, device authentication, and 404 behavior. |
GET /api/v1/reports/:id/results | NixstasisWeb.ReportResultController.show/2 | Current report preview path; no confirmed external export consumer | packages/server/test/nixstasis_web/controllers/report_result_controller_test.exs | website/docs/reference/openapi/report-api.yaml, website/docs/modules/server-reporting.md | deferred | Leave unchanged; revisit only when an external report/export contract is approved | Report result shaping is dynamic and separate from Ash CustomReport CRUD. Controller coverage now verifies trusted operator admission and SQL-level authorized-device scoping for telemetry results. |
GET /api/v1/check_domain | NixstasisWeb.TLSController.check_domain/2 | Caddy on-demand TLS ask endpoint | packages/server/test/nixstasis_web/controllers/tls_controller_test.exs | website/docs/reference/openapi/device-api.yaml, website/docs/client-server-interface.md, website/docs/runtime-boundaries.md, website/docs/modules/deployment-compose.md | retained-controller | Retain | Caddy is the only consumer. This is an ingress protocol with simple allow/deny behavior, so Ash/OpenAPI adds no current product-contract benefit. |
Development Diagnostics
| Route | Current handler | Consumer | Baseline tests | Contract docs | Classification | Migration decision | Rationale |
|---|---|---|---|---|---|---|---|
GET /_nixstasis/laptop/tls_observations | NixstasisWeb.TLSController.observations/2 | Compose development harness diagnostics | packages/server/test/nixstasis_web/controllers/tls_controller_test.exs | website/docs/runtime-boundaries.md, website/docs/features/compose-dev-harness/design.md | retained-controller | Retain | Development-only diagnostic route, not a production product API contract. |
DELETE /_nixstasis/laptop/tls_observations | NixstasisWeb.TLSController.clear_observations/2 | Compose development harness diagnostics | packages/server/test/nixstasis_web/controllers/tls_controller_test.exs | website/docs/runtime-boundaries.md, website/docs/features/compose-dev-harness/design.md | retained-controller | Retain | Development-only diagnostic route, not a production product API contract. |
/e2e Routes
These routes remain retained-controller for this feature. They were added to
exercise end-to-end testing and reporting; their future OpenAPI value and test
model are separate decisions, and this feature does not expand into that review.
| Route | Current handler | Consumer | Baseline tests | Contract docs | Classification | Migration decision | Rationale |
|---|---|---|---|---|---|---|---|
GET /e2e/suites | NixstasisWeb.E2ERunController.suites/2 | E2E harness | packages/server/test/nixstasis_web/controllers/e2e_run_controller_test.exs | website/docs/reference/openapi/e2e-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-e2e.md | retained-controller | Retain by default | Harness workflow route gated by NixstasisWeb.Plugs.E2EEnabled; may become Ash-backed only if suite listing maps cleanly without weakening gate behavior. |
GET /e2e/runs | NixstasisWeb.E2ERunController.index/2 | E2E harness, diagnostics | packages/server/test/nixstasis_web/controllers/e2e_run_controller_test.exs | website/docs/reference/openapi/e2e-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-e2e.md | retained-controller | Retain by default | Run listing is resource-like but belongs to a gated harness API with protocol lifecycle semantics. |
POST /e2e/runs | NixstasisWeb.E2ERunController.create/2 | E2E harness | packages/server/test/nixstasis_web/controllers/e2e_run_controller_test.exs, packages/server/test/nixstasis/e2e/e2e_test.exs | website/docs/reference/openapi/e2e-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-e2e.md | retained-controller | Retain by default | Workflow endpoint with protocol-version header, idempotent reuse, environment locks, seed execution, and typed errors. |
GET /e2e/runs/:id | NixstasisWeb.E2ERunController.show/2 | E2E harness, diagnostics | packages/server/test/nixstasis_web/controllers/e2e_run_controller_test.exs | website/docs/reference/openapi/e2e-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-e2e.md | retained-controller | Retain by default | Resource-like read under gated E2E protocol surface; can be revisited after workflow endpoints are stable. |
POST /e2e/runs/:id/cancel | NixstasisWeb.E2ERunController.cancel/2 | E2E harness | packages/server/test/nixstasis_web/controllers/e2e_run_controller_test.exs | website/docs/reference/openapi/e2e-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-e2e.md | retained-controller | Retain by default | Workflow transition endpoint with typed persistence errors and harness gating. |
GET /e2e/runs/:id/results | NixstasisWeb.E2ERunResultController.index/2 | E2E harness, diagnostics | packages/server/test/nixstasis_web/controllers/e2e_run_result_controller_test.exs | website/docs/reference/openapi/e2e-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-e2e.md | retained-controller | Retain by default | Gated harness result listing; resource-like but tied to E2E lifecycle semantics. |
POST /e2e/runs/:id/results | NixstasisWeb.E2ERunResultController.create/2 | E2E harness | packages/server/test/nixstasis_web/controllers/e2e_run_result_controller_test.exs, packages/server/test/nixstasis/e2e/e2e_test.exs | website/docs/reference/openapi/e2e-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-e2e.md | retained-controller | Retain by default | Workflow ingestion endpoint with result submission and run status side effects. |
GET /e2e/runs/:id/results/:journey_id/log | NixstasisWeb.E2ERunResultController.log/2 | E2E harness, diagnostics | packages/server/test/nixstasis_web/controllers/e2e_run_result_controller_test.exs | website/docs/reference/openapi/e2e-api.yaml, website/docs/client-server-interface.md, website/docs/modules/server-e2e.md | retained-controller | Retain by default | Log retrieval has retention/pruned-log semantics that should stay explicit unless a future Ash action cleanly models them. |
Reconciliation Notes
website/docs/reference/openapi/device-api.yamlcurrently mixes device runtime APIs with Caddy TLS approval. Device runtime routes are an Ash-backed migration group;check_domainremains a Caddy-only retained controller.GET /api/v1/devicesacceptsipv4_address; preserve that filter in the Ash-backed contract or compatibility wrapper and generated/reference docs.- Heartbeat
command_inventoryis untrusted evidence persisted separately from sanitized telemetry; the server-ownedcommand_inventory_probeis returned in the heartbeat response and does not get a new endpoint. Both fields belong in the generated action schema and the compatibility reference. website/docs/reference/openapi/builder-api.yamldocuments compatibility wrappers; generated/api/json/builder_contract/*is the canonical Ash contract. Its generated bearer/report-view authorization,403responses, raw success payloads, and201validation status are documented separately from the/api/v1wrapper pipeline and its legacy200/422behavior.- The six script persistence resources remain Ash-owned for the workbench but
are intentionally excluded from
/api/jsonand the generated OpenAPI artifact. The current workbench calls the domain directly, and generic CRUD would bypass validation, command dispatch, and audit boundaries. A boundary test covers that no generic route is exposed; a future external contract requires audited domain-specific actions and a new design decision. website/docs/reference/openapi/report-api.yamlremains the current preview contract, but report export is deferred until an external consumer is approved.website/docs/reference/openapi/e2e-api.yamlremains the current retained contract; E2E conversion and test-model review are outside this feature.