@@ -3,13 +3,85 @@ import { AuthorizationRequest } from '../authorization-request'
33import { IDToken } from '../id-token'
44import { RequestObject } from '../request-object'
55import { assertValidResponseOpts } from './Opts'
6- import { AuthorizationRequestPayload , AuthorizationResponsePayload , IDTokenPayload , SIOPErrors } from '../types'
6+ import {
7+ AuthorizationRequestPayload ,
8+ AuthorizationResponsePayload ,
9+ DcqlPresentationEntry ,
10+ DcqlVpToken ,
11+ DcqlVpTokenInput ,
12+ IDTokenPayload ,
13+ NonEmptyArray ,
14+ SIOPErrors
15+ } from '../types'
716import { AuthorizationResponseOpts } from './types'
817
18+
19+ /**
20+ * Checks if an object is array-like (has only numeric string keys: "0", "1", "2", etc.)
21+ * This handles objects that were serialized arrays: { "0": val1, "1": val2 }
22+ */
23+ const isArrayLikeObject = ( value : unknown ) : value is Record < string , DcqlPresentationEntry > => {
24+ if ( ! value || typeof value !== 'object' || Array . isArray ( value ) ) {
25+ return false
26+ }
27+ const keys = Object . keys ( value )
28+ return keys . length > 0 && keys . every ( key => / ^ \d + $ / . test ( key ) )
29+ }
30+
31+ /**
32+ * Normalizes a credential query value to an array.
33+ * Handles three input formats:
34+ * 1. Single value: "credential" -> ["credential"]
35+ * 2. Array: ["cred1", "cred2"] -> ["cred1", "cred2"]
36+ * 3. Array-like object: {"0": "cred1", "1": "cred2"} -> ["cred1", "cred2"]
37+ */
38+ const normalizeToArray = (
39+ credentialQueryId : string ,
40+ value : DcqlPresentationEntry | DcqlPresentationEntry [ ] | Record < string , DcqlPresentationEntry >
41+ ) : NonEmptyArray < DcqlPresentationEntry > => {
42+ let presentationsArray : DcqlPresentationEntry [ ]
43+
44+ if ( Array . isArray ( value ) ) {
45+ presentationsArray = value
46+ } else if ( isArrayLikeObject ( value ) ) {
47+ const sortedKeys = Object . keys ( value ) . sort ( ( a , b ) => Number ( a ) - Number ( b ) )
48+ presentationsArray = sortedKeys . map ( key => value [ key ] )
49+ } else {
50+ presentationsArray = [ value ]
51+ }
52+
53+ if ( presentationsArray . length === 0 ) {
54+ throw new Error (
55+ `DCQL presentations for credential query '${ credentialQueryId } ' cannot be empty`
56+ )
57+ }
58+
59+ return presentationsArray as NonEmptyArray < DcqlPresentationEntry >
60+ }
61+
62+ /**
63+ * Converts a DCQL presentation input (which may have mixed formats) to the canonical
64+ * format where all credential queries map to non-empty arrays of presentations.
65+ *
66+ * This ensures consistent handling of:
67+ * - Single presentations: { "PID": "eyJ..." } -> { "PID": ["eyJ..."] }
68+ * - Array presentations: { "PID": ["eyJ..."] } -> { "PID": ["eyJ..."] }
69+ * - Array-like objects: { "PID": {"0": "eyJ..."} } -> { "PID": ["eyJ..."] }
70+ */
71+ const toCanonicalDcqlPresentation = ( input : DcqlVpTokenInput ) : DcqlVpToken => {
72+ return Object . fromEntries (
73+ Object . entries ( input ) . map ( ( [ credentialQueryId , value ] ) => {
74+ const presentationsArray = normalizeToArray ( credentialQueryId , value )
75+ return [ credentialQueryId , presentationsArray ]
76+ } )
77+ ) as DcqlVpToken
78+ }
79+
80+
981export const createResponsePayload = async (
1082 authorizationRequest : AuthorizationRequest ,
1183 responseOpts : AuthorizationResponseOpts ,
12- idTokenPayload ?: IDTokenPayload ,
84+ idTokenPayload ?: IDTokenPayload
1385) : Promise < AuthorizationResponsePayload | undefined > => {
1486 assertValidResponseOpts ( responseOpts )
1587 if ( ! authorizationRequest ) {
@@ -20,15 +92,21 @@ export const createResponsePayload = async (
2092 const state : string | undefined = authorizationRequest . getMergedProperty ( 'state' )
2193
2294 const responsePayload : AuthorizationResponsePayload = {
23- ...( responseOpts . accessToken && { access_token : responseOpts . accessToken , expires_in : responseOpts . expiresIn || 3600 } ) ,
95+ ...( responseOpts . accessToken && {
96+ access_token : responseOpts . accessToken ,
97+ expires_in : responseOpts . expiresIn || 3600
98+ } ) ,
2499 ...( responseOpts . tokenType && { token_type : responseOpts . tokenType } ) ,
25100 ...( responseOpts . refreshToken && { refresh_token : responseOpts . refreshToken } ) ,
26101 ...( responseOpts . isFirstParty && { is_first_party : responseOpts . isFirstParty } ) ,
27- state,
102+ state
28103 }
29104
30105 if ( responseOpts . dcqlResponse ?. dcqlPresentation ) {
31- responsePayload . vp_token = DcqlPresentation . encode ( responseOpts . dcqlResponse . dcqlPresentation as DcqlPresentation )
106+ const canonicalPresentation = toCanonicalDcqlPresentation (
107+ responseOpts . dcqlResponse . dcqlPresentation
108+ )
109+ responsePayload . vp_token = DcqlPresentation . encode ( canonicalPresentation )
32110 }
33111
34112 if ( idTokenPayload ) {
@@ -46,7 +124,7 @@ export const createResponsePayload = async (
46124 */
47125export const mergeOAuth2AndOpenIdInRequestPayload = async (
48126 payload : AuthorizationRequestPayload ,
49- requestObject ?: RequestObject ,
127+ requestObject ?: RequestObject
50128) : Promise < AuthorizationRequestPayload > => {
51129 const payloadCopy = JSON . parse ( JSON . stringify ( payload ) )
52130
0 commit comments