Commit Graph

175 Commits

Author SHA1 Message Date
catarrh 0d63e5592f docs: a refused envelope may carry no response packet (guide + help)
The alfa-dsa control-commands run showed the error case: an applet-level
refusal (envelope 6200 for an unknown command) carries no PoR at all, so
a por expectation on such a step can never match.  The authoring guide's
status-words section now states that a 62XX/63XX envelope does not
guarantee a response packet - other warning paths still deliver one (the
low-counter case arrives via SEND SHORT MESSAGE) - and that a step
expected to be refused should leave por at its "any" default or assert
"none"; help EN/RU notes the same.  docs/test-scripts.md only, no code
change; sw cache simple-v392 (help files are precached).
2026-10-04 22:30:30 +03:00
catarrh 78bc91d297 v3.19.2: the three-status-words disambiguation (envelope SW vs por.sw)
The script author read por.sw as the envelope SW, so a por.sw expectation
on an applet's own TAR failed with (none).

- docs/test-scripts.md: a 'The three status words (envelope SW / PoR
  status / R-APDU SW)' table in the authoring section - where each one
  comes from, the script field and the report row - plus a cross-reference
  from the por check paragraph; help EN/RU 8.9 points at it.
- a failed por.sw check on a response_type: 'raw' decode carries the check
  detail 'the response is application data - an applet's own TAR has no
  R-APDU SW; assert it with data' (the PWA renders check details).
- the runner's log prints APP DATA=<hex> (no R-APDU SW) for application
  responses instead of the misleading R-APDU SW=(none) data=...; the
  compact/scripting lines are unchanged.
- the step editor's custom-PoR block gets a one-line hint about the same.
Tests: 821 frontend + 730 Python; sw cache simple-v391.
2026-10-04 22:19:58 +03:00
catarrh 4a353720d0 v3.19.0 (review fixes): expose the menu-select Match case choice; doc/comment fixes
Review of 7250dbd:
- the step editor's menu-select form gains a Match case select
  (Case-insensitive (default) / Case-sensitive); the step summary flags a
  case-sensitive step with (case), the saved params carry
  case_sensitive: true only when chosen, and the docs say the editor
  authors the flag.
- docs/api.md: fix the 'with an optionally' grammar.
- the Variant-2 menu test's comment claimed a contains match while it is an
  exact one; a real contains leg is added ('ыбор' -> item 128).
- timeless rationale for the case-insensitive default; the format note now
  dates the change.
sw cache simple-v389 (index.html/help changed).
Tests: 821 frontend + 726 Python.
2026-10-04 21:18:36 +03:00
catarrh 7250dbd63a v3.19.0 (cont.): live fixtures; menu text match is case-insensitive by default
No test scripts have been authored yet, so the format is still free to
change - the menu-select text match is now case-insensitive by default
(menu labels are UI text; case_sensitive: true still forces an exact-case
match, and a miss on it names the case-insensitive candidates).

Verification against the live card's own bytes (captured 2026-10-04):
- the Annex A Variant 2 Cyrillic menu (ids 128..130) resolves by text and
  the case behaviour above is pinned ('кофе' -> item 130);
- the applet's SEND SHORT MESSAGE (alpha 'Отправка...', SMS-SUBMIT to
  +79332505884) pins the new alpha and sms checks and the run-log line.

docs/help/api updated; sw cache simple-v388 (help pages changed).
Tests: 819 frontend + 726 Python.
2026-10-04 21:05:19 +03:00
catarrh 92506e72b9 v3.19.0: the applet-agent script batch - por objects, alpha/sms checks, scp80 format
The test scripts get the pieces an applet test needed:

- scp80 check.por accepts an object {status?, sw?, data?} asserting the
  decoded PoR of this very exchange (inline or via SEND SHORT MESSAGE);
  the evaluation is shared with the expect-step por check
  (_por_check_results).  The editor's PoR select gains 'custom' with the
  three fields (an empty object refuses on save).
- a new alpha content check reads the command's Alpha identifier (05/85,
  Annex A coded) - the text check stays bound to the 8D Text string, so a
  command carrying both keeps them distinct.
- a new sms content check asserts a SEND SHORT MESSAGE's submit TPDU:
  DA / PID / DCS / UDL / UD (the parsed fields are logged and reported;
  an applet's own SMS, not the OTA PoR the por check decodes).
- scp80.params.format wraps the C-APDU in the TS 102 226 5.2.1 Command
  Scripting template (expanded = AA..., expanded-ae = AE 80...00 00); the
  log shows WRAPPED=, the report C-APDU=<effective> format=<fmt>.
- docs: Worksheet 1 addresses an RFM TAR (B00000/B00001/B00010 compact,
  B00120/B00140/B00130 expanded) with the own-TAR and MSL/spi1 notes, and
  file-write is documented as a direct APDU that does not fire
  EVENT_REMOTE_FILE_UPDATE.

Tests: 819 frontend + 724 Python; sw cache simple-v387.
2026-10-04 20:57:38 +03:00
catarrh c444468d2a v3.18.3: the test-run card-exchange log
Verifying a script from the log was impossible: the runner wrote no card
lines, SCP80's plaintext C-APDU / secured packet / PoR landed only in the
report, and a menu selection logged nothing (the interactive path showed just
"STATUS poll (menu-select 9000)" / "STATUS -> 9000").

Every step now writes a `TEST-RUN step N:` line family to stderr, request
before response:

- generic actions: the APDU/raw event data sent and the SW/RESP received
  (`APDU TX=...`, `ENVELOPE(Event Download) ... data=...`, `READ/UPDATE ... -> SW=...`);
- SCP80: `C-APDU=` (or `SP=`), `SECURED=<hex> (N B) TAR=... SPI1=... SPI2=... CNTR=...`,
  the response SW, and the PoR - `PoR[inline] status=... TAR=... CNTR=... raw=...`
  plus `R-APDU SW=... data=...` (with the bad-format note);
- expectations: `FETCH=<apdu> -> SW=... <raw>`, `CMD 0x.. <NAME> qual=..`,
  `TR=<apdu> -> SW=...`, and `PoR[sms-submit] ...` + `R-APDU` for the SEND
  SHORT MESSAGE transport;
- status: `STATUS 1/N -> <sw>` per attempt;
- menu selection: `MENU-SELECT ENVELOPE=<apdu> item=<id> ('<text>') -> SW=...`
  (both the runner action and /api/menu-select; the old STATUS lines stay as
  context).

The shared secured-packet sender logs each segment's answer
(`OTA SEND: ENVELOPE i/N -> <SW>`), the scp80 step's report `sent` now carries
the C-APDU/SP hex, and `--apdu-trace` remains the raw transport view.

help EN/RU and docs/test-scripts.md gained the "reading the run log" note.
Tests: 816 frontend + 718 Python; sw cache simple-v385.
2026-10-04 19:50:55 +03:00
catarrh 9109b68924 docs: help/README catch up with the UI (pill count, event preview, store saving)
The audit of the help and READMEs against the current UI found four stale
pill enumerations and five undocumented behaviours:

- The Simulator tab has had five pills since the BIP pill landed; the help
  (EN/RU) and both READMEs said "four" and omitted BIP.  All four now name it
  (the help links §8.10).
- Test-script help: the envelope action's source override (82/83), the event
  action's fields being a JSON object in the step editor, edits being written
  to the store as you change them (no Save button; an incomplete draft stays
  local), and the templates wording adjusted.
- Subscribed-events help: the form's "event data (hex)" preview and how to
  copy it into a raw envelope test step.
- SCP80 help: the counter probe's amber outcome (a security verdict after a
  counter rejection saves the value and warns).
- Cards help: the key table's "Counter" row aligns with the editor's
  "Counter (hex)" heading.

sw cache simple-v384 (help files are precached).
2026-10-04 19:01:26 +03:00
catarrh fb4e4bd7b2 v3.18.0: the semantic event action for test scripts
The other-applet batch: a test script can send the common event downloads
semantically instead of raw hex.

- events.py: server-side mirrors of the PWA's event forms for Location status
  (0x03), Access technology change (0x0B), Network rejection (0x12 - the
  LA/RA/TA object selected by the registration group, the extended EMM cause
  and the extended information object) and Data connection status change
  (0x1D - status/type/TI plus the optional cause, host-clock 8.39 date-time,
  location information, access technology, APN and PDP/PDN/PDU type).  Fields
  are validated before the run (a bad value is a script error, not a step
  failure); `src` overrides the device-identities source (default terminal,
  matching the Phone forms).
- The engine gains the `event` action kind; the runner builds and sends it.
  The PWA's action-kind list stays in sync (drift-guarded - the guard itself
  caught its own regex bug on the first run).
- Byte parity: the vectors are generated from the PWA's own builders and
  pinned in tests/test_events.py; the frontend suites assert the same values.
  PLMN and SCTS helpers are mirrored too.
