feat-dev: add U2F iframe for desktop client authentication (#2236)
This commit is contained in:
Binary file not shown.
BIN
.yarn/cache/@simplewebauthn-browser-npm-7.1.0-356ca6f81a-925e266075.zip
vendored
Normal file
BIN
.yarn/cache/@simplewebauthn-browser-npm-7.1.0-356ca6f81a-925e266075.zip
vendored
Normal file
Binary file not shown.
@@ -79,7 +79,6 @@ import {
|
|||||||
DecryptedItemInterface,
|
DecryptedItemInterface,
|
||||||
EncryptedItemInterface,
|
EncryptedItemInterface,
|
||||||
Environment,
|
Environment,
|
||||||
HistoryEntry,
|
|
||||||
ItemStream,
|
ItemStream,
|
||||||
Platform,
|
Platform,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
@@ -90,7 +89,7 @@ import { SNLog } from '../Log'
|
|||||||
import { ChallengeResponse, ListedClientInterface } from '../Services'
|
import { ChallengeResponse, ListedClientInterface } from '../Services'
|
||||||
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
||||||
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
||||||
import { LegacySession, MapperInterface, Session, UseCaseInterface } from '@standardnotes/domain-core'
|
import { LegacySession, MapperInterface, Session } from '@standardnotes/domain-core'
|
||||||
import { SessionStorageMapper } from '@Lib/Services/Mapping/SessionStorageMapper'
|
import { SessionStorageMapper } from '@Lib/Services/Mapping/SessionStorageMapper'
|
||||||
import { LegacySessionStorageMapper } from '@Lib/Services/Mapping/LegacySessionStorageMapper'
|
import { LegacySessionStorageMapper } from '@Lib/Services/Mapping/LegacySessionStorageMapper'
|
||||||
import { SignInWithRecoveryCodes } from '@Lib/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
|
import { SignInWithRecoveryCodes } from '@Lib/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
|
||||||
@@ -102,7 +101,6 @@ import { DeleteAuthenticator } from '@Lib/Domain/UseCase/DeleteAuthenticator/Del
|
|||||||
import { ListRevisions } from '@Lib/Domain/UseCase/ListRevisions/ListRevisions'
|
import { ListRevisions } from '@Lib/Domain/UseCase/ListRevisions/ListRevisions'
|
||||||
import { GetRevision } from '@Lib/Domain/UseCase/GetRevision/GetRevision'
|
import { GetRevision } from '@Lib/Domain/UseCase/GetRevision/GetRevision'
|
||||||
import { DeleteRevision } from '@Lib/Domain/UseCase/DeleteRevision/DeleteRevision'
|
import { DeleteRevision } from '@Lib/Domain/UseCase/DeleteRevision/DeleteRevision'
|
||||||
import { RevisionMetadata } from '@Lib/Domain/Revision/RevisionMetadata'
|
|
||||||
import { GetAuthenticatorAuthenticationResponse } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse'
|
import { GetAuthenticatorAuthenticationResponse } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse'
|
||||||
|
|
||||||
/** How often to automatically sync, in milliseconds */
|
/** How often to automatically sync, in milliseconds */
|
||||||
@@ -266,39 +264,39 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
return this.subscriptionManager
|
return this.subscriptionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
get signInWithRecoveryCodes(): UseCaseInterface<void> {
|
get signInWithRecoveryCodes(): SignInWithRecoveryCodes {
|
||||||
return this._signInWithRecoveryCodes
|
return this._signInWithRecoveryCodes
|
||||||
}
|
}
|
||||||
|
|
||||||
get getRecoveryCodes(): UseCaseInterface<string> {
|
get getRecoveryCodes(): GetRecoveryCodes {
|
||||||
return this._getRecoveryCodes
|
return this._getRecoveryCodes
|
||||||
}
|
}
|
||||||
|
|
||||||
get addAuthenticator(): UseCaseInterface<void> {
|
get addAuthenticator(): AddAuthenticator {
|
||||||
return this._addAuthenticator
|
return this._addAuthenticator
|
||||||
}
|
}
|
||||||
|
|
||||||
get listAuthenticators(): UseCaseInterface<Array<{ id: string; name: string }>> {
|
get listAuthenticators(): ListAuthenticators {
|
||||||
return this._listAuthenticators
|
return this._listAuthenticators
|
||||||
}
|
}
|
||||||
|
|
||||||
get deleteAuthenticator(): UseCaseInterface<void> {
|
get deleteAuthenticator(): DeleteAuthenticator {
|
||||||
return this._deleteAuthenticator
|
return this._deleteAuthenticator
|
||||||
}
|
}
|
||||||
|
|
||||||
get getAuthenticatorAuthenticationResponse(): UseCaseInterface<Record<string, unknown>> {
|
get getAuthenticatorAuthenticationResponse(): GetAuthenticatorAuthenticationResponse {
|
||||||
return this._getAuthenticatorAuthenticationResponse
|
return this._getAuthenticatorAuthenticationResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
get listRevisions(): UseCaseInterface<Array<RevisionMetadata>> {
|
get listRevisions(): ListRevisions {
|
||||||
return this._listRevisions
|
return this._listRevisions
|
||||||
}
|
}
|
||||||
|
|
||||||
get getRevision(): UseCaseInterface<HistoryEntry> {
|
get getRevision(): GetRevision {
|
||||||
return this._getRevision
|
return this._getRevision
|
||||||
}
|
}
|
||||||
|
|
||||||
get deleteRevision(): UseCaseInterface<void> {
|
get deleteRevision(): DeleteRevision {
|
||||||
return this._deleteRevision
|
return this._deleteRevision
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import { HistoryEntry } from '@standardnotes/models'
|
import { AddAuthenticator } from './AddAuthenticator/AddAuthenticator'
|
||||||
import { UseCaseInterface } from '@standardnotes/domain-core'
|
import { GetRecoveryCodes } from './GetRecoveryCodes/GetRecoveryCodes'
|
||||||
|
import { SignInWithRecoveryCodes } from './SignInWithRecoveryCodes/SignInWithRecoveryCodes'
|
||||||
import { RevisionMetadata } from '../Revision/RevisionMetadata'
|
import { ListAuthenticators } from './ListAuthenticators/ListAuthenticators'
|
||||||
|
import { DeleteAuthenticator } from './DeleteAuthenticator/DeleteAuthenticator'
|
||||||
|
import { GetAuthenticatorAuthenticationResponse } from './GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse'
|
||||||
|
import { ListRevisions } from './ListRevisions/ListRevisions'
|
||||||
|
import { GetRevision } from './GetRevision/GetRevision'
|
||||||
|
import { DeleteRevision } from './DeleteRevision/DeleteRevision'
|
||||||
|
|
||||||
export interface UseCaseContainerInterface {
|
export interface UseCaseContainerInterface {
|
||||||
get signInWithRecoveryCodes(): UseCaseInterface<void>
|
get signInWithRecoveryCodes(): SignInWithRecoveryCodes
|
||||||
get getRecoveryCodes(): UseCaseInterface<string>
|
get getRecoveryCodes(): GetRecoveryCodes
|
||||||
get addAuthenticator(): UseCaseInterface<void>
|
get addAuthenticator(): AddAuthenticator
|
||||||
get listAuthenticators(): UseCaseInterface<Array<{ id: string; name: string }>>
|
get listAuthenticators(): ListAuthenticators
|
||||||
get deleteAuthenticator(): UseCaseInterface<void>
|
get deleteAuthenticator(): DeleteAuthenticator
|
||||||
get getAuthenticatorAuthenticationResponse(): UseCaseInterface<Record<string, unknown>>
|
get getAuthenticatorAuthenticationResponse(): GetAuthenticatorAuthenticationResponse
|
||||||
get listRevisions(): UseCaseInterface<Array<RevisionMetadata>>
|
get listRevisions(): ListRevisions
|
||||||
get getRevision(): UseCaseInterface<HistoryEntry>
|
get getRevision(): GetRevision
|
||||||
get deleteRevision(): UseCaseInterface<void>
|
get deleteRevision(): DeleteRevision
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,18 @@
|
|||||||
export * from './Revision/Revision'
|
export * from './Revision/Revision'
|
||||||
export * from './Revision/RevisionMetadata'
|
export * from './Revision/RevisionMetadata'
|
||||||
|
export * from './UseCase/AddAuthenticator/AddAuthenticator'
|
||||||
|
export * from './UseCase/AddAuthenticator/AddAuthenticatorDTO'
|
||||||
|
export * from './UseCase/DeleteAuthenticator/DeleteAuthenticator'
|
||||||
|
export * from './UseCase/DeleteAuthenticator/DeleteAuthenticatorDTO'
|
||||||
|
export * from './UseCase/DeleteRevision/DeleteRevision'
|
||||||
|
export * from './UseCase/DeleteRevision/DeleteRevisionDTO'
|
||||||
|
export * from './UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse'
|
||||||
|
export * from './UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponseDTO'
|
||||||
|
export * from './UseCase/GetRecoveryCodes/GetRecoveryCodes'
|
||||||
|
export * from './UseCase/GetRevision/GetRevision'
|
||||||
|
export * from './UseCase/GetRevision/GetRevisionDTO'
|
||||||
|
export * from './UseCase/ListAuthenticators/ListAuthenticators'
|
||||||
|
export * from './UseCase/ListRevisions/ListRevisions'
|
||||||
|
export * from './UseCase/ListRevisions/ListRevisionsDTO'
|
||||||
|
export * from './UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
|
||||||
|
export * from './UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodesDTO'
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const ValidAppViewRoutes = ['u2f'] as const
|
||||||
|
|
||||||
|
export type AppViewRouteParam = typeof ValidAppViewRoutes[number]
|
||||||
@@ -4,4 +4,5 @@ export enum RootQueryParam {
|
|||||||
DemoToken = 'demo-token',
|
DemoToken = 'demo-token',
|
||||||
AcceptSubscriptionInvite = 'accept-subscription-invite',
|
AcceptSubscriptionInvite = 'accept-subscription-invite',
|
||||||
UserRequest = 'user-request',
|
UserRequest = 'user-request',
|
||||||
|
AppViewRoute = 'route',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { UserRequestType } from '@standardnotes/common'
|
import { UserRequestType } from '@standardnotes/common'
|
||||||
import { PreferenceId } from './../Preferences/PreferenceId'
|
import { PreferenceId } from './../Preferences/PreferenceId'
|
||||||
|
import { AppViewRouteParam, ValidAppViewRoutes } from './Params/AppViewRouteParams'
|
||||||
import { DemoParams } from './Params/DemoParams'
|
import { DemoParams } from './Params/DemoParams'
|
||||||
import { OnboardingParams } from './Params/OnboardingParams'
|
import { OnboardingParams } from './Params/OnboardingParams'
|
||||||
import { PurchaseParams } from './Params/PurchaseParams'
|
import { PurchaseParams } from './Params/PurchaseParams'
|
||||||
@@ -78,6 +79,18 @@ export class RouteParser implements RouteParserInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get appViewRouteParam(): AppViewRouteParam | undefined {
|
||||||
|
this.checkForProperRouteType(RouteType.AppViewRoute)
|
||||||
|
|
||||||
|
const appViewRoute = this.searchParams.get(RootQueryParam.AppViewRoute) as AppViewRouteParam
|
||||||
|
|
||||||
|
if (!ValidAppViewRoutes.includes(appViewRoute)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.searchParams.get(RootQueryParam.AppViewRoute) as AppViewRouteParam
|
||||||
|
}
|
||||||
|
|
||||||
private checkForProperRouteType(type: RouteType): void {
|
private checkForProperRouteType(type: RouteType): void {
|
||||||
if (this.parsedType !== type) {
|
if (this.parsedType !== type) {
|
||||||
throw new Error('Accessing invalid params')
|
throw new Error('Accessing invalid params')
|
||||||
@@ -99,6 +112,7 @@ export class RouteParser implements RouteParserInterface {
|
|||||||
[RootQueryParam.DemoToken, RouteType.Demo],
|
[RootQueryParam.DemoToken, RouteType.Demo],
|
||||||
[RootQueryParam.AcceptSubscriptionInvite, RouteType.AcceptSubscriptionInvite],
|
[RootQueryParam.AcceptSubscriptionInvite, RouteType.AcceptSubscriptionInvite],
|
||||||
[RootQueryParam.UserRequest, RouteType.UserRequest],
|
[RootQueryParam.UserRequest, RouteType.UserRequest],
|
||||||
|
[RootQueryParam.AppViewRoute, RouteType.AppViewRoute],
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const rootQueryParam of rootQueryParametersMap.keys()) {
|
for (const rootQueryParam of rootQueryParametersMap.keys()) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AppViewRouteParam } from './Params/AppViewRouteParams'
|
||||||
import { DemoParams } from './Params/DemoParams'
|
import { DemoParams } from './Params/DemoParams'
|
||||||
import { OnboardingParams } from './Params/OnboardingParams'
|
import { OnboardingParams } from './Params/OnboardingParams'
|
||||||
import { PurchaseParams } from './Params/PurchaseParams'
|
import { PurchaseParams } from './Params/PurchaseParams'
|
||||||
@@ -13,5 +14,6 @@ export interface RouteParserInterface {
|
|||||||
get onboardingParams(): OnboardingParams
|
get onboardingParams(): OnboardingParams
|
||||||
get subscriptionInviteParams(): SubscriptionInviteParams
|
get subscriptionInviteParams(): SubscriptionInviteParams
|
||||||
get userRequestParams(): UserRequestParams
|
get userRequestParams(): UserRequestParams
|
||||||
|
get appViewRouteParam(): AppViewRouteParam | undefined
|
||||||
get type(): RouteType
|
get type(): RouteType
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ export enum RouteType {
|
|||||||
AcceptSubscriptionInvite = 'accept-subscription-invite',
|
AcceptSubscriptionInvite = 'accept-subscription-invite',
|
||||||
UserRequest = 'user-request',
|
UserRequest = 'user-request',
|
||||||
Demo = 'demo',
|
Demo = 'demo',
|
||||||
|
AppViewRoute = 'route',
|
||||||
None = 'none',
|
None = 'none',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"@reach/listbox": "^0.18.0",
|
"@reach/listbox": "^0.18.0",
|
||||||
"@reach/tooltip": "^0.18.0",
|
"@reach/tooltip": "^0.18.0",
|
||||||
"@reach/visually-hidden": "^0.18.0",
|
"@reach/visually-hidden": "^0.18.0",
|
||||||
|
"@simplewebauthn/browser": "^7.1.0",
|
||||||
"@standardnotes/authenticator": "^2.3.9",
|
"@standardnotes/authenticator": "^2.3.9",
|
||||||
"@standardnotes/autobiography-theme": "^1.2.7",
|
"@standardnotes/autobiography-theme": "^1.2.7",
|
||||||
"@standardnotes/bold-editor": "^1.6.4",
|
"@standardnotes/bold-editor": "^1.6.4",
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import { WebApplication } from './Application/Application'
|
|||||||
import { createRoot, Root } from 'react-dom/client'
|
import { createRoot, Root } from 'react-dom/client'
|
||||||
import { ElementIds } from './Constants/ElementIDs'
|
import { ElementIds } from './Constants/ElementIDs'
|
||||||
import { setDefaultMonospaceFont } from './setDefaultMonospaceFont'
|
import { setDefaultMonospaceFont } from './setDefaultMonospaceFont'
|
||||||
|
import { RouteParser, RouteType } from '@standardnotes/ui-services'
|
||||||
|
import U2FAuthIframe from './Components/U2FAuthIframe/U2FAuthIframe'
|
||||||
|
|
||||||
let keyCount = 0
|
let keyCount = 0
|
||||||
const getKey = () => {
|
const getKey = () => {
|
||||||
@@ -71,6 +73,13 @@ const startApplication: StartApplication = async function startApplication(
|
|||||||
|
|
||||||
setDefaultMonospaceFont(device.platform)
|
setDefaultMonospaceFont(device.platform)
|
||||||
|
|
||||||
|
const route = new RouteParser(window.location.href)
|
||||||
|
|
||||||
|
if (route.type === RouteType.AppViewRoute && route.appViewRouteParam === 'u2f') {
|
||||||
|
root.render(<U2FAuthIframe />)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<ApplicationGroupView
|
<ApplicationGroupView
|
||||||
key={getKey()}
|
key={getKey()}
|
||||||
|
|||||||
@@ -430,6 +430,14 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
this.getViewControllerManager().accountMenuController.setShow(false)
|
this.getViewControllerManager().accountMenuController.setShow(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full U2F clients are only web browser clients. They support adding and removing keys as well as authentication.
|
||||||
|
* The desktop and mobile clients cannot support adding keys.
|
||||||
|
*/
|
||||||
|
get isFullU2FClient(): boolean {
|
||||||
|
return this.environment === Environment.Web
|
||||||
|
}
|
||||||
|
|
||||||
geDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier {
|
geDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier {
|
||||||
return (
|
return (
|
||||||
currentTag?.preferences?.editorIdentifier ||
|
currentTag?.preferences?.editorIdentifier ||
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { RefObject, useState } from 'react'
|
|||||||
import Button from '../Button/Button'
|
import Button from '../Button/Button'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import { InputValue } from './InputValue'
|
import { InputValue } from './InputValue'
|
||||||
|
import U2FPromptIframeContainer from './U2FPromptIframeContainer'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -17,6 +18,18 @@ const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData
|
|||||||
const [authenticatorResponse, setAuthenticatorResponse] = useState<Record<string, unknown> | null>(null)
|
const [authenticatorResponse, setAuthenticatorResponse] = useState<Record<string, unknown> | null>(null)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
if (!application.isFullU2FClient) {
|
||||||
|
return (
|
||||||
|
<U2FPromptIframeContainer
|
||||||
|
contextData={contextData}
|
||||||
|
apiHost={application.getHost() || window.defaultSyncServer}
|
||||||
|
onResponse={(response) => {
|
||||||
|
onValueChange(response, prompt)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-w-76">
|
<div className="min-w-76">
|
||||||
{error && <div className="text-red-500">{error}</div>}
|
{error && <div className="text-red-500">{error}</div>}
|
||||||
@@ -27,18 +40,18 @@ const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!contextData || contextData.username === undefined) {
|
if (!contextData || contextData.username === undefined) {
|
||||||
setError('No username provided')
|
setError('No username provided')
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticatorResponseOrError = await application.getAuthenticatorAuthenticationResponse.execute({
|
const authenticatorResponseOrError = await application.getAuthenticatorAuthenticationResponse.execute({
|
||||||
username: contextData.username,
|
username: contextData.username as string,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (authenticatorResponseOrError.isFailed()) {
|
if (authenticatorResponseOrError.isFailed()) {
|
||||||
setError(authenticatorResponseOrError.getError())
|
setError(authenticatorResponseOrError.getError())
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticatorResponse = authenticatorResponseOrError.getValue()
|
const authenticatorResponse = authenticatorResponseOrError.getValue()
|
||||||
|
|
||||||
setAuthenticatorResponse(authenticatorResponse)
|
setAuthenticatorResponse(authenticatorResponse)
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { log, LoggingDomain } from '@/Logging'
|
||||||
|
import { isDev } from '@/Utils'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contextData?: Record<string, unknown>
|
||||||
|
onResponse: (response: string) => void
|
||||||
|
apiHost: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const U2F_IFRAME_ORIGIN = isDev ? 'http://localhost:3001/?route=u2f' : 'https://app.standardnotes.com/?route=u2f'
|
||||||
|
|
||||||
|
const U2FPromptIframeContainer = ({ contextData, onResponse, apiHost }: Props) => {
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
log(LoggingDomain.U2F, 'Native client received message', event)
|
||||||
|
const eventDoesNotComeFromU2FIFrame = event.origin !== new URL(U2F_IFRAME_ORIGIN).origin
|
||||||
|
if (eventDoesNotComeFromU2FIFrame) {
|
||||||
|
log(
|
||||||
|
LoggingDomain.U2F,
|
||||||
|
'Not sending data to U2F iframe; origin does not match',
|
||||||
|
event.origin,
|
||||||
|
new URL(U2F_IFRAME_ORIGIN).origin,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.mountedAuthView) {
|
||||||
|
if (iframeRef.current?.contentWindow) {
|
||||||
|
log(LoggingDomain.U2F, 'Sending contextData to U2F iframe', contextData)
|
||||||
|
iframeRef.current.contentWindow.postMessage(
|
||||||
|
{ username: (contextData as Record<string, unknown>).username, apiHost },
|
||||||
|
U2F_IFRAME_ORIGIN,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.assertionResponse) {
|
||||||
|
log(LoggingDomain.U2F, 'Received assertion response from U2F iframe', event.data.assertionResponse)
|
||||||
|
onResponse(event.data.assertionResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', messageHandler)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', messageHandler)
|
||||||
|
}
|
||||||
|
}, [contextData, onResponse, apiHost])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
src={U2F_IFRAME_ORIGIN}
|
||||||
|
className="h-40 w-full"
|
||||||
|
title="U2F"
|
||||||
|
allow="publickey-credentials-get"
|
||||||
|
id="u2f"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default U2FPromptIframeContainer
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
import { forwardRef, Fragment, Ref } from 'react'
|
import { forwardRef, Fragment, KeyboardEventHandler, Ref, useCallback } from 'react'
|
||||||
import { DecoratedInputProps } from './DecoratedInputProps'
|
import { DecoratedInputProps } from './DecoratedInputProps'
|
||||||
|
|
||||||
const getClassNames = (hasLeftDecorations: boolean, hasRightDecorations: boolean, roundedFull?: boolean) => {
|
const getClassNames = (hasLeftDecorations: boolean, hasRightDecorations: boolean, roundedFull?: boolean) => {
|
||||||
@@ -31,6 +31,7 @@ const DecoratedInput = forwardRef(
|
|||||||
onFocus,
|
onFocus,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onKeyUp,
|
onKeyUp,
|
||||||
|
onEnter,
|
||||||
placeholder = '',
|
placeholder = '',
|
||||||
right,
|
right,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
@@ -38,6 +39,7 @@ const DecoratedInput = forwardRef(
|
|||||||
value,
|
value,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
roundedFull,
|
roundedFull,
|
||||||
|
autofocus = false,
|
||||||
}: DecoratedInputProps,
|
}: DecoratedInputProps,
|
||||||
ref: Ref<HTMLInputElement>,
|
ref: Ref<HTMLInputElement>,
|
||||||
) => {
|
) => {
|
||||||
@@ -45,6 +47,16 @@ const DecoratedInput = forwardRef(
|
|||||||
const hasRightDecorations = Boolean(right?.length)
|
const hasRightDecorations = Boolean(right?.length)
|
||||||
const computedClassNames = getClassNames(hasLeftDecorations, hasRightDecorations, roundedFull)
|
const computedClassNames = getClassNames(hasLeftDecorations, hasRightDecorations, roundedFull)
|
||||||
|
|
||||||
|
const handleKeyUp: KeyboardEventHandler = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onEnter?.()
|
||||||
|
}
|
||||||
|
onKeyUp?.(e)
|
||||||
|
},
|
||||||
|
[onKeyUp, onEnter],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -63,6 +75,7 @@ const DecoratedInput = forwardRef(
|
|||||||
|
|
||||||
<input
|
<input
|
||||||
autoComplete={autocomplete ? 'on' : 'off'}
|
autoComplete={autocomplete ? 'on' : 'off'}
|
||||||
|
autoFocus={autofocus}
|
||||||
className={`${computedClassNames.input} ${disabled ? computedClassNames.disabled : ''} ${className?.input}`}
|
className={`${computedClassNames.input} ${disabled ? computedClassNames.disabled : ''} ${className?.input}`}
|
||||||
data-lpignore={type !== 'password' ? true : false}
|
data-lpignore={type !== 'password' ? true : false}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -71,7 +84,7 @@ const DecoratedInput = forwardRef(
|
|||||||
onChange={(e) => onChange && onChange((e.target as HTMLInputElement).value)}
|
onChange={(e) => onChange && onChange((e.target as HTMLInputElement).value)}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
title={title}
|
title={title}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { FocusEventHandler, KeyboardEventHandler, ReactNode } from 'react'
|
|||||||
|
|
||||||
export type DecoratedInputProps = {
|
export type DecoratedInputProps = {
|
||||||
autocomplete?: boolean
|
autocomplete?: boolean
|
||||||
|
autofocus?: boolean
|
||||||
spellcheck?: boolean
|
spellcheck?: boolean
|
||||||
className?: {
|
className?: {
|
||||||
container?: string
|
container?: string
|
||||||
@@ -17,6 +18,7 @@ export type DecoratedInputProps = {
|
|||||||
onFocus?: FocusEventHandler
|
onFocus?: FocusEventHandler
|
||||||
onKeyDown?: KeyboardEventHandler
|
onKeyDown?: KeyboardEventHandler
|
||||||
onKeyUp?: KeyboardEventHandler
|
onKeyUp?: KeyboardEventHandler
|
||||||
|
onEnter?: () => void
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
right?: ReactNode[]
|
right?: ReactNode[]
|
||||||
title?: string
|
title?: string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FunctionComponent, useCallback, useState } from 'react'
|
import { FunctionComponent, useCallback, useState } from 'react'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { UseCaseInterface } from '@standardnotes/snjs'
|
import { AddAuthenticator } from '@standardnotes/snjs'
|
||||||
|
|
||||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||||
import { UserProvider } from '@/Components/Preferences/Providers'
|
import { UserProvider } from '@/Components/Preferences/Providers'
|
||||||
@@ -9,7 +9,7 @@ import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/u
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
userProvider: UserProvider
|
userProvider: UserProvider
|
||||||
addAuthenticator: UseCaseInterface<void>
|
addAuthenticator: AddAuthenticator
|
||||||
onDeviceAddingModalToggle: (show: boolean) => void
|
onDeviceAddingModalToggle: (show: boolean) => void
|
||||||
onDeviceAdded: () => Promise<void>
|
onDeviceAdded: () => Promise<void>
|
||||||
}
|
}
|
||||||
@@ -82,11 +82,22 @@ const U2FAddDeviceView: FunctionComponent<Props> = ({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div className="w-25 h-25 flex items-center justify-center bg-info">...Some Cool Device Picture Here...</div>
|
<div className="flex px-4 py-4">
|
||||||
<div className="flex flex-grow flex-col gap-2">
|
<div className="ml-4 flex flex-grow flex-col gap-1">
|
||||||
<DecoratedInput className={{ container: 'w-92 ml-4' }} value={deviceName} onChange={handleDeviceNameChange} />
|
<label htmlFor="u2f-device-name" className="mb-2 text-sm font-semibold">
|
||||||
|
Device Name
|
||||||
|
</label>
|
||||||
|
<DecoratedInput
|
||||||
|
autofocus
|
||||||
|
id="u2f-device-name"
|
||||||
|
className={{ container: 'w-92' }}
|
||||||
|
value={deviceName}
|
||||||
|
onChange={handleDeviceNameChange}
|
||||||
|
onEnter={handleAddDeviceClick}
|
||||||
|
/>
|
||||||
|
{errorMessage && <div className="mt-1.5 text-danger">{errorMessage}</div>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{errorMessage && <div className="text-error">{errorMessage}</div>}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,27 @@ import { observer } from 'mobx-react-lite'
|
|||||||
|
|
||||||
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
import { UserProvider } from '@/Components/Preferences/Providers'
|
import { UserProvider } from '@/Components/Preferences/Providers'
|
||||||
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
userProvider: UserProvider
|
userProvider: UserProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
const U2FDescription: FunctionComponent<Props> = ({ userProvider }) => {
|
const U2FDescription: FunctionComponent<Props> = ({ userProvider }) => {
|
||||||
|
const application = useApplication()
|
||||||
|
|
||||||
if (userProvider.getUser() === undefined) {
|
if (userProvider.getUser() === undefined) {
|
||||||
return <Text>Sign in or register for an account to configure U2F.</Text>
|
return <Text>Sign in or register for an account to configure U2F.</Text>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Text>Authenticate with a U2F hardware device.</Text>
|
return (
|
||||||
|
<div>
|
||||||
|
<Text>Authenticate with a U2F hardware device such as Yubikey.</Text>
|
||||||
|
{!application.isFullU2FClient && (
|
||||||
|
<Text className="italic">Please visit the web app in order to add a U2F Device.</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(U2FDescription)
|
export default observer(U2FDescription)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { FunctionComponent, useCallback } from 'react'
|
import { FunctionComponent, useCallback } from 'react'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
|
|
||||||
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
|
import Icon from '@/Components/Icon/Icon'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -31,24 +32,25 @@ const U2FDevicesList: FunctionComponent<Props> = ({ application, devices, onErro
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center">
|
<div>
|
||||||
{devices.length > 0 && (
|
{devices.length > 0 && (
|
||||||
<div className="flex flex-grow flex-col">
|
<>
|
||||||
<div>
|
<Subtitle>Devices</Subtitle>
|
||||||
<Text>Devices:</Text>
|
<div className="flex flex-grow flex-col divide-y divide-border">
|
||||||
|
{devices.map((device) => (
|
||||||
|
<div className="flex items-center py-2" key={`device-${device.id}`}>
|
||||||
|
<Icon type="security" />
|
||||||
|
<div className="ml-2 mr-auto text-sm">{device.name}</div>
|
||||||
|
<Button
|
||||||
|
small
|
||||||
|
key={device.id}
|
||||||
|
label="Delete"
|
||||||
|
onClick={async () => handleDeleteButtonOnClick(device.id)}
|
||||||
|
></Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{devices.map((device) => (
|
</>
|
||||||
<div key="device-{device.id}">
|
|
||||||
<Text>{device.name}</Text>
|
|
||||||
<Button
|
|
||||||
key={device.id}
|
|
||||||
primary={true}
|
|
||||||
label="Delete"
|
|
||||||
onClick={async () => handleDeleteButtonOnClick(device.id)}
|
|
||||||
></Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ type Props = {
|
|||||||
|
|
||||||
const U2FTitle: FunctionComponent<Props> = ({ userProvider }) => {
|
const U2FTitle: FunctionComponent<Props> = ({ userProvider }) => {
|
||||||
if (userProvider.getUser() === undefined) {
|
if (userProvider.getUser() === undefined) {
|
||||||
return <Title>Universal 2nd Factor authentication not available</Title>
|
return <Title>Universal 2nd factor authentication not available</Title>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Title>Universal 2nd Factor authentication</Title>
|
return <Title>Universal 2nd Factor Authentication</Title>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(U2FTitle)
|
export default observer(U2FTitle)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import U2FDescription from './U2FDescription'
|
|||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import U2FAddDeviceView from '../U2FAddDeviceView'
|
import U2FAddDeviceView from '../U2FAddDeviceView'
|
||||||
import U2FDevicesList from './U2FDevicesList'
|
import U2FDevicesList from './U2FDevicesList'
|
||||||
|
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -45,34 +46,36 @@ const U2FView: FunctionComponent<Props> = ({ application, userProvider }) => {
|
|||||||
<>
|
<>
|
||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-grow flex-col">
|
<U2FTitle userProvider={userProvider} />
|
||||||
<U2FTitle userProvider={userProvider} />
|
<U2FDescription userProvider={userProvider} />
|
||||||
<U2FDescription userProvider={userProvider} />
|
|
||||||
</div>
|
|
||||||
<PreferencesSegment>
|
|
||||||
<Button label="Add Device" primary onClick={handleAddDeviceClick} />
|
|
||||||
</PreferencesSegment>
|
|
||||||
</div>
|
</div>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment classes="mt-2">
|
||||||
{error && <div className="text-red-500">{error}</div>}
|
{error && <div className="text-danger">{error}</div>}
|
||||||
<U2FDevicesList
|
<U2FDevicesList
|
||||||
application={application}
|
application={application}
|
||||||
devices={devices}
|
devices={devices}
|
||||||
onError={setError}
|
onError={setError}
|
||||||
onDeviceDeleted={loadAuthenticatorDevices}
|
onDeviceDeleted={loadAuthenticatorDevices}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
className="mt-1"
|
||||||
|
disabled={!application.isFullU2FClient}
|
||||||
|
label="Add Device"
|
||||||
|
primary
|
||||||
|
onClick={handleAddDeviceClick}
|
||||||
|
/>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
{showDeviceAddingModal && (
|
<ModalOverlay isOpen={showDeviceAddingModal}>
|
||||||
<U2FAddDeviceView
|
<U2FAddDeviceView
|
||||||
onDeviceAddingModalToggle={setShowDeviceAddingModal}
|
onDeviceAddingModalToggle={setShowDeviceAddingModal}
|
||||||
onDeviceAdded={loadAuthenticatorDevices}
|
onDeviceAdded={loadAuthenticatorDevices}
|
||||||
userProvider={userProvider}
|
userProvider={userProvider}
|
||||||
addAuthenticator={application.addAuthenticator}
|
addAuthenticator={application.addAuthenticator}
|
||||||
/>
|
/>
|
||||||
)}
|
</ModalOverlay>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import Button from '../Button/Button'
|
||||||
|
import { startAuthentication } from '@simplewebauthn/browser'
|
||||||
|
import { log, LoggingDomain } from '@/Logging'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An iframe for use in the desktop and mobile application that allows them to load app.standardnotes.com to perform
|
||||||
|
* U2F authentication. Web applications do not need this iframe, as they can perform U2F authentication directly.
|
||||||
|
*/
|
||||||
|
const U2FAuthIframe = () => {
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [apiHost, setApiHost] = useState<string | null>(null)
|
||||||
|
const [source, setSource] = useState<MessageEvent['source'] | null>(null)
|
||||||
|
const NATIVE_CLIENT_ORIGIN = 'file://'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
mountedAuthView: true,
|
||||||
|
},
|
||||||
|
NATIVE_CLIENT_ORIGIN,
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
log(LoggingDomain.U2F, 'U2F iframe received message', event)
|
||||||
|
|
||||||
|
const eventDoesNotComeFromNativeClient = event.origin !== NATIVE_CLIENT_ORIGIN
|
||||||
|
if (eventDoesNotComeFromNativeClient) {
|
||||||
|
log(LoggingDomain.U2F, 'Not setting username; origin does not match', event.origin, NATIVE_CLIENT_ORIGIN)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.username) {
|
||||||
|
setUsername(event.data.username)
|
||||||
|
setApiHost(event.data.apiHost)
|
||||||
|
setSource(event.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', messageHandler)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', messageHandler)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [info, setInfo] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const beginAuthentication = useCallback(async () => {
|
||||||
|
setInfo('')
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!username || !source) {
|
||||||
|
throw new Error('No username provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${apiHost}/v1/authenticators/generate-authentication-options`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const jsonResponse = await response.json()
|
||||||
|
if (!jsonResponse.data || !jsonResponse.data.options) {
|
||||||
|
throw new Error('No options returned from server')
|
||||||
|
}
|
||||||
|
|
||||||
|
setInfo('Waiting for U2F device...')
|
||||||
|
|
||||||
|
const assertionResponse = await startAuthentication(jsonResponse.data.options)
|
||||||
|
|
||||||
|
;(source as WindowProxy).postMessage(
|
||||||
|
{
|
||||||
|
assertionResponse,
|
||||||
|
},
|
||||||
|
NATIVE_CLIENT_ORIGIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
setInfo('Authentication successful!')
|
||||||
|
} catch (error) {
|
||||||
|
if (!error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setError(error.toString())
|
||||||
|
console.error(error.toString())
|
||||||
|
}
|
||||||
|
}, [source, username, apiHost])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
|
||||||
|
<div className="mb-2 text-center">Insert your U2F device, then press the button below to authenticate.</div>
|
||||||
|
<Button onClick={beginAuthentication}>Authenticate</Button>
|
||||||
|
<div className="mt-2">
|
||||||
|
<div>{info}</div>
|
||||||
|
<div className="text-danger">{error}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default U2FAuthIframe
|
||||||
@@ -11,6 +11,7 @@ export enum LoggingDomain {
|
|||||||
BlockEditor,
|
BlockEditor,
|
||||||
Purchasing,
|
Purchasing,
|
||||||
Panes,
|
Panes,
|
||||||
|
U2F,
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoggingStatus: Record<LoggingDomain, boolean> = {
|
const LoggingStatus: Record<LoggingDomain, boolean> = {
|
||||||
@@ -23,6 +24,7 @@ const LoggingStatus: Record<LoggingDomain, boolean> = {
|
|||||||
[LoggingDomain.BlockEditor]: false,
|
[LoggingDomain.BlockEditor]: false,
|
||||||
[LoggingDomain.Purchasing]: false,
|
[LoggingDomain.Purchasing]: false,
|
||||||
[LoggingDomain.Panes]: false,
|
[LoggingDomain.Panes]: false,
|
||||||
|
[LoggingDomain.U2F]: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -4622,10 +4622,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@simplewebauthn/browser@npm:^7.0.0":
|
"@simplewebauthn/browser@npm:^7.1.0":
|
||||||
version: 7.0.0
|
version: 7.1.0
|
||||||
resolution: "@simplewebauthn/browser@npm:7.0.0"
|
resolution: "@simplewebauthn/browser@npm:7.1.0"
|
||||||
checksum: eb8d7e2d923649c116275cc9bfbabfa27a180cd33dbf9d6a28c7aa9460ea79cd25204c9a7d76ed8cc24764da4a09b5939209aa30e9b295b9d54e497bb9b652a4
|
checksum: 925e26607536cd9223e474229df216cc32bc06fac569f27e03c6d31ff502f3dbf6f5a9500caaa37a9abf1632fc99fd582a6596672b7f5468920a74618e1aec6e
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -5582,7 +5582,7 @@ __metadata:
|
|||||||
"@reach/listbox": ^0.18.0
|
"@reach/listbox": ^0.18.0
|
||||||
"@reach/tooltip": ^0.18.0
|
"@reach/tooltip": ^0.18.0
|
||||||
"@reach/visually-hidden": ^0.18.0
|
"@reach/visually-hidden": ^0.18.0
|
||||||
"@simplewebauthn/browser": ^7.0.0
|
"@simplewebauthn/browser": ^7.1.0
|
||||||
"@standardnotes/authenticator": ^2.3.9
|
"@standardnotes/authenticator": ^2.3.9
|
||||||
"@standardnotes/autobiography-theme": ^1.2.7
|
"@standardnotes/autobiography-theme": ^1.2.7
|
||||||
"@standardnotes/bold-editor": ^1.6.4
|
"@standardnotes/bold-editor": ^1.6.4
|
||||||
|
|||||||
Reference in New Issue
Block a user