tempest-react-sdk 0.39.0 → 0.40.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/dist/_virtual/_rolldown/runtime.cjs +1 -1
- package/dist/access/access-control-context.cjs.map +1 -1
- package/dist/access/use-can.cjs.map +1 -1
- package/dist/app/AppProviders.cjs.map +1 -1
- package/dist/app/AppProviders.js.map +1 -1
- package/dist/audio/use-audio-recorder.cjs.map +1 -1
- package/dist/audio/use-audio.cjs.map +1 -1
- package/dist/audio/use-microphone.cjs.map +1 -1
- package/dist/auth/create-auth-store.cjs.map +1 -1
- package/dist/auth/lazy-with-retry.cjs.map +1 -1
- package/dist/auth/passkey.cjs.map +1 -1
- package/dist/auth/passkey.js.map +1 -1
- package/dist/auth/use-passkey.cjs.map +1 -1
- package/dist/br/BrazilMap.cjs +1 -1
- package/dist/br/BrazilMap.cjs.map +1 -1
- package/dist/br/BrazilMap.js +2 -2
- package/dist/br/BrazilMap.js.map +1 -1
- package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
- package/dist/br/BrazilStateCitySelect.js.map +1 -1
- package/dist/br/BrazilStateMap.cjs +1 -1
- package/dist/br/BrazilStateMap.cjs.map +1 -1
- package/dist/br/BrazilStateMap.js +2 -2
- package/dist/br/BrazilStateMap.js.map +1 -1
- package/dist/br/MapLegend.cjs.map +1 -1
- package/dist/br/MapLegend.js.map +1 -1
- package/dist/br/MapMarkers.cjs.map +1 -1
- package/dist/br/MapMarkers.js.map +1 -1
- package/dist/br/MunicipalitySearch.cjs.map +1 -1
- package/dist/br/MunicipalitySearch.js.map +1 -1
- package/dist/br/PixQRCode.cjs.map +1 -1
- package/dist/br/PixQRCode.js.map +1 -1
- package/dist/br/use-map-hover.cjs.map +1 -1
- package/dist/br/use-map-zoom.cjs.map +1 -1
- package/dist/capture/media-recorder.cjs +1 -1
- package/dist/capture/media-recorder.js +2 -2
- package/dist/capture/use-barcode-scanner.cjs +1 -1
- package/dist/capture/use-barcode-scanner.cjs.map +1 -1
- package/dist/capture/use-barcode-scanner.js +2 -2
- package/dist/capture/use-barcode-scanner.js.map +1 -1
- package/dist/capture/use-screen-capture.cjs.map +1 -1
- package/dist/capture/use-speech-recognition.cjs.map +1 -1
- package/dist/capture/use-torch.cjs.map +1 -1
- package/dist/capture/use-video-recorder.cjs.map +1 -1
- package/dist/charts/AreaChart.cjs.map +1 -1
- package/dist/charts/AreaChart.js.map +1 -1
- package/dist/charts/BarChart.cjs.map +1 -1
- package/dist/charts/BarChart.js.map +1 -1
- package/dist/charts/LineChart.cjs.map +1 -1
- package/dist/charts/LineChart.js.map +1 -1
- package/dist/charts/PieChart.cjs.map +1 -1
- package/dist/charts/PieChart.js.map +1 -1
- package/dist/charts/RadarChart.cjs.map +1 -1
- package/dist/charts/RadarChart.js.map +1 -1
- package/dist/charts/use-chart-colors.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/Accordion/Accordion.cjs.map +1 -1
- package/dist/components/Accordion/Accordion.js.map +1 -1
- package/dist/components/Alert/Alert.cjs.map +1 -1
- package/dist/components/Alert/Alert.js.map +1 -1
- package/dist/components/AppBar/AppBar.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.js.map +1 -1
- package/dist/components/AppShell/AppShell.cjs.map +1 -1
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
- package/dist/components/Avatar/Avatar.cjs.map +1 -1
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/AvatarGroup/AvatarGroup.cjs.map +1 -1
- package/dist/components/AvatarGroup/AvatarGroup.js.map +1 -1
- package/dist/components/Badge/Badge.cjs.map +1 -1
- package/dist/components/Badge/Badge.js.map +1 -1
- package/dist/components/Banner/Banner.cjs.map +1 -1
- package/dist/components/Banner/Banner.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.cjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.js.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.js.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.cjs +1 -1
- package/dist/components/Calendar/Calendar.cjs.map +1 -1
- package/dist/components/Calendar/Calendar.js +38 -41
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Card/Card.cjs.map +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/Carousel/Carousel.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.js.map +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/Chat/ChatComposer.cjs.map +1 -1
- package/dist/components/Chat/ChatComposer.js.map +1 -1
- package/dist/components/Chat/chat-groups.cjs +1 -1
- package/dist/components/Chat/chat-groups.cjs.map +1 -1
- package/dist/components/Chat/chat-groups.js +2 -2
- package/dist/components/Chat/chat-groups.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.cjs.map +1 -1
- package/dist/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
- package/dist/components/ChipInput/ChipInput.js.map +1 -1
- package/dist/components/ClickOutside/ClickOutside.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/Collapsible/Collapsible.cjs.map +1 -1
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/Combobox/Combobox.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.cjs.map +1 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
- package/dist/components/CopyButton/CopyButton.js.map +1 -1
- package/dist/components/DataList/DataList.cjs.map +1 -1
- package/dist/components/DataList/DataList.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +2 -2
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs.map +1 -1
- package/dist/components/DataTable/EditableCell.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.cjs.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.js.map +1 -1
- package/dist/components/Divider/Divider.cjs.map +1 -1
- package/dist/components/Divider/Divider.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.js.map +1 -1
- package/dist/components/EmptyState/EmptyState.cjs.map +1 -1
- package/dist/components/EmptyState/EmptyState.js.map +1 -1
- package/dist/components/ErrorState/ErrorState.cjs.map +1 -1
- package/dist/components/ErrorState/ErrorState.js.map +1 -1
- package/dist/components/FileUpload/FileUpload.cjs +1 -1
- package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
- package/dist/components/FileUpload/FileUpload.js +1 -1
- package/dist/components/FileUpload/FileUpload.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.js.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.cjs.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.js.map +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/HoverCard/HoverCard.cjs.map +1 -1
- package/dist/components/HoverCard/HoverCard.js.map +1 -1
- package/dist/components/Image/Image.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.js +1 -1
- package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
- package/dist/components/Input/Input.cjs.map +1 -1
- package/dist/components/Input/Input.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
- package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
- package/dist/components/InstallButton/InstallButton.js.map +1 -1
- package/dist/components/Kanban/Kanban.cjs.map +1 -1
- package/dist/components/Kanban/Kanban.js.map +1 -1
- package/dist/components/Label/Label.cjs.map +1 -1
- package/dist/components/Label/Label.js.map +1 -1
- package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
- package/dist/components/Lightbox/Lightbox.js.map +1 -1
- package/dist/components/ListTile/ListTile.cjs.map +1 -1
- package/dist/components/ListTile/ListTile.js.map +1 -1
- package/dist/components/Markdown/Markdown.cjs +1 -1
- package/dist/components/Markdown/Markdown.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.js +2 -2
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/Masonry/Masonry.cjs.map +1 -1
- package/dist/components/Masonry/Masonry.js.map +1 -1
- package/dist/components/Menubar/Menubar.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.js.map +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
- package/dist/components/Money/Money.cjs.map +1 -1
- package/dist/components/Money/Money.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/dist/components/NProgress/NProgress.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.cjs.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
- package/dist/components/NotificationCenter/use-notification-inbox.cjs.map +1 -1
- package/dist/components/OfflineIndicator/OfflineIndicator.cjs.map +1 -1
- package/dist/components/OfflineIndicator/OfflineIndicator.js.map +1 -1
- package/dist/components/Page/Page.cjs.map +1 -1
- package/dist/components/Page/Page.js.map +1 -1
- package/dist/components/Pagination/Pagination.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
- package/dist/components/PinInput/PinInput.cjs.map +1 -1
- package/dist/components/PinInput/PinInput.js.map +1 -1
- package/dist/components/Popover/Popover.cjs.map +1 -1
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Portal/Portal.cjs.map +1 -1
- package/dist/components/Progress/Progress.cjs.map +1 -1
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/QRCode/QRCode.cjs.map +1 -1
- package/dist/components/QRCode/QRCode.js.map +1 -1
- package/dist/components/Radio/Radio.cjs.map +1 -1
- package/dist/components/Radio/Radio.js.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
- package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
- package/dist/components/RatingStars/RatingStars.js.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
- package/dist/components/RelativeTime/RelativeTime.cjs.map +1 -1
- package/dist/components/RelativeTime/RelativeTime.js.map +1 -1
- package/dist/components/Resizable/Resizable.cjs.map +1 -1
- package/dist/components/Resizable/Resizable.js.map +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.cjs.map +1 -1
- package/dist/components/SearchBar/SearchBar.cjs.map +1 -1
- package/dist/components/SearchBar/SearchBar.js.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.cjs.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/components/Select/Select.cjs.map +1 -1
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
- package/dist/components/Slider/Slider.cjs.map +1 -1
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
- package/dist/components/Sparkline/Sparkline.js.map +1 -1
- package/dist/components/Spinner/Spinner.cjs.map +1 -1
- package/dist/components/Spinner/Spinner.js.map +1 -1
- package/dist/components/Stat/Stat.cjs.map +1 -1
- package/dist/components/Stat/Stat.js.map +1 -1
- package/dist/components/Stepper/Stepper.cjs.map +1 -1
- package/dist/components/Stepper/Stepper.js.map +1 -1
- package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
- package/dist/components/StepperInput/StepperInput.js.map +1 -1
- package/dist/components/Switch/Switch.cjs.map +1 -1
- package/dist/components/Switch/Switch.js.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js.map +1 -1
- package/dist/components/Table/Table.cjs.map +1 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tabs/Tabs.cjs.map +1 -1
- package/dist/components/Tabs/Tabs.js.map +1 -1
- package/dist/components/Tag/Tag.cjs.map +1 -1
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Textarea/Textarea.cjs.map +1 -1
- package/dist/components/Textarea/Textarea.js.map +1 -1
- package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
- package/dist/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/components/Timeline/Timeline.cjs.map +1 -1
- package/dist/components/Timeline/Timeline.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/components/Toggle/Toggle.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.cjs.map +1 -1
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/Tour/Tour.cjs.map +1 -1
- package/dist/components/Tour/Tour.js.map +1 -1
- package/dist/components/Transfer/Transfer.cjs.map +1 -1
- package/dist/components/Transfer/Transfer.js.map +1 -1
- package/dist/components/TreeView/TreeView.cjs +1 -1
- package/dist/components/TreeView/TreeView.cjs.map +1 -1
- package/dist/components/TreeView/TreeView.js +1 -4
- package/dist/components/TreeView/TreeView.js.map +1 -1
- package/dist/components/UpdatePrompt/UpdatePrompt.cjs.map +1 -1
- package/dist/components/UpdatePrompt/UpdatePrompt.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/components/VisuallyHidden/VisuallyHidden.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.js.map +1 -1
- package/dist/data/data-provider-context.cjs.map +1 -1
- package/dist/data/use-resource.cjs.map +1 -1
- package/dist/editor/RichTextEditor.cjs.map +1 -1
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/error-boundary/use-error-handler.cjs.map +1 -1
- package/dist/feature-flags/FeatureFlagsProvider.cjs.map +1 -1
- package/dist/forms/FormField.cjs.map +1 -1
- package/dist/forms/masked-inputs.cjs +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/masked-inputs.js +1 -1
- package/dist/forms/masked-inputs.js.map +1 -1
- package/dist/forms/use-viacep.cjs.map +1 -1
- package/dist/forms/use-zod-form.cjs.map +1 -1
- package/dist/forms/zod-resolver.cjs.map +1 -1
- package/dist/forms/zod-resolver.js.map +1 -1
- package/dist/geo/TrajectoryMap.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.js.map +1 -1
- package/dist/geo/leaflet-map.cjs.map +1 -1
- package/dist/geo/leaflet-map.js.map +1 -1
- package/dist/geo/use-position-tracker.cjs.map +1 -1
- package/dist/hooks/use-announce.cjs.map +1 -1
- package/dist/hooks/use-async.cjs.map +1 -1
- package/dist/hooks/use-before-install-prompt.cjs.map +1 -1
- package/dist/hooks/use-breakpoint.cjs.map +1 -1
- package/dist/hooks/use-click-outside.cjs.map +1 -1
- package/dist/hooks/use-client-filter.cjs.map +1 -1
- package/dist/hooks/use-clipboard.cjs.map +1 -1
- package/dist/hooks/use-counter.cjs.map +1 -1
- package/dist/hooks/use-debounce.cjs.map +1 -1
- package/dist/hooks/use-deep-memo.cjs.map +1 -1
- package/dist/hooks/use-disclosure.cjs.map +1 -1
- package/dist/hooks/use-document-visibility.cjs.map +1 -1
- package/dist/hooks/use-event-listener.cjs.map +1 -1
- package/dist/hooks/use-geolocation.cjs.map +1 -1
- package/dist/hooks/use-hover.cjs.map +1 -1
- package/dist/hooks/use-idle.cjs.map +1 -1
- package/dist/hooks/use-install-prompt.cjs +1 -1
- package/dist/hooks/use-install-prompt.cjs.map +1 -1
- package/dist/hooks/use-install-prompt.js +1 -1
- package/dist/hooks/use-install-prompt.js.map +1 -1
- package/dist/hooks/use-intersection-observer.cjs.map +1 -1
- package/dist/hooks/use-interval.cjs.map +1 -1
- package/dist/hooks/use-is-first-render.cjs.map +1 -1
- package/dist/hooks/use-keyboard-shortcut.cjs +1 -1
- package/dist/hooks/use-keyboard-shortcut.cjs.map +1 -1
- package/dist/hooks/use-keyboard-shortcut.js +1 -1
- package/dist/hooks/use-keyboard-shortcut.js.map +1 -1
- package/dist/hooks/use-list-state.cjs.map +1 -1
- package/dist/hooks/use-local-storage.cjs.map +1 -1
- package/dist/hooks/use-long-press-handlers.cjs.map +1 -1
- package/dist/hooks/use-long-press.cjs.map +1 -1
- package/dist/hooks/use-map.cjs.map +1 -1
- package/dist/hooks/use-media-devices.cjs.map +1 -1
- package/dist/hooks/use-media-permission.cjs.map +1 -1
- package/dist/hooks/use-media-query.cjs.map +1 -1
- package/dist/hooks/use-object-url.cjs.map +1 -1
- package/dist/hooks/use-online.cjs.map +1 -1
- package/dist/hooks/use-pagination.cjs.map +1 -1
- package/dist/hooks/use-previous.cjs.map +1 -1
- package/dist/hooks/use-queue.cjs.map +1 -1
- package/dist/hooks/use-resize-observer.cjs.map +1 -1
- package/dist/hooks/use-scroll-overflow.cjs.map +1 -1
- package/dist/hooks/use-service-worker-update.cjs.map +1 -1
- package/dist/hooks/use-set.cjs.map +1 -1
- package/dist/hooks/use-sortable.cjs +1 -1
- package/dist/hooks/use-sortable.cjs.map +1 -1
- package/dist/hooks/use-sortable.js +0 -2
- package/dist/hooks/use-sortable.js.map +1 -1
- package/dist/hooks/use-stable-callback.cjs.map +1 -1
- package/dist/hooks/use-storage-estimate.cjs.map +1 -1
- package/dist/hooks/use-throttle.cjs.map +1 -1
- package/dist/hooks/use-timeout.cjs.map +1 -1
- package/dist/hooks/use-toggle.cjs.map +1 -1
- package/dist/hooks/use-window-size.cjs.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +7 -5
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +1 -1
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/use-poll.cjs.map +1 -1
- package/dist/i18n/I18nProvider.cjs.map +1 -1
- package/dist/icons/Icon.cjs.map +1 -1
- package/dist/icons/IconProvider.cjs.map +1 -1
- package/dist/icons/icon-context.cjs.map +1 -1
- package/dist/icons/use-icon.cjs.map +1 -1
- package/dist/imaging/use-image-processing.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/offline/use-offline-sync.cjs.map +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/query/QueryProvider.cjs.map +1 -1
- package/dist/query/constants.cjs +1 -1
- package/dist/query/constants.cjs.map +1 -1
- package/dist/query/constants.js +10 -10
- package/dist/query/constants.js.map +1 -1
- package/dist/query/create-query-keys.cjs +1 -1
- package/dist/query/create-query-keys.cjs.map +1 -1
- package/dist/query/create-query-keys.js +1 -1
- package/dist/query/create-query-keys.js.map +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/use-cursor-query.cjs.map +1 -1
- package/dist/query/use-offline-mutation.cjs.map +1 -1
- package/dist/query/use-paginated-query.cjs.map +1 -1
- package/dist/router/AppRouter.cjs +1 -1
- package/dist/router/AppRouter.cjs.map +1 -1
- package/dist/router/AppRouter.js +7 -2
- package/dist/router/AppRouter.js.map +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/store/create-store.cjs.map +1 -1
- package/dist/tabular/use-tabular-predictor.cjs.map +1 -1
- package/dist/telemetry/TelemetryProvider.cjs.map +1 -1
- package/dist/tempest-react-sdk.d.ts +25 -26
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/color.cjs +1 -1
- package/dist/theme/color.cjs.map +1 -1
- package/dist/theme/color.js +1 -1
- package/dist/theme/color.js.map +1 -1
- package/dist/utils/cn.cjs +1 -1
- package/dist/utils/cn.cjs.map +1 -1
- package/dist/utils/cn.js +1 -1
- package/dist/utils/cn.js.map +1 -1
- package/dist/utils/objects.cjs +1 -1
- package/dist/utils/objects.cjs.map +1 -1
- package/dist/utils/objects.js +1 -1
- package/dist/utils/objects.js.map +1 -1
- package/dist/utils/xlsx.cjs +1 -1
- package/dist/utils/xlsx.cjs.map +1 -1
- package/dist/utils/xlsx.js +3 -3
- package/dist/utils/xlsx.js.map +1 -1
- package/dist/vision/core/exceptions.cjs.map +1 -1
- package/dist/vision/core/exceptions.js.map +1 -1
- package/dist/vision/core/metadata.cjs +1 -1
- package/dist/vision/core/metadata.cjs.map +1 -1
- package/dist/vision/core/metadata.js +1 -1
- package/dist/vision/core/metadata.js.map +1 -1
- package/dist/vision/io/image.cjs +1 -1
- package/dist/vision/io/image.cjs.map +1 -1
- package/dist/vision/io/image.js +4 -2
- package/dist/vision/io/image.js.map +1 -1
- package/dist/vision/preprocess/image.cjs +1 -1
- package/dist/vision/preprocess/image.cjs.map +1 -1
- package/dist/vision/preprocess/image.js +4 -2
- package/dist/vision/preprocess/image.js.map +1 -1
- package/dist/vision/tasks/base.cjs +1 -1
- package/dist/vision/tasks/base.cjs.map +1 -1
- package/dist/vision/tasks/base.js +3 -1
- package/dist/vision/tasks/base.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js +2 -2
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detectClassify.cjs +1 -1
- package/dist/vision/tasks/detectClassify.cjs.map +1 -1
- package/dist/vision/tasks/detectClassify.js +16 -15
- package/dist/vision/tasks/detectClassify.js.map +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-live-luminance.cjs.map +1 -1
- package/dist/vite/create-vite-config.cjs +1 -1
- package/dist/vite/create-vite-config.cjs.map +1 -1
- package/dist/vite/create-vite-config.js +3 -3
- package/dist/vite/create-vite-config.js.map +1 -1
- package/dist/vite/tempest-icons.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +5 -3
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +2 -2
- package/template/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,
|
|
1
|
+
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));exports.__toESM=s;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-control-context.cjs","names":[],"sources":["../../src/access/access-control-context.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { ReactNode } from \"react\";\nimport type { AccessControl } from \"./types\";\n\nconst AccessControlContext = createContext<AccessControl | null>(null);\n\nexport interface AccessControlProviderProps {\n /** The access-control strategy made available to descendants. */\n control: AccessControl;\n children: ReactNode;\n}\n\n/**\n * Provide an {@link AccessControl} strategy to the React tree. Components such\n * as `<Can>` and the `useCan` hook read it from context.\n *\n * @example\n * ```tsx\n * <AccessControlProvider control={createRoleAccessControl({ role: \"admin\", roles: { admin: [\"*\"] } })}>\n * <App />\n * </AccessControlProvider>\n * ```\n */\nexport function AccessControlProvider({ control, children }: AccessControlProviderProps) {\n return (\n <AccessControlContext.Provider value={control}>{children}</AccessControlContext.Provider>\n );\n}\n\n/**\n * Read the current {@link AccessControl} from context.\n *\n * Returns `null` when no {@link AccessControlProvider} is present. Callers MUST\n * treat the absence of a provider as **\"allow all\"** — i.e. when this returns\n * `null`, access checks should default to permitted. This keeps the SDK\n * opt-in: dropping in a provider adds enforcement, removing it disables it.\n */\nexport function useAccessControl(): AccessControl | null {\n return useContext(AccessControlContext);\n}\n"],"mappings":"sDAIA,IAAM,GAAA,
|
|
1
|
+
{"version":3,"file":"access-control-context.cjs","names":[],"sources":["../../src/access/access-control-context.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { ReactNode } from \"react\";\nimport type { AccessControl } from \"./types\";\n\nconst AccessControlContext = createContext<AccessControl | null>(null);\n\nexport interface AccessControlProviderProps {\n /** The access-control strategy made available to descendants. */\n control: AccessControl;\n children: ReactNode;\n}\n\n/**\n * Provide an {@link AccessControl} strategy to the React tree. Components such\n * as `<Can>` and the `useCan` hook read it from context.\n *\n * @example\n * ```tsx\n * <AccessControlProvider control={createRoleAccessControl({ role: \"admin\", roles: { admin: [\"*\"] } })}>\n * <App />\n * </AccessControlProvider>\n * ```\n */\nexport function AccessControlProvider({ control, children }: AccessControlProviderProps) {\n return (\n <AccessControlContext.Provider value={control}>{children}</AccessControlContext.Provider>\n );\n}\n\n/**\n * Read the current {@link AccessControl} from context.\n *\n * Returns `null` when no {@link AccessControlProvider} is present. Callers MUST\n * treat the absence of a provider as **\"allow all\"** — i.e. when this returns\n * `null`, access checks should default to permitted. This keeps the SDK\n * opt-in: dropping in a provider adds enforcement, removing it disables it.\n */\nexport function useAccessControl(): AccessControl | null {\n return useContext(AccessControlContext);\n}\n"],"mappings":"sDAIA,IAAM,GAAA,EAAuB,EAAA,cAAA,CAAoC,IAAI,EAmBrE,SAAgB,EAAsB,CAAE,UAAS,YAAwC,CACrF,OACI,EAAA,EAAA,IAAA,CAAC,EAAqB,SAAtB,CAA+B,MAAO,EAAU,UAAwC,CAAA,CAEhG,CAUA,SAAgB,GAAyC,CACrD,OAAA,EAAO,EAAA,WAAA,CAAW,CAAoB,CAC1C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-can.cjs","names":[],"sources":["../../src/access/use-can.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { useAccessControl } from \"./access-control-context\";\nimport type { CanParams, CanResult } from \"./types\";\n\nexport interface UseCanResult {\n /** Whether the action is permitted. Defaults to `true` while loading is not the concern of the caller. */\n allowed: boolean;\n /** `true` while an async `can` check is pending. */\n isLoading: boolean;\n /** Optional explanation, typically present when `allowed` is `false`. */\n reason?: string;\n}\n\nfunction normalize(result: boolean | CanResult): CanResult {\n return typeof result === \"boolean\" ? { can: result } : result;\n}\n\n/**\n * Resolve an access check against the {@link AccessControl} in context.\n *\n * Handles sync booleans, sync {@link CanResult}, and promises of either. When\n * no provider is present, access is allowed (see `useAccessControl`). The check\n * re-runs whenever the params change.\n *\n * @param params - The action/resource being checked.\n */\nexport function useCan(params: CanParams): UseCanResult {\n const control = useAccessControl();\n const paramsKey = JSON.stringify(params);\n\n const [state, setState] = useState<UseCanResult>(() =>\n control ? { allowed: false, isLoading: true } : { allowed: true, isLoading: false },\n );\n\n useEffect(() => {\n if (!control) {\n setState({ allowed: true, isLoading: false });\n return;\n }\n\n let cancelled = false;\n setState((prev) => ({ ...prev, isLoading: true }));\n\n Promise.resolve(control.can(params))\n .then((raw) => {\n if (cancelled) return;\n const result = normalize(raw);\n setState({ allowed: result.can, isLoading: false, reason: result.reason });\n })\n .catch((error: unknown) => {\n if (cancelled) return;\n setState({\n allowed: false,\n isLoading: false,\n reason: error instanceof Error ? error.message : \"access check failed\",\n });\n });\n\n return () => {\n cancelled = true;\n };\n // `paramsKey` is the stable stringified form of `params`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [control, paramsKey]);\n\n return state;\n}\n"],"mappings":"uEAaA,SAAS,EAAU,EAAwC,CACvD,OAAO,OAAO,GAAW,UAAY,CAAE,IAAK,CAAO,EAAI,CAC3D,CAWA,SAAgB,EAAO,EAAiC,CACpD,IAAM,EAAU,EAAA,iBAAiB,EAC3B,EAAY,KAAK,UAAU,CAAM,EAEjC,CAAC,EAAO,IAAA,
|
|
1
|
+
{"version":3,"file":"use-can.cjs","names":[],"sources":["../../src/access/use-can.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { useAccessControl } from \"./access-control-context\";\nimport type { CanParams, CanResult } from \"./types\";\n\nexport interface UseCanResult {\n /** Whether the action is permitted. Defaults to `true` while loading is not the concern of the caller. */\n allowed: boolean;\n /** `true` while an async `can` check is pending. */\n isLoading: boolean;\n /** Optional explanation, typically present when `allowed` is `false`. */\n reason?: string;\n}\n\nfunction normalize(result: boolean | CanResult): CanResult {\n return typeof result === \"boolean\" ? { can: result } : result;\n}\n\n/**\n * Resolve an access check against the {@link AccessControl} in context.\n *\n * Handles sync booleans, sync {@link CanResult}, and promises of either. When\n * no provider is present, access is allowed (see `useAccessControl`). The check\n * re-runs whenever the params change.\n *\n * @param params - The action/resource being checked.\n */\nexport function useCan(params: CanParams): UseCanResult {\n const control = useAccessControl();\n const paramsKey = JSON.stringify(params);\n\n const [state, setState] = useState<UseCanResult>(() =>\n control ? { allowed: false, isLoading: true } : { allowed: true, isLoading: false },\n );\n\n useEffect(() => {\n if (!control) {\n setState({ allowed: true, isLoading: false });\n return;\n }\n\n let cancelled = false;\n setState((prev) => ({ ...prev, isLoading: true }));\n\n Promise.resolve(control.can(params))\n .then((raw) => {\n if (cancelled) return;\n const result = normalize(raw);\n setState({ allowed: result.can, isLoading: false, reason: result.reason });\n })\n .catch((error: unknown) => {\n if (cancelled) return;\n setState({\n allowed: false,\n isLoading: false,\n reason: error instanceof Error ? error.message : \"access check failed\",\n });\n });\n\n return () => {\n cancelled = true;\n };\n // `paramsKey` is the stable stringified form of `params`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [control, paramsKey]);\n\n return state;\n}\n"],"mappings":"uEAaA,SAAS,EAAU,EAAwC,CACvD,OAAO,OAAO,GAAW,UAAY,CAAE,IAAK,CAAO,EAAI,CAC3D,CAWA,SAAgB,EAAO,EAAiC,CACpD,IAAM,EAAU,EAAA,iBAAiB,EAC3B,EAAY,KAAK,UAAU,CAAM,EAEjC,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,KACtB,EAAU,CAAE,QAAS,GAAO,UAAW,EAAK,EAAI,CAAE,QAAS,GAAM,UAAW,EAAM,CACtF,EAiCA,OA/BA,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAS,CACV,EAAS,CAAE,QAAS,GAAM,UAAW,EAAM,CAAC,EAC5C,MACJ,CAEA,IAAI,EAAY,GAkBhB,OAjBA,EAAU,IAAU,CAAE,GAAG,EAAM,UAAW,EAAK,EAAE,EAEjD,QAAQ,QAAQ,EAAQ,IAAI,CAAM,CAAC,CAAC,CAC/B,KAAM,GAAQ,CACX,GAAI,EAAW,OACf,IAAM,EAAS,EAAU,CAAG,EAC5B,EAAS,CAAE,QAAS,EAAO,IAAK,UAAW,GAAO,OAAQ,EAAO,MAAO,CAAC,CAC7E,CAAC,CAAC,CACD,MAAO,GAAmB,CACnB,GACJ,EAAS,CACL,QAAS,GACT,UAAW,GACX,OAAQ,aAAiB,MAAQ,EAAM,QAAU,qBACrD,CAAC,CACL,CAAC,MAEQ,CACT,EAAY,EAChB,CAGJ,EAAG,CAAC,EAAS,CAAS,CAAC,EAEhB,CACX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppProviders.cjs","names":[],"sources":["../../src/app/AppProviders.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { QueryProvider } from \"../query/QueryProvider\";\nimport type { QueryProviderProps } from \"../query/QueryProvider\";\nimport { ThemeProvider } from \"../theme/ThemeProvider\";\nimport type { ThemeProviderProps } from \"../theme/ThemeProvider\";\nimport { I18nProvider } from \"../i18n/I18nProvider\";\nimport type { I18nProviderProps } from \"../i18n/I18nProvider\";\nimport { ErrorBoundary } from \"../error-boundary/ErrorBoundary\";\nimport type { ErrorBoundaryProps } from \"../error-boundary/ErrorBoundary\";\n\nexport interface AppProvidersProps {\n children: ReactNode;\n /**\n * TanStack Query config. Enabled by default with SDK defaults; pass `false`\n * to opt out (e.g. the app already mounts its own `QueryProvider`).\n */\n query?: Omit<QueryProviderProps, \"children\"> | false;\n /**\n * Theme config. Enabled by default; pass `false` to opt out.\n */\n theme?: Omit<ThemeProviderProps, \"children\"> | false;\n /**\n * i18n config. Opt-in: provide `locale` + `messages` to mount the provider.\n */\n i18n?: Omit<I18nProviderProps, \"children\">;\n /**\n * Top-level error boundary. Opt-in: provide a `fallback` to wrap the tree.\n */\n errorBoundary?: Omit<ErrorBoundaryProps, \"children\">;\n}\n\n/**\n * Compose the Tempest app-wide providers in one place: error boundary →\n * TanStack Query → theme → i18n. Query and theme are on by default; i18n and\n * the error boundary mount only when configured. Each provider can be disabled\n * or tuned independently, so an app's `main.tsx` stays a single declarative\n * block instead of a hand-nested provider pyramid.\n *\n * @example\n * <AppProviders\n * errorBoundary={{ fallback: <Crash /> }}\n * i18n={{ locale: \"pt-BR\", messages }}\n * >\n * <AppRouter routes={routes} />\n * </AppProviders>\n */\nexport function AppProviders({ children, query, theme, i18n, errorBoundary }: AppProvidersProps) {\n let tree: ReactNode = children;\n\n if (i18n) {\n tree = <I18nProvider {...i18n}>{tree}</I18nProvider>;\n }\n\n if (theme !== false) {\n tree = <ThemeProvider {...(theme || {})}>{tree}</ThemeProvider>;\n }\n\n if (query !== false) {\n tree = <QueryProvider {...(query || {})}>{tree}</QueryProvider>;\n }\n\n if (errorBoundary) {\n tree = <ErrorBoundary {...errorBoundary}>{tree}</ErrorBoundary>;\n }\n\n return <>{tree}</>;\n}\n"],"mappings":"gNA8CA,SAAgB,EAAa,CAAE,WAAU,QAAO,QAAO,OAAM,iBAAoC,CAC7F,IAAI,EAAkB,EAkBtB,OAhBI,IACA,GAAO,EAAA,EAAA,IAAA,CAAC,EAAA,aAAD,CAAc,GAAI,
|
|
1
|
+
{"version":3,"file":"AppProviders.cjs","names":[],"sources":["../../src/app/AppProviders.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { QueryProvider } from \"../query/QueryProvider\";\nimport type { QueryProviderProps } from \"../query/QueryProvider\";\nimport { ThemeProvider } from \"../theme/ThemeProvider\";\nimport type { ThemeProviderProps } from \"../theme/ThemeProvider\";\nimport { I18nProvider } from \"../i18n/I18nProvider\";\nimport type { I18nProviderProps } from \"../i18n/I18nProvider\";\nimport { ErrorBoundary } from \"../error-boundary/ErrorBoundary\";\nimport type { ErrorBoundaryProps } from \"../error-boundary/ErrorBoundary\";\n\nexport interface AppProvidersProps {\n children: ReactNode;\n /**\n * TanStack Query config. Enabled by default with SDK defaults; pass `false`\n * to opt out (e.g. the app already mounts its own `QueryProvider`).\n */\n query?: Omit<QueryProviderProps, \"children\"> | false;\n /**\n * Theme config. Enabled by default; pass `false` to opt out.\n */\n theme?: Omit<ThemeProviderProps, \"children\"> | false;\n /**\n * i18n config. Opt-in: provide `locale` + `messages` to mount the provider.\n */\n i18n?: Omit<I18nProviderProps, \"children\">;\n /**\n * Top-level error boundary. Opt-in: provide a `fallback` to wrap the tree.\n */\n errorBoundary?: Omit<ErrorBoundaryProps, \"children\">;\n}\n\n/**\n * Compose the Tempest app-wide providers in one place: error boundary →\n * TanStack Query → theme → i18n. Query and theme are on by default; i18n and\n * the error boundary mount only when configured. Each provider can be disabled\n * or tuned independently, so an app's `main.tsx` stays a single declarative\n * block instead of a hand-nested provider pyramid.\n *\n * @example\n * <AppProviders\n * errorBoundary={{ fallback: <Crash /> }}\n * i18n={{ locale: \"pt-BR\", messages }}\n * >\n * <AppRouter routes={routes} />\n * </AppProviders>\n */\nexport function AppProviders({ children, query, theme, i18n, errorBoundary }: AppProvidersProps) {\n let tree: ReactNode = children;\n\n if (i18n) {\n tree = <I18nProvider {...i18n}>{tree}</I18nProvider>;\n }\n\n if (theme !== false) {\n tree = <ThemeProvider {...(theme || {})}>{tree}</ThemeProvider>;\n }\n\n if (query !== false) {\n tree = <QueryProvider {...(query || {})}>{tree}</QueryProvider>;\n }\n\n if (errorBoundary) {\n tree = <ErrorBoundary {...errorBoundary}>{tree}</ErrorBoundary>;\n }\n\n return <>{tree}</>;\n}\n"],"mappings":"gNA8CA,SAAgB,EAAa,CAAE,WAAU,QAAO,QAAO,OAAM,iBAAoC,CAC7F,IAAI,EAAkB,EAkBtB,OAhBI,IACA,GAAO,EAAA,EAAA,IAAA,CAAC,EAAA,aAAD,CAAc,GAAI,EAAO,SAAA,CAAmB,CAAA,GAGnD,IAAU,KACV,GAAO,EAAA,EAAA,IAAA,CAAC,EAAA,cAAD,CAAe,GAAK,GAAS,CAAC,EAAK,SAAA,CAAoB,CAAA,GAG9D,IAAU,KACV,GAAO,EAAA,EAAA,IAAA,CAAC,EAAA,cAAD,CAAe,GAAK,GAAS,CAAC,EAAK,SAAA,CAAoB,CAAA,GAG9D,IACA,GAAO,EAAA,EAAA,IAAA,CAAC,EAAA,cAAD,CAAe,GAAI,EAAgB,SAAA,CAAoB,CAAA,IAG3D,EAAA,EAAA,IAAA,CAAA,EAAA,SAAA,CAAA,SAAG,CAAO,CAAA,CACrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppProviders.js","names":[],"sources":["../../src/app/AppProviders.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { QueryProvider } from \"../query/QueryProvider\";\nimport type { QueryProviderProps } from \"../query/QueryProvider\";\nimport { ThemeProvider } from \"../theme/ThemeProvider\";\nimport type { ThemeProviderProps } from \"../theme/ThemeProvider\";\nimport { I18nProvider } from \"../i18n/I18nProvider\";\nimport type { I18nProviderProps } from \"../i18n/I18nProvider\";\nimport { ErrorBoundary } from \"../error-boundary/ErrorBoundary\";\nimport type { ErrorBoundaryProps } from \"../error-boundary/ErrorBoundary\";\n\nexport interface AppProvidersProps {\n children: ReactNode;\n /**\n * TanStack Query config. Enabled by default with SDK defaults; pass `false`\n * to opt out (e.g. the app already mounts its own `QueryProvider`).\n */\n query?: Omit<QueryProviderProps, \"children\"> | false;\n /**\n * Theme config. Enabled by default; pass `false` to opt out.\n */\n theme?: Omit<ThemeProviderProps, \"children\"> | false;\n /**\n * i18n config. Opt-in: provide `locale` + `messages` to mount the provider.\n */\n i18n?: Omit<I18nProviderProps, \"children\">;\n /**\n * Top-level error boundary. Opt-in: provide a `fallback` to wrap the tree.\n */\n errorBoundary?: Omit<ErrorBoundaryProps, \"children\">;\n}\n\n/**\n * Compose the Tempest app-wide providers in one place: error boundary →\n * TanStack Query → theme → i18n. Query and theme are on by default; i18n and\n * the error boundary mount only when configured. Each provider can be disabled\n * or tuned independently, so an app's `main.tsx` stays a single declarative\n * block instead of a hand-nested provider pyramid.\n *\n * @example\n * <AppProviders\n * errorBoundary={{ fallback: <Crash /> }}\n * i18n={{ locale: \"pt-BR\", messages }}\n * >\n * <AppRouter routes={routes} />\n * </AppProviders>\n */\nexport function AppProviders({ children, query, theme, i18n, errorBoundary }: AppProvidersProps) {\n let tree: ReactNode = children;\n\n if (i18n) {\n tree = <I18nProvider {...i18n}>{tree}</I18nProvider>;\n }\n\n if (theme !== false) {\n tree = <ThemeProvider {...(theme || {})}>{tree}</ThemeProvider>;\n }\n\n if (query !== false) {\n tree = <QueryProvider {...(query || {})}>{tree}</QueryProvider>;\n }\n\n if (errorBoundary) {\n tree = <ErrorBoundary {...errorBoundary}>{tree}</ErrorBoundary>;\n }\n\n return <>{tree}</>;\n}\n"],"mappings":";;;;;;AA8CA,SAAgB,EAAa,EAAE,aAAU,UAAO,UAAO,SAAM,oBAAoC;CAC7F,IAAI,IAAkB;CAkBtB,OAhBI,MACA,IAAO,kBAAC,GAAD;EAAc,GAAI;
|
|
1
|
+
{"version":3,"file":"AppProviders.js","names":[],"sources":["../../src/app/AppProviders.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { QueryProvider } from \"../query/QueryProvider\";\nimport type { QueryProviderProps } from \"../query/QueryProvider\";\nimport { ThemeProvider } from \"../theme/ThemeProvider\";\nimport type { ThemeProviderProps } from \"../theme/ThemeProvider\";\nimport { I18nProvider } from \"../i18n/I18nProvider\";\nimport type { I18nProviderProps } from \"../i18n/I18nProvider\";\nimport { ErrorBoundary } from \"../error-boundary/ErrorBoundary\";\nimport type { ErrorBoundaryProps } from \"../error-boundary/ErrorBoundary\";\n\nexport interface AppProvidersProps {\n children: ReactNode;\n /**\n * TanStack Query config. Enabled by default with SDK defaults; pass `false`\n * to opt out (e.g. the app already mounts its own `QueryProvider`).\n */\n query?: Omit<QueryProviderProps, \"children\"> | false;\n /**\n * Theme config. Enabled by default; pass `false` to opt out.\n */\n theme?: Omit<ThemeProviderProps, \"children\"> | false;\n /**\n * i18n config. Opt-in: provide `locale` + `messages` to mount the provider.\n */\n i18n?: Omit<I18nProviderProps, \"children\">;\n /**\n * Top-level error boundary. Opt-in: provide a `fallback` to wrap the tree.\n */\n errorBoundary?: Omit<ErrorBoundaryProps, \"children\">;\n}\n\n/**\n * Compose the Tempest app-wide providers in one place: error boundary →\n * TanStack Query → theme → i18n. Query and theme are on by default; i18n and\n * the error boundary mount only when configured. Each provider can be disabled\n * or tuned independently, so an app's `main.tsx` stays a single declarative\n * block instead of a hand-nested provider pyramid.\n *\n * @example\n * <AppProviders\n * errorBoundary={{ fallback: <Crash /> }}\n * i18n={{ locale: \"pt-BR\", messages }}\n * >\n * <AppRouter routes={routes} />\n * </AppProviders>\n */\nexport function AppProviders({ children, query, theme, i18n, errorBoundary }: AppProvidersProps) {\n let tree: ReactNode = children;\n\n if (i18n) {\n tree = <I18nProvider {...i18n}>{tree}</I18nProvider>;\n }\n\n if (theme !== false) {\n tree = <ThemeProvider {...(theme || {})}>{tree}</ThemeProvider>;\n }\n\n if (query !== false) {\n tree = <QueryProvider {...(query || {})}>{tree}</QueryProvider>;\n }\n\n if (errorBoundary) {\n tree = <ErrorBoundary {...errorBoundary}>{tree}</ErrorBoundary>;\n }\n\n return <>{tree}</>;\n}\n"],"mappings":";;;;;;AA8CA,SAAgB,EAAa,EAAE,aAAU,UAAO,UAAO,SAAM,oBAAoC;CAC7F,IAAI,IAAkB;CAkBtB,OAhBI,MACA,IAAO,kBAAC,GAAD;EAAc,GAAI;EAAO,UAAA;CAAmB,CAAA,IAGnD,MAAU,OACV,IAAO,kBAAC,GAAD;EAAe,GAAK,KAAS,CAAC;EAAK,UAAA;CAAoB,CAAA,IAG9D,MAAU,OACV,IAAO,kBAAC,GAAD;EAAe,GAAK,KAAS,CAAC;EAAK,UAAA;CAAoB,CAAA,IAG9D,MACA,IAAO,kBAAC,GAAD;EAAe,GAAI;EAAgB,UAAA;CAAoB,CAAA,IAG3D,kBAAA,GAAA,EAAA,UAAG,EAAO,CAAA;AACrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-audio-recorder.cjs","names":[],"sources":["../../src/audio/use-audio-recorder.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the hook is the recorder's lifecycle seen from React:\n * permission, the recorder handle, the clock, the level meter and the auto-stop\n * timer, all of which have to be torn down together on unmount. A second hook would\n * own half the teardown.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport {\n createAudioRecorder,\n type AudioRecorderHandle,\n type AudioRecorderOptions,\n type AudioRecorderStatus,\n type AudioRecording,\n} from \"./audio-recorder\";\nimport { createLevelMeter, type LevelMeter } from \"./level-meter\";\n\n/** Options for {@link useAudioRecorder}. */\nexport interface UseAudioRecorderOptions extends AudioRecorderOptions {\n /**\n * Stop automatically after this many milliseconds.\n *\n * Worth setting on anything user-facing: a recording left running by accident is\n * a memory buffer that grows until the tab dies.\n */\n maxDurationMs?: number;\n /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n onRecorded?: (recording: AudioRecording) => void;\n /** How often the clock and the level are published, in ms. Default 100. */\n tickMs?: number;\n /** Skip the level meter — it costs an `AudioContext`. Default `false`. */\n disableLevelMeter?: boolean;\n}\n\n/** Value returned by {@link useAudioRecorder}. */\nexport interface UseAudioRecorderResult {\n status: AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: number;\n /** Loudness 0–1, or `0` when the meter is off or nothing is recording. */\n level: number;\n /** The finished recording, or `null` before the first `stop()`. */\n recording: AudioRecording | null;\n /** Recorder-level error (encoder failure, device pulled mid-recording). */\n error: unknown;\n /** `false` when there is no stream yet, or the browser cannot record. */\n ready: boolean;\n start: () => void;\n pause: () => void;\n resume: () => void;\n /** Stop and resolve with the recording. Also published on `recording`. */\n stop: () => Promise<AudioRecording | null>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n}\n\n/**\n * Record the given stream, with a clock, a level meter and a state machine.\n *\n * Pass the stream from {@link useMicrophone}; the hook stays `ready: false` until\n * there is one, so a page can render the whole recorder UI before permission is\n * granted and simply have it disabled.\n *\n * The clock and the level are published on an interval (`tickMs`, default 100 ms)\n * rather than per animation frame. A hook that set state 60 times a second would\n * re-render everything the recorder is inside; 10 Hz is smooth enough for a timer and\n * a meter, and the underlying `createLevelMeter` is still available for anyone who\n * wants a frame-accurate bar written straight to the DOM.\n *\n * @param stream - The stream to record, or `null` while permission is pending.\n * @param options - See {@link UseAudioRecorderOptions}.\n *\n * @example\n * const mic = useMicrophone();\n * const rec = useAudioRecorder(mic.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar</button>\n */\nexport function useAudioRecorder(\n stream: MediaStream | null,\n options: UseAudioRecorderOptions = {},\n): UseAudioRecorderResult {\n const {\n maxDurationMs,\n onRecorded,\n onChunk,\n onError,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n tickMs = 100,\n disableLevelMeter = false,\n } = options;\n\n const [status, setStatus] = useState<AudioRecorderStatus>(\"idle\");\n const [durationMs, setDurationMs] = useState(0);\n const [level, setLevel] = useState(0);\n const [recording, setRecording] = useState<AudioRecording | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [ready, setReady] = useState(false);\n\n const recorderRef = useRef<AudioRecorderHandle | null>(null);\n const meterRef = useRef<LevelMeter | null>(null);\n\n const emitRecorded = useStableCallback((result: AudioRecording) => onRecorded?.(result));\n const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n const emitError = useStableCallback((err: unknown) => {\n setError(err);\n onError?.(err);\n });\n\n /**\n * Build the recorder and the meter for a stream, and tear both down when it\n * changes.\n *\n * The recorder is rebuilt per stream because `MediaRecorder` is bound to the\n * stream it was constructed with — swapping microphones mid-session hands us a\n * new `MediaStream`, and reusing the old recorder would keep recording the device\n * the user just switched away from. It is rebuilt on a container or bitrate change\n * for the same reason: both are constructor arguments.\n */\n useEffect(() => {\n if (!stream) {\n setReady(false);\n return;\n }\n let recorder: AudioRecorderHandle;\n try {\n recorder = createAudioRecorder(stream, {\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n onChunk: emitChunk,\n onError: emitError,\n });\n } catch (err) {\n setError(err);\n setReady(false);\n return;\n }\n recorderRef.current = recorder;\n meterRef.current = disableLevelMeter ? null : createLevelMeter(stream);\n setReady(true);\n setStatus(\"idle\");\n\n return () => {\n if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n recorder.cancel();\n }\n meterRef.current?.stop();\n meterRef.current = null;\n recorderRef.current = null;\n setReady(false);\n };\n }, [\n stream,\n disableLevelMeter,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n emitChunk,\n emitError,\n ]);\n\n const stop = useCallback(async (): Promise<AudioRecording | null> => {\n const recorder = recorderRef.current;\n if (!recorder) return null;\n const current = recorder.status();\n if (current !== \"recording\" && current !== \"paused\") return null;\n const result = await recorder.stop();\n setStatus(\"stopped\");\n setDurationMs(result.durationMs);\n setLevel(0);\n setRecording(result);\n emitRecorded(result);\n return result;\n }, [emitRecorded]);\n\n /**\n * Publish the clock and the level while something is running, and enforce\n * `maxDurationMs`.\n *\n * The cap is checked on the same tick rather than with a `setTimeout` armed at\n * `start()`, because a paused recording must not keep counting toward it — a\n * timeout would fire while the user is paused mid-sentence.\n */\n useEffect(() => {\n if (status !== \"recording\" && status !== \"paused\") {\n setLevel(0);\n return;\n }\n const id = setInterval(() => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n const elapsed = recorder.durationMs();\n setDurationMs(elapsed);\n setLevel(status === \"recording\" ? (meterRef.current?.level() ?? 0) : 0);\n if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n }, tickMs);\n return () => clearInterval(id);\n }, [status, tickMs, maxDurationMs, stop]);\n\n const start = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n setRecording(null);\n setError(null);\n setDurationMs(0);\n recorder.start();\n setStatus(recorder.status());\n }, []);\n\n const pause = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.pause();\n setDurationMs(recorder.durationMs());\n setStatus(recorder.status());\n }, []);\n\n const resume = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.resume();\n setStatus(recorder.status());\n }, []);\n\n const cancel = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.cancel();\n setStatus(\"idle\");\n setDurationMs(0);\n setLevel(0);\n setRecording(null);\n }, []);\n\n return {\n status,\n durationMs,\n level,\n recording,\n error,\n ready,\n start,\n pause,\n resume,\n stop,\n cancel,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-audio-recorder.cjs","names":[],"sources":["../../src/audio/use-audio-recorder.ts"],"sourcesContent":["/**\n * @tempest-limits hook-lines — the hook is the recorder's lifecycle seen from React:\n * permission, the recorder handle, the clock, the level meter and the auto-stop\n * timer, all of which have to be torn down together on unmount. A second hook would\n * own half the teardown.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport {\n createAudioRecorder,\n type AudioRecorderHandle,\n type AudioRecorderOptions,\n type AudioRecorderStatus,\n type AudioRecording,\n} from \"./audio-recorder\";\nimport { createLevelMeter, type LevelMeter } from \"./level-meter\";\n\n/** Options for {@link useAudioRecorder}. */\nexport interface UseAudioRecorderOptions extends AudioRecorderOptions {\n /**\n * Stop automatically after this many milliseconds.\n *\n * Worth setting on anything user-facing: a recording left running by accident is\n * a memory buffer that grows until the tab dies.\n */\n maxDurationMs?: number;\n /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n onRecorded?: (recording: AudioRecording) => void;\n /** How often the clock and the level are published, in ms. Default 100. */\n tickMs?: number;\n /** Skip the level meter — it costs an `AudioContext`. Default `false`. */\n disableLevelMeter?: boolean;\n}\n\n/** Value returned by {@link useAudioRecorder}. */\nexport interface UseAudioRecorderResult {\n status: AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: number;\n /** Loudness 0–1, or `0` when the meter is off or nothing is recording. */\n level: number;\n /** The finished recording, or `null` before the first `stop()`. */\n recording: AudioRecording | null;\n /** Recorder-level error (encoder failure, device pulled mid-recording). */\n error: unknown;\n /** `false` when there is no stream yet, or the browser cannot record. */\n ready: boolean;\n start: () => void;\n pause: () => void;\n resume: () => void;\n /** Stop and resolve with the recording. Also published on `recording`. */\n stop: () => Promise<AudioRecording | null>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n}\n\n/**\n * Record the given stream, with a clock, a level meter and a state machine.\n *\n * Pass the stream from {@link useMicrophone}; the hook stays `ready: false` until\n * there is one, so a page can render the whole recorder UI before permission is\n * granted and simply have it disabled.\n *\n * The clock and the level are published on an interval (`tickMs`, default 100 ms)\n * rather than per animation frame. A hook that set state 60 times a second would\n * re-render everything the recorder is inside; 10 Hz is smooth enough for a timer and\n * a meter, and the underlying `createLevelMeter` is still available for anyone who\n * wants a frame-accurate bar written straight to the DOM.\n *\n * @param stream - The stream to record, or `null` while permission is pending.\n * @param options - See {@link UseAudioRecorderOptions}.\n *\n * @example\n * const mic = useMicrophone();\n * const rec = useAudioRecorder(mic.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar</button>\n */\nexport function useAudioRecorder(\n stream: MediaStream | null,\n options: UseAudioRecorderOptions = {},\n): UseAudioRecorderResult {\n const {\n maxDurationMs,\n onRecorded,\n onChunk,\n onError,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n tickMs = 100,\n disableLevelMeter = false,\n } = options;\n\n const [status, setStatus] = useState<AudioRecorderStatus>(\"idle\");\n const [durationMs, setDurationMs] = useState(0);\n const [level, setLevel] = useState(0);\n const [recording, setRecording] = useState<AudioRecording | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [ready, setReady] = useState(false);\n\n const recorderRef = useRef<AudioRecorderHandle | null>(null);\n const meterRef = useRef<LevelMeter | null>(null);\n\n const emitRecorded = useStableCallback((result: AudioRecording) => onRecorded?.(result));\n const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n const emitError = useStableCallback((err: unknown) => {\n setError(err);\n onError?.(err);\n });\n\n /**\n * Build the recorder and the meter for a stream, and tear both down when it\n * changes.\n *\n * The recorder is rebuilt per stream because `MediaRecorder` is bound to the\n * stream it was constructed with — swapping microphones mid-session hands us a\n * new `MediaStream`, and reusing the old recorder would keep recording the device\n * the user just switched away from. It is rebuilt on a container or bitrate change\n * for the same reason: both are constructor arguments.\n */\n useEffect(() => {\n if (!stream) {\n setReady(false);\n return;\n }\n let recorder: AudioRecorderHandle;\n try {\n recorder = createAudioRecorder(stream, {\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n onChunk: emitChunk,\n onError: emitError,\n });\n } catch (err) {\n setError(err);\n setReady(false);\n return;\n }\n recorderRef.current = recorder;\n meterRef.current = disableLevelMeter ? null : createLevelMeter(stream);\n setReady(true);\n setStatus(\"idle\");\n\n return () => {\n if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n recorder.cancel();\n }\n meterRef.current?.stop();\n meterRef.current = null;\n recorderRef.current = null;\n setReady(false);\n };\n }, [\n stream,\n disableLevelMeter,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n emitChunk,\n emitError,\n ]);\n\n const stop = useCallback(async (): Promise<AudioRecording | null> => {\n const recorder = recorderRef.current;\n if (!recorder) return null;\n const current = recorder.status();\n if (current !== \"recording\" && current !== \"paused\") return null;\n const result = await recorder.stop();\n setStatus(\"stopped\");\n setDurationMs(result.durationMs);\n setLevel(0);\n setRecording(result);\n emitRecorded(result);\n return result;\n }, [emitRecorded]);\n\n /**\n * Publish the clock and the level while something is running, and enforce\n * `maxDurationMs`.\n *\n * The cap is checked on the same tick rather than with a `setTimeout` armed at\n * `start()`, because a paused recording must not keep counting toward it — a\n * timeout would fire while the user is paused mid-sentence.\n */\n useEffect(() => {\n if (status !== \"recording\" && status !== \"paused\") {\n setLevel(0);\n return;\n }\n const id = setInterval(() => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n const elapsed = recorder.durationMs();\n setDurationMs(elapsed);\n setLevel(status === \"recording\" ? (meterRef.current?.level() ?? 0) : 0);\n if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n }, tickMs);\n return () => clearInterval(id);\n }, [status, tickMs, maxDurationMs, stop]);\n\n const start = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n setRecording(null);\n setError(null);\n setDurationMs(0);\n recorder.start();\n setStatus(recorder.status());\n }, []);\n\n const pause = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.pause();\n setDurationMs(recorder.durationMs());\n setStatus(recorder.status());\n }, []);\n\n const resume = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.resume();\n setStatus(recorder.status());\n }, []);\n\n const cancel = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.cancel();\n setStatus(\"idle\");\n setDurationMs(0);\n setLevel(0);\n setRecording(null);\n }, []);\n\n return {\n status,\n durationMs,\n level,\n recording,\n error,\n ready,\n start,\n pause,\n resume,\n stop,\n cancel,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-audio.cjs","names":[],"sources":["../../src/audio/use-audio.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { createAudioPlayer, type AudioPlayerHandle, type PlayAudioOptions } from \"./audio-player\";\n\nexport interface UseAudioResult {\n /** Play `src` on the hook's private player. */\n play: (src: string, options?: PlayAudioOptions) => Promise<void>;\n /** Stop the current clip. */\n stop: () => void;\n /**\n * Whether the browser's autoplay policy has been satisfied. Becomes\n * `true` after the first successful `play()`.\n */\n unlocked: boolean;\n}\n\n/**\n * The player lives in a state initializer, not in a lazily-written ref: the\n * initializer runs exactly once and is not a side effect during render, which is\n * what the ref version was. The player identity never changes, so state holds it\n * fine and the value is non-null from the first render.\n *\n * Hook-managed audio player. Each component instance gets its own\n * {@link AudioPlayerHandle}, so unmounting cleanly stops playback. Useful for\n * notification chimes, UI feedback sounds, and per-component soundtracks.\n */\nexport function useAudio(): UseAudioResult {\n const [player] = useState<AudioPlayerHandle>(() => createAudioPlayer());\n const [unlocked, setUnlocked] = useState<boolean>(false);\n\n useEffect(() => {\n return () => {\n player.stop();\n };\n }, [player]);\n\n const play = useCallback(\n async (src: string, options?: PlayAudioOptions): Promise<void> => {\n const result = await player.play(src, options);\n if (result) setUnlocked(true);\n },\n [player],\n );\n\n const stop = useCallback((): void => {\n player.stop();\n }, [player]);\n\n return { play, stop, unlocked };\n}\n"],"mappings":"6DAyBA,SAAgB,GAA2B,CACvC,GAAM,CAAC,IAAA,
|
|
1
|
+
{"version":3,"file":"use-audio.cjs","names":[],"sources":["../../src/audio/use-audio.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { createAudioPlayer, type AudioPlayerHandle, type PlayAudioOptions } from \"./audio-player\";\n\nexport interface UseAudioResult {\n /** Play `src` on the hook's private player. */\n play: (src: string, options?: PlayAudioOptions) => Promise<void>;\n /** Stop the current clip. */\n stop: () => void;\n /**\n * Whether the browser's autoplay policy has been satisfied. Becomes\n * `true` after the first successful `play()`.\n */\n unlocked: boolean;\n}\n\n/**\n * The player lives in a state initializer, not in a lazily-written ref: the\n * initializer runs exactly once and is not a side effect during render, which is\n * what the ref version was. The player identity never changes, so state holds it\n * fine and the value is non-null from the first render.\n *\n * Hook-managed audio player. Each component instance gets its own\n * {@link AudioPlayerHandle}, so unmounting cleanly stops playback. Useful for\n * notification chimes, UI feedback sounds, and per-component soundtracks.\n */\nexport function useAudio(): UseAudioResult {\n const [player] = useState<AudioPlayerHandle>(() => createAudioPlayer());\n const [unlocked, setUnlocked] = useState<boolean>(false);\n\n useEffect(() => {\n return () => {\n player.stop();\n };\n }, [player]);\n\n const play = useCallback(\n async (src: string, options?: PlayAudioOptions): Promise<void> => {\n const result = await player.play(src, options);\n if (result) setUnlocked(true);\n },\n [player],\n );\n\n const stop = useCallback((): void => {\n player.stop();\n }, [player]);\n\n return { play, stop, unlocked };\n}\n"],"mappings":"6DAyBA,SAAgB,GAA2B,CACvC,GAAM,CAAC,IAAA,EAAU,EAAA,SAAA,KAAkC,EAAA,kBAAkB,CAAC,EAChE,CAAC,EAAU,IAAA,EAAe,EAAA,SAAA,CAAkB,EAAK,EAoBvD,OAlBA,EAAA,EAAA,UAAA,SACiB,CACT,EAAO,KAAK,CAChB,EACD,CAAC,CAAM,CAAC,EAcJ,CAAE,MAAA,EAZI,EAAA,YAAA,CACT,MAAO,EAAa,IAA8C,CAE1D,MADiB,EAAO,KAAK,EAAK,CAAO,GACjC,EAAY,EAAI,CAChC,EACA,CAAC,CAAM,CAOF,EAAM,MAAA,EAJF,EAAA,YAAA,KAAwB,CACjC,EAAO,KAAK,CAChB,EAAG,CAAC,CAAM,CAEK,EAAM,UAAS,CAClC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-microphone.cjs","names":[],"sources":["../../src/audio/use-microphone.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n isMediaCaptureSupported,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"./media-access\";\n\n/** Lifecycle of a microphone stream. */\nexport type MicrophoneStatus = \"idle\" | \"requesting\" | \"ready\" | \"error\";\n\n/** Options for {@link useMicrophone}. */\nexport interface UseMicrophoneOptions {\n /**\n * Open the stream on mount instead of waiting for `start()`. Default `false`.\n *\n * Left off on purpose. Opening on mount means prompting on mount, and a\n * permission prompt the user did not provoke is the most reliable way to earn a\n * permanent \"Block\" — after which `getUserMedia` rejects without ever prompting\n * again. Wire `start()` to the button that needs the microphone.\n */\n autoStart?: boolean;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Browser voice processing. All three default to `true`, which is what speech\n * wants; turn them off for music, where a gate chewing on a decaying note is\n * worse than the room noise it removes.\n */\n echoCancellation?: boolean;\n noiseSuppression?: boolean;\n autoGainControl?: boolean;\n /** Escape hatch: full constraints, replacing everything above. */\n constraints?: MediaStreamConstraints;\n}\n\n/** Value returned by {@link useMicrophone}. */\nexport interface UseMicrophoneResult {\n status: MicrophoneStatus;\n /** The live stream, or `null`. Feed it to {@link useAudioRecorder} or an analyser. */\n stream: MediaStream | null;\n /** Classified error, or `null` outside the `error` status. */\n error: MediaAccessError | null;\n /** Open the stream. Safe to call when already open — it is a no-op. */\n start: () => void;\n /** Release the microphone. The browser's recording indicator only clears here. */\n stop: () => void;\n}\n\n/**\n * Acquire a microphone `MediaStream`, classified errors included, and release it\n * properly.\n *\n * Releasing matters more than it looks. Every track has to be stopped by hand:\n * dropping the last reference to a `MediaStream` does **not** turn off the\n * microphone, so the browser keeps showing its recording indicator and the OS keeps\n * the device busy — which then makes the *next* `getUserMedia` fail with\n * `NotReadableError` in another tab. The hook stops tracks on `stop()`, on unmount,\n * and before re-opening.\n *\n * @param options - See {@link UseMicrophoneOptions}.\n * @returns The stream, its status, a classified error, and `start`/`stop`.\n *\n * @example\n * const mic = useMicrophone();\n * const recorder = useAudioRecorder(mic.stream);\n * <button onClick={mic.start}>Liberar microfone</button>\n */\nexport function useMicrophone(options: UseMicrophoneOptions = {}): UseMicrophoneResult {\n const {\n autoStart = false,\n deviceId,\n echoCancellation = true,\n noiseSuppression = true,\n autoGainControl = true,\n constraints,\n } = options;\n\n const [status, setStatus] = useState<MicrophoneStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const generation = useRef(0);\n\n const release = useCallback((): void => {\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n setStream(null);\n setStatus(\"idle\");\n setError(null);\n }, [release]);\n\n const start = useCallback((): void => {\n if (streamRef.current) return;\n const run = (generation.current += 1);\n setStatus(\"requesting\");\n setError(null);\n\n if (!isMediaCaptureSupported()) {\n setError(missingCaptureApiError(\"microphone\"));\n setStatus(\"error\");\n return;\n }\n\n const request: MediaStreamConstraints = constraints ?? {\n audio: {\n echoCancellation,\n noiseSuppression,\n autoGainControl,\n ...(deviceId ? { deviceId: { exact: deviceId } } : {}),\n },\n video: false,\n };\n\n void navigator.mediaDevices\n .getUserMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n setStream(next);\n setStatus(\"ready\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n setError(classifyMediaError(err, \"microphone\"));\n setStatus(\"error\");\n });\n }, [constraints, deviceId, echoCancellation, noiseSuppression, autoGainControl]);\n\n useEffect(() => {\n if (autoStart) start();\n // `start` is stable per constraint set; re-running on every identity change\n // would reopen the device on each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoStart]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, start, stop };\n}\n"],"mappings":"6DAqEA,SAAgB,EAAc,EAAgC,CAAC,EAAwB,CACnF,GAAM,CACF,YAAY,GACZ,WACA,mBAAmB,GACnB,mBAAmB,GACnB,kBAAkB,GAClB,eACA,EAEE,CAAC,EAAQ,IAAA,
|
|
1
|
+
{"version":3,"file":"use-microphone.cjs","names":[],"sources":["../../src/audio/use-microphone.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n isMediaCaptureSupported,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"./media-access\";\n\n/** Lifecycle of a microphone stream. */\nexport type MicrophoneStatus = \"idle\" | \"requesting\" | \"ready\" | \"error\";\n\n/** Options for {@link useMicrophone}. */\nexport interface UseMicrophoneOptions {\n /**\n * Open the stream on mount instead of waiting for `start()`. Default `false`.\n *\n * Left off on purpose. Opening on mount means prompting on mount, and a\n * permission prompt the user did not provoke is the most reliable way to earn a\n * permanent \"Block\" — after which `getUserMedia` rejects without ever prompting\n * again. Wire `start()` to the button that needs the microphone.\n */\n autoStart?: boolean;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Browser voice processing. All three default to `true`, which is what speech\n * wants; turn them off for music, where a gate chewing on a decaying note is\n * worse than the room noise it removes.\n */\n echoCancellation?: boolean;\n noiseSuppression?: boolean;\n autoGainControl?: boolean;\n /** Escape hatch: full constraints, replacing everything above. */\n constraints?: MediaStreamConstraints;\n}\n\n/** Value returned by {@link useMicrophone}. */\nexport interface UseMicrophoneResult {\n status: MicrophoneStatus;\n /** The live stream, or `null`. Feed it to {@link useAudioRecorder} or an analyser. */\n stream: MediaStream | null;\n /** Classified error, or `null` outside the `error` status. */\n error: MediaAccessError | null;\n /** Open the stream. Safe to call when already open — it is a no-op. */\n start: () => void;\n /** Release the microphone. The browser's recording indicator only clears here. */\n stop: () => void;\n}\n\n/**\n * Acquire a microphone `MediaStream`, classified errors included, and release it\n * properly.\n *\n * Releasing matters more than it looks. Every track has to be stopped by hand:\n * dropping the last reference to a `MediaStream` does **not** turn off the\n * microphone, so the browser keeps showing its recording indicator and the OS keeps\n * the device busy — which then makes the *next* `getUserMedia` fail with\n * `NotReadableError` in another tab. The hook stops tracks on `stop()`, on unmount,\n * and before re-opening.\n *\n * @param options - See {@link UseMicrophoneOptions}.\n * @returns The stream, its status, a classified error, and `start`/`stop`.\n *\n * @example\n * const mic = useMicrophone();\n * const recorder = useAudioRecorder(mic.stream);\n * <button onClick={mic.start}>Liberar microfone</button>\n */\nexport function useMicrophone(options: UseMicrophoneOptions = {}): UseMicrophoneResult {\n const {\n autoStart = false,\n deviceId,\n echoCancellation = true,\n noiseSuppression = true,\n autoGainControl = true,\n constraints,\n } = options;\n\n const [status, setStatus] = useState<MicrophoneStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const generation = useRef(0);\n\n const release = useCallback((): void => {\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n setStream(null);\n setStatus(\"idle\");\n setError(null);\n }, [release]);\n\n const start = useCallback((): void => {\n if (streamRef.current) return;\n const run = (generation.current += 1);\n setStatus(\"requesting\");\n setError(null);\n\n if (!isMediaCaptureSupported()) {\n setError(missingCaptureApiError(\"microphone\"));\n setStatus(\"error\");\n return;\n }\n\n const request: MediaStreamConstraints = constraints ?? {\n audio: {\n echoCancellation,\n noiseSuppression,\n autoGainControl,\n ...(deviceId ? { deviceId: { exact: deviceId } } : {}),\n },\n video: false,\n };\n\n void navigator.mediaDevices\n .getUserMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n setStream(next);\n setStatus(\"ready\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n setError(classifyMediaError(err, \"microphone\"));\n setStatus(\"error\");\n });\n }, [constraints, deviceId, echoCancellation, noiseSuppression, autoGainControl]);\n\n useEffect(() => {\n if (autoStart) start();\n // `start` is stable per constraint set; re-running on every identity change\n // would reopen the device on each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoStart]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, start, stop };\n}\n"],"mappings":"6DAqEA,SAAgB,EAAc,EAAgC,CAAC,EAAwB,CACnF,GAAM,CACF,YAAY,GACZ,WACA,mBAAmB,GACnB,mBAAmB,GACnB,kBAAkB,GAClB,eACA,EAEE,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAA2B,MAAM,EACvD,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAA6B,IAAI,EACvD,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAkC,IAAI,EAC1D,GAAA,EAAY,EAAA,OAAA,CAA2B,IAAI,EAC3C,GAAA,EAAa,EAAA,OAAA,CAAO,CAAC,EAErB,GAAA,EAAU,EAAA,YAAA,KAAwB,CACpC,EAAU,SAAS,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAC9D,EAAU,QAAU,IACxB,EAAG,CAAC,CAAC,EAEC,GAAA,EAAO,EAAA,YAAA,KAAwB,CACjC,EAAW,SAAW,EACtB,EAAQ,EACR,EAAU,IAAI,EACd,EAAU,MAAM,EAChB,EAAS,IAAI,CACjB,EAAG,CAAC,CAAO,CAAC,EAEN,GAAA,EAAQ,EAAA,YAAA,KAAwB,CAClC,GAAI,EAAU,QAAS,OACvB,IAAM,EAAO,EAAW,SAAW,EAInC,GAHA,EAAU,YAAY,EACtB,EAAS,IAAI,EAET,CAAC,EAAA,wBAAwB,EAAG,CAC5B,EAAS,EAAA,uBAAuB,YAAY,CAAC,EAC7C,EAAU,OAAO,EACjB,MACJ,CAEA,IAAM,EAAkC,GAAe,CACnD,MAAO,CACH,mBACA,mBACA,kBACA,GAAI,EAAW,CAAE,SAAU,CAAE,MAAO,CAAS,CAAE,EAAI,CAAC,CACxD,EACA,MAAO,EACX,EAEA,UAAe,aACV,aAAa,CAAO,CAAC,CACrB,KAAM,GAAS,CACZ,GAAI,IAAQ,EAAW,QAAS,CAC5B,EAAK,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAChD,MACJ,CACA,EAAU,QAAU,EACpB,EAAU,CAAI,EACd,EAAU,OAAO,CACrB,CAAC,CAAC,CACD,MAAO,GAAiB,CACjB,IAAQ,EAAW,UACvB,EAAS,EAAA,mBAAmB,EAAK,YAAY,CAAC,EAC9C,EAAU,OAAO,EACrB,CAAC,CACT,EAAG,CAAC,EAAa,EAAU,EAAkB,EAAkB,CAAe,CAAC,EAW/E,OATA,EAAA,EAAA,UAAA,KAAgB,CACR,GAAW,EAAM,CAIzB,EAAG,CAAC,CAAS,CAAC,GAEd,EAAA,EAAA,UAAA,KAAgB,EAAS,CAAC,CAAO,CAAC,EAE3B,CAAE,SAAQ,SAAQ,QAAO,QAAO,MAAK,CAChD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-auth-store.cjs","names":[],"sources":["../../src/auth/create-auth-store.ts"],"sourcesContent":["import { create } from \"zustand\";\nimport { persist, createJSONStorage } from \"zustand/middleware\";\n\nexport interface AuthState<TUser> {\n user: TUser | null;\n token: string | null;\n isAuthenticated: boolean;\n setSession: (session: { user: TUser; token: string }) => void;\n setUser: (user: TUser | null) => void;\n setToken: (token: string | null) => void;\n logout: () => void;\n}\n\nexport interface CreateAuthStoreOptions<TUser> {\n /** Persist key used in storage (default: \"tempest-auth\"). */\n name?: string;\n /** Which storage to persist into (default: localStorage). */\n storage?: \"local\" | \"session\";\n /** Initial user (useful for SSR hydration). */\n initialUser?: TUser | null;\n /** Initial token. */\n initialToken?: string | null;\n}\n\n/**\n * Build a typed Zustand auth store with `persist` middleware. Each app passes\n * its own `TUser` shape so the SDK does not own the user model.\n *\n * @example\n * const useAuthStore = createAuthStore<UserResponse>();\n * const { user, token, logout } = useAuthStore();\n */\nexport function createAuthStore<TUser>(options: CreateAuthStoreOptions<TUser> = {}) {\n const name = options.name ?? \"tempest-auth\";\n const storageImpl = options.storage === \"session\" ? () => sessionStorage : () => localStorage;\n\n return create<AuthState<TUser>>()(\n persist(\n (set) => ({\n user: options.initialUser ?? null,\n token: options.initialToken ?? null,\n isAuthenticated: !!options.initialToken,\n setSession: ({ user, token }) => set({ user, token, isAuthenticated: true }),\n setUser: (user) => set({ user }),\n setToken: (token) => set({ token, isAuthenticated: !!token }),\n logout: () => set({ user: null, token: null, isAuthenticated: false }),\n }),\n {\n name,\n storage: createJSONStorage(storageImpl),\n partialize: (state) => ({ user: state.user, token: state.token }),\n onRehydrateStorage: () => (state) => {\n if (state) state.isAuthenticated = !!state.token;\n },\n },\n ),\n );\n}\n"],"mappings":"yDAgCA,SAAgB,EAAuB,EAAyC,CAAC,EAAG,CAChF,IAAM,EAAO,EAAQ,MAAQ,eACvB,EAAc,EAAQ,UAAY,cAAkB,mBAAuB,aAEjF,OAAA,
|
|
1
|
+
{"version":3,"file":"create-auth-store.cjs","names":[],"sources":["../../src/auth/create-auth-store.ts"],"sourcesContent":["import { create } from \"zustand\";\nimport { persist, createJSONStorage } from \"zustand/middleware\";\n\nexport interface AuthState<TUser> {\n user: TUser | null;\n token: string | null;\n isAuthenticated: boolean;\n setSession: (session: { user: TUser; token: string }) => void;\n setUser: (user: TUser | null) => void;\n setToken: (token: string | null) => void;\n logout: () => void;\n}\n\nexport interface CreateAuthStoreOptions<TUser> {\n /** Persist key used in storage (default: \"tempest-auth\"). */\n name?: string;\n /** Which storage to persist into (default: localStorage). */\n storage?: \"local\" | \"session\";\n /** Initial user (useful for SSR hydration). */\n initialUser?: TUser | null;\n /** Initial token. */\n initialToken?: string | null;\n}\n\n/**\n * Build a typed Zustand auth store with `persist` middleware. Each app passes\n * its own `TUser` shape so the SDK does not own the user model.\n *\n * @example\n * const useAuthStore = createAuthStore<UserResponse>();\n * const { user, token, logout } = useAuthStore();\n */\nexport function createAuthStore<TUser>(options: CreateAuthStoreOptions<TUser> = {}) {\n const name = options.name ?? \"tempest-auth\";\n const storageImpl = options.storage === \"session\" ? () => sessionStorage : () => localStorage;\n\n return create<AuthState<TUser>>()(\n persist(\n (set) => ({\n user: options.initialUser ?? null,\n token: options.initialToken ?? null,\n isAuthenticated: !!options.initialToken,\n setSession: ({ user, token }) => set({ user, token, isAuthenticated: true }),\n setUser: (user) => set({ user }),\n setToken: (token) => set({ token, isAuthenticated: !!token }),\n logout: () => set({ user: null, token: null, isAuthenticated: false }),\n }),\n {\n name,\n storage: createJSONStorage(storageImpl),\n partialize: (state) => ({ user: state.user, token: state.token }),\n onRehydrateStorage: () => (state) => {\n if (state) state.isAuthenticated = !!state.token;\n },\n },\n ),\n );\n}\n"],"mappings":"yDAgCA,SAAgB,EAAuB,EAAyC,CAAC,EAAG,CAChF,IAAM,EAAO,EAAQ,MAAQ,eACvB,EAAc,EAAQ,UAAY,cAAkB,mBAAuB,aAEjF,OAAA,EAAO,EAAA,OAAA,CAAyB,CAAC,EAAA,EAC7B,EAAA,QAAA,CACK,IAAS,CACN,KAAM,EAAQ,aAAe,KAC7B,MAAO,EAAQ,cAAgB,KAC/B,gBAAiB,CAAC,CAAC,EAAQ,aAC3B,YAAa,CAAE,OAAM,WAAY,EAAI,CAAE,OAAM,QAAO,gBAAiB,EAAK,CAAC,EAC3E,QAAU,GAAS,EAAI,CAAE,MAAK,CAAC,EAC/B,SAAW,GAAU,EAAI,CAAE,QAAO,gBAAiB,CAAC,CAAC,CAAM,CAAC,EAC5D,WAAc,EAAI,CAAE,KAAM,KAAM,MAAO,KAAM,gBAAiB,EAAM,CAAC,CACzE,GACA,CACI,OACA,SAAA,EAAS,EAAA,kBAAA,CAAkB,CAAW,EACtC,WAAa,IAAW,CAAE,KAAM,EAAM,KAAM,MAAO,EAAM,KAAM,GAC/D,uBAA2B,GAAU,CAC7B,IAAO,EAAM,gBAAkB,CAAC,CAAC,EAAM,MAC/C,CACJ,CACJ,CACJ,CACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy-with-retry.cjs","names":[],"sources":["../../src/auth/lazy-with-retry.ts"],"sourcesContent":["import { lazy } from \"react\";\nimport type { ComponentType } from \"react\";\n\nexport interface LazyWithRetryOptions {\n /** Max attempts. Default: 3. */\n retries?: number;\n /** Initial delay (ms) before retrying. Default: 400. */\n initialDelay?: number;\n /**\n * Reload the page after every retry fails. Helps when the stale chunk\n * error is caused by an old `index.html` referencing deleted bundles.\n * Default: true.\n */\n reloadOnFinalFailure?: boolean;\n}\n\n/**\n * Wrap `React.lazy` with automatic retry. Common cause of failure:\n * deployed-then-cached `index.html` references chunk filenames that no\n * longer exist. Retrying after a short delay typically picks up the new\n * bundle; a final `location.reload()` recovers from stale `index.html`.\n *\n * @example\n * const Settings = lazyWithRetry(() => import(\"./Settings\"));\n */\nexport function lazyWithRetry<T extends ComponentType<unknown>>(\n factory: () => Promise<{ default: T }>,\n options: LazyWithRetryOptions = {},\n): ReturnType<typeof lazy<T>> {\n const { retries = 3, initialDelay = 400, reloadOnFinalFailure = true } = options;\n\n async function load(attempt = 1): Promise<{ default: T }> {\n try {\n return await factory();\n } catch (error) {\n if (attempt >= retries) {\n if (reloadOnFinalFailure && typeof window !== \"undefined\") {\n window.location.reload();\n }\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, initialDelay * 2 ** (attempt - 1)));\n return load(attempt + 1);\n }\n }\n\n return lazy(load) as ReturnType<typeof lazy<T>>;\n}\n"],"mappings":"uBAyBA,SAAgB,EACZ,EACA,EAAgC,CAAC,EACP,CAC1B,GAAM,CAAE,UAAU,EAAG,eAAe,IAAK,uBAAuB,IAAS,EAEzE,eAAe,EAAK,EAAU,EAA4B,CACtD,GAAI,CACA,OAAO,MAAM,EAAQ,CACzB,OAAS,EAAO,CACZ,GAAI,GAAW,EAIX,MAHI,GAAwB,OAAO,OAAW,KAC1C,OAAO,SAAS,OAAO,EAErB,EAGV,OADA,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAe,IAAM,EAAU,EAAE,CAAC,EAC9E,EAAK,EAAU,CAAC,CAC3B,CACJ,CAEA,OAAA,
|
|
1
|
+
{"version":3,"file":"lazy-with-retry.cjs","names":[],"sources":["../../src/auth/lazy-with-retry.ts"],"sourcesContent":["import { lazy } from \"react\";\nimport type { ComponentType } from \"react\";\n\nexport interface LazyWithRetryOptions {\n /** Max attempts. Default: 3. */\n retries?: number;\n /** Initial delay (ms) before retrying. Default: 400. */\n initialDelay?: number;\n /**\n * Reload the page after every retry fails. Helps when the stale chunk\n * error is caused by an old `index.html` referencing deleted bundles.\n * Default: true.\n */\n reloadOnFinalFailure?: boolean;\n}\n\n/**\n * Wrap `React.lazy` with automatic retry. Common cause of failure:\n * deployed-then-cached `index.html` references chunk filenames that no\n * longer exist. Retrying after a short delay typically picks up the new\n * bundle; a final `location.reload()` recovers from stale `index.html`.\n *\n * @example\n * const Settings = lazyWithRetry(() => import(\"./Settings\"));\n */\nexport function lazyWithRetry<T extends ComponentType<unknown>>(\n factory: () => Promise<{ default: T }>,\n options: LazyWithRetryOptions = {},\n): ReturnType<typeof lazy<T>> {\n const { retries = 3, initialDelay = 400, reloadOnFinalFailure = true } = options;\n\n async function load(attempt = 1): Promise<{ default: T }> {\n try {\n return await factory();\n } catch (error) {\n if (attempt >= retries) {\n if (reloadOnFinalFailure && typeof window !== \"undefined\") {\n window.location.reload();\n }\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, initialDelay * 2 ** (attempt - 1)));\n return load(attempt + 1);\n }\n }\n\n return lazy(load) as ReturnType<typeof lazy<T>>;\n}\n"],"mappings":"uBAyBA,SAAgB,EACZ,EACA,EAAgC,CAAC,EACP,CAC1B,GAAM,CAAE,UAAU,EAAG,eAAe,IAAK,uBAAuB,IAAS,EAEzE,eAAe,EAAK,EAAU,EAA4B,CACtD,GAAI,CACA,OAAO,MAAM,EAAQ,CACzB,OAAS,EAAO,CACZ,GAAI,GAAW,EAIX,MAHI,GAAwB,OAAO,OAAW,KAC1C,OAAO,SAAS,OAAO,EAErB,EAGV,OADA,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAe,IAAM,EAAU,EAAE,CAAC,EAC9E,EAAK,EAAU,CAAC,CAC3B,CACJ,CAEA,OAAA,EAAO,EAAA,KAAA,CAAK,CAAI,CACpB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passkey.cjs","names":[],"sources":["../../src/auth/passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the WebAuthn client half: the JSON\n * shapes both ceremonies exchange with a backend, base64url ↔ ArrayBuffer, the\n * capability probes and the error classifier. The two ceremonies are near-mirrors\n * that must not drift — register and authenticate encode the same credential fields\n * in the same order — and the file's docstring is also the specification of the four\n * backend routes it expects.\n */\n/**\n * Classified reason a passkey ceremony did not produce a credential.\n *\n * The kinds group the raw `DOMException.name` values the way a UI has to branch\n * on them, which is *not* how the spec groups them:\n *\n * - `\"cancelled\"` covers `NotAllowedError`, which the browser raises both when\n * the user dismissed the sheet and when the ceremony timed out. They are\n * indistinguishable **by design** — telling a site \"the user has no credential\n * for you\" would leak account existence — so a UI must treat them as one thing.\n * - `\"already-registered\"` (`InvalidStateError`) is not really a failure: this\n * device already holds a credential for this user. The correct reaction is\n * \"you are already set up on this device\", never a red error.\n * - `\"rp-mismatch\"` (`SecurityError`) is the single most common integration bug:\n * `rp.id` must equal the page's domain or a registrable parent of it.\n */\nexport type PasskeyErrorKind =\n | \"unsupported\"\n | \"insecure\"\n | \"cancelled\"\n | \"already-registered\"\n | \"not-supported\"\n | \"rp-mismatch\"\n | \"invalid-options\"\n | \"aborted\"\n | \"unknown\";\n\n/** Which ceremony was running, so the message can name it. */\nexport type PasskeyCeremony = \"register\" | \"authenticate\";\n\n/**\n * A passkey failure carrying a stable {@link PasskeyErrorKind} plus an English\n * message safe to show a user.\n *\n * A class rather than the plain `{ kind, message }` object the media classifier\n * returns, because these surface by rejecting a promise: an `Error` subclass keeps\n * stack traces, `instanceof` checks and logging intact, and `kind` is what code\n * branches on.\n */\nexport class PasskeyError extends Error {\n /** Stable, branchable classification. */\n readonly kind: PasskeyErrorKind;\n\n /**\n * Build a classified passkey error.\n *\n * @param kind - The classification a UI branches on.\n * @param message - English, user-safe explanation.\n * @param cause - The original thrown value, when there was one.\n */\n constructor(kind: PasskeyErrorKind, message: string, cause?: unknown) {\n super(message);\n this.name = \"PasskeyError\";\n this.kind = kind;\n this.cause = cause;\n }\n}\n\n/**\n * Minimal subset of `navigator.credentials` the passkey client touches.\n *\n * Declared here — the `<X>Like` pattern the SDK's adapters use — for two reasons:\n * jsdom has no `navigator.credentials` at all, so tests must inject a double; and\n * `mediation: \"conditional\"` is newer than some TypeScript DOM libs, which would\n * otherwise reject the call that makes autofill work.\n */\nexport interface CredentialsContainerLike {\n /** Runs the registration ceremony. */\n create(options: {\n publicKey: PublicKeyCredentialCreationOptions;\n signal?: AbortSignal;\n }): Promise<Credential | null>;\n /** Runs the authentication ceremony. */\n get(options: {\n publicKey: PublicKeyCredentialRequestOptions;\n signal?: AbortSignal;\n mediation?: string;\n }): Promise<Credential | null>;\n}\n\n/** How the browser should surface the authentication ceremony. */\nexport type PasskeyMediation = \"optional\" | \"conditional\" | \"required\" | \"silent\";\n\n/**\n * Server-issued registration options, in the base64url JSON shape every WebAuthn\n * backend speaks (`PublicKeyCredentialCreationOptionsJSON` in the spec).\n *\n * `challenge`, `user.id` and every `excludeCredentials[].id` are **base64url**\n * strings here and `ArrayBuffer`s in the DOM API. Converting them is the plumbing\n * this client owns.\n */\nexport interface PasskeyCreationOptionsJSON {\n /** Base64url server challenge. Single-use; the server must remember it. */\n challenge: string;\n /** Relying party. `id` defaults to the client's `rpId`, then to the origin. */\n rp: { name: string; id?: string };\n /** The account. `id` is base64url of an opaque, stable user handle. */\n user: { id: string; name: string; displayName: string };\n /** Allowed COSE algorithms. Defaults to {@link DEFAULT_PUB_KEY_CRED_PARAMS}. */\n pubKeyCredParams?: { type: \"public-key\"; alg: number }[];\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Credentials this user already has, so the authenticator refuses a duplicate. */\n excludeCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Resident-key / user-verification / attachment requirements. */\n authenticatorSelection?: AuthenticatorSelectionCriteria;\n /** Attestation conveyance. Leave unset (`\"none\"`) unless you verify it. */\n attestation?: AttestationConveyancePreference;\n /** Client extension inputs (`credProps`, `largeBlob`, …). */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** Server-issued authentication options, base64url JSON. */\nexport interface PasskeyRequestOptionsJSON {\n /** Base64url server challenge. */\n challenge: string;\n /** Relying party id. Defaults to the client's `rpId`, then to the origin. */\n rpId?: string;\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Restrict to these credentials. **Omit it** for usernameless / autofill flows. */\n allowCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Whether the authenticator must verify the user (biometric / PIN). */\n userVerification?: UserVerificationRequirement;\n /** Client extension inputs. */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** What the client sends to the backend to finish registration. */\nexport interface PasskeyRegistrationJSON {\n /** Base64url credential id. */\n id: string;\n /** Same bytes as `id`; both are sent because servers differ on which they read. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` (this device) or `\"cross-platform\"` (a phone or key). */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data — the server re-checks challenge, origin and type. */\n clientDataJSON: string;\n /** Base64url attestation object, holding the new public key. */\n attestationObject: string;\n /** Transports the authenticator advertises, when the browser exposes them. */\n transports?: string[];\n /** COSE algorithm of the new key, when the browser exposes it. */\n publicKeyAlgorithm?: number;\n /** Base64url SPKI public key, when the browser exposes it. */\n publicKey?: string;\n /** Base64url authenticator data, when the browser exposes it. */\n authenticatorData?: string;\n };\n /** Extension outputs — `credProps.rk` tells you whether it is discoverable. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** What the client sends to the backend to finish authentication. */\nexport interface PasskeyAuthenticationJSON {\n /** Base64url credential id — the server looks up the stored public key by it. */\n id: string;\n /** Same bytes as `id`. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` or `\"cross-platform\"`. */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data. */\n clientDataJSON: string;\n /** Base64url authenticator data, carrying the signature counter. */\n authenticatorData: string;\n /** Base64url signature over `authenticatorData || sha256(clientDataJSON)`. */\n signature: string;\n /** Base64url user handle — present on discoverable credentials, else null. */\n userHandle: string | null;\n };\n /** Extension outputs. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** Per-call knobs for {@link PasskeyClient.register}. */\nexport interface PasskeyRegisterInit {\n /** Cancel the ceremony (closes the browser sheet). */\n signal?: AbortSignal;\n}\n\n/** Per-call knobs for {@link PasskeyClient.authenticate}. */\nexport interface PasskeyAuthenticateInit extends PasskeyRegisterInit {\n /**\n * `\"conditional\"` is the autofill flow: no modal, the browser offers passkeys\n * inside a field marked `autocomplete=\"webauthn\"`. Requires a `signal`, and\n * only one conditional request may be live per page.\n */\n mediation?: PasskeyMediation;\n}\n\n/** Framework-free WebAuthn client. Build one with {@link createPasskeyClient}. */\nexport interface PasskeyClient {\n /** Run the registration ceremony and return the JSON your backend verifies. */\n register(\n options: PasskeyCreationOptionsJSON,\n init?: PasskeyRegisterInit,\n ): Promise<PasskeyRegistrationJSON>;\n /** Run the authentication ceremony and return the JSON your backend verifies. */\n authenticate(\n options: PasskeyRequestOptionsJSON,\n init?: PasskeyAuthenticateInit,\n ): Promise<PasskeyAuthenticationJSON>;\n /**\n * Whether this client can run a ceremony at all — the WebAuthn API exists, or a\n * `credentials` container was injected.\n */\n isSupported(): boolean;\n /** Whether this device has a built-in authenticator (Face ID, Hello, …). */\n isPlatformAuthenticatorAvailable(): Promise<boolean>;\n /** Whether autofill-driven (`\"conditional\"`) requests are available. */\n isConditionalMediationAvailable(): Promise<boolean>;\n}\n\n/** Options for {@link createPasskeyClient}. */\nexport interface CreatePasskeyClientOptions {\n /**\n * Default relying-party id, applied when the server options omit one. Must be\n * the page's domain or a registrable parent of it (`app.acme.com` may use\n * `acme.com`, never the other way round).\n */\n rpId?: string;\n /** Default ceremony timeout in ms. Default `60_000`. */\n timeoutMs?: number;\n /** `navigator.credentials` replacement, for tests. */\n credentials?: CredentialsContainerLike;\n}\n\n/**\n * COSE algorithms offered when the server sends no `pubKeyCredParams`, in\n * preference order.\n *\n * `-8` is Ed25519, which modern authenticators prefer and which produces the\n * smallest signatures. `-7` is ES256, the one algorithm every WebAuthn\n * authenticator supports. `-257` is RS256, needed for TPM-backed Windows Hello.\n * Offering all three is what avoids a `NotSupportedError` on some device you do\n * not own; a server that cannot verify one of them should send its own list.\n */\nexport const DEFAULT_PUB_KEY_CRED_PARAMS: { type: \"public-key\"; alg: number }[] = [\n { type: \"public-key\", alg: -8 },\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n];\n\n/** Static members of `PublicKeyCredential` that are not in every DOM lib yet. */\ninterface PublicKeyCredentialStatics {\n isUserVerifyingPlatformAuthenticatorAvailable?: () => Promise<boolean>;\n isConditionalMediationAvailable?: () => Promise<boolean>;\n}\n\n/** The browser-supplied extras on an attestation response, all optional. */\ninterface AttestationExtras {\n getTransports?: () => string[];\n getPublicKey?: () => ArrayBuffer | null;\n getPublicKeyAlgorithm?: () => number;\n getAuthenticatorData?: () => ArrayBuffer;\n}\n\nfunction publicKeyCredentialStatics(): PublicKeyCredentialStatics | undefined {\n return (globalThis as { PublicKeyCredential?: PublicKeyCredentialStatics }).PublicKeyCredential;\n}\n\n/**\n * Decode a base64url string into bytes.\n *\n * WebAuthn transports every binary field as base64url (`-`/`_`, no padding)\n * because that is what survives JSON, while the DOM API insists on\n * `ArrayBuffer`. Getting this pair wrong — usually by feeding plain base64 to\n * `atob` and losing the last byte — is the classic broken-WebAuthn bug, which is\n * why the SDK owns it instead of leaving it to each app.\n *\n * @param value - Base64url text, with or without `=` padding.\n * @returns The decoded bytes.\n */\nexport function base64UrlToBytes(value: string): Uint8Array {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\n\n/**\n * Encode bytes as an unpadded base64url string.\n *\n * @param value - Bytes to encode, as a view or a raw buffer.\n * @returns Base64url text, safe to put in JSON and in a URL.\n */\nexport function bytesToBase64Url(value: ArrayBuffer | Uint8Array): string {\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Whether this browser exposes WebAuthn at all.\n *\n * Note that \"supported\" is not \"usable\": WebAuthn also requires a secure context,\n * and a device may have no authenticator. Use\n * {@link isPlatformAuthenticatorAvailable} before offering a passkey button.\n */\nexport function isPasskeySupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n navigator.credentials !== undefined &&\n typeof navigator.credentials.create === \"function\" &&\n publicKeyCredentialStatics() !== undefined\n );\n}\n\n/**\n * Whether the device has a **built-in** authenticator — Face ID, Touch ID,\n * Windows Hello, an Android screen lock.\n *\n * This is the check that decides whether \"Entrar com passkey\" may be shown at\n * all. `isPasskeySupported()` is true on a desktop with no biometrics and no\n * security key, and offering a passkey there sends the user into a sheet that can\n * only be cancelled. A `false` here does not forbid passkeys — a phone can still\n * be used over hybrid/QR — it means the flow needs a second step, so present it\n * as \"usar meu celular\", not as one tap.\n *\n * @returns `false` when the API is missing, so a caller never has to null-check.\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isUserVerifyingPlatformAuthenticatorAvailable) return false;\n try {\n return await statics.isUserVerifyingPlatformAuthenticatorAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Whether autofill-driven passkeys (`mediation: \"conditional\"`) are available.\n *\n * @returns `false` when the API is missing or throws.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isConditionalMediationAvailable) return false;\n try {\n return await statics.isConditionalMediationAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Map a thrown WebAuthn failure onto a {@link PasskeyError}.\n *\n * The secure-context check runs first for the same reason it does in the media\n * classifier: over plain HTTP the whole API is absent or refuses, and reporting\n * \"not supported\" sends a developer hunting for a polyfill for something an\n * `https://` URL fixes.\n *\n * @param error - Whatever `navigator.credentials` rejected with.\n * @param ceremony - Which ceremony was running, for the message.\n * @returns The classified error, ready to throw or to show.\n */\nexport function classifyPasskeyError(error: unknown, ceremony: PasskeyCeremony): PasskeyError {\n if (error instanceof PasskeyError) return error;\n\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return new PasskeyError(\"insecure\", \"Passkeys require a secure (HTTPS) connection.\", error);\n }\n\n if (error instanceof DOMException) {\n switch (error.name) {\n case \"NotAllowedError\":\n return new PasskeyError(\n \"cancelled\",\n \"The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.\",\n error,\n );\n case \"InvalidStateError\":\n return new PasskeyError(\n \"already-registered\",\n \"This device already has a passkey for this account. Sign in with it instead of creating another.\",\n error,\n );\n case \"NotSupportedError\":\n return new PasskeyError(\n \"not-supported\",\n \"No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.\",\n error,\n );\n case \"SecurityError\":\n return new PasskeyError(\n \"rp-mismatch\",\n \"The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.\",\n error,\n );\n case \"AbortError\":\n return new PasskeyError(\"aborted\", \"The passkey request was aborted.\", error);\n }\n }\n\n if (error instanceof TypeError) {\n return new PasskeyError(\n \"invalid-options\",\n `The ${ceremony} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,\n error,\n );\n }\n\n return new PasskeyError(\n \"unknown\",\n error instanceof Error\n ? error.message\n : `Unexpected error during the passkey ${ceremony} ceremony.`,\n error,\n );\n}\n\nfunction toDescriptors(\n list: { id: string; type: \"public-key\"; transports?: string[] }[] | undefined,\n): PublicKeyCredentialDescriptor[] | undefined {\n if (!list) return undefined;\n return list.map((item) => ({\n id: base64UrlToBytes(item.id) as unknown as BufferSource,\n type: item.type,\n transports: item.transports as AuthenticatorTransport[] | undefined,\n }));\n}\n\n/**\n * Build a WebAuthn client: the base64url ↔ `ArrayBuffer` plumbing, the two\n * ceremonies, and one classified error type.\n *\n * ## What your backend must do\n *\n * This is the **client half only**, and a WebAuthn client that documents only its\n * own half is unusable. Four routes are yours to implement:\n *\n * 1. `POST /webauthn/register/begin` → a {@link PasskeyCreationOptionsJSON}. Mint a\n * random `challenge` (≥16 bytes), store it against the session, and list the\n * user's existing credentials in `excludeCredentials`.\n * 2. `POST /webauthn/register/finish` ← a {@link PasskeyRegistrationJSON}. Verify\n * the challenge, `origin` and `type` inside `clientDataJSON`, parse the\n * attestation object, then store the credential id, public key and signature\n * counter.\n * 3. `POST /webauthn/signin/begin` → a {@link PasskeyRequestOptionsJSON}. New\n * challenge. Omit `allowCredentials` for a usernameless or autofill flow.\n * 4. `POST /webauthn/signin/finish` ← a {@link PasskeyAuthenticationJSON}. Look the\n * credential up by `id`, verify the signature over\n * `authenticatorData || sha256(clientDataJSON)`, and reject a signature counter\n * that did not grow (a clone). Only then issue your session token.\n *\n * @param options - Defaults applied when the server options omit them.\n * @returns A client usable from anywhere — React, a plain form, a worker.\n *\n * @example\n * const passkeys = createPasskeyClient({ rpId: \"acme.com\" });\n *\n * const options = await api.post(\"/webauthn/register/begin\");\n * const credential = await passkeys.register(options);\n * await api.post(\"/webauthn/register/finish\", { body: credential });\n */\nexport function createPasskeyClient(options: CreatePasskeyClientOptions = {}): PasskeyClient {\n const { rpId, timeoutMs = 60_000, credentials } = options;\n\n function container(): CredentialsContainerLike {\n if (credentials) return credentials;\n if (!isPasskeySupported()) {\n throw new PasskeyError(\n \"unsupported\",\n typeof window !== \"undefined\" && !window.isSecureContext\n ? \"Passkeys require a secure (HTTPS) connection.\"\n : \"This browser does not support passkeys (WebAuthn).\",\n );\n }\n return navigator.credentials as unknown as CredentialsContainerLike;\n }\n\n async function register(\n json: PasskeyCreationOptionsJSON,\n init: PasskeyRegisterInit = {},\n ): Promise<PasskeyRegistrationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialCreationOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rp: { name: json.rp.name, id: json.rp.id ?? rpId },\n user: {\n id: base64UrlToBytes(json.user.id) as unknown as BufferSource,\n name: json.user.name,\n displayName: json.user.displayName,\n },\n pubKeyCredParams: json.pubKeyCredParams ?? DEFAULT_PUB_KEY_CRED_PARAMS,\n timeout: json.timeout ?? timeoutMs,\n excludeCredentials: toDescriptors(json.excludeCredentials),\n authenticatorSelection: json.authenticatorSelection,\n attestation: json.attestation,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.create({ publicKey, signal: init.signal });\n } catch (error) {\n throw classifyPasskeyError(error, \"register\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAttestationResponse & AttestationExtras;\n const publicKeyBytes = response.getPublicKey?.();\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n attestationObject: bytesToBase64Url(response.attestationObject),\n transports: response.getTransports?.(),\n publicKeyAlgorithm: response.getPublicKeyAlgorithm?.(),\n publicKey: publicKeyBytes ? bytesToBase64Url(publicKeyBytes) : undefined,\n authenticatorData: response.getAuthenticatorData\n ? bytesToBase64Url(response.getAuthenticatorData())\n : undefined,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n async function authenticate(\n json: PasskeyRequestOptionsJSON,\n init: PasskeyAuthenticateInit = {},\n ): Promise<PasskeyAuthenticationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialRequestOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rpId: json.rpId ?? rpId,\n timeout: json.timeout ?? timeoutMs,\n allowCredentials: toDescriptors(json.allowCredentials),\n userVerification: json.userVerification,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.get({\n publicKey,\n signal: init.signal,\n mediation: init.mediation,\n });\n } catch (error) {\n throw classifyPasskeyError(error, \"authenticate\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAssertionResponse;\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n authenticatorData: bytesToBase64Url(response.authenticatorData),\n signature: bytesToBase64Url(response.signature),\n userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n return {\n register,\n authenticate,\n isSupported: () => credentials !== undefined || isPasskeySupported(),\n isPlatformAuthenticatorAvailable,\n isConditionalMediationAvailable,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"passkey.cjs","names":[],"sources":["../../src/auth/passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the WebAuthn client half: the JSON\n * shapes both ceremonies exchange with a backend, base64url ↔ ArrayBuffer, the\n * capability probes and the error classifier. The two ceremonies are near-mirrors\n * that must not drift — register and authenticate encode the same credential fields\n * in the same order — and the file's docstring is also the specification of the four\n * backend routes it expects.\n */\n/**\n * Classified reason a passkey ceremony did not produce a credential.\n *\n * The kinds group the raw `DOMException.name` values the way a UI has to branch\n * on them, which is *not* how the spec groups them:\n *\n * - `\"cancelled\"` covers `NotAllowedError`, which the browser raises both when\n * the user dismissed the sheet and when the ceremony timed out. They are\n * indistinguishable **by design** — telling a site \"the user has no credential\n * for you\" would leak account existence — so a UI must treat them as one thing.\n * - `\"already-registered\"` (`InvalidStateError`) is not really a failure: this\n * device already holds a credential for this user. The correct reaction is\n * \"you are already set up on this device\", never a red error.\n * - `\"rp-mismatch\"` (`SecurityError`) is the single most common integration bug:\n * `rp.id` must equal the page's domain or a registrable parent of it.\n */\nexport type PasskeyErrorKind =\n | \"unsupported\"\n | \"insecure\"\n | \"cancelled\"\n | \"already-registered\"\n | \"not-supported\"\n | \"rp-mismatch\"\n | \"invalid-options\"\n | \"aborted\"\n | \"unknown\";\n\n/** Which ceremony was running, so the message can name it. */\nexport type PasskeyCeremony = \"register\" | \"authenticate\";\n\n/**\n * A passkey failure carrying a stable {@link PasskeyErrorKind} plus an English\n * message safe to show a user.\n *\n * A class rather than the plain `{ kind, message }` object the media classifier\n * returns, because these surface by rejecting a promise: an `Error` subclass keeps\n * stack traces, `instanceof` checks and logging intact, and `kind` is what code\n * branches on.\n */\nexport class PasskeyError extends Error {\n /** Stable, branchable classification. */\n readonly kind: PasskeyErrorKind;\n\n /**\n * Build a classified passkey error.\n *\n * @param kind - The classification a UI branches on.\n * @param message - English, user-safe explanation.\n * @param cause - The original thrown value, when there was one.\n */\n constructor(kind: PasskeyErrorKind, message: string, cause?: unknown) {\n super(message);\n this.name = \"PasskeyError\";\n this.kind = kind;\n this.cause = cause;\n }\n}\n\n/**\n * Minimal subset of `navigator.credentials` the passkey client touches.\n *\n * Declared here — the `<X>Like` pattern the SDK's adapters use — for two reasons:\n * jsdom has no `navigator.credentials` at all, so tests must inject a double; and\n * `mediation: \"conditional\"` is newer than some TypeScript DOM libs, which would\n * otherwise reject the call that makes autofill work.\n */\nexport interface CredentialsContainerLike {\n /** Runs the registration ceremony. */\n create(options: {\n publicKey: PublicKeyCredentialCreationOptions;\n signal?: AbortSignal;\n }): Promise<Credential | null>;\n /** Runs the authentication ceremony. */\n get(options: {\n publicKey: PublicKeyCredentialRequestOptions;\n signal?: AbortSignal;\n mediation?: string;\n }): Promise<Credential | null>;\n}\n\n/** How the browser should surface the authentication ceremony. */\nexport type PasskeyMediation = \"optional\" | \"conditional\" | \"required\" | \"silent\";\n\n/**\n * Server-issued registration options, in the base64url JSON shape every WebAuthn\n * backend speaks (`PublicKeyCredentialCreationOptionsJSON` in the spec).\n *\n * `challenge`, `user.id` and every `excludeCredentials[].id` are **base64url**\n * strings here and `ArrayBuffer`s in the DOM API. Converting them is the plumbing\n * this client owns.\n */\nexport interface PasskeyCreationOptionsJSON {\n /** Base64url server challenge. Single-use; the server must remember it. */\n challenge: string;\n /** Relying party. `id` defaults to the client's `rpId`, then to the origin. */\n rp: { name: string; id?: string };\n /** The account. `id` is base64url of an opaque, stable user handle. */\n user: { id: string; name: string; displayName: string };\n /** Allowed COSE algorithms. Defaults to {@link DEFAULT_PUB_KEY_CRED_PARAMS}. */\n pubKeyCredParams?: { type: \"public-key\"; alg: number }[];\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Credentials this user already has, so the authenticator refuses a duplicate. */\n excludeCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Resident-key / user-verification / attachment requirements. */\n authenticatorSelection?: AuthenticatorSelectionCriteria;\n /** Attestation conveyance. Leave unset (`\"none\"`) unless you verify it. */\n attestation?: AttestationConveyancePreference;\n /** Client extension inputs (`credProps`, `largeBlob`, …). */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** Server-issued authentication options, base64url JSON. */\nexport interface PasskeyRequestOptionsJSON {\n /** Base64url server challenge. */\n challenge: string;\n /** Relying party id. Defaults to the client's `rpId`, then to the origin. */\n rpId?: string;\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Restrict to these credentials. **Omit it** for usernameless / autofill flows. */\n allowCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Whether the authenticator must verify the user (biometric / PIN). */\n userVerification?: UserVerificationRequirement;\n /** Client extension inputs. */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** What the client sends to the backend to finish registration. */\nexport interface PasskeyRegistrationJSON {\n /** Base64url credential id. */\n id: string;\n /** Same bytes as `id`; both are sent because servers differ on which they read. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` (this device) or `\"cross-platform\"` (a phone or key). */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data — the server re-checks challenge, origin and type. */\n clientDataJSON: string;\n /** Base64url attestation object, holding the new public key. */\n attestationObject: string;\n /** Transports the authenticator advertises, when the browser exposes them. */\n transports?: string[];\n /** COSE algorithm of the new key, when the browser exposes it. */\n publicKeyAlgorithm?: number;\n /** Base64url SPKI public key, when the browser exposes it. */\n publicKey?: string;\n /** Base64url authenticator data, when the browser exposes it. */\n authenticatorData?: string;\n };\n /** Extension outputs — `credProps.rk` tells you whether it is discoverable. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** What the client sends to the backend to finish authentication. */\nexport interface PasskeyAuthenticationJSON {\n /** Base64url credential id — the server looks up the stored public key by it. */\n id: string;\n /** Same bytes as `id`. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` or `\"cross-platform\"`. */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data. */\n clientDataJSON: string;\n /** Base64url authenticator data, carrying the signature counter. */\n authenticatorData: string;\n /** Base64url signature over `authenticatorData || sha256(clientDataJSON)`. */\n signature: string;\n /** Base64url user handle — present on discoverable credentials, else null. */\n userHandle: string | null;\n };\n /** Extension outputs. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** Per-call knobs for {@link PasskeyClient.register}. */\nexport interface PasskeyRegisterInit {\n /** Cancel the ceremony (closes the browser sheet). */\n signal?: AbortSignal;\n}\n\n/** Per-call knobs for {@link PasskeyClient.authenticate}. */\nexport interface PasskeyAuthenticateInit extends PasskeyRegisterInit {\n /**\n * `\"conditional\"` is the autofill flow: no modal, the browser offers passkeys\n * inside a field marked `autocomplete=\"webauthn\"`. Requires a `signal`, and\n * only one conditional request may be live per page.\n */\n mediation?: PasskeyMediation;\n}\n\n/** Framework-free WebAuthn client. Build one with {@link createPasskeyClient}. */\nexport interface PasskeyClient {\n /** Run the registration ceremony and return the JSON your backend verifies. */\n register(\n options: PasskeyCreationOptionsJSON,\n init?: PasskeyRegisterInit,\n ): Promise<PasskeyRegistrationJSON>;\n /** Run the authentication ceremony and return the JSON your backend verifies. */\n authenticate(\n options: PasskeyRequestOptionsJSON,\n init?: PasskeyAuthenticateInit,\n ): Promise<PasskeyAuthenticationJSON>;\n /**\n * Whether this client can run a ceremony at all — the WebAuthn API exists, or a\n * `credentials` container was injected.\n */\n isSupported(): boolean;\n /** Whether this device has a built-in authenticator (Face ID, Hello, …). */\n isPlatformAuthenticatorAvailable(): Promise<boolean>;\n /** Whether autofill-driven (`\"conditional\"`) requests are available. */\n isConditionalMediationAvailable(): Promise<boolean>;\n}\n\n/** Options for {@link createPasskeyClient}. */\nexport interface CreatePasskeyClientOptions {\n /**\n * Default relying-party id, applied when the server options omit one. Must be\n * the page's domain or a registrable parent of it (`app.acme.com` may use\n * `acme.com`, never the other way round).\n */\n rpId?: string;\n /** Default ceremony timeout in ms. Default `60_000`. */\n timeoutMs?: number;\n /** `navigator.credentials` replacement, for tests. */\n credentials?: CredentialsContainerLike;\n}\n\n/**\n * COSE algorithms offered when the server sends no `pubKeyCredParams`, in\n * preference order.\n *\n * `-8` is Ed25519, which modern authenticators prefer and which produces the\n * smallest signatures. `-7` is ES256, the one algorithm every WebAuthn\n * authenticator supports. `-257` is RS256, needed for TPM-backed Windows Hello.\n * Offering all three is what avoids a `NotSupportedError` on some device you do\n * not own; a server that cannot verify one of them should send its own list.\n */\nexport const DEFAULT_PUB_KEY_CRED_PARAMS: { type: \"public-key\"; alg: number }[] = [\n { type: \"public-key\", alg: -8 },\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n];\n\n/** Static members of `PublicKeyCredential` that are not in every DOM lib yet. */\ninterface PublicKeyCredentialStatics {\n isUserVerifyingPlatformAuthenticatorAvailable?: () => Promise<boolean>;\n isConditionalMediationAvailable?: () => Promise<boolean>;\n}\n\n/** The browser-supplied extras on an attestation response, all optional. */\ninterface AttestationExtras {\n getTransports?: () => string[];\n getPublicKey?: () => ArrayBuffer | null;\n getPublicKeyAlgorithm?: () => number;\n getAuthenticatorData?: () => ArrayBuffer;\n}\n\nfunction publicKeyCredentialStatics(): PublicKeyCredentialStatics | undefined {\n return (globalThis as { PublicKeyCredential?: PublicKeyCredentialStatics }).PublicKeyCredential;\n}\n\n/**\n * Decode a base64url string into bytes.\n *\n * WebAuthn transports every binary field as base64url (`-`/`_`, no padding)\n * because that is what survives JSON, while the DOM API insists on\n * `ArrayBuffer`. Getting this pair wrong — usually by feeding plain base64 to\n * `atob` and losing the last byte — is the classic broken-WebAuthn bug, which is\n * why the SDK owns it instead of leaving it to each app.\n *\n * @param value - Base64url text, with or without `=` padding.\n * @returns The decoded bytes.\n */\nexport function base64UrlToBytes(value: string): Uint8Array<ArrayBuffer> {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\n\n/**\n * Encode bytes as an unpadded base64url string.\n *\n * @param value - Bytes to encode, as a view or a raw buffer.\n * @returns Base64url text, safe to put in JSON and in a URL.\n */\nexport function bytesToBase64Url(value: ArrayBuffer | Uint8Array): string {\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Whether this browser exposes WebAuthn at all.\n *\n * Note that \"supported\" is not \"usable\": WebAuthn also requires a secure context,\n * and a device may have no authenticator. Use\n * {@link isPlatformAuthenticatorAvailable} before offering a passkey button.\n */\nexport function isPasskeySupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n navigator.credentials !== undefined &&\n typeof navigator.credentials.create === \"function\" &&\n publicKeyCredentialStatics() !== undefined\n );\n}\n\n/**\n * Whether the device has a **built-in** authenticator — Face ID, Touch ID,\n * Windows Hello, an Android screen lock.\n *\n * This is the check that decides whether \"Entrar com passkey\" may be shown at\n * all. `isPasskeySupported()` is true on a desktop with no biometrics and no\n * security key, and offering a passkey there sends the user into a sheet that can\n * only be cancelled. A `false` here does not forbid passkeys — a phone can still\n * be used over hybrid/QR — it means the flow needs a second step, so present it\n * as \"usar meu celular\", not as one tap.\n *\n * @returns `false` when the API is missing, so a caller never has to null-check.\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isUserVerifyingPlatformAuthenticatorAvailable) return false;\n try {\n return await statics.isUserVerifyingPlatformAuthenticatorAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Whether autofill-driven passkeys (`mediation: \"conditional\"`) are available.\n *\n * @returns `false` when the API is missing or throws.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isConditionalMediationAvailable) return false;\n try {\n return await statics.isConditionalMediationAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Map a thrown WebAuthn failure onto a {@link PasskeyError}.\n *\n * The secure-context check runs first for the same reason it does in the media\n * classifier: over plain HTTP the whole API is absent or refuses, and reporting\n * \"not supported\" sends a developer hunting for a polyfill for something an\n * `https://` URL fixes.\n *\n * @param error - Whatever `navigator.credentials` rejected with.\n * @param ceremony - Which ceremony was running, for the message.\n * @returns The classified error, ready to throw or to show.\n */\nexport function classifyPasskeyError(error: unknown, ceremony: PasskeyCeremony): PasskeyError {\n if (error instanceof PasskeyError) return error;\n\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return new PasskeyError(\"insecure\", \"Passkeys require a secure (HTTPS) connection.\", error);\n }\n\n if (error instanceof DOMException) {\n switch (error.name) {\n case \"NotAllowedError\":\n return new PasskeyError(\n \"cancelled\",\n \"The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.\",\n error,\n );\n case \"InvalidStateError\":\n return new PasskeyError(\n \"already-registered\",\n \"This device already has a passkey for this account. Sign in with it instead of creating another.\",\n error,\n );\n case \"NotSupportedError\":\n return new PasskeyError(\n \"not-supported\",\n \"No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.\",\n error,\n );\n case \"SecurityError\":\n return new PasskeyError(\n \"rp-mismatch\",\n \"The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.\",\n error,\n );\n case \"AbortError\":\n return new PasskeyError(\"aborted\", \"The passkey request was aborted.\", error);\n }\n }\n\n if (error instanceof TypeError) {\n return new PasskeyError(\n \"invalid-options\",\n `The ${ceremony} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,\n error,\n );\n }\n\n return new PasskeyError(\n \"unknown\",\n error instanceof Error\n ? error.message\n : `Unexpected error during the passkey ${ceremony} ceremony.`,\n error,\n );\n}\n\nfunction toDescriptors(\n list: { id: string; type: \"public-key\"; transports?: string[] }[] | undefined,\n): PublicKeyCredentialDescriptor[] | undefined {\n if (!list) return undefined;\n return list.map((item) => ({\n id: base64UrlToBytes(item.id) as unknown as BufferSource,\n type: item.type,\n transports: item.transports as AuthenticatorTransport[] | undefined,\n }));\n}\n\n/**\n * Build a WebAuthn client: the base64url ↔ `ArrayBuffer` plumbing, the two\n * ceremonies, and one classified error type.\n *\n * ## What your backend must do\n *\n * This is the **client half only**, and a WebAuthn client that documents only its\n * own half is unusable. Four routes are yours to implement:\n *\n * 1. `POST /webauthn/register/begin` → a {@link PasskeyCreationOptionsJSON}. Mint a\n * random `challenge` (≥16 bytes), store it against the session, and list the\n * user's existing credentials in `excludeCredentials`.\n * 2. `POST /webauthn/register/finish` ← a {@link PasskeyRegistrationJSON}. Verify\n * the challenge, `origin` and `type` inside `clientDataJSON`, parse the\n * attestation object, then store the credential id, public key and signature\n * counter.\n * 3. `POST /webauthn/signin/begin` → a {@link PasskeyRequestOptionsJSON}. New\n * challenge. Omit `allowCredentials` for a usernameless or autofill flow.\n * 4. `POST /webauthn/signin/finish` ← a {@link PasskeyAuthenticationJSON}. Look the\n * credential up by `id`, verify the signature over\n * `authenticatorData || sha256(clientDataJSON)`, and reject a signature counter\n * that did not grow (a clone). Only then issue your session token.\n *\n * @param options - Defaults applied when the server options omit them.\n * @returns A client usable from anywhere — React, a plain form, a worker.\n *\n * @example\n * const passkeys = createPasskeyClient({ rpId: \"acme.com\" });\n *\n * const options = await api.post(\"/webauthn/register/begin\");\n * const credential = await passkeys.register(options);\n * await api.post(\"/webauthn/register/finish\", { body: credential });\n */\nexport function createPasskeyClient(options: CreatePasskeyClientOptions = {}): PasskeyClient {\n const { rpId, timeoutMs = 60_000, credentials } = options;\n\n function container(): CredentialsContainerLike {\n if (credentials) return credentials;\n if (!isPasskeySupported()) {\n throw new PasskeyError(\n \"unsupported\",\n typeof window !== \"undefined\" && !window.isSecureContext\n ? \"Passkeys require a secure (HTTPS) connection.\"\n : \"This browser does not support passkeys (WebAuthn).\",\n );\n }\n return navigator.credentials as unknown as CredentialsContainerLike;\n }\n\n async function register(\n json: PasskeyCreationOptionsJSON,\n init: PasskeyRegisterInit = {},\n ): Promise<PasskeyRegistrationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialCreationOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rp: { name: json.rp.name, id: json.rp.id ?? rpId },\n user: {\n id: base64UrlToBytes(json.user.id) as unknown as BufferSource,\n name: json.user.name,\n displayName: json.user.displayName,\n },\n pubKeyCredParams: json.pubKeyCredParams ?? DEFAULT_PUB_KEY_CRED_PARAMS,\n timeout: json.timeout ?? timeoutMs,\n excludeCredentials: toDescriptors(json.excludeCredentials),\n authenticatorSelection: json.authenticatorSelection,\n attestation: json.attestation,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.create({ publicKey, signal: init.signal });\n } catch (error) {\n throw classifyPasskeyError(error, \"register\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAttestationResponse & AttestationExtras;\n const publicKeyBytes = response.getPublicKey?.();\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n attestationObject: bytesToBase64Url(response.attestationObject),\n transports: response.getTransports?.(),\n publicKeyAlgorithm: response.getPublicKeyAlgorithm?.(),\n publicKey: publicKeyBytes ? bytesToBase64Url(publicKeyBytes) : undefined,\n authenticatorData: response.getAuthenticatorData\n ? bytesToBase64Url(response.getAuthenticatorData())\n : undefined,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n async function authenticate(\n json: PasskeyRequestOptionsJSON,\n init: PasskeyAuthenticateInit = {},\n ): Promise<PasskeyAuthenticationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialRequestOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rpId: json.rpId ?? rpId,\n timeout: json.timeout ?? timeoutMs,\n allowCredentials: toDescriptors(json.allowCredentials),\n userVerification: json.userVerification,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.get({\n publicKey,\n signal: init.signal,\n mediation: init.mediation,\n });\n } catch (error) {\n throw classifyPasskeyError(error, \"authenticate\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAssertionResponse;\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n authenticatorData: bytesToBase64Url(response.authenticatorData),\n signature: bytesToBase64Url(response.signature),\n userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n return {\n register,\n authenticate,\n isSupported: () => credentials !== undefined || isPasskeySupported(),\n isPlatformAuthenticatorAvailable,\n isConditionalMediationAvailable,\n };\n}\n"],"mappings":"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"}
|
package/dist/auth/passkey.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passkey.js","names":[],"sources":["../../src/auth/passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the WebAuthn client half: the JSON\n * shapes both ceremonies exchange with a backend, base64url ↔ ArrayBuffer, the\n * capability probes and the error classifier. The two ceremonies are near-mirrors\n * that must not drift — register and authenticate encode the same credential fields\n * in the same order — and the file's docstring is also the specification of the four\n * backend routes it expects.\n */\n/**\n * Classified reason a passkey ceremony did not produce a credential.\n *\n * The kinds group the raw `DOMException.name` values the way a UI has to branch\n * on them, which is *not* how the spec groups them:\n *\n * - `\"cancelled\"` covers `NotAllowedError`, which the browser raises both when\n * the user dismissed the sheet and when the ceremony timed out. They are\n * indistinguishable **by design** — telling a site \"the user has no credential\n * for you\" would leak account existence — so a UI must treat them as one thing.\n * - `\"already-registered\"` (`InvalidStateError`) is not really a failure: this\n * device already holds a credential for this user. The correct reaction is\n * \"you are already set up on this device\", never a red error.\n * - `\"rp-mismatch\"` (`SecurityError`) is the single most common integration bug:\n * `rp.id` must equal the page's domain or a registrable parent of it.\n */\nexport type PasskeyErrorKind =\n | \"unsupported\"\n | \"insecure\"\n | \"cancelled\"\n | \"already-registered\"\n | \"not-supported\"\n | \"rp-mismatch\"\n | \"invalid-options\"\n | \"aborted\"\n | \"unknown\";\n\n/** Which ceremony was running, so the message can name it. */\nexport type PasskeyCeremony = \"register\" | \"authenticate\";\n\n/**\n * A passkey failure carrying a stable {@link PasskeyErrorKind} plus an English\n * message safe to show a user.\n *\n * A class rather than the plain `{ kind, message }` object the media classifier\n * returns, because these surface by rejecting a promise: an `Error` subclass keeps\n * stack traces, `instanceof` checks and logging intact, and `kind` is what code\n * branches on.\n */\nexport class PasskeyError extends Error {\n /** Stable, branchable classification. */\n readonly kind: PasskeyErrorKind;\n\n /**\n * Build a classified passkey error.\n *\n * @param kind - The classification a UI branches on.\n * @param message - English, user-safe explanation.\n * @param cause - The original thrown value, when there was one.\n */\n constructor(kind: PasskeyErrorKind, message: string, cause?: unknown) {\n super(message);\n this.name = \"PasskeyError\";\n this.kind = kind;\n this.cause = cause;\n }\n}\n\n/**\n * Minimal subset of `navigator.credentials` the passkey client touches.\n *\n * Declared here — the `<X>Like` pattern the SDK's adapters use — for two reasons:\n * jsdom has no `navigator.credentials` at all, so tests must inject a double; and\n * `mediation: \"conditional\"` is newer than some TypeScript DOM libs, which would\n * otherwise reject the call that makes autofill work.\n */\nexport interface CredentialsContainerLike {\n /** Runs the registration ceremony. */\n create(options: {\n publicKey: PublicKeyCredentialCreationOptions;\n signal?: AbortSignal;\n }): Promise<Credential | null>;\n /** Runs the authentication ceremony. */\n get(options: {\n publicKey: PublicKeyCredentialRequestOptions;\n signal?: AbortSignal;\n mediation?: string;\n }): Promise<Credential | null>;\n}\n\n/** How the browser should surface the authentication ceremony. */\nexport type PasskeyMediation = \"optional\" | \"conditional\" | \"required\" | \"silent\";\n\n/**\n * Server-issued registration options, in the base64url JSON shape every WebAuthn\n * backend speaks (`PublicKeyCredentialCreationOptionsJSON` in the spec).\n *\n * `challenge`, `user.id` and every `excludeCredentials[].id` are **base64url**\n * strings here and `ArrayBuffer`s in the DOM API. Converting them is the plumbing\n * this client owns.\n */\nexport interface PasskeyCreationOptionsJSON {\n /** Base64url server challenge. Single-use; the server must remember it. */\n challenge: string;\n /** Relying party. `id` defaults to the client's `rpId`, then to the origin. */\n rp: { name: string; id?: string };\n /** The account. `id` is base64url of an opaque, stable user handle. */\n user: { id: string; name: string; displayName: string };\n /** Allowed COSE algorithms. Defaults to {@link DEFAULT_PUB_KEY_CRED_PARAMS}. */\n pubKeyCredParams?: { type: \"public-key\"; alg: number }[];\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Credentials this user already has, so the authenticator refuses a duplicate. */\n excludeCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Resident-key / user-verification / attachment requirements. */\n authenticatorSelection?: AuthenticatorSelectionCriteria;\n /** Attestation conveyance. Leave unset (`\"none\"`) unless you verify it. */\n attestation?: AttestationConveyancePreference;\n /** Client extension inputs (`credProps`, `largeBlob`, …). */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** Server-issued authentication options, base64url JSON. */\nexport interface PasskeyRequestOptionsJSON {\n /** Base64url server challenge. */\n challenge: string;\n /** Relying party id. Defaults to the client's `rpId`, then to the origin. */\n rpId?: string;\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Restrict to these credentials. **Omit it** for usernameless / autofill flows. */\n allowCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Whether the authenticator must verify the user (biometric / PIN). */\n userVerification?: UserVerificationRequirement;\n /** Client extension inputs. */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** What the client sends to the backend to finish registration. */\nexport interface PasskeyRegistrationJSON {\n /** Base64url credential id. */\n id: string;\n /** Same bytes as `id`; both are sent because servers differ on which they read. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` (this device) or `\"cross-platform\"` (a phone or key). */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data — the server re-checks challenge, origin and type. */\n clientDataJSON: string;\n /** Base64url attestation object, holding the new public key. */\n attestationObject: string;\n /** Transports the authenticator advertises, when the browser exposes them. */\n transports?: string[];\n /** COSE algorithm of the new key, when the browser exposes it. */\n publicKeyAlgorithm?: number;\n /** Base64url SPKI public key, when the browser exposes it. */\n publicKey?: string;\n /** Base64url authenticator data, when the browser exposes it. */\n authenticatorData?: string;\n };\n /** Extension outputs — `credProps.rk` tells you whether it is discoverable. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** What the client sends to the backend to finish authentication. */\nexport interface PasskeyAuthenticationJSON {\n /** Base64url credential id — the server looks up the stored public key by it. */\n id: string;\n /** Same bytes as `id`. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` or `\"cross-platform\"`. */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data. */\n clientDataJSON: string;\n /** Base64url authenticator data, carrying the signature counter. */\n authenticatorData: string;\n /** Base64url signature over `authenticatorData || sha256(clientDataJSON)`. */\n signature: string;\n /** Base64url user handle — present on discoverable credentials, else null. */\n userHandle: string | null;\n };\n /** Extension outputs. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** Per-call knobs for {@link PasskeyClient.register}. */\nexport interface PasskeyRegisterInit {\n /** Cancel the ceremony (closes the browser sheet). */\n signal?: AbortSignal;\n}\n\n/** Per-call knobs for {@link PasskeyClient.authenticate}. */\nexport interface PasskeyAuthenticateInit extends PasskeyRegisterInit {\n /**\n * `\"conditional\"` is the autofill flow: no modal, the browser offers passkeys\n * inside a field marked `autocomplete=\"webauthn\"`. Requires a `signal`, and\n * only one conditional request may be live per page.\n */\n mediation?: PasskeyMediation;\n}\n\n/** Framework-free WebAuthn client. Build one with {@link createPasskeyClient}. */\nexport interface PasskeyClient {\n /** Run the registration ceremony and return the JSON your backend verifies. */\n register(\n options: PasskeyCreationOptionsJSON,\n init?: PasskeyRegisterInit,\n ): Promise<PasskeyRegistrationJSON>;\n /** Run the authentication ceremony and return the JSON your backend verifies. */\n authenticate(\n options: PasskeyRequestOptionsJSON,\n init?: PasskeyAuthenticateInit,\n ): Promise<PasskeyAuthenticationJSON>;\n /**\n * Whether this client can run a ceremony at all — the WebAuthn API exists, or a\n * `credentials` container was injected.\n */\n isSupported(): boolean;\n /** Whether this device has a built-in authenticator (Face ID, Hello, …). */\n isPlatformAuthenticatorAvailable(): Promise<boolean>;\n /** Whether autofill-driven (`\"conditional\"`) requests are available. */\n isConditionalMediationAvailable(): Promise<boolean>;\n}\n\n/** Options for {@link createPasskeyClient}. */\nexport interface CreatePasskeyClientOptions {\n /**\n * Default relying-party id, applied when the server options omit one. Must be\n * the page's domain or a registrable parent of it (`app.acme.com` may use\n * `acme.com`, never the other way round).\n */\n rpId?: string;\n /** Default ceremony timeout in ms. Default `60_000`. */\n timeoutMs?: number;\n /** `navigator.credentials` replacement, for tests. */\n credentials?: CredentialsContainerLike;\n}\n\n/**\n * COSE algorithms offered when the server sends no `pubKeyCredParams`, in\n * preference order.\n *\n * `-8` is Ed25519, which modern authenticators prefer and which produces the\n * smallest signatures. `-7` is ES256, the one algorithm every WebAuthn\n * authenticator supports. `-257` is RS256, needed for TPM-backed Windows Hello.\n * Offering all three is what avoids a `NotSupportedError` on some device you do\n * not own; a server that cannot verify one of them should send its own list.\n */\nexport const DEFAULT_PUB_KEY_CRED_PARAMS: { type: \"public-key\"; alg: number }[] = [\n { type: \"public-key\", alg: -8 },\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n];\n\n/** Static members of `PublicKeyCredential` that are not in every DOM lib yet. */\ninterface PublicKeyCredentialStatics {\n isUserVerifyingPlatformAuthenticatorAvailable?: () => Promise<boolean>;\n isConditionalMediationAvailable?: () => Promise<boolean>;\n}\n\n/** The browser-supplied extras on an attestation response, all optional. */\ninterface AttestationExtras {\n getTransports?: () => string[];\n getPublicKey?: () => ArrayBuffer | null;\n getPublicKeyAlgorithm?: () => number;\n getAuthenticatorData?: () => ArrayBuffer;\n}\n\nfunction publicKeyCredentialStatics(): PublicKeyCredentialStatics | undefined {\n return (globalThis as { PublicKeyCredential?: PublicKeyCredentialStatics }).PublicKeyCredential;\n}\n\n/**\n * Decode a base64url string into bytes.\n *\n * WebAuthn transports every binary field as base64url (`-`/`_`, no padding)\n * because that is what survives JSON, while the DOM API insists on\n * `ArrayBuffer`. Getting this pair wrong — usually by feeding plain base64 to\n * `atob` and losing the last byte — is the classic broken-WebAuthn bug, which is\n * why the SDK owns it instead of leaving it to each app.\n *\n * @param value - Base64url text, with or without `=` padding.\n * @returns The decoded bytes.\n */\nexport function base64UrlToBytes(value: string): Uint8Array {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\n\n/**\n * Encode bytes as an unpadded base64url string.\n *\n * @param value - Bytes to encode, as a view or a raw buffer.\n * @returns Base64url text, safe to put in JSON and in a URL.\n */\nexport function bytesToBase64Url(value: ArrayBuffer | Uint8Array): string {\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Whether this browser exposes WebAuthn at all.\n *\n * Note that \"supported\" is not \"usable\": WebAuthn also requires a secure context,\n * and a device may have no authenticator. Use\n * {@link isPlatformAuthenticatorAvailable} before offering a passkey button.\n */\nexport function isPasskeySupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n navigator.credentials !== undefined &&\n typeof navigator.credentials.create === \"function\" &&\n publicKeyCredentialStatics() !== undefined\n );\n}\n\n/**\n * Whether the device has a **built-in** authenticator — Face ID, Touch ID,\n * Windows Hello, an Android screen lock.\n *\n * This is the check that decides whether \"Entrar com passkey\" may be shown at\n * all. `isPasskeySupported()` is true on a desktop with no biometrics and no\n * security key, and offering a passkey there sends the user into a sheet that can\n * only be cancelled. A `false` here does not forbid passkeys — a phone can still\n * be used over hybrid/QR — it means the flow needs a second step, so present it\n * as \"usar meu celular\", not as one tap.\n *\n * @returns `false` when the API is missing, so a caller never has to null-check.\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isUserVerifyingPlatformAuthenticatorAvailable) return false;\n try {\n return await statics.isUserVerifyingPlatformAuthenticatorAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Whether autofill-driven passkeys (`mediation: \"conditional\"`) are available.\n *\n * @returns `false` when the API is missing or throws.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isConditionalMediationAvailable) return false;\n try {\n return await statics.isConditionalMediationAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Map a thrown WebAuthn failure onto a {@link PasskeyError}.\n *\n * The secure-context check runs first for the same reason it does in the media\n * classifier: over plain HTTP the whole API is absent or refuses, and reporting\n * \"not supported\" sends a developer hunting for a polyfill for something an\n * `https://` URL fixes.\n *\n * @param error - Whatever `navigator.credentials` rejected with.\n * @param ceremony - Which ceremony was running, for the message.\n * @returns The classified error, ready to throw or to show.\n */\nexport function classifyPasskeyError(error: unknown, ceremony: PasskeyCeremony): PasskeyError {\n if (error instanceof PasskeyError) return error;\n\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return new PasskeyError(\"insecure\", \"Passkeys require a secure (HTTPS) connection.\", error);\n }\n\n if (error instanceof DOMException) {\n switch (error.name) {\n case \"NotAllowedError\":\n return new PasskeyError(\n \"cancelled\",\n \"The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.\",\n error,\n );\n case \"InvalidStateError\":\n return new PasskeyError(\n \"already-registered\",\n \"This device already has a passkey for this account. Sign in with it instead of creating another.\",\n error,\n );\n case \"NotSupportedError\":\n return new PasskeyError(\n \"not-supported\",\n \"No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.\",\n error,\n );\n case \"SecurityError\":\n return new PasskeyError(\n \"rp-mismatch\",\n \"The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.\",\n error,\n );\n case \"AbortError\":\n return new PasskeyError(\"aborted\", \"The passkey request was aborted.\", error);\n }\n }\n\n if (error instanceof TypeError) {\n return new PasskeyError(\n \"invalid-options\",\n `The ${ceremony} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,\n error,\n );\n }\n\n return new PasskeyError(\n \"unknown\",\n error instanceof Error\n ? error.message\n : `Unexpected error during the passkey ${ceremony} ceremony.`,\n error,\n );\n}\n\nfunction toDescriptors(\n list: { id: string; type: \"public-key\"; transports?: string[] }[] | undefined,\n): PublicKeyCredentialDescriptor[] | undefined {\n if (!list) return undefined;\n return list.map((item) => ({\n id: base64UrlToBytes(item.id) as unknown as BufferSource,\n type: item.type,\n transports: item.transports as AuthenticatorTransport[] | undefined,\n }));\n}\n\n/**\n * Build a WebAuthn client: the base64url ↔ `ArrayBuffer` plumbing, the two\n * ceremonies, and one classified error type.\n *\n * ## What your backend must do\n *\n * This is the **client half only**, and a WebAuthn client that documents only its\n * own half is unusable. Four routes are yours to implement:\n *\n * 1. `POST /webauthn/register/begin` → a {@link PasskeyCreationOptionsJSON}. Mint a\n * random `challenge` (≥16 bytes), store it against the session, and list the\n * user's existing credentials in `excludeCredentials`.\n * 2. `POST /webauthn/register/finish` ← a {@link PasskeyRegistrationJSON}. Verify\n * the challenge, `origin` and `type` inside `clientDataJSON`, parse the\n * attestation object, then store the credential id, public key and signature\n * counter.\n * 3. `POST /webauthn/signin/begin` → a {@link PasskeyRequestOptionsJSON}. New\n * challenge. Omit `allowCredentials` for a usernameless or autofill flow.\n * 4. `POST /webauthn/signin/finish` ← a {@link PasskeyAuthenticationJSON}. Look the\n * credential up by `id`, verify the signature over\n * `authenticatorData || sha256(clientDataJSON)`, and reject a signature counter\n * that did not grow (a clone). Only then issue your session token.\n *\n * @param options - Defaults applied when the server options omit them.\n * @returns A client usable from anywhere — React, a plain form, a worker.\n *\n * @example\n * const passkeys = createPasskeyClient({ rpId: \"acme.com\" });\n *\n * const options = await api.post(\"/webauthn/register/begin\");\n * const credential = await passkeys.register(options);\n * await api.post(\"/webauthn/register/finish\", { body: credential });\n */\nexport function createPasskeyClient(options: CreatePasskeyClientOptions = {}): PasskeyClient {\n const { rpId, timeoutMs = 60_000, credentials } = options;\n\n function container(): CredentialsContainerLike {\n if (credentials) return credentials;\n if (!isPasskeySupported()) {\n throw new PasskeyError(\n \"unsupported\",\n typeof window !== \"undefined\" && !window.isSecureContext\n ? \"Passkeys require a secure (HTTPS) connection.\"\n : \"This browser does not support passkeys (WebAuthn).\",\n );\n }\n return navigator.credentials as unknown as CredentialsContainerLike;\n }\n\n async function register(\n json: PasskeyCreationOptionsJSON,\n init: PasskeyRegisterInit = {},\n ): Promise<PasskeyRegistrationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialCreationOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rp: { name: json.rp.name, id: json.rp.id ?? rpId },\n user: {\n id: base64UrlToBytes(json.user.id) as unknown as BufferSource,\n name: json.user.name,\n displayName: json.user.displayName,\n },\n pubKeyCredParams: json.pubKeyCredParams ?? DEFAULT_PUB_KEY_CRED_PARAMS,\n timeout: json.timeout ?? timeoutMs,\n excludeCredentials: toDescriptors(json.excludeCredentials),\n authenticatorSelection: json.authenticatorSelection,\n attestation: json.attestation,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.create({ publicKey, signal: init.signal });\n } catch (error) {\n throw classifyPasskeyError(error, \"register\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAttestationResponse & AttestationExtras;\n const publicKeyBytes = response.getPublicKey?.();\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n attestationObject: bytesToBase64Url(response.attestationObject),\n transports: response.getTransports?.(),\n publicKeyAlgorithm: response.getPublicKeyAlgorithm?.(),\n publicKey: publicKeyBytes ? bytesToBase64Url(publicKeyBytes) : undefined,\n authenticatorData: response.getAuthenticatorData\n ? bytesToBase64Url(response.getAuthenticatorData())\n : undefined,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n async function authenticate(\n json: PasskeyRequestOptionsJSON,\n init: PasskeyAuthenticateInit = {},\n ): Promise<PasskeyAuthenticationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialRequestOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rpId: json.rpId ?? rpId,\n timeout: json.timeout ?? timeoutMs,\n allowCredentials: toDescriptors(json.allowCredentials),\n userVerification: json.userVerification,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.get({\n publicKey,\n signal: init.signal,\n mediation: init.mediation,\n });\n } catch (error) {\n throw classifyPasskeyError(error, \"authenticate\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAssertionResponse;\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n authenticatorData: bytesToBase64Url(response.authenticatorData),\n signature: bytesToBase64Url(response.signature),\n userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n return {\n register,\n authenticate,\n isSupported: () => credentials !== undefined || isPasskeySupported(),\n isPlatformAuthenticatorAvailable,\n isConditionalMediationAvailable,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"passkey.js","names":[],"sources":["../../src/auth/passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the WebAuthn client half: the JSON\n * shapes both ceremonies exchange with a backend, base64url ↔ ArrayBuffer, the\n * capability probes and the error classifier. The two ceremonies are near-mirrors\n * that must not drift — register and authenticate encode the same credential fields\n * in the same order — and the file's docstring is also the specification of the four\n * backend routes it expects.\n */\n/**\n * Classified reason a passkey ceremony did not produce a credential.\n *\n * The kinds group the raw `DOMException.name` values the way a UI has to branch\n * on them, which is *not* how the spec groups them:\n *\n * - `\"cancelled\"` covers `NotAllowedError`, which the browser raises both when\n * the user dismissed the sheet and when the ceremony timed out. They are\n * indistinguishable **by design** — telling a site \"the user has no credential\n * for you\" would leak account existence — so a UI must treat them as one thing.\n * - `\"already-registered\"` (`InvalidStateError`) is not really a failure: this\n * device already holds a credential for this user. The correct reaction is\n * \"you are already set up on this device\", never a red error.\n * - `\"rp-mismatch\"` (`SecurityError`) is the single most common integration bug:\n * `rp.id` must equal the page's domain or a registrable parent of it.\n */\nexport type PasskeyErrorKind =\n | \"unsupported\"\n | \"insecure\"\n | \"cancelled\"\n | \"already-registered\"\n | \"not-supported\"\n | \"rp-mismatch\"\n | \"invalid-options\"\n | \"aborted\"\n | \"unknown\";\n\n/** Which ceremony was running, so the message can name it. */\nexport type PasskeyCeremony = \"register\" | \"authenticate\";\n\n/**\n * A passkey failure carrying a stable {@link PasskeyErrorKind} plus an English\n * message safe to show a user.\n *\n * A class rather than the plain `{ kind, message }` object the media classifier\n * returns, because these surface by rejecting a promise: an `Error` subclass keeps\n * stack traces, `instanceof` checks and logging intact, and `kind` is what code\n * branches on.\n */\nexport class PasskeyError extends Error {\n /** Stable, branchable classification. */\n readonly kind: PasskeyErrorKind;\n\n /**\n * Build a classified passkey error.\n *\n * @param kind - The classification a UI branches on.\n * @param message - English, user-safe explanation.\n * @param cause - The original thrown value, when there was one.\n */\n constructor(kind: PasskeyErrorKind, message: string, cause?: unknown) {\n super(message);\n this.name = \"PasskeyError\";\n this.kind = kind;\n this.cause = cause;\n }\n}\n\n/**\n * Minimal subset of `navigator.credentials` the passkey client touches.\n *\n * Declared here — the `<X>Like` pattern the SDK's adapters use — for two reasons:\n * jsdom has no `navigator.credentials` at all, so tests must inject a double; and\n * `mediation: \"conditional\"` is newer than some TypeScript DOM libs, which would\n * otherwise reject the call that makes autofill work.\n */\nexport interface CredentialsContainerLike {\n /** Runs the registration ceremony. */\n create(options: {\n publicKey: PublicKeyCredentialCreationOptions;\n signal?: AbortSignal;\n }): Promise<Credential | null>;\n /** Runs the authentication ceremony. */\n get(options: {\n publicKey: PublicKeyCredentialRequestOptions;\n signal?: AbortSignal;\n mediation?: string;\n }): Promise<Credential | null>;\n}\n\n/** How the browser should surface the authentication ceremony. */\nexport type PasskeyMediation = \"optional\" | \"conditional\" | \"required\" | \"silent\";\n\n/**\n * Server-issued registration options, in the base64url JSON shape every WebAuthn\n * backend speaks (`PublicKeyCredentialCreationOptionsJSON` in the spec).\n *\n * `challenge`, `user.id` and every `excludeCredentials[].id` are **base64url**\n * strings here and `ArrayBuffer`s in the DOM API. Converting them is the plumbing\n * this client owns.\n */\nexport interface PasskeyCreationOptionsJSON {\n /** Base64url server challenge. Single-use; the server must remember it. */\n challenge: string;\n /** Relying party. `id` defaults to the client's `rpId`, then to the origin. */\n rp: { name: string; id?: string };\n /** The account. `id` is base64url of an opaque, stable user handle. */\n user: { id: string; name: string; displayName: string };\n /** Allowed COSE algorithms. Defaults to {@link DEFAULT_PUB_KEY_CRED_PARAMS}. */\n pubKeyCredParams?: { type: \"public-key\"; alg: number }[];\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Credentials this user already has, so the authenticator refuses a duplicate. */\n excludeCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Resident-key / user-verification / attachment requirements. */\n authenticatorSelection?: AuthenticatorSelectionCriteria;\n /** Attestation conveyance. Leave unset (`\"none\"`) unless you verify it. */\n attestation?: AttestationConveyancePreference;\n /** Client extension inputs (`credProps`, `largeBlob`, …). */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** Server-issued authentication options, base64url JSON. */\nexport interface PasskeyRequestOptionsJSON {\n /** Base64url server challenge. */\n challenge: string;\n /** Relying party id. Defaults to the client's `rpId`, then to the origin. */\n rpId?: string;\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Restrict to these credentials. **Omit it** for usernameless / autofill flows. */\n allowCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Whether the authenticator must verify the user (biometric / PIN). */\n userVerification?: UserVerificationRequirement;\n /** Client extension inputs. */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** What the client sends to the backend to finish registration. */\nexport interface PasskeyRegistrationJSON {\n /** Base64url credential id. */\n id: string;\n /** Same bytes as `id`; both are sent because servers differ on which they read. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` (this device) or `\"cross-platform\"` (a phone or key). */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data — the server re-checks challenge, origin and type. */\n clientDataJSON: string;\n /** Base64url attestation object, holding the new public key. */\n attestationObject: string;\n /** Transports the authenticator advertises, when the browser exposes them. */\n transports?: string[];\n /** COSE algorithm of the new key, when the browser exposes it. */\n publicKeyAlgorithm?: number;\n /** Base64url SPKI public key, when the browser exposes it. */\n publicKey?: string;\n /** Base64url authenticator data, when the browser exposes it. */\n authenticatorData?: string;\n };\n /** Extension outputs — `credProps.rk` tells you whether it is discoverable. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** What the client sends to the backend to finish authentication. */\nexport interface PasskeyAuthenticationJSON {\n /** Base64url credential id — the server looks up the stored public key by it. */\n id: string;\n /** Same bytes as `id`. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` or `\"cross-platform\"`. */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data. */\n clientDataJSON: string;\n /** Base64url authenticator data, carrying the signature counter. */\n authenticatorData: string;\n /** Base64url signature over `authenticatorData || sha256(clientDataJSON)`. */\n signature: string;\n /** Base64url user handle — present on discoverable credentials, else null. */\n userHandle: string | null;\n };\n /** Extension outputs. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** Per-call knobs for {@link PasskeyClient.register}. */\nexport interface PasskeyRegisterInit {\n /** Cancel the ceremony (closes the browser sheet). */\n signal?: AbortSignal;\n}\n\n/** Per-call knobs for {@link PasskeyClient.authenticate}. */\nexport interface PasskeyAuthenticateInit extends PasskeyRegisterInit {\n /**\n * `\"conditional\"` is the autofill flow: no modal, the browser offers passkeys\n * inside a field marked `autocomplete=\"webauthn\"`. Requires a `signal`, and\n * only one conditional request may be live per page.\n */\n mediation?: PasskeyMediation;\n}\n\n/** Framework-free WebAuthn client. Build one with {@link createPasskeyClient}. */\nexport interface PasskeyClient {\n /** Run the registration ceremony and return the JSON your backend verifies. */\n register(\n options: PasskeyCreationOptionsJSON,\n init?: PasskeyRegisterInit,\n ): Promise<PasskeyRegistrationJSON>;\n /** Run the authentication ceremony and return the JSON your backend verifies. */\n authenticate(\n options: PasskeyRequestOptionsJSON,\n init?: PasskeyAuthenticateInit,\n ): Promise<PasskeyAuthenticationJSON>;\n /**\n * Whether this client can run a ceremony at all — the WebAuthn API exists, or a\n * `credentials` container was injected.\n */\n isSupported(): boolean;\n /** Whether this device has a built-in authenticator (Face ID, Hello, …). */\n isPlatformAuthenticatorAvailable(): Promise<boolean>;\n /** Whether autofill-driven (`\"conditional\"`) requests are available. */\n isConditionalMediationAvailable(): Promise<boolean>;\n}\n\n/** Options for {@link createPasskeyClient}. */\nexport interface CreatePasskeyClientOptions {\n /**\n * Default relying-party id, applied when the server options omit one. Must be\n * the page's domain or a registrable parent of it (`app.acme.com` may use\n * `acme.com`, never the other way round).\n */\n rpId?: string;\n /** Default ceremony timeout in ms. Default `60_000`. */\n timeoutMs?: number;\n /** `navigator.credentials` replacement, for tests. */\n credentials?: CredentialsContainerLike;\n}\n\n/**\n * COSE algorithms offered when the server sends no `pubKeyCredParams`, in\n * preference order.\n *\n * `-8` is Ed25519, which modern authenticators prefer and which produces the\n * smallest signatures. `-7` is ES256, the one algorithm every WebAuthn\n * authenticator supports. `-257` is RS256, needed for TPM-backed Windows Hello.\n * Offering all three is what avoids a `NotSupportedError` on some device you do\n * not own; a server that cannot verify one of them should send its own list.\n */\nexport const DEFAULT_PUB_KEY_CRED_PARAMS: { type: \"public-key\"; alg: number }[] = [\n { type: \"public-key\", alg: -8 },\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n];\n\n/** Static members of `PublicKeyCredential` that are not in every DOM lib yet. */\ninterface PublicKeyCredentialStatics {\n isUserVerifyingPlatformAuthenticatorAvailable?: () => Promise<boolean>;\n isConditionalMediationAvailable?: () => Promise<boolean>;\n}\n\n/** The browser-supplied extras on an attestation response, all optional. */\ninterface AttestationExtras {\n getTransports?: () => string[];\n getPublicKey?: () => ArrayBuffer | null;\n getPublicKeyAlgorithm?: () => number;\n getAuthenticatorData?: () => ArrayBuffer;\n}\n\nfunction publicKeyCredentialStatics(): PublicKeyCredentialStatics | undefined {\n return (globalThis as { PublicKeyCredential?: PublicKeyCredentialStatics }).PublicKeyCredential;\n}\n\n/**\n * Decode a base64url string into bytes.\n *\n * WebAuthn transports every binary field as base64url (`-`/`_`, no padding)\n * because that is what survives JSON, while the DOM API insists on\n * `ArrayBuffer`. Getting this pair wrong — usually by feeding plain base64 to\n * `atob` and losing the last byte — is the classic broken-WebAuthn bug, which is\n * why the SDK owns it instead of leaving it to each app.\n *\n * @param value - Base64url text, with or without `=` padding.\n * @returns The decoded bytes.\n */\nexport function base64UrlToBytes(value: string): Uint8Array<ArrayBuffer> {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\n\n/**\n * Encode bytes as an unpadded base64url string.\n *\n * @param value - Bytes to encode, as a view or a raw buffer.\n * @returns Base64url text, safe to put in JSON and in a URL.\n */\nexport function bytesToBase64Url(value: ArrayBuffer | Uint8Array): string {\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Whether this browser exposes WebAuthn at all.\n *\n * Note that \"supported\" is not \"usable\": WebAuthn also requires a secure context,\n * and a device may have no authenticator. Use\n * {@link isPlatformAuthenticatorAvailable} before offering a passkey button.\n */\nexport function isPasskeySupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n navigator.credentials !== undefined &&\n typeof navigator.credentials.create === \"function\" &&\n publicKeyCredentialStatics() !== undefined\n );\n}\n\n/**\n * Whether the device has a **built-in** authenticator — Face ID, Touch ID,\n * Windows Hello, an Android screen lock.\n *\n * This is the check that decides whether \"Entrar com passkey\" may be shown at\n * all. `isPasskeySupported()` is true on a desktop with no biometrics and no\n * security key, and offering a passkey there sends the user into a sheet that can\n * only be cancelled. A `false` here does not forbid passkeys — a phone can still\n * be used over hybrid/QR — it means the flow needs a second step, so present it\n * as \"usar meu celular\", not as one tap.\n *\n * @returns `false` when the API is missing, so a caller never has to null-check.\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isUserVerifyingPlatformAuthenticatorAvailable) return false;\n try {\n return await statics.isUserVerifyingPlatformAuthenticatorAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Whether autofill-driven passkeys (`mediation: \"conditional\"`) are available.\n *\n * @returns `false` when the API is missing or throws.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isConditionalMediationAvailable) return false;\n try {\n return await statics.isConditionalMediationAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Map a thrown WebAuthn failure onto a {@link PasskeyError}.\n *\n * The secure-context check runs first for the same reason it does in the media\n * classifier: over plain HTTP the whole API is absent or refuses, and reporting\n * \"not supported\" sends a developer hunting for a polyfill for something an\n * `https://` URL fixes.\n *\n * @param error - Whatever `navigator.credentials` rejected with.\n * @param ceremony - Which ceremony was running, for the message.\n * @returns The classified error, ready to throw or to show.\n */\nexport function classifyPasskeyError(error: unknown, ceremony: PasskeyCeremony): PasskeyError {\n if (error instanceof PasskeyError) return error;\n\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return new PasskeyError(\"insecure\", \"Passkeys require a secure (HTTPS) connection.\", error);\n }\n\n if (error instanceof DOMException) {\n switch (error.name) {\n case \"NotAllowedError\":\n return new PasskeyError(\n \"cancelled\",\n \"The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.\",\n error,\n );\n case \"InvalidStateError\":\n return new PasskeyError(\n \"already-registered\",\n \"This device already has a passkey for this account. Sign in with it instead of creating another.\",\n error,\n );\n case \"NotSupportedError\":\n return new PasskeyError(\n \"not-supported\",\n \"No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.\",\n error,\n );\n case \"SecurityError\":\n return new PasskeyError(\n \"rp-mismatch\",\n \"The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.\",\n error,\n );\n case \"AbortError\":\n return new PasskeyError(\"aborted\", \"The passkey request was aborted.\", error);\n }\n }\n\n if (error instanceof TypeError) {\n return new PasskeyError(\n \"invalid-options\",\n `The ${ceremony} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,\n error,\n );\n }\n\n return new PasskeyError(\n \"unknown\",\n error instanceof Error\n ? error.message\n : `Unexpected error during the passkey ${ceremony} ceremony.`,\n error,\n );\n}\n\nfunction toDescriptors(\n list: { id: string; type: \"public-key\"; transports?: string[] }[] | undefined,\n): PublicKeyCredentialDescriptor[] | undefined {\n if (!list) return undefined;\n return list.map((item) => ({\n id: base64UrlToBytes(item.id) as unknown as BufferSource,\n type: item.type,\n transports: item.transports as AuthenticatorTransport[] | undefined,\n }));\n}\n\n/**\n * Build a WebAuthn client: the base64url ↔ `ArrayBuffer` plumbing, the two\n * ceremonies, and one classified error type.\n *\n * ## What your backend must do\n *\n * This is the **client half only**, and a WebAuthn client that documents only its\n * own half is unusable. Four routes are yours to implement:\n *\n * 1. `POST /webauthn/register/begin` → a {@link PasskeyCreationOptionsJSON}. Mint a\n * random `challenge` (≥16 bytes), store it against the session, and list the\n * user's existing credentials in `excludeCredentials`.\n * 2. `POST /webauthn/register/finish` ← a {@link PasskeyRegistrationJSON}. Verify\n * the challenge, `origin` and `type` inside `clientDataJSON`, parse the\n * attestation object, then store the credential id, public key and signature\n * counter.\n * 3. `POST /webauthn/signin/begin` → a {@link PasskeyRequestOptionsJSON}. New\n * challenge. Omit `allowCredentials` for a usernameless or autofill flow.\n * 4. `POST /webauthn/signin/finish` ← a {@link PasskeyAuthenticationJSON}. Look the\n * credential up by `id`, verify the signature over\n * `authenticatorData || sha256(clientDataJSON)`, and reject a signature counter\n * that did not grow (a clone). Only then issue your session token.\n *\n * @param options - Defaults applied when the server options omit them.\n * @returns A client usable from anywhere — React, a plain form, a worker.\n *\n * @example\n * const passkeys = createPasskeyClient({ rpId: \"acme.com\" });\n *\n * const options = await api.post(\"/webauthn/register/begin\");\n * const credential = await passkeys.register(options);\n * await api.post(\"/webauthn/register/finish\", { body: credential });\n */\nexport function createPasskeyClient(options: CreatePasskeyClientOptions = {}): PasskeyClient {\n const { rpId, timeoutMs = 60_000, credentials } = options;\n\n function container(): CredentialsContainerLike {\n if (credentials) return credentials;\n if (!isPasskeySupported()) {\n throw new PasskeyError(\n \"unsupported\",\n typeof window !== \"undefined\" && !window.isSecureContext\n ? \"Passkeys require a secure (HTTPS) connection.\"\n : \"This browser does not support passkeys (WebAuthn).\",\n );\n }\n return navigator.credentials as unknown as CredentialsContainerLike;\n }\n\n async function register(\n json: PasskeyCreationOptionsJSON,\n init: PasskeyRegisterInit = {},\n ): Promise<PasskeyRegistrationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialCreationOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rp: { name: json.rp.name, id: json.rp.id ?? rpId },\n user: {\n id: base64UrlToBytes(json.user.id) as unknown as BufferSource,\n name: json.user.name,\n displayName: json.user.displayName,\n },\n pubKeyCredParams: json.pubKeyCredParams ?? DEFAULT_PUB_KEY_CRED_PARAMS,\n timeout: json.timeout ?? timeoutMs,\n excludeCredentials: toDescriptors(json.excludeCredentials),\n authenticatorSelection: json.authenticatorSelection,\n attestation: json.attestation,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.create({ publicKey, signal: init.signal });\n } catch (error) {\n throw classifyPasskeyError(error, \"register\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAttestationResponse & AttestationExtras;\n const publicKeyBytes = response.getPublicKey?.();\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n attestationObject: bytesToBase64Url(response.attestationObject),\n transports: response.getTransports?.(),\n publicKeyAlgorithm: response.getPublicKeyAlgorithm?.(),\n publicKey: publicKeyBytes ? bytesToBase64Url(publicKeyBytes) : undefined,\n authenticatorData: response.getAuthenticatorData\n ? bytesToBase64Url(response.getAuthenticatorData())\n : undefined,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n async function authenticate(\n json: PasskeyRequestOptionsJSON,\n init: PasskeyAuthenticateInit = {},\n ): Promise<PasskeyAuthenticationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialRequestOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rpId: json.rpId ?? rpId,\n timeout: json.timeout ?? timeoutMs,\n allowCredentials: toDescriptors(json.allowCredentials),\n userVerification: json.userVerification,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.get({\n publicKey,\n signal: init.signal,\n mediation: init.mediation,\n });\n } catch (error) {\n throw classifyPasskeyError(error, \"authenticate\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAssertionResponse;\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n authenticatorData: bytesToBase64Url(response.authenticatorData),\n signature: bytesToBase64Url(response.signature),\n userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n return {\n register,\n authenticate,\n isSupported: () => credentials !== undefined || isPasskeySupported(),\n isPlatformAuthenticatorAvailable,\n isConditionalMediationAvailable,\n };\n}\n"],"mappings":"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"}
|