Skip to main content

Configuration

MwenClientConfig is the single configuration object passed to MwenClient (or MwenProvider). This page documents every field, its default, and guidance on what to set.


MwenClientConfig

interface MwenClientConfig {
clientId: string;
appName: string;
scopes?: readonly string[];
revocationPath?: string;
relayPath?: string;
verifyPath?: string;
sessionTtlMs?: number;

// Advanced — issued credential gating
vct?: string;
vctRequired?: boolean;
onRefreshError?: (error: Error) => void;
onCredentialNotFound?: (error: MwenCredentialNotFoundError) => AuthenticateResult | null | undefined | Promise<AuthenticateResult | null | undefined>;
onCredentialRevoked?: (error: MwenCredentialRevokedError) => AuthenticateResult | null | undefined | Promise<AuthenticateResult | null | undefined>;
}

Fields

clientId — required

clientId: string

Your application's identifier. This is the value used as:

  • The client_id in the auth request sent to the extension.
  • The HKDF info input for per-app key derivation — the user's did:jwk identifier is derived from this value.
  • The audience claim that server-side token verification checks against.

Use your app's origin domain — e.g. "shop.example.com" or "myapp.io". Do not include https:// or a trailing slash.

For local development, use "localhost:3001" (or whatever port you are running on). The user will get a different did:jwk for localhost:3001 vs. shop.example.com — keep this consistent in your environment config.

clientId: process.env.NEXT_PUBLIC_CLIENT_ID ?? 'localhost:3001',

Important: Changing clientId in production means all existing users will have a different did:jwk the next time they sign in. They will appear as new users to your database. Do not change this value after go-live.


appName — required

appName: string

Human-readable display name shown to the user on the consent screen inside the extension popup. Maximum 100 characters.

appName: 'Acme Wine & Spirits',

scopes — optional

scopes?: readonly string[]
// Default: ['openid', 'nickname', 'name', 'age']

The identity claims you want to request from the user. The extension consent screen will show only the scopes you list here.

Always include 'openid' as the first scope. It is required by the protocol and carries no claims itself.

ScopeClaims producedNotes
openid(none)Required
identitysub (did:jwk)Always included; listing it explicitly is optional
namegiven_name, family_name
nicknamenickname
ageage_over_18, age_over_13, age_over_21BBS+ ZK proofs — birth date never revealed
birthdatebirth_dateRaw date — user can hide
countrycountryISO 3166-1 alpha-2
emailemail, email_verified
phonephone_number, phone_verified
addressstreet_address, locality, region, postal_code

Request only the scopes your app actually needs. Requesting unnecessary claims reduces user trust and increases the probability of denial.

// Age-gated content: ask for pseudonymous ID + age proof only
scopes: ['openid', 'age'],

// E-commerce checkout: ask for name, email, and address
scopes: ['openid', 'name', 'email', 'address'],

revocationPath — optional

revocationPath?: string
// Default: '/revoked'

