Skip to content
Get startedGetting started →

Getting started

What mera does:

The pipeline from a passkey to a signature. A sign-in ceremony returns 32 secret bytes, the same bytes on every sign-in. The app derives a private key from them. A signing session holds the key and produces signatures. mera owns every step except the derivation, which belongs to the app. Sign in mera 32 secret bytes mera Derive a key the app Signing session mera Signature

Prerequisites:

  • A secure context: HTTPS, or localhost during development.
  • An authenticator that supports the PRF extension. Authenticator support lists the combinations known to work. 1Password and iCloud Keychain are good first choices.
Terminal window
npm install @category-labs/mera

This walkthrough derives accounts with @scure/bip32 and @scure/bip39. Any derivation scheme works.

Terminal window
npm install @scure/bip32 @scure/bip39

createPasskeyWithPrfOutput creates a passkey and evaluates its PRF in one call.

import {
function createPasskeyWithPrfOutput({ rp, user, timeout, prfSalt, webAuthnClient, }: createPasskeyWithPrfOutput.Options): Promise<createPasskeyWithPrfOutput.Result>

Creates a discoverable, user-verified passkey that requires WebAuthn PRF support and returns the first PRF output.

@paramoptions - Passkey creation inputs.

@returnsCredential metadata and the first PRF output.

@remarks

Runs one creation ceremony and shows one user-verification prompt. On authenticators that do not evaluate PRF during creation, a fallback assertion evaluates the same salt and shows a second prompt.

WebAuthn challenges and the credential's user handle (user.id) are generated internally, 32 random bytes each. An authenticator overwrites a discoverable credential that has the same rp.id and user.id, so a fresh handle per call adds a passkey instead of replacing one.

The credential is requested with fixed parameters: ES256 or RS256 key types, attestation "none", a required resident key, and required user verification (getPasskeyPrfOutput explains the requirement).

Any failure after the creation ceremony completes leaves the passkey on the authenticator, but the thrown error does not carry its metadata.

@throwsMeraError with code PRF_UNAVAILABLE when the authenticator reports no PRF support and returns no create-time output, returns an output that is not 32 bytes, or returns none on the fallback ceremony.

@throwsMeraError with code INPUT_INVALID when an explicit prfSalt is not 32 bytes.

@throwsMeraError with code CRYPTO_UNAVAILABLE when crypto.getRandomValues is unavailable.

@throwsMeraError with code PASSKEY_OPERATION_FAILED when WebAuthn is unavailable, cancelled, or returns an unexpected credential.

createPasskeyWithPrfOutput
} from "@category-labs/mera";
// The relying party ID, the domain the passkey is bound to.
const
const rpId: "account.example.com"
rpId
= "account.example.com";
const {
const prfOutput: Uint8Array<ArrayBuffer>

First WebAuthn PRF output for prfSalt. Always 32 bytes.

prfOutput
} = await
function createPasskeyWithPrfOutput({ rp, user, timeout, prfSalt, webAuthnClient, }: createPasskeyWithPrfOutput.Options): Promise<createPasskeyWithPrfOutput.Result>

Creates a discoverable, user-verified passkey that requires WebAuthn PRF support and returns the first PRF output.

@paramoptions - Passkey creation inputs.

@returnsCredential metadata and the first PRF output.

@remarks

Runs one creation ceremony and shows one user-verification prompt. On authenticators that do not evaluate PRF during creation, a fallback assertion evaluates the same salt and shows a second prompt.

WebAuthn challenges and the credential's user handle (user.id) are generated internally, 32 random bytes each. An authenticator overwrites a discoverable credential that has the same rp.id and user.id, so a fresh handle per call adds a passkey instead of replacing one.

The credential is requested with fixed parameters: ES256 or RS256 key types, attestation "none", a required resident key, and required user verification (getPasskeyPrfOutput explains the requirement).

Any failure after the creation ceremony completes leaves the passkey on the authenticator, but the thrown error does not carry its metadata.

@throwsMeraError with code PRF_UNAVAILABLE when the authenticator reports no PRF support and returns no create-time output, returns an output that is not 32 bytes, or returns none on the fallback ceremony.

@throwsMeraError with code INPUT_INVALID when an explicit prfSalt is not 32 bytes.

@throwsMeraError with code CRYPTO_UNAVAILABLE when crypto.getRandomValues is unavailable.

