# RuntimeStore HTTP contract This contract exposes the complete `RuntimeStore` interface over JSON HTTP. All paths below are relative to a deployment-defined base URL. Path segments and query parameter values are percent-encoded UTF-8 strings. Identifiers used as path segments MUST NOT be exactly `.` or `..`, because URL parsers normalize those dot segments before routing. Request and response bodies use `application/json`; successful operations with no return value respond with `204 No Content`. ## Endpoints | Method | Path | Purpose | Success | | --- | --- | --- | --- | | `POST` | `/runtime/sessions` | Create a session from a `RuntimeSessionInit`. The server stamps `runtimeDslVersion`; a client-submitted value is ignored. | `201` with the full `RuntimeSession` | | `GET` | `/runtime/sessions/{sessionId}` | Get one session. | `200` with the `RuntimeSession`, or `404` if absent | | `PATCH` | `/runtime/sessions/{sessionId}/status` | Set status from `{ "status", "updatedAt", "expectedLastSeq"? }`. `expectedLastSeq` is a non-negative integer or `null`. | `204` | | `DELETE` | `/runtime/sessions/{sessionId}` | Delete a session and all of its records. | `204` | | `GET` | `/runtime/stages/{stageId}/learners/{learnerKey}/sessions` | List a partition's sessions, ordered by the instant represented by `createdAt`, then by `id` for deterministic ties. | `200` with `RuntimeSession[]` | | `POST` | `/runtime/sessions/{sessionId}/records` | Append a `RuntimeRecordInit` plus optional top-level `expectedLastSeq` and `sessionTransition`; body `sessionId` must match the path. | `201` with the full `RuntimeRecord` | | `GET` | `/runtime/sessions/{sessionId}/records` | List records ordered by `seq`. Optional `?sceneId={sceneId}` returns only records anchored to that scene and excludes unanchored records. | `200` with `RuntimeRecord[]` | | `POST` | `/runtime/learners/merge` | Atomically re-key all sessions across all stages from `{ "fromLearnerKey", "toLearnerKey" }`. | `200` with `{ "moved": number }` | | `DELETE` | `/runtime/stages/{stageId}/learners/{learnerKey}` | Delete one learner's sessions and records on one stage. | `204` | | `DELETE` | `/runtime/stages/{stageId}` | Cascade-delete every learner's sessions and records on one stage. | `204` | | `DELETE` | `/runtime` | Delete every runtime session and record. Idempotent; an administrative operation — servers MUST gate it behind an operator-level authorization check, never expose it to learner credentials. | `204` | `GET /runtime/sessions/{sessionId}/records` returns an empty array when the session has no records, matching `RuntimeStore.listRecords`. For an absent session a server MAY answer `404 SESSION_NOT_FOUND` instead of `200 []` — authorization-aware servers SHOULD, so absent and unowned sessions are indistinguishable — and the client MUST map that `404` back to an empty array, preserving the store contract. Deleting an absent target succeeds. ## Server-assigned sequence `seq` is server-assigned. The append request body is a `RuntimeRecordInit`. If it includes `seq`, that value is ignored; the store's assigned value is authoritative. The server allocates the next per-session monotonic sequence number atomically with the insert, starting at `0`, and the response returns the full `RuntimeRecord`, including the assigned `seq`. Likewise, `runtimeDslVersion` is server-assigned when a session is created. If the request body includes `runtimeDslVersion`, that value is ignored; the store's assigned value is authoritative. ## Compare guards and session transitions `expectedLastSeq` is an optional compare-and-swap guard shared by status updates and record appends. A non-negative integer means the caller observed that sequence at the record tail; `null` means the caller observed no records. Omission means no precondition. The server compares the value with the actual tail inside the same transaction as the write. A mismatch commits neither the status update nor the append and returns `409 RUNTIME_APPEND_CONFLICT` with the expected and actual tails in `error.details`. An append may also include a top-level `sessionTransition` object with `{ "status", "updatedAt" }`. The server validates and commits the completed parent session in the same transaction as the record. A tail conflict or validation failure persists neither change. For example: ```json { "id": "record-1", "sessionId": "session-1", "createdAt": "2026-01-01T00:00:00.000Z", "payload": null, "expectedLastSeq": null, "sessionTransition": { "status": "completed", "updatedAt": "2026-01-01T00:00:01.000Z" } } ``` ## Payload domain HTTP implementations carry record payloads through JSON and therefore MUST accept only plain JSON values that survive serialization without changing meaning. They MUST fail loud before sending values such as `Map`, `Set`, `Date`, non-finite numbers, negative zero, nested `undefined`, `bigint`, sparse arrays, symbol-keyed properties, non-enumerable properties, arrays with non-index own properties, strings containing U+0000, class instances, and circular references. U+2028 and U+2029 are valid JSON string contents and MUST be accepted. This is intentionally narrower than `BrowserRuntimeStore`, whose structured-clone persistence can preserve values such as `Map`, `Set`, and `Date` that JSON cannot. The reference handler streams request bodies into a bounded buffer. `maxBodyBytes` configures the bound on the runtime, composed-storage, and reference-server factories; it defaults to 32 MiB. A body that crosses the bound is rejected immediately with `413 PAYLOAD_TOO_LARGE`. ## learnerKey security model `learnerKey` appears in paths, query parameters, and request bodies, but the server MUST derive or verify `learnerKey` from the authenticated session and MUST NOT blindly trust the client-submitted value. `learnerKey` is an opaque partition key, not proof of identity or authorization; trusting it verbatim creates a lateral-authorization vulnerability that lets one learner read, merge, or delete another learner's runtime data. The same rule applies to both keys in `mergeLearner`; authorization policy must explicitly permit the authenticated principal to migrate the source partition into the destination partition. The conformance server in this package is test-only and does not implement an authentication or authorization model; deriving `learnerKey` from authentication is left to the reference server in #939 Part D. ## Errors Every non-2xx response has this machine-readable JSON shape: ```json { "error": { "code": "VALIDATION_FAILED", "message": "@openmaic/storage: invalid runtime session ...", "details": [] } } ``` `details` is optional. `message` preserves the semantic wording of the backing store error so an HTTP client can reconstitute the fail-loud exceptions callers receive from `BrowserRuntimeStore`. | Condition | HTTP status | Error code | Client behavior | | --- | --- | --- | --- | | Malformed JSON, an invalid envelope or payload, invalid learner keys, a body/path mismatch, or append to a non-active session | `400` | `VALIDATION_FAILED` | Throw `HttpRuntimeStoreError` (an `Error`) with the server message | | Request body exceeds `maxBodyBytes` | `413` | `PAYLOAD_TOO_LARGE` | Throw `HttpRuntimeStoreError` | | Session does not exist | `404` | `SESSION_NOT_FOUND` | `getSession` returns `undefined`; operations that require the session throw `HttpRuntimeStoreError` with the browser store's `no session` semantics | | Route does not exist | `404` | `ROUTE_NOT_FOUND` | Throw `HttpRuntimeStoreError` | | A stored session has a future runtime DSL version | `409` | `FUTURE_VERSION` | Throw `HttpRuntimeStoreError` with the browser store's fail-loud `newer than this client's` semantics | | Session id already exists | `409` | `SESSION_ALREADY_EXISTS` | Throw `HttpRuntimeStoreError` with the browser store's `already exists` semantics | | `expectedLastSeq` does not match the current record tail | `409` | `RUNTIME_APPEND_CONFLICT` | Reconstitute and throw `RuntimeAppendConflictError` with `sessionId`, `expectedLastSeq`, and `actualLastSeq` from `error.details` | | Unexpected server failure | `500` | `INTERNAL_ERROR` | Throw `HttpRuntimeStoreError` | The client MUST use the machine-readable code, not status alone, when an operation has special behavior. In particular, only `SESSION_NOT_FOUND` is translated to `undefined` by `getSession`. Sessions returned by reads MUST be forward-migrated through the client's `migrateRuntime` before they are returned, because the server may lag the client schema version. Corrupt sessions remain fail-loud on direct reads and are omitted from partition listings, matching `BrowserRuntimeStore`. ## Retry and atomicity guarantees `mergeLearner`, `DELETE /runtime/sessions/{sessionId}`, `DELETE /runtime/stages/{stageId}/learners/{learnerKey}`, and `DELETE /runtime/stages/{stageId}` MUST be safely retryable. Repeating a completed merge moves `0`; repeating a delete succeeds with `204`. A merge is atomic across every matching source session and MUST NOT expose a partial move. Delete endpoints cascade atomically to the target sessions' records. The test-only conformance server may report a future-stamped-row conflict encountered mid-merge as `500 INTERNAL_ERROR`; structured classification of that specific case is deferred to the real Part D server implementation. `POST /runtime/sessions` and record append are not implicitly retry-safe: clients must not retry them after an ambiguous transport failure unless a deployment adds a separate idempotency-key policy. Session ids and record ids remain caller-owned uniqueness keys, while record `seq` remains exclusively server-owned.