@meith/accounts 0.16.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/LICENSE.md +165 -0
- package/package.json +25 -0
- package/src/ban-filter.ts +127 -0
- package/src/ban-service.ts +71 -0
- package/src/case-fold.ts +3 -0
- package/src/credential-proof.ts +17 -0
- package/src/crypto/base64url.ts +53 -0
- package/src/crypto/legacy.ts +127 -0
- package/src/crypto/password.ts +92 -0
- package/src/crypto/tokens.ts +28 -0
- package/src/federation/catalog.ts +115 -0
- package/src/federation/github.ts +117 -0
- package/src/federation/http.ts +48 -0
- package/src/federation/jwt.ts +198 -0
- package/src/federation/oidc.ts +175 -0
- package/src/federation/pkce.ts +20 -0
- package/src/federation/service.ts +198 -0
- package/src/federation/types.ts +50 -0
- package/src/index.ts +223 -0
- package/src/member-settings.ts +390 -0
- package/src/memory-bans.ts +88 -0
- package/src/memory-repos.ts +665 -0
- package/src/policy.ts +104 -0
- package/src/ports.ts +415 -0
- package/src/register-fields.ts +17 -0
- package/src/service.ts +684 -0
- package/src/session-service.ts +134 -0
- package/src/test-support.fixture.ts +8 -0
- package/src/totp/base32.ts +52 -0
- package/src/totp/secret-box.ts +83 -0
- package/src/totp/service.ts +252 -0
- package/src/totp/totp.ts +120 -0
- package/src/webauthn/authenticator.fixture.ts +268 -0
- package/src/webauthn/cbor.ts +110 -0
- package/src/webauthn/cose.ts +130 -0
- package/src/webauthn/service.ts +266 -0
- package/src/webauthn/verify.ts +231 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { ConflictError, ForbiddenError, ValidationError } from '@meith/core'
|
|
2
|
+
import { msg } from '@meith/i18n'
|
|
3
|
+
|
|
4
|
+
import { foldIdentifier } from '../case-fold'
|
|
5
|
+
import type {
|
|
6
|
+
AccountRecord,
|
|
7
|
+
AccountRepository,
|
|
8
|
+
Clock,
|
|
9
|
+
UserIdentityRecord,
|
|
10
|
+
UserIdentityRepository,
|
|
11
|
+
} from '../ports'
|
|
12
|
+
import type { IdentityService, LoginResult, RequestContext } from '../service'
|
|
13
|
+
import type { IdentityProvider, ProviderProfile } from './types'
|
|
14
|
+
|
|
15
|
+
export interface FederationDeps {
|
|
16
|
+
readonly identity: IdentityService
|
|
17
|
+
readonly accounts: AccountRepository
|
|
18
|
+
readonly identities: UserIdentityRepository
|
|
19
|
+
readonly clock?: Clock
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CompleteSignInInput {
|
|
23
|
+
readonly provider: IdentityProvider
|
|
24
|
+
readonly profile: ProviderProfile
|
|
25
|
+
readonly context?: RequestContext
|
|
26
|
+
readonly beforeProvision?: () => Promise<void>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type FederationOutcome =
|
|
30
|
+
| {
|
|
31
|
+
readonly status: 'signed-in'
|
|
32
|
+
readonly account: AccountRecord
|
|
33
|
+
readonly login: LoginResult
|
|
34
|
+
readonly created: boolean
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
readonly status: 'pending'
|
|
38
|
+
readonly account: AccountRecord
|
|
39
|
+
readonly verificationToken: string | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const UNLINK_LAST_CREDENTIAL =
|
|
43
|
+
'That is the only way you have left to sign in. Set a password first, then unlink it.'
|
|
44
|
+
|
|
45
|
+
export class FederationService {
|
|
46
|
+
private readonly identity: IdentityService
|
|
47
|
+
private readonly accounts: AccountRepository
|
|
48
|
+
private readonly identities: UserIdentityRepository
|
|
49
|
+
private readonly now: Clock
|
|
50
|
+
|
|
51
|
+
constructor(deps: FederationDeps) {
|
|
52
|
+
this.identity = deps.identity
|
|
53
|
+
this.accounts = deps.accounts
|
|
54
|
+
this.identities = deps.identities
|
|
55
|
+
this.now = deps.clock ?? (() => new Date())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async completeSignIn(input: CompleteSignInInput): Promise<FederationOutcome> {
|
|
59
|
+
const context = input.context ?? {}
|
|
60
|
+
const linked = await this.identities.findBySubject(input.provider.id, input.profile.subject)
|
|
61
|
+
|
|
62
|
+
if (linked !== null) {
|
|
63
|
+
const account = await this.accounts.findById(linked.userId)
|
|
64
|
+
if (account === null) {
|
|
65
|
+
throw new ForbiddenError(
|
|
66
|
+
`The account behind that ${input.provider.label} sign-in no longer exists.`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const login = await this.identity.startSessionFor(account, context)
|
|
71
|
+
await this.identities.markUsed(linked.id, this.now())
|
|
72
|
+
return { status: 'signed-in', account, login, created: false }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const email = normalisedEmail(input.profile)
|
|
76
|
+
const existing =
|
|
77
|
+
email === null ? null : await this.accounts.findByEmailLower(foldIdentifier(email))
|
|
78
|
+
|
|
79
|
+
if (existing !== null) {
|
|
80
|
+
if (!input.profile.emailVerified) {
|
|
81
|
+
throw new ForbiddenError(
|
|
82
|
+
`An account here already uses that address, and ${input.provider.label} has not ` +
|
|
83
|
+
'confirmed it belongs to you. Sign in with your password and link the two from ' +
|
|
84
|
+
'your account security page.',
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const login = await this.identity.startSessionFor(existing, context)
|
|
89
|
+
await this.link({
|
|
90
|
+
userId: existing.id,
|
|
91
|
+
provider: input.provider,
|
|
92
|
+
profile: input.profile,
|
|
93
|
+
})
|
|
94
|
+
return { status: 'signed-in', account: existing, login, created: false }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (email === null) {
|
|
98
|
+
throw new ValidationError(
|
|
99
|
+
`${input.provider.label} did not share a confirmed e-mail address, so there is ` +
|
|
100
|
+
'nothing to register an account against. Register with a password instead.',
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await input.beforeProvision?.()
|
|
105
|
+
|
|
106
|
+
const provisioned = await this.identity.provisionFederated(
|
|
107
|
+
{
|
|
108
|
+
username: suggestedUsername(input.profile, email),
|
|
109
|
+
email,
|
|
110
|
+
emailVerified: input.profile.emailVerified,
|
|
111
|
+
},
|
|
112
|
+
context,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
await this.link({
|
|
116
|
+
userId: provisioned.account.id,
|
|
117
|
+
provider: input.provider,
|
|
118
|
+
profile: input.profile,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if (provisioned.account.state !== 'active') {
|
|
122
|
+
return {
|
|
123
|
+
status: 'pending',
|
|
124
|
+
account: provisioned.account,
|
|
125
|
+
verificationToken: provisioned.verificationToken ?? null,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
status: 'signed-in',
|
|
131
|
+
account: provisioned.account,
|
|
132
|
+
login: await this.identity.startSessionFor(provisioned.account, context),
|
|
133
|
+
created: true,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async linkToViewer(input: {
|
|
138
|
+
readonly userId: number
|
|
139
|
+
readonly provider: IdentityProvider
|
|
140
|
+
readonly profile: ProviderProfile
|
|
141
|
+
}): Promise<UserIdentityRecord> {
|
|
142
|
+
const linked = await this.identities.findBySubject(input.provider.id, input.profile.subject)
|
|
143
|
+
|
|
144
|
+
if (linked !== null && linked.userId !== input.userId) {
|
|
145
|
+
throw new ConflictError(
|
|
146
|
+
`That ${input.provider.label} account is already linked to another member here.`,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return linked ?? this.link(input)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async unlink(input: {
|
|
154
|
+
readonly userId: number
|
|
155
|
+
readonly identityId: number
|
|
156
|
+
readonly hasPassword: boolean
|
|
157
|
+
readonly usablePasskeys: number
|
|
158
|
+
readonly usableProviders: readonly string[]
|
|
159
|
+
}): Promise<void> {
|
|
160
|
+
const remaining = (await this.identities.listForUser(input.userId)).filter(
|
|
161
|
+
(identity) =>
|
|
162
|
+
identity.id !== input.identityId && input.usableProviders.includes(identity.provider),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (!input.hasPassword && input.usablePasskeys === 0 && remaining.length === 0) {
|
|
166
|
+
throw new ForbiddenError(UNLINK_LAST_CREDENTIAL)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const removed = await this.identities.unlink(input.userId, input.identityId)
|
|
170
|
+
if (!removed) {
|
|
171
|
+
throw new ConflictError(msg('error.accounts.sign-in-already-unlinked'))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async link(input: {
|
|
176
|
+
readonly userId: number
|
|
177
|
+
readonly provider: IdentityProvider
|
|
178
|
+
readonly profile: ProviderProfile
|
|
179
|
+
}): Promise<UserIdentityRecord> {
|
|
180
|
+
return this.identities.link({
|
|
181
|
+
userId: input.userId,
|
|
182
|
+
provider: input.provider.id,
|
|
183
|
+
subject: input.profile.subject,
|
|
184
|
+
label: input.profile.username ?? input.profile.email ?? input.profile.displayName,
|
|
185
|
+
now: this.now(),
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalisedEmail(profile: ProviderProfile): string | null {
|
|
191
|
+
const email = profile.email?.trim() ?? ''
|
|
192
|
+
return email === '' ? null : email
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function suggestedUsername(profile: ProviderProfile, email: string): string {
|
|
196
|
+
const local = email.split('@')[0] ?? ''
|
|
197
|
+
return profile.username ?? profile.displayName ?? local
|
|
198
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type ProviderKind = 'github' | 'google' | 'oidc'
|
|
2
|
+
|
|
3
|
+
export interface ProviderProfile {
|
|
4
|
+
readonly subject: string
|
|
5
|
+
readonly email: string | null
|
|
6
|
+
readonly emailVerified: boolean
|
|
7
|
+
readonly username: string | null
|
|
8
|
+
readonly displayName: string | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AuthorizeInput {
|
|
12
|
+
readonly redirectUri: string
|
|
13
|
+
readonly state: string
|
|
14
|
+
readonly nonce: string
|
|
15
|
+
readonly codeVerifier: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ExchangeInput {
|
|
19
|
+
readonly code: string
|
|
20
|
+
readonly redirectUri: string
|
|
21
|
+
readonly nonce: string
|
|
22
|
+
readonly codeVerifier: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface IdentityProvider {
|
|
26
|
+
readonly id: ProviderKind
|
|
27
|
+
readonly label: string
|
|
28
|
+
authorizationUrl(input: AuthorizeInput): Promise<string>
|
|
29
|
+
exchange(input: ExchangeInput): Promise<ProviderProfile>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Fetcher = typeof fetch
|
|
33
|
+
|
|
34
|
+
export interface ProviderCredentials {
|
|
35
|
+
readonly enabled: boolean
|
|
36
|
+
readonly clientId: string
|
|
37
|
+
readonly clientSecret: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface OidcCredentials extends ProviderCredentials {
|
|
41
|
+
readonly issuer: string
|
|
42
|
+
readonly label: string
|
|
43
|
+
readonly scopes: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FederationOptions {
|
|
47
|
+
readonly github: ProviderCredentials
|
|
48
|
+
readonly google: ProviderCredentials
|
|
49
|
+
readonly oidc: OidcCredentials
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
export {
|
|
2
|
+
assertUsableFilter,
|
|
3
|
+
BAN_FILTER_PATTERN_MAX,
|
|
4
|
+
BAN_FILTER_TYPES,
|
|
5
|
+
BAN_FILTER_WILDCARD_MAX,
|
|
6
|
+
type BanFilter,
|
|
7
|
+
type BanFilterSubject,
|
|
8
|
+
type BanFilterType,
|
|
9
|
+
matchBanFilter,
|
|
10
|
+
} from './ban-filter'
|
|
11
|
+
export { type BanInput, BanService, type BanServiceDeps } from './ban-service'
|
|
12
|
+
export { foldIdentifier } from './case-fold'
|
|
13
|
+
export {
|
|
14
|
+
CREDENTIAL_PROOF_TTL_MS,
|
|
15
|
+
hasFreshCredentialProof,
|
|
16
|
+
} from './credential-proof'
|
|
17
|
+
export {
|
|
18
|
+
decodeBase64Url,
|
|
19
|
+
decodeBase64UrlText,
|
|
20
|
+
encodeBase64Url,
|
|
21
|
+
randomBase64Url,
|
|
22
|
+
} from './crypto/base64url'
|
|
23
|
+
export {
|
|
24
|
+
isLegacyHash,
|
|
25
|
+
type LegacyMybbHash,
|
|
26
|
+
MYBB_PREFIX,
|
|
27
|
+
PHPBB_PREFIX,
|
|
28
|
+
parseMybbHash,
|
|
29
|
+
verifyLegacyPassword,
|
|
30
|
+
verifyMybbPassword,
|
|
31
|
+
verifyPhpbbPassword,
|
|
32
|
+
} from './crypto/legacy'
|
|
33
|
+
export {
|
|
34
|
+
type Argon2Params,
|
|
35
|
+
CURRENT_PASSWORD_POLICY,
|
|
36
|
+
hashPassword,
|
|
37
|
+
needsRehash,
|
|
38
|
+
type PasswordPolicy,
|
|
39
|
+
parseArgon2Params,
|
|
40
|
+
verifyPassword,
|
|
41
|
+
} from './crypto/password'
|
|
42
|
+
export {
|
|
43
|
+
generateToken,
|
|
44
|
+
hashToken,
|
|
45
|
+
timingSafeEqual,
|
|
46
|
+
} from './crypto/tokens'
|
|
47
|
+
export {
|
|
48
|
+
configuredProviders,
|
|
49
|
+
isProviderKind,
|
|
50
|
+
PROVIDER_KINDS,
|
|
51
|
+
type ProviderBuildDeps,
|
|
52
|
+
parseScopes,
|
|
53
|
+
providerFor,
|
|
54
|
+
providerLabel,
|
|
55
|
+
} from './federation/catalog'
|
|
56
|
+
export { type GithubProviderConfig, githubProvider } from './federation/github'
|
|
57
|
+
export {
|
|
58
|
+
DEFAULT_OIDC_SCOPES,
|
|
59
|
+
type OidcProviderConfig,
|
|
60
|
+
oidcProvider,
|
|
61
|
+
} from './federation/oidc'
|
|
62
|
+
export { codeChallenge, type HandshakeSecrets, newHandshake } from './federation/pkce'
|
|
63
|
+
export {
|
|
64
|
+
type CompleteSignInInput,
|
|
65
|
+
type FederationDeps,
|
|
66
|
+
type FederationOutcome,
|
|
67
|
+
FederationService,
|
|
68
|
+
UNLINK_LAST_CREDENTIAL,
|
|
69
|
+
} from './federation/service'
|
|
70
|
+
export type {
|
|
71
|
+
AuthorizeInput,
|
|
72
|
+
ExchangeInput,
|
|
73
|
+
FederationOptions,
|
|
74
|
+
IdentityProvider,
|
|
75
|
+
OidcCredentials,
|
|
76
|
+
ProviderCredentials,
|
|
77
|
+
ProviderKind,
|
|
78
|
+
ProviderProfile,
|
|
79
|
+
} from './federation/types'
|
|
80
|
+
export {
|
|
81
|
+
AUTOMATIC_LOCALE,
|
|
82
|
+
AUTOMATIC_TIMEZONE,
|
|
83
|
+
BIO_MAX,
|
|
84
|
+
EMAIL_CHANGE_TTL_MINUTES,
|
|
85
|
+
isKnownTimezone,
|
|
86
|
+
isLocalePreference,
|
|
87
|
+
isTimezonePreference,
|
|
88
|
+
LOCATION_MAX,
|
|
89
|
+
type MemberGroupChoice,
|
|
90
|
+
type MemberSettings,
|
|
91
|
+
type MemberSettingsRepository,
|
|
92
|
+
MemberSettingsService,
|
|
93
|
+
PAGE_SIZE_MAX,
|
|
94
|
+
PAGE_SIZE_MIN,
|
|
95
|
+
WEBSITE_MAX,
|
|
96
|
+
} from './member-settings'
|
|
97
|
+
export { MemoryBanFilters, MemoryBans } from './memory-bans'
|
|
98
|
+
export { createMemoryStore } from './memory-repos'
|
|
99
|
+
export {
|
|
100
|
+
AUTH_SETTING_KEYS,
|
|
101
|
+
type AuthPolicy,
|
|
102
|
+
DEFAULT_AUTH_POLICY,
|
|
103
|
+
type ResolvedAuthSettings,
|
|
104
|
+
resolveAuthPolicy,
|
|
105
|
+
type SettingReader,
|
|
106
|
+
} from './policy'
|
|
107
|
+
export type {
|
|
108
|
+
AccountRecord,
|
|
109
|
+
AccountRepository,
|
|
110
|
+
AccountState,
|
|
111
|
+
AccountStore,
|
|
112
|
+
ActiveSessionRecord,
|
|
113
|
+
AuthConfig,
|
|
114
|
+
AuthEventKind,
|
|
115
|
+
AuthEventRecord,
|
|
116
|
+
AuthEventRepository,
|
|
117
|
+
BanFilterRepository,
|
|
118
|
+
BanRecord,
|
|
119
|
+
BanRepository,
|
|
120
|
+
Clock,
|
|
121
|
+
CreateBanInput,
|
|
122
|
+
CredentialPurpose,
|
|
123
|
+
CredentialTokenRepository,
|
|
124
|
+
LinkIdentityInput,
|
|
125
|
+
LoginAttemptRepository,
|
|
126
|
+
LoginBucket,
|
|
127
|
+
MemberProfileRecord,
|
|
128
|
+
MemberProfileRepository,
|
|
129
|
+
MemberSuggestion,
|
|
130
|
+
NewAccount,
|
|
131
|
+
NewAuthEvent,
|
|
132
|
+
NewPasskey,
|
|
133
|
+
PasskeyRecord,
|
|
134
|
+
PasskeyRepository,
|
|
135
|
+
RecoveryCodeRepository,
|
|
136
|
+
RememberRotation,
|
|
137
|
+
RememberTokenRepository,
|
|
138
|
+
SessionLocation,
|
|
139
|
+
SessionRecord,
|
|
140
|
+
SessionRepository,
|
|
141
|
+
TwoFactorRecord,
|
|
142
|
+
TwoFactorRepository,
|
|
143
|
+
UserIdentityRecord,
|
|
144
|
+
UserIdentityRepository,
|
|
145
|
+
} from './ports'
|
|
146
|
+
export { AUTH_EVENT_KINDS, REMEMBER_ROTATION_GRACE_SECONDS, withinRotationGrace } from './ports'
|
|
147
|
+
export {
|
|
148
|
+
REGISTER_FIELD,
|
|
149
|
+
type RegisterField,
|
|
150
|
+
rejectedField,
|
|
151
|
+
} from './register-fields'
|
|
152
|
+
export {
|
|
153
|
+
type ActivationOutcome,
|
|
154
|
+
type ActivationResult,
|
|
155
|
+
type BanLookup,
|
|
156
|
+
type FederatedProvision,
|
|
157
|
+
type IdentityDeps,
|
|
158
|
+
IdentityService,
|
|
159
|
+
type LoginOutcome,
|
|
160
|
+
type LoginResult,
|
|
161
|
+
type PendingSecondFactor,
|
|
162
|
+
REGISTRATION_CLOSED,
|
|
163
|
+
type RegisterInput,
|
|
164
|
+
type RegisterResult,
|
|
165
|
+
type RequestContext,
|
|
166
|
+
type ResendVerification,
|
|
167
|
+
type ResetRequest,
|
|
168
|
+
SECOND_FACTOR_TTL_MINUTES,
|
|
169
|
+
type SecondFactorLookup,
|
|
170
|
+
VERIFICATION_TTL_HOURS,
|
|
171
|
+
} from './service'
|
|
172
|
+
export {
|
|
173
|
+
type RememberedLogin,
|
|
174
|
+
type ResumeOutcome,
|
|
175
|
+
SessionService,
|
|
176
|
+
type SessionServiceDeps,
|
|
177
|
+
} from './session-service'
|
|
178
|
+
export { decodeBase32, encodeBase32, isBase32 } from './totp/base32'
|
|
179
|
+
export { assertSealingKey, openSecret, sealSecret } from './totp/secret-box'
|
|
180
|
+
export {
|
|
181
|
+
clearSecondFactor,
|
|
182
|
+
type Enrolment,
|
|
183
|
+
enrolmentLookup,
|
|
184
|
+
holdsSecondFactor,
|
|
185
|
+
newRecoveryCode,
|
|
186
|
+
normaliseRecoveryCode,
|
|
187
|
+
RECOVERY_CODE_COUNT,
|
|
188
|
+
REPLAYED_CODE,
|
|
189
|
+
type SecondFactorOutcome,
|
|
190
|
+
type TwoFactorDeps,
|
|
191
|
+
TwoFactorService,
|
|
192
|
+
type TwoFactorState,
|
|
193
|
+
WRONG_CODE,
|
|
194
|
+
} from './totp/service'
|
|
195
|
+
export {
|
|
196
|
+
generateTotpSecret,
|
|
197
|
+
matchTotp,
|
|
198
|
+
otpauthUri,
|
|
199
|
+
stepAt,
|
|
200
|
+
TOTP_DIGITS,
|
|
201
|
+
TOTP_PERIOD_SECONDS,
|
|
202
|
+
totpCode,
|
|
203
|
+
} from './totp/totp'
|
|
204
|
+
export {
|
|
205
|
+
CHALLENGE_BYTES,
|
|
206
|
+
newChallenge,
|
|
207
|
+
PASSKEY_LABEL_MAX,
|
|
208
|
+
PASSKEY_LIMIT,
|
|
209
|
+
type PasskeyAssertionOptions,
|
|
210
|
+
type PasskeyDeps,
|
|
211
|
+
type PasskeyRegistrationOptions,
|
|
212
|
+
PasskeyService,
|
|
213
|
+
passkeyLabel,
|
|
214
|
+
REMOVE_LAST_CREDENTIAL,
|
|
215
|
+
} from './webauthn/service'
|
|
216
|
+
export {
|
|
217
|
+
type AssertionResponse,
|
|
218
|
+
type RegisteredCredential,
|
|
219
|
+
type RegistrationResponse,
|
|
220
|
+
type RelyingParty,
|
|
221
|
+
verifyAssertion,
|
|
222
|
+
verifyRegistration,
|
|
223
|
+
} from './webauthn/verify'
|