- The Phone tab's event form shows the built "Event data (hex)" (updated as
  the fields change), so every event without a builder can be used through
  the raw `envelope` action.
- The test-step editor offers the event select + a JSON fields textarea;
  malformed JSON refuses the save with a clear message.
- docs/test-scripts.md (the semantic-event table, an event example, the
  format stamp), docs/api.md and help EN/RU updated.

Tests: 812 frontend + 717 Python; sw cache simple-v381.
2026-10-04 18:00:53 +03:00
catarrh 0b9e9e4c5e v3.17.0: test scripts for the applet handoff - menu by text, spec-order TRs, codings
The applet-testing batch agreed after v3.15/3.16:

- menu-select by text: the step names the item by `text` (+ `mode`
  exact/contains, `case_sensitive`) instead of the install-dependent id
  (both or neither is refused); the runner resolves it against the card's
  cached menu and raises a step error naming the available items when nothing
  (or several items) matches.  The cache is refreshed on every SET UP MENU
  fetch - the proactive chain and the expect step - so an applet install (new
  items/ids after its REFRESH) no longer leaves a stale menu behind.  The
  step editor offers the id or the text + mode and the client-side pre-check
  bounds them.
- TERMINAL RESPONSE object order: both builders emitted the Item identifier
  before the Result (and the scripted builder its Text string before it too);
  TS 102 223 6.8.0 mandates Command details, Device identities, Result,
  Duration, Text string, Item identifier.  `testscript.build_tr` and
  `_menu_send_response` now follow it; `respond.raw` is documented as
  appended after the standard objects, and the exact-byte tests pin the order.
- Text codings: the scripted response codes the GSM default alphabet via the
  GSM 03.38 codec ('00' packed, '04' 8-bit) instead of latin-1, keeps '08'
  UCS2 and sends the spec's null text string for an empty answer; the packing
  helper is shared with the interactive path (`testscript.pack_gsm7`).  A
  UCS2 GET INPUT answer is documented and tested.
- docs/test-scripts.md gained the handoff brief for the applet agent: the
  value-collection table, fill-in worksheets for the two applet scenarios, a
  menu-by-text example, the validation checklist and a format stamp; help
  EN/RU and docs/api.md updated.

Tests: 809 frontend + 705 Python; sw cache simple-v380.
2026-10-04 17:48:29 +03:00
catarrh 8430dd1694 v3.16.0: test scripts move to a server-side store
The scripts lived in the browser's localStorage although only the local
server can run them.  The new test_scripts.py store
(~/.pysim-simple-server/test_scripts.json, --test-scripts PATH) sits next to
the card presets: stable uuids and timestamps, atomic writes, and every
mutation is validated with the runner's own engine (the submitted shape is
kept - the normaliser's output is not re-normalisable: it fills defaults
like an 'any' PoR check that the check normaliser then rejects), so a stored
script cannot fail at run start for a validation reason.  Import accepts the
old localStorage export as-is (export/import remains the migration path; the
PWA import also takes an exported array).

- /api/test/scripts (GET card-free: path/count + scripts), POST create,
  /update, /delete and /import serve the store; the endpoints answer 503
  without a store and 500 on a write failure, like the preset endpoints.
- /api/test/run takes a stored script_id (resolved when no inline script is
  sent); the PWA sends the edited copy inline plus the id for the report,
  and the run snapshot carries script_id.
- The PWA's Test script pill loads from the store, writes through on edits
  (debounced name, immediate step/new/copy/delete), reports load/save/delete
  failures inline, and exports the portable {name, steps} shape.

Tests: 808 frontend + 701 Python; sw cache simple-v379.
2026-10-04 17:24:21 +03:00
catarrh abc9ea2751 v3.15.0: test-script fixes for applet testing - preset id, kvn, PoR and file-list checks
Three gaps blocked SCP80 applet testing from the PWA:

- testRunPreset() still marshalled the pre-v3.8 flat preset fields
  (kic/kid/keys/counter/tar/spi1/spi2, all empty on a stored preset), so
  every SCP80 run failed with "SCP80 preset is incomplete: no keyset is
  defined".  The PWA sends the matched preset's id now and the server
  resolves it from the store (_test_run_preset_from_body; the inline
  `preset` body stays for API callers), so a run always uses the stored
  keysets, TARs and counters.  Regression tests on both sides.
- testscript._normalise_params dropped the scp80 step's `kvn` (the editor
  exposed it and the runner and validation consumed it, so scripts
  silently used the preset's first keyset).  kvn is validated (1-15) and
  passed through; the client-side problem check bounds it too.
- The PoR delivered as a SEND SHORT MESSAGE (PoR-in-submit, SPI2 bit 0x20)
  is decoded for the expect step that fetches it: the new `por` content
  check asserts the response status / R-APDU SW / response data, decoded
  with the preceding scp80 step's context (concatenated SMS accumulate
  across expectations).  The inline transport stays on the action
  (check.por / check.data).
- The new `files` content check asserts the File List of a REFRESH
  (TS 102 223 8.18, tag 12/92) - e.g. qualifier 00 = NAA initialization
  with full file change notification - as an order-insensitive set of FID
  paths.

The step editor exposes both checks; help EN/RU, docs/api.md and the new
authoring reference docs/test-scripts.md document the format.  Tests: 803
frontend + 680 Python; sw cache simple-v378.
2026-10-04 17:09:06 +03:00
catarrh c18b6cd0d4 v3.14.0: event form object-set fixes + the slices single-envelope mandate
An audit of every event form against the pinned specs (31.111 7.5.x/9.3,
102 223 8.87, TS 101 220's master tag table) found three missing objects,
three malformed/wrong ones, and settled the slice-data scope.

- 0x00 MT call: the IMS URI object is cut at 100 bytes and now carries the
  URI truncated object (8.135, F3 00) when the cut happened.
- 0x1F Slices: the form gains the Partial NSSAI object (8.154, F9, rel 18);
  a single envelope carrying it is marked as the Last Envelope (8.79,
  F0 00) automatically (7.5.27.2).
- 0x06 Card reader status and 0x07 Language: the builders now emit their
  object tags (A0 01 ..., AD ...) - they emitted bare values before.
- 0x18 Incoming IMS data: the IARI tag is F6 (76/F6), not F7 (which is the
  IMPU list tag - the two-column 9.3 table had been misread).
- Single-envelope mandate: the event data is capped at 244 bytes (inner 252;
  the D6 long-form length and the APDU Lc stay in one byte).  The server
  refuses more with a 400 and the PWA shows the size error before sending.
  Chained (multi-envelope) delivery, the More Data object and envelope
  sequences are not implemented - documented in the help and the repo AGENTS.
- Tests: event form vectors (URI truncation, the restored tags, partial /
  Last Envelope, the size helper) and the server guard; the release
  reference and its PWA/code mirrors gain the three objects and the
  corrected IARI key.
- sw cache simple-v375.
2026-10-02 14:34:47 +03:00
catarrh cc480c30b7 v3.13.3: ELF version retry for both P1 values + query-based per-AID budget
elfTagListRetry walks the untried tag-list forms for P1=10 and P1=20
(skipping combinations the main pass already refused via the expandedTried
map; only the tag-list forms are retried - the no-tag-list form never
carries CE - and the walk stops as soon as a version appears).  The form
memory can skip the tag-list forms for P1=10 when the Apps listing served
only the no-tag-list form (live 2026-10-02: EP BAP - its P1=10 tag lists
were never queried while the P1=20 retry refused them).

elfPerAidVersions filters to packages that actually have an applet
candidate before the six-query cap: the budget counts queries, not list
positions (EP BAP's first six packages are Oracle/JC-API entries with no
app instances - they consumed the cap and the A113 packages were never
queried).

Docs: help EN/RU, repo AGENTS (v3.13.3 note).  sw cache simple-v373.
Tests: 667 Python + 796 frontend green.
2026-10-02 13:30:03 +03:00
catarrh 2c49c54d2d v3.13.2: ELF versions derived through the package's module AIDs
ramDeriveElfVersions matches an applet instance to its package through a
shared ramElfAppMatch: the app's C4 (Load File AID) when the card reports
the real one, then the package's module AIDs (the applet instance AIDs),
then the shared-prefix fallback (the same tiers ramElfAppletCandidate uses
for the per-AID queries).  Live 2026-10-02 (test-eSIM): the card returns
C4 = the applet's own AID (c4 10 ...3908 for package ...3900), so the old
C4-only derivation filled nothing; the module lists map ...3900 -> ...3908
(9.3), ...0200 -> ...0208/...494D4508 (1.1) and F0414C46416001 ->
F0414C4641600101 (0.1).

The ELF version hint names the remaining case: a package without an
applet instance has no version to derive either (the Oracle
A00000006203010101 stays bare - the card refuses every ELF tag-list form,
so its CE is unreachable).

Docs: help EN/RU, repo AGENTS (v3.13.2 note).  sw cache simple-v372.
Tests: 667 Python + 796 frontend green.
2026-10-02 13:16:08 +03:00
catarrh 5b8324ed02 v3.13.1: expanded-registry pages try every command format
The explorer's next-occurrence page (P2=03) is retried in the other
command formats (ramPageFormats: the expanded codings first - the serving
one first when it is expanded - and the compact command string last,
because a compact page attempt can consume the page without delivering
it) and the format that serves a page is kept for the following ones.
Live 2026-10-02 (test-eSIM card): a compact page 2 answered 61B6 and the
chained GET RESPONSE 6A86, after which the AA and AE 80 retries both saw
the occurrence already gone; the AA-wrapped page returns 182 B with the
remaining app instances' CE/CC/EA, while AE 80 is refused.  A page that
no format serves ends the listing.

Only an explicit 6310 promise counts for the "(first page only)" flag
(ramExpandedMore): a probe after a 61xx page that every format refuses is
the card's final answer, so that card's ELF listing is no longer flagged
(its expanded registry has three entries; the fourth package exists only
in the compact listing - the ELF tag-list forms and per-AID queries are
refused in every format).

In Auto the explorer also queries entries the bulk listing left without
expanded fields individually by AID (ramAppsMissingDetails, <= 6 packets,
the compact command string carries the GET RESPONSE for 61xx).

Docs: help EN/RU, repo AGENTS (v3.13.1 note).  sw cache simple-v371.
Tests: 667 Python + 794 frontend green.
2026-10-02 12:47:02 +03:00
catarrh 4f0b2a743c v3.13.0: the Simulator's event forms honour the preset's CAT release
The consumption half of the release guard: the event-download forms hide the
parameters introduced after the effective release and say so.

- catEffectiveReleaseInfo(cards, iccid): the equipped card's preset (ICCID
  match, the same rule the SCP80 views use) resolved through presetRelease(),
  else the default Rel-12.
