fix(machine): read NTAG424 NDEF via Capability Container (Bolt Card FileID)

First real-card tap read the NDEF file with id 0004 and got "not a Bolt
Card" — NTAG424 (Bolt Cards) use FileID E104. Read the Capability Container
(EF E103) after selecting the NDEF app to learn the advertised NDEF FileID,
then read that file; fall back to E104/0004. Tolerates a transient transmit
error (surfaced as a retryable status; the next tap re-reads).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-08-05 20:02:26 +02:00
commit ea736dfab0
2 changed files with 76 additions and 40 deletions

View file

@ -39,35 +39,27 @@ export function extractLnurlw(ndef: Buffer): string | null {
return m ? m[0] : null
}
/**
* Read the NDEF file of a Type-4 tag and return the extracted lnurlw, or null.
* `transmit(apdu, maxLen) => Buffer` including the trailing SW1 SW2.
*/
export async function readNdefLnurlw(
transmit: (apdu: Buffer, maxLen: number) => Promise<Buffer>
): Promise<string | null> {
const send = (bytes: number[]) => transmit(Buffer.from(bytes), 256)
const ok = (r: Buffer) => r.length >= 2 && r[r.length - 2] === 0x90 && r[r.length - 1] === 0x00
const swOk = (r: Buffer) => r.length >= 2 && r[r.length - 2] === 0x90 && r[r.length - 1] === 0x00
// 1) Select the NDEF Tag Application (AID D2760000850101).
if (!ok(await send([0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00]))) {
return null
}
// 2) Select the NDEF file (EF 0x0004).
if (!ok(await send([0x00, 0xa4, 0x00, 0x0c, 0x02, 0x00, 0x04]))) return null
// 3) Read the 2-byte NLEN header.
/** Select an EF by its 2-byte file id and read + parse its NDEF message. */
async function readNdefFile(
send: (bytes: number[]) => Promise<Buffer>,
fid: [number, number]
): Promise<string | null> {
if (!swOk(await send([0x00, 0xa4, 0x00, 0x0c, 0x02, fid[0], fid[1]]))) return null
// 2-byte NLEN header at offset 0.
const lenResp = await send([0x00, 0xb0, 0x00, 0x00, 0x02])
if (!ok(lenResp)) return null
if (!swOk(lenResp)) return null
const nlen = (lenResp[0] << 8) | lenResp[1]
if (nlen <= 0 || nlen > 0x2000) return null
// 4) Read the NDEF message (starts at offset 2), in <=250-byte chunks.
// NDEF message starts at offset 2; read in <=250-byte chunks.
const chunks: Buffer[] = []
let offset = 2
let remaining = nlen
while (remaining > 0) {
const toRead = Math.min(remaining, 0xfa)
const resp = await send([0x00, 0xb0, (offset >> 8) & 0xff, offset & 0xff, toRead])
if (!ok(resp)) break
if (!swOk(resp)) break
const data = resp.subarray(0, resp.length - 2)
if (data.length === 0) break
chunks.push(data)
@ -77,6 +69,42 @@ export async function readNdefLnurlw(
return extractLnurlw(Buffer.concat(chunks))
}
/**
* Read the NDEF of a Type-4 tag and return the extracted lnurlw, or null.
* `transmit(apdu, maxLen) => Buffer` including the trailing SW1 SW2.
*
* Select the NDEF Tag Application, read the Capability Container to learn the
* real NDEF FileID (NTAG424 Bolt Cards use E104, not the 0004 some tags use),
* then read that file. Falls back to E104/0004 if the CC read is unavailable.
*/
export async function readNdefLnurlw(
transmit: (apdu: Buffer, maxLen: number) => Promise<Buffer>
): Promise<string | null> {
const send = (bytes: number[]) => transmit(Buffer.from(bytes), 256)
// Select the NDEF Tag Application (AID D2760000850101).
if (!swOk(await send([0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00]))) {
return null
}
// Prefer the FileID advertised by the Capability Container (EF E103).
const candidates: Array<[number, number]> = []
if (swOk(await send([0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x03]))) {
const cc = await send([0x00, 0xb0, 0x00, 0x00, 0x0f])
// CC layout: …[07]=TLV tag 0x04, [08]=len, [09..10]=NDEF FileID.
if (swOk(cc) && cc.length >= 13 && cc[7] === 0x04) candidates.push([cc[9], cc[10]])
}
for (const fid of [[0xe1, 0x04] as [number, number], [0x00, 0x04] as [number, number]]) {
if (!candidates.some((c) => c[0] === fid[0] && c[1] === fid[1])) candidates.push(fid)
}
for (const fid of candidates) {
const found = await readNdefFile(send, fid)
if (found) return found
}
return null
}
let stopFn: (() => void) | null = null
/**