@vidos-id/openid4vc-wallet 0.10.1 → 0.11.0

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
@@ -43,13 +43,17 @@ This package is currently published as raw TypeScript and is intended for Bun-ba
43
43
  - `openid4vp://` authorization URL parsing for by-value DCQL requests
44
44
  - selective disclosure presentation building
45
45
  - KB-JWT holder binding
46
- - `direct_post` and `direct_post.jwt` authorization response submission
46
+ - prepared `direct_post` and `direct_post.jwt` authorization response delivery
47
47
 
48
48
  ## Example
49
49
 
50
50
  ```ts
51
51
  import {
52
+ createOpenId4VpAuthorizationResponse,
52
53
  InMemoryWalletStorage,
54
+ parseOpenid4VpAuthorizationUrl,
55
+ prepareOpenId4VpAuthorizationResponseSubmission,
56
+ submitPreparedOpenId4VpAuthorizationResponse,
53
57
  Wallet,
54
58
  receiveCredentialFromOffer,
55
59
  } from "@vidos-id/openid4vc-wallet";
@@ -92,9 +96,11 @@ await receiveCredentialFromOffer(
92
96
  const status = await wallet.getCredentialStatus("credential-id");
93
97
 
94
98
  // Create a presentation from a DCQL request
95
- const presentation = await wallet.createPresentation({
99
+ const presentationRequest = {
96
100
  client_id: "https://verifier.example",
97
101
  nonce: "nonce-123",
102
+ response_mode: "direct_post",
103
+ response_uri: "https://verifier.example/response",
98
104
  dcql_query: {
99
105
  credentials: [
100
106
  {
@@ -104,12 +110,53 @@ const presentation = await wallet.createPresentation({
104
110
  },
105
111
  ],
106
112
  },
107
- });
113
+ };
114
+
115
+ const presentation = await wallet.createPresentation(presentationRequest);
116
+
117
+ const authorizationResponse = createOpenId4VpAuthorizationResponse(
118
+ presentationRequest,
119
+ presentation,
120
+ );
121
+
122
+ const preparedSubmission =
123
+ await prepareOpenId4VpAuthorizationResponseSubmission(
124
+ presentationRequest,
125
+ authorizationResponse,
126
+ );
127
+
128
+ // Default path: submit exactly as prepared.
129
+ await submitPreparedOpenId4VpAuthorizationResponse(preparedSubmission);
130
+
131
+ // Local/e2e path: rewrite the destination or deliver in-process.
132
+ await submitPreparedOpenId4VpAuthorizationResponse(
133
+ {
134
+ ...preparedSubmission,
135
+ url: "http://127.0.0.1:3000/response",
136
+ },
137
+ {
138
+ transport: async (submission) => {
139
+ const body = Object.fromEntries(submission.body.entries());
140
+ console.log(submission.url, body.vp_token);
141
+ return Response.json({ redirect_uri: "http://localhost:3000/done" });
142
+ },
143
+ },
144
+ );
108
145
 
109
146
  // Parse an openid4vp:// authorization URL
110
- const request = Wallet.parseAuthorizationRequestUrl("openid4vp://authorize?...");
147
+ const request = await parseOpenid4VpAuthorizationUrl("openid4vp://authorize?...");
111
148
  ```
112
149
 
150
+ OID4VP response delivery flow:
151
+ - `createOpenId4VpAuthorizationResponse(...)` builds the protocol response payload
152
+ - `prepareOpenId4VpAuthorizationResponseSubmission(...)` builds the HTTP submission request
153
+ - `submitPreparedOpenId4VpAuthorizationResponse(...)` sends the prepared request with either the default fetch transport or a caller-provided transport
154
+
155
+ This keeps protocol construction in the wallet while letting callers:
156
+ - inspect the exact outgoing request before submission
157
+ - rewrite the destination URL for localhost or reverse-proxy setups
158
+ - submit in-process during tests without standing up a network proxy
159
+
113
160
  Supported `openid4vp://` subset:
114
161
  - by-value only
115
162
  - requires `client_id`, `nonce`, and `dcql_query`
@@ -144,7 +191,7 @@ Current limitations:
144
191
  ## See also
145
192
 
146
193
  - [`@vidos-id/openid4vc-issuer`](../issuer/) - issuer library for credential issuance
147
- - [`scripts/demo-e2e.ts`](../../scripts/demo-e2e.ts) - full programmatic flow using both libraries
194
+ - [`scripts/demo-e2e.ts`](../../scripts/demo-e2e.ts) - full programmatic flow including prepared OID4VP submission with local transport rewriting
148
195
 
149
196
  ## Test
