# web-authn

Passkey register / authenticate with HTML. Pair it with Quark to render the result.


```html
<section>
  <web-authn options-url="/api/webauthn/register/options" verify-url="/api/webauthn/register/verify"
    start-method="register">
    <form>
      <label>
        Username
        <input name="username" required autocomplete="username">
      </label>
      <button type="submit">Register passkey</button>
    </form>
    <output></output>
  </web-authn>
  <quark-sheet>
    web-authn[is-success] {
      $res: prop("provision").body;
      output {
        content: "verified: #{$res.verified}";
      }
    }
    web-authn[is-error] output {
      content: "Ceremony failed (this mock needs an authenticator).";
    }
  </quark-sheet>
</section>
```
 

```html
<web-authn start-method="register" options-url="/api/registration-options" verify-url="/api/users">
    <form>
        <button type="submit">One click sign up!</button>
    </form>
</web-authn>
```

## Features

- **Provides data** Use Quark to render the verify response
- **Full ceremony** Fetches options, runs the browser's WebAuthn prompt, then verifies — one element
- **Register or authenticate** `start-method` picks the ceremony
- **Submit command** `--submit` starts the ceremony programmatically — for forms outside the DOM subtree, or buttons outside the `<form>`
- **Chainable** `web-authn-success` fires like any `{tag}-success` event — chain a redirect or next step
- **Highly configurable** Headers, credentials, redirect, etc

## Installation


`@excom/web-authn` v0.1.5

```bash
pnpm add @excom/web-authn
```

```bash
npm install @excom/web-authn
```

```bash
yarn add @excom/web-authn
```

### Import

```ts
import "@excom/web-authn";
```