- eventFieldsForRelease(cfg, rel): a field whose introduction release (`rel`)
  is above the effective one is hidden; a field without a known release is
  never hidden.  The EVENT_FORMS annotations verified against the pinned
  specs/histories: the MT call IMS URI (Rel-12, CR 12.4.0 "URI support in MT
  Call event"), the CSG cell selection PLMN ID (Rel-12, 12.1.0), the IMS
  registration IMPU list + status code (Rel-11), the slices S-NSSAI-mapping
  and rejected objects (Rel-18).
- The form notes what was hidden: "hidden parameters: N · introduced after
  Rel-12 · preset: X" (or "default release"), with a tooltip naming the
  source; the send path already skips absent fields, so no builder changes.
- help EN/RU; sw cache simple-v369.
2026-10-02 11:45:21 +03:00
catarrh 3660a7e173 v3.12.0: the card preset carries the card's CAT (USAT) release
The release selector lives in the preset rather than a global chip: presets
already travel with the card (ICCID auto-selection, export/import), and the
operations that will consume the release are preset-aware.

- presets.py: the optional `release` field (4-18; empty = unspecified) joins
  PRESET_FIELDS; a value outside 4-18 is refused on save/import; the store
  schema version is 5.
- catrelease.py: DEFAULT_RELEASE = 12 and preset_release() - the effective
  release is the preset's own value, else Rel-12 (the safe level for cards
  whose release is not known; an unset release never filters anything away).
  The PWA mirrors both (CAT_RELEASE_DEFAULT + presetRelease()), drift-guarded.
- Cards tab: the editor's header row gains a compact CAT release select
  (unspecified / pre-Rel-6 stored as 5 / Rel-6 ... Rel-18); the empty option
  names the Rel-12 default and the tooltip explains the guard; the field is
  collected, cleared and restored with the other form fields.
- Nothing filters yet: the Simulator's event parameters (stage 3) will use
  the effective release.
- docs/api.md + help EN/RU updated; sw cache simple-v368.
2026-10-02 11:28:33 +03:00
catarrh 5a3c4c3529 v3.11.0 review fixes: ADM length, TAR re-render hygiene
- the ADM input capped the key at 8 chars, but the server's _verify_adm
  takes 4-16 hex digits (padded to 8 CHV bytes) and the old input had no
  limit; maxlength=16 now, with a static guard in html.test.js.
- cardsTarAdd/cardsTarRemove re-rendered the table from the save-path
  reader, so an emptied role field was silently refilled with its default.
  The renderer no longer invents defaults; a raw reader (cardsTarsFromTable)
  feeds Add/Remove and the language-switch refresh, while the save path
  (cardsTarsFromForm) keeps the default-filling for the mandatory roles.
- the editor's dynamic TAR/keyset rows now rebuild on a language switch
  (refreshDynamicI18n) from what is typed - their placeholders and titles
  carry t() strings.  (The title/Save label were already refreshed there.)
- dropped the now-unused cardsRoleMsl helper and its test references.
- docs: the ADM is documented as 4-16 hex digits (help EN/RU, api.md,
  AGENTS).  sw cache simple-v362.

Tests: 665 Python + 784 frontend green; the whole inline PWA script
syntax-checked.
2026-10-02 10:27:42 +03:00
catarrh 65c565e058 v3.11.0: Cards tab redesign (list + editor), preset PIN/PUK codes
The Cards tab now shows the preset list by default; "Add preset" / "Edit"
open a separate editor view (Save stores and returns to the list, Cancel
discards; ramGoToPreset opens it too).  The editor keeps the name/ICCID
header (with From card) and groups its fields into bordered blocks:
- PIN/ADM: the new optional card codes PIN1/PUK1/PIN2/PUK2 (plain text,
  4-8 decimal digits) plus ADM, moved out of the top row; nothing consumes
  the PIN/PUK codes yet.
- TAR: one uniform table (TAR, MSL, description); the first three rows are
  the fixed role entries with the immutable English descriptions "ISD
  compact format" / "UICC shared FS RFM compact format" / "ADF FS RFM
  compact format"; further rows are deletable with an optional description.
- SCP80 (keyset editor) and SCP81 (PSK pair) as before.

Store: PRESET_FIELDS gains pin1/puk1/pin2/puk2 (optional; whitespace
stripped; 4-8 decimal digits validated on save and import), SCHEMA_VERSION
4, docs/help/AGENTS updated.  TAR rows use unified ids (cards-tr-N-*), the
editor switches via cardsShowList/cardsShowEditor.

