Skip to content

Test stand authenticated controls and reports

These endpoints belong to the dedicated test stand, not the trading server or client REST API. Read-only monitoring, reports, and evidence remain accessible without a session. Start, cancel, and delete require a stand user session. The default standalone account is configured in test-stand/.env with STAND_AUTH_USERNAME and STAND_AUTH_PASSWORD; it has all three stand-control permissions. It is not a trading-server manager account. Restrict this destructive test environment to trusted operators.

Authentication and request security

Sign-in verifies the configured username/password locally, using constant-time comparisons of fixed-length digests. It makes no AuthManager request and does not depend on trading-server availability, managers, OTP, test-generated users, or restored trading data. Sessions contain only username and expiry; passwords are not retained in them. Trading-server restarts do not affect sign-in or sessions. A stand process restart clears all sessions and reloads credentials.

Required environment fields: STAND_AUTH_USERNAME (1–128 ASCII letters/digits or _.@-) and STAND_AUTH_PASSWORD (12–256 characters). No hardcoded default password exists. Without valid credentials the normal stand process refuses to start. Add these variables to the existing .env without overwriting PostgreSQL settings; keep the file mode 0600 and outside Git. Restart the stand after changing credentials.

All modifying endpoints, including login/logout, require the exact Origin header. The expected value is STAND_PUBLIC_ORIGIN when set, otherwise the request protocol and Host. Sec-Fetch-Site: cross-site is rejected. Configure STAND_PUBLIC_ORIGIN=https://stand.example behind a TLS proxy, without a trailing slash, and restrict direct access to the HTTP port. Never send credentials over public plain HTTP.

The browser receives an opaque stand_session cookie with Path=/, HttpOnly, SameSite=Strict, and Max-Age=28800. Secure is added on HTTPS or with an HTTPS public origin. Modifying clients send this cookie and the matching Origin. Authentication status responses are marked Cache-Control: no-store.

POST /api/auth/login

JSON request fields:

Field Type Required Description
username string Yes Nonempty standalone username, at most 128 characters.
password string Yes Nonempty password, at most 256 characters.

Request example:

{ "username": "admin", "password": "<stand-password>" }

Complete success response (200; also sets the session cookie):

{
  "authenticated": true,
  "user": { "username": "admin", "expiresAt": "2026-10-02T02:00:00.000Z" }
}

authenticated is boolean. user contains username (string) and expiresAt (ISO-8601 UTC timestamp). Login attempts are limited to five per IP per minute. Error responses contain only error (string code): 400 INVALID_CREDENTIALS_FORMAT, 401 INVALID_CREDENTIALS, 403 INVALID_ORIGIN, 429 LOGIN_RATE_LIMITED with Retry-After: 60, or 503 SESSION_CAPACITY_REACHED / AUTH_NOT_CONFIGURED (credentials absent if this module is embedded without the normal startup validation). The request body limit is 4096 bytes; oversized requests receive the framework's 413 response. Neither password nor a trading-manager token is returned in the JSON response.

GET /api/auth/session

No request fields. A valid cookie returns the same complete 200 response shape as login. Without a valid cookie:

{ "authenticated": false, "user": null }

POST /api/auth/logout

No request body fields. Requires a valid cookie and Origin. Invalidates the current session and clears its cookie. Complete 200 response:

{ "authenticated": false, "user": null }

Protected mutations return 401 {"error":"AUTHENTICATION_REQUIRED"} without a valid session, or 403 {"error":"INVALID_ORIGIN"} for an invalid Origin. Authorization is enforced by the server, not only the UI.

Run controls

POST /api/scenarios/:id/run

Requires standalone user authentication and Origin. id is the scenario identifier. Optional JSON field snapshotId (string) selects a snapshot; omitted uses the configured default. A successful request returns 202 and a compact run summary. Unknown scenarios return 404 {"error":"SCENARIO_NOT_FOUND"}. Existing runner/storage failures retain their existing HTTP behavior.

Run-summary fields:

Field Type Description
id string Run identifier.
scenarioId, scenarioName string Scenario identity and display name.
status string queued, preparing, running, passed, failed, or cancelled.
requestedAt string ISO-8601 UTC request time.
startedAt, finishedAt string, optional ISO-8601 UTC lifecycle times.
snapshotId, snapshotName string, optional Snapshot identity and display name.
testedBuild object, optional version and buildDate, both strings.
error string, optional Compact run error; full evidence remains separate.
revision integer Durable journal revision.
logCount, assertionCount, passed, failed integer Saved log and check counters.
levels object Integer counts keyed by info, success, warning, error.
sections object Section names mapped to { "passed": integer, "failed": integer }.
reportReady boolean Derived report availability.

Complete response example (optional absent fields are omitted on the wire):

{
  "id": "2026-10-01T18-09-56-520Z-basic-regression-2e03df1f",
  "scenarioId": "basic-regression",
  "scenarioName": "Basic trading and finance regression",
  "status": "queued",
  "requestedAt": "2026-10-01T18:09:56.520Z",
  "snapshotId": "clean",
  "revision": 3,
  "logCount": 1,
  "assertionCount": 0,
  "passed": 0,
  "failed": 0,
  "levels": { "info": 1, "success": 0, "warning": 0, "error": 0 },
  "sections": {},
  "reportReady": false
}

