@bedrock/vc-delivery 7.11.2 → 7.12.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/lib/ExchangeProcessor.js +577 -0
- package/lib/helpers.js +13 -77
- package/lib/inviteRequest/inviteRequest.js +22 -72
- package/lib/issue.js +35 -23
- package/lib/oid4/authorizationRequest.js +10 -2
- package/lib/oid4/authorizationResponse.js +20 -31
- package/lib/oid4/http.js +1 -1
- package/lib/oid4/oid4vci.js +59 -81
- package/lib/oid4/oid4vp.js +180 -230
- package/lib/vcapi.js +15 -230
- package/lib/verify.js +10 -8
- package/package.json +2 -2
- package/schemas/bedrock-vc-workflow.js +8 -0
package/lib/oid4/oid4vp.js
CHANGED
|
@@ -2,22 +2,17 @@
|
|
|
2
2
|
* Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import * as bedrock from '@bedrock/core';
|
|
5
|
-
import * as exchanges from '../storage/exchanges.js';
|
|
6
5
|
import {
|
|
7
|
-
buildPresentationFromResults,
|
|
8
|
-
buildVerifyPresentationResults,
|
|
9
|
-
emitExchangeUpdated,
|
|
10
6
|
evaluateExchangeStep,
|
|
11
7
|
resolveVariableName,
|
|
12
8
|
setVariable
|
|
13
9
|
} from '../helpers.js';
|
|
14
10
|
import {getClientBaseUrl, getClientProfile} from './clientProfiles.js';
|
|
15
|
-
import {compile} from '@bedrock/validation';
|
|
16
11
|
import {create as createAuthorizationRequest} from './authorizationRequest.js';
|
|
17
|
-
import {
|
|
12
|
+
import {verify as defaultVerify} from '../verify.js';
|
|
13
|
+
import {ExchangeProcessor} from '../ExchangeProcessor.js';
|
|
18
14
|
import {oid4vp} from '@digitalbazaar/oid4-client';
|
|
19
15
|
import {parse as parseAuthorizationResponse} from './authorizationResponse.js';
|
|
20
|
-
import {verify} from '../verify.js';
|
|
21
16
|
|
|
22
17
|
const {util: {BedrockError}} = bedrock;
|
|
23
18
|
|
|
@@ -26,73 +21,59 @@ export {encode as encodeAuthorizationRequest} from './authorizationRequest.js';
|
|
|
26
21
|
export async function getAuthorizationRequest({req, clientProfileId}) {
|
|
27
22
|
const {config: workflow} = req.serviceObject;
|
|
28
23
|
const exchangeRecord = await req.getExchange();
|
|
29
|
-
let {exchange} = exchangeRecord;
|
|
30
|
-
let step;
|
|
31
|
-
let clientProfile;
|
|
32
|
-
|
|
33
|
-
while(true) {
|
|
34
|
-
// exchange step required for OID4VP
|
|
35
|
-
const currentStep = exchange.step;
|
|
36
|
-
if(!currentStep) {
|
|
37
|
-
_throwUnsupportedProtocol();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
step = await evaluateExchangeStep({workflow, exchange});
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
// process exchange and capture values to return
|
|
26
|
+
const result = {};
|
|
27
|
+
const exchangeProcessor = new ExchangeProcessor({
|
|
28
|
+
workflow, exchangeRecord,
|
|
29
|
+
async prepareStep({exchange, step}) {
|
|
30
|
+
const {
|
|
31
|
+
clientProfile, authorizationRequest
|
|
32
|
+
} = await getStepAuthorizationRequest({
|
|
33
|
+
workflow, exchange, step, clientProfileId
|
|
34
|
+
});
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
// save values to return
|
|
37
|
+
result.clientProfile = clientProfile;
|
|
38
|
+
result.authorizationRequest = authorizationRequest;
|
|
39
|
+
result.exchange = exchange;
|
|
40
|
+
result.step = step;
|
|
41
|
+
},
|
|
42
|
+
inputRequired() {
|
|
43
|
+
// input always required (authz response required)
|
|
44
|
+
return true;
|
|
55
45
|
}
|
|
46
|
+
});
|
|
47
|
+
await exchangeProcessor.process();
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
export async function getStepAuthorizationRequest({
|
|
53
|
+
workflow, exchange, step, clientProfileId
|
|
54
|
+
}) {
|
|
55
|
+
// step must have `openId` to perform OID4VP
|
|
56
|
+
if(!step.openId) {
|
|
57
|
+
_throwUnsupportedProtocol();
|
|
58
|
+
}
|
|
59
|
+
// deny retrieval of authorization request if an authorization response
|
|
60
|
+
// has already been accepted for this step
|
|
61
|
+
if(exchange.variables.results[exchange.step]?.verifiablePresentation) {
|
|
62
|
+
throw new BedrockError('This OID4VP exchange is already in progress.', {
|
|
63
|
+
name: 'NotAllowedError',
|
|
64
|
+
details: {httpStatusCode: 403, public: true}
|
|
65
65
|
});
|
|
66
|
+
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if(exchange.state === 'pending') {
|
|
70
|
-
exchange.state = 'active';
|
|
71
|
-
updateExchange = true;
|
|
72
|
-
}
|
|
68
|
+
// get OID4VP client profile
|
|
69
|
+
const clientProfile = getClientProfile({step, clientProfileId});
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await emitExchangeUpdated({workflow, exchange, step});
|
|
79
|
-
} catch(e) {
|
|
80
|
-
exchange.state = prevState;
|
|
81
|
-
exchange.sequence--;
|
|
82
|
-
if(e.name !== 'InvalidStateError') {
|
|
83
|
-
// unrecoverable error
|
|
84
|
-
throw e;
|
|
85
|
-
}
|
|
86
|
-
// get exchange and loop to try again on `InvalidStateError`
|
|
87
|
-
const record = await exchanges.get(
|
|
88
|
-
{workflowId: workflow.id, id: exchange.id});
|
|
89
|
-
({exchange} = record);
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
71
|
+
// generate authorization request
|
|
72
|
+
const {authorizationRequest} = await _getOrCreateStepAuthorizationRequest({
|
|
73
|
+
workflow, exchange, clientProfileId, clientProfile, step
|
|
74
|
+
});
|
|
93
75
|
|
|
94
|
-
|
|
95
|
-
}
|
|
76
|
+
return {clientProfile, authorizationRequest};
|
|
96
77
|
}
|
|
97
78
|
|
|
98
79
|
export async function getOID4VPProtocols({workflow, exchange, step}) {
|
|
@@ -125,7 +106,7 @@ export async function getOID4VPProtocols({workflow, exchange, step}) {
|
|
|
125
106
|
});
|
|
126
107
|
const {
|
|
127
108
|
authorizationRequest: {client_id}
|
|
128
|
-
} = await
|
|
109
|
+
} = await _getOrCreateStepAuthorizationRequest({
|
|
129
110
|
workflow, exchange, clientProfileId, clientProfile, step
|
|
130
111
|
});
|
|
131
112
|
const searchParams = new URLSearchParams({
|
|
@@ -148,7 +129,7 @@ export async function initExchange({workflow, exchange, initialStep} = {}) {
|
|
|
148
129
|
const clientProfiles = openId.clientProfiles ?
|
|
149
130
|
Object.entries(openId.clientProfiles) : [[undefined, openId]];
|
|
150
131
|
await Promise.all(clientProfiles.map(([clientProfileId, clientProfile]) =>
|
|
151
|
-
|
|
132
|
+
_getOrCreateStepAuthorizationRequest({
|
|
152
133
|
workflow, exchange, clientProfileId, clientProfile, step: initialStep
|
|
153
134
|
})));
|
|
154
135
|
}
|
|
@@ -156,184 +137,153 @@ export async function initExchange({workflow, exchange, initialStep} = {}) {
|
|
|
156
137
|
export async function processAuthorizationResponse({req, clientProfileId}) {
|
|
157
138
|
const {config: workflow} = req.serviceObject;
|
|
158
139
|
const exchangeRecord = await req.getExchange();
|
|
159
|
-
let {exchange} = exchangeRecord;
|
|
160
|
-
|
|
161
|
-
// ensure authz response can be parsed
|
|
162
|
-
const {
|
|
163
|
-
presentation, envelope, presentationSubmission,
|
|
164
|
-
responseMode, protectedHeader
|
|
165
|
-
} = await parseAuthorizationResponse({req, exchange, clientProfileId});
|
|
166
|
-
|
|
167
|
-
let {meta: {updated: lastUpdated}} = exchangeRecord;
|
|
168
|
-
let step;
|
|
169
|
-
try {
|
|
170
|
-
// get authorization request and updated exchange associated with exchange
|
|
171
|
-
const arResult = await getAuthorizationRequest({req, clientProfileId});
|
|
172
|
-
const {authorizationRequest} = arResult;
|
|
173
|
-
({exchange, step} = arResult);
|
|
174
|
-
|
|
175
|
-
// ensure a result for this step has not already been stored
|
|
176
|
-
const currentStep = exchange.step;
|
|
177
|
-
if(exchange.variables?.results?.[currentStep]) {
|
|
178
|
-
throw new BedrockError(
|
|
179
|
-
'This OID4VP exchange is already in progress.', {
|
|
180
|
-
name: 'NotAllowedError',
|
|
181
|
-
details: {httpStatusCode: 403, public: true}
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
140
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
141
|
+
// process exchange and produce result
|
|
142
|
+
let parseResponseResult;
|
|
143
|
+
const result = {};
|
|
144
|
+
const exchangeProcessor = new ExchangeProcessor({
|
|
145
|
+
workflow, exchangeRecord,
|
|
146
|
+
async prepareStep({exchange, step}) {
|
|
147
|
+
const {authorizationRequest} = await getStepAuthorizationRequest({
|
|
148
|
+
workflow, exchange, step, clientProfileId
|
|
149
|
+
});
|
|
150
|
+
result.authorizationRequest = authorizationRequest;
|
|
151
|
+
|
|
152
|
+
// ensure authz response can be parsed
|
|
153
|
+
parseResponseResult = await parseAuthorizationResponse({
|
|
154
|
+
req, exchange: exchangeRecord.exchange, clientProfileId,
|
|
155
|
+
authorizationRequest
|
|
156
|
+
});
|
|
194
157
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const toValidate = envelope ? envelope.contents : presentation;
|
|
201
|
-
|
|
202
|
-
// validate the received VP / envelope contents
|
|
203
|
-
const {jsonSchema: schema} = presentationSchema;
|
|
204
|
-
const validate = compile({schema});
|
|
205
|
-
const {valid, error} = validate(toValidate);
|
|
206
|
-
if(!valid) {
|
|
207
|
-
throw error;
|
|
158
|
+
// only mark exchange complete if there is nothing to be issued; this
|
|
159
|
+
// handles same-step OID4VCI+OID4VP case
|
|
160
|
+
const {credentialTemplates = []} = workflow;
|
|
161
|
+
if(credentialTemplates.length === 0) {
|
|
162
|
+
exchange.state = 'complete';
|
|
208
163
|
}
|
|
209
|
-
}
|
|
210
164
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
{
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
verifyPresentationResultSchema
|
|
217
|
-
} = step;
|
|
218
|
-
const verifyPresentationOptions = {
|
|
219
|
-
...step.verifyPresentationOptions
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
// if `direct_post.jwt` used w/ mDL presentation, include `mDL` options
|
|
223
|
-
if(responseMode === 'direct_post.jwt' &&
|
|
224
|
-
oid4vp.authzResponse.submitsFormat({
|
|
225
|
-
presentationSubmission, format: 'mso_mdoc'
|
|
226
|
-
})) {
|
|
227
|
-
verifyPresentationOptions.challenge = authorizationRequest.nonce;
|
|
228
|
-
verifyPresentationOptions.domain = authorizationRequest.response_uri;
|
|
229
|
-
verifyPresentationOptions.mdl = {
|
|
230
|
-
...verifyPresentationOptions.mdl,
|
|
231
|
-
// note: in session transcript:
|
|
232
|
-
// `domain` option above will be used for `responseUri`
|
|
233
|
-
// `challenge` option above will be used for `verifierGeneratedNonce`
|
|
234
|
-
// so do not send here to avoid redundancy
|
|
235
|
-
sessionTranscript: {
|
|
236
|
-
// per ISO 18013-7 the `mdocGeneratedNonce` is base64url-encoded
|
|
237
|
-
// and put into the `apu` protected header parameter -- and the
|
|
238
|
-
// VC API `mdl.sessionTranscript` option expects the
|
|
239
|
-
// `mdocGeneratedNonce` to be base64url-encoded, so we can pass
|
|
240
|
-
// it straight through
|
|
241
|
-
mdocGeneratedNonce: protectedHeader.apu,
|
|
242
|
-
clientId: authorizationRequest.client_id
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
}
|
|
165
|
+
// include `redirect_uri` if specified in step
|
|
166
|
+
const {redirect_uri} = step.openId;
|
|
167
|
+
if(redirect_uri) {
|
|
168
|
+
result.redirect_uri = redirect_uri;
|
|
169
|
+
}
|
|
246
170
|
|
|
247
|
-
|
|
248
|
-
|
|
171
|
+
const {presentation: receivedPresentation} = parseResponseResult;
|
|
172
|
+
return {receivedPresentation};
|
|
173
|
+
},
|
|
174
|
+
inputRequired({step}) {
|
|
175
|
+
// indicate input always required to avoid automatically advancing
|
|
176
|
+
// to the next step, but clear `step.verifiablePresentationRequest`
|
|
177
|
+
// to avoid overwriting previous value
|
|
178
|
+
delete step.verifiablePresentationRequest;
|
|
179
|
+
return true;
|
|
180
|
+
},
|
|
181
|
+
async verify({
|
|
182
|
+
workflow, exchange,
|
|
249
183
|
verifyPresentationOptions,
|
|
250
184
|
verifyPresentationResultSchema,
|
|
251
|
-
verifiablePresentationRequest,
|
|
252
185
|
presentation,
|
|
253
186
|
allowUnprotectedPresentation,
|
|
254
|
-
expectedChallenge
|
|
255
|
-
|
|
256
|
-
|
|
187
|
+
expectedChallenge,
|
|
188
|
+
expectedDomain
|
|
189
|
+
}) {
|
|
190
|
+
const {authorizationRequest} = result;
|
|
191
|
+
verifyPresentationOptions.challenge = authorizationRequest.nonce;
|
|
192
|
+
verifyPresentationOptions.domain = authorizationRequest.response_uri;
|
|
257
193
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
194
|
+
// FIXME: OID4VP 1.0+ does not have a presentation submission
|
|
195
|
+
// handle mDL submission
|
|
196
|
+
const {envelope} = parseResponseResult;
|
|
197
|
+
if(envelope?.mediaType === 'application/mdl-vp-token') {
|
|
198
|
+
// generate `handover` for mDL verification
|
|
199
|
+
let handover;
|
|
200
|
+
|
|
201
|
+
// common `handover` parameters:
|
|
202
|
+
const origin = authorizationRequest?.expected_origins?.[0] ??
|
|
203
|
+
new URL(authorizationRequest.response_uri).origin;
|
|
204
|
+
const nonce = authorizationRequest.nonce;
|
|
205
|
+
|
|
206
|
+
// `direct_post.jwt` => ISO18013-7 Annex B
|
|
207
|
+
// FIXME: same response mode is also used for OID4VP 1.0 with
|
|
208
|
+
// `OpenID4VPHandover` where `presentationSubmission` will be absent;
|
|
209
|
+
// this is not yet supported
|
|
210
|
+
// https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-invocation-via-redirects
|
|
211
|
+
const {responseMode} = parseResponseResult;
|
|
212
|
+
if(responseMode === 'direct_post.jwt') {
|
|
213
|
+
handover = {
|
|
214
|
+
type: 'AnnexBHandover',
|
|
215
|
+
// per ISO 18013-7 B the `mdocGeneratedNonce` is base64url-encoded
|
|
216
|
+
// and put into the `apu` protected header parameter, so parse that
|
|
217
|
+
// here and convert it to a UTF-8 string instead
|
|
218
|
+
mdocGeneratedNonce: Buffer
|
|
219
|
+
.from(parseResponseResult.protectedHeader?.apu ?? '', 'base64url')
|
|
220
|
+
.toString('utf8'),
|
|
221
|
+
clientId: authorizationRequest.client_id,
|
|
222
|
+
responseUri: authorizationRequest.response_uri,
|
|
223
|
+
verifierGeneratedNonce: nonce
|
|
224
|
+
};
|
|
225
|
+
} else if(responseMode === 'dc_api') {
|
|
226
|
+
// `dc_api` => ISO18013-7 Annex C
|
|
227
|
+
handover = {
|
|
228
|
+
type: 'dcapi',
|
|
229
|
+
origin,
|
|
230
|
+
nonce,
|
|
231
|
+
recipientPublicJwk: parseResponseResult.recipientPublicJwk
|
|
232
|
+
};
|
|
233
|
+
} else if(responseMode === 'dc_api.jwt') {
|
|
234
|
+
// `dc_api.jwt` => ISO18013-7 Annex D
|
|
235
|
+
handover = {
|
|
236
|
+
type: 'OpenID4VPDCAPIHandover',
|
|
237
|
+
origin,
|
|
238
|
+
nonce,
|
|
239
|
+
jwkThumbprint: parseResponseResult.recipientPublicJwkThumbprint
|
|
240
|
+
};
|
|
241
|
+
}
|
|
288
242
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
exchange.state = 'active';
|
|
297
|
-
await exchanges.update({workflowId: workflow.id, exchange});
|
|
298
|
-
} else {
|
|
299
|
-
// mark exchange complete
|
|
300
|
-
exchange.state = 'complete';
|
|
301
|
-
await exchanges.complete({workflowId: workflow.id, exchange});
|
|
243
|
+
verifyPresentationOptions.mdl = {
|
|
244
|
+
...verifyPresentationOptions.mdl,
|
|
245
|
+
// send encoded mDL `sessionTranscript`
|
|
246
|
+
sessionTranscript: Buffer
|
|
247
|
+
.from(await oid4vp.mdl.encodeSessionTranscript({handover}))
|
|
248
|
+
.toString('base64url')
|
|
249
|
+
};
|
|
302
250
|
}
|
|
303
|
-
await emitExchangeUpdated({workflow, exchange, step});
|
|
304
|
-
lastUpdated = Date.now();
|
|
305
|
-
} catch(e) {
|
|
306
|
-
// revert exchange changes as it couldn't be written
|
|
307
|
-
exchange.sequence--;
|
|
308
|
-
exchange.state = prevState;
|
|
309
|
-
delete exchange.variables.results[currentStep];
|
|
310
|
-
throw e;
|
|
311
|
-
}
|
|
312
251
|
|
|
313
|
-
|
|
252
|
+
// verify presentation
|
|
253
|
+
const verifyResult = await defaultVerify({
|
|
254
|
+
workflow,
|
|
255
|
+
verifyPresentationOptions,
|
|
256
|
+
verifyPresentationResultSchema,
|
|
257
|
+
presentation,
|
|
258
|
+
allowUnprotectedPresentation,
|
|
259
|
+
expectedChallenge,
|
|
260
|
+
expectedDomain
|
|
261
|
+
});
|
|
314
262
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
263
|
+
// save OID4VP results in exchange
|
|
264
|
+
const {presentationSubmission} = parseResponseResult;
|
|
265
|
+
exchange.variables.results[exchange.step] = {
|
|
266
|
+
...exchange.variables.results[exchange.step],
|
|
267
|
+
openId: {
|
|
268
|
+
clientProfileId,
|
|
269
|
+
authorizationRequest,
|
|
270
|
+
presentationSubmission
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
// FIXME: do w/o `parseResponseResult.envelope` to eliminate envelope
|
|
274
|
+
// parsing (let verifier do it)
|
|
275
|
+
if(parseResponseResult.envelope) {
|
|
276
|
+
// include enveloped VP in step result
|
|
277
|
+
exchange.variables.results[exchange.step]
|
|
278
|
+
.envelopedPresentation = presentation;
|
|
279
|
+
}
|
|
320
280
|
|
|
321
|
-
|
|
322
|
-
} catch(e) {
|
|
323
|
-
if(e.name === 'InvalidStateError') {
|
|
324
|
-
throw e;
|
|
281
|
+
return verifyResult;
|
|
325
282
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
copy.lastError = e;
|
|
331
|
-
await exchanges.setLastError({workflowId, exchange: copy, lastUpdated})
|
|
332
|
-
.catch(error => logger.error(
|
|
333
|
-
'Could not set last exchange error: ' + error.message, {error}));
|
|
334
|
-
await emitExchangeUpdated({workflow, exchange, step});
|
|
335
|
-
throw e;
|
|
336
|
-
}
|
|
283
|
+
});
|
|
284
|
+
await exchangeProcessor.process();
|
|
285
|
+
|
|
286
|
+
return result;
|
|
337
287
|
}
|
|
338
288
|
|
|
339
289
|
export async function supportsOID4VP({workflow, exchange, step}) {
|
|
@@ -346,7 +296,7 @@ export async function supportsOID4VP({workflow, exchange, step}) {
|
|
|
346
296
|
return step.openId !== undefined;
|
|
347
297
|
}
|
|
348
298
|
|
|
349
|
-
async function
|
|
299
|
+
async function _getOrCreateStepAuthorizationRequest({
|
|
350
300
|
workflow, exchange, clientProfileId, clientProfile, step
|
|
351
301
|
}) {
|
|
352
302
|
let authorizationRequest;
|