@eudiplo/sdk-core 1.14.0-main.a39e98f → 1.14.0-main.a3bed4b

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/README.md CHANGED
@@ -82,6 +82,158 @@ await waitForCompletion();
82
82
  | `verifyAndWait(options)` | One-liner: create request + wait for result |
83
83
  | `issueAndWait(options)` | One-liner: create offer + wait for result |
84
84
 
85
+ ### Digital Credentials API (Browser Native)
86
+
87
+ The SDK includes utilities for the [Digital Credentials API](https://wicg.github.io/digital-credentials/), enabling browser-native credential presentation without QR codes.
88
+
89
+ ```typescript
90
+ import { isDcApiAvailable, verifyWithDcApi } from '@eudiplo/sdk-core';
91
+
92
+ // Check if browser supports DC API
93
+ if (isDcApiAvailable()) {
94
+ const result = await verifyWithDcApi({
95
+ baseUrl: 'https://eudiplo.example.com',
96
+ clientId: 'my-demo',
97
+ clientSecret: 'secret',
98
+ configId: 'age-over-18',
99
+ });
100
+
101
+ console.log('Verified!', result.session.credentials);
102
+ } else {
103
+ // Fall back to QR code flow
104
+ const session = await verifyAndWait({...});
105
+ }
106
+ ```
107
+
108
+ #### DC API Functions
109
+
110
+ | Function | Description |
111
+ | ---------------------- | --------------------------------------------------- |
112
+ | `isDcApiAvailable()` | Check if browser supports Digital Credentials API |
113
+ | `verifyWithDcApi()` | Complete verification flow using browser-native API |
114
+ | `createDcApiRequest()` | Create a `DigitalCredentialRequestOptions` object |
115
+
116
+ #### Lower-level DC API Usage
117
+
118
+ ```typescript
119
+ import { createDcApiRequest, EudiploClient } from '@eudiplo/sdk-core';
120
+
121
+ const client = new EudiploClient({...});
122
+
123
+ // Create presentation request
124
+ const { uri, sessionId } = await client.createPresentationRequest({
125
+ configId: 'age-over-18',
126
+ responseType: 'dc-api',
127
+ });
128
+
129
+ // Create the browser request object
130
+ const request = createDcApiRequest(uri);
131
+
132
+ // Call the browser API directly
133
+ const credential = await navigator.credentials.get(request);
134
+
135
+ // Submit the response and get verified session
136
+ const session = await client.submitDcApiResponse(sessionId, credential);
137
+ ```
138
+
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
+
145
+ | Function | Description |
146
+ | -------------------------------- | ------------------------------------------------------ |
147
+ | `createDcApiRequestForBrowser()` | Create request on server, return safe data for browser |
148
+ | `submitDcApiWalletResponse()` | Submit wallet response to EUDIPLO from server |
149
+
150
+ **Browser-side Functions:**
151
+
152
+ | Function | Description |
153
+ | ------------- | ------------------------------------------------------ |
154
+ | `callDcApi()` | Call the native DC API with a request from your server |
155
+
156
+ ##### Example: Express.js Backend + Browser Frontend
157
+
158
+ **Server (Express.js / Next.js API route):**
159
+
160
+ ```typescript
161
+ import {
162
+ createDcApiRequestForBrowser,
163
+ submitDcApiWalletResponse,
164
+ } from '@eudiplo/sdk-core';
165
+
166
+ // POST /api/start-verification
167
+ app.post('/api/start-verification', async (req, res) => {
168
+ const requestData = await createDcApiRequestForBrowser({
169
+ baseUrl: process.env.EUDIPLO_URL,
170
+ clientId: process.env.EUDIPLO_CLIENT_ID, // ✅ Safe on server
171
+ clientSecret: process.env.EUDIPLO_SECRET, // ✅ Safe on server
172
+ configId: 'age-over-18',
173
+ });
174
+
175
+ // Only safe data is sent to browser (no secrets)
176
+ res.json(requestData);
177
+ });
178
+
179
+ // POST /api/complete-verification
180
+ app.post('/api/complete-verification', async (req, res) => {
181
+ const { responseUri, walletResponse } = req.body;
182
+
183
+ const result = await submitDcApiWalletResponse({
184
+ responseUri,
185
+ walletResponse,
186
+ sendResponse: true, // Get verified claims back
187
+ });
188
+
189
+ // result.credentials contains the verified data
190
+ res.json(result);
191
+ });
192
+ ```
193
+
194
+ **Browser (React / vanilla JS):**
195
+
196
+ ```typescript
197
+ import { callDcApi, isDcApiAvailable } from '@eudiplo/sdk-core';
198
+
199
+ async function verifyAge() {
200
+ // 1. Get the request from your server (credentials stay on server)
201
+ const requestData = await fetch('/api/start-verification', {
202
+ method: 'POST',
203
+ }).then((r) => r.json());
204
+
205
+ // 2. Check DC API support and call it locally
206
+ if (!isDcApiAvailable()) {
207
+ throw new Error('Digital Credentials API not supported');
208
+ }
209
+
210
+ const walletResponse = await callDcApi(requestData.requestObject);
211
+
212
+ // 3. Send the wallet response back to your server for verification
213
+ const result = await fetch('/api/complete-verification', {
214
+ method: 'POST',
215
+ headers: { 'Content-Type': 'application/json' },
216
+ body: JSON.stringify({
217
+ responseUri: requestData.responseUri,
218
+ walletResponse,
219
+ }),
220
+ }).then((r) => r.json());
221
+
222
+ console.log('Verified!', result.credentials);
223
+ return result;
224
+ }
225
+ ```
226
+
227
+ **What stays where:**
228
+
229
+ | Data | Location | Safe to expose? |
230
+ | ------------------------------ | ---------------- | ------------------- |
231
+ | `clientId` / `clientSecret` | Server only | ❌ Never expose |
232
+ | `requestObject` (signed JWT) | Server → Browser | ✅ Yes |
233
+ | `responseUri` | Server → Browser | ✅ Yes |
234
+ | Wallet response (encrypted VP) | Browser → Server | ✅ Yes |
235
+ | Verified credentials | Server only | Depends on use case |
236
+
85
237
  ### Class-based API (More Control)
86
238
 
87
239
  ```typescript
@@ -175,6 +327,43 @@ const session = await client.waitForSession(sessionId, {
175
327
  });
176
328
  ```
177
329
 
330
+ ### `subscribeToSession(sessionId, options)`
331
+
332
+ Subscribe to real-time session status updates via Server-Sent Events (SSE).
333
+ This is more efficient than polling and provides instant updates.
334
+
335
+ ```typescript
336
+ const subscription = await client.subscribeToSession(sessionId, {
337
+ onStatusChange: (event) => {
338
+ console.log(`Status: ${event.status}`);
339
+ if (['completed', 'expired', 'failed'].includes(event.status)) {
340
+ subscription.close();
341
+ }
342
+ },
343
+ onError: (error) => console.error('SSE error:', error),
344
+ onOpen: () => console.log('Connected'),
345
+ });
346
+
347
+ // Later, to close the connection:
348
+ subscription.close();
349
+ ```
350
+
351
+ ### `waitForSessionWithSse(sessionId, options)`
352
+
353
+ Wait for session completion using SSE instead of polling. Returns a Promise
354
+ that resolves when the session completes.
355
+
356
+ ```typescript
357
+ try {
358
+ const finalStatus = await client.waitForSessionWithSse(sessionId, {
359
+ onStatusChange: (event) => console.log('Status:', event.status),
360
+ });
361
+ console.log('Session completed:', finalStatus);
362
+ } catch (error) {
363
+ console.error('Session failed:', error);
364
+ }
365
+ ```
366
+
178
367
  ## Examples
179
368
 
180
369
  ### Age Verification in a Web Shop
@@ -1,4 +1,4 @@
1
- import { C as Config, a as Client } from '../../types.gen-CIiveH8G.mjs';
1
+ import { b as Config, C as Client } from '../../types.gen-B0nhFpj5.mjs';
2
2
 
3
3
  declare const createClient: (config?: Config) => Client;
4
4
 
@@ -1,4 +1,4 @@
1
- import { C as Config, a as Client } from '../../types.gen-CIiveH8G.js';
1
+ import { b as Config, C as Client } from '../../types.gen-B0nhFpj5.js';
2
2
 
3
3
  declare const createClient: (config?: Config) => Client;
4
4
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // src/api/core/serverSentEvents.gen.ts
4
- var createSseClient = ({
4
+ function createSseClient({
5
5
  onRequest,
6
6
  onSseError,
7
7
  onSseEvent,
@@ -13,7 +13,7 @@ var createSseClient = ({
13
13
  sseSleepFn,
14
14
  url,
15
15
  ...options
16
- }) => {
16
+ }) {
17
17
  let lastEventId;
18
18
  const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
19
19
  const createStream = async function* () {
@@ -41,10 +41,7 @@ var createSseClient = ({
41
41
  }
42
42
  const _fetch = options.fetch ?? globalThis.fetch;
43
43
  const response = await _fetch(request);
44
- if (!response.ok)
45
- throw new Error(
46
- `SSE failed: ${response.status} ${response.statusText}`
47
- );
44
+ if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
48
45
  if (!response.body) throw new Error("No body in SSE response");
49
46
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
50
47
  let buffer = "";
@@ -60,7 +57,7 @@ var createSseClient = ({
60
57
  const { done, value } = await reader.read();
61
58
  if (done) break;
62
59
  buffer += value;
63
- buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
60
+ buffer = buffer.replace(/\r\n?/g, "\n");
64
61
  const chunks = buffer.split("\n\n");
65
62
  buffer = chunks.pop() ?? "";
66
63
  for (const chunk of chunks) {
@@ -75,10 +72,7 @@ var createSseClient = ({
75
72
  } else if (line.startsWith("id:")) {
76
73
  lastEventId = line.replace(/^id:\s*/, "");
77
74
  } else if (line.startsWith("retry:")) {
78
- const parsed = Number.parseInt(
79
- line.replace(/^retry:\s*/, ""),
80
- 10
81
- );
75
+ const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
82
76
  if (!Number.isNaN(parsed)) {
83
77
  retryDelay = parsed;
84
78
  }
@@ -124,17 +118,14 @@ var createSseClient = ({
124
118
  if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) {
125
119
  break;
126
120
  }
127
- const backoff = Math.min(
128
- retryDelay * 2 ** (attempt - 1),
129
- sseMaxRetryDelay ?? 3e4
130
- );
121
+ const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4);
131
122
  await sleep(backoff);
132
123
  }
133
124
  }
134
125
  };
135
126
  const stream = createStream();
136
127
  return { stream };
137
- };
128
+ }
138
129
 
139
130
  // src/api/core/pathSerializer.gen.ts
140
131
  var separatorArrayExplode = (style) => {
@@ -235,11 +226,7 @@ var serializeObjectParam = ({
235
226
  if (style !== "deepObject" && !explode) {
236
227
  let values = [];
237
228
  Object.entries(value).forEach(([key, v]) => {
238
- values = [
239
- ...values,
240
- key,
241
- allowReserved ? v : encodeURIComponent(v)
242
- ];
229
+ values = [...values, key, allowReserved ? v : encodeURIComponent(v)];
243
230
  });
244
231
  const joinedValues2 = values.join(",");
245
232
  switch (style) {
@@ -290,10 +277,7 @@ var defaultPathSerializer = ({ path, url: _url }) => {
290
277
  continue;
291
278
  }
292
279
  if (Array.isArray(value)) {
293
- url = url.replace(
294
- match,
295
- serializeArrayParam({ explode, name, style, value })
296
- );
280
+ url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
297
281
  continue;
298
282
  }
299
283
  if (typeof value === "object") {
@@ -381,10 +365,7 @@ var getAuthToken = async (auth, callback) => {
381
365
 
382
366
  // src/api/core/bodySerializer.gen.ts
383
367
  var jsonBodySerializer = {
384
- bodySerializer: (body) => JSON.stringify(
385
- body,
386
- (_key, value) => typeof value === "bigint" ? value.toString() : value
387
- )
368
+ bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value)
388
369
  };
389
370
 
390
371
  // src/api/client/utils.gen.ts
@@ -449,9 +430,7 @@ var getParseAs = (contentType) => {
449
430
  if (cleanContent === "multipart/form-data") {
450
431
  return "formData";
451
432
  }
452
- if (["application/", "audio/", "image/", "video/"].some(
453
- (type) => cleanContent.startsWith(type)
454
- )) {
433
+ if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) {
455
434
  return "blob";
456
435
  }
457
436
  if (cleanContent.startsWith("text/")) {
@@ -637,132 +616,121 @@ var createClient = (config = {}) => {
637
616
  if (opts.body === void 0 || opts.serializedBody === "") {
638
617
  opts.headers.delete("Content-Type");
639
618
  }
640
- const url = buildUrl(opts);
641
- return { opts, url };
619
+ const resolvedOpts = opts;
620
+ const url = buildUrl(resolvedOpts);
621
+ return { opts: resolvedOpts, url };
642
622
  };
643
623
  const request = async (options) => {
644
- const { opts, url } = await beforeRequest(options);
645
- const requestInit = {
646
- redirect: "follow",
647
- ...opts,
648
- body: getValidRequestBody(opts)
649
- };
650
- let request2 = new Request(url, requestInit);
651
- for (const fn of interceptors.request.fns) {
652
- if (fn) {
653
- request2 = await fn(request2, opts);
654
- }
655
- }
656
- const _fetch = opts.fetch;
624
+ const throwOnError = options.throwOnError ?? _config.throwOnError;
625
+ const responseStyle = options.responseStyle ?? _config.responseStyle;
626
+ let request2;
657
627
  let response;
658
628
  try {
659
- response = await _fetch(request2);
660
- } catch (error2) {
661
- let finalError2 = error2;
662
- for (const fn of interceptors.error.fns) {
629
+ const { opts, url } = await beforeRequest(options);
630
+ const requestInit = {
631
+ redirect: "follow",
632
+ ...opts,
633
+ body: getValidRequestBody(opts)
634
+ };
635
+ request2 = new Request(url, requestInit);
636
+ for (const fn of interceptors.request.fns) {
663
637
  if (fn) {
664
- finalError2 = await fn(
665
- error2,
666
- void 0,
667
- request2,
668
- opts
669
- );
638
+ request2 = await fn(request2, opts);
670
639
  }
671
640
  }
672
- finalError2 = finalError2 || {};
673
- if (opts.throwOnError) {
674
- throw finalError2;
641
+ const _fetch = opts.fetch;
642
+ response = await _fetch(request2);
643
+ for (const fn of interceptors.response.fns) {
644
+ if (fn) {
645
+ response = await fn(response, request2, opts);
646
+ }
675
647
  }
676
- return opts.responseStyle === "data" ? void 0 : {
677
- error: finalError2,
648
+ const result = {
678
649
  request: request2,
679
- response: void 0
650
+ response
680
651
  };
681
- }
682
- for (const fn of interceptors.response.fns) {
683
- if (fn) {
684
- response = await fn(response, request2, opts);
685
- }
686
- }
687
- const result = {
688
- request: request2,
689
- response
690
- };
691
- if (response.ok) {
692
- const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
693
- if (response.status === 204 || response.headers.get("Content-Length") === "0") {
694
- let emptyData;
652
+ if (response.ok) {
653
+ const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
654
+ if (response.status === 204 || response.headers.get("Content-Length") === "0") {
655
+ let emptyData;
656
+ switch (parseAs) {
657
+ case "arrayBuffer":
658
+ case "blob":
659
+ case "text":
660
+ emptyData = await response[parseAs]();
661
+ break;
662
+ case "formData":
663
+ emptyData = new FormData();
664
+ break;
665
+ case "stream":
666
+ emptyData = response.body;
667
+ break;
668
+ case "json":
669
+ default:
670
+ emptyData = {};
671
+ break;
672
+ }
673
+ return opts.responseStyle === "data" ? emptyData : {
674
+ data: emptyData,
675
+ ...result
676
+ };
677
+ }
678
+ let data;
695
679
  switch (parseAs) {
696
680
  case "arrayBuffer":
697
681
  case "blob":
682
+ case "formData":
698
683
  case "text":
699
- emptyData = await response[parseAs]();
684
+ data = await response[parseAs]();
700
685
  break;
701
- case "formData":
702
- emptyData = new FormData();
686
+ case "json": {
687
+ const text = await response.text();
688
+ data = text ? JSON.parse(text) : {};
703
689
  break;
690
+ }
704
691
  case "stream":
705
- emptyData = response.body;
706
- break;
707
- case "json":
708
- default:
709
- emptyData = {};
710
- break;
692
+ return opts.responseStyle === "data" ? response.body : {
693
+ data: response.body,
694
+ ...result
695
+ };
711
696
  }
712
- return opts.responseStyle === "data" ? emptyData : {
713
- data: emptyData,
697
+ if (parseAs === "json") {
698
+ if (opts.responseValidator) {
699
+ await opts.responseValidator(data);
700
+ }
701
+ if (opts.responseTransformer) {
702
+ data = await opts.responseTransformer(data);
703
+ }
704
+ }
705
+ return opts.responseStyle === "data" ? data : {
706
+ data,
714
707
  ...result
715
708
  };
716
709
  }
717
- let data;
718
- switch (parseAs) {
719
- case "arrayBuffer":
720
- case "blob":
721
- case "formData":
722
- case "json":
723
- case "text":
724
- data = await response[parseAs]();
725
- break;
726
- case "stream":
727
- return opts.responseStyle === "data" ? response.body : {
728
- data: response.body,
729
- ...result
730
- };
710
+ const textError = await response.text();
711
+ let jsonError;
712
+ try {
713
+ jsonError = JSON.parse(textError);
714
+ } catch {
731
715
  }
732
- if (parseAs === "json") {
733
- if (opts.responseValidator) {
734
- await opts.responseValidator(data);
735
- }
736
- if (opts.responseTransformer) {
737
- data = await opts.responseTransformer(data);
716
+ throw jsonError ?? textError;
717
+ } catch (error) {
718
+ let finalError = error;
719
+ for (const fn of interceptors.error.fns) {
720
+ if (fn) {
721
+ finalError = await fn(finalError, response, request2, options);
738
722
  }
739
723
  }
740
- return opts.responseStyle === "data" ? data : {
741
- data,
742
- ...result
743
- };
744
- }
745
- const textError = await response.text();
746
- let jsonError;
747
- try {
748
- jsonError = JSON.parse(textError);
749
- } catch {
750
- }
751
- const error = jsonError ?? textError;
752
- let finalError = error;
753
- for (const fn of interceptors.error.fns) {
754
- if (fn) {
755
- finalError = await fn(error, response, request2, opts);
724
+ finalError = finalError || {};
725
+ if (throwOnError) {
726
+ throw finalError;
756
727
  }
728
+ return responseStyle === "data" ? void 0 : {
729
+ error: finalError,
730
+ request: request2,
731
+ response
732
+ };
757
733
  }
758
- finalError = finalError || {};
759
- if (opts.throwOnError) {
760
- throw finalError;
761
- }
762
- return opts.responseStyle === "data" ? void 0 : {
763
- error: finalError,
764
- ...result
765
- };
766
734
  };
767
735
  const makeMethodFn = (method) => (options) => request({ ...options, method });
768
736
  const makeSseFn = (method) => async (options) => {
@@ -770,7 +738,6 @@ var createClient = (config = {}) => {
770
738
  return createSseClient({
771
739
  ...opts,
772
740
  body: opts.body,
773
- headers: opts.headers,
774
741
  method,
775
742
  onRequest: async (url2, init) => {
776
743
  let request2 = new Request(url2, init);
@@ -785,8 +752,9 @@ var createClient = (config = {}) => {
785
752
  url
786
753
  });
787
754
  };
755
+ const _buildUrl = (options) => buildUrl({ ..._config, ...options });
788
756
  return {
789
- buildUrl,
757
+ buildUrl: _buildUrl,
790
758
  connect: makeMethodFn("CONNECT"),
791
759
  delete: makeMethodFn("DELETE"),
792
760
  get: makeMethodFn("GET"),