POST /api/runs/:id/cancel

Requires authentication and Origin. No body fields. Complete 202 response:

{ "cancelled": true }

The boolean acknowledges cancellation was requested, not that all cleanup has finished. Noncancellable or nonexistent runs return 409 {"error":"RUN_NOT_CANCELLABLE"}.

DELETE /api/runs/:id

Requires authentication and Origin. No body fields. Only terminal runs with completed reports and no ongoing execution/report task may be removed. Complete 200 response:

{
  "deleted": true,
  "id": "2026-10-01T18-09-56-520Z-basic-regression-2e03df1f",
  "evidenceRetained": true
}

deleted and evidenceRetained are booleans; id is the removed run identifier. Errors contain only error: 404 RUN_NOT_FOUND, 409 RUN_BUSY, or 503 RUN_DELETE_FAILED.

Removal hides the run from history and evidence/report endpoints. It deletes the PostgreSQL run/entry index transactionally and records a deletion tombstone. If PostgreSQL is unavailable, the durable local deletion takes effect and index cleanup retries in the background. Original artifact files are not erased. deleted.json remains in the artifact directory with this complete format:

{
  "id": "2026-10-01T18-09-56-520Z-basic-regression-2e03df1f",
  "username": "admin",
  "deletedAt": "2026-10-01T18:30:00.000Z"
}

Recovery skips directories containing this marker. The fields are id (string), username (string), and deletedAt (ISO-8601 UTC timestamp). Older internal deletion markers containing a positive integer managerId instead of username remain readable and are labeled legacy-manager-<id> in the rebuilt index; original markers are not rewritten. This UI action does not reclaim artifact disk space. Restoring a deleted run requires an administrator to reconcile both local and PostgreSQL tombstones offline; no restore/purge endpoint is exposed.

Section-specific check pages

GET /api/runs/:id/assertions adds optional section (nonempty string, maximum 200 characters), an exact section-name match. Existing fields: limit (integer 1–200, default 100), after (nonnegative integer sequence, default 0), failedOnly (true/false), and tail (true selects the latest unfiltered sequence window before filtering). The section filter is supported only for assertion pages; invalid filters return 400 {"error":"INVALID_FILTER"}. Invalid numeric pagination returns the existing framework 400 response; missing runs return 404 {"error":"RUN_NOT_FOUND"}; index failures return 503 with error: "RUN_INDEX_UNAVAILABLE" and storage as below.

Example URL: /api/runs/<id>/assertions?limit=50&after=0&failedOnly=false&section=Recovery.

Response fields: items (array of check previews), nextAfter (integer last returned sequence, or the supplied cursor), hasMore (boolean), and storage (status object). Each item has sequence and revision (integers), name and section (strings), passed and hasDetails (booleans), and detailUrl (string). Fetch detailUrl only to retrieve the original full check, unchanged and untruncated.

storage fields: mode (string), pendingRuns, pendingRecords, pendingWrites (integer counts), indexReady (boolean), error (string or null), and recovery. Recovery contains state (loading, ready, failed), totalRuns, loadedRuns, processedEntries, totalEntries (integers), currentDirectory and error (string or null), and phase (scan, import, report, done).

Complete response example:

{
  "items": [{
    "sequence": 12, "revision": 26, "name": "Recovery: portfolio restored",
    "passed": true, "section": "Recovery", "hasDetails": true,
    "detailUrl": "/api/runs/example-run/entries/assertion/12"
  }],
  "nextAfter": 12,
  "hasMore": false,
  "storage": {
    "mode": "postgres-with-local-journal", "pendingRuns": 0,
    "pendingRecords": 0, "pendingWrites": 0, "indexReady": true, "error": null,
    "recovery": {
      "state": "ready", "totalRuns": 1, "loadedRuns": 1, "currentDirectory": null,
      "phase": "done", "processedEntries": 0, "totalEntries": 0, "error": null
    }
  }
}

Interactive reports

GET /api/runs/:id/report serves bounded HTML with section PASS/FAIL charts, count/percentage scaling, sorting, filtering, section drill-down, observed-order distribution, financial search/mismatch filtering, on-demand full JSON, and Copy JSON. No external chart service/CDN or periodic REST polling is used. Old derived reports upgrade on first open. Original checks and journals are not rewritten. A section with no checks is not a PASS; pass percentage is not engine coverage. Order counts are deduplicated observed artifacts, not current positions; SO/closure counts overlap order categories. Financial currencies are not aggregated and display filtering does not change test assertions.

The derived report-summary.json artifact now has schemaVersion: 2. All fields: schemaVersion (integer), id (string), passed, failed, warnings (integer check/log counts), sections (section-name object of passed/failed integers), and evidence. Evidence contains integer counters market, buy, sell, closed, so, pending, finance, plus warnings (array of unavailable-artifact messages). Complete example:

{
  "schemaVersion": 2,
  "id": "example-run",
  "passed": 100,
  "failed": 0,
  "warnings": 2,
  "sections": { "Recovery": { "passed": 100, "failed": 0 } },
  "evidence": {
    "market": 20, "buy": 12, "sell": 8, "closed": 20,
    "so": 5, "pending": 3, "finance": 4, "warnings": []
  }
}