# `WebAuthnCredential`

A passkey a person registered (W3C WebAuthn), as `webauthn-rs` keeps one: its credential ID, its public key as a COSE_Key in CBOR, its signature counter, the transports it says it can be reached by, whether it may be and is backed up, the name the person gave it, and when it was made and last used. Never a biometric.

[Security](types.md#security); not ordered; since Jadpo 0.1.0.

## Make one

Build one from its fields, `WebAuthnCredential { credential_id: ...  public_key: ...  sign_count: ... }`.

## Fields

| Field | Type |
|---|---|
| `credential_id` | `Bytes` |
| `public_key` | `Bytes` |
| `sign_count` | `Int` |
| `transports` | `List<Text>` |
| `backup_eligible` | `Bool` |
| `backed_up` | `Bool` |
| `name` | `Text` |
| `registered_at` | [`Instant`](type-instant.md) |
| `last_used_at` | [`Instant`](type-instant.md)`?` |

## Text, JSON and storage

It has no single text form, so `text.from` and `text.parse` don't take it. JSON sends an object of its parts.

| JSON |
|---|
| `{"credential_id":"AAECAwQFBgcICQoLDA0ODw==","public_key":"pQECAyYgASFYIGXtpaEld8K66ClDf+M4cBoQqqN14btbXeEI3kOcCFUdIlggHlLtdXARY/f55A3fnzQbPcm6hgr34Mp8p+nuzQCE0Zw=","sign_count":0,"transports":["internal","hybrid"],"backup_eligible":true,"backed_up":true,"name":"Work phone","registered_at":"2026-10-18T09:00:00Z","last_used_at":"2026-10-18T09:05:00Z"}` |
| `{"credential_id":"8dA=","public_key":"pAEBAycgBiFYINdamAGCsQq31Uv+08lkBzoO4XLz2qYjJa8CGmj3B1Ea","sign_count":42,"transports":["usb","nfc"],"backup_eligible":false,"backed_up":false,"name":"Security key","registered_at":"2026-10-18T09:00:00Z"}` |

It is stored in 9 columns, each named for the field, `_` and its suffix. This is `{"credential_id":"8dA=","public_key":"pAEBAycgBiFYINdamAGCsQq31Uv+08lkBzoO4XLz2qYjJa8CGmj3B1Ea","sign_count":42,"transports":["usb","nfc"],"backup_eligible":false,"backed_up":false,"name":"Security key","registered_at":"2026-10-18T09:00:00Z"}`:

| Column | Type | Holds |
|---|---|---|
| `<field>_credential_id` | `bytea` | `E'\\xf1d0'::bytea` |
| `<field>_public_key` | `bytea` | `E'\\xa4010103272006215820d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'::bytea` |
| `<field>_sign_count` | `bigint` | `42` |
| `<field>_transports` | `text[]` | `ARRAY['usb', 'nfc']::text[]` |
| `<field>_backup_eligible` | `boolean` | `FALSE` |
| `<field>_backed_up` | `boolean` | `FALSE` |
| `<field>_name` | `text` | `'Security key'` |
| `<field>_registered_at` | `timestamptz(3)` | `'2026-10-18 09:00:00.000000+00'::timestamptz` |
| `<field>_last_used_at` | `timestamptz(3)` | `NULL::timestamptz` |

## Shown to people

`text.format(value, locale:)` shows it to people in their locale.

With `locale: Locale.en_gb`:

| Value | Shown |
|---|---|
| `{"credential_id":"AAECAwQFBgcICQoLDA0ODw==","public_key":"pQECAyYgASFYIGXtpaEld8K66ClDf+M4cBoQqqN14btbXeEI3kOcCFUdIlggHlLtdXARY/f55A3fnzQbPcm6hgr34Mp8p+nuzQCE0Zw=","sign_count":0,"transports":["internal","hybrid"],"backup_eligible":true,"backed_up":true,"name":"Work phone","registered_at":"2026-10-18T09:00:00Z","last_used_at":"2026-10-18T09:05:00Z"}` | `Work phone` |
| `{"credential_id":"8dA=","public_key":"pAEBAycgBiFYINdamAGCsQq31Uv+08lkBzoO4XLz2qYjJa8CGmj3B1Ea","sign_count":42,"transports":["usb","nfc"],"backup_eligible":false,"backed_up":false,"name":"Security key","registered_at":"2026-10-18T09:00:00Z"}` | `Security key` |

## Operations

It has the contract's own operations: `text.format`, `json.read`, `json.write` and `==`. It isn't ordered, so `<` and sorting don't take it. [Every standard type meets one contract](../topics/every-standard-type-meets-one-contract.md) says how they work.
