JavaScript bridge API
Biometrics and the keystore
Fingerprint and face unlock, and hardware-backed keys the page can encrypt with but never read.
AppMintKeystore.decrypt helper#
window.AppMintKeystore.decrypt(alias, base64)
Example
Decrypts data made by AppMintKeystore.encrypt with the same key.
Returns: an object, at once (not a Promise): {ok:true, base64} (the plain data) or {ok:false, error, detail}. Needs: Secure Keys (encryption) turned on when you build.
function fromBase64(b64) { return decodeURIComponent(escape(atob(b64))); }
function readSecret() {
var sealed = localStorage.getItem('secret');
if (!sealed || !window.AppMintKeystore) return null;
var r = window.AppMintKeystore.decrypt('vault', sealed);
if (r.ok) return fromBase64(r.base64);
switch (r.error) {
case 'auth-required': unlockThen(readSecretAndShow); break; // unlock, then try again
case 'no-such-key':
case 'key-invalidated': alert('This secret can no longer be opened.'); break;
default: alert('Could not decrypt: ' + r.error);
}
return null;
}For a file written with WebToApkFS.writeSealed, read it back with WebToApkFS.readSealed(path, alias) - same result shape.
Notes: After deleteKey the data is gone for good; that is the point. A payload that was changed fails the GCM check. Other errors: bad base64, disabled, foreign_origin, unsupported.
AppMintKeystore.deleteKey helper#
window.AppMintKeystore.deleteKey(alias)
Example
Destroys a secure key permanently. Everything encrypted with it can never be read again - by anyone.
Returns: true (deleted) or false (no such key, or the feature is off), at once. Needs: Secure Keys (encryption) turned on when you build.
Overwriting a file does not erase it on flash memory. Encrypt it, then destroy the key - that is a real delete:
function deleteNoteForReal() {
if (!window.AppMintKeystore || !window.WebToApkFS) return;
if (!confirm('Delete this note forever?')) return;
var gone = window.AppMintKeystore.deleteKey('vault'); // secret.bin is unreadable from now on
window.WebToApkFS.delete('secret.bin'); // tidy up the file too
alert(gone ? 'Deleted.' : 'Nothing to delete.');
}Notes: There is no undo and no recovery. If isHardwareBacked(alias) was false, the key lived in software - say so before you promise the user an irreversible delete.
AppMintKeystore.encrypt helper#
window.AppMintKeystore.encrypt(alias, base64)
Example
Encrypts base64 data with a secure key. Store the result anywhere - it is useless without the key.
Returns: an object, at once (not a Promise): {ok:true, base64} (IV + ciphertext + tag) or {ok:false, error, detail}. Needs: Secure Keys (encryption) turned on when you build, and a key from AppMintKeystore.generateKey.
function toBase64(text) { return btoa(unescape(encodeURIComponent(text))); }
function saveSecret(text) {
if (!window.AppMintKeystore || !window.AppMintKeystore.isAvailable()) return false;
var r = window.AppMintKeystore.encrypt('vault', toBase64(text));
if (r.ok) {
localStorage.setItem('secret', r.base64);
return true;
}
if (r.error === 'auth-required') {
// the key needs a fresh unlock: ask for the fingerprint, then call saveSecret again
unlockThen(function () { saveSecret(text); });
} else {
alert('Could not encrypt: ' + r.error);
}
return false;
}unlockThen can be built on authenticateBiometricEx (see that example).
To write an encrypted FILE of any size, use WebToApkFS.writeSealed(path, base64, alias) - it encrypts chunk by chunk with this same key.
Notes: Max 4 MB per call (chunk too large). Errors: no-such-key, auth-required, key-invalidated (the screen lock changed - the key is gone), bad base64, disabled, foreign_origin, unsupported.
AppMintKeystore.generateKey helper#
window.AppMintKeystore.generateKey(alias, options)
Example
Creates (or replaces) an AES-256-GCM key in the Android Keystore. The key never leaves secure hardware; your page only uses its name.
Returns: an object, at once (not a Promise): {ok:true, alias, hardwareBacked} or {ok:false, error, detail}. Needs: Secure Keys (encryption) turned on when you build (see AppMintKeystore.isAvailable).
function createVault() {
if (!window.AppMintKeystore || !window.AppMintKeystore.isAvailable()) {
alert('Secure keys are not available in this app.');
return;
}
var r = window.AppMintKeystore.generateKey('vault', {
requireAuth: true, // usable only after a fingerprint / PIN unlock...
authValiditySeconds: 30 // ...for 30 seconds after it
});
if (!r.ok) { alert('Could not create the key: ' + r.error); return; }
console.log('Hardware key:', r.hardwareBacked);
}A plain key, no unlock needed:
var r = window.AppMintKeystore.generateKey('notes'); // options default to {}Demand the separate secure chip (StrongBox). It fails instead of giving you a weaker key:
var r = window.AppMintKeystore.generateKey('wallet', { strongBox: true });
if (!r.ok && r.error === 'strongbox-unavailable') {
alert('This phone has no secure chip.');
}Notes: Alias: letters, digits, _ - ., up to 64 characters (error:"bad alias" otherwise). Using an existing alias replaces the key, and data sealed with the old one is lost. Other errors: disabled, bad options json, foreign_origin, unsupported (no bridge).
AppMintKeystore.hasKey helper#
window.AppMintKeystore.hasKey(alias)
Example
Tells you whether a secure key with this alias exists.
Returns: true / false, at once. Needs: Secure Keys (encryption) turned on when you build.
function startVault() {
if (!window.AppMintKeystore || !window.AppMintKeystore.isAvailable()) return;
if (window.AppMintKeystore.hasKey('vault')) {
showUnlockScreen();
} else {
showCreateVaultScreen();
}
}
window.addEventListener('load', startVault);Notes: Answers false when Secure Keys is off, so check isAvailable() first if you need to tell "no key yet" from "feature not in this app".
AppMintKeystore.isAvailable helper#
window.AppMintKeystore.isAvailable()
Example
Tells you whether hardware-backed secure keys can be used in this app.
Returns: true / false, at once (not a Promise). Needs: Secure Keys (encryption) turned on when you build. The window.AppMintKeystore object is installed (before your first script runs) whenever Secure Keys or Native Folder Access (SAF) is on; isAvailable() answers for Secure Keys.
function secureKeysReady() {
return !!window.AppMintKeystore && window.AppMintKeystore.isAvailable();
}
// Check when the user acts (the helper is added when the page finishes loading):
document.getElementById('vault-button').addEventListener('click', function () {
if (!secureKeysReady()) {
alert('The secure vault is not available in this app.');
return;
}
openVault();
});Notes: The helper is added after the page has loaded, so do not test for it in a script that runs while the page is still parsing - test in a click handler or after load. In a browser there is no window.AppMintKeystore; the check above returns false without throwing.
AppMintKeystore.isHardwareBacked helper#
window.AppMintKeystore.isHardwareBacked(alias)
Example
Tells you whether a key lives in the phone's secure hardware (TEE or StrongBox) rather than in software.
Returns: true / false, at once. false when the key does not exist. Needs: Secure Keys (encryption) turned on when you build.
function showKeyStrength() {
if (!window.AppMintKeystore || !window.AppMintKeystore.hasKey('vault')) return;
var hw = window.AppMintKeystore.isHardwareBacked('vault');
document.getElementById('strength').textContent = hw
? 'Protected by secure hardware'
: 'Protected by software (this phone has no secure hardware for keys)';
}Notes: generateKey also returns hardwareBacked right after it creates the key.
AppMintKeystore.listKeys helper#
window.AppMintKeystore.listKeys()
Example
Lists the aliases of every secure key your pages created.
Returns: an array of strings, sorted, at once, e.g. ['notes', 'vault']. [] when there are none or the feature is off. Needs: Secure Keys (encryption) turned on when you build.
function renderKeyList() {
var list = document.getElementById('key-list');
list.innerHTML = '';
if (!window.AppMintKeystore) return;
window.AppMintKeystore.listKeys().forEach(function (alias) {
var li = document.createElement('li');
li.textContent = alias + (window.AppMintKeystore.isHardwareBacked(alias) ? ' (hardware)' : '');
list.appendChild(li);
});
}Notes: Only keys made through the bridge are listed; the app's own internal keys are never shown.
authenticateBiometric bridge#
window.WebToApk.authenticateBiometric(callbackId: String, title: String, subtitle: String)
WebToApk.isBiometricAvailable() -> "available" | "no_hardware" | "not_enrolled" | "unavailable" WebToApk.authenticateBiometric(id, title, subtitle) Result is delivered to the same __webToApkAuth callback table the Google Sign-In bridge uses: window.__webToApkAuth = window.__webToApkAuth || {}; window.__webToApkAuth['unlock'] = function (json) { var r = JSON.parse(json); // {ok:true} | {ok:false,error:"..."} if (r.ok) showApp(); }; WebToApk.authenticateBiometric('unlock', 'Unlock', 'Use your fingerprint');
Described by its group, Fingerprint / biometric bridge, rather than on its own.
Example
Shows the system fingerprint prompt with a title and subtitle. The simple form of authenticateBiometricEx (use that one for the PIN option, a custom Cancel label or "strong" biometrics).
Returns: nothing. The answer arrives later: the shell calls window.__webToApkAuthcallbackId with '{"ok":true}' or '{"ok":false,"error":"<code>"}', then removes that entry. Needs: Fingerprint ticked when you build (without it: error:"permission_missing").
Register the callback FIRST, then call:
function unlockWithFingerprint() {
return new Promise(function (resolve) {
if (!window.WebToApk || typeof window.WebToApk.authenticateBiometric !== 'function') {
resolve({ ok: false, error: 'not_in_app' });
return;
}
var id = 'bio_' + Date.now();
window.__webToApkAuth = window.__webToApkAuth || {};
window.__webToApkAuth[id] = function (json) { resolve(JSON.parse(json)); };
window.WebToApk.authenticateBiometric(id, 'Unlock', 'Touch the fingerprint sensor');
});
}
document.getElementById('unlock').addEventListener('click', async function () {
var r = await unlockWithFingerprint();
if (r.ok) showMyApp();
else if (r.error === 'user_cancel') { /* the user closed it — do nothing */ }
else if (r.error === 'none_enrolled') alert('Please add a fingerprint in phone settings.');
else if (r.error === 'lockout') alert('Too many tries. Wait a little and try again.');
else alert('Could not unlock: ' + r.error);
});Notes: An empty title uses the app's name. A wrong finger does NOT end the prompt - the callback fires only on success, cancel or a real error. Error codes are the same as authenticateBiometricEx. This proves the phone's owner is present; it does not prove who they are to a server.
authenticateBiometricEx bridge#
window.WebToApk.authenticateBiometricEx(callbackId: String, optionsJson: String)
The full form. [optionsJson] accepts: title, subtitle, description, negativeButtonText, allowDeviceCredential (PIN/pattern/password fallback), strong (require BIOMETRIC_STRONG - use for anything payment-shaped). Errors are distinct codes, not one boolean. Before any prompt: no_hardware | none_enrolled | unavailable (the status check could not run - see isBiometricAvailable). From the Android 9+ system prompt: user_cancel | no_hardware | hw_unavailable | none_enrolled | lockout | lockout_permanent | no_device_credential | timeout | no_space | unable_to_process | vendor_error, and for a code Android adds later its own message text (or `error_<code>`). permission_missing when the build lacks the Fingerprint permission. On Android 7-8 (API 24-27, fingerprint only) an error is the phone's own message text as the OEM wrote it - not a fixed code.
Example
Shows the system fingerprint / face prompt with full options: PIN or pattern as a second way in, a custom Cancel label, and "strong" biometrics only.
Returns: nothing. The answer arrives later: the shell calls window.__webToApkAuthcallbackId with '{"ok":true}' or '{"ok":false,"error":"<code>"}', then removes that entry. Needs: Fingerprint ticked when you build (without it: error:"permission_missing").
A small Promise wrapper you can reuse:
function verifyUser(options) {
return new Promise(function (resolve) {
if (!window.WebToApk || typeof window.WebToApk.authenticateBiometricEx !== 'function') {
resolve({ ok: false, error: 'not_in_app' });
return;
}
var id = 'bio_' + Date.now() + '_' + Math.random().toString(36).slice(2);
window.__webToApkAuth = window.__webToApkAuth || {};
window.__webToApkAuth[id] = function (json) { resolve(JSON.parse(json)); };
window.WebToApk.authenticateBiometricEx(id, JSON.stringify(options));
});
}Unlock the app - fingerprint, face, or the phone's PIN/pattern:
async function unlockApp() {
var r = await verifyUser({
title: 'Unlock',
subtitle: 'Use your fingerprint or PIN',
allowDeviceCredential: true // Android 11+: the system adds "Use PIN"
});
if (r.ok) showMyApp();
else if (r.error === 'lockout') alert('Too many tries. Wait a little and try again.');
}Confirm a payment - strong biometrics only, your own Cancel label:
async function confirmPayment() {
var r = await verifyUser({
title: 'Confirm payment',
description: 'Pay 12.00 to Coffee House',
negativeButtonText: 'Not now',
strong: true // BIOMETRIC_STRONG (Android 11+)
});
if (r.ok) sendPayment();
else if (r.error !== 'user_cancel') alert('Could not confirm: ' + r.error);
}Unlock a Keystore key made with requireAuth, then retry:
async function openVault() {
var r = await verifyUser({ title: 'Open vault', allowDeviceCredential: true });
if (r.ok) readSecret(); // AppMintKeystore.decrypt works for authValiditySeconds now
}Notes: Error codes: user_cancel, none_enrolled, no_hardware, hw_unavailable, lockout (wait and retry), lockout_permanent (the user must unlock the phone with PIN first), no_device_credential, timeout, permission_missing, unavailable, and rarely no_space, unable_to_process, vendor_error. On Android 7-8 the shell shows its own dialog and an error is the system's message text. allowDeviceCredential and negativeButtonText do not mix: with the PIN option the system draws its own button. A wrong finger does not end the prompt.
decryptWithKey bridge#
window.WebToApk.decryptWithKey(alias: String, base64Payload: String): String
Reverses [encryptWithKey]. Fails with `error:"no-such-key"` once the key has been deleted - the payload is then unrecoverable by design. @return JSON `{ok, base64, error}`.
Example
Decrypts data made by encryptWithKey with the same Keystore key. Most pages use AppMintKeystore.decrypt(alias, base64), which returns the parsed object.
Returns: a JSON string, at once: {"ok":true,"base64":"<plain data>"} or {"ok":false,"error":"...","detail":"..."}. Needs: Secure Keys (encryption) turned on when you build.
function fromBase64(b64) { return decodeURIComponent(escape(atob(b64))); }
function openNote() {
if (!window.WebToApk || typeof window.WebToApk.decryptWithKey !== 'function') return null;
var sealed = localStorage.getItem('note');
if (!sealed) return null;
var r = JSON.parse(window.WebToApk.decryptWithKey('vault', sealed));
if (r.ok) return fromBase64(r.base64);
if (r.error === 'auth-required') {
askUserToUnlockThenRetry(); // unlock with authenticateBiometricEx, then call again
} else if (r.error === 'no-such-key' || r.error === 'key-invalidated') {
alert('This note can no longer be opened.'); // the key was deleted or the screen lock changed
} else {
alert('Could not open the note: ' + r.error);
}
return null;
}Notes: Once the key is deleted (deleteKey) the data can never be decrypted again - by design. A changed or damaged payload fails (the GCM tag does not match); the error is then the platform's message, not one of the codes above. Other codes: bad base64, disabled, foreign_origin.
deleteKey bridge#
window.WebToApk.deleteKey(alias: String): String
Destroys [alias] permanently. Everything encrypted with it becomes undecryptable - including by this app, and by anyone holding the device.
Example
Destroys a Keystore key for good. Everything encrypted with it can never be read again - by you, the user or anyone holding the phone. Most pages use AppMintKeystore.deleteKey(alias), which returns a real boolean.
Returns: the STRING "true" (deleted) or "false" (no such key, Secure Keys off, or not the app's own page), at once. Needs: Secure Keys (encryption) turned on when you build.
This is the only real "secure delete" on a phone: overwriting a file does not erase it on flash memory, destroying the key does.
function destroyVault() {
if (!window.WebToApk || typeof window.WebToApk.deleteKey !== 'function') return;
if (!confirm('Delete the vault? This cannot be undone.')) return;
var gone = window.WebToApk.deleteKey('vault') === 'true';
localStorage.removeItem('note'); // the sealed data is now useless anyway
alert(gone ? 'Vault deleted.' : 'There was no vault to delete.');
}Notes: Check isKeyHardwareBacked before you promise a user that deletion is irreversible - a software-only key is weaker.
encryptWithKey bridge#
window.WebToApk.encryptWithKey(alias: String, base64Plaintext: String): String
Encrypts base64 [base64Plaintext] with [alias]. Each call generates its own IV, so a large file is encrypted chunk by chunk - chunks are independent and may be decrypted in any order. @return JSON `{ok, base64, error}` where `base64` is `IV || ciphertext || tag`.
Example
Encrypts base64 data with a Keystore key. Most pages use AppMintKeystore.encrypt(alias, base64), which returns the parsed object.
Returns: a JSON string, at once: {"ok":true,"base64":"<IV + ciphertext + tag>"} or {"ok":false,"error":"...","detail":"..."}. Needs: Secure Keys (encryption) turned on when you build, and a key made with generateKey.
The input is base64. For text, turn it into UTF-8 base64 first:
function toBase64(text) { return btoa(unescape(encodeURIComponent(text))); }
function sealNote(text) {
if (!window.WebToApk || typeof window.WebToApk.encryptWithKey !== 'function') return null;
var r = JSON.parse(window.WebToApk.encryptWithKey('vault', toBase64(text)));
if (r.ok) {
localStorage.setItem('note', r.base64); // safe to store anywhere: useless without the key
return r.base64;
}
if (r.error === 'auth-required') {
// key was made with requireAuth — unlock first (authenticateBiometricEx), then call again
askUserToUnlockThenRetry();
} else {
alert('Could not encrypt: ' + r.error);
}
return null;
}Notes: Each call uses a fresh IV, so the same text gives a different result every time. At most 4 MB per call (error:"chunk too large ..."); for bigger files encrypt chunk by chunk, or use WebToApkFS.writeSealed. Error codes: no-such-key, auth-required, key-invalidated (the user changed the screen lock - the key is gone for good), bad base64, disabled, foreign_origin.
generateKey bridge#
window.WebToApk.generateKey(alias: String, optionsJson: String): String
Creates (or replaces) an AES-256-GCM key called [alias]. @param optionsJson `{"requireAuth":bool, "authValiditySeconds":int, "strongBox":bool}`. `requireAuth` binds the key to the device lock; `strongBox` fails outright on devices without a secure element rather than handing back a weaker key than was asked for. @return JSON `{ok, alias, hardwareBacked, error}`.
Example
Creates (or replaces) an AES-256-GCM key in the Android Keystore. The page gets a name for the key, never the key itself. Most pages use AppMintKeystore.generateKey(alias, options), which does the JSON work for you.
Returns: a JSON string, at once: {"ok":true,"alias":"vault","hardwareBacked":true} or {"ok":false,"error":"...","detail":"..."}. Needs: Secure Keys (encryption) turned on when you build.
function createVaultKey() {
if (!window.WebToApk || typeof window.WebToApk.generateKey !== 'function') {
alert('Secure keys work only in the app.');
return false;
}
var options = {
requireAuth: true, // key works only after the user unlocks (fingerprint / PIN)
authValiditySeconds: 30, // ...and for 30 seconds after that unlock
strongBox: false // true = demand the separate secure chip (fails if missing)
};
var r = JSON.parse(window.WebToApk.generateKey('vault', JSON.stringify(options)));
if (!r.ok) {
alert('Could not create the key: ' + r.error);
return false;
}
if (!r.hardwareBacked) console.log('Key is kept in software on this phone');
return true;
}A plain key with no unlock rule (options may be '{}'):
var r = JSON.parse(window.WebToApk.generateKey('notes', '{}'));Notes: The alias may use only letters, digits, _, -, . and at most 64 characters, otherwise error:"bad alias". Calling it again with the same alias REPLACES the key, and data sealed with the old key can no longer be read. Error codes: disabled (Secure Keys not turned on), bad options json, strongbox-unavailable (you asked for strongBox:true and the phone has no secure chip, or it is older than Android 9), foreign_origin (called from a page that is not the app's own).
hasKey bridge#
window.WebToApk.hasKey(alias: String): String
True when a key called [alias] exists in this app's keystore.
Example
Tells you whether a Keystore key with this alias exists. Most pages use AppMintKeystore.hasKey(alias), which returns a real boolean.
Returns: the STRING "true" or "false", at once. Needs: Secure Keys (encryption) turned on when you build (without it the answer is always "false").
function vaultExists() {
if (!window.WebToApk || typeof window.WebToApk.hasKey !== 'function') return false;
return window.WebToApk.hasKey('vault') === 'true'; // compare the string
}
if (!vaultExists()) {
showCreateVaultScreen();
}Notes: The answer is a string, so if (WebToApk.hasKey('vault')) is always true - compare with === 'true'. You only see keys this app created through the bridge; the shell's own keys are hidden.
isBiometricAvailable bridge#
window.WebToApk.isBiometricAvailable(): String
"available" | "no_hardware" | "not_enrolled" | "unavailable". Android 10+ counts face, iris and fingerprint; Android 6-9 the fingerprint sensor only. "unavailable" = the sensor is temporarily unusable, or the check could not run (e.g. the build did not enable the Fingerprint permission). See [BiometricStatus].
Example
Tells you whether the phone can do a biometric unlock (face, iris or fingerprint) right now. Check it before you show an unlock button.
Returns: a string, at once: "available", "no_hardware" (no sensor), "not_enrolled" (sensor, but nothing registered, or no screen lock set) or "unavailable" (the sensor is temporarily unusable, or the check could not run - for example the build did not tick Fingerprint). Needs: Fingerprint ticked when you build - without it Android refuses to answer and the result is "unavailable".
function setupFingerprintButton() {
var btn = document.getElementById('fingerprint-button');
if (!window.WebToApk || typeof window.WebToApk.isBiometricAvailable !== 'function') {
btn.hidden = true; // browser: no fingerprint bridge
return;
}
var state = window.WebToApk.isBiometricAvailable();
if (state === 'available') {
btn.hidden = false;
} else if (state === 'not_enrolled') {
btn.hidden = true;
showHint('Add a fingerprint in your phone settings to unlock with it.');
} else {
btn.hidden = true; // no_hardware / unavailable
}
}Notes: On Android 10 and newer the check counts face, iris and fingerprint, so a face-unlock-only phone answers "available". On Android 6-9 it looks at the fingerprint sensor only, so a face-only phone there answers "no_hardware"; with allowDeviceCredential:true in authenticateBiometricEx the user can still unlock with the phone's PIN, pattern or password (Android 11+).
isKeyHardwareBacked bridge#
window.WebToApk.isKeyHardwareBacked(alias: String): String
True when [alias] lives in the TEE or a secure element rather than in software. Worth checking before promising a user that destroying a key is irreversible.
Example
Tells you whether a key lives inside the phone's secure hardware (TEE or StrongBox chip) rather than in software. Most pages use AppMintKeystore.isHardwareBacked(alias), which returns a real boolean.
Returns: the STRING "true" or "false", at once. "false" also when the key does not exist. Needs: Secure Keys (encryption) turned on when you build.
function describeVault() {
if (!window.WebToApk || typeof window.WebToApk.isKeyHardwareBacked !== 'function') return;
var hw = window.WebToApk.isKeyHardwareBacked('vault') === 'true';
document.getElementById('vault-info').textContent = hw
? 'Your key is locked in this phone\'s secure hardware.'
: 'Your key is protected by software on this phone.';
}Notes: Say "hardware" in your UI only when this is "true".
isKeystoreEnabled bridge#
window.WebToApk.isKeystoreEnabled(): String
Whether the creator enabled the keystore bridge for this app.
Example
Tells you whether the creator turned on the Keystore bridge for this app. Most pages use AppMintKeystore.isAvailable(), which returns a real boolean.
Returns: the STRING "true" or "false", at once. Needs: nothing to call it; it answers "true" only when Secure Keys (encryption) was turned on when you built.
function keystoreReady() {
return !!window.WebToApk &&
typeof window.WebToApk.isKeystoreEnabled === 'function' &&
window.WebToApk.isKeystoreEnabled() === 'true';
}
document.getElementById('vault-button').hidden = !keystoreReady();Notes: When it is "false", every other key method answers "false", "[]" or {ok:false, error:"disabled"}. Hide the feature instead of offering a button that cannot work.
listKeys bridge#
window.WebToApk.listKeys(): String
JSON array of every key alias this page has created.
Example
Lists the aliases of every Keystore key this app's pages created. Most pages use AppMintKeystore.listKeys(), which returns the parsed array.
Returns: a JSON array string, sorted by name, at once: '["notes","vault"]'. Needs: Secure Keys (encryption) turned on when you build (without it: "[]").
function showKeys() {
if (!window.WebToApk || typeof window.WebToApk.listKeys !== 'function') return;
var aliases = JSON.parse(window.WebToApk.listKeys());
document.getElementById('keys').textContent =
aliases.length ? aliases.join(', ') : 'No keys yet';
}Notes: Only keys made through the bridge are listed; the app's own internal keys are never shown. Also answers "[]" when it is called from a page that is not the app's own (another site loaded in the app).