API reference

Types & error codes

Every type and error code exported by the library.


Types

AppInfo

interface AppInfo {
  appId: string // Team ID + bundle ID, e.g. "TEAMID1234.com.example.app"
  developmentEnv?: boolean // Default false. true = development AAGUID.
}

Used by verifyAttestation(), verifyAssertion(), and withAssertion().

AttestationResult

interface AttestationResult {
  publicKeyPem: string // PEM-encoded SPKI P-256 public key
  receipt: Uint8Array // Apple receipt for fraud risk assessment
  signCount: number // Always 0 for attestations
}

Returned by verifyAttestation(). Persist all three fields.

AssertionResult

interface AssertionResult {
  signCount: number // New counter value — must be persisted
}

Returned by verifyAssertion().

VerifyAttestationOptions

interface VerifyAttestationOptions {
  checkDate?: Date // Override date for certificate validity checks
}

DeviceKey

type DeviceKey = {
  publicKeyPem: string // PEM from attestation result
  signCount: number // Last stored counter value
}

Used by withAssertion()'s getDeviceKey callback.

AssertionContext

type AssertionContext = {
  deviceId: string // Device identifier from extraction
  signCount: number // New counter value (already persisted)
  rawBody: Uint8Array // Raw request body bytes
}

Passed to your withAssertion() handler.

WithAssertionOptions

type WithAssertionOptions = {
  appId: string
  developmentEnv?: boolean
  getDeviceKey: (deviceId: string) => Promise<DeviceKey | null>
  updateSignCount: (deviceId: string, newSignCount: number) => Promise<void>
  extractAssertion?: ExtractAssertionFn
  onError?: (
    error: AssertionError,
    req: Request,
  ) => Response | Promise<Response>
}

ExtractAssertionFn

type ExtractAssertionFn = (req: Request) => Promise<{
  assertion: string
  deviceId: string
  clientData: Uint8Array
}>

Custom extraction callback for withAssertion(). The default reads from X-App-Attest-Assertion and X-App-Attest-Device-Id headers.


AttestationErrorCode

AttestationError is thrown by verifyAttestation(). It extends Error with a typed code property.

class AttestationError extends Error {
  readonly name: 'AttestationError'
  readonly code: AttestationErrorCode
}

INVALID_FORMAT

The attestation object couldn't be decoded (bad CBOR, bad base64) or the format field is not "apple-appattest".

Resolution: Verify the client is sending the raw attestation object from DCAppAttestService.attestKey(), base64-encoded.

INVALID_CERTIFICATE_CHAIN

The X.509 certificate chain failed validation against Apple's App Attestation Root CA. This includes expired certificates, broken chain linkage, or invalid signatures.

Resolution: Verify the device is using genuine Apple attestation. If testing, pass the checkDate option to account for expired test certificates.

NONCE_MISMATCH

The computed nonce (SHA-256(authData || challenge)) doesn't match the nonce in the leaf certificate.

Resolution: Ensure you're passing the exact same challenge value that was active when the client called attestKey(). Check that the challenge hasn't been URL-encoded or otherwise transformed.

RP_ID_MISMATCH

SHA-256(appInfo.appId) doesn't match the rpIdHash in the authenticator data.

Resolution: Check that appInfo.appId is your full Team ID + bundle ID (e.g., "TEAMID1234.com.example.app"). This must match what the client used.

KEY_ID_MISMATCH

The keyId doesn't match the public key hash or credential ID in the attestation.

Resolution: Ensure the client sends the keyId from generateKey() without modification.

INVALID_COUNTER

signCount is not 0 in the attestation.

Resolution: This attestation object has been used before or is malformed. Request a fresh attestation from the client.

INVALID_AAGUID

The AAGUID in the authenticator data doesn't match the expected environment.

Resolution: Check appInfo.developmentEnv. Production devices use "appattest" + 7 null bytes; development builds use "appattestdevelop".


AssertionErrorCode

AssertionError is thrown by verifyAssertion() and withAssertion(). It extends Error with a typed code property.

class AssertionError extends Error {
  readonly name: 'AssertionError'
  readonly code: AssertionErrorCode
}

INVALID_FORMAT

The assertion couldn't be decoded (bad CBOR, bad base64), the authenticator data is malformed, the DER signature is invalid, or the PEM public key can't be imported.

Resolution: Verify the client is sending the raw assertion from generateAssertion(), base64-encoded. Check that the stored public key PEM is valid.

RP_ID_MISMATCH

SHA-256(appInfo.appId) doesn't match the rpIdHash in the assertion's authenticator data.

Resolution: Same as attestation — check appInfo.appId.

COUNTER_NOT_INCREMENTED

The assertion's signCount is not strictly greater than previousSignCount.

Resolution: This may indicate a replay attack. Verify your counter persistence — if the counter wasn't updated after the last successful assertion, valid requests will be rejected.

SIGNATURE_INVALID

ECDSA signature verification failed.

Resolution: The assertion was signed by a different key than expected, or the clientData bytes don't match what was signed. Ensure you're passing the raw request body as clientData.

DEVICE_NOT_FOUND

withAssertion() only. The getDeviceKey callback returned null.

Resolution: The device hasn't been attested yet, or the device ID is incorrect. The client should re-attest.

INTERNAL_ERROR

withAssertion() only. The getDeviceKey or updateSignCount callback threw an error.

Resolution: Check your database connection and storage logic. The original error is available via error.cause.


Error handling pattern

import {
  verifyAttestation,
  AttestationError,
  AttestationErrorCode,
} from '@bradford-tech/supabase-integrity-attest'

try {
  const result = await verifyAttestation(appInfo, keyId, challenge, attestation)
  // Success — store result
} catch (error) {
  if (error instanceof AttestationError) {
    switch (error.code) {
      case AttestationErrorCode.NONCE_MISMATCH:
      case AttestationErrorCode.RP_ID_MISMATCH:
      case AttestationErrorCode.KEY_ID_MISMATCH:
        // Client error — return 401
        break
      case AttestationErrorCode.INVALID_FORMAT:
        // Bad request — return 400
        break
      default:
        // Unexpected — log and return 500
        console.error('Attestation failed:', error.code, error.message)
    }
  }
}
Previous
withAssertion() reference