Tests: 665 Python + 783 frontend green; the whole inline PWA script
syntax-checked.  sw cache simple-v361.
2026-10-02 09:32:45 +03:00
catarrh 6360dc5bae v3.10.0: preset TAR table with per-TAR MSL; derive the SPI1 from the TAR
Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar
fields: a preset now carries `tars` - the three mandatory role entries
(isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an
optional description, each with its MSL (Minimum SPI1, TS 102 226
8.2.1.3.2.4).  v3.9.1 presets convert on load/import (the spi1 seeds all
three MSLs); duplicate TARs and empty TAR/MSL values are refused; the
store schema version is 3.

Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the
RAM ops, the test runner (per-step overrides still win), the counter probe
and each TAR-probe row; the plain send keeps an explicit SPI1 with a
`warning` when it is below the MSL, and a TAR outside the preset without an
explicit SPI1 is refused with a hint.  The TAR probe resolves per checked
TAR before sending and reports the effective spi1 per row.  The secured
packet form's SPI2 default is 00 (the RAM/probe operations pin their own).

PWA: the Cards form edits the TAR table (role rows with MSL inputs plus
Add/Remove free rows with a description), the list shows a TARs (MSL)
column, the SCP80 TAR field suggests the preset's entries (datalist) and a
preset apply seeds the SPI1 from the operation TAR's MSL with an amber
warning below it; the preset-wide SPI1/SPI2 fields and the header marker's
SPI requirement are gone (completeness = a keyset + the three role entries
with their MSLs).  Help EN/RU, docs/api.md and AGENTS updated; sw cache
simple-v358.

Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter
advanced); a send without spi1 derived 16 (por_ok, counter advanced); a
send with spi1 0A returned the warning and the card refused it (6200, no
counter consumed); an unknown TAR without spi1 was refused with the hint.
2026-10-02 08:28:46 +03:00
catarrh 00181ba3ad Infer the card's ETSI release from its installed uicc.toolkit version
The explorer's card-resources block now names the card's ETSI release, using
the same mapping as the CAP analysis (capmem.ETSI_TOOLKIT_RELEASE: uicc.toolkit
version -> REL-N, basis in brackets; the live card's 1.12 -> REL-15/16/17).
The table is mirrored in the PWA (drift-guarded by tests/test_cap_memory.py,
like JC_FRAMEWORK_SDK); the version comes from the expanded registry's CE tag
and degrades to the version hint when the listing has none; SIM-only cards (no
uicc.toolkit) get no label, the same rule as SIM-only CAPs.

- cardEtsiRelease(elfs) (pure) + UICC_TOOLKIT_AID / ETSI_TOOLKIT_RELEASE in the
  PWA; rendered after the free-memory lines with a tooltip naming the basis.
- Tests: helper vectors (1.12/1.11/1.5, unknown version, versionless,
  SIM-only), the rendering guard, and the table/AID drift guard.
- Help EN/RU; sw cache simple-v354.
2026-10-02 00:48:45 +03:00
catarrh 6e4eb5932c Expanded listings follow the command format; registry gets a format fallback
The TCA loader's Explore traces settle the format question: it sends the same
P2=00 listing queries wrapped and unchained (`AA0A2208 80F24000024F0000`,
pages `...01024F0000`) - no GET RESPONSE anywhere - while the live card needs
the chain and answers 6700 to the wrapped unchained form; and the live ISD
refuses the compact P2=02 registry query (6700) while serving it wrapped
(`AA11220F 80F2400209 4F005C054F9F70C5CC00` -> 6310 + entries with CE
versions).  Both the listing and the registry queries now follow the
operation's command format and fall back across formats.

- ramFormatIsExpanded(fmt) + ramFmtLadder(runFmt, working) (pure): the run's
  format first, then expanded, expanded-ae and compact as the last resort; a
  format that served data is tried first and never repeated.
- ramGetStatusApdu(p1, p2, fmt): the chain exists in compact only (TS 102 226
  5.2.1.1) - an expanded run sends the bare query.
- paginate: the P2=00 listing follows the run's format and pins it per send,
  so a GET RESPONSE can never ride inside an expanded packet.
- expandedPass: each registry form goes through the format ladder; the chained
  61xx retry is compact-only (it was guarded for 'expanded' but not
  'expanded-ae'); the serving format is remembered for the remaining listings
  and named in the report ([AA]/[AE 80]); ramExpandedGroups carries it.
- Tests: the helper + ladder vectors, the unchained expanded listing builder
  (both codings), the fallback/chain source guards, the report marker.
- Help EN/RU, the repo AGENTS.md; sw cache simple-v353.
2026-10-02 00:40:09 +03:00
catarrh 391d4a322d Low-counter reporting, counter sync probe, Bad-format decode, expanded forms
Live findings that drove this: the "card counter 0000000000" the low-counter
notice showed is not the card's counter at all (the PoR's CNTR is a copy of
the command's counter, TS 102 225 5.2 table 3, and a rejection leaves it
zeroed); and an earlier "no card accepts the expanded format" observation was
a hand-made template whose declared length did not match its body - the card
validates the lengths and answers with a Bad format TLV.

- _cntr_low_fields(status) reports the verdict as a flag; card_cntr/
  suggested_cntr and the UI's "(card counter ...)" text are gone - the value
  is not observable.  The notice offers a bounded Sync counter probe instead.
- POST /api/counter-probe: a doubling counter ladder (start+1, +2, +4, ...)
  with a read-only command until the card accepts one, then accepted+1 is
  persisted (monotonic, source counter-probe).  Rejected attempts change
  nothing on the card; the search stops at a ceiling (default FFFFFFFF, far
  below the 40-bit maximum where the counter blocks) and after max_attempts
  (default 40, clamped 1-64); a non-cntr_low failure stops it at once.
- _parse_response_scripting decodes the Bad format TLV (90, TS 101 220 table
  7.20; error types per TS 102 226 table 5.12) and returns a dict; the PoR
  carries bad_format/bad_format_name and _ram_step_result fails the step with
  "bad format 02 (wrong length)" instead of falling through to the compact
  decoder (which read the AB tag as 171 commands).
- _wrap_expanded_apdu(apdu, form) emits the definite (AA) or the indefinite
  (AE 80 ... 00 00) scripting template; the RAM command formats are compact/
  expanded/expanded-ae and _ram_detect_format probes compact, then both
  expanded codings (the expanded probes use Le='00' and no chained GET
  RESPONSE, TS 102 226 5.2.1.1).  The RAM form gains a Format selector.
- Live-verified: the verdict carries no value; the probe walked
  0x1000001/0x1000003 (low) then 0x1000007 (accepted, stored 0x1000008); the
  ISD executes both expanded codings (R-APDU 08A0000000030000000F80, SW 9000)
  and a length-mismatched template answers bad_format 02 - decoded now.
- Tests: the ladder, the probe walk + persistence, params validation, both
  template forms, the Bad-format TLV (definite and plain tag styles), the
  live ISD response through _decode_por, the step-result failure, and the PWA
  guards; help EN/RU, docs/api.md, the repo AGENTS.md; sw cache simple-v352.
2026-10-02 00:17:10 +03:00
catarrh 69abbe2fa5 feat: the conditional event objects are complete
The six forms that still missed conditional/optional objects now carry them:

- Location status (0x03): Location Information (MCC/MNC + LAC + Cell ID) -
  included only for Normal service, as TS 102 223 8.19 requires.
- Channel status (0x0A): Bearer Description (B5, type + parameters) and
  Other address (BE, IPv4 dotted / IPv6 hex / the empty "dynamic address"
  form); new eventIpBytes() helper.
- Call disconnected (0x02): IMS call disconnection cause (D5, SIP/Q.850 +
  2-byte cause).
- Network rejection (0x12): Extended information (F2, CAG ID / NID / RedCap).
- CAG cell selection (0x1E): CAG Human-readable network name list (D7,
  80-tagged names).
- Slices status change (0x1F): Allowed slices with S-NSSAI mapping (F7),
  Allowed slices (F8), Rejected slices with mapping (D7) and Rejected slices
  (B1) beside the served list.

Every implemented event form now carries all the objects of its spec table.
Tests: the new build vectors.  Help EN/RU and both AGENTS files; sw
simple-v350.
2026-10-01 22:57:01 +03:00
catarrh 7061f42781 feat: the remaining spec-defined event forms (data/display/search/frames, IMS/CAG/slices)
Group 1 (TS 102 223): Data available (Channel status B8 + channel data length
B7), Display parameters changed (rows/columns + sizing/fonts/effects, C0),
Network search mode change (manual/automatic, E5), Browsing status (browser
error code, E4) and Frames information change (default frame + per-frame info,
E7).
Group 2 (TS 31.111): IMS registration (IMPU list as 80-tagged URIs + IMS status
code), Incoming IMS data (IARI), CAG cell selection (status + mechanism, access
technology, CAG information list) and Slices status change (allowed / rejected
/ served / partially, access technology, served S-NSSAIs) - all with source
network.

Still unimplemented by design: 0x0D Local connection, 0x16 Contactless state
request (3GPP: not required) and the eCAT containers 0x19/0x1B; 0x04/0x05/0x13/
0x1A are data-free, where the one-click send is spec-correct.

Tests: the new build vectors.  Help EN/RU, both AGENTS files; sw simple-v349.
2026-10-01 22:30:19 +03:00
catarrh a26a2b6648 feat: the remaining six event types get their parameter forms
MT call, Call connected, Call disconnected, Browser termination, (I-)WLAN
access status and CSG cell selection no longer fall back to the generic
"send a bare notification" prompt:

- MT call (TS 31.111 7.5.1A.2): transaction identifier, calling party number
  + TON -> Address (EFADN BCD coding, encBcdDigits/eventAddressTlv),
  subaddress, IMS URI, media type; source network.
- Call connected (7.5.2A.2) / Call disconnected (7.5.3A.2): transaction
  identifier, near/far-end source select, media type, and a cause - absent,
  the zero-length radio-link-timeout form or custom bytes (the hex field shows
  only for "custom").
- Browser termination (TS 102 223 7.5.9.2): user / error cause.
- (I-)WLAN access status (TS 31.111 7.5.1.2): no coverage / coverage /
  connection on-going.
- CSG cell selection (TS 31.111 7.5.3.2): status + selection mechanism,
  optional access technology, CSG ID, HNB name and PLMN ID; source network.

The device-identities source (TS 102 223 8.7: 82 terminal / 83 network) is now
selectable: EVENT_FORMS carries a fixed `src` or a `srcField`, and
_send_event_download()/`/api/event-send` take an `event_src` override (an
invalid value falls back to the terminal).