150
197
 
package/dist/index.d.mts CHANGED
@@ -310,21 +310,38 @@ declare function receiveCredentialFromOffer(wallet: Wallet, offerInput: unknown,
310
310
  fetch?: typeof fetch;
311
311
  }): Promise<StoredCredentialRecord>;
312
312
  //#endregion
313
- //#region src/openid4vp.d.ts
313
+ //#region src/openid4vp/request.d.ts
314
+ declare function parseOpenid4VpAuthorizationUrl(input: string): Promise<OpenId4VpRequestInput>;
315
+ declare function resolveOpenId4VpRequest(input: unknown): Promise<OpenId4VpRequestInput>;
316
+ //#endregion
317
+ //#region src/openid4vp/response.d.ts
314
318
  type OpenId4VpAuthorizationResponse = {
315
319
  vp_token: string;
316
320
  state?: string;
317
321
  };
322
+ declare function createOpenId4VpAuthorizationResponse(request: OpenId4VpRequestInput, presentation: CreatePresentationResult): OpenId4VpAuthorizationResponse;
323
+ //#endregion
324
+ //#region src/openid4vp/submission.d.ts
325
+ type PreparedOpenId4VpAuthorizationResponseSubmission = {
326
+ responseMode: ResponseMode;
327
+ url: string;
328
+ method: "POST";
329
+ headers: Record<string, string>;
330
+ body: URLSearchParams;
331
+ };
332
+ type OpenId4VpResponseTransport = (submission: PreparedOpenId4VpAuthorizationResponseSubmission) => Promise<Response>;
318
333
  type OpenId4VpResponseSubmissionResult = {
319
- responseMode: "direct_post" | "direct_post.jwt";
320
- responseUri: string;
334
+ responseMode: ResponseMode;
335
+ url: string;
321
336
  status: number;
322
337
  body?: unknown;
323
338
  redirectUri?: string;
324
339
  };
325
- declare function parseOpenid4VpAuthorizationUrl(input: string): Promise<OpenId4VpRequestInput>;
326
- declare function resolveOpenId4VpRequest(input: unknown): Promise<OpenId4VpRequestInput>;
327
- declare function createOpenId4VpAuthorizationResponse(request: OpenId4VpRequestInput, presentation: CreatePresentationResult): OpenId4VpAuthorizationResponse;
328
- declare function submitOpenId4VpAuthorizationResponse(request: OpenId4VpRequestInput, response: OpenId4VpAuthorizationResponse): Promise<OpenId4VpResponseSubmissionResult>;
340
+ type ResponseMode = OpenId4VpRequestInput["response_mode"] extends infer T ? Exclude<T, undefined> : never;
341
+ declare function prepareOpenId4VpAuthorizationResponseSubmission(request: OpenId4VpRequestInput, response: OpenId4VpAuthorizationResponse): Promise<PreparedOpenId4VpAuthorizationResponseSubmission>;
342
+ declare function submitPreparedOpenId4VpAuthorizationResponse(submission: PreparedOpenId4VpAuthorizationResponseSubmission, options?: {
343
+ transport?: OpenId4VpResponseTransport;
344
+ }): Promise<OpenId4VpResponseSubmissionResult>;
345
+ declare function defaultOpenId4VpResponseTransport(submission: PreparedOpenId4VpAuthorizationResponseSubmission): Promise<Response>;
329
346
  //#endregion
