ngx-webauthn 0.0.2

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.
@@ -0,0 +1,1058 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, Injectable } from '@angular/core';
3
+ import { throwError, from } from 'rxjs';
4
+ import { map, catchError } from 'rxjs/operators';
5
+
6
+ /**
7
+ * High-level configuration interfaces for WebAuthn operations
8
+ *
9
+ * These interfaces provide a convenient, preset-driven API while still allowing
10
+ * full customization through override properties. They derive from standard WebAuthn
11
+ * types where possible to reduce duplication and ensure compatibility.
12
+ */
13
+ /**
14
+ * Type guard to check if input is a RegisterConfig
15
+ */
16
+ function isRegisterConfig(input) {
17
+ return typeof input === 'object' && input !== null && 'username' in input;
18
+ }
19
+ /**
20
+ * Type guard to check if input is an AuthenticateConfig
21
+ */
22
+ function isAuthenticateConfig(input) {
23
+ return (typeof input === 'object' &&
24
+ input !== null &&
25
+ ('username' in input || 'preset' in input) &&
26
+ !('rp' in input) && // WebAuthn options have 'rp'
27
+ !('user' in input)); // WebAuthn options have 'user'
28
+ }
29
+ /**
30
+ * Type guard to check if input is WebAuthn creation options
31
+ */
32
+ function isCreationOptions(input) {
33
+ return (typeof input === 'object' &&
34
+ input !== null &&
35
+ 'rp' in input &&
36
+ 'user' in input);
37
+ }
38
+ /**
39
+ * Type guard to check if input is WebAuthn request options
40
+ */
41
+ function isRequestOptions(input) {
42
+ return (typeof input === 'object' &&
43
+ input !== null &&
44
+ !('username' in input) &&
45
+ !('rp' in input) &&
46
+ !('user' in input));
47
+ }
48
+
49
+ /**
50
+ * WebAuthn Preset Configurations
51
+ *
52
+ * This file contains predefined configurations for common WebAuthn use cases.
53
+ * These presets provide sensible defaults while remaining fully customizable.
54
+ */
55
+ /**
56
+ * A shared configuration for common, strong, and widely-supported
57
+ * public key credential algorithms.
58
+ */
59
+ const COMMON_PUB_KEY_CRED_PARAMS = {
60
+ pubKeyCredParams: [
61
+ { type: 'public-key', alg: -7 }, // ES256 (ECDSA w/ SHA-256)
62
+ { type: 'public-key', alg: -257 }, // RS256 (RSASSA-PKCS1-v1_5 w/ SHA-256)
63
+ ],
64
+ };
65
+ /**
66
+ * Preset for modern, passwordless, cross-device credentials.
67
+ *
68
+ * Best for: Passkey-based authentication where users can sync credentials
69
+ * across devices and use them for passwordless login.
70
+ *
71
+ * Features:
72
+ * - Requires resident keys (discoverable credentials)
73
+ * - Prefers user verification but doesn't require it
74
+ * - Works with both platform and cross-platform authenticators
75
+ * - Supports credential syncing across devices
76
+ */
77
+ const PASSKEY_PRESET = {
78
+ ...COMMON_PUB_KEY_CRED_PARAMS,
79
+ authenticatorSelection: {
80
+ residentKey: 'required',
81
+ userVerification: 'preferred',
82
+ },
83
+ };
84
+ /**
85
+ * Preset for using a security key as a second factor after a password.
86
+ *
87
+ * Best for: Traditional 2FA scenarios where users already have a password
88
+ * and want to add hardware security key as a second factor.
89
+ *
90
+ * Features:
91
+ * - Discourages resident keys (server-side credential storage)
92
+ * - Prefers user verification
93
+ * - Favors cross-platform authenticators (USB/NFC security keys)
94
+ * - Credentials typically not synced between devices
95
+ */
96
+ const SECOND_FACTOR_PRESET = {
97
+ ...COMMON_PUB_KEY_CRED_PARAMS,
98
+ authenticatorSelection: {
99
+ residentKey: 'discouraged',
100
+ userVerification: 'preferred',
101
+ authenticatorAttachment: 'cross-platform',
102
+ },
103
+ };
104
+ /**
105
+ * Preset for high-security, non-synced, single-device credentials.
106
+ *
107
+ * Best for: High-security scenarios where credentials must stay on a single
108
+ * device and user verification is mandatory.
109
+ *
110
+ * Features:
111
+ * - Requires platform authenticators (built-in biometrics/PIN)
112
+ * - Requires resident keys for discoverability
113
+ * - Requires user verification (biometric/PIN)
114
+ * - Credentials bound to specific device (no syncing)
115
+ */
116
+ const DEVICE_BOUND_PRESET = {
117
+ ...COMMON_PUB_KEY_CRED_PARAMS,
118
+ authenticatorSelection: {
119
+ authenticatorAttachment: 'platform',
120
+ residentKey: 'required',
121
+ userVerification: 'required',
122
+ },
123
+ };
124
+ /**
125
+ * Map of preset names to their configurations
126
+ * Used internally for preset resolution
127
+ */
128
+ const PRESET_MAP = {
129
+ passkey: PASSKEY_PRESET,
130
+ secondFactor: SECOND_FACTOR_PRESET,
131
+ deviceBound: DEVICE_BOUND_PRESET,
132
+ };
133
+
134
+ /**
135
+ * Utility functions for resolving and merging WebAuthn presets
136
+ *
137
+ * These utilities handle the logic of taking a RegisterConfig or AuthenticateConfig,
138
+ * resolving any preset, and merging it with user overrides to produce final
139
+ * WebAuthn options ready for the browser API.
140
+ */
141
+ /**
142
+ * Deep merge utility that properly handles nested objects
143
+ * Later properties override earlier ones
144
+ */
145
+ function deepMerge(target, ...sources) {
146
+ if (!sources.length)
147
+ return target;
148
+ const source = sources.shift();
149
+ if (isObject(target) && isObject(source)) {
150
+ for (const key in source) {
151
+ if (isObject(source[key])) {
152
+ if (!target[key])
153
+ Object.assign(target, { [key]: {} });
154
+ deepMerge(target[key], source[key]);
155
+ }
156
+ else {
157
+ Object.assign(target, { [key]: source[key] });
158
+ }
159
+ }
160
+ }
161
+ return deepMerge(target, ...sources);
162
+ }
163
+ /**
164
+ * Check if a value is a plain object
165
+ */
166
+ function isObject(item) {
167
+ return item && typeof item === 'object' && !Array.isArray(item);
168
+ }
169
+ /**
170
+ * Generate a secure random challenge as Uint8Array
171
+ */
172
+ function generateChallenge$1() {
173
+ return crypto.getRandomValues(new Uint8Array(32));
174
+ }
175
+ /**
176
+ * Generate a user ID from username
177
+ * Uses TextEncoder to convert username to Uint8Array for consistency
178
+ */
179
+ function generateUserId$1(username) {
180
+ return new TextEncoder().encode(username);
181
+ }
182
+ /**
183
+ * Convert challenge to appropriate format
184
+ * Handles both string (base64url) and Uint8Array inputs
185
+ */
186
+ function processChallenge(challenge) {
187
+ if (!challenge) {
188
+ return generateChallenge$1();
189
+ }
190
+ if (typeof challenge === 'string') {
191
+ // Assume base64url string, convert to Uint8Array
192
+ return Uint8Array.from(atob(challenge.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
193
+ }
194
+ return challenge;
195
+ }
196
+ /**
197
+ * Convert user ID to appropriate format
198
+ * Handles both string (base64url) and Uint8Array inputs
199
+ */
200
+ function processUserId(userId, username) {
201
+ if (userId) {
202
+ if (typeof userId === 'string') {
203
+ // Assume base64url string, convert to Uint8Array
204
+ return Uint8Array.from(atob(userId.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
205
+ }
206
+ return userId;
207
+ }
208
+ if (username) {
209
+ return generateUserId$1(username);
210
+ }
211
+ // Fallback to random ID
212
+ return crypto.getRandomValues(new Uint8Array(16));
213
+ }
214
+ /**
215
+ * Convert string credential IDs to PublicKeyCredentialDescriptor format
216
+ */
217
+ function processCredentialDescriptors(credentials) {
218
+ if (!credentials || credentials.length === 0) {
219
+ return undefined;
220
+ }
221
+ // If already in descriptor format, return as-is
222
+ if (typeof credentials[0] === 'object' && 'type' in credentials[0]) {
223
+ return credentials;
224
+ }
225
+ // Convert string IDs to descriptors
226
+ return credentials.map((id) => ({
227
+ type: 'public-key',
228
+ id: Uint8Array.from(atob(id.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0)),
229
+ }));
230
+ }
231
+ /**
232
+ * Resolve a preset configuration by name
233
+ */
234
+ function resolvePreset(presetName) {
235
+ const preset = PRESET_MAP[presetName];
236
+ if (!preset) {
237
+ throw new Error(`Unknown preset: ${presetName}`);
238
+ }
239
+ return preset;
240
+ }
241
+ /**
242
+ * Build complete PublicKeyCredentialCreationOptions from RegisterConfig
243
+ * Now uses WebAuthnConfig for better defaults and relying party information
244
+ */
245
+ function buildCreationOptionsFromConfig(config, webAuthnConfig) {
246
+ // Start with base configuration from WebAuthnConfig
247
+ let options = {
248
+ timeout: webAuthnConfig.defaultTimeout || 60000,
249
+ attestation: webAuthnConfig.defaultAttestation || 'none',
250
+ };
251
+ // Apply preset if specified
252
+ if (config.preset) {
253
+ const preset = resolvePreset(config.preset);
254
+ options = deepMerge(options, {
255
+ authenticatorSelection: preset.authenticatorSelection,
256
+ pubKeyCredParams: [...preset.pubKeyCredParams], // Convert readonly to mutable
257
+ });
258
+ }
259
+ else {
260
+ // Apply default authenticator selection from config
261
+ if (webAuthnConfig.defaultAuthenticatorSelection) {
262
+ options.authenticatorSelection =
263
+ webAuthnConfig.defaultAuthenticatorSelection;
264
+ }
265
+ }
266
+ // Apply user overrides
267
+ if (config.timeout !== undefined) {
268
+ options.timeout = config.timeout;
269
+ }
270
+ if (config.attestation !== undefined) {
271
+ options.attestation = config.attestation;
272
+ }
273
+ if (config.authenticatorSelection !== undefined) {
274
+ options.authenticatorSelection = deepMerge(options.authenticatorSelection || {}, config.authenticatorSelection);
275
+ }
276
+ if (config.pubKeyCredParams !== undefined) {
277
+ options.pubKeyCredParams = config.pubKeyCredParams;
278
+ }
279
+ if (config.extensions !== undefined) {
280
+ options.extensions = config.extensions;
281
+ }
282
+ // Handle required fields
283
+ const challenge = processChallenge(config.challenge);
284
+ const userId = processUserId(config.userId, config.username);
285
+ // Use relying party from config, with user override capability
286
+ const relyingParty = config.rp || webAuthnConfig.relyingParty;
287
+ // Build final options
288
+ const finalOptions = {
289
+ ...options,
290
+ rp: relyingParty,
291
+ user: {
292
+ id: userId,
293
+ name: config.username,
294
+ displayName: config.displayName || config.username,
295
+ },
296
+ challenge,
297
+ pubKeyCredParams: options.pubKeyCredParams ||
298
+ webAuthnConfig.defaultAlgorithms || [
299
+ { type: 'public-key', alg: -7 }, // ES256
300
+ { type: 'public-key', alg: -257 }, // RS256
301
+ ],
302
+ excludeCredentials: processCredentialDescriptors(config.excludeCredentials),
303
+ };
304
+ return finalOptions;
305
+ }
306
+ /**
307
+ * Build complete PublicKeyCredentialRequestOptions from AuthenticateConfig
308
+ * Now uses WebAuthnConfig for better defaults
309
+ */
310
+ function buildRequestOptionsFromConfig(config, webAuthnConfig) {
311
+ // Start with base configuration from WebAuthnConfig
312
+ const options = {
313
+ timeout: webAuthnConfig.defaultTimeout || 60000,
314
+ userVerification: webAuthnConfig.enforceUserVerification
315
+ ? 'required'
316
+ : 'preferred',
317
+ };
318
+ // Apply preset if specified
319
+ if (config.preset) {
320
+ const preset = resolvePreset(config.preset);
321
+ // Extract relevant parts for authentication (userVerification from authenticatorSelection)
322
+ if (preset.authenticatorSelection?.userVerification) {
323
+ options.userVerification = preset.authenticatorSelection.userVerification;
324
+ }
325
+ }
326
+ // Apply user overrides
327
+ if (config.timeout !== undefined) {
328
+ options.timeout = config.timeout;
329
+ }
330
+ if (config.userVerification !== undefined) {
331
+ options.userVerification = config.userVerification;
332
+ }
333
+ if (config.extensions !== undefined) {
334
+ options.extensions = config.extensions;
335
+ }
336
+ // Handle required fields
337
+ const challenge = processChallenge(config.challenge);
338
+ // Build final options
339
+ const finalOptions = {
340
+ ...options,
341
+ challenge,
342
+ allowCredentials: processCredentialDescriptors(config.allowCredentials),
343
+ };
344
+ return finalOptions;
345
+ }
346
+ /**
347
+ * Validate that a RegisterConfig has all required fields
348
+ */
349
+ function validateRegisterConfig(config) {
350
+ if (!config.username || typeof config.username !== 'string') {
351
+ throw new Error('RegisterConfig must have a valid username');
352
+ }
353
+ if (config.preset && !PRESET_MAP[config.preset]) {
354
+ throw new Error(`Invalid preset: ${config.preset}. Valid presets are: ${Object.keys(PRESET_MAP).join(', ')}`);
355
+ }
356
+ }
357
+ /**
358
+ * Validate that an AuthenticateConfig has all required fields
359
+ */
360
+ function validateAuthenticateConfig(config) {
361
+ if (config.preset && !PRESET_MAP[config.preset]) {
362
+ throw new Error(`Invalid preset: ${config.preset}. Valid presets are: ${Object.keys(PRESET_MAP).join(', ')}`);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Enhanced WebAuthn Error Classes
368
+ * Provides specific, actionable error types for better developer experience
369
+ */
370
+ var WebAuthnErrorType;
371
+ (function (WebAuthnErrorType) {
372
+ WebAuthnErrorType["NOT_SUPPORTED"] = "NOT_SUPPORTED";
373
+ WebAuthnErrorType["USER_CANCELLED"] = "USER_CANCELLED";
374
+ WebAuthnErrorType["AUTHENTICATOR_ERROR"] = "AUTHENTICATOR_ERROR";
375
+ WebAuthnErrorType["INVALID_OPTIONS"] = "INVALID_OPTIONS";
376
+ WebAuthnErrorType["UNSUPPORTED_OPERATION"] = "UNSUPPORTED_OPERATION";
377
+ WebAuthnErrorType["NETWORK_ERROR"] = "NETWORK_ERROR";
378
+ WebAuthnErrorType["SECURITY_ERROR"] = "SECURITY_ERROR";
379
+ WebAuthnErrorType["TIMEOUT_ERROR"] = "TIMEOUT_ERROR";
380
+ WebAuthnErrorType["UNKNOWN"] = "UNKNOWN";
381
+ })(WebAuthnErrorType || (WebAuthnErrorType = {}));
382
+ /**
383
+ * Base WebAuthn error class with additional context
384
+ */
385
+ class WebAuthnError extends Error {
386
+ type;
387
+ originalError;
388
+ constructor(type, message, originalError) {
389
+ super(message);
390
+ this.type = type;
391
+ this.originalError = originalError;
392
+ this.name = 'WebAuthnError';
393
+ }
394
+ static fromDOMException(error) {
395
+ const type = WebAuthnError.mapDOMExceptionToType(error.name);
396
+ return new WebAuthnError(type, error.message, error);
397
+ }
398
+ static mapDOMExceptionToType(name) {
399
+ switch (name) {
400
+ case 'NotSupportedError':
401
+ return WebAuthnErrorType.NOT_SUPPORTED;
402
+ case 'NotAllowedError':
403
+ return WebAuthnErrorType.USER_CANCELLED;
404
+ case 'InvalidStateError':
405
+ return WebAuthnErrorType.AUTHENTICATOR_ERROR;
406
+ case 'SecurityError':
407
+ return WebAuthnErrorType.SECURITY_ERROR;
408
+ case 'TimeoutError':
409
+ return WebAuthnErrorType.TIMEOUT_ERROR;
410
+ case 'NetworkError':
411
+ return WebAuthnErrorType.NETWORK_ERROR;
412
+ case 'EncodingError':
413
+ return WebAuthnErrorType.INVALID_OPTIONS;
414
+ default:
415
+ return WebAuthnErrorType.UNKNOWN;
416
+ }
417
+ }
418
+ }
419
+ /**
420
+ * Error thrown when user cancels the WebAuthn operation
421
+ */
422
+ class UserCancelledError extends WebAuthnError {
423
+ constructor(originalError) {
424
+ super(WebAuthnErrorType.USER_CANCELLED, 'User cancelled the WebAuthn operation', originalError);
425
+ this.name = 'UserCancelledError';
426
+ }
427
+ }
428
+ /**
429
+ * Error thrown when there's an issue with the authenticator
430
+ */
431
+ class AuthenticatorError extends WebAuthnError {
432
+ constructor(message, originalError) {
433
+ super(WebAuthnErrorType.AUTHENTICATOR_ERROR, `Authenticator error: ${message}`, originalError);
434
+ this.name = 'AuthenticatorError';
435
+ }
436
+ }
437
+ /**
438
+ * Error thrown when the provided options are invalid
439
+ */
440
+ class InvalidOptionsError extends WebAuthnError {
441
+ constructor(message, originalError) {
442
+ super(WebAuthnErrorType.INVALID_OPTIONS, `Invalid options: ${message}`, originalError);
443
+ this.name = 'InvalidOptionsError';
444
+ }
445
+ }
446
+ /**
447
+ * Error thrown when the requested operation is not supported
448
+ */
449
+ class UnsupportedOperationError extends WebAuthnError {
450
+ constructor(message, originalError) {
451
+ super(WebAuthnErrorType.UNSUPPORTED_OPERATION, `Unsupported operation: ${message}`, originalError);
452
+ this.name = 'UnsupportedOperationError';
453
+ }
454
+ }
455
+ /**
456
+ * Error thrown when there's a network-related issue
457
+ */
458
+ class NetworkError extends WebAuthnError {
459
+ constructor(message, originalError) {
460
+ super(WebAuthnErrorType.NETWORK_ERROR, `Network error: ${message}`, originalError);
461
+ this.name = 'NetworkError';
462
+ }
463
+ }
464
+ /**
465
+ * Error thrown when there's a security-related issue
466
+ */
467
+ class SecurityError extends WebAuthnError {
468
+ constructor(message, originalError) {
469
+ super(WebAuthnErrorType.SECURITY_ERROR, `Security error: ${message}`, originalError);
470
+ this.name = 'SecurityError';
471
+ }
472
+ }
473
+ /**
474
+ * Error thrown when the operation times out
475
+ */
476
+ class TimeoutError extends WebAuthnError {
477
+ constructor(message, originalError) {
478
+ super(WebAuthnErrorType.TIMEOUT_ERROR, `Timeout error: ${message}`, originalError);
479
+ this.name = 'TimeoutError';
480
+ }
481
+ }
482
+
483
+ /**
484
+ * WebAuthn Configuration
485
+ * Provides configuration interfaces and injection token for WebAuthn service
486
+ */
487
+ /**
488
+ * Default configuration for WebAuthn service
489
+ * Note: relyingParty must be provided by the application
490
+ */
491
+ const DEFAULT_WEBAUTHN_CONFIG = {
492
+ defaultTimeout: 60000,
493
+ defaultAlgorithms: [
494
+ { type: 'public-key', alg: -7 }, // ES256 (ECDSA w/ SHA-256)
495
+ { type: 'public-key', alg: -257 }, // RS256 (RSASSA-PKCS1-v1_5 w/ SHA-256)
496
+ ],
497
+ enforceUserVerification: false,
498
+ defaultAttestation: 'none',
499
+ defaultAuthenticatorSelection: {
500
+ userVerification: 'preferred',
501
+ },
502
+ };
503
+ /**
504
+ * Injection token for WebAuthn configuration
505
+ */
506
+ const WEBAUTHN_CONFIG = new InjectionToken('WEBAUTHN_CONFIG');
507
+ /**
508
+ * Creates a complete WebAuthn configuration with required relying party information
509
+ * @param relyingParty Required relying party configuration
510
+ * @param overrides Optional configuration overrides
511
+ */
512
+ function createWebAuthnConfig(relyingParty, overrides) {
513
+ return {
514
+ relyingParty,
515
+ ...DEFAULT_WEBAUTHN_CONFIG,
516
+ ...overrides,
517
+ };
518
+ }
519
+
520
+ /**
521
+ * WebAuthn Utility Functions
522
+ * Centralized utilities for ArrayBuffer conversions and WebAuthn-specific operations
523
+ * This is the single source of truth for data transformation functions
524
+ */
525
+ /**
526
+ * Converts a string to ArrayBuffer
527
+ */
528
+ function stringToArrayBuffer(str) {
529
+ const encoder = new TextEncoder();
530
+ return encoder.encode(str);
531
+ }
532
+ /**
533
+ * Converts ArrayBuffer to string
534
+ */
535
+ function arrayBufferToString(buffer) {
536
+ const decoder = new TextDecoder();
537
+ return decoder.decode(buffer);
538
+ }
539
+ /**
540
+ * Converts ArrayBuffer to base64 string
541
+ */
542
+ function arrayBufferToBase64(buffer) {
543
+ const bytes = new Uint8Array(buffer);
544
+ let binary = '';
545
+ for (let i = 0; i < bytes.byteLength; i++) {
546
+ binary += String.fromCharCode(bytes[i]);
547
+ }
548
+ return btoa(binary);
549
+ }
550
+ /**
551
+ * Converts base64 string to ArrayBuffer
552
+ */
553
+ function base64ToArrayBuffer(base64) {
554
+ const binary = atob(base64);
555
+ const bytes = new Uint8Array(binary.length);
556
+ for (let i = 0; i < binary.length; i++) {
557
+ bytes[i] = binary.charCodeAt(i);
558
+ }
559
+ return bytes.buffer;
560
+ }
561
+ /**
562
+ * Converts base64url string to ArrayBuffer
563
+ */
564
+ function base64urlToArrayBuffer(base64url) {
565
+ // Convert base64url to base64
566
+ const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
567
+ // Add padding if necessary
568
+ const padded = base64 + '==='.slice(0, (4 - (base64.length % 4)) % 4);
569
+ return base64ToArrayBuffer(padded);
570
+ }
571
+ /**
572
+ * Converts ArrayBuffer to base64url string
573
+ * This is the canonical implementation used throughout the library
574
+ */
575
+ function arrayBufferToBase64url(buffer) {
576
+ const base64 = arrayBufferToBase64(buffer);
577
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
578
+ }
579
+ /**
580
+ * Generates a random challenge as ArrayBuffer
581
+ */
582
+ function generateChallenge(length = 32) {
583
+ const array = new Uint8Array(length);
584
+ crypto.getRandomValues(array);
585
+ return array.buffer;
586
+ }
587
+ /**
588
+ * Generates a random user ID as ArrayBuffer
589
+ */
590
+ function generateUserId(length = 32) {
591
+ const array = new Uint8Array(length);
592
+ crypto.getRandomValues(array);
593
+ return array.buffer;
594
+ }
595
+ /**
596
+ * Converts credential ID string to ArrayBuffer for WebAuthn API
597
+ */
598
+ function credentialIdToArrayBuffer(credentialId) {
599
+ return base64urlToArrayBuffer(credentialId);
600
+ }
601
+ /**
602
+ * Converts ArrayBuffer credential ID to string
603
+ */
604
+ function arrayBufferToCredentialId(buffer) {
605
+ return arrayBufferToBase64url(buffer);
606
+ }
607
+ /**
608
+ * Checks if the current environment supports WebAuthn
609
+ */
610
+ function isWebAuthnSupported() {
611
+ return !!(typeof window !== 'undefined' &&
612
+ window.PublicKeyCredential &&
613
+ typeof navigator !== 'undefined' &&
614
+ navigator.credentials &&
615
+ typeof navigator.credentials.create === 'function' &&
616
+ typeof navigator.credentials.get === 'function');
617
+ }
618
+ /**
619
+ * Checks if platform authenticator is available
620
+ */
621
+ async function isPlatformAuthenticatorAvailable() {
622
+ if (!isWebAuthnSupported()) {
623
+ return false;
624
+ }
625
+ try {
626
+ return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
627
+ }
628
+ catch {
629
+ return false;
630
+ }
631
+ }
632
+ /**
633
+ * Gets supported authenticator transports for this platform
634
+ * Enhanced detection with better browser compatibility
635
+ */
636
+ function getSupportedTransports() {
637
+ const transports = ['usb', 'internal'];
638
+ // Add NFC support for Android devices
639
+ if (typeof navigator !== 'undefined' &&
640
+ /Android/i.test(navigator.userAgent)) {
641
+ transports.push('nfc');
642
+ }
643
+ // Add BLE support for modern browsers with Web Bluetooth API
644
+ if (typeof navigator !== 'undefined' && navigator.bluetooth) {
645
+ transports.push('ble');
646
+ }
647
+ return transports;
648
+ }
649
+ /**
650
+ * Validates that required WebAuthn options are present
651
+ */
652
+ function validateRegistrationOptions(options) {
653
+ if (!options.user) {
654
+ throw new Error('User information is required for registration');
655
+ }
656
+ if (!options.user.id) {
657
+ throw new Error('User ID is required for registration');
658
+ }
659
+ if (!options.user.name) {
660
+ throw new Error('User name is required for registration');
661
+ }
662
+ if (!options.user.displayName) {
663
+ throw new Error('User display name is required for registration');
664
+ }
665
+ if (!options.rp) {
666
+ throw new Error('Relying party information is required for registration');
667
+ }
668
+ if (!options.rp.name) {
669
+ throw new Error('Relying party name is required for registration');
670
+ }
671
+ }
672
+ /**
673
+ * Creates default public key credential parameters
674
+ */
675
+ function getDefaultPubKeyCredParams() {
676
+ return [
677
+ {
678
+ type: 'public-key',
679
+ alg: -7, // ES256
680
+ },
681
+ {
682
+ type: 'public-key',
683
+ alg: -257, // RS256
684
+ },
685
+ ];
686
+ }
687
+ /**
688
+ * Enhanced type guard to detect JSON-formatted WebAuthn options
689
+ * More robust than simple challenge type checking
690
+ */
691
+ function isJSONOptions(options) {
692
+ if (!options || typeof options !== 'object') {
693
+ return false;
694
+ }
695
+ // Check multiple indicators that this is JSON format (base64url strings)
696
+ return (typeof options.challenge === 'string' ||
697
+ (options.user && typeof options.user.id === 'string') ||
698
+ (options.allowCredentials?.length > 0 &&
699
+ typeof options.allowCredentials[0]?.id === 'string') ||
700
+ (options.excludeCredentials?.length > 0 &&
701
+ typeof options.excludeCredentials[0]?.id === 'string'));
702
+ }
703
+ /**
704
+ * Type guard to check if input is a PublicKeyCredential
705
+ */
706
+ function isPublicKeyCredential(credential) {
707
+ return credential !== null && credential.type === 'public-key';
708
+ }
709
+
710
+ /**
711
+ * Enhanced WebAuthn Service
712
+ *
713
+ * Provides a clean, high-level API for WebAuthn operations with:
714
+ * - Modern inject() pattern instead of constructor DI
715
+ * - Flexible options (JSON base64url strings OR native ArrayBuffers)
716
+ * - Enhanced error handling with specific error types
717
+ * - Native browser parsing functions for optimal performance
718
+ * - Clean, developer-friendly response objects
719
+ */
720
+ /**
721
+ * Enhanced Angular service for WebAuthn operations
722
+ * Provides a clean abstraction over the WebAuthn API with RxJS observables
723
+ * and enhanced error handling
724
+ */
725
+ class WebAuthnService {
726
+ config = inject(WEBAUTHN_CONFIG);
727
+ /**
728
+ * Checks if WebAuthn is supported in the current browser
729
+ */
730
+ isSupported() {
731
+ return isWebAuthnSupported();
732
+ }
733
+ /**
734
+ * Gets comprehensive WebAuthn support information
735
+ */
736
+ getSupport() {
737
+ if (!this.isSupported()) {
738
+ return throwError(() => new UnsupportedOperationError('WebAuthn is not supported in this browser'));
739
+ }
740
+ return from(PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()).pipe(map((isPlatformAvailable) => ({
741
+ isSupported: true,
742
+ isPlatformAuthenticatorAvailable: isPlatformAvailable,
743
+ supportedTransports: getSupportedTransports(),
744
+ })), catchError((error) => throwError(() => new WebAuthnError(WebAuthnErrorType.UNKNOWN, 'Failed to check WebAuthn support', error))));
745
+ }
746
+ /**
747
+ * Registers a new WebAuthn credential with flexible configuration support
748
+ *
749
+ * @param input Either a high-level RegisterConfig with presets, or direct WebAuthn creation options
750
+ * @returns Observable of RegistrationResponse with clean, developer-friendly format
751
+ *
752
+ * @example
753
+ * ```typescript
754
+ * // Simple preset usage
755
+ * this.webAuthnService.register({ username: 'john.doe', preset: 'passkey' });
756
+ *
757
+ * // Preset with overrides
758
+ * this.webAuthnService.register({
759
+ * username: 'john.doe',
760
+ * preset: 'passkey',
761
+ * authenticatorSelection: { userVerification: 'required' }
762
+ * });
763
+ *
764
+ * // Direct WebAuthn options (native)
765
+ * const nativeOptions: PublicKeyCredentialCreationOptions = {
766
+ * challenge: new Uint8Array([...]),
767
+ * rp: { name: "My App" },
768
+ * user: { id: new Uint8Array([...]), name: "user@example.com", displayName: "User" },
769
+ * pubKeyCredParams: [{ type: "public-key", alg: -7 }]
770
+ * };
771
+ * this.webAuthnService.register(nativeOptions);
772
+ *
773
+ * // Direct WebAuthn options (JSON)
774
+ * const jsonOptions: PublicKeyCredentialCreationOptionsJSON = {
775
+ * challenge: "Y2hhbGxlbmdl",
776
+ * rp: { name: "My App" },
777
+ * user: { id: "dXNlcklk", name: "user@example.com", displayName: "User" },
778
+ * pubKeyCredParams: [{ type: "public-key", alg: -7 }]
779
+ * };
780
+ * this.webAuthnService.register(jsonOptions);
781
+ * ```
782
+ */
783
+ register(input) {
784
+ if (!this.isSupported()) {
785
+ return throwError(() => new UnsupportedOperationError('WebAuthn is not supported in this browser'));
786
+ }
787
+ try {
788
+ let creationOptions;
789
+ if (isRegisterConfig(input)) {
790
+ // High-level config path: validate, resolve preset, build options
791
+ validateRegisterConfig(input);
792
+ creationOptions = buildCreationOptionsFromConfig(input, this.config);
793
+ }
794
+ else {
795
+ // Direct options path: use provided options
796
+ creationOptions = input;
797
+ }
798
+ const parsedOptions = this.parseRegistrationOptions(creationOptions);
799
+ return from(navigator.credentials.create({ publicKey: parsedOptions })).pipe(map((credential) => this.processRegistrationResult(credential)), catchError((error) => this.handleWebAuthnError(error)));
800
+ }
801
+ catch (error) {
802
+ return throwError(() => new InvalidOptionsError('Failed to process registration input', error));
803
+ }
804
+ }
805
+ /**
806
+ * Authenticates using an existing WebAuthn credential with flexible configuration support
807
+ *
808
+ * @param input Either a high-level AuthenticateConfig with presets, or direct WebAuthn request options
809
+ * @returns Observable of AuthenticationResponse with clean, developer-friendly format
810
+ *
811
+ * @example
812
+ * ```typescript
813
+ * // Simple preset usage
814
+ * this.webAuthnService.authenticate({ preset: 'passkey' });
815
+ *
816
+ * // Config with credential filtering
817
+ * this.webAuthnService.authenticate({
818
+ * username: 'john.doe',
819
+ * preset: 'secondFactor',
820
+ * allowCredentials: ['credential-id-1', 'credential-id-2']
821
+ * });
822
+ *
823
+ * // Direct WebAuthn options (JSON)
824
+ * const jsonOptions: PublicKeyCredentialRequestOptionsJSON = {
825
+ * challenge: "Y2hhbGxlbmdl",
826
+ * allowCredentials: [{
827
+ * type: "public-key",
828
+ * id: "Y3JlZElk"
829
+ * }]
830
+ * };
831
+ * this.webAuthnService.authenticate(jsonOptions);
832
+ *
833
+ * // Direct WebAuthn options (native)
834
+ * const nativeOptions: PublicKeyCredentialRequestOptions = {
835
+ * challenge: new Uint8Array([...]),
836
+ * allowCredentials: [{
837
+ * type: "public-key",
838
+ * id: new Uint8Array([...])
839
+ * }]
840
+ * };
841
+ * this.webAuthnService.authenticate(nativeOptions);
842
+ * ```
843
+ */
844
+ authenticate(input) {
845
+ if (!this.isSupported()) {
846
+ return throwError(() => new UnsupportedOperationError('WebAuthn is not supported in this browser'));
847
+ }
848
+ try {
849
+ let requestOptions;
850
+ if (isAuthenticateConfig(input)) {
851
+ // High-level config path: validate, resolve preset, build options
852
+ validateAuthenticateConfig(input);
853
+ requestOptions = buildRequestOptionsFromConfig(input, this.config);
854
+ }
855
+ else {
856
+ // Direct options path: use provided options
857
+ requestOptions = input;
858
+ }
859
+ const parsedOptions = this.parseAuthenticationOptions(requestOptions);
860
+ return from(navigator.credentials.get({ publicKey: parsedOptions })).pipe(map((credential) => this.processAuthenticationResult(credential)), catchError((error) => this.handleWebAuthnError(error)));
861
+ }
862
+ catch (error) {
863
+ return throwError(() => new InvalidOptionsError('Failed to process authentication input', error));
864
+ }
865
+ }
866
+ /**
867
+ * Parses registration options, handling both JSON and native formats
868
+ */
869
+ parseRegistrationOptions(options) {
870
+ if (isJSONOptions(options)) {
871
+ // Use native browser function for JSON options
872
+ return PublicKeyCredential.parseCreationOptionsFromJSON(options);
873
+ }
874
+ else {
875
+ // Options are already in native format
876
+ return options;
877
+ }
878
+ }
879
+ /**
880
+ * Parses authentication options, handling both JSON and native formats
881
+ */
882
+ parseAuthenticationOptions(options) {
883
+ if (isJSONOptions(options)) {
884
+ // Use native browser function for JSON options
885
+ return PublicKeyCredential.parseRequestOptionsFromJSON(options);
886
+ }
887
+ else {
888
+ // Options are already in native format
889
+ return options;
890
+ }
891
+ }
892
+ /**
893
+ * Processes the raw credential result into a clean RegistrationResponse
894
+ */
895
+ processRegistrationResult(credential) {
896
+ if (!isPublicKeyCredential(credential)) {
897
+ throw new AuthenticatorError('No credential returned from authenticator');
898
+ }
899
+ const response = credential.response;
900
+ // Extract data using the response methods
901
+ const credentialId = arrayBufferToBase64url(credential.rawId);
902
+ const transports = (response.getTransports?.() ||
903
+ []);
904
+ let publicKey;
905
+ try {
906
+ const publicKeyBuffer = response.getPublicKey?.();
907
+ if (publicKeyBuffer) {
908
+ publicKey = arrayBufferToBase64url(publicKeyBuffer);
909
+ }
910
+ }
911
+ catch {
912
+ // Public key extraction failed - this is okay, not all algorithms are supported
913
+ }
914
+ // Create the raw response for backward compatibility
915
+ const rawResponse = {
916
+ credentialId,
917
+ publicKey: publicKey ||
918
+ arrayBufferToBase64url(response.getPublicKey?.() || new ArrayBuffer(0)),
919
+ attestationObject: arrayBufferToBase64url(response.attestationObject),
920
+ clientDataJSON: arrayBufferToBase64url(response.clientDataJSON),
921
+ transports: transports,
922
+ };
923
+ return {
924
+ success: true,
925
+ credentialId,
926
+ publicKey,
927
+ transports,
928
+ rawResponse,
929
+ };
930
+ }
931
+ /**
932
+ * Processes the raw credential result into a clean AuthenticationResponse
933
+ */
934
+ processAuthenticationResult(credential) {
935
+ if (!isPublicKeyCredential(credential)) {
936
+ throw new AuthenticatorError('No credential returned from authenticator');
937
+ }
938
+ const response = credential.response;
939
+ const credentialId = arrayBufferToBase64url(credential.rawId);
940
+ let userHandle;
941
+ if (response.userHandle) {
942
+ userHandle = arrayBufferToBase64url(response.userHandle);
943
+ }
944
+ // Create the raw response for backward compatibility
945
+ const rawResponse = {
946
+ credentialId,
947
+ authenticatorData: arrayBufferToBase64url(response.authenticatorData),
948
+ clientDataJSON: arrayBufferToBase64url(response.clientDataJSON),
949
+ signature: arrayBufferToBase64url(response.signature),
950
+ userHandle,
951
+ };
952
+ return {
953
+ success: true,
954
+ credentialId,
955
+ userHandle,
956
+ rawResponse,
957
+ };
958
+ }
959
+ /**
960
+ * Enhanced error handling that maps DOMExceptions to specific error types
961
+ */
962
+ handleWebAuthnError(error) {
963
+ // Handle DOMExceptions from WebAuthn API
964
+ if (error instanceof DOMException) {
965
+ switch (error.name) {
966
+ case 'NotAllowedError':
967
+ return throwError(() => new UserCancelledError(error));
968
+ case 'InvalidStateError':
969
+ return throwError(() => new AuthenticatorError('Invalid authenticator state', error));
970
+ case 'NotSupportedError':
971
+ return throwError(() => new UnsupportedOperationError('Operation not supported', error));
972
+ case 'SecurityError':
973
+ return throwError(() => new SecurityError('Security error occurred', error));
974
+ case 'TimeoutError':
975
+ return throwError(() => new TimeoutError('Operation timed out', error));
976
+ case 'EncodingError':
977
+ return throwError(() => new InvalidOptionsError('Encoding error in options', error));
978
+ default:
979
+ return throwError(() => new WebAuthnError(WebAuthnErrorType.UNKNOWN, `Unknown WebAuthn error: ${error.message}`, error));
980
+ }
981
+ }
982
+ // Handle JSON parsing errors
983
+ if (error instanceof TypeError &&
984
+ (error.message.includes('parseCreationOptionsFromJSON') ||
985
+ error.message.includes('parseRequestOptionsFromJSON'))) {
986
+ return throwError(() => new InvalidOptionsError('Invalid JSON options format', error));
987
+ }
988
+ // Handle other errors
989
+ return throwError(() => new WebAuthnError(WebAuthnErrorType.UNKNOWN, `Unexpected error: ${error.message}`, error));
990
+ }
991
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: WebAuthnService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
992
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: WebAuthnService, providedIn: 'root' });
993
+ }
994
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: WebAuthnService, decorators: [{
995
+ type: Injectable,
996
+ args: [{
997
+ providedIn: 'root',
998
+ }]
999
+ }] });
1000
+
1001
+ /**
1002
+ * WebAuthn Providers
1003
+ * Modern Angular standalone provider function for WebAuthn service
1004
+ */
1005
+ /**
1006
+ * Provides WebAuthn service with required relying party configuration
1007
+ *
1008
+ * @param relyingParty Required relying party configuration
1009
+ * @param config Optional configuration overrides
1010
+ * @returns Array of providers for WebAuthn functionality
1011
+ *
1012
+ * @example
1013
+ * ```typescript
1014
+ * // main.ts
1015
+ * bootstrapApplication(AppComponent, {
1016
+ * providers: [
1017
+ * provideWebAuthn(
1018
+ * { name: 'My App', id: 'myapp.com' },
1019
+ * { defaultTimeout: 30000 }
1020
+ * )
1021
+ * ]
1022
+ * });
1023
+ * ```
1024
+ */
1025
+ function provideWebAuthn(relyingParty, config = {}) {
1026
+ return [
1027
+ {
1028
+ provide: WEBAUTHN_CONFIG,
1029
+ useValue: createWebAuthnConfig(relyingParty, config),
1030
+ },
1031
+ WebAuthnService,
1032
+ ];
1033
+ }
1034
+ /**
1035
+ * @deprecated Use provideWebAuthn(relyingParty, config) instead.
1036
+ * This version is kept for backward compatibility but requires relying party information.
1037
+ */
1038
+ function provideWebAuthnLegacy(config) {
1039
+ if (!config.relyingParty) {
1040
+ throw new Error('WebAuthn configuration must include relying party information. Use provideWebAuthn(relyingParty, config) instead.');
1041
+ }
1042
+ return [
1043
+ {
1044
+ provide: WEBAUTHN_CONFIG,
1045
+ useValue: config,
1046
+ },
1047
+ WebAuthnService,
1048
+ ];
1049
+ }
1050
+
1051
+ // Core service
1052
+
1053
+ /**
1054
+ * Generated bundle index. Do not edit.
1055
+ */
1056
+
1057
+ export { AuthenticatorError, DEFAULT_WEBAUTHN_CONFIG, DEVICE_BOUND_PRESET, InvalidOptionsError, NetworkError, PASSKEY_PRESET, PRESET_MAP, SECOND_FACTOR_PRESET, SecurityError, TimeoutError, UnsupportedOperationError, UserCancelledError, WEBAUTHN_CONFIG, WebAuthnError, WebAuthnErrorType, WebAuthnService, arrayBufferToBase64, arrayBufferToBase64url, arrayBufferToCredentialId, arrayBufferToString, base64ToArrayBuffer, base64urlToArrayBuffer, createWebAuthnConfig, credentialIdToArrayBuffer, generateChallenge, generateUserId, getDefaultPubKeyCredParams, getSupportedTransports, isJSONOptions, isPlatformAuthenticatorAvailable, isPublicKeyCredential, isWebAuthnSupported, provideWebAuthn, provideWebAuthnLegacy, stringToArrayBuffer, validateRegistrationOptions };
1058
+ //# sourceMappingURL=ngx-webauthn.mjs.map