Skip to content

Commit 38a4b64

Browse files
committed
feat(davinci-client): add fido module
1 parent ea4e774 commit 38a4b64

12 files changed

Lines changed: 372 additions & 21 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"ci:release": "pnpm publish -r --no-git-checks && changeset tag",
1919
"ci:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm nx format:write --uncommitted",
2020
"circular-dep-check": "madge --circular .",
21-
"clean": "shx rm -rf ./{coverage,dist,docs,node_modules,tmp}/ ./{packages,e2e}/*/{dist,node_modules}/ && git clean -fX -e \"!.env*,nx-cloud.env\" -e \"!**/GEMINI.md\"",
21+
"clean": "shx rm -rf ./{coverage,dist,docs,node_modules,tmp}/ ./{packages,e2e}/*/{dist,node_modules}/ ./e2e/node_modules/ && git clean -fX -e \"!.env*,nx-cloud.env\" -e \"!**/GEMINI.md\"",
2222
"commit": "git cz",
2323
"commitlint": "commitlint --edit",
2424
"create-package": "nx g @nx/js:library",

packages/davinci-client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@forgerock/sdk-types": "workspace:*",
3030
"@forgerock/storage": "workspace:*",
3131
"@reduxjs/toolkit": "catalog:",
32+
"effect": "catalog:effect",
3233
"immer": "catalog:"
3334
},
3435
"devDependencies": {

packages/davinci-client/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
* of the MIT license. See the LICENSE file for details.
66
*/
77
import { davinci } from './lib/client.store.js';
8+
import { fido } from './lib/fido/fido.js';
89

9-
export { davinci };
10+
export { davinci, fido };

packages/davinci-client/src/lib/client.types.test-d.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import type { GenericError } from '@forgerock/sdk-types';
1111

1212
import type { InitFlow, InternalErrorResponse, Updater } from './client.types.js';
1313
import type { ErrorNode, FailureNode, ContinueNode, StartNode, SuccessNode } from './node.types.js';
14-
import type { PhoneNumberInputValue } from './collector.types.js';
14+
import type {
15+
FidoAuthenticationInputValue,
16+
FidoRegistrationInputValue,
17+
PhoneNumberInputValue,
18+
} from './collector.types.js';
1519

1620
describe('Client Types', () => {
1721
it('should allow function returning error', async () => {
@@ -170,15 +174,29 @@ describe('Client Types', () => {
170174
describe('Updater', () => {
171175
it('should accept string value and optional index', () => {
172176
const updater: Updater = (
173-
value: string | string[] | boolean | PhoneNumberInputValue,
177+
value:
178+
| string
179+
| string[]
180+
| boolean
181+
| PhoneNumberInputValue
182+
| FidoRegistrationInputValue
183+
| FidoAuthenticationInputValue,
174184
index?: number,
175185
) => {
176186
return {
177187
error: { message: 'Invalid value', code: 'INVALID', type: 'state_error' },
178188
type: 'internal_error',
179189
};
180190
};
181-
expectTypeOf(updater).parameter(0).toEqualTypeOf<string | string[] | PhoneNumberInputValue>();
191+
expectTypeOf(updater)
192+
.parameter(0)
193+
.toEqualTypeOf<
194+
| string
195+
| string[]
196+
| PhoneNumberInputValue
197+
| FidoRegistrationInputValue
198+
| FidoAuthenticationInputValue
199+
>();
182200
expectTypeOf(updater).parameter(1).toBeNullable();
183201
expectTypeOf(updater).parameter(1).toBeNullable();
184202
});

packages/davinci-client/src/lib/client.types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
*/
77
import type { GenericError } from '@forgerock/sdk-types';
88

9-
import type { PhoneNumberInputValue } from './collector.types.js';
9+
import type {
10+
FidoRegistrationInputValue,
11+
FidoAuthenticationInputValue,
12+
PhoneNumberInputValue,
13+
} from './collector.types.js';
1014
import type { ErrorNode, FailureNode, ContinueNode, StartNode, SuccessNode } from './node.types.js';
1115

1216
export type FlowNode = ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode;
@@ -19,7 +23,12 @@ export interface InternalErrorResponse {
1923
export type InitFlow = () => Promise<FlowNode | InternalErrorResponse>;
2024

2125
export type Updater = (
22-
value: string | string[] | PhoneNumberInputValue,
26+
value:
27+
| string
28+
| string[]
29+
| PhoneNumberInputValue
30+
| FidoRegistrationInputValue
31+
| FidoAuthenticationInputValue,
2332
index?: number,
2433
) => InternalErrorResponse | null;
2534
export type Validator = (value: string) =>

packages/davinci-client/src/lib/collector.types.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* of the MIT license. See the LICENSE file for details.
66
*/
77

8+
import type { FidoAuthenticationOptions, FidoRegistrationOptions } from './davinci.types.js';
9+
810
/** *********************************************************************
911
* SINGLE-VALUE COLLECTORS
1012
*/
@@ -302,14 +304,6 @@ export interface PhoneNumberOutputValue {
302304
phoneNumber?: string;
303305
}
304306

305-
export interface FidoRegistrationInputValue {
306-
attestationValue?: PublicKeyCredential;
307-
}
308-
309-
export interface FidoAuthenticationInputValue {
310-
assertionValue?: PublicKeyCredential;
311-
}
312-
313307
export interface ObjectOptionsCollectorWithStringValue<
314308
T extends ObjectValueCollectorTypes,
315309
V = string,
@@ -544,6 +538,51 @@ export type UnknownCollector = {
544538
* @interface AutoCollector - Represents a collector that collects a value programmatically without user intervention.
545539
*/
546540

541+
export interface ProtectOutputValue {
542+
behavioralDataCollection: boolean;
543+
universalDeviceIdentification: boolean;
544+
}
545+
546+
export interface AttestationValue
547+
extends Omit<PublicKeyCredential, 'rawId' | 'response' | 'getClientExtensionResults' | 'toJSON'> {
548+
rawId: string;
549+
response: {
550+
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse
551+
clientDataJSON: string;
552+
attestationObject: string;
553+
};
554+
}
555+
export interface FidoRegistrationInputValue {
556+
attestationValue?: AttestationValue;
557+
}
558+
559+
export interface FidoRegistrationOutputValue {
560+
publicKeyCredentialCreationOptions: FidoRegistrationOptions;
561+
action: 'REGISTER';
562+
trigger: string;
563+
}
564+
565+
export interface AssertionValue
566+
extends Omit<PublicKeyCredential, 'rawId' | 'response' | 'getClientExtensionResults' | 'toJSON'> {
567+
rawId: string;
568+
response: {
569+
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse
570+
clientDataJSON: string;
571+
authenticatorData: string;
572+
signature: string;
573+
userHandle: string | null;
574+
};
575+
}
576+
export interface FidoAuthenticationInputValue {
577+
assertionValue?: AssertionValue;
578+
}
579+
580+
export interface FidoAuthenticationOutputValue {
581+
publicKeyCredentialRequestOptions: FidoAuthenticationOptions;
582+
action: 'AUTHENTICATE';
583+
trigger: string;
584+
}
585+
547586
export type AutoCollectorCategories = 'SingleValueAutoCollector' | 'ObjectValueAutoCollector';
548587
export type SingleValueAutoCollectorTypes = 'SingleValueAutoCollector' | 'ProtectCollector';
549588
export type ObjectValueAutoCollectorTypes =
@@ -556,6 +595,7 @@ export interface AutoCollector<
556595
C extends AutoCollectorCategories,
557596
T extends AutoCollectorTypes,
558597
IV = string,
598+
OV = Record<string, unknown>,
559599
> {
560600
category: C;
561601
error: string | null;
@@ -571,24 +611,27 @@ export interface AutoCollector<
571611
output: {
572612
key: string;
573613
type: string;
574-
config: Record<string, unknown>;
614+
config: OV;
575615
};
576616
}
577617

578618
export type ProtectCollector = AutoCollector<
579619
'SingleValueAutoCollector',
580620
'ProtectCollector',
581-
string
621+
string,
622+
ProtectOutputValue
582623
>;
583624
export type FidoRegistrationCollector = AutoCollector<
584625
'ObjectValueAutoCollector',
585626
'FidoRegistrationCollector',
586-
FidoRegistrationInputValue
627+
FidoRegistrationInputValue,
628+
FidoRegistrationOutputValue
587629
>;
588630
export type FidoAuthenticationCollector = AutoCollector<
589631
'ObjectValueAutoCollector',
590632
'FidoAuthenticationCollector',
591-
FidoAuthenticationInputValue
633+
FidoAuthenticationInputValue,
634+
FidoAuthenticationOutputValue
592635
>;
593636
export type SingleValueAutoCollector = AutoCollector<
594637
'SingleValueAutoCollector',

packages/davinci-client/src/lib/davinci.types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,25 @@ export type ProtectField = {
161161
};
162162

163163
export interface FidoRegistrationOptions
164-
extends Omit<PublicKeyCredentialCreationOptions, 'challenge' | 'user'> {
164+
extends Omit<
165+
PublicKeyCredentialCreationOptions,
166+
'challenge' | 'user' | 'pubKeyCredParams' | 'excludeCredentials'
167+
> {
165168
challenge: number[];
166169
user: {
167170
id: number[];
168171
name: string;
169172
displayName: string;
170173
};
174+
pubKeyCredParams: {
175+
alg: string | number;
176+
type: PublicKeyCredentialType;
177+
}[];
178+
excludeCredentials?: {
179+
id: number[];
180+
transports?: AuthenticatorTransport[];
181+
type: PublicKeyCredentialType;
182+
}[];
171183
}
172184

173185
export type FidoRegistrationField = {

packages/davinci-client/src/lib/davinci.utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ export function transformSubmitRequest(
4444
collector.category === 'SingleValueCollector' ||
4545
collector.category === 'ValidatedSingleValueCollector' ||
4646
collector.category === 'ObjectValueCollector' ||
47-
collector.category === 'SingleValueAutoCollector',
47+
collector.category === 'SingleValueAutoCollector' ||
48+
collector.category === 'ObjectValueAutoCollector',
4849
);
4950

5051
const formData = collectors?.reduce<{
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import { Micro } from 'effect';
8+
import { exitIsFail, exitIsSuccess } from 'effect/Micro';
9+
import {
10+
transformAssertion,
11+
transformAuthenticationOptions,
12+
transformPublicKeyCredential,
13+
transformRegistrationOptions,
14+
} from './fido.utils.js';
15+
16+
import type { GenericError } from '@forgerock/sdk-types';
17+
import type {
18+
FidoAuthenticationInputValue,
19+
FidoRegistrationInputValue,
20+
} from '../collector.types.js';
21+
import type { FidoAuthenticationOptions, FidoRegistrationOptions } from '../davinci.types.js';
22+
23+
export function fido() {
24+
return {
25+
register: async function register(
26+
options: FidoRegistrationOptions,
27+
): Promise<FidoRegistrationInputValue | GenericError> {
28+
/**
29+
* Call WebAuthn API to create key pair and get public key credential
30+
*/
31+
const createCredentialµ = Micro.sync(() => transformRegistrationOptions(options)).pipe(
32+
Micro.flatMap((publicKeyCredentialCreationOptions) =>
33+
Micro.tryPromise({
34+
try: () =>
35+
navigator.credentials.create({
36+
publicKey: publicKeyCredentialCreationOptions,
37+
}),
38+
catch: (error) => {
39+
console.error('Failed to register key pair: ', error);
40+
return {
41+
error: 'registration_error',
42+
message: 'FIDO registration failed',
43+
type: 'fido_error',
44+
} as GenericError;
45+
},
46+
}),
47+
),
48+
Micro.flatMap((credential) => {
49+
if (!credential) {
50+
return Micro.fail({
51+
error: 'registration_error',
52+
message: 'FIDO registration failed: No credential returned',
53+
type: 'fido_error',
54+
} as GenericError);
55+
} else {
56+
const formattedCredential = transformPublicKeyCredential(
57+
credential as PublicKeyCredential,
58+
);
59+
return Micro.succeed(formattedCredential);
60+
}
61+
}),
62+
);
63+
64+
const result = await Micro.runPromiseExit(createCredentialµ);
65+
66+
if (exitIsSuccess(result)) {
67+
return result.value;
68+
} else if (exitIsFail(result)) {
69+
return result.cause.error;
70+
} else {
71+
return {
72+
error: 'fido_registration_error',
73+
message: result.cause.message,
74+
type: 'unknown_error',
75+
};
76+
}
77+
},
78+
authenticate: async function authenticate(
79+
options: FidoAuthenticationOptions,
80+
): Promise<FidoAuthenticationInputValue | GenericError> {
81+
/**
82+
* Call WebAuthn API to get assertion
83+
*/
84+
const getAssertionµ = Micro.sync(() => transformAuthenticationOptions(options)).pipe(
85+
Micro.flatMap((publicKeyCredentialRequestOptions) =>
86+
Micro.tryPromise({
87+
try: () =>
88+
navigator.credentials.get({
89+
publicKey: publicKeyCredentialRequestOptions,
90+
}),
91+
catch: (error) => {
92+
console.error('Failed to authenticate: ', error);
93+
return {
94+
error: 'authentication_error',
95+
message: 'FIDO authentication failed',
96+
type: 'fido_error',
97+
} as GenericError;
98+
},
99+
}),
100+
),
101+
Micro.flatMap((assertion) => {
102+
if (!assertion) {
103+
return Micro.fail({
104+
error: 'authentication_error',
105+
message: 'FIDO authentication failed: No credential returned',
106+
type: 'fido_error',
107+
} as GenericError);
108+
} else {
109+
const formattedAssertion = transformAssertion(assertion as PublicKeyCredential);
110+
return Micro.succeed(formattedAssertion);
111+
}
112+
}),
113+
);
114+
115+
const result = await Micro.runPromiseExit(getAssertionµ);
116+
117+
if (exitIsSuccess(result)) {
118+
return result.value;
119+
} else if (exitIsFail(result)) {
120+
return result.cause.error;
121+
} else {
122+
return {
123+
error: 'fido_authentication_error',
124+
message: result.cause.message,
125+
type: 'unknown_error',
126+
};
127+
}
128+
},
129+
};
130+
}

0 commit comments

Comments
 (0)