feat: single-APDU INSTALL [for install] / [for make selectable] ops (v3.6.18)

Iterating the final install step no longer needs a delete + re-load of the
.cap:

- `POST /api/ram-install-app` runs INSTALL [for install] (`mode: "install"`,
  P1 0x0C/0x04 by make_selectable) or INSTALL [for make selectable]
  (`mode: "make_selectable"`, GP 11.5.2.3.3 Table 11-44) for an already
  loaded package.  The APDU builders are shared with `_cap_apdu_sequence`
  (`_cap_install_apdu`, `_cap_make_selectable_apdu` - one builder, no drift)
  and the per-step send/PoR/counter logic is the extracted shared
  `_ram_send_gp_apdu` (used by /api/ram-install too).
- PWA: the RAM op selector gains "INSTALL [for install] (loaded package)"
  and "INSTALL [for make selectable]" with Load File / Module / Instance AID
  inputs (instance defaults to the module AID); the result renders through
  the shared `ramShowInstallResult` and `final_cntr` keeps the preset in
  sync.
- Explore: an **Install** button on a package's module AIDs that have no
  instance yet, and a **Make selectable** button on INSTALLED (0x03)
  instances - both switch the op and pre-fill the AIDs.
- tests: the install APDU pinned to the live no-STK packet and to the
  sequence tail, the make-selectable APDU, ramHasInstance, the Explore
  buttons and the op wiring; docs/api.md updated.

650 frontend / 498 Python green; version 3.6.18; sw simple-v291.
This commit is contained in:
2026-09-28 08:10:40 +03:00
parent 6403fb9f29
commit face2268ca
7 changed files with 414 additions and 109 deletions
+32 -1
View File
@@ -23,7 +23,7 @@ function extractFunc(src, name) {
// Extract chain builder functions and dependencies
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
@@ -441,6 +441,37 @@ test('ramRemoteSwOk mirrors the server success set', () => {
}
});
test('ramHasInstance matches instances by AID prefix', () => {
const apps = [{ aid: 'F0414C4641610101' },
{ aid: 'A1130001180001FFFFFFFF89A1003908' }];
assert.strictEqual(ramHasInstance(apps, 'f0414c46416101'), true);
assert.strictEqual(ramHasInstance(apps, 'F0414C4641610101'), true);
assert.strictEqual(ramHasInstance(apps, 'F0414C46416001'), false);
assert.strictEqual(ramHasInstance([], 'F0414C46416101'), false);
});
test('the Explore result offers Install / Make selectable actions', () => {
global.t = s => s;
const out = ramRenderExploreHtml(
null,
[],
[{ aid: 'A000000151000000', lifecycle: '03', privileges: '' }],
[{ aid: 'F0414C46416101', lifecycle: '01', version: '0.0',
moduleAids: ['F0414C4641610101'] }]
);
// the module has no instance yet -> Install; the INSTALLED instance -> Make selectable
assert.ok(out.includes("ramInstallFromExplorer('F0414C46416101','F0414C4641610101')"), out);
assert.ok(out.includes("ramMakeSelectableFromExplorer('A000000151000000')"), out);
delete global.t;
});
test('the single-APDU ops are wired', () => {
assert.ok(/value="install-app"/.test(html), 'the INSTALL [for install] op is missing');
assert.ok(/value="make-selectable"/.test(html), 'the make selectable op is missing');
assert.ok(/async function ramInstallApp\(sp, op\)/.test(html), 'ramInstallApp is missing');
assert.ok(/pysimFetch\('\/api\/ram-install-app'/.test(html), 'the endpoint call is missing');
});
test('ramRemoveFromExplorer drops the object locally (cascade drops its applets)', () => {
let renders = 0;
globalThis.ramRenderExplorer = () => { renders++; };