From 8746bc083e5927571ff57a091d4b8d3a0cce56eb Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 19 May 2026 06:24:06 +0000 Subject: [PATCH] feat(interfaces): add MFA and passkey contracts --- ts/data/activity.ts | 7 ++ ts/data/index.ts | 2 + ts/data/mfa.ts | 45 ++++++++++ ts/data/passkey.ts | 49 +++++++++++ ts/request/index.ts | 1 + ts/request/login.ts | 7 +- ts/request/mfa.ts | 208 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 ts/data/mfa.ts create mode 100644 ts/data/passkey.ts create mode 100644 ts/request/mfa.ts diff --git a/ts/data/activity.ts b/ts/data/activity.ts index 98d8913..b7c741d 100644 --- a/ts/data/activity.ts +++ b/ts/data/activity.ts @@ -16,6 +16,13 @@ export type TActivityAction = | 'role_changed' | 'org_app_role_mappings_updated' | 'profile_updated' + | 'totp_enabled' + | 'totp_disabled' + | 'backup_codes_regenerated' + | 'mfa_completed' + | 'passkey_registered' + | 'passkey_revoked' + | 'passkey_login' | 'app_connected' | 'app_disconnected'; diff --git a/ts/data/index.ts b/ts/data/index.ts index 355e4ce..dfa981b 100644 --- a/ts/data/index.ts +++ b/ts/data/index.ts @@ -10,8 +10,10 @@ export * from './billingplan.js'; export * from './device.js'; export * from './jwt.js'; export * from './loginsession.js'; +export * from './mfa.js'; export * from './organization.js'; export * from './paddlecheckoutdata.js'; +export * from './passkey.js'; export * from './passportchallenge.js'; export * from './passportdevice.js'; export * from './passportnonce.js'; diff --git a/ts/data/mfa.ts b/ts/data/mfa.ts new file mode 100644 index 0000000..b71b800 --- /dev/null +++ b/ts/data/mfa.ts @@ -0,0 +1,45 @@ +export type TMfaMethod = 'totp' | 'backupCode' | 'passkey'; + +export type TMfaChallengeStatus = 'pending' | 'completed' | 'expired'; + +export type TTotpCredentialStatus = 'pending' | 'active' | 'disabled'; + +export interface ITotpBackupCode { + id: string; + codeHash: string; + usedAt?: number | null; + createdAt: number; +} + +export interface ITotpCredential { + id: string; + data: { + userId: string; + status: TTotpCredentialStatus; + secretCiphertext: string; + secretIv: string; + secretAuthTag: string; + algorithm: 'sha1' | 'sha256' | 'sha512'; + digits: 6 | 7 | 8; + period: number; + backupCodes: ITotpBackupCode[]; + createdAt: number; + verifiedAt?: number | null; + disabledAt?: number | null; + lastUsedAt?: number | null; + }; +} + +export interface IMfaChallenge { + id: string; + data: { + userId: string; + tokenHash: string; + status: TMfaChallengeStatus; + availableMethods: TMfaMethod[]; + primaryAuthMethod: 'password' | 'email'; + createdAt: number; + expiresAt: number; + completedAt?: number | null; + }; +} diff --git a/ts/data/passkey.ts b/ts/data/passkey.ts new file mode 100644 index 0000000..9106b39 --- /dev/null +++ b/ts/data/passkey.ts @@ -0,0 +1,49 @@ +export type TPasskeyCredentialStatus = 'active' | 'revoked'; + +export type TPasskeyChallengeType = 'registration' | 'login' | 'mfa'; + +export type TPasskeyChallengeStatus = 'pending' | 'completed' | 'expired'; + +export type TPasskeyTransport = + | 'ble' + | 'cable' + | 'hybrid' + | 'internal' + | 'nfc' + | 'smart-card' + | 'usb'; + +export type TPasskeyDeviceType = 'singleDevice' | 'multiDevice'; + +export interface IPasskeyCredential { + id: string; + data: { + userId: string; + label: string; + credentialId: string; + publicKeyBase64: string; + counter: number; + deviceType: TPasskeyDeviceType; + backedUp: boolean; + transports?: TPasskeyTransport[]; + status: TPasskeyCredentialStatus; + createdAt: number; + lastUsedAt?: number | null; + revokedAt?: number | null; + }; +} + +export interface IWebAuthnChallenge { + id: string; + data: { + userId?: string | null; + username?: string | null; + mfaChallengeId?: string | null; + type: TPasskeyChallengeType; + challenge: string; + status: TPasskeyChallengeStatus; + createdAt: number; + expiresAt: number; + completedAt?: number | null; + }; +} diff --git a/ts/request/index.ts b/ts/request/index.ts index c29328b..834e0bb 100644 --- a/ts/request/index.ts +++ b/ts/request/index.ts @@ -6,6 +6,7 @@ export * from './authorization.js'; export * from './billingplan.js'; export * from './jwt.js'; export * from './login.js'; +export * from './mfa.js'; export * from './organization.js'; export * from './passport.js'; export * from './plan.js'; diff --git a/ts/request/login.ts b/ts/request/login.ts index 66ec022..4e07ce1 100644 --- a/ts/request/login.ts +++ b/ts/request/login.ts @@ -14,6 +14,8 @@ export interface IReq_LoginWithEmailOrUsernameAndPassword response: { refreshToken?: string; twoFaNeeded: boolean; + mfaChallengeToken?: string; + availableMfaMethods?: data.TMfaMethod[]; }; } @@ -43,7 +45,10 @@ export interface IReq_LoginWithEmailAfterEmailTokenAquired token: string; }; response: { - refreshToken: string; + refreshToken?: string; + twoFaNeeded?: boolean; + mfaChallengeToken?: string; + availableMfaMethods?: data.TMfaMethod[]; }; } diff --git a/ts/request/mfa.ts b/ts/request/mfa.ts new file mode 100644 index 0000000..09945cc --- /dev/null +++ b/ts/request/mfa.ts @@ -0,0 +1,208 @@ +import * as plugins from '../plugins.js'; +import * as data from '../data/index.js'; + +export interface IReq_GetMfaStatus + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_GetMfaStatus + > { + method: 'getMfaStatus'; + request: { + jwt: string; + }; + response: { + totpEnabled: boolean; + backupCodesRemaining: number; + passkeys: data.IPasskeyCredential[]; + availableMethods: data.TMfaMethod[]; + }; +} + +export interface IReq_StartTotpEnrollment + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_StartTotpEnrollment + > { + method: 'startTotpEnrollment'; + request: { + jwt: string; + }; + response: { + credentialId: string; + secret: string; + otpauthUrl: string; + }; +} + +export interface IReq_FinishTotpEnrollment + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_FinishTotpEnrollment + > { + method: 'finishTotpEnrollment'; + request: { + jwt: string; + credentialId: string; + code: string; + }; + response: { + success: boolean; + backupCodes: string[]; + }; +} + +export interface IReq_DisableTotp + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_DisableTotp + > { + method: 'disableTotp'; + request: { + jwt: string; + code: string; + }; + response: { + success: boolean; + }; +} + +export interface IReq_RegenerateBackupCodes + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_RegenerateBackupCodes + > { + method: 'regenerateBackupCodes'; + request: { + jwt: string; + code: string; + }; + response: { + backupCodes: string[]; + }; +} + +export interface IReq_VerifyMfaChallenge + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_VerifyMfaChallenge + > { + method: 'verifyMfaChallenge'; + request: { + mfaChallengeToken: string; + method: Extract; + code: string; + }; + response: { + refreshToken: string; + }; +} + +export interface IReq_StartPasskeyRegistration + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_StartPasskeyRegistration + > { + method: 'startPasskeyRegistration'; + request: { + jwt: string; + label?: string; + }; + response: { + challengeId: string; + options: Record; + }; +} + +export interface IReq_FinishPasskeyRegistration + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_FinishPasskeyRegistration + > { + method: 'finishPasskeyRegistration'; + request: { + jwt: string; + challengeId: string; + label?: string; + response: Record; + }; + response: { + success: boolean; + passkey: data.IPasskeyCredential; + }; +} + +export interface IReq_RevokePasskey + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_RevokePasskey + > { + method: 'revokePasskey'; + request: { + jwt: string; + passkeyId: string; + }; + response: { + success: boolean; + }; +} + +export interface IReq_StartPasskeyLogin + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_StartPasskeyLogin + > { + method: 'startPasskeyLogin'; + request: { + username?: string; + }; + response: { + challengeId: string; + options: Record; + }; +} + +export interface IReq_FinishPasskeyLogin + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_FinishPasskeyLogin + > { + method: 'finishPasskeyLogin'; + request: { + challengeId: string; + response: Record; + }; + response: { + refreshToken: string; + }; +} + +export interface IReq_StartPasskeyMfa + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_StartPasskeyMfa + > { + method: 'startPasskeyMfa'; + request: { + mfaChallengeToken: string; + }; + response: { + challengeId: string; + options: Record; + }; +} + +export interface IReq_FinishPasskeyMfa + extends plugins.typedRequestInterfaces.implementsTR< + plugins.typedRequestInterfaces.ITypedRequest, + IReq_FinishPasskeyMfa + > { + method: 'finishPasskeyMfa'; + request: { + mfaChallengeToken: string; + challengeId: string; + response: Record; + }; + response: { + refreshToken: string; + }; +}