330
- export { CreatePresentationResult, CredentialStatus, CredentialStatusListReference, CredentialStatusListReferenceSchema, CredentialStatusSchema, HOLDER_KEY_ALG, HolderKeyRecord, HolderKeyRecordSchema, ImportCredentialInput, ImportCredentialInputSchema, InMemoryWalletStorage, InspectDcqlQueryResult, IssuerJwkSchema, IssuerJwksSchema, IssuerKeyMaterial, IssuerKeyMaterialSchema, JwkSchema, MatchDcqlQueryResult, MatchedCredential, OpenId4VciCredentialOffer, OpenId4VciIssuerMetadata, OpenId4VpAuthorizationResponse, OpenId4VpRequestInput, OpenId4VpRequestSchema, OpenId4VpResponseSubmissionResult, ParsedDcqlQuery, QueryCredentialMatches, ResolvedCredentialStatus, ResponseModeSchema, SD_JWT_HASH_ALG, StoredCredentialRecord, StoredCredentialRecordSchema, TokenStatusLabel, VerifierClientMetadata, VerifierClientMetadataSchema, Wallet, WalletConfigSchema, WalletError, WalletStorage, createHolderKeyRecord, createKbJwt, createOpenId4VciProofJwt, createOpenId4VpAuthorizationResponse, fetchIssuerMetadata, getJwkThumbprint, importPrivateKey, importPublicKey, issueDemoCredential, parseCredentialOffer, parseOpenid4VpAuthorizationUrl, receiveCredentialFromOffer, resolveOpenId4VpRequest, sdJwtHasher, sha256Base64Url, submitOpenId4VpAuthorizationResponse };
347
+ export { CreatePresentationResult, CredentialStatus, CredentialStatusListReference, CredentialStatusListReferenceSchema, CredentialStatusSchema, HOLDER_KEY_ALG, HolderKeyRecord, HolderKeyRecordSchema, ImportCredentialInput, ImportCredentialInputSchema, InMemoryWalletStorage, InspectDcqlQueryResult, IssuerJwkSchema, IssuerJwksSchema, IssuerKeyMaterial, IssuerKeyMaterialSchema, JwkSchema, MatchDcqlQueryResult, MatchedCredential, OpenId4VciCredentialOffer, OpenId4VciIssuerMetadata, OpenId4VpAuthorizationResponse, OpenId4VpRequestInput, OpenId4VpRequestSchema, OpenId4VpResponseSubmissionResult, OpenId4VpResponseTransport, ParsedDcqlQuery, PreparedOpenId4VpAuthorizationResponseSubmission, QueryCredentialMatches, ResolvedCredentialStatus, ResponseModeSchema, SD_JWT_HASH_ALG, StoredCredentialRecord, StoredCredentialRecordSchema, TokenStatusLabel, VerifierClientMetadata, VerifierClientMetadataSchema, Wallet, WalletConfigSchema, WalletError, WalletStorage, createHolderKeyRecord, createKbJwt, createOpenId4VciProofJwt, createOpenId4VpAuthorizationResponse, defaultOpenId4VpResponseTransport, fetchIssuerMetadata, getJwkThumbprint, importPrivateKey, importPublicKey, issueDemoCredential, parseCredentialOffer, parseOpenid4VpAuthorizationUrl, prepareOpenId4VpAuthorizationResponseSubmission, receiveCredentialFromOffer, resolveOpenId4VpRequest, sdJwtHasher, sha256Base64Url, submitPreparedOpenId4VpAuthorizationResponse };
package/dist/index.mjs CHANGED
@@ -771,7 +771,7 @@ function getSingleSearchParam$1(url, key) {
771
771
  return values[0] || void 0;
772
772
  }
773
773
  //#endregion
774
- //#region src/openid4vp.ts
774
+ //#region src/openid4vp/request.ts
775
775
  const openid4vpAuthorizationUrlSchema = z.string().min(1);
776
776
  const requestObjectHeaderSchema = z.object({ typ: z.literal("oauth-authz-req+jwt") });
777
777
  const requestObjectClaimsSchema = z.object({
@@ -860,38 +860,6 @@ async function resolveOpenId4VpRequest(input) {
860
860
  presentation_definition: request.presentation_definition ?? requestObject?.presentation_definition
861
861
  });
862
862
  }
863
- function createOpenId4VpAuthorizationResponse(request, presentation) {
864
- const parsedRequest = OpenId4VpRequestSchema.parse(request);
865
- return {
866
- vp_token: presentation.vpToken,
867
- state: parsedRequest.state
868
- };
869
- }
870
- async function submitOpenId4VpAuthorizationResponse(request, response) {
871
- const parsedRequest = OpenId4VpRequestSchema.parse(request);
872
- if (!parsedRequest.response_mode) throw new WalletError("response_mode is required for submission");
873
- const responseUrl = parseHttpsUrl(parsedRequest.response_uri, "response_uri must use https");
874
- const body = parsedRequest.response_mode === "direct_post" ? createDirectPostBody(response) : await createDirectPostJwtBody(parsedRequest, response);
875
- let fetchResponse;
876
- try {
877
- fetchResponse = await fetch(responseUrl, {
878
- method: "POST",
879
- headers: { "content-type": "application/x-www-form-urlencoded" },
880
- body
881
- });
882
- } catch {
883
- throw new WalletError("Failed to submit authorization response");
884
- }
885
- const parsedBody = await parseSubmissionBody(fetchResponse);
886
- const redirectUri = readRedirectUri(parsedBody);
887
- return {
888
- responseMode: parsedRequest.response_mode,
889
- responseUri: responseUrl.toString(),
890
- status: fetchResponse.status,
891
- body: parsedBody,
892
- redirectUri
893
- };
894
- }
895
863
  async function fetchRequestObject(requestUri, clientId) {
896
864
  let url;
897
865
  try {
@@ -945,6 +913,84 @@ function parseClientMetadata(value) {
945
913
  vp_formats_supported: z.unknown().optional()
946
914
  }).passthrough().parse(value);
947
915
  }