Tests: the BCD/Address builders, each event's build vectors, the "no not_yet
left" guard and the server source override.  Help EN/RU, both AGENTS files;
sw cache simple-v348.
2026-10-01 22:19:55 +03:00
catarrh 994ee6d2a3 ui: the Data Connection event gains the date-time object; the "more" flag is precise
- The 0x1D event form now offers the Date-Time and Time zone object
  (TS 102 223 8.39): an optional select that includes the host clock's
  date-time, built by eventDateTimeTlv() as seven semi-octet BCD bytes in the
  TS 23.040 9.2.3.11 order (units high, tens low) with the time zone in
  quarters of an hour and its sign in bit 3 of the first semi-octet.
- The Cards list's empty-state colspan is 8 now (the table has had 8 columns
  since the keyset refactor, not 10).
- The expanded-registry "(first page only)" note marks only an unfulfilled
  6310: ramExpandedMore() keeps the promise set when the follow-up page was
  refused/failed or the page cap stopped the paging, and clears it when an
  accepted page ended the listing (a completed multi-page listing was
  annotated wrongly before).

Tests: eventDateTimeTlv (fixed dates, both timezone signs), the 0x1D build
with and without the date-time, and ramExpandedMore's cases.  Help EN/RU and
both AGENTS files; sw cache simple-v347.
2026-10-01 22:07:24 +03:00
catarrh 0cccd62230 release: v3.9.0 - the operation start buttons wait for a preset and a keyset
Gating: the SCP80 "Send to Card", the RAM Execute and the TAR-probe Execute
buttons carry `data-needs-keyset`; pysimApplyAvailability() disables them
(with the hint "Select a card preset and its keyset" when the card itself is
fine) unless the view's preset is selected and the form's keyset number is one
of its keysets - `spKeysetStartReady`, the same rule the click-time guards
enforce, which stay as the last line of defence.  spKeysetSync() refreshes the
gate on every preset/keyset change, and the KIc/KID index selects now call
spKeysetSync() so the keyset selector follows a manual index change too.

Version 3.9.0: server VERSION, pyproject.toml and the PWA header; sw cache
simple-v346; the workspace AGENTS gains its v3.9.0 history entry.

Tests: indicator.test.js drives the gate (no preset -> disabled with the hint,
an undefined keyset -> disabled, a matching keyset -> enabled) and guards the
three buttons' attribute; the cards/keysets/ram sandboxes stub
pysimApplyAvailability.  Help EN/RU notes the disabled start buttons.
2026-10-01 22:03:36 +03:00
catarrh 39723406b4 ui: the preset list highlights the equipped card's preset
The Cards list now marks the row whose ICCID matches the equipped card
(cardsFindByIccid handles digits / spaced / raw EF hex alike): an emerald row
tint plus a small dot with a "The equipped card's preset" tooltip in the Name
cell.  There is no highlight while the ICCID is unknown (no card, unreadable
EF.ICCID) or when no preset matches - the amber banner already covers that.

pysimCardStateUpdate() re-renders the list when the ICCID actually changes, so
a visible Cards tab follows equip/removal live (opening the tab re-renders
anyway through switchTab).

Tests: cards_iccid.test.js drives cardsRender with a fake tbody - the matching
preset is highlighted (both ICCID forms), the others stay plain and no ICCID
means no highlight; card_state.test.js stubs cardsRender.  Help EN/RU and the
repo AGENTS updated; sw cache simple-v344.
2026-10-01 21:50:29 +03:00
catarrh 12da7ed593 ui: the keyset selector is disabled and cleared while no preset is selected
A keyset only exists inside a card preset, but the SCP80/RAM/TAR keyset
selectors stayed enabled with no preset selected - showing either
"— no keyset —" or a stale "Keyset N — not defined in the preset" entry left
over from an earlier preset.  spKeysetSync() now disables the selector and
reduces it to a "— select a card preset —" placeholder whenever its view has
no preset selected, and re-enables/populates it as soon as one is.

Clearing the preset dropdown ("— Select card —") also refreshed nothing:
cardsApply() returned before the sync, so the stale keyset list stayed; it now
syncs the selectors (and the preset warning) on the empty selection too.

The three selects get disabled:opacity-40 disabled:cursor-not-allowed (the
rules are already in the compiled style.css).  Tests: keysets.test.js covers
the disabled/cleared state and the re-enable plus a cardsApply source guard;
cards_form.test.js and cards_counter.test.js gain tarPresetIdx in their
sandboxes.  Help EN/RU and both AGENTS files; sw cache simple-v343.
2026-10-01 21:41:07 +03:00
catarrh cafec549fa feat: the Data Connection Status Change event gets its parameter form (0x1D)
EVENT_FORMS[0x1D] was missing, so its Send button posted event_data: null -
the ENVELOPE carried only the event list and device identities, none of the
event's data objects.  The form now builds the TS 31.111 7.5.25.2 object set
(tags per TS 101 220 / TS 31.111 9.3): data connection status (9D), connection
type (AA), optional (E/5G)SM cause (AE), transaction identifier (1C), optional
location information (13, CR flag 0 as the spec requires for this command),
optional access technology (BF), location status (9B), optional APN (C7) and
PDP/PDN/PDU type (0B).

Adjacent spec fixes found while verifying:
- the Access Technology Change options (0x0B) did not match the pinned
  TS 102 223 V18.3.0 8.61 coding (GSM Compact / GSM w-EGPRS / UTRAN w-HSDPA
  ...); they now use the shared ACCESSTECH list, whose two labels were also
  corrected (TIA/EIA-553-A, TIA/EIA-136-270);
- _send_event_download() wrote the D6 length as a single byte - a long APN
  would overflow it; 128..255 bytes now use the BER long form (81 <len>).

Tests: the 0x1D build (defaults, full form, fallbacks), the 8.61 option list,
the long-form D6.  Help EN/RU and both AGENTS files; sw cache simple-v342.
2026-10-01 21:35:23 +03:00
catarrh 0114c4748b ui: the STK menu button shows the title without the "STK: " prefix
The Simulator view's "STK menu" block already carries the legend, so the
emerald button's hardcoded "STK: " prefix was redundant.  It now shows just
the card's menu title (e.g. "Alfa Mobile"), or "Menu"/"Меню" for a
title-less SET UP MENU (`stkMenuLabel`, unchanged).

The help pages (EN/RU) describe the button as the menu title, both AGENTS
files drop the old `STK: Menu` wording, and stk_menu.test.js gains a source
guard against the prefix returning.  sw cache simple-v341.
Tests: 743 Node / 619 Python.
2026-10-01 21:23:06 +03:00
catarrh 9f7876b5fa docs: help/README catch up with v3.8.0; keyset-number wording; RU "пресет"
Documentation:
- help (§1/§3): the SCP80 tab is switched by three pills - the tab list and the
  section intro now name the TAR probe (RU "TAR-сканер"); §3.1/§3.2 describe the
  Keyset selector next to the preset selector (and that a successful PoR
  advances the counter only when SPI1 carries a counter check); §4 describes
  the keyset editor rows, the derived number and the per-keyset counters; the
  probe section keeps its counter rule.
- README/README_RUS: the SCP80 tab says three pills and gains a "TAR probe" /
  "TAR-сканер" section (standard list, verdicts, counter persistence); the
  Cards section is rewritten for the server-side store - keysets, server-owned
  per-keyset counters with the audit trail, JSON export/import as the migration
  path, the keyset editor, the equipped-card mismatch banner - and the field
  table, "Add a card" and "Edit / remove" follow.

Terminology:
- "key version" is no longer used for SCP80 keysets in the UI, the server error
  strings and the docs: the selector is "Keyset", its options "Keyset 1 - ...",
  the messages say "keyset N ...".  The TS 102 225 A.1/A.2 quotes and the GP
  PUT KEY P1 field keep the spec's own wording.
- RU keeps the word "keyset" untranslated ('Добавить keyset', 'Номер keyset',
  "не является keyset пресета", ...) and uses "пресет" instead of
  "предустановка" throughout the dictionary, the help and README_RUS.

sw cache simple-v340.  Tests: 742 Node / 619 Python.
2026-09-30 23:48:32 +03:00
catarrh 18e705a8c3 feat: TAR probe becomes its own SCP80 pill with a TAR checklist (v3.8.0)
The probe is not a RAM operation, so it moves out of the RAM view into a third
SCP80 pill (Secured packet | RAM | TAR probe, RU "TAR-сканер"), with its own
preset and key-version selectors, its own equipped-card mismatch banner and its
own result line; the help gains a "3.3 TAR probe" section and the anchor map
follows the pill.

The TAR field is a checklist now: pre-populated with the standard allocations
of TS 101 220 V18.3.0 Annex D, Table D.1 (each row carrying the document it
comes from, all checked by default, Select all / Select none), plus custom
TARs added through a small form (three hex bytes + an optional description,
removed with a x).  Custom entries live in localStorage['simple_tar_probe'] on
a best-effort basis - losing them is harmless, the standard list is always
there.  Only the checked TARs are sent; nothing checked is refused with a
message.  A Python test guards the PWA's list against the server's
TAR_PROBE_TARS (the endpoint keeps its own default for API callers).