@throwsMeraError with code PASSKEY_OPERATION_FAILED when WebAuthn is unavailable, cancelled, or returns an unexpected credential.

createPasskeyWithPrfOutput
({
rp: PasskeyRelyingParty

Relying party identity passed to WebAuthn. id is required so the fallback assertion can target the same relying party.

rp
: {
id: string

Relying party ID: the host the passkey is scoped to.

id
:
const rpId: "account.example.com"
rpId
,
name: string

Relying party name the authenticator may show.

name
: "Example" },
user: {
name: string;
displayName: string;
}

User identity passed to WebAuthn.

user
: {
name: string

User name displayed or stored by the authenticator.

name
: "account@example.com",
displayName: string

Human-readable display name for the authenticator UI.

displayName
: "Example account" },
});

mera uses a stable PRF salt for this call, so a later sign-in reproduces the same 32 bytes.

The PRF output is the root of every account. This walkthrough maps it through BIP-39 to a phrase and a seed, then derives keys with BIP-32.

import {
class HDKey

HDKey from BIP32

@paramopt - Node fields used to construct one HDKey instance.

@example

import { HDKey } from '@scure/bip32';
import { randomBytes } from '@noble/hashes/utils.js';
const seed = randomBytes(32);
const root = HDKey.fromMasterSeed(seed);
const account0 = root.derive("m/0/1'");
account0.publicKey;

HDKey
} from "@scure/bip32";
import {
function entropyToMnemonic(entropy: TArg<Uint8Array>, wordlist: string[]): string

Reversible: Converts raw entropy in form of byte array to mnemonic string.

@paramentropy - Byte array.

@paramwordlist - Imported wordlist for a specific language.

@returns12-24 words.

@throwsOn wrong argument types. TypeError

@throwsOn wrong argument ranges or values. RangeError

@example

Convert raw entropy into an English mnemonic.

import { entropyToMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english.js';
const ent = new Uint8Array([
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f
]);
const mnemonic = entropyToMnemonic(ent, wordlist);
// 'legal winner thank year wave sausage worth useful legal winner thank yellow'

entropyToMnemonic
,
function mnemonicToSeedSync(mnemonic: string, passphrase?: string): TRet<Uint8Array>

Irreversible: Uses KDF to derive 64 bytes of key data from mnemonic + optional password.

@parammnemonic - 12-24 words.

@parampassphrase - String that will additionally protect the key.

@returns64 bytes of key data.

@throwsIf the mnemonic shape is invalid. Error

@throwsOn wrong argument types. TypeError

@example

Derive a seed from a mnemonic with the sync PBKDF2 helper.

const mnem = 'legal winner thank year wave sausage worth useful legal winner thank yellow';
const seed = mnemonicToSeedSync(mnem, 'password');
// => new Uint8Array([...64 bytes])

mnemonicToSeedSync
} from "@scure/bip39";
import {
const wordlist: string[]

English BIP39 wordlist.

wordlist
} from "@scure/bip39/wordlists/english.js";
const
const firstEthereumAccountPath: "m/44'/60'/0'/0/0"
firstEthereumAccountPath
= "m/44'/60'/0'/0/0";
const
const mnemonic: string
mnemonic
=
function entropyToMnemonic(entropy: TArg<Uint8Array>, wordlist: string[]): string

Reversible: Converts raw entropy in form of byte array to mnemonic string.

@paramentropy - Byte array.

@paramwordlist - Imported wordlist for a specific language.

@returns12-24 words.

@throwsOn wrong argument types. TypeError

@throwsOn wrong argument ranges or values. RangeError

@example

Convert raw entropy into an English mnemonic.

import { entropyToMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english.js';
const ent = new Uint8Array([
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f
]);
const mnemonic = entropyToMnemonic(ent, wordlist);
// 'legal winner thank year wave sausage worth useful legal winner thank yellow'

entropyToMnemonic
(
const prfOutput: Uint8Array<ArrayBufferLike>
prfOutput
,
const wordlist: string[]

English BIP39 wordlist.

wordlist
);
const
const seed: Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>
seed
=
function mnemonicToSeedSync(mnemonic: string, passphrase?: string): TRet<Uint8Array>

Irreversible: Uses KDF to derive 64 bytes of key data from mnemonic + optional password.

@parammnemonic - 12-24 words.

@parampassphrase - String that will additionally protect the key.

@returns64 bytes of key data.

@throwsIf the mnemonic shape is invalid. Error

@throwsOn wrong argument types. TypeError

@example

Derive a seed from a mnemonic with the sync PBKDF2 helper.

const mnem = 'legal winner thank year wave sausage worth useful legal winner thank yellow';
const seed = mnemonicToSeedSync(mnem, 'password');
// => new Uint8Array([...64 bytes])

mnemonicToSeedSync
(
const mnemonic: string
mnemonic
);
const
const node: HDKey
node
=
class HDKey

HDKey from BIP32

@paramopt - Node fields used to construct one HDKey instance.

@example

import { HDKey } from '@scure/bip32';
import { randomBytes } from '@noble/hashes/utils.js';
const seed = randomBytes(32);
const root = HDKey.fromMasterSeed(seed);
const account0 = root.derive("m/0/1'");
account0.publicKey;

HDKey
.
HDKey.fromMasterSeed(seed: Uint8Array, versions?: Versions): HDKey
fromMasterSeed
(
const seed: Uint8Array<ArrayBufferLike> & Uint8Array<ArrayBuffer>
seed
).
HDKey.derive(path: string): HDKey
derive
(
const firstEthereumAccountPath: "m/44'/60'/0'/0/0"
firstEthereumAccountPath
);
if (
const node: HDKey
node
.
HDKey.privateKey: Uint8Array<ArrayBufferLike> | null
privateKey
=== null) throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("derivation produced no key");
import {
function createSecp256k1SigningSession({ privateKey, }: CreateSigningSessionOptions): Secp256k1SigningSession

Creates a signing session from a secp256k1 private key.

@paramoptions - Signing session inputs.

@returnsA live secp256k1 signing session.

@throwsMeraError with code INPUT_INVALID when privateKey is not a valid secp256k1 scalar.

createSecp256k1SigningSession
,
function getEvmAddress(publicKey: Uint8Array): EvmAddress

Derives the EIP-55 checksummed EVM address for a secp256k1 public key.

@parampublicKey - A compressed or uncompressed secp256k1 public key.

@returnsThe EIP-55 mixed-case checksummed EVM address.

@throwsMeraError with code INPUT_INVALID when publicKey is not valid secp256k1.

getEvmAddress
,
} from "@category-labs/mera";
const
const session: Secp256k1SigningSession
session
=
function createSecp256k1SigningSession({ privateKey, }: CreateSigningSessionOptions): Secp256k1SigningSession

Creates a signing session from a secp256k1 private key.

@paramoptions - Signing session inputs.

@returnsA live secp256k1 signing session.

@throwsMeraError with code INPUT_INVALID when privateKey is not a valid secp256k1 scalar.

createSecp256k1SigningSession
({
privateKey: Uint8Array<ArrayBufferLike>

Curve private key. Must be exactly 32 bytes and, for secp256k1, a valid scalar.

privateKey
:
const node: HDKey
node
.
HDKey.privateKey: Uint8Array<ArrayBufferLike>
privateKey
,
});
const
const address: `0x${string}`
address
=
function getEvmAddress(publicKey: Uint8Array): EvmAddress

Derives the EIP-55 checksummed EVM address for a secp256k1 public key.

@parampublicKey - A compressed or uncompressed secp256k1 public key.

@returnsThe EIP-55 mixed-case checksummed EVM address.

@throwsMeraError with code INPUT_INVALID when publicKey is not valid secp256k1.

getEvmAddress
(
const session: Secp256k1SigningSession
session
.
publicKey: Uint8Array<ArrayBuffer>

65-byte uncompressed secp256k1 public key for the session.

publicKey
);
const
const digest: Uint8Array<ArrayBuffer>
digest
= new
var Uint8Array: Uint8ArrayConstructor
new (length: number) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
(32);
const
const signature: Secp256k1Signature
signature
= await
const session: Secp256k1SigningSession
session
.
function signDigest(digest32: Uint8Array): Promise<Secp256k1Signature>

Signs a 32-byte digest without prehashing it.

@paramdigest32 - The digest to sign.

@returnsA compact secp256k1 ECDSA signature with its recovery ID.

@throwsMeraError with code INPUT_INVALID when digest32 is not 32 bytes.

@throwsMeraError with code SESSION_ENDED after end has been called.

signDigest
(
const digest: Uint8Array<ArrayBuffer>
digest
);
const session: Secp256k1SigningSession
session
.
function end(): void

Zeroes the session-owned private-key copy; later signing throws SESSION_ENDED.

end
();

A later visit reproduces the same account.

import {
function getPasskeyPrfOutput({ rpId, credential: allowCredential, prfSalt, timeout, webAuthnClient, }: getPasskeyPrfOutput.Options): Promise<getPasskeyPrfOutput.Result>

Requests a passkey PRF evaluation and returns the first output.

@paramoptions - Passkey PRF request inputs.

@returnsThe selected credential ID and first WebAuthn PRF output.

@remarks

Runs one assertion ceremony and shows one user-verification prompt.

The WebAuthn challenge is generated internally.

The default salt is sha256("mera.prf.salt.v1") and will not change across library versions. The PRF output is a deterministic function of the credential, rpId, and salt; a different salt yields an unrelated output.

The assertion requires user verification, and the requirement is not configurable. User verification is the authenticator's local check; the gesture depends on the platform (a biometric, a device PIN, or a password). Authenticators built on CTAP's hmac-secret keep two PRFs per credential, one for user-verified requests and one for the rest; WebAuthn exposes only the user-verified PRF and overrides a weaker userVerification setting when evaluating it, so a configurable setting could neither change the PRF output nor skip the check.

@seehttps://www.w3.org/TR/webauthn-3/#prf-extension WebAuthn: the PRF extension

@seehttps://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement WebAuthn: UserVerificationRequirement

@throwsMeraError with code PRF_UNAVAILABLE when the authenticator does not return a usable 32-byte PRF output.

@throwsMeraError with code INPUT_INVALID when an explicit prfSalt is not 32 bytes, or credential.credentialId is empty or not canonical base64url.

@throwsMeraError with code CRYPTO_UNAVAILABLE when crypto.getRandomValues is unavailable.

@throwsMeraError with code PASSKEY_OPERATION_FAILED when WebAuthn is unavailable, cancelled, or returns an unexpected credential.

getPasskeyPrfOutput
} from "@category-labs/mera";
const {
const prfOutput: Uint8Array<ArrayBuffer>

First PRF output from WebAuthn. Always 32 bytes.

prfOutput
} = await
function getPasskeyPrfOutput({ rpId, credential: allowCredential, prfSalt, timeout, webAuthnClient, }: getPasskeyPrfOutput.Options): Promise<getPasskeyPrfOutput.Result>

Requests a passkey PRF evaluation and returns the first output.

@paramoptions - Passkey PRF request inputs.

@returnsThe selected credential ID and first WebAuthn PRF output.

@remarks

Runs one assertion ceremony and shows one user-verification prompt.

The WebAuthn challenge is generated internally.

The default salt is sha256("mera.prf.salt.v1") and will not change across library versions. The PRF output is a deterministic function of the credential, rpId, and salt; a different salt yields an unrelated output.

The assertion requires user verification, and the requirement is not configurable. User verification is the authenticator's local check; the gesture depends on the platform (a biometric, a device PIN, or a password). Authenticators built on CTAP's hmac-secret keep two PRFs per credential, one for user-verified requests and one for the rest; WebAuthn exposes only the user-verified PRF and overrides a weaker userVerification setting when evaluating it, so a configurable setting could neither change the PRF output nor skip the check.

@seehttps://www.w3.org/TR/webauthn-3/#prf-extension WebAuthn: the PRF extension

@seehttps://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement WebAuthn: UserVerificationRequirement

@throwsMeraError with code PRF_UNAVAILABLE when the authenticator does not return a usable 32-byte PRF output.

@throwsMeraError with code INPUT_INVALID when an explicit prfSalt is not 32 bytes, or credential.credentialId is empty or not canonical base64url.

@throwsMeraError with code CRYPTO_UNAVAILABLE when crypto.getRandomValues is unavailable.

@throwsMeraError with code PASSKEY_OPERATION_FAILED when WebAuthn is unavailable, cancelled, or returns an unexpected credential.

getPasskeyPrfOutput
({
rpId: string

Relying party ID for the WebAuthn assertion.

rpId
,
});

Without credential, the browser offers any discoverable passkey it holds for the relying party. The Create passkey accounts recipe shows the app storing the credential ID at create time and passing it back to pin later sign-ins.