All work BROWSER EXTENSION / GOVERNMENT AUTOMATION internal

15  /  BROWSER EXTENSION / GOVERNMENT AUTOMATION

Singapore Visa Autofill

On a government visa form, a plausible wrong value is a false statement. Mine leaves the box blank.

September 2026Chrome Extension MV3JavaScript (ES modules)MV3 service workerOffscreen Documents APIchrome.storage.sessionTesseract.js (vendored WASM)pdf.jsjsdom

The challenge

A travel agency files Singapore visas for CIS nationals. Every application gets keyed twice. Once into the agency's own dashboard when the traveller hands over their documents, then again into ICA's SAVE portal: five stages of a Java form behind an agent login, with an idle timeout and a Back button that kills the transaction. Several fields on the first stage are not in the dashboard at all. They exist only on the printed passport page.

What I built

I built a Manifest V3 extension with no build step and no runtime dependencies. The operator loads an applicant from the dashboard panel or from an uploaded macro recording, and on ICA SAVE a panel works out which stage is open, fills it, reads every value back off the page, and reports per field what it filled, skipped, could not find, or refused to answer. The passport read starts the moment an applicant is loaded, so by the time the operator reaches the form the issue date, place of issue and country of birth are already in their boxes. The last click is always the operator's. The extension will press Next between stages if you enable it, and refuses any button whose label reads like Submit.

How it works

AUTOMATED Applicant loadeddashboard, or a recording Passport read twiceevery page, at two scales Filled in passesdropdowns first, then refill Next, if you enable itbetween the form's stages WHERE IT REFUSES The readings disagreeleft blank, with a reason No matching optionreported, not a neighbour Submit stays unpressedthe operator makes that one
The mechanism, drawn from the build.

The detail

01

Field map

The ICA form sits behind an agent login, so I could not write its layout in advance. I split one recorded iMacros script in two: the real element ids, dropdown codes and date formats became a reusable field map, while I threw away every value identifying the recorded traveller and bound it to the dashboard field that supplies it. I committed that map, so a dashboard applicant now fills with no import at all, and importing a further recording still adds that person as an applicant.

02

Adjectives, not countries

Matching dropdown options on their visible text worked for Russia and quietly failed for Ukraine, Moldova, Kyrgyzstan and Turkmenistan, because ICA renders UKRAINIAN, MOLDAVIAN, KYRGYZ REPUBLIC. I read the real option codes off the live form and committed them to the vocabulary file. If ICA changes a list, the fill reports no option with that code rather than picking a neighbour.

03

Filling in passes

SAVE is JSF with PrimeFaces and cascading dropdowns: setting Country of Birth fires an ajax response that replaces that part of the page, taking anything already filled there with it, so a single sweep ends with only the last field surviving. The fill runs in passes instead. Dropdowns go first while the text boxes are still empty, then it waits for the page to go quiet, looks again, and refills what got wiped; PrimeFaces selects are not real select elements, so I write both the hidden select and the visible label through the prototype's native setter.

04

Reading the passport

I built and threw away two passport-reading designs before this one, which runs in an offscreen document, the only MV3 context with both a DOM and a Worker, and fetches the scan itself so megabytes of pixels never cross a message channel or reach the government page. Most passports on file turned out to be PDFs rather than images, several of them multi-page and some rotated, so every page and orientation is searched and the machine-readable zone's check digits decide which combination was right. Each page is read twice at different scales and a field is only offered when both readings agree; anything ambiguous is left blank with a reason, because a plausible wrong value is a false statement on a government application.

05

Identity, not index

Adversarial review of my passport work kept turning up defects of one shape: a path that produces a wrong value and reports it as verified. The worst keyed the in-flight read by applicant index rather than by person, and every application has an applicant 0, so switching applications mid-read merged one traveller's details into another's profile, past the identity cross-check, while the panel told the operator those fields needed no checking. Identity is now the traveller's own passport number, date of birth and name; an unidentifiable record yields an empty key, and an empty key matches nothing, including another empty one.

The outcome

I verified the dashboard-to-ICA fill end to end against the live SAVE form while signed in on 14 September. I verified the passport-reading work that followed in a browser against the real vendored engines and against passports on the live dashboard, but I have not re-run it against ICA: the last commit is a critical identity fix that adversarial review found, not a live pass. 368 assertions across 23 suites pass on a clean local run, one suite devoted entirely to the refusals, the cases where filling the wrong box would put one person's identity document on another person's application. Stage one still cannot be completed entirely by the extension. Race is mandatory on ICA and is collected nowhere in the agency's system, and the sex the passport supplies sits outside the check digits, so I report it as unverified rather than asserted. The fix is adding those fields to the agency's own intake form. You install it by loading the unpacked folder; it is not published to the Chrome Web Store.