RAM leftovers removed (the tar-probe operation entry, the #ram-tar-rows block,
the ramExecute dispatch, ramTarProbe); getRamSpParams() takes the view's
selector id; tarRender/tarApplyCard/tarKeysetChanged/tarKeysetGuard mirror the
RAM view; spKeysetSync and spPresetWarningRender cover the new selectors.
sw cache simple-v338.  Tests: 740 Node (the checklist rows, the custom
round-trip, the checked-list read, the renderer, the pill wiring) and 619
Python (the list drift guard).
2026-09-30 22:36:03 +03:00
catarrh 6eeb8dacb3 feat: TAR probe + the SPI1 counter-tracking rule; keyless packets build again (v3.8.0)
Three things learned from probing a live card, plus the capability they enabled:

1. Counter tracking follows SPI1.b5b4 (TS 102 225 5.1.1): with 00 the counter
   is "present, ignored, never updated", so a packet without a counter check
   must leave every counter untouched.  _counter_tracked() now guards
   /api/send-ota (no final_cntr, no persistence, a log line), the RAM step
   sender and the test runner; _preset_counter_persist() requires a non-zero
   key version (the "single keyset" fallback could advance a keyset spuriously
   for a keyless send).  The PWA advances the form counter only when the server
   reports final_cntr and says "counter not tracked" otherwise.

2. The spec's no-security form builds again: _ota_keyset() refused the valid
   KIc/KID '00' (TS 102 225 A.2: '00' is legal with SPI1.b3=0 for KIc and
   SPI1.b2b1=00 for KID) because pySim wants an algorithm.  An unknown nibble
   is now refused only when the algorithm is actually used (ciphering / a
   ciphered PoR for KIc, RC/CC/DS / a MAC'd PoR for KID) and 'implicit' codes
   the nibble as 0 otherwise - POST /api/send-ota {apdu, spi1:"00", kic:"00",
   kid:"00"} now builds 0D00010000B0000000000000010000A40000, the exact packet
   the card accepted (a fake 15/15 was refused with 6200).  A no-security PoR
   is decodable again too, and a requested-but-missing PoR sets por_missing
   on the plain path as well.

3. TAR probe (POST /api/tar-probe, RAM op "Probe TARs"): one secured packet per
   TAR - the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1
   (TAR_PROBE_TARS), or a custom list - carrying a harmless C-APDU (SELECT MF)
   with the preset's keyset and SPI 16/01.  Each TAR is classified
   (registered / no_answer / no_por / refused) from the ENVELOPE verdict and
   the application's PoR, counters advance and persist normally, and the run is
   refused when SPI1 has no counter check.  Live on a card: 000000, B00000 and
   B00001 registered (compact-format applications, answering 6D00 / 6B00),
   the expanded-format TARs, USAT, multiplexing and CASD refused with 6200 -
   with the keyless form the same card accepted every TAR and answered no PoR.

Docs (api.md, help EN/RU, AGENTS), sw cache simple-v337.  Tests: 618 Python
(no-security build, the algorithm-only-when-used rule, the tracking rule, the
persist guard, the probe classification) and 736 Node (the probe renderer and
wiring, spCounterTracked).
2026-09-30 21:17:59 +03:00
catarrh 386958b261 feat: warn when the equipped card and the selected preset disagree (v3.8.0)
Autoselection normally keeps the SCP80/RAM preset in step with the card, so
the mismatch is easy to miss.  Both views now carry an amber banner above the
preset selector (the existing warning style - no new Tailwind classes):

- the equipped card matches no preset: "The equipped card <ICCID> does not
  match any card preset - the selected preset may belong to another card.
  Add it in the Cards tab (From card)"; with no presets at all the banner says
  so instead;
- a preset is selected but it is not the equipped card's: a softer line naming
  the matching preset ("check that its keys and counters belong to this card").

`spPresetWarning(iccid, presets, selectedIdx)` is pure (unit-tested with the
ICCID forms the store accepts: digits, spaced, raw EF hex), `spPresetWarningRender`
fills both banners and runs on every card-state poll, preset load, preset
selection and page init.  No card session or an unreadable ICCID shows nothing
(the card-iccid gating already explains that state); the banner warns only -
it never blocks a send (preparing a packet for another card stays possible).

Tests: frontend/tests/preset_warning.test.js (5), the card-state/preset
harnesses stub the DOM-bound renderer.  sw cache simple-v336.
2026-09-30 15:37:51 +03:00
catarrh cc2edb3d8a feat: one keyset per key version - indexed keys and counters (v3.8.0)
A card usually holds several OTA key sets, and the SCP80 counter is tracked
per key version on the card, so a preset now carries one keyset per
GlobalPlatform key version - the b8..b5 nibble of KIc/KID (TS 102 225
5.1.2/A.2), the version is implicit in the bytes.  The spec is explicit:
"a dedicated counter shall be associated to each key version" (Annex A.1) and
"the key version number indicated in the KIc and KID (bits b8..b5) fields
shall be identical when different from 0" - a mismatch is rejected by the card
with "Unidentified security error" (A.2).

Server (presets.py, schema 2):
- a preset carries `keysets: [{kic, kid, kicKey, kidKey, cntr}]`; validation
  refuses a KIc/KID version mismatch (A.2), a version outside 01-0F ('00'
  means "no security" and is a packet-level choice, not a keyset), duplicate
  versions and missing keys/counters; a v3.8.0 flat preset converts into one
  keyset on load and on import;
- `set_counter(..., kvn)` writes the counter of that keyset (monotonic per
  keyset), the audit line carries the kvn, and `find_keyset()` serves the
  lookups;
- the operations derive the key version from the request's KIc byte and
  persist into that keyset: /api/send-ota, the RAM install/install-app/compat
  steps (_ram_send_gp_apdu) and the test runner's SCP80 steps (which may name
  a `kvn` per step and keep one working counter per keyset);
- a non-zero key version the named preset does not define, or a KIc/KID
  mismatch, is refused before anything is built (_preset_keyset_check /
  _keyset_guard: 400, or a step error in the test runner).  Without a
  preset_id only the mismatch rule applies.

PWA:
- the Cards form's KIc/KID/counter/keys inputs become a keyset editor (one row
  per version: KIc, KID, both keys, that version's counter, Add/Remove; the
  version is shown derived from the bytes and a mismatch is flagged);
- the SCP80 and RAM views get a Key-version selector beside the preset
  selector, driving the KIc/KID bytes and loading that keyset's keys and
  counter; the packet builder refuses a version the preset does not define
  (spKeysetCheck/spKeysetGuard, pure and unit-tested);
- the counter syncs are per keyset (spCntrLocalSync/ramSaveCntr), the SCP80
  form's counter edit posts the keysets, and the test-run write-back syncs
  every counter the server reports (scp80_counters).

Docs/help/AGENTS updated; sw cache simple-v335.  Tests: 610 Python (store
keysets, endpoint refusals, the counter landing in the right keyset) and 726
Node (keysets.test.js plus the updated harnesses).
2026-09-30 15:15:21 +03:00
catarrh 795a1fea49 feat: card presets move to a server-side store; the server owns the SCP80 counter (v3.8.0)
The presets lived in the browser's localStorage and the PWA wrote the SCP80
counter back after every operation, so an increment could be lost when the
response never reached the tab, when two browser contexts (or the demo origin
plus a local one) held separate copies, or when the tab was closed mid-run.
The counter is card state and the server performs every card operation, so the
store now lives next to the server:

- pysim_simple_server/presets.py: one JSON file
  (~/.pysim-simple-server/card_presets.json, --card-presets overrides; Path.home()
  resolves the same way on Windows), atomic writes (temp + fsync + os.replace),
  an RLock, stable uuid ids, the ICCID normalisation the PWA uses (digits /
  spaced / raw EF hex), a duplicate-ICCID refusal and a monotonic set_counter;
  every counter change is appended to card_presets.json.audit.jsonl.
- API: GET /api/presets (card-free), POST /api/presets and
  /api/presets/update|delete|import; the import also takes the old localStorage
  export shape - that is the migration path, there is no automatic migration.
- Operations name a preset_id and the server persists the counter they
  consumed: /api/send-ota (the plain SCP80 path reports final_cntr now too),
  the RAM install/install-app/compat steps (_ram_send_gp_apdu) and the test
  runner's SCP80 steps (_preset_counter_persist).
