# pysim-simple-server — HTTP API reference The server exposes a JSON HTTP API under `/api/*`. All responses carry `Access-Control-Allow-Origin: *` (plus `Access-Control-Allow-Private-Network: true` on the preflight), so the API is reachable from a separately-hosted PWA. ## Version compatibility | Server | PWA (SIMple) | Status | |--------|-------------|--------| | same major | any | ✅ Compatible | | older major | any | ❌ Outdated — update server | | newer major | any | ⚠️ Server newer — update PWA | The server reports its version via `GET /api/version`. The PWA checks this on connect and compares the major version (a 2.x server is flagged as outdated by a 3.x PWA). ## Endpoints | Endpoint | Method | Purpose | |----------|--------|---------| | `/api/version` | GET | Server version string | | `/api/status` | GET | Card reader + card info + current selection | | `/api/command` | POST | pySim command (equip, status, tree, etc.) | | `/api/commands` | GET | List available pySim commands | | `/api/cardinfo` | GET | pySim `cardinfo` output | | `/api/tree` | POST | File tree browser for given FID/name | | `/api/select` | POST | Select a file by name or FID | | `/api/read` | POST | Read file content | | `/api/write` | POST | Write raw hex data to a file | | `/api/apdu` | POST | Raw APDU send | | `/api/verify-adm` | POST | Verify the card's ADM PIN (from the matched card preset) | | `/api/presets` | GET | Card preset store: all presets + the store path | | `/api/presets` | POST | Create a card preset (server-side store) | | `/api/presets/update` | POST | Update a preset (`{id, fields}`; the counter may be raised) | | `/api/presets/delete` | POST | Delete a preset (`{id}`) | | `/api/presets/import` | POST | Import presets (`{presets: [...], mode: merge\|replace}`) | | `/api/presets/import-outfile` | POST | Import a card vendor's **outfile** (`{text, name?, algorithm16?, tars?, tar_keyset?, mode?}`; without `mode` a dry-run preview) | | `/api/tar-probe` | POST | Probe the card's registered OTA applications (one secured packet per TAR, TS 101 220 Annex D) | | `/api/counter-probe` | POST | Bounded counter sync: probe increasing counter values (start+1, +2, +4, …) with a read-only command until the card accepts one (ceiling/attempt-capped), then persist accepted+1 | | `/api/esim/chip` | GET | eUICC chip details (EID, EUICCInfo1/2, configured addresses) | | `/api/esim/profiles` | GET | Installed eSIM profiles with their metadata | | `/api/esim/notifications` | GET | Pending eSIM notifications (read-only) | | `/api/esim/profile` | POST | Enable/disable an eSIM profile (card re-initialized after the switch) | | `/api/help` | POST | pySim help for a given command | | `/api/send-ota` | POST | SCP80 OTA secured packet delivery | | `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]); `delete_package` prepends a cascade DELETE on the package AID | | `/api/ram-batch/start` | POST | Start a **batch operation** (v3.28.0; kinds v3.29.0): `install-cap` (the same `.cap` per equipped card), `reinstall-app` (DELETE + INSTALL [for install]) or `delete` (one GP DELETE), using each card's preset (matched by ICCID) | | `/api/ram-batch/status` | GET | The batch state: phase, current card, counts and the attempts report (card-free) | | `/api/ram-batch/stop` | POST | Stop the batch — a running install finishes first (card-free, so it works while the install holds the card lock) | | `/api/ram-batch/clear` | POST | Drop the finished batch's report (`409` while a batch is active) | | `/api/ram-install-app` | POST | INSTALL [for install] (deletes the instance first, tolerating 'not found') / INSTALL [for make selectable] for an already loaded package (no re-load) | | `/api/cap-info` | POST | Validate a `.cap` archive and estimate its code/NVRAM/RAM requirements (read-only) | | `/api/cap-compat` | POST | CAP compatibility test: LOAD only to the block completing the Import component (nothing committed) | | `/api/test/run` | POST | Start a test script (actions + proactive expectations; `script` inline or `script_id` from the store) or a test suite (`suite_id`; ordered scripts, one card session) | | `/api/test/status` | GET | Test script/suite run state, per-step results and the suite report | | `/api/test/stop` | POST | Request a running test script/suite to stop | | `/api/test/clear` | POST | Clear the finished run report | | `/api/test/scripts` | GET | Test script store (server-side `test_scripts.json`): all scripts + the store path | | `/api/test/scripts` | POST | Create a test script (`{name, steps, suite_id}` or `{script}`; the script joins the suite) | | `/api/test/scripts/update` | POST | Update a test script (`{id, script}`; a changed `suite_id` is a move) | | `/api/test/scripts/delete` | POST | Delete a test script (`{id}`; detaches it from its suite first) | | `/api/test/scripts/move` | POST | Move a script to another suite (`{id, suite_id}`) | | `/api/test/scripts/copy` | POST | Copy a script into another suite (`{id, suite_id, name?}`) | | `/api/test/scripts/import` | POST | Import scripts (`{scripts: [...], mode: merge\|replace, suite_id?}`; accepts the old localStorage export shape) | | `/api/test/suites` | GET | Test suite store (server-side `test_suites.json`): all suites + the store path | | `/api/test/suites` | POST | Create a suite (`{name, require_adm?, start_reset?, scripts?}` or `{suite}`) | | `/api/test/suites/update` | POST | Update a suite (`{id, suite}`; the member set changes through the script endpoints) | | `/api/test/suites/delete` | POST | Delete a suite (`{id}`; refused while it holds scripts) | | `/api/test/suites/import` | POST | Import a suite bundle (`{suites, scripts, mode}`; script references remapped) | | `/api/sp-verify` | POST | Verify secured packet against pySim reference | | `/api/menu` | GET | Current STK menu (title + items + active) | | `/api/menu-select` | POST | ENVELOPE(Menu Selection) with item_id | | `/api/menu-respond` | POST | TERMINAL RESPONSE for paused STK command | | `/api/stk-status` | GET | STK session state (active/pending/type) | | `/api/events` | GET | Event list from SET UP EVENT LIST | | `/api/event-send` | POST | Send ENVELOPE(Event Download) | | `/api/net-sim` | POST | Run a network-condition scenario (attach, service loss, roaming, churn, 2G, SMS, CB, AUTHENTICATE) | | `/api/net-state` | GET | Cached network-state monitor (service, location, per-file state) | | `/api/net-state-refresh` | POST | Re-read the monitored files (optional `files` list) and return the updated monitor state | | `/api/arr` | GET | Cached EF.ARR copies (the FCP '8B' security attribute references): records per ARR file | | `/api/mcc-mnc` | GET | Search the optional MCC/MNC operator list (`?q=`; `?random=1&exclude=`) | | `/api/proactive-log` | GET | Last 50 proactive commands | | `/api/status-poll` | POST | Manual STATUS poll + FETCH if 91XX | | `/api/rescue` | POST | Re-send TERMINAL PROFILE to recover CAT session | | `/api/terminal-profile` | GET | Current TERMINAL PROFILE (hex) + CLI default | | `/api/terminal-profile` | POST | Set and re-send the TERMINAL PROFILE at runtime (in-memory) | | `/api/poll-status` | GET | Background STATUS polling state | | `/api/poll-toggle` | POST | Enable/disable background polling | | `/api/pli-qualifiers` | GET | List of qualifier codes with descriptions | | `/api/pli-dict` | GET | Current dictionary (hex values per qualifier) | | `/api/pli-dict` | POST | Update dictionary entries | | `/api/scp81/bip` | POST | Start/stop the HTTP OTA listener (dump capture or PSK TLS server) | | `/api/scp81/status` | GET | BIP terminal + listener state (channels, PSK identities, handshake identity, `owner`) | | `/api/scp81/log` | GET | HTTP OTA event log (`?after=`) | | `/api/scp81/log-clear` | POST | Clear the HTTP OTA event log | | `/api/bip/control` | POST | Start/stop the generic BIP session (`sink` / `passthru` / `redirect`) | | `/api/bip/status` | GET | BIP session state + owner (same shape as `/api/scp81/status`) | | `/api/bip/log` | GET | BIP event log (`?after=`) | | `/api/bip/log-clear` | POST | Clear the BIP event log | | `/api/scp81/queue` | POST | Replace the SCP81 command script (optionally force-restart) | | `/api/scp81/script` | GET | Active command script + execution state and R-APDUs | | `/api/scp81/psk-map` | POST | Replace the PSK table of a running TLS listener | | `/api/scp81/gen-install` | POST | Generate the RAM APDU list for a `.cap` (no queueing) | ## Endpoint details ### `GET /api/version` Returns server version for compatibility checking. **Example response:** ```json {"version": "3.0.1"} ``` ### `GET /api/status` Card reader, card type, current selection, and card state. Reported fields: `reader`, `connected`, `card_present`, `card_session` (increments on every equip/disconnect), `proactive_seq`, `equipping`, `auto_equip`, `card`, `profile`, `eid`, `euicc`, `iccid`, `app_ready`, `adm_verified`, `atr`, `cla_byte`, `sel_ctrl`, `current_selection`, `channels`. `eid` is the eUICC identifier read from the ISD-R at equip (`null` for a non-eUICC card) and `euicc` says whether the equipped card is an eUICC (SGP.22/32); both are used by the PWA's eSIM view. `iccid` is the E.118 digit string read from EF.ICCID (MF/2FE2) when the card is equipped, or `null` when it is not connected / the card does not let the terminal read it. The equip path reads it **before** the TERMINAL PROFILE, so no CAT session is active yet; the read is best-effort and never fails an equip (and a card removal clears it). The PWA uses it to auto-select the card preset with the same ICCID in both SCP80 views (Secured Packet and RAM). ### `GET /api/commands` List all available shell commands for the current card profile. ### `GET /api/cardinfo` Runs pySim's `cardinfo` command and returns its output as `{"output": "..."}` (card type, ATR, ICCID and other information pySim reports for the equipped card). A shortcut for `POST /api/command` with `{"cmd": "cardinfo"}`. ### `GET /api/mcc-mnc` Searches the optional worldwide operator list loaded from `--mcc-mnc-list` (default `/samples/mcc-mnc-list.json`). `?q=` matches country, country code, MCC/MNC, brand and operator (compact results, max 50); `?random=1[&exclude=MCCMNC]` returns one random entry (for roaming tests). When the list is not configured or missing the response is `{"available": false}`. ### `POST /api/command` Execute any pysim-shell command. ```json {"cmd": "select MF"} ``` Returns: ```json {"output": "..."} ``` ### `POST /api/apdu` Send a raw APDU to the card. ```json {"apdu": "00A4040000..."} ``` Returns: ```json {"response": "...", "sw": "9000"} ``` ### `POST /api/verify-adm` Verify the card's ADM PIN (TS 102 221 VERIFY, CHV number from the card model). The key comes from the PWA's matched card preset and is never stored; it is redacted from the request log. Short keys (4-16 hex digits) are padded to the 8 CHV bytes with `f`, like pySim's `verify_adm`. The response is structured so the UI can warn about the remaining attempts before retrying. ```json {"adm": "0011223344556677"} ``` Returns on success: ```json {"ok": true, "sw": "9000"} ``` On a wrong key (`63Cx`, x attempts left): ```json {"ok": false, "sw": "63C2", "attempts_left": 2} ``` A blocked ADM (`6983`/`9804`) reports `{"ok": false, "sw": "9804", "blocked": true}` and cannot be recovered without the card's unblock key. ### Card presets (server-side store) Card presets (keysets, a TAR table with per-TAR MSLs, PSK pair, ADM) live on the server in `~/.pysim-simple-server/card_presets.json` (`--card-presets PATH` overrides it; `Path.home()` resolves the same way on Linux, macOS and Windows). The file is written atomically and every counter change is appended to `card_presets.json.audit.jsonl`. **Keysets.** A preset carries several keysets — a keyset's number is the `b8..b5` nibble of its KIc/KID (TS 102 225 §5.1.2/A.2); the number is implicit in the bytes, there is no separate field: ```json {"id": "…", "name": "Card A", "iccid": "…", "pin1": "", "puk1": "", "pin2": "", "puk2": "", "pskIdentity": "", "pskKey": "", "adm": "", "tars": [ {"role": "isd", "tar": "000000", "msl": "16", "desc": ""}, {"role": "uiccRfm", "tar": "B00000", "msl": "16", "desc": ""}, {"role": "usimRfm", "tar": "B00001", "msl": "16", "desc": ""}, {"tar": "AF4D01", "msl": "1A", "desc": "my applet"} ], "keysets": [ {"kic": "15", "kid": "15", "kicKey": "…", "kidKey": "…", "cntr": "0000000001"}, {"kic": "29", "kid": "29", "kicKey": "…", "kidKey": "…", "cntr": "0000000005"} ]} ``` Rules (all refused with `400`): KIc and KID must carry the **same** keyset number (A.2 — the card rejects a mismatch with "Unidentified security error"), the number must be `01`–`0F` (`00` means "no security" and is a packet-level choice, not a preset keyset), no two keysets may share a number, both keys and a counter (1–10 hex digits) are required, and the counters are stored in the fixed-width 10-hex form. A v3.8.0 flat preset (`kic`/`kid`/`kicKey`/`kidKey`/ `cntr` at the top level) converts into a single keyset on load and on import, so old files and exports keep working. **TAR table.** Every Remote Management application is addressed by its TAR and each entry carries its own **MSL** (Minimum SPI1, TS 102 226 §8.2.1.3.2.4): the card checks it before the security processing and answers response status `0A` "Insufficient security level" when a packet's SPI1 is below it. The three role entries (`isd`, `uiccRfm`, `usimRfm` — GP ISD `000000`, TS 31.116 UICC RFM `B00000`, TS 31.115 ADF RFM `B00001`) are **mandatory** with a non-empty TAR and MSL; further TARs can be added with an optional `desc`. A TAR is three hex bytes, an MSL one, and a TAR may appear once (a duplicate is refused). A v3.9.1 preset's card-wide `spi1`/`tar`/`uiccTar`/`usimTar` convert into the three role entries, the `spi1` as their MSL. Operations **derive a packet's SPI1 from the MSL of its TAR**; a request naming a TAR outside the preset must pass `spi1` itself, otherwise it is refused with `400` and a hint. **Card codes.** The optional `pin1`/`puk1`/`pin2`/`puk2`/`adm` fields are stored for later use (nothing consumes them yet). PIN/PUK must be **4–8 decimal digits** when set (empty is fine; anything else is refused with `400` naming the field); the ADM key is 4–16 hex digits (a decimal PIN is fine). **CAT (USAT) release.** The optional `release` field (4–18) names the release the card implements; the release guard hides CAT items introduced after it (the Simulator's event parameters, see `catrelease`). Empty means unspecified and resolves to Rel-12 (`catrelease.DEFAULT_RELEASE`) — an unset release never filters anything away. **Counters.** The store is the source of truth for the SCP80 counters, one per keyset ("a dedicated counter shall be associated to each key version", Annex A.1 — the spec's wording). The operations below accept a `preset_id` and **persist the counter they consumed themselves** (monotonic per keyset — a stale value never regresses it), so a closed tab, a lost response or a second browser window can no longer lose an increment. A request whose KIc/KID name a **non-zero key number the preset does not define** is refused (`400 {"error": "keyset N is not defined in preset '…' - add it in the Cards tab"}`), and so is a KIc/KID number mismatch. The Cards tab's export/import uses this API; the import also accepts presets exported by the older localStorage-based builds. - `GET /api/presets` — `{"path": "…", "count": 2, "version": 5, "presets": [{…}]}` (card-free: it answers while a long card operation runs). - `POST /api/presets` — body = the preset fields (`keysets` included); returns `{"ok": true, "preset": {…}}`. A duplicate ICCID (digits, spaced digits, digits with the odd-nibble F pad, or raw EF hex — all normalised to one form) is refused with `400 {"error": "card with this ICCID already exists: …"}`. - `POST /api/presets/update` — `{"id": "…", "fields": {…}}` (partial update; a plain `{id, name, …}` body works too, `keysets` replaces the list and `tars` the TAR table — a non-list `tars` value is refused). A counter is written as given — this is the deliberate human edit; unknown ids answer `404`. - `POST /api/presets/delete` — `{"id": "…"}` → `{"ok": true, "removed": true}`. - `POST /api/presets/import` — `{"presets": […], "mode": "merge"}` (or `"replace"`) → `{"ok": true, "added": N, "skipped": M, "errors": […]}`; invalid entries and duplicates are skipped and reported, never fatal. ### `POST /api/presets/import-outfile` Import many cards at once from a card vendor's **outfile** (v3.27.0). An outfile is the vendor personalization tool's plain-text dump: a banner and a configuration header (customer, batch, profile, …) followed by the actual data. The data starts at a **column-name line** — whitespace-separated tokens that include `ICCID` (the first token may carry a `var_out:`-style prefix) — and continues with one whitespace-separated row per card; everything before the column line and every column the presets do not use is ignored. Without a `mode` the call is a **dry run**: the parsed presets and the report come back for the preview (nothing is stored). With `mode: "merge"` / `"replace"` the same parse runs and the presets are imported through the store (`replace` wipes the store first, like `/api/presets/import`); any other `mode` value is refused with `400` — a typo must not silently turn an import into a preview. ```json {"text": "…", "name": "Batch 0002", "algorithm16": "3des2", "tars": [{"role": "isd", "tar": "000000", "msl": "16", "desc": ""}, …], "tar_keyset": 1, "mode": "merge"} ``` - `text` (required) — the outfile contents. A file without a column line answers `{"ok": true, "preview": true, "presets": [], "report": {"error": "no column-name line found - …"}}` (a preview, not a `400`: the panel shows the message). The same report error covers a structurally invalid file (v3.29.2): a data row whose token count differs from the header's column count (the format is positional, so an empty value drops its token - including in the middle, which would otherwise shift the columns), or a `KICn`/`KIDn` pair declared in the header without a usable hex value in some row. An import (`mode`) of such a file answers `400` with that error - it is never partially imported. - `name` — one common name applied to every imported card; empty falls back to the card's ICCID. - `algorithm16` — the algorithm nibble for a **16-byte** key: `"3des2"` (default, KIc/KID nibble `5`) or `"aes"` (nibble `2`); any other value is refused with `400` (a misspelled algorithm must never silently pick one — the nibble decides whether the card accepts the packet). The key size alone decides the others: 8 bytes → DES (`1`), 24 bytes → 3DES3 (`9`); any other size skips that keyset with a report line. - `tars` — the TAR table every imported card gets (defaults to the three role entries with their spec TARs and MSL 16). A malformed table (a non-3-byte TAR, a non-1-byte MSL, a duplicate) is refused with `400`. - `tars` entries may carry their own `keyset` (1–15 — the preview's per-row choice, v3.29.1): it wins; the entries without one fall back to `tar_keyset`, and without that to the card's first keyset (a multi-keyset card would otherwise fail the store's "a non-zero MSL needs a default keyset" rule). A keyset outside 1–15 is refused with `400`. - `tar_keyset` — an optional **global default keyset** (1–15) for the TAR entries that do not name one (the API's shorthand; the PWA sends per-row keysets instead). Recognised columns (case-insensitive): `ICCID` (the digits + odd-nibble F pad, the plain digits, spaced digits or the raw nibble-swapped EF hex all normalise to the same value), `PIN1`/`PIN2`, `PUK1`/`PUK2` (4–8 digits; an invalid value is dropped and reported, the card is kept), `ADM` (the key's **hex value**, 4–16 hex digits in byte pairs — the rule `/api/verify-adm` applies), `PSK` (the SCP81 key, exactly 32 hex chars) and `KICn`/`KIDn` — one keyset per numbered pair, up to 15. The **KIc/KID bytes are never in the file**: the keyset number is the column number (`KIC2` → keyset 2) and the algorithm nibble follows the key size, both bytes carrying the same number (TS 102 225 §A.2). A `PSKID` / `PSK_ID` column names the PSK identity; without one the card's ICCID is used (the identity the card sends in the ClientKeyExchange is ICCID-like). `IMSI*`, `EKI` and `KIKn` (the key-management key, unused) are ignored. The response carries the parsed presets (a dry run) or the import result (`added`/`skipped`/`errors`) plus the `report`: ```json {"ok": true, "preview": false, "added": 5, "skipped": 0, "errors": [], "report": {"columns": ["IMSI", "ICCID", "KIC1", …], "header_lines": 11, "rows": 5, "cards": 5, "keysets": ["KIC1/KID1", …], "tars": 3, "skipped": ["line 4: keyset 2 has an unsupported key size (10 bytes)"], "skipped_more": 0}} ``` ### `POST /api/counter-probe` Sync a preset's SCP80 counter with the card. The card accepts only a counter *above* its own (TS 102 225 §5.1.1, SPI1 b5b4 = `10`) and updates it only when the packet is accepted, so rejected attempts change nothing; the probe walks a doubling ladder (`start+1`, `+2`, `+4`, …) with a fixed read-only command until one is accepted, then persists `accepted+1` (monotonic) into the keyset. The search stops at a ceiling and after a bounded number of packets, so it can never run towards the 40-bit maximum, where the card blocks the counter. The card's own counter is never reported by the card (the PoR's CNTR is a copy of the packet's, TS 102 225 §5.2 Table 3), which is why this probe exists. ```json {"preset_id": "…", "kvn": 2, "tar": "000000", "spi1": "16", "spi2": "21", "cntr": "0001000000", "ceiling": "FFFFFFFF", "max_attempts": 40} ``` All fields except `preset_id` are optional: `kvn` picks a keyset (the first otherwise), `tar` defaults to the preset's ISD entry and `spi1` to that TAR's MSL, `spi2` to the RAM listing transport `21`, `cntr` to the keyset's counter, `ceiling` to `FFFFFFFF` (must stay below `FFFFFFFFFF`) and `max_attempts` to 40 (clamped to 1–64). The probe needs the PoR verdict, so bit b1 of `spi2` is always set (a caller's `00` becomes `01`; the transport bits are kept) - without a PoR the probe is blind. The probe always sends the read-only `SELECT MF` - the counter advances on the packet's security acceptance whatever the applet answers (`6A86`/`6D00` are normal there). A SPI1 without a counter check is refused (400). The ladder only advances on a `cntr_low` verdict (the card's "your counter is not above mine"); **any other verdict stops the search**. A stop on a non-`por_ok` verdict (`rc_cc_ds_failed`, `tar_unknown`, …) *after* at least one `cntr_low` keeps the counter value anyway - the earlier rejection proves the value is above the card's counter - but the response reports `success: false`, `counter_saved: true`, the `verdict` and the saved counter, stating that something is likely wrong with the security settings. Without a prior `cntr_low` nothing is saved (the value is not established), and a missing PoR is an error too. **Response:** `{"success": true, "accepted_cntr": "0001000000", "stored_cntr": "0001000001", "packets": 3, "stopped": "accepted", "attempts": [{"cntr": "…", "por_status": "cntr_low"}, …], "start": "…", "ceiling": "FFFFFFFF", "kvn": 2}` `stopped` is `accepted`, `ceiling`, `attempts` or `error`; on a non-`cntr_low` failure (refused ENVELOPE, wrong TAR, security error) the probe stops at the first attempt instead of hammering the card. ### `POST /api/tar-probe` Probe which OTA applications the card has registered: one **secured packet per TAR of the named preset** (v3.26.0 — the probe checks the preset's TAR list only; a `tars` list may select a subset, and a TAR outside the preset is refused with `400`), each carrying a harmless C-APDU (`SELECT MF` by default). Each row uses **its own MSL** (the request's `spi1` is only a fallback for a TAR the preset does not carry) and **its own default keyset** — the keyset resolved from that TAR's entry, so the request does not carry KIc/KID/keys and the probe's keyset selector is gone. A TAR whose MSL requires security (non-zero) without a default keyset, or one naming an undefined keyset, is refused with a hint **before any packet is sent**. The resolved SPI1 must carry a counter check (b5b4 ≠ 00) and ask for a PoR. Every accepted packet consumes a counter, which is persisted into that row's keyset like any other operation (each keyset has its own counter, TS 102 225 A.1); an explicit `cntr` seeds the first row only. The delivered SMS carries the OTA CPI IE by default (`includeCpi`, as in `/api/send-ota`). ```json {"preset_id": "…", "spi1": "16", "apdu": "00A40000023F00", "tars": ["000000", "B00000"]} ``` **Response:** `{"success": true, "registered": N, "total": M, "final_cntr": "…", "kvn": 2, "spi1": "16", "spi2": "01", "results": [{"tar": "000000", "label": "ISD", "spi1": "16", "kvn": 2, "verdict": "registered", "sw": "9000", "por_status": "por_ok", "por_sw": "6D00", "por_data": ""}], "steps": [...]}` Verdicts: `registered` (ENVELOPE 9000 + a PoR carrying the application's own status word/data), `no_answer` (accepted, PoR without an R-APDU), `no_por` (accepted, no PoR at all — the packet was silently dropped), `refused` (a non-9000 ENVELOPE, typically `6200` for a TAR without a registered application). Observed on a live card: `000000`, `B00000` and `B00001` registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused. ### eSIM / LPA (local ES10 operations) These endpoints work only when the equipped card is an eUICC (SGP.22/32); otherwise they answer `400` with `{"error": "The equipped card is not an eUICC"}`. They use pySim's ES10 static API — no lpac, no SM-DP+ contact, no profile downloads and no notification processing. All run under the card lock; `GET /api/status` reports `euicc` and `eid` for the PWA. - `GET /api/esim/chip` — `{"eid": "8908…", "info1": {…}, "info2": {…}, "addresses": {"default_dp_address": …, "root_ds_address": …}, "rat": [{…}], "errors": {"": ""}}` (a part the card does not support is reported in `errors` instead of failing the whole request). The EUICCInfo TLVs are requested raw and decoded per SGP.22 v2.6 §5.7.8 (pySim's classes are incomplete), `rat` comes from the ES10b GetRat command (§5.7.13) and undecoded TLVs are preserved in a `raw_tlvs` map: ```json {"eid": "89086030202200000026000024920451", "info1": {"svn": "2.2.2", "euicc_ci_pki_list_for_verification": ["8137…FB"], "euicc_ci_pki_list_for_signing": ["8137…FB"]}, "info2": {"profile_version": "2.3.1", "svn": "2.2.2", "euicc_firmware_ver": "4.2.0", "ext_card_resource": {"installed_application": 0, "free_non_volatile_memory": 439084, "free_volatile_memory": 9798}, "uicc_capability": ["usimSupport", "isimSupport", "…"], "ts102241_version": "9.2.0", "globalplatform_version": "2.3.0", "rsp_capability": ["additionalProfile", "testProfileSupport"], "euicc_category": "other", "forbidden_profile_policy_rules": ["ppr1"], "pp_version": "1.0.0", "ss_acreditation_number": "ED-ZI-UP-0826"}, "addresses": {"default_dp_address": null, "root_ds_address": "testrootsmds.gsma.com"}, "rat": [{"ppr_ids": ["ppr1", "ppr2"], "allowed_operators": [{"plmn": "EEEEEE", "gid1": null, "gid2": null}], "ppr_flags": ["consentRequired"]}], "errors": {}} ``` `uicc_capability`, `rsp_capability`, `forbidden_profile_policy_rules` and `ppr_flags` are ASN.1 BIT STRINGs decoded to the names of the set bits (the first content octet is the unused-bit count, bits are MSB-first); `euicc_category` accepts both the implicit (`0x8B`) and explicit (`0xAB`) tag encodings; `pp_version` (`0x04`) and `ss_acreditation_number` (`0x0C`) are the bare, untagged SGP.22 types; `tre_properties`, `tre_product_reference`, `additional_euicc_profile_package_versions` and `certification_data_object` are decoded when a card sends them. - `GET /api/esim/profiles` — `{"profiles": [{"iccid": "8970…", "isdp_aid": "A000…", "state": "enabled"|"disabled", "nickname": …, "provider": …, "name": …, "class": "test"|"provisioning"|"operational", "owner": "250-99", "icon_type": "png"|"jpg", "icon": "89504E47…", "icon_size": 1234}], "error": null}`. `icon` is the SGP.22 icon data (tag `0x94`) as hex and `icon_size` its byte count, so a client can show the profile image (`data:image/;base64,…`); both are `null` when the card sent no icon. - `GET /api/esim/notifications` — `{"notifications": [{"seq_number": 3, "operations": ["enable"], "address": "smdp.example.org", "iccid": "8970…"}], "error": null}`. `operations` are the decoded `ProfileMgmtOperation` flags (`install`, `enable`, `disable`, `delete`, in that order) — one or more per notification. - `POST /api/esim/profile` — `{"action": "enable"|"disable", "iccid"?: …, "isdp_aid"?: …, "refresh"?: true}` (one identifier required). With the refresh flag set the ISD-R returns OK *before* the REFRESH (SGP.22 v2.6 §5.7.16/§5.7.17 step 6) and the switch completes upon the TERMINAL RESPONSE or the following RESET (step 8): a `91xx` answer is that OK, so the server answers the REFRESH proactive command (logged in `/api/proactive-log`), **never retries the STORE DATA** (the mid-switch card answers `6985`) and re-initializes the card like an equip (physical reset, re-read ICCID/network state, new `card_session`). It then re-reads the profile list and reports the verified result: ```json {"ok": true, "result": "ok", "refresh_seen": true, "reinitialized": true, "verified": true, "state_after": "disabled", "iccid": "8970…", "card_session": 7} ``` `verified` is `false` when the target profile is not in the requested state after the re-init (`null` when the state could not be read). Failures carry the ES10c result code (`iccidOrAidNotFound`, `profileNotInDisabledState`, `profileNotInEnabledState`, `disallowedByPolicy`, `wrongProfileReenabling`, `catBusy`, `undefinedError`) and a short `message`; an unexpected status word is returned as `{"ok": false, "result": "undefinedError", "sw": "6985", "message": "SW 6985"}`. ### `POST /api/help` Get structured help for a shell command. ```json {"cmd": "apdu"} ``` Returns: ```json {"usage": "apdu [-h] [--expect-sw EXPECT_SW] [--raw] APDU", "description": "...", "args": [{"name": "APDU", "type": "positional", "help": "..."}]} ``` ### `POST /api/send-ota` Send an OTA command (SCP80) to the card via SMS-PP-DOWNLOAD ENVELOPE (one ENVELOPE per SMS). With `apdu` the secured packet is built server-side (no single-SMS limit); a packet that does not fit one SMS is sent as a **concatenated** download per TS 31.115 §4.3: the packet is split into SMS user-data parts (first SM 132 octets, following ones 134 — the first one additionally carries the concatenation and CPI IEs) and the segments are sent in order. A packet that would need more than 5 segments is refused (the card's concatenation buffer is the limit). With `sp` a pre-built packet is delivered the same way. **SMS framing — `includeCpi`.** By default (`true`) the delivered SMS TPDUs carry the OTA **CPI information element** (IEI `70`, null IED — TS 31.115 §4.2/§4.3), the marker of a command packet; it shrinks the SMS user data — a single SM takes up to **137** packet octets instead of 140, and the first concatenated segment **132** instead of 134. The RAM/ISD operations send with `includeCpi: true` — a concatenated command carries the CPI IE in its first SM (TS 31.115 §4.3), and those are the packets that span several SMs. With `false` no CPI IE is added (a single SM then carries no UDH at all and the full 140-octet budget is available) — for a send whose receiver does not expect the marker. **SPI1 and the TAR's MSL.** With a `preset_id` the packet's SPI1 is derived from the MSL of its `tar` (TS 102 226 §8.2.1.3.2.4) unless the request passes `spi1` — an explicit value wins (a hand send may deliberately ask for more security than the minimum), and a value below the TAR's MSL is reported as `"warning"` in the response (numeric compare). A TAR the preset does not carry, without an explicit `spi1`, is refused with `400` and a hint. The card may answer with PoR status `actual_response_sms_submit` (`0x0B`): the real response (a big GET STATUS listing, for example) then arrives as one or more proactive SEND SHORT MESSAGE commands. The server captures those SMS-SUBMIT TPDUs, reassembles the concatenated segments and returns the decoded response in `por` (as if it had arrived in the ENVELOPE), so callers see a normal `por.response_status == "por_ok"` with the remote status word and response data. Responses wrapped in the TS 102 226 5.2.2 Response Scripting template (`AB`/`AF`: executed-count TLV `80` + R-APDU TLV `23`) are decoded the same way (`response_type: "scripting"`), with the R-APDU's own status word and data. With `ram_format` the server handles the two RAM command formats of TS 102 226 §5.2.1: `"auto"` (the default when the key is present) sends a read-only `GET STATUS [ISD]` probe first — compact, then wrapped as `AA` (Command TLV `22`) — and sends the command in the format the card answered; `"compact"` / `"expanded"` pin it. The response reports the detected `ram_format` and `final_cntr` (the counter to use next — accepted probe packets consume counters). Without the key the command data is sent byte-exact: applet-directed payloads (HTTP OTA triggers, §9 push, expanded scripts) must not be wrapped. `final_cntr` is reported for the plain `sp` path too (a pre-built packet): the counter that was sent, advanced by one when the card accepted the packet. **`cntr` is optional (v3.20.0)** — the preset store is the source of truth: - with `apdu` (the server builds the packet) a missing `cntr` is seeded from the named preset's keyset (the one the packet's KIc/KID resolve to); the effective value is echoed back as `cntr`, so a caller that omits it learns what was used. The seed uses the packet's keyset whatever `SPI1.b5b4` says — with no counter check the CNTR field is spec-ignored, nothing is advanced or persisted, and the `OTA SEND: counter not tracked` line names it; - an explicit `cntr` always wins — the packet uses it verbatim (a hand send, the counter probe or a pre-built `sp` may deliberately pick one); - the store only moves **forward**: an explicit value (or its next counter) below the stored one is used for the packet but never lowers the store; a higher accepted value is persisted; - the `sp` (pre-built packet) path never seeds — the packet carries its own counter — so pass `cntr` with it to get `final_cntr` / persistence. A request with neither simply reports no `final_cntr` — it never fails the send. With `preset_id` the server persists the counter into the keyset the packet used (see *Card presets*), so a lost response or a closed tab cannot lose the increment; the request is refused when KIc/KID carry different keyset numbers (TS 102 225 A.2) or a non-zero keyset number the preset does not define. **Counter tracking follows SPI1.b5b4** (TS 102 225 §5.1.1): with `00` the counter field is "present, ignored, never updated", so a packet whose SPI1 has no counter check (the keyless `SPI1 00` form) reports **no** `final_cntr` and persists nothing — a keyless send must leave every preset counter untouched. The spec's no-security KIc/KID values `00/00` are accepted when the SPI says no ciphering / no RC-CC-DS (A.2), so `{apdu, spi1: "00", kic: "00", kid: "00"}` builds a keyless packet (the card accepted exactly that form; a fake `15/15` was refused with `6200`). When a PoR was requested and the card did not answer, the response carries `por_missing: true`. **Request body:** ```json { "sp": "00201516011515b00000...", "spi1": "16", "spi2": "01", "kic": "15", "kid": "15", "tar": "b00000", "cntr": "0000000001", "kicKey": "D6FCC023...", "kidKey": "1B07E7E0...", "preset_id": "7ee0367f3a444b28988fea5fbc50de9f", "ram_format": "auto", "includeCpi": true } ``` **Response (delivery PoR):** ```json {"success": true, "sw": "9000", "response_data": "027100000e0a...", "bytes": 36, "segments": 1, "ram_format": "compact", "final_cntr": "0000000002", "por": {"response_status": "por_ok", "tar": "B00000", "pcntr": 0, "decoded": {"number_of_commands": 1, "last_status_word": "6e00", "last_response_data": ""}}} ``` `bytes` is the secured packet size and `segments` the number of SMS segments sent (1 = single SMS, >1 = concatenated download; the failure response carries them too, plus an `error`). **Response (submit PoR):** PoR is extracted from the SMS-SUBMIT TPDU fetched via a proactive command (FETCH). The response contains the same `por` structure if decoding succeeds. The response form follows the command's TAR (v3.21.0): a remote-management TAR (the preset's role entries and the standard ISD/RFM allocations of TS 101 220 Annex D) is decoded as an RM response; any other TAR's secured data is the receiving application's own (TS 102 226 4) and is reported with `por.response_type` `raw` - the whole secured data in `decoded.last_response_data` and no status word (`secured_data` carries the response bytes in every case). The same `raw` classification covers a secured data that cannot be the compact structure (its command count exceeds the command script that was sent, or it is too short for Table 5.1), while a genuinely malformed packet or a failed RC/CC/DS check still yields no `por`. The SPI2 `por_in_submit` bit (0x20) selects submit-mode PoR. ### `POST /api/cap-info` Validate a Java Card `.cap` archive and estimate its memory requirements. Read-only: it runs the same structural parse as the install paths (so a corrupt or wrong-format file fails here first) plus the bundled CAP analyzer (`pysim_simple_server/capmem.py` — component parsers and a method-bytecode allocation scan). It never touches the card, the SCP81 listener or the scripts; the install endpoints stay self-sufficient and the estimate is not used for installation. ```json {"cap_hex": "504B0304..."} ``` **Response (ok):** ```json { "ok": true, "load_file_aid": "AA1902BC226001", "module_aid": "AA1902BC226001", "load_file_bytes": 1234, "memory": { "package_aid": "AA1902BC226001", "applet_count": 1, "applets": ["AA1902BC226001"], "imports": [{"aid": "A0000000620101", "minor": 0, "major": 1, "refs": 6}, {"aid": "A0000000090005FFFFFFFF8912000000", "minor": 11, "major": 1, "refs": 7}], "flags": {"raw": 4, "int": false, "export": false, "applet": true}, "package_name": null, "components": [{"name": "Header", "size": 20}, {"name": "Method", "size": 433}], "class_count": 3, "method_count": 12, "code": {"method_component": 850, "load_file": 1234}, "nvram": {"static_image": 12, "array_init": 4, "install_objects": 100, "header_overhead": 24, "ref_storage": 8, "total": 148, "runtime": 0, "requirement": 1382}, "ram": {"transient_arrays": 16, "runtime_transient": 0, "peak_frame": 8, "total": 24}, "suggested": {"c6": 850, "c7": 272, "c8": 148}, "warnings": [] } } ``` `imports` are the libraries the CAP is linked against (JC VM spec §6.6, the Import component), each with the export-file version recorded in the CAP and the number of distinct constant-pool references to it (§6.7; a linked but unreferenced package is 0). A card resolves an import only when the resident package has the **same major** version and a **minor ≥** the recorded one (§4.5.2), hence the `name >= version` display. `flags` decodes the Header package flags (Table 6-4: `0x01` uses `int`, `0x02` exports an API, `0x04` applet package); `package_name` is only present in JC 2.2+ headers (CAP 2.1 files have none); `components` lists the archive entries in load-file order (each size includes the component's tag and size header, so the sizes sum to `load_file_bytes`). `code.load_file` is the concatenated load file (all components) - the package image the card stores, our proxy for the GlobalPlatform Card Spec v2.3.1 Table 11-48 "non-volatile code" minimum memory requirement; the tool's bytecode-only figure stays in `code.method_component`. The suggested `C6` follows the load file, and `nvram.requirement` = `code.load_file` + `nvram.total` (the C6+C8-style total per §11.5.2.3.7: with no code/data split in the card's memory the required minimum is the sum of both). The estimate excludes card-specific memory management, allocation rounding and the GP registry entry. **Errors** (HTTP 200 with `ok: false`, like `/api/scp81/gen-install`): `{"ok": false, "error": "cap parse failed: File is not a zip file"}` for a corrupt/wrong archive, or `cap analysis failed: …` when a component passes the structural parse but not the analyzer. The numbers are an estimate: the model assumes 2-byte references, a 6-byte object header and NVM cell rounding, and does not include applet-created runtime objects/arrays. ### `POST /api/test/run` Runs a **test script**: an ordered list of actions and proactive-command expectations, executed server-side on the equipped card. While a run is active the card is owned by the script - other card endpoints answer `409 {"error": "test script running ..."}` and background STATUS polling is suspended; only `/api/test/*`, `/api/status`, `/api/poll-status`, `/api/version` and static files stay available. ```json {"script": {"name": "STK menu browsing", "steps": [ {"type": "action", "kind": "menu-select", "params": {"item_id": 128}, "check": {"sw": "91??"}}, {"type": "expect", "command": "SELECT ITEM", "checks": [{"kind": "item", "id": 1, "text": "test"}], "respond": {"result": "ok", "item_id": 1}}, {"type": "expect", "command": "DISPLAY TEXT", "checks": [{"kind": "text", "value": "hello"}], "respond": {"result": "ok"}} ]}, "preset": {"name": "lab card", "keysets": [{"kic": "15", "kid": "15", "kicKey": "...", "kidKey": "...", "cntr": "0000000A"}], "tars": [{"role": "isd", "tar": "000000", "msl": "16"}, {"role": "uiccRfm", "tar": "B00000", "msl": "16"}, {"role": "usimRfm", "tar": "B00001", "msl": "16"}]}} ``` The PWA sends the current script inline (a run always uses what the editor shows) plus `script_id` when the script is stored; sending `script_id` alone resolves the script from the store. The PWA sends `preset_id` (the stored preset's id) instead of `preset`; the server resolves it from the preset store, so a run always uses the stored keysets, TARs and counters. The inline `preset` form stays for external callers. **Action steps** (`type: "action"`): `kind` is `envelope` (`event`, `data`, optional `src` - the device-identities source override), `event` (`event` name or hex + `fields` for the four server-built events 0x03/0x0B/0x12/0x1D, or the PWA form's built `data` hex for any other event - validated as hex and against the one-ENVELOPE budget at load; optional `src`, the device-identities override), `menu-select` (`item_id` 1-255, or `text` + `mode` `exact`/`contains` (case-insensitive by default; `case_sensitive: true` forces an exact-case match) - the text is resolved against the card's cached SET UP MENU, which is refreshed whenever the card sends the menu), `file-write` (`path`, `data`, `mode` `auto`/`binary`/`record`, `record`), `file-read` (same, verifies `check.data`), `apdu` (raw transport, no auto-handler), `scp80` (`apdu` or `sp`, selected by the optional `source` field when both are present; optional `kvn` (keyset number 1-15), `tar`/`spi1`/`spi2` overrides and `format` (`compact` default / `expanded` / `expanded-ae` - the TS 102 226 5.2.1 Command Scripting template around the C-APDU) - KIc/KID and the counter always come from the resolved preset, which must match the equipped card and be complete), `status` (`attempts`, `interval_ms` - when `attempts > 1` the default SW check is the mask `91??`, i.e. poll until the card announces a command) or `proactive-drain` (the proactive drain: poll STATUS and consume every announced command - `respond`/`first` TERMINAL RESPONSE results, `attempts`/`interval_ms` bound, optional `require` `{command, qualifier?}` asserting at least one drained command; the final SW is the step's `check.sw`, so a still-pending card fails and hands the command to the next step - an empty drain passes unless `require` was given). It is exempt from the runner's unexpected-pending-command guard, so it also consumes a command a previous step left pending (`91XX`). `check` is `{"sw": ..., "data": ...}` (exact or `{"mode": "mask", "value": "91??"}`, `?` = per-nibble wildcard) plus `"por"` for SCP80 - `"none"`/`"ok"`/`"any"` or an object `{"status"?, "sw"?, "data"?}` asserting the decoded PoR of this exchange (inline or SEND SHORT MESSAGE; at least one field). `on_fail` is `error` (terminates the script) or `warning` (continues) and applies to every step failure - including an expectation that finds no pending command (v3.21.0; `warning` is the tolerant consume-if-pending form) and a card/transport error raised during the step (v3.22.7 - the error is reported as a failed `Error` check at the step's level). **Expectation steps** (`type: "expect"`) require a command pending from the previous step (`91XX`); they never poll - a `9000` response means no command and fails the step at its `on_fail` level (TS 102 221 7.4.2.1 / TS 102 223 6.3; a `status` action with `attempts > 1` polls when the card delivers on poll). `command` is a name or type code; `checks` may be `text` (contains/exact), `alpha` (the command's Alpha identifier `05`/`85`, contains/exact - distinct from the `8D` text string), `item` (`id`/`text` for SELECT ITEM / SET UP MENU), `raw` (mask), `por` (a SEND SHORT MESSAGE carrying the PoR of the preceding `scp80` step - PoR-in-submit, SPI2 bit `20`: `status` (the response status name, `ok` = `por_ok`), `sw` (the R-APDU status word, exact/mask) and `data` (the R-APDU response data, exact/mask)), `sms` (the SEND SHORT MESSAGE TPDU: `da` digits with whitespace skipped and a leading `+` ignored, `pid`/`dcs`/`ud` hex exact/mask, `udl` decimal; SMS-SUBMIT only) or `files` (the File List of a REFRESH, TS 102 223 8.18; `files` is a list of path hex strings, matched order-insensitively); `respond` is the TERMINAL RESPONSE in the TS 102 223 6.8.0 object order (`result` - a script alias or a two-digit hex value from the full TS 102 223 8.12.0 list; `additional_info` - the general result's additional-information hex byte, sent as `83 02 `, default `00`; `text`+`dcs` for GET INKEY/GET INPUT, `dcs` `00` packed / `04` 8-bit / `08` UCS2; `item_id`; extra `raw` TLVs appended after the standard objects). The aliases (`ok`=00, `partial`=01, `missing`=02, `modified`=07, `cancel`=10, `back`=11, `timeout`=12, `refused`=22, `not_understood`=32, plus the compatibility-only `no_response`=12) follow the spec; v3.23.5 corrected `refused`, `not_understood`, `modified` and `no_response` (they used to carry legacy values). The terminal's own objects ride along unless `raw` overrides them: a successful PROVIDE LOCAL INFORMATION answer carries the TR Config PLI dictionary entry (or the host clock for the date/time qualifier) and a POLL INTERVAL answer the current-interval echo. The response is the initial state (`running: true`), the final counter (`scp80_counter`) and the step list; poll `/api/test/status`. The **server persists** every counter an SCP80 step consumed into the preset store as the run proceeds; the snapshot's `scp80_counter`/`scp80_counters` carry the latest values (the PWA only syncs its local copy - nothing is written back). A run that ends (an error, a stop, an unexpected pending command) with a command still announced **drains it with `ok` (0x00)** - fetching while the TERMINAL RESPONSE answers `91XX` (bounded); a cancel is never sent automatically (it tells the card the user aborted the session and can push assertive applications onto an error path). The cleanup is reported as a `cleanup` step; a script that wants the refusal path answers explicitly (an expectation's `respond`, or the `proactive-drain` action's `first`). ### `GET /api/test/scripts` and the script store Test scripts live in a server-side store (`~/.pysim-simple-server/test_scripts.json`, `--test-scripts PATH`) - the runner is server-side, so the store is the source of truth and every browser manages the same set. The store validates with the same engine the runner uses, so a stored script can never fail at run start for a validation reason. ```json {"path": "/home/user/.pysim-simple-server/test_scripts.json", "count": 1, "version": 2, "scripts": [{"id": "…", "name": "applet RFM update", "suite_id": "…", "require_adm": false, "steps": [...], "created": 1690000000.0, "updated": 1690000000.0}]} ``` **Schema v2 (v3.22.0): every script belongs to exactly one test suite** (`suite_id`; the suite is the root object) and may declare `require_adm` (the run verifies the ADM before starting). A v1 entry loads with `suite_id: null` and the server attaches it to an auto-created "Imported scripts" suite on startup. `POST /api/test/scripts` creates one (`{name, steps, suite_id, require_adm?}` or `{script: {...}}`; a valid `suite_id` is required - the script joins the suite's member list) and `POST /api/test/scripts/update` replaces name/steps/require_adm (`{id, script}`). A `suite_id` that differs from the current one is a **move** (both suites are updated in the request). `POST /api/test/scripts/delete` (`{id}`) detaches the script from its suite first - the suite's member list is the only reference, so no dangling id is left. `POST /api/test/scripts/move` (`{id, suite_id}`) moves a script to another suite (the target appends it as a member, keeping the `on_fail` policy) and `POST /api/test/scripts/copy` (`{id, suite_id, name?}`) clones it into the target. `POST /api/test/scripts/import` takes `{scripts: [...], mode: "merge"|"replace", suite_id?}` (the old localStorage export shape; invalid entries are reported in `errors` instead of failing the whole import; `suite_id` attaches the imported scripts to that suite). A `replace` is **refused while any suite still lists scripts** — the wipe would leave dangling references; delete those scripts first, or import with `merge`. ### `GET /api/test/suites` and the suite store Test suites live next to the scripts (`~/.pysim-simple-server/test_suites.json`, `--test-suites PATH`): a suite is the ordered list of its scripts - the optional `setup` first, the `member` scripts, the optional `teardown` last - plus the per-member `on_fail` policy, the suite-level `require_adm` and the optional **`start_reset`** (reset + fully re-initialize the card before the run - see `/api/test/run`). ```json {"path": "/home/user/.pysim-simple-server/test_suites.json", "count": 1, "version": 2, "suites": [{"id": "…", "name": "alfa regression", "require_adm": true, "start_reset": false, "scripts": [{"script_id": "…", "role": "setup", "on_fail": "stop"}, {"script_id": "…", "role": "member", "on_fail": "stop"}, {"script_id": "…", "role": "teardown", "on_fail": "stop"}], "created": 1690000000.0, "updated": 1690000000.0}]} ``` **Schema v2 (v3.23.0)** adds `start_reset` (default `false`); a v1 file loads with it `false`. The store normalises the order (setup first, teardown last, at most one of each; a script appears once per suite) and validates `on_fail` as `stop` (default) or `continue` - "which failures cascade" is a visible per-suite decision. `POST /api/test/suites` creates one (`{name, require_adm?, start_reset?, scripts?}` or `{suite: {...}}`), `POST /api/test/suites/update` replaces name/require_adm/start_reset/scripts (`{id, suite}`; the **member set** changes through the script endpoints, not here), `POST /api/test/suites/delete` (`{id}`) removes it - refused while the suite still holds scripts - and `POST /api/test/suites/import` takes `{suites: [...], scripts: [...], mode: "merge"|"replace"}`: the scripts are imported first **always with `merge`** (a script replace would wipe the other suites' scripts; the mode applies to the suite store), ids preserved, a collision gets a fresh uuid and the suites' references are remapped; then the suites are imported (`replace` wipes the suite store first); an unresolvable reference is reported in `errors` and the suite is skipped. After the import the ownership invariant is reconciled: a script the imported suites list adopts its listing suite, a script listed by several suites keeps its explicit owner and the other listings are pruned, and a script no suite lists any more — the `replace` mode wipes the suites that owned them — is **adopted into "Imported scripts"** instead of becoming invisible (the same reconciliation runs at startup for a v3.21.0 store). The import always **merges**: an id already in the store gets a fresh uuid (scripts) / a fresh suite id, so importing a bundle **into the store it came from duplicates the suite and its scripts** — the round-trip is not idempotent. Restore a bundle into a fresh store, or wipe the target first (`mode: "replace"` for the suites). ### `POST /api/test/run` (single script or suite) `{script, script_id?, preset_id?, require_adm?}` runs one script (the PWA sends the edited copy; the server validates and runs it). `{suite_id, preset_id?}` runs a suite: its scripts in order, one card session, no resume. With `start_reset` the card is **physically reset and fully re-initialized** first (ICCID, network state, TERMINAL PROFILE, the equip drain); the run's preset must still match the card's ICCID afterwards (a mismatch refuses the start) and a required ADM is verified against the **new** card session. Then the ADM prerequisite (suite- or member-level `require_adm`: the matched preset's key is verified **once**; a failure refuses the start, is not retried automatically, and requires a manual verify), setup, members (per-member `on_fail`), teardown. The initialization consumes the card's initial proactive commands (they stay in the proactive log), so a script cannot catch the first command after the reset. **The teardown runs on every stop** as long as the card is in the reader; a card reset (session change) stops the suite, skips the remaining members and is flagged in the report. The suite run's snapshot adds `suite`: the member records (`status`, `steps`, `note`, `log` - the semantic run-log excerpt), the `summary` (ok/warning/ error/skipped counts, `wall_ms`, `stopped`, `session_changed`, the SCP80 `counters_before`/`counters_after` per keyset), the `start_reset` flag and the `reset` result (ICCID + ms) and the run's `adm` state. The report is exported from the PWA as JSON or Markdown; the run state lives in the server until `POST /api/test/clear` (no history). ### `GET /api/test/status` The current (or last) run: ```json {"running": true, "name": "STK menu browsing", "status": null, "index": 1, "total": 3, "scp80_counter": null, "steps": [{"index": 0, "type": "action", "label": "ENVELOPE(Menu Selection)", "status": "ok", "sw": "9103", "sent": "80C2000009...", "checks": [{"label": "SW", "ok": true, "expected": "91??", "actual": "9103", "level": "error"}], "ms": 4}]} ``` `status` becomes `ok`/`warning`/`error`/`stopped` when the run finishes; expected/actual pairs are reported per check. ### `POST /api/test/stop` Requests a stop (`{"stop": true}` is set on the run); the runner finishes the current step, drains any pending proactive command with an **ok** (0x00) TERMINAL RESPONSE (the same bounded cleanup as a run that ends - a cancel is never sent automatically, v3.21.0) and reports the run as `stopped`. ### `POST /api/test/clear` Clears a finished run report (409 while a run is active). ### `POST /api/ram-install` Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE; the PoR verdict and the remote command's own status word are both checked and the sequence aborts on the first failure (a non-`por_ok` PoR, a remote SW outside the success set, or an undecodable PoR). The counter advances only for a packet the card accepted (PoR `por_ok`); `final_cntr` is returned on success **and** on failure, so the caller keeps the card's consumed counter (a rejected packet leaves it unchanged). A missing `cntr` is seeded from the preset's keyset (v3.20.0); an explicit value wins but never lowers the stored counter (only forward moves are persisted). The RAM command format is detected per operation: a read-only `GET STATUS [ISD]` probe (`FORMAT CHECK (compact)`, then `FORMAT CHECK (expanded)`) decides between the compact C-APDU and the expanded `AA`/`22` form (TS 102 226 §5.2.1), and every INSTALL/LOAD step of the chain then uses the detected format — the response carries `ram_format`. Pass `ram_format` (`compact`/`expanded`) to pin it. The **NV footprint** is measured around the chain: a best-effort `GET DATA FF21` (Extended Card Resources, TS 102 226 §8.2.1.7.2) read before the first step and again after the last one (also on failure) reports the free non-volatile memory and its delta — the read is silent (no step record, no failure) and a card without FF21 simply yields no fields. The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required. `delete_package: true` prepends a **cascade GP DELETE** (`P2=0x80`) on the package AID — a leftover package blocks `INSTALL [for load]`, so a re-install can clear it first; the delete step tolerates 'not found' (`6A88`/`6A82`, a fresh card) and any other delete failure stops the chain with its reason (the same policy as the batch and `/api/ram-install-app`). **Request body:** ```json { "cap_hex": "DECAFFED...", "sd_aid": "A000000003000000", "install_params": "C900", "stk_params": "", "nv_quota": 0, "volatile_quota": 0, "make_selectable": true, "spi1": "0E", "spi2": "01", "kic": "15", "kid": "15", "tar": "000000", "cntr": "0000000001", "kicKey": "D6FCC023...", "kidKey": "1B07E7E0..." } ``` | Field | Req | Description | |---|---|---| | `cap_hex` | yes | Even-length hex of the `.cap` file (zipped Java Card CAP), max 48 kB (98304 hex chars) | | `sd_aid` | no | Security Domain AID for INSTALL[for load]; empty → default ISD `A000000003000000` | | `install_params` | no | Hex of the install-parameter field — the mandatory `C9` TLV (application-specific parameters; `C900` when empty), the System Specific Parameters `EF`, extra raw TLVs. When empty the server **composes** it from `volatile_quota`/`nv_quota` and `stk_params` (see below) | | `stk_params` | no | The STK part appended to the install parameters: the **EF-wrapped** CA (`EF CA …`, TS 102 226 §8.2.1.3.2.1 — the SIM access domain leads the CA value as `01 ` / `00`) or the UICC template (`EA 80 … [81 …]`). A bare `CA` is refused (it belongs inside the EF) | | `nv_quota` / `volatile_quota` | no | Integer memory quotas (bytes) coded as `C8` / `C7` in the install EF; only composed when `install_params` is empty. `0` is a valid quota (the vendor reference form carried `C7 02 0000 C8 02 0000`); omit the field to leave the TLV out | | `make_selectable` | no | If true (default), final INSTALL uses P1=`0C` (install + make selectable) | | `load_block_size` | no | Bytes of load-file payload per LOAD APDU, 1–240 (default 240 when omitted). SCP80 concatenation carries a secured packet larger than one SMS over up to 5 SMs, so the block size is no longer clamped to fit a single SMS. | | `ram_format` | no | RAM command format: `auto` (default — the read-only probe decides, recorded as `FORMAT CHECK (...)` steps), `compact` or `expanded` to pin it | | `includeCpi` | no | As in `/api/send-ota`: `true` (default) marks the delivered SMS with the OTA CPI IE; `false` drops it (the full 140-octet single-SM budget then applies) | | `delete_package` | no | If true, prepend a cascade DELETE on the package AID (`P2=0x80` — the load file with its modules and installed applications). The delete step tolerates 'not found' (a fresh card proceeds); any other delete failure stops the chain | **Install-parameter composition (TS 102 226 §8.2.1.3.2.1; GPD_SPE_013 v1.1 Table 6-5).** The memory quotas (`C7`/`C8`) and an EF-form STK part (the CA) must share **one** System Specific Parameters `EF`; an EA-form STK part is a sibling of `EF` (the reference TCA loader sends `C9 00 EF 00 EA …`). A second `EF` makes the card read the quotas and ignore the CA (live 2026-10-05: `por_ok` but no toolkit registration and no menu), so the server refuses the combination with a 400 and names the fix: - caller-composed `install_params` that already carry an `EF` **plus** an EF-form `stk_params` → refused; - `nv_quota`/`volatile_quota` next to non-empty `install_params` → refused; - with an empty `install_params` the server composes `C900 EF{C7?,C8?,CA}` (or `C900 EF{quotas} EA…` for the EA form). The PWA composes the same single EF itself, so its RAM installs never hit the refusal. **Response (success):** ```json {"success": true, "failed_step": null, "steps": [{"name": "INSTALL [for load]", "por_status": "por_ok", "por_sw": "9000", "por_type": "compact", "por_cntr": "000000011B", "sw": "9000", "bytes": 58, "segments": 1}, {"name": "LOAD (1/9)", "por_status": "por_ok", "por_sw": "9000", "por_type": "compact", "sw": "9000", "bytes": 274, "segments": 3}, {"name": "INSTALL [for install]", "por_status": "por_ok", "por_sw": "9000", "por_type": "compact", "sw": "9000", "bytes": 66, "segments": 1}], "final_cntr": "0000000004", "load_file_aid": "A000000003000000", "module_aid": "A000000003000000", "application_aid": "A000000003000000", "load_block_size": 240, "load_block_size_requested": null, "load_block_size_auto": true, "nv_before": 50646, "nv_after": 23400, "nv_delta": 27246} ``` `load_block_size` is the effective size used for the LOAD blocks (240 by default), `load_block_size_requested` echoes an explicit `load_block_size` (null = the default was used) and `load_block_size_auto` marks that default. Each step reports the secured packet size `bytes` and the number of SMS `segments` it took. `sw` is the transport (ENVELOPE/GET RESPONSE) status word; the RAM results are in `por_status` (the PoR verdict: `por_ok`, `rc_cc_ds_failed`, `cntr_low`, ... or `no_por` when the card sent none) and `por_sw` (the remote command's own status word from the compact/expanded response, with `por_type`/`por_cntr`/`por_data`/`por_raw` for context). `nv_before`/`nv_after` are the free non-volatile memory (bytes) read via `GET DATA FF21` before and after the chain, `nv_delta` their difference (what the operation consumed; negative = memory was released, e.g. a discarded partial load). The three fields are omitted when the card has no FF21 readout; on a failure the values still show what the failed attempt consumed. The two FF21 reads consume counters like any accepted packet, so `final_cntr` already includes them. A step fails when the PoR is not `por_ok`, when `por_sw` is outside the success set (`9000`, `61xx` more data, `62xx`/`63xx` warnings, `CAFE` GP "more data"), or when response data arrived but could not be decoded. The failing step carries `por_error` (e.g. `remote SW 6700`) and the response sets `success: false`, `failed_step` and a detailed `error` such as `LOAD (1/9): remote SW 6700`. **Response (failure):** ```json {"success": false, "failed_step": "load_1", "steps": [{"name": "install_for_load", "por_status": "por_ok", "sw": "9000"}, {"name": "load_1", "por_status": "rc_error", "sw": null}], "error": "..."} ``` The `steps` array contains one entry per GP command. `final_cntr` is the counter value after all successful steps (use it to update the card preset). The response is not streamed — all steps run server-side before the JSON is returned. ### `POST /api/ram-batch/start`, `GET /api/ram-batch/status`, `POST /api/ram-batch/stop`, `POST /api/ram-batch/clear` The **batch operations** (v3.28.0; the `reinstall-app`/`delete` kinds v3.29.0): one operation applied to card after card, for bulk provisioning or repair. The loop runs **in the server** (a worker thread), so a page reload or a closed tab does not interrupt it; the state lives in memory and a server restart ends the run. - `POST /api/ram-batch/start` — `kind` selects the operation; the plan is card independent and is built once, so a bad `.cap`, AID or parameter combination is a `400` at Start, not a per-card failure. The body carries the kind's fields **without** the per-card ones (`preset_id`, `tar`, `spi1`, `cntr`, `kic`/`kid`/`kicKey`/`kidKey`): - `kind: "install-cap"` (default) — the `/api/ram-install` payload (`cap_hex`, `sd_aid`, `install_params`, …) plus `delete_package` (a cascade DELETE on the package AID before the chain, see `/api/ram-install`); - `kind: "reinstall-app"` — the `/api/ram-install-app` payload (`loadfile_aid`, `module_aid`, `instance_aid`, `privileges`, `install_params`, `stk_params`, `make_selectable`, `nv_quota`/`volatile_quota`): DELETE the applet instance (tolerating 'not found'), then INSTALL [for install] from the loaded package — no `.cap`, no LOAD; - `kind: "delete"` — `aid` (5-16 bytes of hex) and `target`: `"app"` deletes the object only (an applet instance, `P2=00`), `"package"` the object and its related objects (the load file with its modules and installed applications, `P2=80`). **No card is needed** — the worker waits for one. `400` for a bad request or an unknown `kind`, `409` when another batch or a test run is active. The response is the status payload below plus `ok: true`. - `GET /api/ram-batch/status` — **card-free** (the panel polls it while an install holds the card lock): ```json {"active": true, "phase": "waiting_card", "kind": "reinstall-app", "attempts": [{"ts": 1760000000.0, "iccid": "8970119000004600098", "preset": "Batch 0002", "result": "installed", "duration_ms": 4120, "final_cntr": "0000000005", "steps": [{"name": "DELETE (application)", "ok": true, "sw": "6101", "not_found": false}, {"name": "INSTALL [for install]", "ok": true, "sw": "6101", "not_found": false}], "steps_total": 2}], "attempts_total": 3, "current": null, "notice": "Card 8970119000004600098 reinstalled.", "counts": {"installed": 2, "failed": 1, "skipped": 0}, "started": 1760000000.0, "stopped": null, "progress": null} ``` `phase` is `idle` (no run), `waiting_card`, `installing`, `stopping`, `stopped` or `finished`; `kind` names the operation (the panel's labels follow it: installed / deleted / reinstalled). A failed attempt carries `failed_step` (e.g. `LOAD (3/7)`), `por_sw` (the card's own status word), `por_status`, `por_error` and `sw` — or, when the chain died before a verdict (a lost card, a transport error), just `error`; a skipped one carries `reason` (a request/eligibility error: the operation never started). An attempt also carries the last three steps (`steps`: `[{name, ok, sw, not_found}]` plus `steps_total`), so the panel shows the per-step verdicts (`DELETE (application) 6101 · INSTALL [for install] 6101`) for the short delete/reinstall chains; a delete step that answered 'not found' has `not_found: true` and `ok: true`. `current` names the card being processed (`{iccid, preset, started}`) and `progress` mirrors the RAM progress payload while a chain runs. The report keeps the last 500 attempts (`attempts_total` counts them all). - `POST /api/ram-batch/stop` — sets the stop flag: a running install finishes first (an interrupted LOAD chain can leave a half-loaded package), then the run ends. It touches no card state, so it answers while the card lock is held (a `_CARD_FREE_POST` exception); a repeated stop is idempotent, `409` when no run exists. - `POST /api/ram-batch/clear` — drops the finished run's report (`409` while a batch is active). **Per-card resolution.** Every equipped card's **preset is matched by ICCID**; the preset's ISD role TAR supplies the keyset (v3.26.0), the keys and the counter seed (v3.20.0) — the same rules as `/api/ram-install`. A card with no readable ICCID, no matching preset or no usable ISD keyset is recorded as `skipped` with its reason and the batch keeps waiting. A card is attempted **once per run** (any outcome): re-equipping it is recorded as `already processed in this batch ()`, never installed again — restart the batch to retry it. Failures never stop the run and are never retried automatically. A **delete step** (the reinstall's instance delete, the delete kind, the `delete_package` pre-step) tolerates 'not found' (`6A88`/`6A82` — the object is already gone, the goal is met); any other delete failure stops that card's chain and the attempt reports the delete's reason. `/api/status` carries a small summary in `ram_batch` — the detailed state **minus the attempts list** (`{active, phase, current, notice, attempts_total, counts, started, stopped}`), so the panel's headline (the phase, the current card, the counts) rides the existing 2 s status poll; the attempts report is fetched from `/api/ram-batch/status` only when `attempts_total` (or the active flag) changed. ### `POST /api/ram-install-app` Run **INSTALL [for install]** (or **INSTALL [for make selectable]**) for an *already loaded* package — no `.cap` and no LOAD, so the install parameters can be iterated without re-loading. Since v3.29.0 the install **deletes the applet instance first** (`mode: "install"`; the operation does not delete the package, so the instance is removed for a clean re-install), tolerating 'not found' (`6A88`/`6A82` — a fresh card just installs); any other delete failure stops the chain with its reason. `mode: "make_selectable"` stays a single APDU. The APDU builders are shared with `/api/ram-install` (`_cap_install_apdu` / `_cap_make_selectable_apdu`; both case 3 — no trailing `Le`, which the card's SCP80 layer counts as a phantom command) and the step verdict/counter logic is the same (`final_cntr` is returned on success **and** on failure). **Request body:** ```json { "mode": "install", "loadfile_aid": "F0414C46416101", "module_aid": "F0414C4641610101", "instance_aid": "", "privileges": "00", "install_params": "C900", "stk_params": "", "make_selectable": true, "spi1": "0E", "spi2": "01", "kic": "15", "kid": "15", "tar": "000000", "cntr": "0000000001", "kicKey": "D6FCC023...", "kidKey": "1B07E7E0..." } ``` | Field | Req | Description | |---|---|---| | `mode` | no | `install` (default) or `make_selectable` (GP Table 11-44; needs `instance_aid`) | | `loadfile_aid` | cond | Package AID (required for `mode: "install"`) | | `module_aid` | cond | Executable module / applet class AID (required for `mode: "install"`) | | `instance_aid` | no | Application AID; empty → the module AID | | `privileges` | no | Hex privileges value (1 or 3 bytes), default `00` | | `install_params` / `stk_params` | no | As in `/api/ram-install`: the `C9` field (with any extra TLVs) and the STK part; an EF-form STK part must not follow a caller-composed `EF` (the quotas and the CA share one `EF`, v3.20.0) | | `nv_quota` / `volatile_quota` | no | As in `/api/ram-install`: composed into the install `EF` only when `install_params` is empty | | `ram_format` | no | As in `/api/ram-install`: `auto` (default — a read-only probe step decides), `compact` or `expanded` | | `includeCpi` | no | As in `/api/ram-install`: `true` (default) marks the delivered SMS with the OTA CPI IE | **Response:** `{"success": bool, "steps": [...], "final_cntr": "...", "ram_format": "...", "error": "...", "failed_step": N}` — the same step records as `/api/ram-install`. A missing `cntr` is seeded from the preset's keyset (v3.20.0; an explicit value wins but never lowers the stored counter). ### `POST /api/cap-compat` Run the **CAP compatibility test**: the format check, `INSTALL [for load]` and the LOAD blocks only up to the block that completes the **Import** component (the point where the JCRE verifies the import list, JC VM spec 4.5.2), then stop — no last-block flag and no `INSTALL [for install]`, so nothing is committed and nothing has to be deleted afterwards. The verdict (`imports_ok`) covers the **LOAD/import gate only**; the link gate is what a real install checks at `INSTALL [for install]`. **Request body:** as `/api/ram-install` (`cap_hex`, the SCP80 fields, optional `load_block_size`, `ram_format` and `includeCpi`) — a missing `cntr` is seeded from the preset's keyset (v3.20.0) — plus: | Field | Req | Description | |---|---|---| | `probe_imports` | no | `{"all": "x.y"}` or `{aid_hex: "x.y"}`: rewrite the Import component's requested versions (diagnostic) | | `probe_additions` | no | `[{"aid": hex, "version": "x.y"}]`: append synthetic imports (card-capability queries; an AID already present becomes a version override) | **Response:** `{"success": bool, "imports_ok": bool, "steps": [...], "boundary_block": N, "total_blocks": M, "load_file_aid": "...", "module_aid": "...", "ram_format": "...", "probe_imports": [...], "probe_unmatched": [...], "final_cntr": "...", "load_block_size": N, "error": "...", "failed_step": N}`. `probe_imports` lists the entries changed or appended (`{"aid": "...", "from": "1.3", "to": "0.0"}`; `from: null` = an appended synthetic entry). `probe_unmatched` lists override AIDs that matched no Import entry — the CAP does not import them, so the card is never asked about them — as `[{"aid": "...", "version": "0.0"}]` with the version the probe requested. The PWA renders both as a per-AID list in the result popup, together with the failing block's CAP section and the remote status word's meaning. ### `POST /api/sp-verify` Cross-check a secured packet against pySim's `OtaDialectSms.encode_cmd` reference. Returns the JS-generated packet, pySim reference, a match flag, and the decoded SPI fields. A missing `cntr` is seeded from the named preset's keyset (v3.20.0) — the same rule the PWA's builder uses, so a reference for a packet built from an untouched counter field matches. ```json {"spi1": "16", "spi2": "01", "kic": "15", "kid": "15", "tar": "b00000", "cntr": "0000000001", "apdu": "00a40000023f00", "kicKey": "D6FCC023...", "kidKey": "1B07E7E0..."} ``` **Response:** ```json {"js_sp": "...", "py_sp": "...", "match": true, "diffs": [], "spi": {"counter": "counter_must_be_higher", ...}} ``` ### `GET /api/menu` Returns the SIM Toolkit SETUP MENU captured from the card's TERMINAL PROFILE response at startup. Empty `{"items": []}` if the card didn't send a menu. **Response:** ```json {"command_number": 1, "items": [{"id": 128, "text": "Настройки/Settings"}], "title": "Alfa Mobile", "active": false} ``` ### `POST /api/menu-select` Sends an `ENVELOPE(MENU SELECTION)` with the selected item ID, then handles the card's proactive response (DISPLAY TEXT or SELECT ITEM). ```json {"item_id": 128} ``` **Response:** ```json {"type": "display_text", "text": "Hello", "sw": "9122"} ``` or ```json {"type": "select_item", "items": [{"id": 1, "text": "Sub-menu"}], "sw": "9122"} ``` ### `POST /api/menu-respond` Sends `TERMINAL RESPONSE` to the current proactive command with the given result code. Continues the proactive chain if the card responds with `91XX`. If no response arrives within `--menu-timeout` seconds (default 60, `0` disables), the server watchdog sends the `timeout` result itself. ```json {"result": "ok", "item_id": 1} ``` | `result` | TERMINAL RESPONSE code | Meaning | |---|---|---| | `ok` | `0x00` | Command performed successfully | | `cancel` | `0x10` | Proactive session terminated by the user | | `back` | `0x11` | Backward move in the proactive session requested by the user | | `timeout` | `0x12` | No response from the user | ### `GET /api/stk-status` Returns the current STK session state. ```json {"active": true, "pending": true, "pending_type": "select_item"} ``` ### `POST /api/read` Read file content. Auto-detects transparent vs record files. ```json {"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"], "mode": "raw"} ``` Returns transparent data: ```json {"success": true, "sw": "9000", "file_type": "transparent", "data": "...", "apdu_times": [{"type": "select", "ms": 12}, {"type": "read_binary", "ms": 9}]} ``` Returns records: ```json {"success": true, "sw": "9000", "file_type": "linear_fixed", "records": [{"num": 1, "data": "..."}, {"num": 2, "data": "..."}], "apdu_times": [{"type": "select", "ms": 12}, {"type": "read_record", "ms": 11}, {"type": "read_record", "ms": 13}]} ``` `apdu_times` reports each command's duration (command sent to response received) classified as `select`, `read_binary` or `read_record`; the PWA uses it for snapshot timing statistics. Other commands are not reported. ### `POST /api/write` Write raw hex data to a file. ```json {"name": "EF.ICCID", "fid": "2FE2", "data": "A0A1A2...", "parent_path": ["MF"]} ``` A full path may be used instead of name/fid/parent, like `/api/select` and `/api/read`: ```json {"path": "ADF.USIM/6F7E", "data": "4FE69DE7..."} ``` For record files: ```json {"name": "EF.ADN", "fid": "6F3A", "data": "A0A1...", "record_nr": 1, "parent_path": ["MF", "7F10"]} ``` Returns: ```json {"success": true, "sw": "9000"} ``` ### `POST /api/select` Select a file by name or FID, with optional parent selection. ```json {"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"]} ``` `parent_path` lists the path segments from MF to the parent (ADF names or FIDs); the legacy single-segment `parent_sel` is still accepted but is only unambiguous for ADFs. Resolution is strictly parent-scoped: model-known files are selected through the requested parent only (pySim `select_file()`), never via pySim's global selectables or its `probe_file()` model injection, so a same-FID file under another parent is never picked and the filesystem model is not modified. `allow_probe: true` (PWA custom files) additionally allows a model-unknown 4-hex FID to be selected directly; any temporary model object created for it is detached again before the response is sent. Returns: ```json {"name": "EF.ICCID", "fid": "2FE2", "file_type": "transparent", "file_size": 10, "record_len": null, "num_of_rec": null, "fci_hex": "621082024021...", "apdu_times": [{"type": "select", "ms": 12}], "exists": true} ``` `fci_hex` is the raw FCP template (`'62'`) from the SELECT response, used by the PWA's Exact FCI checks; `file_size` / `record_len` / `num_of_rec` drive the profiler's size and record checks. When the file does not exist the endpoint responds `404` with `{"error": "...", "exists": false}`. ### `POST /api/tree` Get directory listing with typed children. ```json {"name": "MF", "fid": "3F00"} ``` Use `parent_path` (or the legacy `parent_sel`) to list a subdirectory, e.g. `{"name": "DF.GSM-ACCESS", "fid": "5F3B", "parent_path": ["MF", "ADF.USIM"]}`. Returns: ```json {"exists": true, "name": "MF", "fid": "3F00", "file_type": "df", "children": [{"name": "EF.ICCID", "fid": "2fe2", "isDir": false}]} ``` ### `GET /api/events` Returns the event list captured from the card's SET UP EVENT LIST (an array of event byte values, or `[]` when none was received). ### `POST /api/event-send` Sends an `ENVELOPE(Event Download)` for a subscribed event. ```json {"event_type": 4, "event_data": "01A0"} ``` `event_type` is required (the SET UP EVENT LIST event byte); `event_data` is optional hex for events that carry data. Returns the SW and any response data: ```json {"sw": "9000", "data": "..."} ``` For **Poll Interval Negotiation** (event `0x1C`, TS 102 223 §7.5.22) the proposal rides in `event_data` as the Duration TLV `04 02 ` (unit `00` minutes / `01` seconds / `02` tenths of seconds) and the UICC's response is decoded into `negotiation`: ```json {"sw": "9000", "data": "1101020402011E", "negotiation": {"result": 2, "result_name": "modified", "unit": 1, "interval": 30, "seconds": 30}} ``` `result` is `00` accepted / `01` rejected / `02` modified (§8.97); no response data means *accepted*. A *modified* answer sets the background poll interval, and a *rejected* answer may carry the closest acceptable duration. Channel status (event `0x0A`, TS 102 223 §8.56) carries the Channel status TLV `B8 02 `, where the status byte is the channel id (1–7) OR-ed with the state bits (0x00 link not established / 0x40 TCP LISTEN / 0x80 link established) and the info byte is `00` (no further info) or `05` (link dropped). The server also sends this event automatically when a BIP link drops outside a proactive command and the card subscribed to `0x0A`. ### `POST /api/net-sim` Runs one network-condition scenario from `projects/UICC_NAA.md` section 13 against the equipped card and returns the step log: ```json {"scenario": "service_lost", "mcc": "262", "mnc": "01", "lac": "6CD7", "send_event": true, "dummy_locations": true, "keep_kasme": true} ``` Scenarios: `cold_boot`, `attach_eps`, `attach_2g`, `service_lost`, `limited_service`, `roaming_denied`, `churn`, `sms_received`, `cb_reconfig`, `authenticate`. Optional parameters: `mcc`/`mnc` (or `plmn`), `lac`, `cell_id`, `tac`, `rac`, `tmsi`, `ptmsi`, `ptmsi_sig`, `guti`, `ksi`, `kasme`, `ul`, `dl`, `algo`, `kc`, `rand`, `autn`, `churn_count`, `churn_delay_ms`, and the toggles `send_event`, `dummy_locations`, `invalidate_epsnsc`, `keep_kasme`, `write_kc`, `sms_location`, `cb_clear` (empty identity values are randomized). The event step is skipped when the card did not subscribe to Location status; only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent — EF.FPLMN is appended only by `roaming_denied` (shift-list semantics of TS 31.102 §4.2.16, duplicates skipped) and an attach to a listed PLMN clears its entry first (successful manual selection, TS 23.122), while the 5GS location files are never written. The response is `{success, error, steps:[{action, file, path, data, sw, ok}], net_state}` where `net_state` is the updated monitor state (see `GET /api/net-state`). ### `GET /api/net-state` Returns the cached **network-state monitor** — the panel the PWA's Phone tab shows next to the simulation buttons. The state is created when a card is equipped (only when EF.ICCID was readable) and is updated in place from the bytes the network simulation wrote, so this endpoint performs no card I/O: ```json {"available": true, "state": { "files": {"imsi": {"name": "EF.IMSI", "fid": "6F07", "path": "ADF.USIM/6F07", "present": true, "source": "init", "updated": 1758396000.0, "kind": "transparent", "data": "0829051032547698"}, "epsnsc": {"kind": "record", "records": [{"num": 1, "data": "..."}]}}, "service": {"state": "normal", "source": "service_lost", "time": 1758396123.0}, "network": {"service": {"state": "normal"}, "location": {"plmn": "26201", "mcc": "262", "mnc": "01", "area": "6CD7", "lac": "6CD7", "country": "Germany", "operator": "Telekom", "roaming": "home", "rejected": false, "source": "write"}, "home": "26201"}, "read_at": 1758396000.0}} ``` `available: false` with `state: null` means no monitor state exists yet (no card equipped or EF.ICCID not readable). `service.state` is the simulated service state (`normal` / `limited` / `none`, `null` until a scenario or a Location status event sets it); `network.location.roaming` is `home` / `equivalent` / `guest` against EF.HPLMNwAcT and EF.EHPLMN, and `rejected` is set when the location files or EF.FPLMN show a permanent rejection. Monitored file keys: `imsi`, `ehplmn`, `spdi`, `hplmnwact`, `loci`, `psloci`, `epsloci`, `epsnsc`, `cbmi`, `cbmir`, `smsstatus`, `fplmn`; each entry's `source` is `init` (equip read), `read`, `refresh` or `write` (patched from the simulator's own writes). The PWA decodes the entries client-side. ### `POST /api/net-state-refresh` Re-reads the monitored files from the card, merges them into the cached state (`source: refresh`) and returns the same shape as `GET /api/net-state`. The optional body filters the read: ```json {"files": ["imsi", "loci", "epsnsc"]} ``` Without a body all monitored files are re-read. Answers `503` when the reader is not initialized. ### `GET /api/arr` Returns the card's cached **EF.ARR copies** — the access-rule files the FCP security attribute DO `'8B'` references (ISO 7816-4 Table 25). The copies are read once per card session when a card is equipped (`MF/2F06`, `MF/7F10/6F06`, `MF/7F20/6F06` and `ADF.USIM/6F06`, best effort) and served from memory, so this endpoint performs no card I/O; a disconnect clears the cache, and an `EF.ARR` write through `/api/write` refreshes it. ```json {"ok": true, "session": 12, "arrs": [ {"fid": "2F06", "path": "MF/2F06", "record_len": 10, "num_records": 23, "records": ["FFFF...", "..."]}]} ``` `ok` is `false` (with an empty `arrs`) when the card has no readable ARR. The PWA's file manager resolves the `'8B'` reference of the selected file's FCI against this cache and shows the referenced record's access rules as indented lines; snapshots resolve the same references from their own captured `EF.ARR` copy. ### `GET /api/proactive-log` Returns the last 50 proactive commands fetched during CAT sessions, newest first: ```json [{"type_hex": "25", "type_name": "SET UP MENU", "elapsed": 3.2, "bytes": 97}] ``` ### `POST /api/status-poll` Manually sends `STATUS` (F2) and, if the card answers `91XX`, runs the proactive chain (FETCH → TERMINAL RESPONSE) until it settles. Returns: ```json {"sw": "9000", "proactive": true} ``` ### `POST /api/rescue` Recovers a stuck CAT session by clearing the pending state and re-sending the TERMINAL PROFILE. Returns whether a menu and event list were captured again (plus the profile used): ```json {"ok": true, "profile": "FFFF...", "menu": true, "events": [4, 5]} ``` ### `GET /api/terminal-profile` The TERMINAL PROFILE currently in effect and the CLI default (for reference; runtime changes are in-memory only): ```json {"profile": "FFFFFFFF7F9F00DFFF03021FE2000000C3FB000704117800710100000038428003", "bytes": 33, "cli_default": "FFFFFFFF7F9F00DFFF03021FE2000000C3FB000704117800710100000038428003"} ``` ### `POST /api/terminal-profile` Sets the TERMINAL PROFILE at runtime (in-memory) and re-sends it to the card, resetting the STK session state exactly like `/api/rescue`. Body with a new profile, or `{}` to re-send the current one: ```json {"profile": "FFFFFFFF7F1F007FFF00001F230811060700"} ``` Hex, even number of digits, 1–255 bytes. Response is the same shape as `/api/rescue` (with `ok: true`); invalid hex is a 400, no reader a 503. ### `GET /api/poll-status` Background STATUS polling state. Polling is enabled by default (a CAT terminal polls during idle, TS 102 221 §14.6.2; `--poll-interval 0` disables it at startup) and `enabled` is the operator's switch. `card_disabled` is true after the card sent POLLING OFF (TS 102 223 §6.4.14): proactive polling stays suspended until a POLL INTERVAL arrives, independent of `enabled` - the PWA shows the effective state (OFF while suspended). ```json {"enabled": true, "interval": 30, "card_disabled": false} ``` ### `POST /api/poll-toggle` Turns background STATUS polling on or off. ```json {"enabled": true} ``` Returns the new state (`{"enabled": ..., "interval": ..., "card_disabled": ...}`); while the card has disabled polling, enabling adds a `warning` explaining that the timer stays suspended until the card sends a new POLL INTERVAL. ### `GET /api/pli-qualifiers` Lists the PROVIDE LOCAL INFORMATION qualifier codes with their names. ```json [{"code": "00", "name": "Location Information"}, {"code": "0A", "name": "Battery Charge Level"}] ``` ### `GET /api/pli-dict` Returns the effective PLI data dictionary as a qualifier-code map: the operator's override, else the built-in default (the qualifiers whose objects the pinned ETSI specs define - location as the test network 001/01, the test IMEI/IMEISV, language, access technology, search mode, battery; v3.23.1). The date/time qualifier stays empty - the host clock answers it while no override is set. ```json {"00": "930500F1100000", "0A": "630104", "03": ""} ``` ### `POST /api/pli-dict` Updates dictionary entries. Body is a map of qualifier code to hex value. A non-empty value becomes an override; an **empty value clears the override**, so the built-in default (if any) applies again. Returns the updated effective dictionary plus, when any field was not saved, the lists that name them: `invalid` (a malformed key or value — not hex, an odd number of digits, a non-string value) and `unknown` (a well-formed code outside the qualifier list); both keys are omitted when everything was applied (v3.25.6). ```json {"00": "130500F1100000", "0A": "630102", "invalid": ["0G"], "unknown": ["18"]} ``` ### `POST /api/scp81/bip` Starts or stops the local target the card's BIP channel is redirected to. Dump mode captures whatever the card sends (e.g. its TLS ClientHello) without answering: ```json {"action": "start", "mode": "dump", "host": "127.0.0.1", "port": 8443} ``` Redirect mode (`mode: "redirect"`) starts **no local listener**: every BIP channel the card opens is connected to the configured target (`host`/`port` are required — no defaults), which terminates TLS and runs the administration dialog; the address the card requests is only logged. The status API reports `mode: "redirect"` with the target while it runs. (This is the behavior that was called `passthru` before 2.2.13 — the name is now taken by the mode below.) ```json {"action": "start", "mode": "redirect", "host": "203.0.113.10", "port": 10174} ``` Pass-through mode (`mode: "passthru"`) starts **no listener and has no target**: every BIP channel dials the destination the card requests in OPEN CHANNEL — the `Other address` (`3E`/`BE`) plus the `Transport level` (`3C`/`BC`) port, TCP client remote (`02`) only. The specs define no default port, so an incomplete or non-TCP request fails the channel with result `3A` and an `open-fail` log reason; `host`/`port` in the request are ignored. The status API reports `{"mode": "passthru"}` and the per-channel targets appear in `bip.channels`. ```json {"action": "start", "mode": "passthru"} ``` TLS mode runs the Phase B PSK TLS server (GPC v2.2 Amendment B): the PSK table is applied to the TLS handshake, and the GP HTTP administration dialog (`X-Admin-*` headers, 200 with a command string or 204 No Content) is served. `psk_map` is the lookup table for the identity the card presents in the TLS handshake — the PWA sends it from the card presets (`{identity, psk_hex}` objects or an `{identity: psk_hex}` map); a handshake whose identity is not listed fails with the log entry `tls-psk-unknown`. The legacy single-key form `psk_hex` (with optional `psk_identity`, empty = accept any identity) is still accepted; when both are omitted the table of the previous start is reused. Keys are never stored or logged. ```json {"action": "start", "mode": "tls", "host": "127.0.0.1", "port": 8443, "psk_map": [{"identity": "89012345678901234567", "psk_hex": "00112233445566778899aabbccddeeff"}], "script": ["80CAFF2100", "80F28002024F0000"], "script_kind": "Explore ISD"} ``` `script` is the APDU list served to the card (an explicit list, or `none`); the server is agnostic to what the APDUs do. `script_kind` is an optional label for the logs/results. Omitting `script` keeps the configured script and its run progress. Stop either mode with `{"action": "stop"}` (also disables the BIP terminal). ### `POST /api/bip/control` Generic BIP terminal control for the Simulator's **BIP** pill - the terminal side of the Bearer Independent Protocol for cards that use TCP without HTTP OTA. The session is shared with the SCP81 listener: only one BIP session runs at a time, starting either control stops the other, and the response reports what was stopped as `replaced: {"owner": "scp81"|"bip", "mode": ...}` (the PWA shows a notice). ```json {"action": "start", "mode": "sink", "host": "127.0.0.1", "port": 0} ``` Modes: - `sink` (default) - starts a local TCP listener that accepts the card's BIP channels and only logs what arrives (`conn`, `sink-rx`, `conn-close`); it never sends anything back. `port` may be `0`/omitted for an ephemeral port; the bound address is reported in the status. Use it for cards that open a TCP connection just to upload data. - `passthru` - no listener and no target: each channel dials the destination the card requests in OPEN CHANNEL (TCP client only, no default port; an incomplete or non-TCP request fails with result `3A`). - `redirect` - every channel connects to the required `host`/`port`; the address the card requests is only logged. `link_events` (default `true`) controls the terminal-side Channel status events (TS 102 223 7.5.11). An invalid request never disturbs a running session. Stop with `{"action": "stop"}`; the response carries the same body as the status endpoints. ### `GET /api/bip/status` Same shape as `GET /api/scp81/status`: `{"owner": "bip"|"scp81"|null, "bip": {...}, "listener": {...}}`. A running sink reports `{"mode": "sink", "host": ..., "port": ..., "connections": N}`. ### `GET /api/bip/log` / `POST /api/bip/log-clear` The shared BIP event log (`?after=`) - identical to `/api/scp81/log`. ### `GET /api/scp81/status` ```json {"bip": {"enabled": true, "mode": "redirect", "target": "127.0.0.1:8443", "channels": [], "seq": 12}, "listener": {"mode": "tls", "host": "127.0.0.1", "port": 8443, "psk_identities": ["89012345678901234567"], "psk_wildcard": false, "identity_seen": "89012345678901234567", "identity_matched": true, "version_seen": "TLSv1.2", "cipher_seen": "PSK-AES128-CBC-SHA256"}} ``` `owner` is `scp81` or `bip` (whichever control started the session), `null` when idle; it is returned by both this endpoint and `/api/bip/status`. Listener modes: `tls` (local PSK TLS server), `dump` (capture-only TCP listener), `redirect` (no local listener; the BIP channels go straight to the configured `host:port`, e.g. an external HTTP OTA platform — reported as `{"mode": "redirect", "host": ..., "port": ..., "target": "host:port"}`) and `passthru` (no listener and no target; each channel dials the destination the card requests in OPEN CHANNEL — reported as `{"mode": "passthru"}`, with the actual peer in `bip.channels[].target`). `psk_identities` lists the identities the listener accepts (keys are never exposed); `psk_wildcard` marks the legacy single-key mode. `identity_seen` / `identity_matched` reflect the last handshake: an unknown identity is logged as `tls-psk-unknown` and the handshake fails. ### `POST /api/scp81/psk-map` Replaces the PSK table of the running TLS listener (the PWA pushes card-preset edits without a listener restart): ```json {"psk_map": [{"identity": "89012345678901234567", "psk_hex": "00112233445566778899aabbccddeeff"}]} ``` Returns `{"ok": true, "identities": [...], "listener": {...}}`; entries without an identity or a valid key are skipped, and an empty table is rejected. ### `GET /api/scp81/log` Returns the BIP/TLS event log (open/close, SEND/RECEIVE DATA hex, TLS handshake and HTTP request/response records). `?after=` returns only newer entries; `seq` echoes the latest sequence number. ### `POST /api/scp81/queue` Replace the SCP81 command script (used by the Remote APDU tab's RAM chain "Queue in SCP81" and the PWA's "Restart script"). Body `{"apdus": ["80E60C002E...", ...]}` (or a single `apdu`), optional `kind` and `force`. Entries that already are Command Scripting templates (`AA...`/`AE80...`, the expanded format) are sent verbatim instead of being wrapped again. Refused while a script is mid-run unless forced; queuing resets the execution progress. ### `POST /api/scp81/gen-install` Generate the RAM (GP) APDU sequence for a `.cap` without touching the listener or the running script; the PWA's "Install from .cap" script template stores the returned list. The `.cap` is parsed server-side (same parser as `/api/ram-install`) and expanded to INSTALL [for load] -> LOAD blocks (240-byte payloads) -> INSTALL [for install]; the file itself is never stored. ```json {"cap_hex": "504B0304...", "sd_aid": "A000000003000000", "privileges": "00", "install_params": "", "stk_params": "", "nv_quota": 0, "volatile_quota": 0, "make_selectable": true} ``` `sd_aid` empty = the ISD. `install_params`, `stk_params` and the `nv_quota`/`volatile_quota` primitives follow the composition rule of `/api/ram-install` (the quotas and an EF-form STK part share one System Specific Parameters `EF`); an invalid combination answers `{"ok": false, "error": "..."}`. Responds with `{"ok": true, "apdus": [...], "load_file_aid": ..., "module_aid": ...}`. ### `GET /api/scp81/script` Returns the configured command script and the execution state: ```json {"script": ["80CAFF2100", "80F28002024F0000"], "next": 2, "total": 2, "done": [0, 1], "kind": "Explore ISD", "pending": {"index": 17, "pos": null, "page": true, "apdu": "80F28003024F0000"}, "pages": 11, "pages_queued": 0, "complete": false, "results": [{"index": 1, "pos": 0, "page": false, "sw": "9000", "apdu": "80CAFF2100", "rapdu": "FF210C810102..."}]} ``` `next` is the index of the next script APDU to send; `done` lists the script indices the card reported. `pending` describes the C-APDU awaiting the card's `X-Admin-Script-Status` report as `{index, pos, page, apdu}` (`pos` = script index, `null` for an auto continuation page) or `null`; `pages` counts the continuation pages queued so far and `pages_queued` those not yet sent. `complete` is true when every configured APDU was reported and nothing is in flight — a script can therefore be complete while a listing page is still being fetched (`pending.page` = true), which is tracked separately from the script's own progress. `results` entries carry the send order (`index`), the script position (`pos`, `null` for continuation pages) and the `page` flag. Execution tracking and resume: an APDU counts as executed only when the card reports it in the next POST's Response Scripting template. A POST with `X-Admin-Resume` continues with the unexecuted tail (the pending APDU is resent if its report never arrived), a POST without it is a fresh dialog where the script runs from the start, and a completed script closes the session with 204. Each APDU is delivered in an `AE 80 22 00 00` Command Scripting template (TS 102 226 §5.2.1) with `X-Admin-Next-URI`; the card returns its R-APDUs in the next POST's Response Scripting template, which is parsed and logged (`script-rapdu`, `script-memory`). Long GET STATUS listings that answer `63 10` / `CA FE` ("more data available") are auto-continued with the same command carrying P2.b1=1. TLS mode also accepts `chunked` (**default `true`** — the reference server's chunked framing; the card rejects a chunked response that also carries a Content-Length) and `chunk_size` (default `0` — the whole response in one TLS record, as in the decrypted reference session; a positive value writes the head and each body piece as its own record). Both are echoed by `GET /api/scp81/status`. The connection stays open between POSTs for the whole dialog (the card is the HTTP client and may reuse it or dial a new one at will — GP Am. B 4.3.1 leaves connection management to the SD); only the 204 ends the session, and the server then shuts the TLS session down cleanly (`close_notify` while the response is still buffered, then FIN). There is no option to close after every response. `compact_headers` (default `false`) omits the optional space after each header colon — legal per RFC 7230 3.2 (OWS), though a single SP is preferred, and it saves one byte per header (useful to keep a response within one card-sized TLS record); `conn_header` (default `'none'` = omit the header; `'keep-alive'` adds it) declares the connection fate, `tls_version` (default `'auto'` — accept TLS 1.0-1.2 and let OpenSSL pick the highest the card offers; `'1.0'`/`'1.1'`/`'1.2'` pin a version for debugging) selects the protocol window, `cipher` pins one suite, `next_uri` overrides the per-command `X-Admin-Next-URI` (`%d` = command id; empty string omits the header), `link_events` (default `true`, all modes) controls the automatic Channel status events, `answer_delay` waits before answering a request. `keylog` writes the TLS traffic secrets to the given file (SSLKEYLOGFILE format) for debugging captures — it contains key material, use a temporary path. The response headers are minimal: `X-Admin-Protocol`, the optional `X-Admin-Next-URI`/`X-Admin-Targeted-Application`, `Content-Type` on 200s and `Transfer-Encoding`/`Content-Length` per `chunked`. The Date/Server/X-Powered-By mimicry (`apache_headers`) was removed in 2.2.14 — the reference server's extra headers were an unnecessary copy (the real blocker was the BIP TERMINAL RESPONSE BER length) and the card accepts the minimal set. `GET /api/scp81/status` echoes the negotiated handshake as `version_seen` / `cipher_seen` (plus `identity_seen`); a handshake that fails for TLS/cipher reasons (no shared cipher, unsupported protocol version, card alert) is logged as `tls-handshake-failed` with the OpenSSL reason, separately from post-handshake record errors (`tls-error`). The PWA's Listener **Options** block (collapsed by default; a `custom` marker appears when anything differs from the reference defaults) exposes `chunked`, `chunk_size`, `conn_header`, `compact_headers`, `next_uri` (checkbox + template), `script_template`, `cr_tag`, `targeted_app` (checkbox + `//aid/...` field) and `link_events` (applied at Start, persisted in `localStorage`); `tls_version`, `cipher`, `keylog` and `answer_delay` stay API-only.