The path (relative to your app's origin) of the page that handles disconnect notifications from the wallet. When a user disconnects your app from their wallet, the extension opens a browser tab at ${origin}${revocationPath}#appIdentity=<did>&signature=<jws>.

Your /revoked page should:

  1. Strip the #fragment from the browser history.
  2. Extract appIdentity and signature from the fragment.
  3. POST them to your API to terminate the session.

See Handling Revocation for the full implementation.


relayPath — optional

relayPath?: string
// Default: '/api/mwen/relay'

The path to your app's relay endpoint. Used by requestAsyncAttestation() and included in client_metadata so the extension knows where to deliver async credential updates.

You must implement the relay endpoints for this to work. See Async Attestations & Relay.


verifyPath — optional

verifyPath?: string
// Default: '/api/verify'

The path to your app's verify endpoint. The extension sends the SD-JWT VP response to ${origin}${verifyPath} via direct_post. This endpoint receives, verifies, and processes the credential.

In the SDK's current Path A (extension) flow, verifyPath is included in the auth request metadata but the response comes back as a Promise — your /api/verify endpoint may still be called by the extension as a secondary delivery mechanism depending on extension version.


sessionTtlMs — optional

sessionTtlMs?: number
// Default: 86_400_000 (24 hours)

Maximum age in milliseconds of a VerifiedSession stored in localStorage. Sessions older than this are treated as expired and getSession() returns null.

This is separate from the credential's own expiresAt — both are checked. The shorter of the two takes precedence.

// 8 hours
sessionTtlMs: 8 * 60 * 60 * 1000,

vct — optional

vct?: string
// Default: '' (disabled)

A Verifiable Credential Type URI. When set, the wallet attempts to find a stored issued credential whose vct matches this URI and uses it as the basis of the auth response, instead of relying solely on self-attested profile data.

The vct URI is defined by the issuer. You can find the correct URI in the issuer's credential schema documentation — for mwen.io-hosted issuers, see the Credential Schemas reference.

vct: 'https://mwen.io/schemas/employee-identity/v1',

Only meaningful when your app is partnered with an issuer and you want to gate access or unlock features based on a specific issued credential. See Credential Gating for the full pattern.


vctRequired — optional

vctRequired?: boolean
// Default: true

Controls what happens when vct is set but no matching credential is found in the user's wallet, or the credential has been revoked.

  • true (default) — authentication fails hard. authenticate() throws MwenCredentialNotFoundError or MwenCredentialRevokedError. Use this when the credential is a strict access requirement.
  • false — the wallet falls back to self-attestation. authenticate() resolves normally. Use AuthenticateResult.presentedCredentials to determine whether the credential was actually presented.

Only meaningful when vct is set. Ignored otherwise.

See Credential Gating for usage examples.


onRefreshError — optional

onRefreshError?: (error: Error) => void

Called when the auto-refresh timer fails after one retry attempt. The auto-refresh runs at 75% of the credential's remaining lifetime — if both the initial attempt and the 5-second retry fail, this callback is invoked with the final error.

Use this to surface refresh failures to the UI without coupling MwenClient to a specific framework. Common patterns:

// Show a toast and prompt re-authentication
onRefreshError: (error) => {
console.error('Session refresh failed:', error);
showToast('Your session has expired. Please sign in again.');
},

If not provided, refresh errors are silently swallowed after the retry.


onCredentialNotFound — optional

onCredentialNotFound?: (
error: MwenCredentialNotFoundError
) => AuthenticateResult | null | undefined | Promise<AuthenticateResult | null | undefined>

Called when vct is set, vctRequired: true, and no stored credential matches the requested vct. The return value controls whether the error propagates:

  • Return an AuthenticateResult — used as the resolved value of authenticate(). The MwenCredentialNotFoundError is not thrown. Use this to complete an alternative flow inline (e.g. open the issuer enrolment portal and retry authentication after the user obtains the credential).
  • Return null, undefined, or nothing — the MwenCredentialNotFoundError is thrown as normal, giving the caller the opportunity to redirect or gate the UI.

The callback may be async.

onCredentialNotFound: async () => {
// Open your issuer's enrolment portal, wait for completion,
// then retry — the user should now have the credential.
await openEnrolmentFlow();
return client.authenticate();
},

onCredentialRevoked — optional

onCredentialRevoked?: (
error: MwenCredentialRevokedError
) => AuthenticateResult | null | undefined | Promise<AuthenticateResult | null | undefined>

Called when vct is set, vctRequired: true, and the matching credential has been revoked by the issuer. Follows the same return-value semantics as onCredentialNotFound.

onCredentialRevoked: async () => {
// Guide the user through re-issuance with the issuer,
// then retry authentication.
await openReissuanceFlow();
return client.authenticate();
},

Full example

const mwenConfig: MwenClientConfig = {
clientId: process.env.NEXT_PUBLIC_CLIENT_ID ?? 'localhost:3001',
appName: 'Acme Wine & Spirits',
scopes: ['openid', 'name', 'age', 'email'],
revocationPath: '/revoked',
relayPath: '/api/mwen/relay',
verifyPath: '/api/verify',
sessionTtlMs: 4 * 60 * 60 * 1000, // 4 hours
};

Advanced example — credential gating

This example requires users to hold a specific issued credential. If they do not, the onCredentialNotFound callback opens an enrolment flow before retrying.

const client = new MwenClient({
clientId: process.env.NEXT_PUBLIC_CLIENT_ID ?? 'localhost:3001',
appName: 'Acme Employee Portal',
scopes: ['openid', 'name', 'email'],
vct: 'https://mwen.io/schemas/employee-identity/v1',
vctRequired: true,
onCredentialNotFound: async () => {
// Redirect to the issuer's enrolment portal. After the user
// obtains their credential, retry authentication.
await openEnrolmentFlow('https://issuer.acme.com/enrol');
return client.authenticate();
},
onCredentialRevoked: async () => {
await openReissuanceFlow('https://issuer.acme.com/renew');
return client.authenticate();
},
onRefreshError: (error) => {
console.error('Auto-refresh failed:', error);
showToast('Session expired — please sign in again.');
},
});

See Credential Gating for the full pattern including soft gating and reading presentedCredentials.