@@ -902,3 +902,220 @@ export async function createDcApiRequest(
902902 } ) ,
903903 } ;
904904}
905+
906+ // ============================================================================
907+ // Server/Client Split Helper Functions
908+ // ============================================================================
909+ // These functions help when you need to keep credentials on your server
910+ // but call the DC API from the browser.
911+
912+ /**
913+ * Data returned from server to browser for DC API flow
914+ */
915+ export interface DcApiRequestData {
916+ /** The signed JWT request object to pass to the DC API */
917+ requestObject : string ;
918+ /** Session ID for tracking */
919+ sessionId : string ;
920+ /** The response_uri where wallet responses should be submitted */
921+ responseUri : string ;
922+ }
923+
924+ /**
925+ * Wallet response data from the DC API to send back to server
926+ */
927+ export interface DcApiWalletResponse {
928+ /** The encrypted VP token response from the wallet */
929+ response ?: string ;
930+ /** Error code if the wallet returned an error */
931+ error ?: string ;
932+ /** Error description if available */
933+ error_description ?: string ;
934+ }
935+
936+ /**
937+ * SERVER-SIDE: Create a DC API request and return the data needed by the browser.
938+ * Use this on your server where credentials are stored securely.
939+ *
940+ * @example
941+ * ```typescript
942+ * // On your server (e.g., Express/Next.js API route)
943+ * import { createDcApiRequestForBrowser } from '@eudiplo/sdk-core';
944+ *
945+ * app.post('/api/start-verification', async (req, res) => {
946+ * const requestData = await createDcApiRequestForBrowser({
947+ * baseUrl: 'https://eudiplo.example.com',
948+ * clientId: process.env.EUDIPLO_CLIENT_ID, // Safe on server
949+ * clientSecret: process.env.EUDIPLO_SECRET, // Safe on server
950+ * configId: 'age-over-18',
951+ * });
952+ *
953+ * // Send only the safe data to the browser
954+ * res.json(requestData);
955+ * });
956+ * ```
957+ */
958+ export async function createDcApiRequestForBrowser (
959+ options : DcApiVerifyOptions
960+ ) : Promise < DcApiRequestData > {
961+ const eudiploClient = new EudiploClient ( {
962+ baseUrl : options . baseUrl ,
963+ clientId : options . clientId ,
964+ clientSecret : options . clientSecret ,
965+ } ) ;
966+
967+ const session = await eudiploClient . createDcApiPresentationRequest ( {
968+ configId : options . configId ,
969+ redirectUri : options . redirectUri ,
970+ } ) ;
971+
972+ if ( ! session . requestObject ) {
973+ throw new Error ( 'Session does not contain a requestObject' ) ;
974+ }
975+
976+ // Extract response_uri from the signed JWT
977+ const requestPayload = decodeJwtPayload < { response_uri ?: string } > (
978+ session . requestObject
979+ ) ;
980+
981+ if ( ! requestPayload . response_uri ) {
982+ throw new Error ( 'No response_uri found in request object' ) ;
983+ }
984+
985+ return {
986+ requestObject : session . requestObject ,
987+ sessionId : session . id ,
988+ responseUri : requestPayload . response_uri ,
989+ } ;
990+ }
991+
992+ /**
993+ * BROWSER-SIDE: Call the Digital Credentials API with a request from your server.
994+ * This function runs in the browser and invokes the native DC API.
995+ *
996+ * @example
997+ * ```typescript
998+ * // In your browser code
999+ * import { callDcApi, isDcApiAvailable } from '@eudiplo/sdk-core';
1000+ *
1001+ * // Get request data from your server
1002+ * const requestData = await fetch('/api/start-verification', { method: 'POST' })
1003+ * .then(r => r.json());
1004+ *
1005+ * if (isDcApiAvailable()) {
1006+ * // This calls the browser's native Digital Credentials API
1007+ * const walletResponse = await callDcApi(requestData.requestObject);
1008+ *
1009+ * // Send the wallet response back to your server for verification
1010+ * const result = await fetch('/api/complete-verification', {
1011+ * method: 'POST',
1012+ * headers: { 'Content-Type': 'application/json' },
1013+ * body: JSON.stringify({
1014+ * sessionId: requestData.sessionId,
1015+ * walletResponse,
1016+ * }),
1017+ * }).then(r => r.json());
1018+ * }
1019+ * ```
1020+ */
1021+ export async function callDcApi (
1022+ requestObject : string
1023+ ) : Promise < DcApiWalletResponse > {
1024+ if ( ! isDcApiAvailable ( ) ) {
1025+ throw new Error (
1026+ 'Digital Credentials API is not available in this browser. ' +
1027+ 'Please use a supported browser or fall back to QR code flow.'
1028+ ) ;
1029+ }
1030+
1031+ const dcResponse = ( await navigator . credentials . get ( {
1032+ mediation : 'required' ,
1033+ digital : {
1034+ requests : [
1035+ {
1036+ protocol : 'openid4vp-v1-signed' ,
1037+ data : { request : requestObject } ,
1038+ } ,
1039+ ] ,
1040+ } ,
1041+ } as CredentialRequestOptions ) ) as DigitalCredentialResponse | null ;
1042+
1043+ if ( ! dcResponse ) {
1044+ throw new Error ( 'No response from Digital Credentials API' ) ;
1045+ }
1046+
1047+ if ( dcResponse . data ?. error ) {
1048+ throw new Error (
1049+ `Wallet error: ${ dcResponse . data . error } ${
1050+ dcResponse . data . error_description
1051+ ? ` - ${ dcResponse . data . error_description } `
1052+ : ''
1053+ } `
1054+ ) ;
1055+ }
1056+
1057+ return dcResponse . data ;
1058+ }
1059+
1060+ /**
1061+ * SERVER-SIDE: Submit the wallet response to EUDIPLO and get verified credentials.
1062+ * Use this on your server after receiving the wallet response from the browser.
1063+ *
1064+ * @example
1065+ * ```typescript
1066+ * // On your server
1067+ * import { submitDcApiWalletResponse } from '@eudiplo/sdk-core';
1068+ *
1069+ * app.post('/api/complete-verification', async (req, res) => {
1070+ * const { sessionId, walletResponse } = req.body;
1071+ *
1072+ * // You stored the responseUri when creating the request, or pass it from client
1073+ * const result = await submitDcApiWalletResponse({
1074+ * responseUri: storedResponseUri, // or from request
1075+ * walletResponse,
1076+ * sendResponse: true, // Get verified claims back
1077+ * });
1078+ *
1079+ * // result.credentials contains the verified data
1080+ * res.json(result);
1081+ * });
1082+ * ```
1083+ */
1084+ export async function submitDcApiWalletResponse ( options : {
1085+ /** The response_uri from the request (from DcApiRequestData.responseUri) */
1086+ responseUri : string ;
1087+ /** The wallet response from callDcApi() */
1088+ walletResponse : DcApiWalletResponse ;
1089+ /** Whether to return full credential data (default: true) */
1090+ sendResponse ?: boolean ;
1091+ /** Custom fetch implementation (optional) */
1092+ fetch ?: typeof fetch ;
1093+ } ) : Promise < DcApiPresentationResult > {
1094+ const fetchImpl = options . fetch ?? fetch ;
1095+
1096+ const submitResponse = await fetchImpl ( options . responseUri , {
1097+ method : 'POST' ,
1098+ headers : {
1099+ 'Content-Type' : 'application/json' ,
1100+ } ,
1101+ body : JSON . stringify ( {
1102+ ...options . walletResponse ,
1103+ sendResponse : options . sendResponse ?? true ,
1104+ } ) ,
1105+ } ) ;
1106+
1107+ if ( ! submitResponse . ok ) {
1108+ const errorText = await submitResponse . text ( ) ;
1109+ throw new Error (
1110+ `Failed to submit presentation: ${ submitResponse . status } ${ errorText } `
1111+ ) ;
1112+ }
1113+
1114+ const result = await submitResponse . json ( ) ;
1115+
1116+ return {
1117+ credentials : result . credentials ?? result ,
1118+ response : result ,
1119+ redirectUri : result . redirect_uri ,
1120+ } ;
1121+ }
0 commit comments