916
+ function assertRequestUriMatchesClientId(url, clientId) {
917
+ const clientIdHostname = getClientIdHostname(clientId);
918
+ if (!clientIdHostname) return;
919
+ if (url.hostname !== clientIdHostname) throw new WalletError("request_uri hostname must match client_id hostname");
920
+ }
921
+ function getClientIdHostname(clientId) {
922
+ if (!clientId) return;
923
+ try {
924
+ return new URL(clientId).hostname;
925
+ } catch {}
926
+ if (clientId.startsWith("x509_san_dns:")) return clientId.slice(13) || void 0;
927
+ const separator = clientId.indexOf(":");
928
+ if (separator > 0) try {
929
+ return new URL(clientId.slice(separator + 1)).hostname;
930
+ } catch {
931
+ return;
932
+ }
933
+ }
934
+ function getSingleSearchParam(url, key) {
935
+ const values = url.searchParams.getAll(key);
936
+ if (values.length === 0 || values[0]?.length === 0) throw new WalletError(`Authorization URL is missing ${key}`);
937
+ if (values.length > 1) throw new WalletError(`Authorization URL must include only one ${key}`);
938
+ return values[0];
939
+ }
940
+ function getOptionalSingleSearchParam(url, key) {
941
+ const values = url.searchParams.getAll(key);
942
+ if (values.length === 0) return;
943
+ if (values.length > 1) throw new WalletError(`Authorization URL must include only one ${key}`);
944
+ return values[0] || void 0;
945
+ }
946
+ //#endregion
947
+ //#region src/openid4vp/response.ts
948
+ function createOpenId4VpAuthorizationResponse(request, presentation) {
949
+ const parsedRequest = OpenId4VpRequestSchema.parse(request);
950
+ return {
951
+ vp_token: presentation.vpToken,
952
+ state: parsedRequest.state
953
+ };
954
+ }
955
+ //#endregion
956
+ //#region src/openid4vp/submission.ts
957
+ async function prepareOpenId4VpAuthorizationResponseSubmission(request, response) {
958
+ const parsedRequest = OpenId4VpRequestSchema.parse(request);
959
+ if (!parsedRequest.response_mode) throw new WalletError("response_mode is required for submission");
960
+ const responseUrl = parseHttpsUrl(parsedRequest.response_uri, "response_uri must use https");
961
+ const body = parsedRequest.response_mode === "direct_post" ? createDirectPostBody(response) : await createDirectPostJwtBody(parsedRequest, response);
962
+ return {
963
+ responseMode: parsedRequest.response_mode,
964
+ url: responseUrl.toString(),
965
+ method: "POST",
966
+ headers: { "content-type": "application/x-www-form-urlencoded" },
967
+ body
968
+ };
969
+ }
970
+ async function submitPreparedOpenId4VpAuthorizationResponse(submission, options) {
971
+ const transport = options?.transport ?? defaultOpenId4VpResponseTransport;
972
+ let response;
973
+ try {
974
+ response = await transport(submission);
975
+ } catch {
976
+ throw new WalletError("Failed to submit authorization response");
977
+ }
978
+ const parsedBody = await parseSubmissionBody(response);
979
+ return {
980
+ responseMode: submission.responseMode,
981
+ url: submission.url,
982
+ status: response.status,
983
+ body: parsedBody,
984
+ redirectUri: readRedirectUri(parsedBody)
985
+ };
986
+ }
987
+ function defaultOpenId4VpResponseTransport(submission) {
988
+ return fetch(submission.url, {
989
+ method: submission.method,
990
+ headers: submission.headers,
991
+ body: submission.body
992
+ });
993
+ }
948
994
  function parseHttpsUrl(value, errorMessage) {
949
995
  if (!value) throw new WalletError(errorMessage);
950
996
  let url;
@@ -975,10 +1021,7 @@ async function encryptAuthorizationResponse(request, response) {
975
1021
  alg,
976
1022
  enc,
977
1023
  typ: "oauth-authz-resp+jwt"
978
- }).setAudience(parsedRequest.client_id).setIssuedAt().encrypt(await importEncryptionKey(jwk, alg));
979
- }
980
- async function importEncryptionKey(jwk, alg) {
981
- return importJWK(jwk, alg);
1024
+ }).setAudience(parsedRequest.client_id).setIssuedAt().encrypt(await importJWK(jwk, alg));
982
1025
  }
