Nimiq cross-device adapter
Use @onmax/cross-device-nimiq with @onmax/better-auth-cross-device when a cross-device proof must come from a Nimiq key.
Do not use this adapter by itself as an auth system. The Better Auth cross-device plugin owns order storage, status transitions, sessions, and SSE.
Register the server adapter
import { createNimiqCrossDeviceAdapter } from '@onmax/cross-device-nimiq/server'
export const nimiqAdapter = createNimiqCrossDeviceAdapter({
id: 'nimiq',
appName: 'Nimiq Checkout',
})
Expected verification output: login and sign proofs expose publicKeyHex, signatureHex, and optional address. Transaction proofs expose publicKeyHex, address, txHash, and providerResultRaw.
Approve from a phone route
Use the approver inside the route opened by the QR claim URL.
import {
createNimiqBrowserHandoff,
createNimiqMiniAppApprover,
getNimiqApprovalEnvironment,
parseCrossDeviceClaimUrl,
} from '@onmax/cross-device-nimiq'
import { authClient } from '../../app/auth-client'
export async function approveFromCurrentUrl() {
const environment = getNimiqApprovalEnvironment()
if (!environment.available)
return createNimiqBrowserHandoff(window.location.href)
const claim = parseCrossDeviceClaimUrl(window.location.href)
const approver = createNimiqMiniAppApprover({
provider: environment.provider,
requestAddress: true,
})
return approver.approve(authClient.$fetch.bind(authClient), {
...claim,
endpointPrefix: '/cross-device',
})
}
Expected response:
{
"ok": true,
"orderId": "HrlSySstW-SR0pbCIY6Rzw",
"status": "approved"
}
Parse claim URLs
The default claim path is /cross-device/claim/{orderId}?token={claimToken}.
import { parseCrossDeviceClaimUrl } from '@onmax/cross-device-nimiq'
export const claim = parseCrossDeviceClaimUrl(
'https://pay.example.com/cross-device/claim/order_123?token=claim_456',
)
Expected result:
{
"orderId": "order_123",
"claimToken": "claim_456"
}
Approve a transaction order
Use resolveTransactionInput only for kind: 'transaction'. The approver signs the challenge, sends sendBasicTransactionWithData(), and submits the transaction artifact to /cross-device/approve.
import {
createNimiqMiniAppApprover,
parseCrossDeviceClaimUrl,
} from '@onmax/cross-device-nimiq'
import { authClient } from '../app/auth-client'
const approver = createNimiqMiniAppApprover({
resolveTransactionInput: async ({ challenge }) => {
if (challenge.kind !== 'transaction')
throw new Error('Unexpected non-transaction challenge')
return {
recipient: 'NQ07 0000 0000 0000 0000 0000 0000 0000 0000',
value: 1299,
data: `checkout:${challenge.orderId}`,
}
},
})
await approver.approve(authClient.$fetch.bind(authClient), {
...parseCrossDeviceClaimUrl(window.location.href),
endpointPrefix: '/cross-device',
})
Expected desktop finalization artifact:
{
"ok": true,
"status": "finalized",
"kind": "transaction",
"proofArtifact": {
"publicKeyHex": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"address": "NQ07 0000 0000 0000 0000 0000 0000 0000 0000",
"txHash": "0xtransaction",
"providerResultRaw": "0xtransaction"
}
}
Build the expected message in tests
Use message builders when tests or previews need the exact signed text.
import { buildNimiqCrossDeviceSignMessage } from '@onmax/cross-device-nimiq'
const message = buildNimiqCrossDeviceSignMessage({
appName: 'Nimiq Checkout',
origin: 'https://pay.example.com',
orderId: 'order_123',
challengeId: 'challenge_123',
nonce: 'nonce_123',
issuedAt: '2026-01-01T00:00:00.000Z',
exp: '2026-01-01T00:02:00.000Z',
title: 'Approve order order_123',
summary: 'Sign the checkout payload for EUR 12.99.',
payloadHash: '7f83b1657ff1fc53b92dc18148a1d65dfa135a13926d73c8042d3a274d49f2fa',
resource: '/orders/order_123',
})
Available builders:
buildNimiqCrossDeviceLoginMessage()buildNimiqCrossDeviceSignMessage()buildNimiqCrossDeviceTransactionMessage()buildNimiqCrossDeviceChallengeMessage()
Normalize proof fields
Use normalizers at trust boundaries before storing or comparing proof fields.
import { normalizeNimiqCrossDeviceProof } from '@onmax/cross-device-nimiq'
const proof = normalizeNimiqCrossDeviceProof({
publicKeyHex: request.publicKeyHex,
signatureHex: request.signatureHex,
address: request.address,
})
These helpers keep phone-side approval code small and server verification deterministic.