@trymellon/js 1.7.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.MD CHANGED
@@ -9,95 +9,67 @@ Official **TryMellon** SDK. Add **Passkeys / WebAuthn** to your app in minutes.
9
9
 
10
10
  ---
11
11
 
12
+ ## Table of Contents
13
+
14
+ - [Why TryMellon?](#why-trymellon)
15
+ - [Installation](#installation)
16
+ - [Requirements](#requirements)
17
+ - [Credentials](#where-to-get-credentials)
18
+ - [Quickstart (5 minutes)](#quickstart-5-minutes)
19
+ - [Framework Support](#framework-support--entry-points)
20
+ - [React](#react)
21
+ - [Vue](#vue)
22
+ - [Angular](#angular)
23
+ - [Vanilla JS / Svelte](#vanilla-javascript)
24
+ - [Web Components](#web-components)
25
+ - [Initialization](#initialization)
26
+ - [Sandbox Mode](#sandbox--development-mode)
27
+ - [Basic Usage](#basic-usage)
28
+ - [Check WebAuthn Support](#check-webauthn-support)
29
+ - [Passkey Registration](#passkey-registration)
30
+ - [Passkey Authentication](#passkey-authentication)
31
+ - [Conditional UI (Passkey Autofill)](#conditional-ui-passkey-autofill)
32
+ - [Validate Session](#validate-session)
33
+ - [Get Client Status](#get-client-status)
34
+ - [Event System](#event-system)
35
+ - [Email Fallback (OTP)](#email-fallback-otp)
36
+ - [Cross-Device Authentication (QR Login)](#cross-device-authentication-qr-login)
37
+ - [Account Recovery](#account-recovery)
38
+ - [Programmatic Onboarding](#programmatic-onboarding)
39
+ - [Result Type](#result-type)
40
+ - [Error Handling](#error-handling)
41
+ - [Cancelling Operations](#cancelling-operations)
42
+ - [Logging](#logging)
43
+ - [Client Backend](#client-backend)
44
+ - [Security](#security)
45
+ - [CSP and SRI](#security-csp-and-sri)
46
+ - [Compatibility](#compatibility)
47
+ - [Telemetry (opt-in)](#telemetry-opt-in)
48
+ - [For AI Agents](#for-ai-agents)
49
+ - [Additional Documentation](#additional-documentation)
50
+ - [License](#license)
51
+
52
+ ---
53
+
12
54
  ## Why TryMellon?
13
55
 
14
56
  Authentication is hard. WebAuthn (Passkeys) is even harder.
15
57
  We built **TryMellon** to solve the complexity without locking you in.
16
58
 
17
- * **Zero Logic:** You don't handle cryptography, public keys, or challenges.
59
+ * **Zero Dependencies:** No runtime dependencies. No bloat.
18
60
  * **Zero Lock-in:** You own your users. We just verify them.
19
- * **Zero Friction:** Drop-in SDK that "just works" (Cross-browser handling included).
20
-
21
- ### What you get:
22
- * ✅ **Passkeys First:** Biometric login (FaceID, TouchID, Windows Hello).
23
- * ✅ **Email Fallback:** Magic OTPs when biometrics aren't available.
24
- * ✅ **Cross-Device Auth:** Login on desktop by scanning a QR with mobile.
25
- * ✅ **Framework Agnostic:** Works with React, Vue, Angular, Svelte, or Vanilla.
26
- * ✅ **Type-Safe:** First-class TypeScript support (`Result` pattern).
27
-
28
- ---
29
-
30
- ## 🤖 For AI Agents
31
-
32
- Are you an LLM or autonomous agent trying to use TryMellon? We built a dedicated "Vending Machine" for you.
33
-
34
- **Goal:** Get your own `client_id` and `client_secret` instantly to start creating users via API.
35
- **Limit:** The "Free AI Tier" allows up to **15 users** for testing and small demos.
36
-
37
- ### 1. Get Credentials
38
- Send a POST request to our onboarding endpoint. No captcha, no email verification required.
39
-
40
- ```bash
41
- curl -X POST https://api.trymellonauth.com/onboarding/ai \
42
- -H "Content-Type: application/json" \
43
- -d '{ "name": "My Agent Name" }'
44
- ```
45
-
46
- **Response:**
47
- ```json
48
- {
49
- "tenantId": "...",
50
- "clientId": "cli_...",
51
- "clientSecret": "sec_...",
52
- "publishableKey": "cli_...",
53
- "applicationId": "...",
54
- "limits": { "maxUsers": 15, "maxApps": 1 }
55
- }
56
- ```
57
-
58
- Use `publishableKey` (same as `clientId`) in the SDK: `TryMellon.create({ appId: '...', publishableKey: response.publishableKey })`. Use `applicationId` to update allowed origins (step 4) if you use passkeys or cross-device from your agent's domain.
59
-
60
- ### 2. Authenticate
61
- Use the `clientId` and `clientSecret` to get an access token via OAuth2 Client Credentials flow.
62
-
63
- ```bash
64
- curl -X POST https://api.trymellonauth.com/oauth/token \
65
- -H "Content-Type: application/json" \
66
- -d '{
67
- "client_id": "cli_...",
68
- "client_secret": "sec_...",
69
- "grant_type": "client_credentials"
70
- }'
71
- ```
72
-
73
- **Response:**
74
- ```json
75
- { "access_token": "ey...", "token_type": "Bearer", "expires_in": 3600 }
76
- ```
77
-
78
- ### 3. Create Users
79
- Use the `access_token` to create users programmatically.
80
-
81
- ```bash
82
- curl -X POST https://api.trymellonauth.com/v1/users \
83
- -H "Authorization: Bearer <access_token>" \
84
- -H "Content-Type: application/json" \
85
- -d '{ "external_user_id": "user_123" }'
86
- ```
61
+ * **Zero Friction:** Drop-in SDK that "just works" (cross-browser handling included).
87
62
 
88
- ### 4. Update allowed origins (required for passkeys / cross-device)
89
- If your agent uses passkeys or cross-device auth from a real domain, the API must allow that origin. Call PATCH with the `applicationId` from step 1:
63
+ **What you get:**
90
64
 
91
- ```bash
92
- curl -X PATCH https://api.trymellonauth.com/v1/applications/<applicationId> \
93
- -H "Authorization: Bearer <access_token>" \
94
- -H "Content-Type: application/json" \
95
- -d '{ "allowed_origins": ["https://your-agent-domain.com"] }'
96
- ```
97
-
98
- Without this, requests from your agent's origin will get 404 when resolving the application. You can keep `https://agent.local` in the list for local testing.
99
-
100
- **Cursor / agent roles (Corsaria, Paladin):** When working in the TryMellon monorepo, orchestration roles are defined in `.cursor/skills/agent-orchestrator/` (agents.json, AGENTS.md).
65
+ * Passkeys first -- biometric login (FaceID, TouchID, Windows Hello)
66
+ * Email fallback -- magic OTPs when biometrics are not available
67
+ * Cross-device auth -- login on desktop by scanning a QR with mobile
68
+ * Account recovery -- OTP-based credential reset with new passkey registration
69
+ * Web Components -- drop-in `<trymellon-auth>` and `<trymellon-auth-modal>` elements
70
+ * Framework adapters -- React hooks, Vue composables, Angular service
71
+ * Type-safe -- first-class TypeScript with `Result` pattern, branded types, strict mode
72
+ * Sandbox mode -- test the full integration flow without a backend or WebAuthn hardware
101
73
 
102
74
  ---
103
75
 
@@ -107,58 +79,91 @@ Without this, requests from your agent's origin will get 404 when resolving the
107
79
  npm install @trymellon/js
108
80
  ```
109
81
 
110
- That's it. No peer dependencies. No bloat.
82
+ No peer dependencies. No bloat.
111
83
 
112
84
  ---
113
85
 
114
86
  ## Requirements
115
87
 
116
- * Node 18+, browsers, and Edge runtimes (Cloudflare Workers, Vercel Edge); no Node-specific APIs (Buffer/crypto module).
88
+ * Node 18+ / browsers / Edge runtimes (Cloudflare Workers, Vercel Edge)
117
89
  * Browser with WebAuthn support (Chrome, Safari, Firefox, Edge)
118
90
  * HTTPS (required except on `localhost`)
119
91
  * A TryMellon Application with the correct origin configured
120
92
 
121
- **Isomorphic / Edge-safe:** The runtime bundle uses only `globalThis.crypto`, `btoa`/`atob`, and `ArrayBuffer`/`Uint8Array` (no Node `Buffer` or `node:crypto`). Safe to import in SSR on Edge. Build-time scripts (e.g. `scripts/sri.js`) may use Node APIs; they do not affect the published bundle.
93
+ **Isomorphic / Edge-safe:** The runtime bundle uses only `globalThis.crypto`, `btoa`/`atob`, and `ArrayBuffer`/`Uint8Array` -- no Node `Buffer` or `node:crypto`. Safe to import in SSR on Edge. Build-time scripts (e.g. `scripts/sri.js`) may use Node APIs; they do not affect the published bundle.
122
94
 
123
95
  ---
124
96
 
125
97
  ## Where to get credentials
126
98
 
127
- You need two values to initialize the SDK. Get both from the [TryMellon dashboard](https://trymellon-landing.pages.dev/dashboard) after creating an application and adding your app's origin.
99
+ You need two values to initialize the SDK. Get both from the [TryMellon dashboard](https://trymellon-landing.pages.dev/dashboard) after creating an application and adding your apps origin.
128
100
 
129
101
  | SDK parameter | What it is | Where to find it |
130
102
  |------------------|-------------------------|-------------------------------------------|
131
- | `appId` | **Application ID** (UUID) | Dashboard Your app **App ID** |
132
- | `publishableKey` | **Client ID** (starts with `cli_`) | Dashboard Your app **Client ID** (same value is your publishable key) |
103
+ | `appId` | **Application ID** (UUID) | Dashboard > Your app > **App ID** |
104
+ | `publishableKey` | **Client ID** (starts with `cli_`) | Dashboard > Your app > **Client ID** (same value is your publishable key) |
133
105
 
134
- The API identifies your app by `publishableKey` (sent as `Authorization: Bearer <publishableKey>`) and the request **Origin**. Ensure your app's origin is allowed in the dashboard for that application.
106
+ The API identifies your app by `publishableKey` (sent as `Authorization: Bearer <publishableKey>`) and the request **Origin**. Ensure your apps origin is allowed in the dashboard for that application.
107
+
108
+ ---
109
+
110
+ ## Quickstart (5 minutes)
111
+
112
+ ```typescript
113
+ import { TryMellon } from ‘@trymellon/js’
114
+
115
+ // 1. Initialize (Factory Pattern -- returns Result, never throws)
116
+ const clientResult = TryMellon.create({
117
+ appId: ‘your-app-id-uuid’, // App ID (UUID) from Dashboard
118
+ publishableKey: ‘cli_xxxx’, // Client ID from Dashboard
119
+ })
120
+
121
+ if (!clientResult.ok) {
122
+ console.error(‘Invalid config:’, clientResult.error.message)
123
+ throw clientResult.error
124
+ }
125
+
126
+ const client = clientResult.value
127
+
128
+ // 2. Register a passkey
129
+ const registerResult = await client.register({ externalUserId: ‘user_123’ })
130
+ if (registerResult.ok) {
131
+ console.log(‘Session token:’, registerResult.value.sessionToken)
132
+ }
133
+
134
+ // 3. Authenticate
135
+ const authResult = await client.authenticate({ externalUserId: ‘user_123’ })
136
+ if (authResult.ok) {
137
+ console.log(‘Session token:’, authResult.value.sessionToken)
138
+ }
139
+ ```
135
140
 
136
141
  ---
137
142
 
138
143
  ## Framework support & entry points
139
144
 
140
- The SDK is **framework-agnostic**. Use the main entry for Vanilla JS, Svelte, or any environment; use framework-specific entry points for React, Vue, and Angular to get hooks/services and tree-shaking.
145
+ The SDK is **framework-agnostic**. Use the main entry for Vanilla JS, Svelte, or any environment; use framework-specific entry points for React, Vue, and Angular to get idiomatic hooks/services and tree-shaking.
141
146
 
142
- | Entry point | Use case | Exports |
143
- |-------------|----------|---------|
144
- | `@trymellon/js` | Vanilla JS, Svelte, Node, or any bundler | `TryMellon`, `TryMellon.isSupported()`, `Result`, `ok`, `err`, `isTryMellonError`, types |
147
+ | Entry point | Use case | Key exports |
148
+ |-------------|----------|-------------|
149
+ | `@trymellon/js` | Vanilla JS, Svelte, Node, any bundler | `TryMellon`, `TryMellon.isSupported()`, `Result`, `ok`, `err`, `isTryMellonError`, types |
145
150
  | `@trymellon/js/react` | React 18+ | `TryMellonProvider`, `useTryMellon`, `useRegister`, `useAuthenticate` |
146
151
  | `@trymellon/js/vue` | Vue 3 (Composition API) | `provideTryMellon`, `useTryMellon`, `useRegister`, `useAuthenticate`, `TryMellonKey` |
147
152
  | `@trymellon/js/angular` | Angular (standalone or NgModule) | `TryMellonService`, `provideTryMellonConfig`, `TRYMELLON_CONFIG` |
153
+ | `@trymellon/js/ui` | Web Components | `TryMellonAuthElement`, `TryMellonAuthModalElement` |
148
154
 
149
155
  **Runtime:** ESM and CJS supported. For UMD/script tag use `@trymellon/js/umd` or the built `dist/index.global.js` (exposes `window.TryMellon`).
150
156
 
151
157
  ### React
152
158
 
153
- ```bash
154
- npm install @trymellon/js
155
- ```
156
-
157
159
  ```tsx
158
- import { TryMellon } from '@trymellon/js'
159
- import { TryMellonProvider, useTryMellon, useRegister, useAuthenticate } from '@trymellon/js/react'
160
+ import { TryMellon } from ‘@trymellon/js
161
+ import { TryMellonProvider, useRegister, useAuthenticate } from ‘@trymellon/js/react
160
162
 
161
- const clientResult = TryMellon.create({ appId: 'your-app-id-uuid', publishableKey: 'cli_xxxx' }) // Get these from Dashboard → Your app → App ID and Client ID
163
+ const clientResult = TryMellon.create({
164
+ appId: ‘your-app-id-uuid’,
165
+ publishableKey: ‘cli_xxxx’,
166
+ })
162
167
  if (!clientResult.ok) throw clientResult.error
163
168
  const client = clientResult.value
164
169
 
@@ -174,67 +179,63 @@ function LoginForm() {
174
179
  const { execute: register, loading } = useRegister()
175
180
  const { execute: authenticate } = useAuthenticate()
176
181
 
177
- const onRegister = () => register({ externalUserId: 'user_123' })
178
- const onLogin = () => authenticate({ externalUserId: 'user_123' })
179
-
180
182
  return (
181
183
  <>
182
- <button onClick={onRegister} disabled={loading}>Register passkey</button>
183
- <button onClick={onLogin} disabled={loading}>Sign in</button>
184
+ <button onClick={() => register({ externalUserId: ‘user_123’ })} disabled={loading}>
185
+ Register passkey
186
+ </button>
187
+ <button onClick={() => authenticate({ externalUserId: ‘user_123’ })} disabled={loading}>
188
+ Sign in
189
+ </button>
184
190
  </>
185
191
  )
186
192
  }
187
193
  ```
188
194
 
189
- - **Requirements:** React 18+. Create a `TryMellon` instance (e.g. at app root), wrap your app (or auth subtree) with `TryMellonProvider` passing `client={client}`; then use `useTryMellon()`, `useRegister()`, and `useAuthenticate()` in children.
195
+ **Requirements:** React 18+. Wrap your app (or auth subtree) with `TryMellonProvider` passing `client={client}`; then use `useTryMellon()`, `useRegister()`, and `useAuthenticate()` in children.
190
196
 
191
197
  ### Vue
192
198
 
193
- ```bash
194
- npm install @trymellon/js
195
- ```
196
-
197
199
  ```vue
198
200
  <script setup lang="ts">
199
- import { TryMellon } from '@trymellon/js'
200
- import { provideTryMellon, useTryMellon, useRegister, useAuthenticate } from '@trymellon/js/vue'
201
+ import { TryMellon } from ‘@trymellon/js
202
+ import { provideTryMellon, useRegister, useAuthenticate } from ‘@trymellon/js/vue
201
203
 
202
- const clientResult = TryMellon.create({ appId: 'your-app-id-uuid', publishableKey: 'cli_xxxx' }) // Get these from Dashboard → Your app → App ID and Client ID
204
+ const clientResult = TryMellon.create({
205
+ appId: ‘your-app-id-uuid’,
206
+ publishableKey: ‘cli_xxxx’,
207
+ })
203
208
  if (!clientResult.ok) throw clientResult.error
204
- const client = clientResult.value
205
- provideTryMellon(client)
209
+ provideTryMellon(clientResult.value)
206
210
 
207
211
  const { execute: register, loading } = useRegister()
208
212
  const { execute: authenticate } = useAuthenticate()
209
-
210
- const onRegister = () => register({ externalUserId: 'user_123' })
211
- const onLogin = () => authenticate({ externalUserId: 'user_123' })
212
213
  </script>
213
214
 
214
215
  <template>
215
- <button @click="onRegister" :disabled="loading">Register passkey</button>
216
- <button @click="onLogin" :disabled="loading">Sign in</button>
216
+ <button @click="register({ externalUserId: ‘user_123’ })" :disabled="loading">
217
+ Register passkey
218
+ </button>
219
+ <button @click="authenticate({ externalUserId: ‘user_123’ })" :disabled="loading">
220
+ Sign in
221
+ </button>
217
222
  </template>
218
223
  ```
219
224
 
220
- - **Requirements:** Vue 3 with Composition API. Create a `TryMellon` instance and call `provideTryMellon(client)` once (e.g. in root or a parent); then use `useTryMellon()`, `useRegister()`, and `useAuthenticate()` in components.
225
+ **Requirements:** Vue 3 with Composition API. Call `provideTryMellon(client)` once in a parent component; then use `useTryMellon()`, `useRegister()`, and `useAuthenticate()` in descendants.
221
226
 
222
227
  ### Angular
223
228
 
224
- ```bash
225
- npm install @trymellon/js
226
- ```
227
-
228
229
  In your app config (e.g. `app.config.ts` or root module):
229
230
 
230
231
  ```typescript
231
- import { provideTryMellonConfig } from '@trymellon/js/angular'
232
+ import { provideTryMellonConfig } from ‘@trymellon/js/angular
232
233
 
233
234
  export const appConfig = {
234
235
  providers: [
235
236
  provideTryMellonConfig({
236
- appId: 'your-app-id-uuid',
237
- publishableKey: 'cli_xxxx',
237
+ appId: your-app-id-uuid’,
238
+ publishableKey: cli_xxxx’,
238
239
  }),
239
240
  ],
240
241
  }
@@ -243,9 +244,10 @@ export const appConfig = {
243
244
  In a component or service:
244
245
 
245
246
  ```typescript
246
- import { TryMellonService } from '@trymellon/js/angular'
247
+ import { inject, Injectable } from ‘@angular/core’
248
+ import { TryMellonService } from ‘@trymellon/js/angular’
247
249
 
248
- @Injectable({ providedIn: 'root' })
250
+ @Injectable({ providedIn: root })
249
251
  export class AuthService {
250
252
  private tryMellon = inject(TryMellonService)
251
253
 
@@ -259,130 +261,185 @@ export class AuthService {
259
261
  }
260
262
  ```
261
263
 
262
- - **Requirements:** Angular (standalone or NgModule). Add `provideTryMellonConfig(config)` to your app `providers`; inject `TryMellonService` and use `.client` for `register()`, `authenticate()`, and other methods.
264
+ **Requirements:** Angular (standalone or NgModule). Add `provideTryMellonConfig(config)` to your app `providers`; inject `TryMellonService` and use `.client` for all SDK methods.
263
265
 
264
266
  ### Vanilla JavaScript
265
267
 
266
- Use the main entry and instantiate `TryMellon` directly (see Quickstart below). Works in any ES module or CJS environment. For script-tag usage, use the UMD build: `@trymellon/js/umd` or `dist/index.global.js`; the global is `window.TryMellon`.
268
+ Use the main entry and instantiate `TryMellon` directly (see [Quickstart](#quickstart-5-minutes)). Works in any ES module or CJS environment. For script-tag usage, use the UMD build: `@trymellon/js/umd` or `dist/index.global.js` (exposes `window.TryMellon`).
267
269
 
268
270
  ### Svelte (and other frameworks)
269
271
 
270
- No dedicated adapter. Use the **main entry** `@trymellon/js`: create one `TryMellon` instance (e.g. in a module or store) and call `register()` / `authenticate()` from your components. Same API as Quickstart; no provider required.
272
+ No dedicated adapter needed. Use the **main entry** `@trymellon/js`: create one `TryMellon` instance (e.g. in a module or store) and call `register()` / `authenticate()` from your components. Same API as the Quickstart.
271
273
 
272
274
  ---
273
275
 
274
- ## Quickstart (5 minutes)
276
+ ## Web Components
275
277
 
276
- ```bash
277
- npm install @trymellon/js
278
- ```
278
+ The SDK ships two custom elements via the `@trymellon/js/ui` entry point. Import the entry once to auto-register both elements. **Tag canónico:** `<trymellon-auth>` (botón + modal por defecto). **Escape hatch:** `trigger-only="true"` — solo emite `mellon:open-request`; el host monta/abre el modal. Ver [Web Components](./documentation/WEB-COMPONENTS.md).
279
279
 
280
280
  ```typescript
281
- import { TryMellon } from '@trymellon/js'
281
+ import ‘@trymellon/js/ui’
282
+ ```
282
283
 
283
- // 1. Initialize safely (Factory Pattern)
284
- const clientResult = TryMellon.create({
285
- appId: 'your-app-id-uuid', // App ID (UUID) from Dashboard → Your app
286
- publishableKey: 'cli_xxxx', // Client ID from Dashboard → Your app
287
- })
284
+ ### `<trymellon-auth>` (tag canónico)
288
285
 
289
- if (!clientResult.ok) {
290
- console.error('Invalid config:', clientResult.error.message);
291
- throw clientResult.error;
292
- }
286
+ Inline authentication widget: **one tag** for both button and modal. Handles environment detection, passkey flows, and fallback states internally.
293
287
 
294
- const client = clientResult.value;
288
+ - **Option A (default):** Button + internal modal. Click emits `mellon:open-request` and opens the modal inside the same WC.
289
+ - **Option B (escape hatch):** Set `trigger-only="true"`. The WC only renders the button and emits `mellon:open-request` on click; it does **not** create or mount a modal. The host must listen for the event and open a separate `<trymellon-auth-modal>`. See [Web Components](./documentation/WEB-COMPONENTS.md).
295
290
 
296
- // 2. Register passkey (camelCase recommended in options)
297
- const registerResult = await client.register({ externalUserId: 'user_123' })
298
- if (registerResult.ok) {
299
- console.log('Session token:', registerResult.value.sessionToken)
300
- }
291
+ ```html
292
+ <trymellon-auth
293
+ app-id="your-app-id-uuid"
294
+ publishable-key="cli_xxxx"
295
+ mode="auto"
296
+ external-user-id="user_123"
297
+ theme="light"
298
+ ></trymellon-auth>
299
+ ```
301
300
 
302
- // 3. Authenticate
303
- const authResult = await client.authenticate({ externalUserId: 'user_123' })
304
- if (authResult.ok) {
305
- console.log('Session token:', authResult.value.sessionToken)
306
- }
301
+ **Attributes:**
302
+
303
+ | Attribute | Type | Description |
304
+ |-----------|------|-------------|
305
+ | `app-id` | `string` | Application ID (UUID) |
306
+ | `publishable-key` | `string` | Client ID (`cli_xxxx`) |
307
+ | `mode` | `’auto’` \| `’login’` \| `’register’` | Auth mode. `auto` defaults to login. |
308
+ | `external-user-id` | `string` | User identifier for the auth flow |
309
+ | `theme` | `’light’` \| `’dark’` | Visual theme |
310
+ | `action` | `'open-modal'` \| `'direct-auth'` | Default: botón + modal. `direct-auth`: ceremonia directa sin modal. |
311
+ | `trigger-only` | `'true'` \| `'false'` | Si `true`, solo emite `mellon:open-request`; el host monta/abre `<trymellon-auth-modal>`. |
312
+
313
+ ### `<trymellon-auth-modal>`
314
+
315
+ Modal-based authentication component with tab switching, onboarding support, and open/close lifecycle.
316
+
317
+ ```html
318
+ <trymellon-auth-modal
319
+ app-id="your-app-id-uuid"
320
+ publishable-key="cli_xxxx"
321
+ open="false"
322
+ tab="login"
323
+ theme="light"
324
+ ></trymellon-auth-modal>
325
+ ```
326
+
327
+ **Attributes:**
328
+
329
+ | Attribute | Type | Description |
330
+ |-----------|------|-------------|
331
+ | `app-id` | `string` | Application ID (UUID) |
332
+ | `publishable-key` | `string` | Client ID (`cli_xxxx`) |
333
+ | `open` | `’true’` \| `’false’` | Controls modal visibility |
334
+ | `tab` | `’login’` \| `’register’` | Active tab |
335
+ | `tab-labels` | `string` | Custom tab labels, comma-separated (e.g. `"Sign Up,Sign In"`) |
336
+ | `mode` | `’modal’` \| `’inline’` | Display mode |
337
+ | `theme` | `’light’` \| `’dark’` | Visual theme |
338
+ | `session-id` | `string` | Onboarding session ID |
339
+ | `onboarding-url` | `string` | Onboarding URL for external completion |
340
+ | `is-mobile-override` | `’true’` \| `’false’` | Override mobile detection |
341
+ | `fallback-type` | `’email’` \| `’qr’` | Preferred fallback channel |
342
+
343
+ **JavaScript API:**
344
+
345
+ ```typescript
346
+ const modal = document.querySelector(‘trymellon-auth-modal’)
347
+ modal.open = true
348
+ modal.tab = ‘register’
349
+ modal.theme = ‘dark’
350
+ modal.reset() // Reset to idle state
307
351
  ```
308
352
 
353
+ **Custom Events:**
354
+
355
+ | Event | Detail | Description |
356
+ |-------|--------|-------------|
357
+ | `mellon:open` | `{}` | Modal opened |
358
+ | `mellon:close` | `{ reason: ‘success’ \| ‘cancel’ \| ‘error’ \| ‘user’ }` | Modal closed |
359
+ | `mellon:start` | `{ operation }` | Auth operation started |
360
+ | `mellon:success` | `{ token, user }` | Auth succeeded |
361
+ | `mellon:error` | `{ error }` | Auth error |
362
+ | `mellon:cancelled` | `{}` | Auth cancelled |
363
+ | `mellon:fallback` | `{ operation? }` | Fallback triggered |
364
+ | `mellon:tab-change` | `{ tab }` | Tab switched |
365
+
309
366
  ---
310
367
 
311
368
  ## Initialization
312
369
 
313
- Prefer **TryMellon.create()** so invalid config returns a Result instead of throwing:
370
+ Prefer **`TryMellon.create()`** so invalid config returns a Result instead of throwing:
314
371
 
315
372
  ```typescript
316
- import { TryMellon } from '@trymellon/js'
373
+ import { TryMellon } from ‘@trymellon/js
317
374
 
318
375
  const clientResult = TryMellon.create({
319
- appId: 'your-app-id-uuid',
320
- publishableKey: 'cli_xxxx',
321
- apiBaseUrl: 'https://api.trymellonauth.com', // optional
322
- timeoutMs: 30000,
323
- maxRetries: 3,
324
- retryDelayMs: 1000,
376
+ appId: your-app-id-uuid’,
377
+ publishableKey: cli_xxxx’,
378
+ apiBaseUrl: https://api.trymellonauth.com’, // optional
379
+ timeoutMs: 30000, // 1000 - 300000
380
+ maxRetries: 3, // 0 - 10
381
+ retryDelayMs: 1000, // 100 - 10000
325
382
  })
326
383
 
327
384
  if (!clientResult.ok) {
328
- console.error('Invalid config:', clientResult.error.message)
385
+ console.error(Invalid config:’, clientResult.error.message)
329
386
  throw clientResult.error
330
387
  }
331
388
  const client = clientResult.value
332
389
  ```
333
390
 
334
- > [!TIP]
335
- > For static, validated config you can use `new TryMellon(config)`; it throws on invalid config. Prefer `TryMellon.create()` for user- or env-driven config so you can handle errors without try/catch.
391
+ > **Tip:** For static, validated config you can use `new TryMellon(config)` (throws on invalid config). Prefer `TryMellon.create()` for user- or env-driven config so you can handle errors without try/catch.
336
392
 
337
393
  **Configuration options:**
338
394
 
339
- - `appId` (required): Your Application ID (UUID). Get it from Dashboard → Your app → App ID. Sent in header `X-App-Id`. The API identifies your app by `publishableKey` and Origin; `appId` is sent for consistency and future use.
340
- - `publishableKey` (required): Your Client ID (value starting with `cli_`). Get it from Dashboard → Your app → Client ID. Sent in header `Authorization: Bearer <publishableKey>`. The API uses this plus the request Origin to resolve your application.
341
- - `apiBaseUrl` (optional): API base URL. Default: `'https://api.trymellonauth.com'`
342
- - `timeoutMs` (optional): HTTP request timeout. Range: `1000` - `300000`. Default: `30000`
343
- - `maxRetries` (optional): Retries for network errors. Range: `0` - `10`. Default: `3`
344
- - `retryDelayMs` (optional): Delay between retries. Range: `100` - `10000`. Default: `1000`
345
- - `logger` (optional): `Logger` implementation for request correlation (e.g. `requestId` in logs and `error.details`).
346
-
347
- **Register/authenticate options:** Both `externalUserId` (camelCase, recommended) and `external_user_id` (snake_case) are accepted. The SDK normalizes to snake_case for the API.
395
+ | Option | Required | Default | Description |
396
+ |--------|----------|---------|-------------|
397
+ | `appId` | Yes | -- | Application ID (UUID). Sent as `X-App-Id` header. |
398
+ | `publishableKey` | Yes | -- | Client ID (`cli_xxxx`). Sent as `Authorization: Bearer`. |
399
+ | `apiBaseUrl` | No | `’https://api.trymellonauth.com’` | API base URL. |
400
+ | `timeoutMs` | No | `30000` | HTTP request timeout in ms (`1000`-`300000`). |
401
+ | `maxRetries` | No | `3` | Retries for network/5xx errors (`0`-`10`). |
402
+ | `retryDelayMs` | No | `1000` | Initial delay between retries in ms (`100`-`10000`). Exponential backoff. |
403
+ | `logger` | No | -- | `Logger` implementation for request correlation. |
404
+ | `sandbox` | No | `false` | Enable sandbox mode (no API/WebAuthn calls). |
405
+ | `sandboxToken` | No | `SANDBOX_SESSION_TOKEN` | Custom token for sandbox mode. |
406
+ | `origin` | No | `window.location.origin` | Override origin header. Useful for SSR or Node environments. |
407
+ | `enableTelemetry` | No | `false` | Send anonymous telemetry (event + latency). |
408
+
409
+ **Register/authenticate options:** Both `externalUserId` (camelCase, recommended) and `external_user_id` (snake_case, deprecated) are accepted. The SDK normalizes to snake_case for the API.
348
410
 
349
411
  ---
350
412
 
351
413
  ## Sandbox / development mode
352
414
 
353
- For local development or testing the integration flow without a real backend or WebAuthn (e.g. no passkey hardware), enable **sandbox mode**. With `sandbox: true`, `register()` and `authenticate()` return immediately with a fixed session token and a demo userno API or WebAuthn calls.
354
-
355
- **Configuration:**
356
-
357
- - `sandbox` (optional): Set to `true` to enable sandbox mode.
358
- - `sandboxToken` (optional): Custom token to return. If not set, the exported constant `SANDBOX_SESSION_TOKEN` is used.
359
-
360
- **Exported constant:** Import `SANDBOX_SESSION_TOKEN` from `@trymellon/js` so your backend can recognize the sandbox token in development. **Your backend MUST NOT accept this token in production**—only in development. See [Backend validation](https://trymellon.com/docs/backend-validation) for the hook contract.
361
-
362
- **Example:**
415
+ For local development or testing the integration flow without a real backend or WebAuthn hardware, enable **sandbox mode**. With `sandbox: true`, `register()` and `authenticate()` return immediately with a fixed session token and a demo user -- no API or WebAuthn calls.
363
416
 
364
417
  ```typescript
365
- import { TryMellon, SANDBOX_SESSION_TOKEN } from '@trymellon/js'
418
+ import { TryMellon, SANDBOX_SESSION_TOKEN } from ‘@trymellon/js
366
419
 
367
420
  const clientResult = TryMellon.create({
368
421
  sandbox: true,
369
- appId: 'sandbox',
370
- publishableKey: 'sandbox',
422
+ appId: sandbox’,
423
+ publishableKey: sandbox’,
371
424
  })
372
425
  if (!clientResult.ok) throw clientResult.error
373
426
  const client = clientResult.value
374
427
 
375
- const result = await client.authenticate({ externalUserId: 'dev_user_1' })
428
+ const result = await client.authenticate({ externalUserId: dev_user_1 })
376
429
  if (result.ok) {
377
430
  // result.value.sessionToken === SANDBOX_SESSION_TOKEN
378
- await fetch('/api/login', {
379
- method: 'POST',
380
- headers: { 'Content-Type': 'application/json' },
431
+ await fetch(‘/api/login’, {
432
+ method: POST’,
433
+ headers: { Content-Type’: application/json },
381
434
  body: JSON.stringify({ sessionToken: result.value.sessionToken }),
382
435
  })
383
436
  }
384
437
  ```
385
438
 
439
+ **Exported constant:** Import `SANDBOX_SESSION_TOKEN` from `@trymellon/js` so your backend can recognize the sandbox token in development. **Your backend MUST NOT accept this token in production.**
440
+
441
+ `validateSession()` also supports sandbox mode -- if called with the sandbox token, it returns a mock valid response.
442
+
386
443
  ---
387
444
 
388
445
  ## Basic usage
@@ -401,7 +458,10 @@ if (TryMellon.isSupported()) {
401
458
 
402
459
  ```typescript
403
460
  const result = await client.register({
404
- externalUserId: 'user_123' // recommended: camelCase. external_user_id also accepted
461
+ externalUserId: user_123’,
462
+ authenticatorType: ‘platform’, // optional: ‘platform’ or ‘cross-platform’
463
+ successUrl: ‘https://app.example.com/welcome’, // optional: redirect URL
464
+ signal: controller.signal, // optional: AbortSignal
405
465
  })
406
466
 
407
467
  if (!result.ok) {
@@ -409,95 +469,79 @@ if (!result.ok) {
409
469
  return
410
470
  }
411
471
 
412
- // Send session_token to your backend
413
- await fetch('/api/login', {
414
- method: 'POST',
415
- headers: { 'Content-Type': 'application/json' },
416
- body: JSON.stringify({
417
- sessionToken: result.value.sessionToken
418
- })
472
+ // result.value contains:
473
+ // {
474
+ // success: true,
475
+ // credentialId: string,
476
+ // status: string,
477
+ // sessionToken: string,
478
+ // user: { userId, externalUserId, email?, metadata? },
479
+ // redirectUrl?: string -- present when successUrl was allowed
480
+ // }
481
+
482
+ await fetch(‘/api/login’, {
483
+ method: ‘POST’,
484
+ headers: { ‘Content-Type’: ‘application/json’ },
485
+ body: JSON.stringify({ sessionToken: result.value.sessionToken }),
419
486
  })
420
487
  ```
421
488
 
422
- **Registration options:**
423
-
424
- - `externalUserId` or `external_user_id` (one required): Unique user ID in your system. camelCase recommended.
425
- - `authenticatorType` (optional): `'platform'` (device) or `'cross-platform'` (USB/NFC)
426
- - `signal` (optional): `AbortSignal` to cancel the operation
427
-
428
- **Response:**
429
-
430
- ```typescript
431
- {
432
- success: true,
433
- credentialId: string,
434
- status: string,
435
- sessionToken: string,
436
- user: {
437
- userId: string,
438
- externalUserId: string,
439
- email?: string,
440
- metadata?: Record<string, unknown>
441
- }
442
- }
443
- ```
444
-
445
489
  ### Passkey authentication
446
490
 
447
491
  ```typescript
448
492
  const result = await client.authenticate({
449
- external_user_id: 'user_123',
450
- hint: 'user@example.com' // optional, improves UX
493
+ externalUserId: user_123’,
494
+ hint: user@example.com’, // optional: improves passkey selection UX
495
+ successUrl: ‘https://app.example.com/dashboard’, // optional
496
+ signal: controller.signal, // optional
451
497
  })
452
498
 
453
- // Send to backend
454
- await fetch('/api/login', {
455
- method: 'POST',
456
- headers: { 'Content-Type': 'application/json' },
457
- body: JSON.stringify({
458
- sessionToken: result.value.sessionToken
499
+ if (result.ok) {
500
+ // result.value contains:
501
+ // {
502
+ // authenticated: boolean,
503
+ // sessionToken: string,
504
+ // user: { userId, externalUserId, email?, metadata? },
505
+ // signals?: { userVerification?, backupEligible?, backupStatus? },
506
+ // redirectUrl?: string
507
+ // }
508
+
509
+ await fetch(‘/api/login’, {
510
+ method: ‘POST’,
511
+ headers: { ‘Content-Type’: ‘application/json’ },
512
+ body: JSON.stringify({ sessionToken: result.value.sessionToken }),
459
513
  })
460
- })
514
+ }
461
515
  ```
462
516
 
463
- **Authentication options:**
517
+ ### Conditional UI (passkey autofill)
464
518
 
465
- - `externalUserId` or `external_user_id` (one required): User ID. camelCase recommended.
466
- - `hint` (optional): Hint for the passkey (e.g. email)
467
- - `signal` (optional): AbortSignal to cancel
468
-
469
- **Response:**
519
+ Use the `mediation` option to integrate with the browser’s passkey autofill (conditional UI). This lets users authenticate by selecting a passkey from the browser’s autofill prompt on an input field.
470
520
 
471
521
  ```typescript
472
- {
473
- authenticated: boolean,
474
- sessionToken: string,
475
- user: {
476
- userId: string,
477
- externalUserId: string,
478
- email?: string,
479
- metadata?: Record<string, unknown>
480
- },
481
- signals: {
482
- userVerification?: boolean,
483
- backupEligible?: boolean,
484
- backupStatus?: boolean
485
- }
522
+ const result = await client.authenticate({
523
+ mediation: ‘conditional’,
524
+ })
525
+
526
+ if (result.ok) {
527
+ console.log(‘Authenticated via autofill:’, result.value.sessionToken)
486
528
  }
487
529
  ```
488
530
 
531
+ Supported `mediation` values: `’optional’` (default), `’conditional’` (autofill), `’required’`.
532
+
489
533
  ### Validate session
490
534
 
491
535
  ```typescript
492
- const validationResult = await client.validateSession('session_token_123')
536
+ const validationResult = await client.validateSession(session_token_123)
493
537
 
494
538
  if (validationResult.ok && validationResult.value.valid) {
495
539
  const v = validationResult.value
496
- console.log('User:', v.external_user_id, 'Tenant:', v.tenant_id, 'App:', v.app_id)
540
+ console.log(User:’, v.external_user_id, Tenant:’, v.tenant_id, App:’, v.app_id)
497
541
  }
498
542
  ```
499
543
 
500
- **Response:**
544
+ **Response shape:**
501
545
 
502
546
  ```typescript
503
547
  {
@@ -515,61 +559,64 @@ if (validationResult.ok && validationResult.value.valid) {
515
559
  const status = await client.getStatus()
516
560
 
517
561
  if (status.isPasskeySupported) {
518
- console.log('Passkeys available')
562
+ console.log(Passkeys available)
519
563
  if (status.platformAuthenticatorAvailable) {
520
- console.log('Platform authenticator available')
564
+ console.log(Platform authenticator available)
521
565
  }
522
566
  } else {
523
- console.log('Use fallback')
567
+ console.log(Use fallback:’, status.recommendedFlow)
524
568
  }
525
569
  ```
526
570
 
527
- **Response:**
571
+ ### SDK version
528
572
 
529
573
  ```typescript
530
- {
531
- isPasskeySupported: boolean,
532
- platformAuthenticatorAvailable: boolean,
533
- recommendedFlow: 'passkey' | 'fallback'
534
- }
574
+ console.log(‘SDK version:’, client.version()) // e.g. "1.7.6"
535
575
  ```
536
576
 
537
577
  ---
538
578
 
539
579
  ## Event system
540
580
 
541
- The SDK emits events for better UX and analytics:
581
+ The SDK emits lifecycle events for UX feedback and analytics:
542
582
 
543
583
  ```typescript
544
- // Subscribe to events
545
- client.on('start', (payload) => {
546
- console.log('Operation started:', payload.operation) // 'register' | 'authenticate'
584
+ client.on(‘start’, (payload) => {
585
+ console.log(‘Operation started:’, payload.operation) // ‘register’ | ‘authenticate’
547
586
  showSpinner()
548
587
  })
549
588
 
550
- client.on('success', (payload) => {
551
- console.log('Operation succeeded:', payload.operation)
589
+ client.on(success’, (payload) => {
590
+ // payload.token -- session token
591
+ // payload.user -- user info (userId, externalUserId, email)
552
592
  hideSpinner()
553
593
  showSuccessMessage()
554
594
  })
555
595
 
556
- client.on('error', (payload) => {
557
- console.error('Error:', payload.error)
596
+ client.on(error’, (payload) => {
597
+ console.error(Error:’, payload.error)
558
598
  hideSpinner()
559
599
  showError(payload.error.message)
560
600
  })
561
601
 
602
+ client.on(‘cancelled’, (payload) => {
603
+ console.log(‘Cancelled:’, payload.operation)
604
+ hideSpinner()
605
+ })
606
+
562
607
  // Unsubscribe
563
- const unsubscribe = client.on('start', handler)
608
+ const unsubscribe = client.on(start’, handler)
564
609
  unsubscribe()
565
610
  ```
566
611
 
567
612
  **Available events:**
568
613
 
569
- - `'start'`: Operation started (`register` or `authenticate`)
570
- - `'success'`: Operation completed successfully
571
- - `'error'`: Error during the operation
572
- - `'cancelled'`: Operation cancelled (future)
614
+ | Event | Payload | Description |
615
+ |-------|---------|-------------|
616
+ | `’start’` | `{ type, operation, nonce? }` | Operation started |
617
+ | `’success’` | `{ type, operation, token, user?, nonce? }` | Operation completed successfully |
618
+ | `’error’` | `{ type, error, operation?, nonce? }` | Error during operation |
619
+ | `’cancelled’` | `{ type, operation, nonce? }` | Operation cancelled by user or abort |
573
620
 
574
621
  ---
575
622
 
@@ -580,40 +627,42 @@ When WebAuthn is not available, you can use the email fallback. All methods retu
580
627
  ```typescript
581
628
  // 1. Send OTP code by email
582
629
  const startResult = await client.fallback.email.start({
583
- userId: 'user_123',
584
- email: 'user@example.com'
630
+ userId: user_123’,
631
+ email: user@example.com’,
585
632
  })
586
633
  if (!startResult.ok) { console.error(startResult.error); return }
587
634
 
588
635
  // 2. Ask user for the code
589
- const code = prompt('Enter the code sent by email:')
636
+ const code = prompt(Enter the code sent by email:’)
590
637
 
591
638
  // 3. Verify code
592
639
  const verifyResult = await client.fallback.email.verify({
593
- userId: 'user_123',
594
- code: code
640
+ userId: user_123’,
641
+ code: code,
642
+ successUrl: ‘https://app.example.com/dashboard’, // optional
595
643
  })
596
644
  if (!verifyResult.ok) { console.error(verifyResult.error); return }
597
645
 
598
- // 4. Send sessionToken to backend
599
- await fetch('/api/login', {
600
- method: 'POST',
601
- body: JSON.stringify({ sessionToken: verifyResult.value.sessionToken })
646
+ // verifyResult.value: { sessionToken: string, redirectUrl?: string }
647
+ await fetch(‘/api/login’, {
648
+ method: POST’,
649
+ body: JSON.stringify({ sessionToken: verifyResult.value.sessionToken }),
602
650
  })
603
651
  ```
604
652
 
605
- **Full flow with fallback:**
653
+ **Full flow with automatic fallback:**
606
654
 
607
655
  ```typescript
608
- async function authenticateUser(userId: string) {
656
+ async function authenticateUser(userId: string, email: string) {
609
657
  if (!TryMellon.isSupported()) {
610
- return await authenticateWithEmail(userId, userId)
658
+ return await authenticateWithEmail(userId, email)
611
659
  }
612
660
 
613
661
  const authResult = await client.authenticate({ externalUserId: userId })
614
662
  if (authResult.ok) return authResult
615
- if (authResult.error.code === 'PASSKEY_NOT_FOUND' || authResult.error.code === 'NOT_SUPPORTED') {
616
- return await authenticateWithEmail(userId, userId)
663
+
664
+ if (authResult.error.code === ‘PASSKEY_NOT_FOUND’ || authResult.error.code === ‘NOT_SUPPORTED’) {
665
+ return await authenticateWithEmail(userId, email)
617
666
  }
618
667
  return authResult
619
668
  }
@@ -621,8 +670,8 @@ async function authenticateUser(userId: string) {
621
670
  async function authenticateWithEmail(userId: string, email: string) {
622
671
  const startRes = await client.fallback.email.start({ userId, email })
623
672
  if (!startRes.ok) return startRes
624
- const code = prompt('Enter the code sent by email:')
625
- return await client.fallback.email.verify({ userId, code })
673
+ const code = prompt(Enter the code sent by email:’)
674
+ return await client.fallback.email.verify({ userId, code: code! })
626
675
  }
627
676
  ```
628
677
 
@@ -630,112 +679,199 @@ async function authenticateWithEmail(userId: string, email: string) {
630
679
 
631
680
  ## Cross-Device Authentication (QR Login)
632
681
 
633
- Enable users to sign in on a desktop device by scanning a QR code with their mobile phone (where their passkey is stored).
682
+ Enable users to sign in or register on a desktop device by scanning a QR code with their mobile phone (where their passkey is stored or will be created).
634
683
 
635
- **About `qr_url` and the mobile app:** The API returns `qr_url` in the form `{baseUrl}/mobile-auth?session_id={session_id}` (where `session_id` is a UUID). Your **mobile web app** must be deployed and its URL/origin **allowed in the TryMellon dashboard** for that application. The user scans the QR, opens that URL (which includes the `session_id`), and your mobile app must call `approve(session_id)` to complete the flow. If the mobile app’s origin is not allowed, the API will reject the request.
684
+ **About `qr_url` and the mobile app:** The API returns `qr_url` in the form `{baseUrl}/mobile-auth?session_id={session_id}`. Your **mobile web app** must be deployed and its URL/origin **allowed in the TryMellon dashboard**. The user scans the QR, opens that URL, and your mobile app calls `approve(session_id)` to complete the flow.
636
685
 
637
- ### 1. Desktop: Initialize and Show QR
686
+ ### Desktop: Authentication via QR
638
687
 
639
688
  ```typescript
640
689
  // Initialize session
641
690
  const initResult = await client.auth.crossDevice.init()
642
691
  if (!initResult.ok) { console.error(initResult.error); return }
643
692
 
644
- const { session_id, qr_url } = initResult.value
693
+ const { session_id, qr_url, polling_token } = initResult.value
645
694
 
646
- // Show QR code with `qr_url`
695
+ // Show QR code with qr_url (use any QR library)
647
696
  renderQrCode(qr_url)
648
697
 
649
698
  // Start polling for approval
650
- // Use AbortController to cancel if user leaves the page
651
699
  const controller = new AbortController()
652
-
653
700
  const pollResult = await client.auth.crossDevice.waitForSession(
654
- session_id,
655
- controller.signal
701
+ session_id,
702
+ controller.signal,
703
+ polling_token, // pass the polling token for secure polling
656
704
  )
657
705
 
658
706
  if (!pollResult.ok) {
659
- if (pollResult.error.code === 'TIMEOUT') {
660
- showError('QR code expired')
661
- }
707
+ if (pollResult.error.code === TIMEOUT) showError(‘QR code expired’)
662
708
  return
663
709
  }
664
710
 
665
- // Success!
666
- console.log('Session token:', pollResult.value.sessionToken)
711
+ console.log(‘Session token:’, pollResult.value.session_token)
712
+ ```
713
+
714
+ ### Desktop: Registration via QR
715
+
716
+ For anonymous registration you can omit `externalUserId` in `initRegistration()`; the backend will generate an id.
717
+
718
+ ```typescript
719
+ const initResult = await client.auth.crossDevice.initRegistration({
720
+ externalUserId: ‘new_user_123’,
721
+ })
722
+ if (!initResult.ok) { console.error(initResult.error); return }
723
+
724
+ const { session_id, qr_url, polling_token } = initResult.value
725
+ renderQrCode(qr_url)
726
+
727
+ const pollResult = await client.auth.crossDevice.waitForSession(
728
+ session_id,
729
+ controller.signal,
730
+ polling_token,
731
+ )
667
732
  ```
668
733
 
669
- ### 2. Mobile: Approve Login
734
+ ### Mobile: Approve
670
735
 
671
- When the user scans the QR code, your mobile web app should handle the URL (containing `session_id`) and call `approve`:
736
+ When the user scans the QR code, your mobile web app handles the URL and calls `approve`. The SDK auto-detects whether the session is for authentication or registration and runs the appropriate WebAuthn ceremony.
672
737
 
673
738
  ```typescript
674
- // Extract session_id from URL query params
675
739
  const sessionId = getSessionIdFromUrl()
676
740
 
677
- // Trigger WebAuthn flow on mobile
678
741
  const approveResult = await client.auth.crossDevice.approve(sessionId)
679
742
 
680
743
  if (approveResult.ok) {
681
- showSuccess('Process complete! Check your desktop.')
744
+ showSuccess(‘Done! Check your desktop.’)
682
745
  } else {
683
- showError('Failed to approve login: ' + approveResult.error.message)
746
+ showError(Failed: + approveResult.error.message)
747
+ }
748
+ ```
749
+
750
+ ### Get Session Context (advanced)
751
+
752
+ ```typescript
753
+ const contextResult = await client.auth.crossDevice.getContext(sessionId)
754
+ if (contextResult.ok) {
755
+ // contextResult.value.type === ‘auth’ | ‘registration’
756
+ // contextResult.value.options -- WebAuthn challenge options
757
+ // contextResult.value.application_name -- app name for display
684
758
  }
685
759
  ```
686
760
 
687
761
  ---
688
762
 
763
+ ## Account Recovery
764
+
765
+ When a user loses access to their passkey (new device, cleared browser data), they can recover their account via email OTP and register a new passkey in a single operation.
766
+
767
+ ```typescript
768
+ const result = await client.auth.recoverAccount({
769
+ externalUserId: ‘user_123’,
770
+ otp: ‘123456’, // 6-digit code sent to user’s email
771
+ })
772
+
773
+ if (result.ok) {
774
+ // result.value contains:
775
+ // {
776
+ // success: true,
777
+ // credentialId: string,
778
+ // status: string,
779
+ // sessionToken: string,
780
+ // user: { userId, externalUserId, email?, metadata? },
781
+ // redirectUrl?: string
782
+ // }
783
+ console.log(‘Account recovered, new passkey registered’)
784
+ console.log(‘Session token:’, result.value.sessionToken)
785
+ }
786
+ ```
787
+
788
+ The flow: your backend sends a recovery OTP to the user’s email. The user enters the OTP. The SDK verifies the OTP with the API, triggers a WebAuthn registration ceremony for the new passkey, and returns a session token on success.
789
+
790
+ ---
791
+
792
+ ## Programmatic Onboarding
793
+
794
+ The SDK provides a programmatic onboarding flow for new tenants/users. This handles the complete lifecycle: start session, poll for status, register passkey via WebAuthn, and complete onboarding.
795
+
796
+ ```typescript
797
+ const result = await client.onboarding.startFlow({
798
+ user_role: ‘maintainer’, // ‘maintainer’ or ‘app_user’
799
+ company_name: ‘Acme Corp’, // optional
800
+ })
801
+
802
+ if (result.ok) {
803
+ // result.value contains:
804
+ // {
805
+ // session_id: string,
806
+ // status: ‘completed’,
807
+ // user_id: string,
808
+ // tenant_id: string,
809
+ // session_token: string
810
+ // }
811
+ console.log(‘Onboarding complete:’, result.value.tenant_id)
812
+ }
813
+ ```
814
+
815
+ If the onboarding requires the user to complete passkey registration externally (e.g. in a browser when running from a non-WebAuthn environment), the SDK returns a `NOT_SUPPORTED` error with the `onboarding_url` in `error.details` for the user to complete the flow.
816
+
817
+ ---
818
+
689
819
  ## Result type
690
820
 
691
- The SDK exports the `Result<T, E>` type and the `ok(value)` and `err(error)` helpers for typing and building results (useful in tests or utilities):
821
+ Every SDK method returns `Result<T, TryMellonError>` -- a discriminated union that eliminates try/catch:
692
822
 
693
823
  ```typescript
694
- import { Result, ok, err } from '@trymellon/js'
824
+ import { Result, ok, err } from ‘@trymellon/js
825
+
826
+ // Check results
827
+ const result = await client.register({ externalUserId: ‘user_123’ })
828
+ if (result.ok) {
829
+ console.log(result.value.sessionToken) // T
830
+ } else {
831
+ console.error(result.error.code) // TryMellonError
832
+ }
695
833
 
696
- const result: Result<{ id: string }, Error> = ok({ id: '123' })
697
- if (result.ok) console.log(result.value.id)
834
+ // Build results (useful in tests or utilities)
835
+ const success: Result<{ id: string }, Error> = ok({ id: ‘123’ })
836
+ const failure: Result<never, Error> = err(new Error(‘fail’))
698
837
  ```
699
838
 
700
839
  ---
701
840
 
702
841
  ## Error handling
703
842
 
704
- The SDK returns `Result<T, TryMellonError>`: check `result.ok` and on error use `result.error.code`:
843
+ Check `result.ok` and use `result.error.code` for structured error handling:
705
844
 
706
845
  ```typescript
707
- import { isTryMellonError } from '@trymellon/js'
846
+ import { isTryMellonError } from ‘@trymellon/js
708
847
 
709
- const result = await client.authenticate({ externalUserId: 'user_123' })
848
+ const result = await client.authenticate({ externalUserId: user_123 })
710
849
 
711
850
  if (!result.ok) {
712
- const error = result.error
713
- switch (error.code) {
714
- case 'USER_CANCELLED':
715
- console.log('User cancelled the operation')
851
+ switch (result.error.code) {
852
+ case ‘USER_CANCELLED’:
853
+ console.log(‘User dismissed the prompt’)
716
854
  break
717
- case 'NOT_SUPPORTED':
718
- await client.fallback.email.start({
719
- userId: 'user_123',
720
- email: 'user@example.com'
721
- })
855
+ case NOT_SUPPORTED’:
856
+ await client.fallback.email.start({ userId: ‘user_123’, email: ‘user@example.com’ })
722
857
  break
723
- case 'PASSKEY_NOT_FOUND':
724
- await client.register({ externalUserId: 'user_123' })
858
+ case PASSKEY_NOT_FOUND’:
859
+ await client.register({ externalUserId: user_123 })
725
860
  break
726
- case 'NETWORK_FAILURE':
727
- console.error('Network error:', error.details)
861
+ case NETWORK_FAILURE’:
862
+ console.error(Network error:’, result.error.details)
728
863
  break
729
- case 'TIMEOUT':
730
- console.error('Operation timed out')
864
+ case TIMEOUT’:
865
+ console.error(Operation timed out)
866
+ break
867
+ case ‘CHALLENGE_MISMATCH’:
868
+ console.error(‘QR link expired or already used -- scan again’)
731
869
  break
732
870
  default:
733
- console.error('Error:', error.code, error.message)
871
+ console.error(Error:’, result.error.code, result.error.message)
734
872
  }
735
873
  return
736
874
  }
737
-
738
- // result.value contains session_token, user, etc.
739
875
  ```
740
876
 
741
877
  **Error codes:**
@@ -743,21 +879,24 @@ if (!result.ok) {
743
879
  | Code | Description |
744
880
  |------|-------------|
745
881
  | `NOT_SUPPORTED` | WebAuthn not available in this environment |
746
- | `USER_CANCELLED` | User cancelled the operation |
882
+ | `USER_CANCELLED` | User cancelled the operation (dismissed prompt) |
747
883
  | `PASSKEY_NOT_FOUND` | No passkey found for the user |
748
884
  | `SESSION_EXPIRED` | Session expired |
749
885
  | `NETWORK_FAILURE` | Network error (with automatic retries) |
750
886
  | `INVALID_ARGUMENT` | Invalid argument in config or method call |
751
887
  | `TIMEOUT` | Operation timed out |
752
888
  | `ABORTED` | Operation aborted via AbortSignal |
753
- | `CHALLENGE_MISMATCH` | Cross-device: link already used or expired; ask user to scan QR again from computer |
754
- | `UNKNOWN_ERROR` | Unknown error |
889
+ | `ABORT_ERROR` | Operation aborted by user or timeout (cross-device polling) |
890
+ | `CHALLENGE_MISMATCH` | Cross-device: link already used or expired |
891
+ | `UNKNOWN_ERROR` | Unknown or unmapped error |
892
+
893
+ **Retry behavior:** The SDK retries automatically with exponential backoff for HTTP 5xx, HTTP 429 (rate limiting), and transient network errors. HTTP 4xx (except 429), timeouts, and validation errors are not retried.
755
894
 
756
895
  ---
757
896
 
758
897
  ## Cancelling operations
759
898
 
760
- You can cancel operations with `AbortSignal`:
899
+ Cancel any operation with `AbortSignal`:
761
900
 
762
901
  ```typescript
763
902
  const controller = new AbortController()
@@ -766,11 +905,40 @@ const controller = new AbortController()
766
905
  setTimeout(() => controller.abort(), 10000)
767
906
 
768
907
  const result = await client.register({
769
- externalUserId: 'user_123',
770
- signal: controller.signal
908
+ externalUserId: user_123’,
909
+ signal: controller.signal,
910
+ })
911
+ if (!result.ok && result.error.code === ‘ABORTED’) {
912
+ console.log(‘Operation cancelled’)
913
+ }
914
+ ```
915
+
916
+ Works with `register()`, `authenticate()`, and `waitForSession()`.
917
+
918
+ ---
919
+
920
+ ## Logging
921
+
922
+ Inject a `Logger` for request correlation and debugging. The SDK exports a `ConsoleLogger` or you can implement the `Logger` interface:
923
+
924
+ ```typescript
925
+ import { TryMellon, ConsoleLogger } from ‘@trymellon/js’
926
+
927
+ const clientResult = TryMellon.create({
928
+ appId: ‘your-app-id-uuid’,
929
+ publishableKey: ‘cli_xxxx’,
930
+ logger: new ConsoleLogger(),
771
931
  })
772
- if (!result.ok && result.error.code === 'ABORTED') {
773
- console.log('Operation cancelled')
932
+ ```
933
+
934
+ **Logger interface:**
935
+
936
+ ```typescript
937
+ interface Logger {
938
+ debug(message: string, meta?: Record<string, unknown>): void
939
+ info(message: string, meta?: Record<string, unknown>): void
940
+ warn(message: string, meta?: Record<string, unknown>): void
941
+ error(message: string, meta?: Record<string, unknown>): void
774
942
  }
775
943
  ```
776
944
 
@@ -780,178 +948,187 @@ if (!result.ok && result.error.code === 'ABORTED') {
780
948
 
781
949
  Your backend must validate the `session_token` with TryMellon and create its own session:
782
950
 
783
- ```typescript
784
- // POST /api/login
951
+ ```
785
952
  POST https://api.trymellonauth.com/v1/sessions/validate
786
953
  Authorization: Bearer {session_token}
954
+ ```
787
955
 
788
- // TryMellon response
956
+ **Response:**
957
+
958
+ ```json
789
959
  {
790
- valid: true,
791
- user_id: string,
792
- external_user_id: string,
793
- tenant_id: string,
794
- app_id: string
960
+ "valid": true,
961
+ "user_id": "...",
962
+ "external_user_id": "...",
963
+ "tenant_id": "...",
964
+ "app_id": "..."
795
965
  }
796
966
  ```
797
967
 
798
- Then create your own session in your system.
968
+ Then create your own session (JWT, cookie, etc.) in your system. The SDK also provides `client.validateSession(token)` if you want to validate from the client side.
799
969
 
800
970
  ---
801
971
 
802
972
  ## Security
803
973
 
804
- * Native browser WebAuthn (no client-side cryptography)
805
- * Short-lived challenges generated by TryMellon
806
- * Replay attack protection (automatic counters)
807
- * SDK never handles secrets or private keys
808
- * Thorough validation of inputs and API responses
809
- * Robust error handling with typed errors
810
- * Guaranteed cleanup of resources (timeouts, signals)
811
- * Automatic origin validation
974
+ * Native browser WebAuthn -- no client-side cryptography
975
+ * Short-lived challenges generated by TryMellon
976
+ * Replay attack protection (automatic counters)
977
+ * SDK never handles secrets or private keys
978
+ * Thorough validation of inputs and API responses
979
+ * Robust error handling with typed errors
980
+ * Guaranteed cleanup of resources (timeouts, signals)
981
+ * Automatic origin validation
812
982
 
813
983
  ---
814
984
 
815
- ## Compatibility
985
+ ## Security: CSP and SRI
816
986
 
817
- | Browser | WebAuthn support | Passkeys support |
818
- |---------|-------------------|-------------------|
819
- | Chrome | ✅ | ✅ |
820
- | Safari | ✅ | ✅ |
821
- | Firefox | ✅ | ✅ |
822
- | Edge | ✅ | ✅ |
987
+ ### Content-Security-Policy (CSP)
823
988
 
824
- **Requirements:**
825
- - HTTPS (required except on `localhost`)
826
- - Modern browser with WebAuthn support
989
+ If you load the SDK via `<script>` or enforce a content security policy, include:
827
990
 
828
- ---
991
+ - **script-src:** The script origin (e.g. `https://cdn.trymellon.com` or `’self’`)
992
+ - **connect-src:** The TryMellon API origin: `https://api.trymellonauth.com`
829
993
 
830
- ## Features
994
+ ```
995
+ Content-Security-Policy: script-src ‘self’; connect-src ‘self’ https://api.trymellonauth.com;
996
+ ```
831
997
 
832
- * **Zero runtime dependencies** – No external runtime dependencies in the core bundle
833
- * ✅ **TypeScript first** – Full types and strict mode; all entry points typed
834
- * ✅ **Framework support** – Dedicated entry points: `@trymellon/js` (core), `@trymellon/js/react`, `@trymellon/js/vue`, `@trymellon/js/angular`; Vanilla and Svelte use core
835
- * ✅ **Automatic retries** – Exponential backoff for transient errors
836
- * ✅ **Thorough validation** – Input and API response validation
837
- * ✅ **Robust error handling** – Typed, descriptive errors
838
- * ✅ **Events for UX** – Event system for spinners and analytics
839
- * ✅ **Email fallback** – OTP by email when WebAuthn is unavailable
840
- * ✅ **Operation cancellation** – AbortSignal support
841
- * ✅ **Cross-Device Auth** – QR Login flow support (Desktop to Mobile)
842
- * ✅ **Automatic detection** – Origin and WebAuthn support detected automatically
998
+ ### SRI (Subresource Integrity)
843
999
 
844
- ---
1000
+ To load `index.global.js` with integrity:
845
1001
 
846
- ## Troubleshooting
1002
+ ```bash
1003
+ openssl dgst -sha384 -binary dist/index.global.js | openssl base64 -A
1004
+ ```
847
1005
 
848
- ### WebAuthn not available
1006
+ ```html
1007
+ <script
1008
+ src="https://cdn.example.com/trymellon/index.global.js"
1009
+ integrity="sha384-<generated-hash>"
1010
+ crossorigin="anonymous"
1011
+ ></script>
1012
+ ```
849
1013
 
850
- If `TryMellon.isSupported()` returns `false`:
1014
+ ---
851
1015
 
852
- - Ensure you are on HTTPS (required except on `localhost`)
853
- - Ensure your browser supports WebAuthn (Chrome, Safari, Firefox, Edge)
854
- - Use the email fallback: `client.fallback.email.start({ userId, email })`
1016
+ ## Compatibility
855
1017
 
856
- ### User cancelled the operation
1018
+ | Browser | WebAuthn | Passkeys |
1019
+ |---------|----------|----------|
1020
+ | Chrome | Yes | Yes |
1021
+ | Safari | Yes | Yes |
1022
+ | Firefox | Yes | Yes |
1023
+ | Edge | Yes | Yes |
857
1024
 
858
- If you get `USER_CANCELLED`:
1025
+ **Requirements:** HTTPS (except `localhost`), modern browser with WebAuthn support.
859
1026
 
860
- - This is normal when the user dismisses the prompt
861
- - Not a critical error; inform the user and optionally retry
1027
+ ---
862
1028
 
863
- ### Passkey not found
1029
+ ## Telemetry (opt-in)
864
1030
 
865
- If you get `PASSKEY_NOT_FOUND`:
1031
+ The SDK can send anonymous telemetry (event + latency, no user identifiers) when `enableTelemetry: true` in config. Payload: `{ event: ‘register’|’authenticate’, latencyMs: number, ok: true }`. You can inject a custom `telemetrySender` to send to your own endpoint or disable with `enableTelemetry: false` (default).
866
1032
 
867
- - The user has no registered passkey
868
- - Offer to register: `client.register()`
869
- - Or use email fallback: `client.fallback.email.start({ userId, email })`
1033
+ ---
870
1034
 
871
- ### Network errors
1035
+ ## For AI Agents
872
1036
 
873
- If you get `NETWORK_FAILURE`:
1037
+ Are you an LLM or autonomous agent trying to use TryMellon? We built a dedicated "Vending Machine" for you.
874
1038
 
875
- - Check your internet connection
876
- - Ensure `apiBaseUrl` is a valid URL
877
- - The SDK retries automatically with exponential backoff for:
878
- - HTTP 5xx (server errors)
879
- - HTTP 429 (rate limiting)
880
- - Transient network errors
881
- - You can configure `maxRetries` and `retryDelayMs` to tune behavior
1039
+ **Goal:** Get your own `client_id` and `client_secret` instantly to start creating users via API.
1040
+ **Limit:** The "Free AI Tier" allows up to **15 users** for testing and small demos.
882
1041
 
883
- ---
1042
+ ### 1. Get Credentials
884
1043
 
885
- ## Security: CSP and SRI
1044
+ ```bash
1045
+ curl -X POST https://api.trymellonauth.com/onboarding/ai \
1046
+ -H "Content-Type: application/json" \
1047
+ -d ‘{ "name": "My Agent Name" }’
1048
+ ```
886
1049
 
887
- ### Content-Security-Policy (CSP)
1050
+ **Response:**
888
1051
 
889
- If you load the SDK via `<script>` or enforce a content security policy, include in your `Content-Security-Policy`:
1052
+ ```json
1053
+ {
1054
+ "tenantId": "...",
1055
+ "clientId": "cli_...",
1056
+ "clientSecret": "sec_...",
1057
+ "publishableKey": "cli_...",
1058
+ "applicationId": "...",
1059
+ "limits": { "maxUsers": 15, "maxApps": 1 }
1060
+ }
1061
+ ```
890
1062
 
891
- - **script-src:** The script origin (e.g. `https://cdn.trymellon.com` or `'self'`) and `'unsafe-inline'` only if you use inline scripts; not needed for a bundled SDK.
892
- - **connect-src:** The TryMellon API origin (e.g. `https://api.trymellonauth.com`) so register/authenticate requests are not blocked.
1063
+ Use `publishableKey` (same as `clientId`) in the SDK: `TryMellon.create({ appId: ‘...’, publishableKey: response.publishableKey })`. Use `applicationId` to update allowed origins (step 4) if you use passkeys or cross-device from your agent’s domain.
893
1064
 
894
- Minimal example (adjust origins to your environment):
1065
+ ### 2. Authenticate
895
1066
 
1067
+ ```bash
1068
+ curl -X POST https://api.trymellonauth.com/oauth/token \
1069
+ -H "Content-Type: application/json" \
1070
+ -d ‘{
1071
+ "client_id": "cli_...",
1072
+ "client_secret": "sec_...",
1073
+ "grant_type": "client_credentials"
1074
+ }’
896
1075
  ```
897
- Content-Security-Policy: script-src 'self'; connect-src 'self' https://api.trymellonauth.com;
898
- ```
899
-
900
- ### SRI (Subresource Integrity)
901
1076
 
902
- To load `index.global.js` with integrity, generate the SRI hash after building:
1077
+ ### 3. Create Users
903
1078
 
904
1079
  ```bash
905
- openssl dgst -sha384 -binary dist/index.global.js | openssl base64 -A
1080
+ curl -X POST https://api.trymellonauth.com/v1/users \
1081
+ -H "Authorization: Bearer <access_token>" \
1082
+ -H "Content-Type: application/json" \
1083
+ -d ‘{ "external_user_id": "user_123" }’
906
1084
  ```
907
1085
 
908
- Use the value in the `integrity` attribute:
1086
+ ### 4. Update Allowed Origins
909
1087
 
910
- ```html
911
- <script
912
- src="https://cdn.example.com/trymellon/index.global.js"
913
- integrity="sha384-<generated-hash>"
914
- crossorigin="anonymous"
915
- ></script>
1088
+ ```bash
1089
+ curl -X PATCH https://api.trymellonauth.com/v1/applications/<applicationId> \
1090
+ -H "Authorization: Bearer <access_token>" \
1091
+ -H "Content-Type: application/json" \
1092
+ -d ‘{ "allowed_origins": ["https://your-agent-domain.com"] }’
916
1093
  ```
917
1094
 
918
- Optional: the build can generate `dist/sri.json` with the hash (see `package.json` script: `"sri": "node ..."`).
1095
+ Without this, requests from your agent’s origin will get 404 when resolving the application.
919
1096
 
920
1097
  ---
921
1098
 
922
- ## Telemetry (opt-in)
1099
+ ## Troubleshooting
923
1100
 
924
- The SDK can send anonymous telemetry (event + latency, no user identifiers) when `enableTelemetry: true` in config. Used to improve the product. Minimal payload: `{ event: 'register'|'authenticate', latencyMs: number, ok: true }`. You can inject a custom `telemetrySender` to send to your own endpoint or disable with `enableTelemetry: false` (default).
1101
+ ### WebAuthn not available
925
1102
 
926
- ---
1103
+ If `TryMellon.isSupported()` returns `false`:
1104
+ - Ensure you are on HTTPS (required except on `localhost`)
1105
+ - Ensure your browser supports WebAuthn
1106
+ - Use the email fallback: `client.fallback.email.start({ userId, email })`
927
1107
 
928
- ## Specification summary (for project ingestion)
1108
+ ### User cancelled the operation
929
1109
 
930
- Projects integrating this SDK should document in their README:
1110
+ `USER_CANCELLED` is normal when the user dismisses the browser prompt. Not a critical error; inform the user and optionally retry.
931
1111
 
932
- - **SDK:** `@trymellon/js` (and optionally `/react`, `/vue`, `/angular` if using those entry points)
933
- - **Node:** >= 18 (per `engines`)
934
- - **Browsers:** WebAuthn-capable (Chrome, Safari, Firefox, Edge); HTTPS required except `localhost`
935
- - **Config:** `appId` and `publishableKey` from TryMellon dashboard; optional `apiBaseUrl` for self-hosted API
936
- - **Backend:** Must validate `session_token` via TryMellon API (`GET /v1/sessions/validate`) and create own session
1112
+ ### Passkey not found
937
1113
 
938
- Framework-specific: React uses `TryMellonProvider` + hooks; Vue uses `provideTryMellon` + composables; Angular uses `provideTryMellonConfig` + `TryMellonService`; Vanilla/Svelte use core `TryMellon` only.
1114
+ `PASSKEY_NOT_FOUND` means the user has no registered passkey. Offer registration or email fallback.
939
1115
 
940
- ---
1116
+ ### Network errors
941
1117
 
942
- ## Contact & landing
1118
+ `NETWORK_FAILURE` -- check internet connection and `apiBaseUrl`. The SDK retries automatically with exponential backoff for HTTP 5xx, 429, and transient network errors. Configure `maxRetries` and `retryDelayMs` to tune.
943
1119
 
944
- - **Landing:** [https://trymellon-landing.pages.dev/](https://trymellon-landing.pages.dev/)
945
- - **Contact:** [siliangrove@gmail.com](mailto:siliangrove@gmail.com) — Sales inquiries by email only.
1120
+ ### QR link expired
1121
+
1122
+ `CHALLENGE_MISMATCH` -- the cross-device link was already used or expired. Ask the user to scan a new QR code from the desktop.
946
1123
 
947
1124
  ---
948
1125
 
949
1126
  ## Additional documentation
950
1127
 
951
- - [API Reference](./documentation/API.md) Full API reference
952
- - [Usage examples](./documentation/EXAMPLES.md) Practical integration examples (React, Vue, Vanilla, events, fallback)
953
- - [Contributing](./documentation/CONTRIBUTING.md) How to contribute (including running tests, coverage, Angular, E2E, audit and workflow lint locally)
954
- - [CI standards (fintech)](./documentation/CI-FINTECH-STANDARDS.md) Coverage, security, E2E and workflow validation criteria
1128
+ - [API Reference](./documentation/API.md) Full API reference
1129
+ - [Web Components](./documentation/WEB-COMPONENTS.md) `<trymellon-auth>` and `<trymellon-auth-modal>`: attributes, events, trigger-only
1130
+ - [Usage examples](./documentation/EXAMPLES.md) Practical integration examples
1131
+ - [Contributing](./CONTRIBUTING.md) How to contribute (tests, coverage, E2E)
955
1132
 
956
1133
  ---
957
1134
 
@@ -968,7 +1145,12 @@ Releases are published to npm automatically by **semantic-release** in CI. The v
968
1145
  - **Triggers a release:** `fix:` (patch), `feat:` (minor), `BREAKING CHANGE:` / `feat!:` (major).
969
1146
  - **Does not trigger a release:** `chore:`, `refactor:`, `docs:`, `style:`, `test:`.
970
1147
 
971
- If changes must be published, ensure at least one commit since the last tag uses `fix:` or `feat:`. See monorepo [docs/ai_context_leePrimero.md](../docs/ai_context_leePrimero.md) §4.D.
1148
+ ---
1149
+
1150
+ ## Contact & Landing
1151
+
1152
+ - **Landing:** [https://trymellon-landing.pages.dev/](https://trymellon-landing.pages.dev/)
1153
+ - **Contact:** [siliangrove@gmail.com](mailto:siliangrove@gmail.com)
972
1154
 
973
1155
  ---
974
1156
 
@@ -978,7 +1160,4 @@ MIT
978
1160
 
979
1161
  ---
980
1162
 
981
- ## Philosophy
982
-
983
- > Implementing Passkeys should take minutes, not weeks.
984
- > The backend remains yours.
1163
+ > Implementing Passkeys should take minutes, not weeks. The backend remains yours.