Depends on [`@simplewebauthn/browser`](https://simplewebauthn.dev/) for the actual WebAuthn calls (`startRegistration` / `startAuthentication`). You can use any server-side library to handle the WebAuthn requests, but it is recommended to use the counterpart library, [`@simplewebauthn/server`](https://simplewebauthn.dev/), since they seamlessly understand the same contract.

## Usage

```html
<web-authn options-url="/api/webauthn/register/options"
  verify-url="/api/webauthn/register/verify" start-method="register">
  <form>
    <input name="username" required>
    <button type="submit">Register passkey</button>
  </form>
</web-authn>
```

On submit: `options-url` is fetched for ceremony options, the browser's native passkey prompt runs (`@simplewebauthn/browser`), and the resulting credential is posted to `verify-url`. Use `start-method="authenticate"` for sign-in instead of registration.

Hook the lifecycle state with CSS:

```css
web-authn[is-loading] { /* show loading spinner */ }
web-authn[is-error]::before { content: "An error occurred." }
```

Or Quark:

```quark
web-authn[is-success] {
  $res: prop("provision").body;
  span { content: $res.verified; }
}
```

Chain a next step off success the same way you would for any `<super-form>` or `<provider-fetch>`:

```html
<event-handler listen-for="web-authn-success" fire-event="onboarding-step-complete">
  <web-authn options-url="/api/webauthn/register/options"
    verify-url="/api/webauthn/register/verify" start-method="register">
    <form><input name="username"><button type="submit">Register</button></form>
  </web-authn>
</event-handler>
```

### Examples

#### Authenticate


```html
<section>
  <web-authn options-url="/api/webauthn/authenticate/options" verify-url="/api/webauthn/authenticate/verify"
    start-method="authenticate">
    <form>
      <label>
        Username
        <input name="username" required autocomplete="username webauthn">
      </label>
      <button type="submit">Sign in with passkey</button>
    </form>
    <output></output>
  </web-authn>
  <quark-sheet>
    web-authn[is-success] {
      $res: prop("provision").body;
      output {
        content: "verified: #{$res.verified}";
      }
    }
    web-authn[is-error] output {
      content: "Ceremony failed (this mock needs an authenticator).";
    }
  </quark-sheet>
</section>
```


### API Reference


#### Attributes

| Name | Surface | Type | Default | Values | Description | Inherited from |
| --- | --- | --- | --- | --- | --- | --- |
| `form-ref` | option | `string` | `":scope form"` | `<CSS Selector>` | CSS selector for the `<form>` to intercept. Must be a descendant to be heard directly — point elsewhere and invoke the `--submit` command instead. |  |
| `start-method` | option | `string` |  | `"register"` \| `"authenticate"` | Which WebAuthn ceremony to run. |  |
| `verify-url` | option | `string` |  | `<URL>` | Endpoint that verifies the credential produced by the browser prompt (your server's `verifyRegistrationResponse` / `verifyAuthenticationResponse`). Receives the credential as the request body. |  |
| `has-body` | option | `boolean` |  |  | Force a request body even for methods that don't imply one (`GET` / `HEAD`). Already implied for `POST` / `PUT` / `PATCH`. | `@excom/fetchable-element` |
| `api-url` | option | `string` | `""` |  | Endpoint URL. When the request has no body, the JSON payload (from `form-ref` or custom `doFetch()` args) is merged in as query params instead. | `@excom/fetchable-element` |
| `api-method` | option | `string` | `"GET"` |  | HTTP method. Always uppercased before the request is sent. | `@excom/fetchable-element` |
| `header-accept` | option | `string` | `"application/json"` |  | `Accept` request header. | `@excom/fetchable-element` |
| `header-content-type` | option | `string` | `"application/json"` |  | `Content-Type` request header. Dropped entirely when the request has no body. | `@excom/fetchable-element` |
| `header-cache-control` | option | `string` |  |  | `Cache-Control` request header. Unset by default (browser default caching applies). | `@excom/fetchable-element` |
| `fetch-redirect` | option | `string` |  | `"follow"` \| `"error"` \| `"manual"` | `RequestInit.redirect` mode. Unset defers to the browser default (`follow`). | `@excom/fetchable-element` |
| `fetch-credentials` | option | `string` | `"include"` | `"omit"` \| `"same-origin"` \| `"include"` | `RequestInit.credentials` mode. | `@excom/fetchable-element` |
| `is-loading` | state | `boolean` |  |  | A request is currently in flight. | `@excom/fetchable-element` |
| `is-success` | state | `boolean` |  |  | The most recent request resolved successfully. Mutually exclusive with `is-loading` and `is-error`. | `@excom/fetchable-element` |
| `is-error` | state | `boolean` |  |  | The most recent request failed (status 400 or above, network error, or a thrown error other than `AbortError`). Fires with the `error` event. | `@excom/fetchable-element` |

#### Provision

| Name | Type | Description | Inherited from |
| --- | --- | --- | --- |
| `provision` | `FetchResponse` (`{ bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; }`) | Response payload on success, or error payload on failure. Success shape: `{ status, statusText, ok, headers, url, redirected, bodyUsed, type, body }`. Failure shape is either that same response shape (server responded with an error status) or `{ message, stack }` (request never completed). Not reflected as an attribute. | `@excom/fetchable-element` |

#### Fires

| Name | Type | Description | Inherited from |
| --- | --- | --- | --- |
| `web-authn-submit` | `WebAuthnSubmitEvent` (`CustomEvent & { type: "web-authn-submit"; detail: [url: string, requestInit: RequestInit]; bubbles: true; cancelable: true; composed: true }`) | Internal — dispatched once the browser ceremony (register or authenticate) resolves, just before the verify-url fetch runs. The credential is the request body. Default action calls `doFetch()`. |  |
| `web-authn-loading` | `FetchableLoadingEvent` (`CustomEvent & { type: "{tag}-loading"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched immediately before the request is sent. | `@excom/fetchable-element` |
| `web-authn-success` | `FetchableSuccessEvent` (`CustomEvent & { type: "{tag}-success"; detail: { bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; }; bubbles: true; cancelable: true; composed: true }`) | Dispatched when the request resolves successfully. `event.detail` is the parsed response (see `provision`). | `@excom/fetchable-element` |
| `web-authn-error` | `FetchableErrorEvent` (`CustomEvent & { type: "{tag}-error"; detail: { bodyUsed: boolean; headers: [string, string][]; ok: boolean; redirected: boolean; status: number; statusText: string; type: ResponseType; url: string; body: unknown; } \| { message: string; stack?: string }; bubbles: true; cancelable: true; composed: true }`) | Dispatched when the request fails — status 400 or above, network error, or a thrown error. `event.detail` is the error payload (see `provision`). Not dispatched for aborted requests. | `@excom/fetchable-element` |

#### Listens for

| Name | Type | Description |
| --- | --- | --- |
| `submit` | `WebAuthnNativeSubmitEvent` (`SubmitEvent & { type: "submit"; bubbles: true; cancelable: true; composed: false; }`) | The default action of the `<form>` matched by `form-ref`; prevented, then starts the ceremony. |

#### Commands

| Command | Action |
| --- | --- |
| `--submit` | Starts the ceremony programmatically (`<button command="--submit" commandfor="…">`) — the only option when `form-ref` points to a form that isn't a descendant. |

#### Default actions

| Event | Default behavior (unless preventDefault() is called) |
| --- | --- |
| `web-authn-submit` | Calls `doFetch([url, requestInit])` with the event's detail (the verify-url request). |

## Release notes

### 0.1.4 (2026-10-01)

- A failed ceremony or options request now clears is-loading and sets is-error with the message in provision

### 0.1.1 (2026-09-23)

- Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
