@trymellon/js 1.7.7 → 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 +637 -458
- package/dist/angular.cjs +1 -1
- package/dist/angular.cjs.map +1 -1
- package/dist/angular.d.cts +1 -1
- package/dist/angular.d.ts +1 -1
- package/dist/angular.js +1 -1
- package/dist/angular.js.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -19
- package/dist/index.d.ts +34 -19
- package/dist/index.global.js +2 -2
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/{trymellon-CItwBg_1.d.cts → trymellon-P7BPxIry.d.cts} +34 -19
- package/dist/{trymellon-CItwBg_1.d.ts → trymellon-P7BPxIry.d.ts} +34 -19
- package/dist/ui/index.d.ts +514 -0
- package/dist/ui/index.js +489 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/vue.d.cts +1 -1
- package/dist/vue.d.ts +1 -1
- package/package.json +8 -2
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
|
|
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" (
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
82
|
+
No peer dependencies. No bloat.
|
|
111
83
|
|
|
112
84
|
---
|
|
113
85
|
|
|
114
86
|
## Requirements
|
|
115
87
|
|
|
116
|
-
* Node 18
|
|
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`
|
|
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
|
|
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 app’s origin.
|
|
128
100
|
|
|
129
101
|
| SDK parameter | What it is | Where to find it |
|
|
130
102
|
|------------------|-------------------------|-------------------------------------------|
|
|
131
|
-
| `appId` | **Application ID** (UUID) | Dashboard
|
|
132
|
-
| `publishableKey` | **Client ID** (starts with `cli_`) | Dashboard
|
|
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
|
|
106
|
+
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.
|
|
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 |
|
|
143
|
-
|
|
144
|
-
| `@trymellon/js` | Vanilla JS, Svelte, Node,
|
|
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
|
|
159
|
-
import { TryMellonProvider,
|
|
160
|
+
import { TryMellon } from ‘@trymellon/js’
|
|
161
|
+
import { TryMellonProvider, useRegister, useAuthenticate } from ‘@trymellon/js/react’
|
|
160
162
|
|
|
161
|
-
const clientResult = TryMellon.create({
|
|
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={
|
|
183
|
-
|
|
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
|
-
|
|
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
|
|
200
|
-
import { provideTryMellon,
|
|
201
|
+
import { TryMellon } from ‘@trymellon/js’
|
|
202
|
+
import { provideTryMellon, useRegister, useAuthenticate } from ‘@trymellon/js/vue’
|
|
201
203
|
|
|
202
|
-
const clientResult = TryMellon.create({
|
|
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
|
-
|
|
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="
|
|
216
|
-
|
|
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
|
-
|
|
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
|
|
232
|
+
import { provideTryMellonConfig } from ‘@trymellon/js/angular’
|
|
232
233
|
|
|
233
234
|
export const appConfig = {
|
|
234
235
|
providers: [
|
|
235
236
|
provideTryMellonConfig({
|
|
236
|
-
appId:
|
|
237
|
-
publishableKey:
|
|
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 {
|
|
247
|
+
import { inject, Injectable } from ‘@angular/core’
|
|
248
|
+
import { TryMellonService } from ‘@trymellon/js/angular’
|
|
247
249
|
|
|
248
|
-
@Injectable({ providedIn:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
##
|
|
276
|
+
## Web Components
|
|
275
277
|
|
|
276
|
-
|
|
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
|
|
281
|
+
import ‘@trymellon/js/ui’
|
|
282
|
+
```
|
|
282
283
|
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
370
|
+
Prefer **`TryMellon.create()`** so invalid config returns a Result instead of throwing:
|
|
314
371
|
|
|
315
372
|
```typescript
|
|
316
|
-
import { TryMellon } from
|
|
373
|
+
import { TryMellon } from ‘@trymellon/js’
|
|
317
374
|
|
|
318
375
|
const clientResult = TryMellon.create({
|
|
319
|
-
appId:
|
|
320
|
-
publishableKey:
|
|
321
|
-
apiBaseUrl:
|
|
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(
|
|
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
|
-
>
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
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
|
|
418
|
+
import { TryMellon, SANDBOX_SESSION_TOKEN } from ‘@trymellon/js’
|
|
366
419
|
|
|
367
420
|
const clientResult = TryMellon.create({
|
|
368
421
|
sandbox: true,
|
|
369
|
-
appId:
|
|
370
|
-
publishableKey:
|
|
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:
|
|
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(
|
|
379
|
-
method:
|
|
380
|
-
headers: {
|
|
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:
|
|
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
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
450
|
-
hint:
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
517
|
+
### Conditional UI (passkey autofill)
|
|
464
518
|
|
|
465
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
562
|
+
console.log(‘Passkeys available’)
|
|
519
563
|
if (status.platformAuthenticatorAvailable) {
|
|
520
|
-
console.log(
|
|
564
|
+
console.log(‘Platform authenticator available’)
|
|
521
565
|
}
|
|
522
566
|
} else {
|
|
523
|
-
console.log(
|
|
567
|
+
console.log(‘Use fallback:’, status.recommendedFlow)
|
|
524
568
|
}
|
|
525
569
|
```
|
|
526
570
|
|
|
527
|
-
|
|
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
|
|
581
|
+
The SDK emits lifecycle events for UX feedback and analytics:
|
|
542
582
|
|
|
543
583
|
```typescript
|
|
544
|
-
|
|
545
|
-
|
|
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(
|
|
551
|
-
|
|
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(
|
|
557
|
-
console.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(
|
|
608
|
+
const unsubscribe = client.on(‘start’, handler)
|
|
564
609
|
unsubscribe()
|
|
565
610
|
```
|
|
566
611
|
|
|
567
612
|
**Available events:**
|
|
568
613
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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:
|
|
584
|
-
email:
|
|
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(
|
|
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:
|
|
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
|
-
//
|
|
599
|
-
await fetch(
|
|
600
|
-
method:
|
|
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,
|
|
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
|
-
|
|
616
|
-
|
|
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(
|
|
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}
|
|
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
|
-
###
|
|
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
|
|
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 ===
|
|
660
|
-
showError('QR code expired')
|
|
661
|
-
}
|
|
707
|
+
if (pollResult.error.code === ‘TIMEOUT’) showError(‘QR code expired’)
|
|
662
708
|
return
|
|
663
709
|
}
|
|
664
710
|
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
###
|
|
734
|
+
### Mobile: Approve
|
|
670
735
|
|
|
671
|
-
When the user scans the QR code, your mobile web app
|
|
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(
|
|
744
|
+
showSuccess(‘Done! Check your desktop.’)
|
|
682
745
|
} else {
|
|
683
|
-
showError(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
697
|
-
|
|
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
|
-
|
|
843
|
+
Check `result.ok` and use `result.error.code` for structured error handling:
|
|
705
844
|
|
|
706
845
|
```typescript
|
|
707
|
-
import { isTryMellonError } from
|
|
846
|
+
import { isTryMellonError } from ‘@trymellon/js’
|
|
708
847
|
|
|
709
|
-
const result = await client.authenticate({ externalUserId:
|
|
848
|
+
const result = await client.authenticate({ externalUserId: ‘user_123’ })
|
|
710
849
|
|
|
711
850
|
if (!result.ok) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
|
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
|
|
724
|
-
await client.register({ externalUserId:
|
|
858
|
+
case ‘PASSKEY_NOT_FOUND’:
|
|
859
|
+
await client.register({ externalUserId: ‘user_123’ })
|
|
725
860
|
break
|
|
726
|
-
case
|
|
727
|
-
console.error(
|
|
861
|
+
case ‘NETWORK_FAILURE’:
|
|
862
|
+
console.error(‘Network error:’, result.error.details)
|
|
728
863
|
break
|
|
729
|
-
case
|
|
730
|
-
console.error(
|
|
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(
|
|
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
|
-
| `
|
|
754
|
-
| `
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
773
|
-
|
|
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
|
-
```
|
|
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
|
-
|
|
956
|
+
**Response:**
|
|
957
|
+
|
|
958
|
+
```json
|
|
789
959
|
{
|
|
790
|
-
valid: true,
|
|
791
|
-
user_id:
|
|
792
|
-
external_user_id:
|
|
793
|
-
tenant_id:
|
|
794
|
-
app_id:
|
|
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
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
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
|
-
##
|
|
985
|
+
## Security: CSP and SRI
|
|
816
986
|
|
|
817
|
-
|
|
818
|
-
|---------|-------------------|-------------------|
|
|
819
|
-
| Chrome | ✅ | ✅ |
|
|
820
|
-
| Safari | ✅ | ✅ |
|
|
821
|
-
| Firefox | ✅ | ✅ |
|
|
822
|
-
| Edge | ✅ | ✅ |
|
|
987
|
+
### Content-Security-Policy (CSP)
|
|
823
988
|
|
|
824
|
-
|
|
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
|
-
|
|
994
|
+
```
|
|
995
|
+
Content-Security-Policy: script-src ‘self’; connect-src ‘self’ https://api.trymellonauth.com;
|
|
996
|
+
```
|
|
831
997
|
|
|
832
|
-
|
|
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
|
-
|
|
1002
|
+
```bash
|
|
1003
|
+
openssl dgst -sha384 -binary dist/index.global.js | openssl base64 -A
|
|
1004
|
+
```
|
|
847
1005
|
|
|
848
|
-
|
|
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
|
-
|
|
1014
|
+
---
|
|
851
1015
|
|
|
852
|
-
|
|
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
|
-
|
|
1018
|
+
| Browser | WebAuthn | Passkeys |
|
|
1019
|
+
|---------|----------|----------|
|
|
1020
|
+
| Chrome | Yes | Yes |
|
|
1021
|
+
| Safari | Yes | Yes |
|
|
1022
|
+
| Firefox | Yes | Yes |
|
|
1023
|
+
| Edge | Yes | Yes |
|
|
857
1024
|
|
|
858
|
-
|
|
1025
|
+
**Requirements:** HTTPS (except `localhost`), modern browser with WebAuthn support.
|
|
859
1026
|
|
|
860
|
-
|
|
861
|
-
- Not a critical error; inform the user and optionally retry
|
|
1027
|
+
---
|
|
862
1028
|
|
|
863
|
-
|
|
1029
|
+
## Telemetry (opt-in)
|
|
864
1030
|
|
|
865
|
-
|
|
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
|
-
|
|
868
|
-
- Offer to register: `client.register()`
|
|
869
|
-
- Or use email fallback: `client.fallback.email.start({ userId, email })`
|
|
1033
|
+
---
|
|
870
1034
|
|
|
871
|
-
|
|
1035
|
+
## For AI Agents
|
|
872
1036
|
|
|
873
|
-
|
|
1037
|
+
Are you an LLM or autonomous agent trying to use TryMellon? We built a dedicated "Vending Machine" for you.
|
|
874
1038
|
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1050
|
+
**Response:**
|
|
888
1051
|
|
|
889
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1077
|
+
### 3. Create Users
|
|
903
1078
|
|
|
904
1079
|
```bash
|
|
905
|
-
|
|
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
|
-
|
|
1086
|
+
### 4. Update Allowed Origins
|
|
909
1087
|
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
|
|
1095
|
+
Without this, requests from your agent’s origin will get 404 when resolving the application.
|
|
919
1096
|
|
|
920
1097
|
---
|
|
921
1098
|
|
|
922
|
-
##
|
|
1099
|
+
## Troubleshooting
|
|
923
1100
|
|
|
924
|
-
|
|
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
|
-
|
|
1108
|
+
### User cancelled the operation
|
|
929
1109
|
|
|
930
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
945
|
-
|
|
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)
|
|
952
|
-
- [
|
|
953
|
-
- [
|
|
954
|
-
- [
|
|
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
|
-
|
|
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
|
-
|
|
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.
|