1
1
/*!
2
- * Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2020-2025 Digital Bazaar, Inc. All rights reserved.
3
3
*/
4
4
import * as bedrock from '@bedrock/core' ;
5
5
import * as schemas from '../schemas/bedrock-profile-http.js' ;
6
+ import { poll , pollers } from '@bedrock/notify' ;
7
+ import { agent } from '@bedrock/https-agent' ;
6
8
import { asyncHandler } from '@bedrock/express' ;
7
9
import { ensureAuthenticated } from '@bedrock/passport' ;
8
- import { LRUCache } from 'lru-cache ' ;
10
+ import { httpClient } from '@digitalbazaar/http-client ' ;
9
11
import { createValidateMiddleware as validate } from '@bedrock/validation' ;
10
- import { ZCAP_CLIENT } from './zcapClient.js' ;
12
+ import { ZCAP_CLIENT as zcapClient } from './zcapClient.js' ;
11
13
12
14
const { config, util : { BedrockError} } = bedrock ;
13
15
14
16
let WORKFLOWS_BY_NAME_MAP ;
15
17
let WORKFLOWS_BY_ID_MAP ;
16
- let EXCHANGE_POLLING_CACHE ;
17
18
18
19
bedrock . events . on ( 'bedrock.init' , ( ) => {
19
20
const cfg = config [ 'profile-http' ] ;
@@ -52,10 +53,6 @@ bedrock.events.on('bedrock.init', () => {
52
53
WORKFLOWS_BY_NAME_MAP . set ( workflowName , workflow ) ;
53
54
WORKFLOWS_BY_ID_MAP . set ( localInteractionId , workflow ) ;
54
55
}
55
-
56
- // setup caches
57
- const { caches} = interactions ;
58
- EXCHANGE_POLLING_CACHE = new LRUCache ( caches . exchangePolling ) ;
59
56
} ) ;
60
57
61
58
bedrock . events . on ( 'bedrock-express.configure.routes' , app => {
@@ -73,8 +70,6 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
73
70
interaction : `${ interactionsPath } /:localInteractionId/:localExchangeId`
74
71
} ;
75
72
76
- const retryAfter = Math . ceil ( EXCHANGE_POLLING_CACHE . ttl / 1000 ) ;
77
-
78
73
// create an interaction to exchange VCs
79
74
app . post (
80
75
routes . interactions ,
@@ -97,6 +92,7 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
97
92
98
93
// create exchange with given variables
99
94
const exchange = {
95
+ // FIXME: use `expires` instead of now-deprecated `ttl`
100
96
// 15 minute expiry in seconds
101
97
ttl : 60 * 15 ,
102
98
// template variables
@@ -106,7 +102,7 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
106
102
}
107
103
} ;
108
104
const capability = workflow . zcaps . get ( 'readWriteExchanges' ) ;
109
- const response = await ZCAP_CLIENT . write ( { json : exchange , capability} ) ;
105
+ const response = await zcapClient . write ( { json : exchange , capability} ) ;
110
106
const exchangeId = response . headers . get ( 'location' ) ;
111
107
const { localInteractionId} = workflow ;
112
108
// reuse `localExchangeId` in path
@@ -120,9 +116,13 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
120
116
app . get (
121
117
routes . interaction ,
122
118
ensureAuthenticated ,
119
+ // FIXME: add URL query validator that requires no query or `iuv=1` only
123
120
asyncHandler ( async ( req , res ) => {
124
121
const { id : accountId } = req . user . account || { } ;
125
- const { localInteractionId, localExchangeId} = req . params ;
122
+ const {
123
+ params : { localInteractionId, localExchangeId} ,
124
+ query : { iuv}
125
+ } = req ;
126
126
127
127
const workflow = WORKFLOWS_BY_ID_MAP . get ( localInteractionId ) ;
128
128
if ( ! workflow ) {
@@ -136,34 +136,66 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
136
136
} ) ;
137
137
}
138
138
139
- // check if entry is in exchange polling cache, if so, return
140
- // "too many requests" error response
141
- const key = `${ localExchangeId } /${ localExchangeId } ` ;
142
- if ( EXCHANGE_POLLING_CACHE . get ( key ) ) {
143
- res . set ( 'retry-after' , retryAfter ) ;
144
- res . status ( 429 ) . json ( { retryAfter} ) ;
145
- return ;
139
+ // determine full exchange ID based on related capability
140
+ const capability = workflow . zcaps . get ( 'readWriteExchanges' ) ;
141
+ const exchangeId = `${ capability . invocationTarget } /${ localExchangeId } ` ;
142
+
143
+ // if an "Interaction URL Version" is present send "protocols"
144
+ // (note: validation requires it to be `1`, so no need to check its value)
145
+ if ( iuv ) {
146
+ // FIXME: send to a QR-code page if supported
147
+ // FIXME: check config for supported QR code route and use it
148
+ // instead of hard-coded value
149
+ if ( req . accepts ( 'html' ) || ! req . accepts ( 'json' ) ) {
150
+ return res . redirect ( `${ req . originalUrl } /qr-code` ) ;
151
+ }
152
+ try {
153
+ const url = `${ exchangeId } /protocols` ;
154
+ const { data : protocols } = await httpClient . get ( url , { agent} ) ;
155
+ res . json ( protocols ) ;
156
+ } catch ( cause ) {
157
+ throw new BedrockError (
158
+ 'Unable to serve protocols object: ' + cause . message , {
159
+ name : 'OperationError' ,
160
+ details : { httpStatusCode : 500 , public : true } ,
161
+ cause
162
+ } ) ;
163
+ }
146
164
}
147
165
148
- // fetch exchange
149
- const capability = workflow . zcaps . get ( 'readWriteExchanges' ) ;
150
- const response = await ZCAP_CLIENT . read ( {
151
- url : `${ capability . invocationTarget } /${ localExchangeId } ` ,
152
- capability
166
+ // poll the exchange...
167
+ const result = await poll ( {
168
+ id : exchangeId ,
169
+ poller : pollers . createExchangePoller ( {
170
+ zcapClient,
171
+ capability,
172
+ filterExchange ( { exchange/*, previousPollResult*/ } ) {
173
+ // ensure `accountId` matches exchange variables
174
+ if ( exchange ?. variables . accountId !== accountId ) {
175
+ throw new BedrockError (
176
+ 'Not authorized.' ,
177
+ 'NotAllowedError' ,
178
+ { httpStatusCode : 403 , public : true } ) ;
179
+ }
180
+ // return only information that should be accessible to client
181
+ return {
182
+ exchange
183
+ // FIXME: filter info once final step name and info is determined
184
+ /*
185
+ exchange: {
186
+ state: exchange.state,
187
+ result: exchange.variables.results?.finish
188
+ }*/
189
+ } ;
190
+ }
191
+ } ) ,
192
+ // set a TTL of 1 seconds to account for the case where a push
193
+ // notification isn't received by the same instance that the client
194
+ // hits, but prevent requests from triggering a hit to the backend more
195
+ // frequently than 1 second
196
+ ttl : 1000
153
197
} ) ;
154
198
155
- // prevent more polling on the same exchange for another second
156
- EXCHANGE_POLLING_CACHE . set ( key , true ) ;
157
-
158
- // ensure `accountId` matches exchange variables
159
- const { exchange : { state, variables} } = response ;
160
- if ( variables . accountId !== accountId ) {
161
- throw new BedrockError (
162
- 'The "account" is not authorized.' ,
163
- 'NotAllowedError' ,
164
- { httpStatusCode : 403 , public : true } ) ;
165
- }
166
-
167
- res . json ( { exchange : { state, variables} } ) ;
199
+ res . json ( result ) ;
168
200
} ) ) ;
169
201
} ) ;
0 commit comments