983
1026
  function resolveJweAlg(jwk) {
984
1027
  if (typeof jwk.alg === "string" && jwk.alg.length > 0) return jwk.alg;
@@ -998,36 +1041,6 @@ function readRedirectUri(body) {
998
1041
  const value = body.redirect_uri;
999
1042
  return typeof value === "string" && value.length > 0 ? value : void 0;
1000
1043
  }
1001
- function assertRequestUriMatchesClientId(url, clientId) {
1002
- const clientIdHostname = getClientIdHostname(clientId);
1003
- if (!clientIdHostname) return;
1004
- if (url.hostname !== clientIdHostname) throw new WalletError("request_uri hostname must match client_id hostname");
1005
- }
1006
- function getClientIdHostname(clientId) {
1007
- if (!clientId) return;
1008
- try {
1009
- return new URL(clientId).hostname;
1010
- } catch {}
1011
- if (clientId.startsWith("x509_san_dns:")) return clientId.slice(13) || void 0;
1012
- const separator = clientId.indexOf(":");
1013
- if (separator > 0) try {
1014
- return new URL(clientId.slice(separator + 1)).hostname;
1015
- } catch {
1016
- return;
1017
- }
1018
- }
1019
- function getSingleSearchParam(url, key) {
1020
- const values = url.searchParams.getAll(key);
1021
- if (values.length === 0 || values[0]?.length === 0) throw new WalletError(`Authorization URL is missing ${key}`);
1022
- if (values.length > 1) throw new WalletError(`Authorization URL must include only one ${key}`);
1023
- return values[0];
1024
- }
1025
- function getOptionalSingleSearchParam(url, key) {
1026
- const values = url.searchParams.getAll(key);
1027
- if (values.length === 0) return;
1028
- if (values.length > 1) throw new WalletError(`Authorization URL must include only one ${key}`);
1029
- return values[0] || void 0;
1030
- }
1031
1044
  //#endregion
1032
1045
  //#region src/storage.ts
1033
1046
  var InMemoryWalletStorage = class {
@@ -1052,4 +1065,4 @@ var InMemoryWalletStorage = class {
1052
1065
  }
1053
1066
  };
1054
1067
  //#endregion
1055
- export { CredentialStatusListReferenceSchema, CredentialStatusSchema, HOLDER_KEY_ALG, HolderKeyRecordSchema, ImportCredentialInputSchema, InMemoryWalletStorage, IssuerJwkSchema, IssuerJwksSchema, IssuerKeyMaterialSchema, JwkSchema, OpenId4VpRequestSchema, ResponseModeSchema, SD_JWT_HASH_ALG, StoredCredentialRecordSchema, VerifierClientMetadataSchema, Wallet, WalletConfigSchema, WalletError, createHolderKeyRecord, createKbJwt, createOpenId4VciProofJwt, createOpenId4VpAuthorizationResponse, fetchIssuerMetadata, getJwkThumbprint, importPrivateKey, importPublicKey, issueDemoCredential, parseCredentialOffer, parseOpenid4VpAuthorizationUrl, receiveCredentialFromOffer, resolveOpenId4VpRequest, sdJwtHasher, sha256Base64Url, submitOpenId4VpAuthorizationResponse };
1068
+ export { CredentialStatusListReferenceSchema, CredentialStatusSchema, HOLDER_KEY_ALG, HolderKeyRecordSchema, ImportCredentialInputSchema, InMemoryWalletStorage, IssuerJwkSchema, IssuerJwksSchema, IssuerKeyMaterialSchema, JwkSchema, OpenId4VpRequestSchema, ResponseModeSchema, SD_JWT_HASH_ALG, StoredCredentialRecordSchema, VerifierClientMetadataSchema, Wallet, WalletConfigSchema, WalletError, createHolderKeyRecord, createKbJwt, createOpenId4VciProofJwt, createOpenId4VpAuthorizationResponse, defaultOpenId4VpResponseTransport, fetchIssuerMetadata, getJwkThumbprint, importPrivateKey, importPublicKey, issueDemoCredential, parseCredentialOffer, parseOpenid4VpAuthorizationUrl, prepareOpenId4VpAuthorizationResponseSubmission, receiveCredentialFromOffer, resolveOpenId4VpRequest, sdJwtHasher, sha256Base64Url, submitPreparedOpenId4VpAuthorizationResponse };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vidos-id/openid4vc-wallet",
3
3
  "description": "Wallet library for dc+sd-jwt storage and OpenID4VP presentation.",
4
- "version": "0.10.1",
4
+ "version": "0.11.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vidos-id/openid4vc-tools.git",