- PWA: the Cards tab is a thin client of the store (load on connect, CRUD via
  the API, export/import), the counter write-backs became local display syncs
  (the SCP80 form's counter input stays a deliberate human edit) and the
  SCP80/RAM bodies carry preset_id; a new pysimFetchOk() surfaces the server's
  error bodies instead of silently using them.

Policy: card keys in logs and local files are not a risk for this workbench
tool (AGENTS.md) - the store is not encrypted and no redaction is required.

Tests: tests/test_presets.py (store: ICCID forms, counter monotonicity and
wrap, atomic writes, corrupt-file recovery, import), the preset endpoints in
tests/test_fetch_routes.py (incl. the counter persistence via /api/send-ota)
and the updated PWA tests.  sw cache simple-v334.
2026-09-30 13:59:47 +03:00
catarrh 162930eb96 feat: low-counter guard for every SCP80/RAM operation (v3.6.58)
A cntr_low PoR verdict is not recoverable by retrying - every packet whose
counter is not above the card's is rejected again - so it now stops the
operation and is stated as plain data instead of a generic PoR error.

Server:
- _cntr_low_fields(status, cntr) -> {cntr_low, card_cntr, suggested_cntr}
  (fixed-width wrap like the counter field); carried by /api/send-ota (plus
  an 'OTA CNTR-LOW: card counter ...' log line) and by the RAM install /
  install-app / cap-compat failure responses (from the failed step);
- _send_envelope's late-PoR poll now also runs after a warning ENVELOPE
  answer (62xx/63xx - the live card answers 6200 for a low counter) and
  _send_secured_packet passes the captured PoR on, so the verdict is named
  instead of a bare 'ENVELOPE failed' (that is why it only surfaced on the
  later SPI2=0x21 query in the live log).

PWA:
- spCntrLow(res) guards every flow; the Explore routes the whole run through
  a checked sendOta(), so the paginate/expanded loops stop, no further
  packet is sent and no counter is written back (delete and Send to Card
  check it too);
- the result states the card's counter and offers 'Go to preset'
  (ramGoToPreset: opens the failed operation's preset in the Cards tab for
  editing and focuses the counter field) - nothing is written
  automatically; the operation modal carries the same notice;
- help (EN/RU) documents the guard.

sw cache simple-v331; tests: 557 Python / 714 Node.
2026-09-30 11:10:54 +03:00
catarrh aa4bf8e008 feat: expanded registry on by default, form ladder + fallback report (v3.6.54)
The 'Fetch expanded registry details' checkbox is now on by default: the
expanded pass carries the package versions, the SD/load-file associations
and the applet menu entries; an explicit stored opt-out is still honoured.

Live 2026-09-30: a card with a non-default ISD AID answered 6A88 to the
reference expanded query (P2=02 + the tag list), so the ELF rows had no
versions.  Cards differ in what they accept, so the query is now tried in
three forms (RAM_EXPANDED_FORMS):

  tags   - the reference tag list (incl. the TS 102 226 EA SCP Registry Data)
  noea   - the same list without EA (a card that errors on it)
  notags - no tag list at all (GPPro's 4F00-only data field); the card
           builds the default E3

The first form that answers is remembered for the run and tried first for
the later listings; the ISD gets a single attempt - its 6A88/6A86 is normal
('no data to report', GPPro) and must not poison the detection for the
apps/ELF queries.

Every listing's outcome is reported in the explorer (ramExpandedReport):
'Expanded registry: tag list + EA (Apps, ELFs) - ISD: no expanded data
(6A88) - compact listing used', or 'Expanded registry refused by the card -
compact listing only (package versions unavailable)'; the ELF section hints
when no version came back.  Tooltip/help (EN/RU) updated.

sw cache simple-v327; tests: 550 Python / 707 Node.
2026-09-30 09:44:39 +03:00
catarrh ee747003f2 feat: CAP compatibility test in the popup + per-AID failure analysis (v3.6.52)
The compatibility test now runs in the same operation modal as the installs
(live progress from the server's ram_progress, Close disabled until done) and
its result gains a per-AID detail block instead of the comma list:

- the probe summary (probe: 2 imports -> 0.0 . +1 appended);
- the import list as tested (ramCompatImportRows): the CAP's imports with the
  version the card was asked for and the CAP's own version when the probe
  lowered it, appended synthetic entries and unmatched lines - each with the
  resolved library name (jcAidName), so a raw AID list becomes readable rows;
- on a rejection, the failure analysis (ramCompatFailureHtml): which CAP
  section the failing block covers (load-file byte range and component names,
  'completes Import'), what the card checks at that point (JC VM spec 4.5.2)
  or that the import list already passed, the remote status word with its
  name, and what the probe state implies (every import at 0.0 -> a missing
  package or another load-file check is the cause; otherwise lower the
  versions and bisect).

ramCompatVerdict names the remote SW; the inline result keeps the verdict +
short probe note and the modal carries the escaped step lines + the detail
HTML (ramOpModalFinish gained an asHtml mode); the server reports
probe_unmatched as [{'aid', 'version'}] (_probe_unmatched), documented in
docs/api.md.  The same failure block for the real install is the next step.

sw cache simple-v325; tests: 550 Python / 703 Node.
2026-09-30 09:22:22 +03:00
catarrh 599d84fd66 feat: Explore exposes the rest of the registry data + install NV footprint (v3.6.51)
The compact (legacy) GET STATUS entry is <aid_len><AID><life cycle>
<privileges> + (P1=10) <module count> (<len><module AID>)*, cross-checked
against GlobalPlatformPro's populate_legacy (the 4th byte is privileges;
0x00 is required for load files).  The parser read it as a 'version' one
byte late, so ELF rows showed the module count / the next entry's length as
Version: 00/10 - the compact listing has no version at all; it now comes
from the expanded CE tag (ramFmtVersion: 2 bytes -> major.minor, GP Table
11-37 Note 1) and merges (the bogus compact value no longer blocked it).

New data exposed:
- EA SCP Registry Data (TS 102 226 8.2.1.6) -> 80 Menu Parameters -> the
  applet's STK menu entries: STK menu entry: 0x80 (position 1, Enabled)
- unknown registry objects are preserved (rawTlvs) and shown as
  'Other registry data: 8F=752B . 90=012D ...' instead of being dropped
- 84 parses the GP coding (one Executable Module AID per TLV, repeated; the
  concatenated 4F<len><aid> form stays as fallback) and the ELF expanded
  queries add 84 to the tag list (with a tag list the card returns only the
  listed tags - the fallback path would otherwise lose the module lists and
  their Install buttons)
- life cycle names decode per object kind (GP Tables 11-3..11-6: ISD/card,
  app, SD - detected via the Security Domain privilege - and ELF), with
  multi-byte 9F70 (02 0700 on live cards) decoding the first byte and
  surfacing non-zero extras; the R-APDU parser's LIFECYCLE_MAP labels were
  aligned with the same tables

/api/ram-install measures the NV footprint: a silent GET DATA FF21 read
before and after the chain (also on failure) returns nv_before/nv_after/
nv_delta; the reads never fail the operation (no FF21 -> no fields) and the
PWA shows 'NV used: ~N B (free A -> B)' in the install result.

sw cache simple-v324; tests: 548 Python / 698 Node.
2026-09-30 09:00:33 +03:00
catarrh 0dc79a3ba4 feat: CAP compatibility test as its own operation + append-only imports (v3.6.48)
The import probe no longer lives in 'Install Package (.cap file)': a
diagnostic that can leave a LOADED-only package does not belong in the
operation meant to produce a working applet.  /api/ram-install no longer
takes probe_imports; the install form's probe fieldset is gone.

New RAM operation CAP compatibility test (POST /api/cap-compat): the
server runs the format check, INSTALL [for load] and the LOAD blocks only
up to the block that completes the Import component (_cap_import_end +
_cap_compat_blocks, computed from the load file and the C4 TLV framing),
then stops - no last-block flag, no INSTALL [for install], so nothing is
committed and nothing needs deleting.  The verdict (imports_ok,
boundary_block/total_blocks) states it covers the LOAD/import gate only;
the link gate stays the real install's business.  Verified on the real
RemMobileID CAP: the boundary is LOAD 1/64, 2/128, 3/240 for 240/120/64 B
blocks - exactly the blocks where the live cards rejected the load.

_probe_import_versions(load_file, overrides, additions) now supports
append-only synthetic imports: +AID=version appends a new entry, the
Import component's size field and the Directory's Import size are patched,
and existing entries keep their positions (constant-pool import indices
stay valid); an AID that is already imported becomes a version override.
This is the card-capability query ('does this card export package X at
>= Y?') without building a probe applet.

PWA: new op section (CAP file + analysis + the version list + block size),
ramCompatTest with inline results and ramCompatVerdict, ramProbeParse
understands +AID=version; help EN/RU documents the operation, the two-step
bisect and the gate scope.

sw cache simple-v321; tests: 538 Python / 686 Node.
2026-09-30 07:57:11 +03:00
catarrh cf9c9b7bab feat: clean up the Explore UX (v3.6.45)
- Explore runs inline: no blocking modal (that stays for the install
  operations, which can leave a half-loaded package; Explore leaves the
  card consistent).  A test guards that ramExplore never calls the modal.
- Minimal success line: just OK - the details live in the explorer below;
  error runs keep the detailed 'Partial - ...' line.
- Counts moved into the section headings (Applications / Applet Instances
  (n), Executable Load Files (ELFs) / Packages (n)); the redundant '1 ISD'
  count is gone (P1=80 returns the ISD as a single occurrence, GP
  11.4.2.1).
- The RAM command format line gained a tooltip: compact = TS 102 226
  5.2.1 command string, expanded = the AA/22 scripting template (5.2.1.0),
  detected per operation with a read-only GET STATUS probe.
- The expanded-registry checkbox is relabelled 'Fetch expanded registry
  details (load-file / SD associations)' with a tooltip (extra secured
  packets, card support varies, the fallback is automatic) and is now a
  sticky per-browser preference (simple_ram_expanded).

sw cache simple-v318; tests: 531 Python / 681 Node.
2026-09-29 23:34:42 +03:00
catarrh 3d4e4e81f8 feat: infer the ETSI release from the CAP's uicc.toolkit import (v3.6.44)
The CAP details' Platform row named only the Java Card level; it now also
shows the ETSI/3GPP release the UICC API imports come from, with its basis
in brackets, e.g. 'Java Card >= 2.2.2 - ETSI REL-7 (uicc.toolkit 1.4) -
CAP format 2.1'.

capmem.ETSI_TOOLKIT_RELEASE maps the uicc.toolkit export version to the
release, sourced from the workspace corpus: sim-tools/ETSI-REL.md for the
prepared trees (api21_rel7_export_files carries uicc.toolkit 1.4 = the
REL-7 jar, api21_export_files 1.11 = the REL-12 jar) and
sim-tools/etsi/102241/README.md's package-version table for the rest
(1.0 -> REL-6/7, 1.3 -> REL-8, 1.5 -> REL-9, 1.12-1.14 -> REL-15..18; the
1.11 spec range is 13.01-14.02 but the tree the builds use is REL-12).
Exposed as requires_etsi_release/requires_etsi_basis in /api/cap-info and
the CLI; SIM-only CAPs (no uicc.toolkit) get no label.

Verified: RemMobileID -> REL-7 (uicc.toolkit 1.4), AlfaDSA -> REL-12
(uicc.toolkit 1.11).  sw cache simple-v317; tests: 531 Python / 677 Node.
2026-09-29 23:17:27 +03:00
catarrh f21d47538f feat: clearer import-probe wording, tooltips, translatable titles (v3.6.43)
The probe's checkbox described the mechanism ('Rewrite the CAP's import
versions before the load'); it now reads 'Enable import probe' (purpose
first) with the mechanism in a tooltip, and the field carries two short
hint lines: what the card checks while loading, and the workflow (Set all
0.0 first; if the LOAD passes the list was the cause, then restore one
line at a time to find the package/version the card lacks).

Static tooltips are translatable now via data-l10n-title (handled by
translatePage, checked against LANG_RU by html.test.js together with
data-l10n); the help's RAM-install section documents the two-step
workflow (EN/RU).

sw cache simple-v316; tests: 530 Python / 676 Node.
2026-09-29 23:07:25 +03:00
catarrh 755470e4ba feat: RAM-install import probe for LOAD rejections (v3.6.42)
Cards that cannot satisfy the CAP's import list reject the LOAD as soon
as the Import component is complete (live 2026-09-29: 6438/6985 in the
block containing its last byte, on several cards incl. an eUICC profile
whose ISD is not the default AID and which does not answer GET DATA 0066 -
no direct platform-version readout exists).

_probe_import_versions (pure) rewrites the Import component's two version
bytes per entry in the assembled load file: same length, component sizes,
Directory and offsets untouched; unknown AIDs and bad versions are
rejected.  /api/ram-install takes probe_imports ({'all': '0.0'} or
per-AID), echoes the applied changes in the response and logs them.  The
PWA's RAM install form gains a collapsed Import probe fieldset (checkbox,
AID=version lines prefilled from the CAP analysis, 'Set all 0.0') and the
result line reports the applied changes.  Diagnostic only - never used for
a real install.

