@trymellon/js 1.1.3
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/LICENSE +21 -0
- package/README.MD +608 -0
- package/dist/angular.cjs +3 -0
- package/dist/angular.cjs.map +1 -0
- package/dist/angular.d.cts +13 -0
- package/dist/angular.d.ts +13 -0
- package/dist/angular.js +3 -0
- package/dist/angular.js.map +1 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +421 -0
- package/dist/index.d.ts +421 -0
- package/dist/index.global.js +3 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +3 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +25 -0
- package/dist/react.d.ts +25 -0
- package/dist/react.js +3 -0
- package/dist/react.js.map +1 -0
- package/dist/trymellon-Ca4kob_K.d.cts +364 -0
- package/dist/trymellon-Ca4kob_K.d.ts +364 -0
- package/dist/vue.cjs +3 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.cts +22 -0
- package/dist/vue.d.ts +22 -0
- package/dist/vue.js +3 -0
- package/dist/vue.js.map +1 -0
- package/package.json +121 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TryMellon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.MD
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
# @trymellon/js
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@trymellon/js)
|
|
4
|
+
[](https://github.com/ResakaGit/trymellon-js/actions)
|
|
5
|
+
[](https://codecov.io/gh/ResakaGit/trymellon-js)
|
|
6
|
+
|
|
7
|
+
Official **TryMellon** SDK for integrating **passwordless authentication** with **Passkeys / WebAuthn** in web applications.
|
|
8
|
+
|
|
9
|
+
This SDK fully abstracts WebAuthn and lets you authenticate users without passwords, returning a result your backend can use to create your own session.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What does this SDK do?
|
|
14
|
+
|
|
15
|
+
* ✅ Handles the full Passkeys flow in the browser
|
|
16
|
+
* ✅ Communicates directly with the TryMellon API
|
|
17
|
+
* ✅ Returns a `session_token` your backend can verify
|
|
18
|
+
* ✅ Handles Base64URL ↔ ArrayBuffer conversion automatically
|
|
19
|
+
* ✅ Event system for better UX (spinners, analytics)
|
|
20
|
+
* ✅ Email (OTP) fallback when WebAuthn is not available
|
|
21
|
+
* ✅ Automatic retries with exponential backoff
|
|
22
|
+
* ✅ Thorough validation of inputs and API responses
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## What does it NOT do?
|
|
27
|
+
|
|
28
|
+
* ❌ Does not create user sessions (your backend does)
|
|
29
|
+
* ❌ Does not replace your auth system
|
|
30
|
+
* ❌ Does not store end users
|
|
31
|
+
* ❌ Does not expose raw WebAuthn APIs
|
|
32
|
+
* ❌ Does not manage cookies or local storage
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @trymellon/js
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
* Browser with WebAuthn support (Chrome, Safari, Firefox, Edge)
|
|
47
|
+
* HTTPS (required except on `localhost`)
|
|
48
|
+
* A TryMellon Application with the correct origin configured
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Quickstart (5 minutes)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @trymellon/js
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { TryMellon } from '@trymellon/js'
|
|
60
|
+
|
|
61
|
+
const client = new TryMellon({
|
|
62
|
+
appId: 'app_live_xxxx', // From your TryMellon dashboard
|
|
63
|
+
publishableKey: 'key_live_xxxx', // Application API key
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Register passkey (camelCase recommended in options)
|
|
67
|
+
const registerResult = await client.register({ externalUserId: 'user_123' })
|
|
68
|
+
if (registerResult.ok) {
|
|
69
|
+
console.log('Session token:', registerResult.value.session_token)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Authenticate
|
|
73
|
+
const authResult = await client.authenticate({ externalUserId: 'user_123' })
|
|
74
|
+
if (authResult.ok) {
|
|
75
|
+
console.log('Session token:', authResult.value.session_token)
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Initialization
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { TryMellon } from '@trymellon/js'
|
|
85
|
+
|
|
86
|
+
const client = new TryMellon({
|
|
87
|
+
appId: 'app_live_xxxx', // Required: application (tenant) identifier
|
|
88
|
+
publishableKey: 'key_live_xxxx', // Required: API key for authentication
|
|
89
|
+
apiBaseUrl: 'https://api.trymellonauth.com', // optional
|
|
90
|
+
timeoutMs: 30000, // optional, default: 30000
|
|
91
|
+
maxRetries: 3, // optional
|
|
92
|
+
retryDelayMs: 1000, // optional
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Configuration options:**
|
|
97
|
+
|
|
98
|
+
- `appId` (required): Your application identifier in TryMellon. Sent in header `X-App-Id`.
|
|
99
|
+
- `publishableKey` (required): Public API key for authentication. Sent in header `Authorization: Bearer <publishableKey>`.
|
|
100
|
+
- `apiBaseUrl` (optional): API base URL. Default: `'https://api.trymellonauth.com'`
|
|
101
|
+
- `timeoutMs` (optional): HTTP request timeout. Range: `1000` - `300000`. Default: `30000`
|
|
102
|
+
- `maxRetries` (optional): Retries for network errors. Range: `0` - `10`. Default: `3`
|
|
103
|
+
- `retryDelayMs` (optional): Delay between retries. Range: `100` - `10000`. Default: `1000`
|
|
104
|
+
- `logger` (optional): `Logger` implementation for request correlation (e.g. `requestId` in logs and `error.details`).
|
|
105
|
+
|
|
106
|
+
**Register/authenticate options:** Both `externalUserId` (camelCase, recommended) and `external_user_id` (snake_case) are accepted. The SDK normalizes to snake_case for the API.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Basic usage
|
|
111
|
+
|
|
112
|
+
### Check WebAuthn support
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
if (TryMellon.isSupported()) {
|
|
116
|
+
// WebAuthn available, use passkeys
|
|
117
|
+
} else {
|
|
118
|
+
// Use email fallback
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Passkey registration
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const result = await client.register({
|
|
126
|
+
externalUserId: 'user_123' // recommended: camelCase. external_user_id also accepted
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
if (!result.ok) {
|
|
130
|
+
console.error(result.error.code, result.error.message)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Send session_token to your backend
|
|
135
|
+
await fetch('/api/login', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
sessionToken: result.value.session_token
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Registration options:**
|
|
145
|
+
|
|
146
|
+
- `externalUserId` or `external_user_id` (one required): Unique user ID in your system. camelCase recommended.
|
|
147
|
+
- `authenticatorType` (optional): `'platform'` (device) or `'cross-platform'` (USB/NFC)
|
|
148
|
+
- `signal` (optional): `AbortSignal` to cancel the operation
|
|
149
|
+
|
|
150
|
+
**Response:**
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
{
|
|
154
|
+
success: true,
|
|
155
|
+
credential_id: string,
|
|
156
|
+
status: string,
|
|
157
|
+
session_token: string,
|
|
158
|
+
user: {
|
|
159
|
+
user_id: string,
|
|
160
|
+
external_user_id: string,
|
|
161
|
+
email?: string,
|
|
162
|
+
metadata?: Record<string, unknown>
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Passkey authentication
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const result = await client.authenticate({
|
|
171
|
+
external_user_id: 'user_123',
|
|
172
|
+
hint: 'user@example.com' // optional, improves UX
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Send to backend
|
|
176
|
+
await fetch('/api/login', {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { 'Content-Type': 'application/json' },
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
sessionToken: result.session_token
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Authentication options:**
|
|
186
|
+
|
|
187
|
+
- `externalUserId` or `external_user_id` (one required): User ID. camelCase recommended.
|
|
188
|
+
- `hint` (optional): Hint for the passkey (e.g. email)
|
|
189
|
+
- `signal` (optional): AbortSignal to cancel
|
|
190
|
+
|
|
191
|
+
**Response:**
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
{
|
|
195
|
+
authenticated: boolean,
|
|
196
|
+
session_token: string,
|
|
197
|
+
user: {
|
|
198
|
+
user_id: string,
|
|
199
|
+
external_user_id: string,
|
|
200
|
+
email?: string,
|
|
201
|
+
metadata?: Record<string, unknown>
|
|
202
|
+
},
|
|
203
|
+
signals: {
|
|
204
|
+
userVerification?: boolean,
|
|
205
|
+
backupEligible?: boolean,
|
|
206
|
+
backupStatus?: boolean
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Validate session
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const validationResult = await client.validateSession('session_token_123')
|
|
215
|
+
|
|
216
|
+
if (validationResult.ok && validationResult.value.valid) {
|
|
217
|
+
const v = validationResult.value
|
|
218
|
+
console.log('User:', v.external_user_id, 'Tenant:', v.tenant_id, 'App:', v.app_id)
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Response:**
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
{
|
|
226
|
+
valid: boolean,
|
|
227
|
+
user_id: string,
|
|
228
|
+
external_user_id: string,
|
|
229
|
+
tenant_id: string,
|
|
230
|
+
app_id: string
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Get client status
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
const status = await client.getStatus()
|
|
238
|
+
|
|
239
|
+
if (status.isPasskeySupported) {
|
|
240
|
+
console.log('Passkeys available')
|
|
241
|
+
if (status.platformAuthenticatorAvailable) {
|
|
242
|
+
console.log('Platform authenticator available')
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
console.log('Use fallback')
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Response:**
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
{
|
|
253
|
+
isPasskeySupported: boolean,
|
|
254
|
+
platformAuthenticatorAvailable: boolean,
|
|
255
|
+
recommendedFlow: 'passkey' | 'fallback'
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Event system
|
|
262
|
+
|
|
263
|
+
The SDK emits events for better UX and analytics:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Subscribe to events
|
|
267
|
+
client.on('start', (payload) => {
|
|
268
|
+
console.log('Operation started:', payload.operation) // 'register' | 'authenticate'
|
|
269
|
+
showSpinner()
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
client.on('success', (payload) => {
|
|
273
|
+
console.log('Operation succeeded:', payload.operation)
|
|
274
|
+
hideSpinner()
|
|
275
|
+
showSuccessMessage()
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
client.on('error', (payload) => {
|
|
279
|
+
console.error('Error:', payload.error)
|
|
280
|
+
hideSpinner()
|
|
281
|
+
showError(payload.error.message)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// Unsubscribe
|
|
285
|
+
const unsubscribe = client.on('start', handler)
|
|
286
|
+
unsubscribe()
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Available events:**
|
|
290
|
+
|
|
291
|
+
- `'start'`: Operation started (`register` or `authenticate`)
|
|
292
|
+
- `'success'`: Operation completed successfully
|
|
293
|
+
- `'error'`: Error during the operation
|
|
294
|
+
- `'cancelled'`: Operation cancelled (future)
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Email fallback (OTP)
|
|
299
|
+
|
|
300
|
+
When WebAuthn is not available, you can use the email fallback. All methods return `Result<T, TryMellonError>`:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// 1. Send OTP code by email
|
|
304
|
+
const startResult = await client.fallback.email.start({ userId: 'user_123' })
|
|
305
|
+
if (!startResult.ok) { console.error(startResult.error); return }
|
|
306
|
+
|
|
307
|
+
// 2. Ask user for the code
|
|
308
|
+
const code = prompt('Enter the code sent by email:')
|
|
309
|
+
|
|
310
|
+
// 3. Verify code
|
|
311
|
+
const verifyResult = await client.fallback.email.verify({
|
|
312
|
+
userId: 'user_123',
|
|
313
|
+
code: code
|
|
314
|
+
})
|
|
315
|
+
if (!verifyResult.ok) { console.error(verifyResult.error); return }
|
|
316
|
+
|
|
317
|
+
// 4. Send sessionToken to backend
|
|
318
|
+
await fetch('/api/login', {
|
|
319
|
+
method: 'POST',
|
|
320
|
+
body: JSON.stringify({ sessionToken: verifyResult.value.sessionToken })
|
|
321
|
+
})
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Full flow with fallback:**
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
async function authenticateUser(userId: string) {
|
|
328
|
+
if (!TryMellon.isSupported()) {
|
|
329
|
+
return await authenticateWithEmail(userId)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const authResult = await client.authenticate({ externalUserId: userId })
|
|
333
|
+
if (authResult.ok) return authResult
|
|
334
|
+
if (authResult.error.code === 'PASSKEY_NOT_FOUND' || authResult.error.code === 'NOT_SUPPORTED') {
|
|
335
|
+
return await authenticateWithEmail(userId)
|
|
336
|
+
}
|
|
337
|
+
return authResult
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function authenticateWithEmail(userId: string) {
|
|
341
|
+
const startRes = await client.fallback.email.start({ userId })
|
|
342
|
+
if (!startRes.ok) return startRes
|
|
343
|
+
const code = prompt('Enter the code sent by email:')
|
|
344
|
+
return await client.fallback.email.verify({ userId, code })
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Result type
|
|
351
|
+
|
|
352
|
+
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):
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import { Result, ok, err } from '@trymellon/js'
|
|
356
|
+
|
|
357
|
+
const result: Result<{ id: string }, Error> = ok({ id: '123' })
|
|
358
|
+
if (result.ok) console.log(result.value.id)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Error handling
|
|
364
|
+
|
|
365
|
+
The SDK returns `Result<T, TryMellonError>`: check `result.ok` and on error use `result.error.code`:
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { isTryMellonError } from '@trymellon/js'
|
|
369
|
+
|
|
370
|
+
const result = await client.authenticate({ externalUserId: 'user_123' })
|
|
371
|
+
|
|
372
|
+
if (!result.ok) {
|
|
373
|
+
const error = result.error
|
|
374
|
+
switch (error.code) {
|
|
375
|
+
case 'USER_CANCELLED':
|
|
376
|
+
console.log('User cancelled the operation')
|
|
377
|
+
break
|
|
378
|
+
case 'NOT_SUPPORTED':
|
|
379
|
+
await client.fallback.email.start({ userId: 'user_123' })
|
|
380
|
+
break
|
|
381
|
+
case 'PASSKEY_NOT_FOUND':
|
|
382
|
+
await client.register({ externalUserId: 'user_123' })
|
|
383
|
+
break
|
|
384
|
+
case 'NETWORK_FAILURE':
|
|
385
|
+
console.error('Network error:', error.details)
|
|
386
|
+
break
|
|
387
|
+
case 'TIMEOUT':
|
|
388
|
+
console.error('Operation timed out')
|
|
389
|
+
break
|
|
390
|
+
default:
|
|
391
|
+
console.error('Error:', error.code, error.message)
|
|
392
|
+
}
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// result.value contains session_token, user, etc.
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Error codes:**
|
|
400
|
+
|
|
401
|
+
| Code | Description |
|
|
402
|
+
|------|-------------|
|
|
403
|
+
| `NOT_SUPPORTED` | WebAuthn not available in this environment |
|
|
404
|
+
| `USER_CANCELLED` | User cancelled the operation |
|
|
405
|
+
| `PASSKEY_NOT_FOUND` | No passkey found for the user |
|
|
406
|
+
| `SESSION_EXPIRED` | Session expired |
|
|
407
|
+
| `NETWORK_FAILURE` | Network error (with automatic retries) |
|
|
408
|
+
| `INVALID_ARGUMENT` | Invalid argument in config or method call |
|
|
409
|
+
| `TIMEOUT` | Operation timed out |
|
|
410
|
+
| `ABORTED` | Operation aborted via AbortSignal |
|
|
411
|
+
| `UNKNOWN_ERROR` | Unknown error |
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Cancelling operations
|
|
416
|
+
|
|
417
|
+
You can cancel operations with `AbortSignal`:
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
const controller = new AbortController()
|
|
421
|
+
|
|
422
|
+
// Cancel after 10 seconds
|
|
423
|
+
setTimeout(() => controller.abort(), 10000)
|
|
424
|
+
|
|
425
|
+
const result = await client.register({
|
|
426
|
+
externalUserId: 'user_123',
|
|
427
|
+
signal: controller.signal
|
|
428
|
+
})
|
|
429
|
+
if (!result.ok && result.error.code === 'ABORTED') {
|
|
430
|
+
console.log('Operation cancelled')
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Client backend
|
|
437
|
+
|
|
438
|
+
Your backend must validate the `session_token` with TryMellon and create its own session:
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
// POST /api/login
|
|
442
|
+
POST https://api.trymellonauth.com/v1/sessions/validate
|
|
443
|
+
Authorization: Bearer {session_token}
|
|
444
|
+
|
|
445
|
+
// TryMellon response
|
|
446
|
+
{
|
|
447
|
+
valid: true,
|
|
448
|
+
user_id: string,
|
|
449
|
+
external_user_id: string,
|
|
450
|
+
tenant_id: string,
|
|
451
|
+
app_id: string
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Then create your own session in your system.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Security
|
|
460
|
+
|
|
461
|
+
* ✅ Native browser WebAuthn (no client-side cryptography)
|
|
462
|
+
* ✅ Short-lived challenges generated by TryMellon
|
|
463
|
+
* ✅ Replay attack protection (automatic counters)
|
|
464
|
+
* ✅ SDK never handles secrets or private keys
|
|
465
|
+
* ✅ Thorough validation of inputs and API responses
|
|
466
|
+
* ✅ Robust error handling with typed errors
|
|
467
|
+
* ✅ Guaranteed cleanup of resources (timeouts, signals)
|
|
468
|
+
* ✅ Automatic origin validation
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Compatibility
|
|
473
|
+
|
|
474
|
+
| Browser | WebAuthn support | Passkeys support |
|
|
475
|
+
|---------|-------------------|-------------------|
|
|
476
|
+
| Chrome | ✅ | ✅ |
|
|
477
|
+
| Safari | ✅ | ✅ |
|
|
478
|
+
| Firefox | ✅ | ✅ |
|
|
479
|
+
| Edge | ✅ | ✅ |
|
|
480
|
+
|
|
481
|
+
**Requirements:**
|
|
482
|
+
- HTTPS (required except on `localhost`)
|
|
483
|
+
- Modern browser with WebAuthn support
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Features
|
|
488
|
+
|
|
489
|
+
* ✅ **Zero runtime dependencies** – No external runtime dependencies
|
|
490
|
+
* ✅ **TypeScript first** – Full types and strict mode
|
|
491
|
+
* ✅ **Framework agnostic** – Works with React, Vue, Angular, Vanilla JS, etc.
|
|
492
|
+
* ✅ **Automatic retries** – Exponential backoff for transient errors
|
|
493
|
+
* ✅ **Thorough validation** – Input and API response validation
|
|
494
|
+
* ✅ **Robust error handling** – Typed, descriptive errors
|
|
495
|
+
* ✅ **Events for UX** – Event system for spinners and analytics
|
|
496
|
+
* ✅ **Email fallback** – OTP by email when WebAuthn is unavailable
|
|
497
|
+
* ✅ **Operation cancellation** – AbortSignal support
|
|
498
|
+
* ✅ **Automatic detection** – Origin and WebAuthn support detected automatically
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Troubleshooting
|
|
503
|
+
|
|
504
|
+
### WebAuthn not available
|
|
505
|
+
|
|
506
|
+
If `TryMellon.isSupported()` returns `false`:
|
|
507
|
+
|
|
508
|
+
- Ensure you are on HTTPS (required except on `localhost`)
|
|
509
|
+
- Ensure your browser supports WebAuthn (Chrome, Safari, Firefox, Edge)
|
|
510
|
+
- Use the email fallback: `client.fallback.email.start({ userId })`
|
|
511
|
+
|
|
512
|
+
### User cancelled the operation
|
|
513
|
+
|
|
514
|
+
If you get `USER_CANCELLED`:
|
|
515
|
+
|
|
516
|
+
- This is normal when the user dismisses the prompt
|
|
517
|
+
- Not a critical error; inform the user and optionally retry
|
|
518
|
+
|
|
519
|
+
### Passkey not found
|
|
520
|
+
|
|
521
|
+
If you get `PASSKEY_NOT_FOUND`:
|
|
522
|
+
|
|
523
|
+
- The user has no registered passkey
|
|
524
|
+
- Offer to register: `client.register()`
|
|
525
|
+
- Or use email fallback: `client.fallback.email.start()`
|
|
526
|
+
|
|
527
|
+
### Network errors
|
|
528
|
+
|
|
529
|
+
If you get `NETWORK_FAILURE`:
|
|
530
|
+
|
|
531
|
+
- Check your internet connection
|
|
532
|
+
- Ensure `apiBaseUrl` is a valid URL
|
|
533
|
+
- The SDK retries automatically with exponential backoff for:
|
|
534
|
+
- HTTP 5xx (server errors)
|
|
535
|
+
- HTTP 429 (rate limiting)
|
|
536
|
+
- Transient network errors
|
|
537
|
+
- You can configure `maxRetries` and `retryDelayMs` to tune behavior
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## Security: CSP and SRI
|
|
542
|
+
|
|
543
|
+
### Content-Security-Policy (CSP)
|
|
544
|
+
|
|
545
|
+
If you load the SDK via `<script>` or enforce a content security policy, include in your `Content-Security-Policy`:
|
|
546
|
+
|
|
547
|
+
- **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.
|
|
548
|
+
- **connect-src:** The TryMellon API origin (e.g. `https://api.trymellonauth.com`) so register/authenticate requests are not blocked.
|
|
549
|
+
|
|
550
|
+
Minimal example (adjust origins to your environment):
|
|
551
|
+
|
|
552
|
+
```
|
|
553
|
+
Content-Security-Policy: script-src 'self'; connect-src 'self' https://api.trymellonauth.com;
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### SRI (Subresource Integrity)
|
|
557
|
+
|
|
558
|
+
To load `index.global.js` with integrity, generate the SRI hash after building:
|
|
559
|
+
|
|
560
|
+
```bash
|
|
561
|
+
openssl dgst -sha384 -binary dist/index.global.js | openssl base64 -A
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Use the value in the `integrity` attribute:
|
|
565
|
+
|
|
566
|
+
```html
|
|
567
|
+
<script
|
|
568
|
+
src="https://cdn.example.com/trymellon/index.global.js"
|
|
569
|
+
integrity="sha384-<generated-hash>"
|
|
570
|
+
crossorigin="anonymous"
|
|
571
|
+
></script>
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Optional: the build can generate `dist/sri.json` with the hash (see `package.json` script: `"sri": "node ..."`).
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Telemetry (opt-in)
|
|
579
|
+
|
|
580
|
+
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).
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## Additional documentation
|
|
585
|
+
|
|
586
|
+
- [API Reference](./documentation/API.md) – Full API reference
|
|
587
|
+
- [Usage examples](./documentation/EXAMPLES.md) – Practical integration examples
|
|
588
|
+
- [Contributing](./documentation/CONTRIBUTING.md) – How to contribute (including running tests, coverage, Angular, E2E, audit and workflow lint locally)
|
|
589
|
+
- [CI standards (fintech)](./documentation/CI-FINTECH-STANDARDS.md) – Coverage, security, E2E and workflow validation criteria
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Changelog
|
|
594
|
+
|
|
595
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the change history.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## License
|
|
600
|
+
|
|
601
|
+
MIT
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## Philosophy
|
|
606
|
+
|
|
607
|
+
> Implementing Passkeys should take minutes, not weeks.
|
|
608
|
+
> The backend remains yours.
|