Skip to content

Commit 127d78c

Browse files
authored
Merge pull request #408 from openwallet-foundation-labs/fix/sdk-improve
fix: improve sdk
2 parents 748e526 + 7f7f98d commit 127d78c

File tree

3 files changed

+320
-0
lines changed

3 files changed

+320
-0
lines changed

packages/eudiplo-sdk-core/README.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,102 @@ const credential = await navigator.credentials.get(request);
136136
const session = await client.submitDcApiResponse(sessionId, credential);
137137
```
138138

139+
#### Secure Server/Client Deployment (Recommended for Production)
140+
141+
When deploying to production, you should **never expose your client credentials to the browser**. The SDK provides helper functions to split the DC API flow between your server (where credentials are safe) and the browser (where the DC API runs).
142+
143+
**Server-side Functions:**
144+
| Function | Description |
145+
| ---------------------- | --------------------------------------------------- |
146+
| `createDcApiRequestForBrowser()` | Create request on server, return safe data for browser |
147+
| `submitDcApiWalletResponse()` | Submit wallet response to EUDIPLO from server |
148+
149+
**Browser-side Functions:**
150+
| Function | Description |
151+
| ---------------------- | --------------------------------------------------- |
152+
| `callDcApi()` | Call the native DC API with a request from your server |
153+
154+
##### Example: Express.js Backend + Browser Frontend
155+
156+
**Server (Express.js / Next.js API route):**
157+
158+
```typescript
159+
import {
160+
createDcApiRequestForBrowser,
161+
submitDcApiWalletResponse,
162+
} from '@eudiplo/sdk-core';
163+
164+
// POST /api/start-verification
165+
app.post('/api/start-verification', async (req, res) => {
166+
const requestData = await createDcApiRequestForBrowser({
167+
baseUrl: process.env.EUDIPLO_URL,
168+
clientId: process.env.EUDIPLO_CLIENT_ID, // ✅ Safe on server
169+
clientSecret: process.env.EUDIPLO_SECRET, // ✅ Safe on server
170+
configId: 'age-over-18',
171+
});
172+
173+
// Only safe data is sent to browser (no secrets)
174+
res.json(requestData);
175+
});
176+
177+
// POST /api/complete-verification
178+
app.post('/api/complete-verification', async (req, res) => {
179+
const { responseUri, walletResponse } = req.body;
180+
181+
const result = await submitDcApiWalletResponse({
182+
responseUri,
183+
walletResponse,
184+
sendResponse: true, // Get verified claims back
185+
});
186+
187+
// result.credentials contains the verified data
188+
res.json(result);
189+
});
190+
```
191+
192+
**Browser (React / vanilla JS):**
193+
194+
```typescript
195+
import { callDcApi, isDcApiAvailable } from '@eudiplo/sdk-core';
196+
197+
async function verifyAge() {
198+
// 1. Get the request from your server (credentials stay on server)
199+
const requestData = await fetch('/api/start-verification', {
200+
method: 'POST',
201+
}).then((r) => r.json());
202+
203+
// 2. Check DC API support and call it locally
204+
if (!isDcApiAvailable()) {
205+
throw new Error('Digital Credentials API not supported');
206+
}
207+
208+
const walletResponse = await callDcApi(requestData.requestObject);
209+
210+
// 3. Send the wallet response back to your server for verification
211+
const result = await fetch('/api/complete-verification', {
212+
method: 'POST',
213+
headers: { 'Content-Type': 'application/json' },
214+
body: JSON.stringify({
215+
responseUri: requestData.responseUri,
216+
walletResponse,
217+
}),
218+
}).then((r) => r.json());
219+
220+
console.log('Verified!', result.credentials);
221+
return result;
222+
}
223+
```
224+
225+
**What stays where:**
226+
227+
| Data | Location | Safe to expose? |
228+
|------|----------|-----------------|
229+
| `clientId` / `clientSecret` | Server only | ❌ Never expose |
230+
| `requestObject` (signed JWT) | Server → Browser | ✅ Yes |
231+
| `responseUri` | Server → Browser | ✅ Yes |
232+
| Wallet response (encrypted VP) | Browser → Server | ✅ Yes |
233+
| Verified credentials | Server only | Depends on use case |
234+
139235
### Class-based API (More Control)
140236

141237
```typescript

packages/eudiplo-sdk-core/src/client.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

packages/eudiplo-sdk-core/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,19 @@ export {
1919
isDcApiAvailable,
2020
verifyWithDcApi,
2121
createDcApiRequest,
22+
// Server/Client split helpers
23+
createDcApiRequestForBrowser,
24+
callDcApi,
25+
submitDcApiWalletResponse,
2226
} from './client';
2327
export type {
2428
DcApiVerifyOptions,
2529
DcApiPresentationOptions,
2630
DcApiPresentationResult,
2731
DigitalCredentialResponse,
32+
// Server/Client split types
33+
DcApiRequestData,
34+
DcApiWalletResponse,
2835
} from './client';
2936

3037
// Re-export the HTTP client instance for direct API usage

0 commit comments

Comments
 (0)