Also verified _cap_parse's component order against the reference TCA load
trace (Header, Directory, Import, Applet, Class, Method, StaticField,
ConstantPool, RefLocation, Descriptor - Import before Applet, not
ascending tag order), so the order is correct and the import list remains
the prime suspect.

sw cache simple-v315; tests: 530 Python / 676 Node.
2026-09-29 03:41:07 +03:00
catarrh 904eca1758 fix: accept comprehension-required tag variants everywhere (v3.6.40)
The TERMINAL PROFILE chain parsed the SET UP MENU title with 'tag == 0x05'
only, so the live Alfa card's title (CR-set 0x85, 'Alfa Mobile') was
dropped: /api/menu returned items without a title and the PWA showed
'No menu set by the card' - card-specific only because cards pick the tag
variant (this one sends plain 0x8F items and a CR-set title in the same
command).

- _parse_setup_menu_command (pure, tested with the live FETCH bytes)
  replaces the inline parse; the PWA shows a title-less menu as 'STK: Menu'
  instead of 'No menu set by the card' (stkMenuLabel, pure).
- _cmd_tlv is now the single lookup helper: exact tag, then tag ^ 0x80
  (bit 8 is the CR flag, TS 101 220 7.1.1); the old '| 0x80'-only helper and
  the duplicate _cmd_tlv_any are gone.
- Remaining single-style checks fixed: _parse_setup_menu_items (0x8F/0x0F),
  _parse_select_item title (0x85/0x05), _find_sms_tpdu (0x8B/0x0B - the
  plain variant lost the SEND SHORT MESSAGE PoR TPDU), the response
  scripting parsers (R-APDU 0x23/0xA3, executed count 0x80/0x00) and the
  FF21 resource tags (0x81-0x83/0x01-0x03).
- tests/test_tag_variants.py flips the CR flag on every top-level tag of
  real command fixtures and asserts identical parse results for all
  parsers (verified to fail when a single-style lookup is reintroduced).

sw cache simple-v313; tests: 524 Python / 675 Node.
2026-09-29 01:54:39 +03:00
catarrh 0027c405c1 feat: answer GET INKEY / GET INPUT interactively (v3.6.38)
The menu chain paused only for DISPLAY TEXT / SELECT ITEM / SET UP MENU,
so a GET INPUT was auto-answered with a bare success TR and no Text
string - the live RemMobileID PIN prompt got an empty answer.

Server: _parse_get_inkey/_parse_get_input read the prompt, qualifier
flags (digits-only, UCS2, Yes/No, immediate, hidden, packed, help),
Response length 91 and Default Text 9D; the fetch handler pauses with
them; _menu_send_response(..., text=) validates the answer (digits set,
min/max, one char for GET INKEY) and builds the Text string TLV 8D in the
requested coding (SMS unpacked 04 / UCS2 08 / packed 00 via _pack_gsm7),
Yes/No as 01/00, empty input as the null text string, help as result 13
and the variable-timeout Duration echoed (6.8.4).  /api/menu-respond
takes text; /api/stk-status carries pending_data so a card-initiated
prompt opens the panel; _decode_cmd decodes both commands.

PWA: stkInputHtml/stkInputHint/stkInputValidate render the constrained
panel (password field for hidden entry, numeric keyboard, default text,
Yes/No buttons, help, immediate-digit auto-submit); OK submits through
stkMenuOk; the poll opens the panel for card-initiated prompts.

sw cache simple-v311; tests: 517 Python / 674 Node.
2026-09-29 01:30:40 +03:00
catarrh ddf5cbb8e7 fix: late-PoR handler on every path, one poll per packet, LOAD-step components (v3.6.37)
_send_envelope defined its SEND SHORT MESSAGE capture handler inside the
91xx branch, but the 'STATUS poll (PoR not captured)' path used it: a
packet whose ENVELOPE answered 9000 and whose late PoR arrived on the
STATUS poll raised UnboundLocalError, returned 500 and aborted the RAM
operation mid-packet with the card's command left pending (live
2026-09-29, 120-byte LOAD blocks).  The handler is now defined for every
path, and the late-PoR poll runs only after a packet's last segment
(poll_status from _send_secured_packet) - a concatenated download makes
no spurious mid-packet STATUS exchanges.

The PWA annotates every LOAD step with the CAP components its block
covers (ramStepComponents: 'completes Import' / 'inside Method' /
'Header...Import') and ramInstallFailHint names the component the failing
block completed - the live import-verification rejection lands in the
block completing Import (6438/6985 at both 240 B and 64 B blocks).

sw cache simple-v310; tests: 509 Python / 672 Node.
2026-09-29 01:14:37 +03:00