@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 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
+ [![npm version](https://img.shields.io/npm/v/@trymellon/js)](https://www.npmjs.com/package/@trymellon/js)
4
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/ResakaGit/trymellon-js/ci.yml)](https://github.com/ResakaGit/trymellon-js/actions)
5
+ [![Coverage](https://img.shields.io/codecov/c/github/ResakaGit/trymellon-js)](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.