@verana-labs/vs-agent-sdk 2.0.0-dev.70 → 2.0.0-dev.72
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/build/adminApi/AdminApiError.d.ts +1 -0
- package/build/adminApi/AdminApiError.js +1 -0
- package/build/adminApi/AdminApiError.js.map +1 -1
- package/build/blockchain/VeranaChainService.d.ts +1 -0
- package/build/blockchain/VeranaChainService.js +16 -14
- package/build/blockchain/VeranaChainService.js.map +1 -1
- package/build/blockchain/handlers/defaultHandlers.js +6 -3
- package/build/blockchain/handlers/defaultHandlers.js.map +1 -1
- package/build/blockchain/handlers/selfIssuanceHandlers.js +7 -2
- package/build/blockchain/handlers/selfIssuanceHandlers.js.map +1 -1
- package/build/blockchain/handlers/stateMutations.d.ts +8 -10
- package/build/blockchain/handlers/stateMutations.js +23 -26
- package/build/blockchain/handlers/stateMutations.js.map +1 -1
- package/build/bootstrap/EcsBootstrapService.js +13 -4
- package/build/bootstrap/EcsBootstrapService.js.map +1 -1
- package/build/events/VtFlowEvents.js +3 -0
- package/build/events/VtFlowEvents.js.map +1 -1
- package/build/types.d.ts +0 -13
- package/build/types.js +1 -2
- package/build/types.js.map +1 -1
- package/build/utils/selfIssuedEcsCredential.d.ts +1 -1
- package/build/utils/selfIssuedEcsCredential.js +13 -6
- package/build/utils/selfIssuedEcsCredential.js.map +1 -1
- package/build/utils/setupSelfTr.js +3 -1
- package/build/utils/setupSelfTr.js.map +1 -1
- package/build/utils/trustCredentialStore.d.ts +1 -1
- package/build/utils/trustCredentialStore.js +2 -2
- package/build/utils/trustCredentialStore.js.map +1 -1
- package/build/utils/vcdm2.d.ts +1 -1
- package/build/utils/vcdm2.js +3 -5
- package/build/utils/vcdm2.js.map +1 -1
- package/build/vtFlow/VtFlowOrchestrator.d.ts +26 -4
- package/build/vtFlow/VtFlowOrchestrator.js +277 -50
- package/build/vtFlow/VtFlowOrchestrator.js.map +1 -1
- package/package.json +3 -3
|
@@ -17,6 +17,8 @@ const DISCOUNT_SCALE = 10000;
|
|
|
17
17
|
const TX_LOOKUP_TIMEOUT_MS = 60000;
|
|
18
18
|
const TX_LOOKUP_INTERVAL_MS = 3000;
|
|
19
19
|
const FEE_DENOM = 'uvna';
|
|
20
|
+
// the notification handler and the tx lookup can both reach markValidated for one flow at the same time
|
|
21
|
+
const markingValidated = new Set();
|
|
20
22
|
const FEE_KEYS = ['validationFees', 'issuanceFees', 'verificationFees'];
|
|
21
23
|
const DISCOUNT_KEYS = ['issuanceFeeDiscount', 'verificationFeeDiscount'];
|
|
22
24
|
function invalidInput(message) {
|
|
@@ -87,9 +89,7 @@ class VtFlowOrchestrator {
|
|
|
87
89
|
}))
|
|
88
90
|
.filter(record => !(0, credo_ts_didcomm_vt_flow_1.isVtFlowTerminalState)(record.state))
|
|
89
91
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
90
|
-
const running = latest !== undefined &&
|
|
91
|
-
latest.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.Completed &&
|
|
92
|
-
latest.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.CredRevoked;
|
|
92
|
+
const running = latest !== undefined && !(0, credo_ts_didcomm_vt_flow_1.isVtFlowRenewable)(latest);
|
|
93
93
|
let connectionId;
|
|
94
94
|
if (latest) {
|
|
95
95
|
const connection = await this.agent.didcomm.connections.findById(latest.connectionId);
|
|
@@ -183,7 +183,7 @@ class VtFlowOrchestrator {
|
|
|
183
183
|
* that validateOnboardingProcess built, so that the process does not build the credential twice.
|
|
184
184
|
*/
|
|
185
185
|
async offerOnboardingCredential(input) {
|
|
186
|
-
var _a, _b, _c, _d;
|
|
186
|
+
var _a, _b, _c, _d, _e;
|
|
187
187
|
this.requireChain();
|
|
188
188
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
189
189
|
const record = await vtFlowApi.findById(input.vtFlowRecordId);
|
|
@@ -200,6 +200,8 @@ class VtFlowOrchestrator {
|
|
|
200
200
|
claims: ((_d = record.claims) !== null && _d !== void 0 ? _d : {}),
|
|
201
201
|
credentialType: input.credentialType,
|
|
202
202
|
credentialContext: input.credentialContext,
|
|
203
|
+
// [VSA-VTI-FLOW-FMT-1]: markValidated sets it from the entry on every pass, a renewal included
|
|
204
|
+
validUntil: (_e = record.validation) === null || _e === void 0 ? void 0 : _e.effectiveUntil,
|
|
203
205
|
}));
|
|
204
206
|
// W3C Data Integrity attachment format (RFC 0809). The applicant DID is the credential subject
|
|
205
207
|
// and the exchange runs over its authenticated connection, so no extra binding is required. The
|
|
@@ -222,6 +224,7 @@ class VtFlowOrchestrator {
|
|
|
222
224
|
}
|
|
223
225
|
/** [VSA-ADM-VT-FL-VALIDATE] */
|
|
224
226
|
async validateFlow(input) {
|
|
227
|
+
var _a, _b;
|
|
225
228
|
this.requireChain();
|
|
226
229
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
227
230
|
const record = await vtFlowApi.findById(input.vtFlowRecordId);
|
|
@@ -230,24 +233,38 @@ class VtFlowOrchestrator {
|
|
|
230
233
|
}
|
|
231
234
|
if (record.role !== credo_ts_didcomm_vt_flow_1.VtFlowRole.Validator)
|
|
232
235
|
throw invalidState('validate is a validator action');
|
|
236
|
+
const reissue = record.state === credo_ts_didcomm_vt_flow_1.VtFlowState.CredOffered && ((_b = (_a = record.issuance) === null || _a === void 0 ? void 0 : _a.tx) === null || _b === void 0 ? void 0 : _b.status) === credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Failed;
|
|
237
|
+
if (reissue && record.credentialExchangeRecordId) {
|
|
238
|
+
const { record: issued } = await vtFlowApi.issueCredentialForSession({
|
|
239
|
+
vtFlowRecordId: record.id,
|
|
240
|
+
credentialExchangeRecordId: record.credentialExchangeRecordId,
|
|
241
|
+
});
|
|
242
|
+
return issued;
|
|
243
|
+
}
|
|
233
244
|
if (record.variant === credo_ts_didcomm_vt_flow_1.VtFlowVariant.DirectIssuance)
|
|
234
245
|
return this.validateDirectIssuance(record);
|
|
235
246
|
if (!record.applicantParticipantId)
|
|
236
247
|
throw invalidState('the flow names no applicant participant');
|
|
237
248
|
const applicant = await this.agent.indexer.getParticipant(record.applicantParticipantId);
|
|
238
249
|
const entryValidated = applicant.op_state === 'VALIDATED';
|
|
239
|
-
this.
|
|
250
|
+
const rejectedInFlight = entryValidated && (await this.isRejectedInFlight(record));
|
|
251
|
+
if (!rejectedInFlight) {
|
|
252
|
+
this.assertValidateState(record, entryValidated);
|
|
253
|
+
if (record.connectionTerminated)
|
|
254
|
+
throw invalidState('the flow connection is TERMINATED until the applicant reconnects');
|
|
255
|
+
}
|
|
240
256
|
const terms = this.validationTerms(input, applicant, record);
|
|
241
257
|
await this.assertClaimsAndTerm(record, applicant, terms);
|
|
242
|
-
|
|
243
|
-
|
|
258
|
+
// [VSA-VTI-FLOW-OP-ISSUE-6]: the connection stays TERMINATED, so issuance waits for the applicant to reconnect
|
|
259
|
+
if (rejectedInFlight)
|
|
260
|
+
return this.markValidated(record.id, applicant);
|
|
244
261
|
if (record.state === credo_ts_didcomm_vt_flow_1.VtFlowState.OobPending)
|
|
245
262
|
await this.sendValidating(record);
|
|
246
263
|
if (record.state === credo_ts_didcomm_vt_flow_1.VtFlowState.ValidatedPendingClaims)
|
|
247
264
|
return this.continueAfterValidated(record.id);
|
|
248
265
|
if (entryValidated) {
|
|
249
266
|
if (record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.Validated)
|
|
250
|
-
await
|
|
267
|
+
await this.markValidated(record.id, applicant);
|
|
251
268
|
return this.continueAfterValidated(record.id);
|
|
252
269
|
}
|
|
253
270
|
const validation = Object.assign({ decidedAt: new Date().toISOString(), submission: await this.submissionPath(applicant) }, terms);
|
|
@@ -292,7 +309,6 @@ class VtFlowOrchestrator {
|
|
|
292
309
|
}
|
|
293
310
|
assertValidateState(record, entryValidated) {
|
|
294
311
|
const accepted = [
|
|
295
|
-
credo_ts_didcomm_vt_flow_1.VtFlowState.AwaitingOr,
|
|
296
312
|
credo_ts_didcomm_vt_flow_1.VtFlowState.Validating,
|
|
297
313
|
credo_ts_didcomm_vt_flow_1.VtFlowState.OobPending,
|
|
298
314
|
credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxFailed,
|
|
@@ -301,6 +317,7 @@ class VtFlowOrchestrator {
|
|
|
301
317
|
// Once the entry is VALIDATED on chain any validator-side state resumes into issuance, which is
|
|
302
318
|
// how a backend that disabled the default notification handler drives the flow ([VSA-ADM-VT-FL-VALIDATE-11]).
|
|
303
319
|
const acceptedOnceValidated = [
|
|
320
|
+
credo_ts_didcomm_vt_flow_1.VtFlowState.AwaitingOr,
|
|
304
321
|
credo_ts_didcomm_vt_flow_1.VtFlowState.AwaitingValidationTx,
|
|
305
322
|
credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxSubmitted,
|
|
306
323
|
credo_ts_didcomm_vt_flow_1.VtFlowState.Validated,
|
|
@@ -365,8 +382,7 @@ class VtFlowOrchestrator {
|
|
|
365
382
|
this.assertClaims(schema.json_schema, applicant.did, record.claims);
|
|
366
383
|
// An ECS Organization or Persona credential needs a validUntil. With a validity period of 0 the
|
|
367
384
|
// VPR sets no effective_until, so the call has to carry one.
|
|
368
|
-
const
|
|
369
|
-
const needsValidUntil = ecsKey === vs_agent_model_1.ECS.ORG || ecsKey === vs_agent_model_1.ECS.PERSONA;
|
|
385
|
+
const needsValidUntil = (0, vs_agent_model_1.ecsRequiresValidUntil)(await (0, vs_agent_model_1.classifyEcsSchema)(schema.json_schema));
|
|
370
386
|
if (needsValidUntil && ((_a = schema.holder_validation_validity_period) !== null && _a !== void 0 ? _a : 0) === 0 && !terms.effectiveUntil) {
|
|
371
387
|
throw invalidInput('this schema has no validity period, so effectiveUntil is required');
|
|
372
388
|
}
|
|
@@ -387,55 +403,79 @@ class VtFlowOrchestrator {
|
|
|
387
403
|
: credo_ts_didcomm_vt_flow_1.VtFlowSubmission.Operator;
|
|
388
404
|
}
|
|
389
405
|
async submitValidation(recordId, applicant, validation) {
|
|
390
|
-
var _a, _b
|
|
406
|
+
var _a, _b;
|
|
391
407
|
const chain = this.requireChain();
|
|
392
408
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
393
409
|
const fail = (reason, error) => vtFlowApi.recordValidation(recordId, Object.assign(Object.assign({}, validation), { tx: { status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Failed, reason, error } }), credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxFailed);
|
|
394
|
-
const grant = applicant.validator_participant_id == null
|
|
395
|
-
? undefined
|
|
396
|
-
: (_a = this.agent.authorizationService) === null || _a === void 0 ? void 0 : _a.getVsOperatorAuthorizationRecord(applicant.validator_participant_id);
|
|
397
|
-
const granter = (grant === null || grant === void 0 ? void 0 : grant.withFeegrant) ? chain.corporation : undefined;
|
|
398
410
|
const message = chain.setParticipantOPToValidatedMsg({
|
|
399
411
|
id: applicant.id,
|
|
400
412
|
effectiveUntil: validation.effectiveUntil ? new Date(validation.effectiveUntil) : undefined,
|
|
401
413
|
validationFees: validation.validationFees,
|
|
402
414
|
issuanceFees: validation.issuanceFees,
|
|
403
415
|
verificationFees: validation.verificationFees,
|
|
404
|
-
issuanceFeeDiscount: Math.round(((
|
|
405
|
-
verificationFeeDiscount: Math.round(((
|
|
416
|
+
issuanceFeeDiscount: Math.round(((_a = validation.issuanceFeeDiscount) !== null && _a !== void 0 ? _a : 0) * DISCOUNT_SCALE),
|
|
417
|
+
verificationFeeDiscount: Math.round(((_b = validation.verificationFeeDiscount) !== null && _b !== void 0 ? _b : 0) * DISCOUNT_SCALE),
|
|
406
418
|
opSummaryDigest: validation.opSummaryDigest,
|
|
407
419
|
});
|
|
420
|
+
const checked = await this.preflight(message, applicant.validator_participant_id);
|
|
421
|
+
if ('reason' in checked)
|
|
422
|
+
return fail(checked.reason, checked.error);
|
|
423
|
+
let hash;
|
|
424
|
+
try {
|
|
425
|
+
hash = await chain.broadcastWithoutWaiting([message], checked.fee);
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
return fail(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.BroadcastError, errorMessage(error));
|
|
429
|
+
}
|
|
430
|
+
const submitted = await vtFlowApi.recordValidation(recordId, Object.assign(Object.assign({}, validation), { tx: { hash, submittedAt: new Date().toISOString(), status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Submitted } }), credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxSubmitted);
|
|
431
|
+
void this.resolveValidationTx(recordId).catch(error => this.agent.config.logger.error(`[vt-flow] resolving the validation of ${recordId} failed`, {
|
|
432
|
+
error: errorMessage(error),
|
|
433
|
+
}));
|
|
434
|
+
return submitted;
|
|
435
|
+
}
|
|
436
|
+
// [VSA-ADM-VT-FL-VALIDATE-6]: the fee payer is the Corporation when the grant of the entry has with_feegrant
|
|
437
|
+
async preflight(message, participantId) {
|
|
438
|
+
var _a, _b, _c;
|
|
439
|
+
const chain = this.requireChain();
|
|
440
|
+
const grant = participantId == null
|
|
441
|
+
? undefined
|
|
442
|
+
: (_a = this.agent.authorizationService) === null || _a === void 0 ? void 0 : _a.getVsOperatorAuthorizationRecord(participantId);
|
|
443
|
+
const granter = (grant === null || grant === void 0 ? void 0 : grant.withFeegrant) ? chain.corporation : undefined;
|
|
444
|
+
const failed = (reason, error) => ({
|
|
445
|
+
reason,
|
|
446
|
+
error,
|
|
447
|
+
});
|
|
408
448
|
let allowance;
|
|
409
449
|
try {
|
|
410
450
|
allowance = granter ? await chain.feeAllowance(granter, FEE_DENOM) : undefined;
|
|
411
451
|
}
|
|
412
452
|
catch (error) {
|
|
413
|
-
return
|
|
453
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
|
|
414
454
|
}
|
|
415
455
|
if (granter && !allowance) {
|
|
416
|
-
return
|
|
456
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.FeegrantExpired, 'the Corporation grants the agent no active fee allowance');
|
|
417
457
|
}
|
|
418
458
|
let fee;
|
|
419
459
|
try {
|
|
420
460
|
fee = await chain.estimateFee([message], granter);
|
|
421
461
|
}
|
|
422
462
|
catch (error) {
|
|
423
|
-
return
|
|
463
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
|
|
424
464
|
}
|
|
425
|
-
const amount = BigInt((
|
|
465
|
+
const amount = BigInt((_c = (_b = fee.amount.find(coin => coin.denom === FEE_DENOM)) === null || _b === void 0 ? void 0 : _b.amount) !== null && _c !== void 0 ? _c : '0');
|
|
426
466
|
if (granter && allowance) {
|
|
427
467
|
if (!allowance.unlimited && allowance.remaining < amount) {
|
|
428
|
-
return
|
|
468
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.FeegrantExhausted, `the fee allowance has ${allowance.remaining}${FEE_DENOM} left`);
|
|
429
469
|
}
|
|
430
470
|
let corporation;
|
|
431
471
|
try {
|
|
432
472
|
corporation = BigInt((await chain.getAccountBalance(granter, FEE_DENOM)).amount);
|
|
433
473
|
}
|
|
434
474
|
catch (error) {
|
|
435
|
-
return
|
|
475
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
|
|
436
476
|
}
|
|
437
477
|
if (corporation < amount) {
|
|
438
|
-
return
|
|
478
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.InsufficientFundsCorporation, `the Corporation holds ${corporation}${FEE_DENOM}`);
|
|
439
479
|
}
|
|
440
480
|
}
|
|
441
481
|
else {
|
|
@@ -444,24 +484,55 @@ class VtFlowOrchestrator {
|
|
|
444
484
|
own = BigInt((await chain.getBalance(FEE_DENOM)).amount);
|
|
445
485
|
}
|
|
446
486
|
catch (error) {
|
|
447
|
-
return
|
|
487
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
|
|
448
488
|
}
|
|
449
489
|
if (own < amount) {
|
|
450
|
-
return
|
|
490
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.InsufficientFundsAgent, `the agent account holds ${own}${FEE_DENOM}`);
|
|
451
491
|
}
|
|
452
492
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
493
|
+
return { fee };
|
|
494
|
+
}
|
|
495
|
+
// [VSA-VTI-FLOW-ISSUE-1]: [VSA-ADM-VT-FL-VALIDATE-6] to -9 applied to the anchoring, which the delivery waits for.
|
|
496
|
+
// A transaction left SUBMITTED by a restart is looked up again, not broadcast twice.
|
|
497
|
+
async anchor(record, message, issuerParticipantId) {
|
|
498
|
+
var _a, _b, _c;
|
|
499
|
+
const chain = this.requireChain();
|
|
500
|
+
const pending = ((_b = (_a = record.issuance) === null || _a === void 0 ? void 0 : _a.tx) === null || _b === void 0 ? void 0 : _b.status) === credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Submitted ? record.issuance.tx : undefined;
|
|
501
|
+
let hash = pending === null || pending === void 0 ? void 0 : pending.hash;
|
|
502
|
+
let submittedAt = (_c = pending === null || pending === void 0 ? void 0 : pending.submittedAt) !== null && _c !== void 0 ? _c : new Date().toISOString();
|
|
503
|
+
if (!hash) {
|
|
504
|
+
const checked = await this.preflight(message, issuerParticipantId);
|
|
505
|
+
if ('reason' in checked)
|
|
506
|
+
return Object.assign({ status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Failed }, checked);
|
|
507
|
+
try {
|
|
508
|
+
hash = await chain.broadcastWithoutWaiting([message], checked.fee);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
return {
|
|
512
|
+
status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Failed,
|
|
513
|
+
reason: credo_ts_didcomm_vt_flow_1.VtFlowTxReason.BroadcastError,
|
|
514
|
+
error: errorMessage(error),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
submittedAt = new Date().toISOString();
|
|
518
|
+
await this.resolveVtFlowApi().recordIssuance(record.id, {
|
|
519
|
+
tx: { hash, submittedAt, status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Submitted },
|
|
520
|
+
});
|
|
456
521
|
}
|
|
457
|
-
|
|
458
|
-
|
|
522
|
+
for (;;) {
|
|
523
|
+
const tx = await chain.findTx(hash).catch(() => undefined);
|
|
524
|
+
if ((tx === null || tx === void 0 ? void 0 : tx.code) === 0)
|
|
525
|
+
return { hash, submittedAt, height: tx.height, status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Succeeded };
|
|
526
|
+
if (tx) {
|
|
527
|
+
const failure = { status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Failed, reason: credo_ts_didcomm_vt_flow_1.VtFlowTxReason.TxFailed, error: tx.rawLog };
|
|
528
|
+
return Object.assign({ hash, submittedAt, height: tx.height }, failure);
|
|
529
|
+
}
|
|
530
|
+
if (Date.now() - Date.parse(submittedAt) >= TX_LOOKUP_TIMEOUT_MS) {
|
|
531
|
+
const error = 'not found 60 seconds after the broadcast';
|
|
532
|
+
return { hash, submittedAt, status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Failed, reason: credo_ts_didcomm_vt_flow_1.VtFlowTxReason.TxNotFound, error };
|
|
533
|
+
}
|
|
534
|
+
await new Promise(resolve => setTimeout(resolve, TX_LOOKUP_INTERVAL_MS));
|
|
459
535
|
}
|
|
460
|
-
const submitted = await vtFlowApi.recordValidation(recordId, Object.assign(Object.assign({}, validation), { tx: { hash, submittedAt: new Date().toISOString(), status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Submitted } }), credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxSubmitted);
|
|
461
|
-
void this.resolveValidationTx(recordId).catch(error => this.agent.config.logger.error(`[vt-flow] resolving the validation of ${recordId} failed`, {
|
|
462
|
-
error: errorMessage(error),
|
|
463
|
-
}));
|
|
464
|
-
return submitted;
|
|
465
536
|
}
|
|
466
537
|
// [VSA-ADM-VT-FL-VALIDATE-8]. Leaving VALIDATION_TX_SUBMITTED ends the loop, so the notification
|
|
467
538
|
// handler moving the flow first wins the race.
|
|
@@ -469,23 +540,35 @@ class VtFlowOrchestrator {
|
|
|
469
540
|
var _a, _b, _c;
|
|
470
541
|
const chain = this.requireChain();
|
|
471
542
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
543
|
+
let landed;
|
|
472
544
|
for (;;) {
|
|
473
545
|
const record = await vtFlowApi.findById(recordId);
|
|
474
546
|
const validation = record === null || record === void 0 ? void 0 : record.validation;
|
|
475
547
|
const hash = (_a = validation === null || validation === void 0 ? void 0 : validation.tx) === null || _a === void 0 ? void 0 : _a.hash;
|
|
476
|
-
if (!record
|
|
548
|
+
if (!(record === null || record === void 0 ? void 0 : record.applicantParticipantId) || !validation || !hash)
|
|
477
549
|
return;
|
|
478
|
-
|
|
479
|
-
if (tx && tx.code === 0) {
|
|
480
|
-
await vtFlowApi.recordValidation(recordId, Object.assign(Object.assign({}, validation), { tx: Object.assign(Object.assign({}, validation.tx), { height: tx.height, status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Succeeded }) }));
|
|
481
|
-
await vtFlowApi.markValidated(recordId);
|
|
482
|
-
await this.continueAfterValidated(recordId);
|
|
550
|
+
if (record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxSubmitted)
|
|
483
551
|
return;
|
|
552
|
+
const tx = landed !== null && landed !== void 0 ? landed : (await chain.findTx(hash).catch(() => undefined));
|
|
553
|
+
if (tx && tx.code === 0) {
|
|
554
|
+
landed = tx;
|
|
555
|
+
// the indexer can lag the block of the transaction, and the fill needs the validated entry
|
|
556
|
+
const entry = await this.agent.indexer.getParticipant(record.applicantParticipantId);
|
|
557
|
+
if (entry.op_state === 'VALIDATED') {
|
|
558
|
+
await this.markValidated(recordId, entry, { hash, height: tx.height });
|
|
559
|
+
await this.continueAfterValidated(recordId);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
484
562
|
}
|
|
485
|
-
if (tx)
|
|
563
|
+
else if (tx) {
|
|
486
564
|
return this.failUnlessValidated(record, validation, credo_ts_didcomm_vt_flow_1.VtFlowTxReason.TxFailed, tx.rawLog, tx.height);
|
|
565
|
+
}
|
|
487
566
|
const submittedAt = (_c = (_b = validation.tx) === null || _b === void 0 ? void 0 : _b.submittedAt) !== null && _c !== void 0 ? _c : validation.decidedAt;
|
|
488
567
|
if (Date.now() - Date.parse(submittedAt) >= TX_LOOKUP_TIMEOUT_MS) {
|
|
568
|
+
if (tx) {
|
|
569
|
+
this.agent.config.logger.warn(`[vt-flow] the transaction of flow ${recordId} landed, but the indexer does not show the entry VALIDATED yet: its notification completes the flow`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
489
572
|
return this.failUnlessValidated(record, validation, credo_ts_didcomm_vt_flow_1.VtFlowTxReason.TxNotFound, 'not found 60 seconds after the broadcast');
|
|
490
573
|
}
|
|
491
574
|
await new Promise(resolve => setTimeout(resolve, TX_LOOKUP_INTERVAL_MS));
|
|
@@ -494,18 +577,77 @@ class VtFlowOrchestrator {
|
|
|
494
577
|
// The VPR has no distinct error for a second SetParticipantOPtoValidated, so the entry tells a
|
|
495
578
|
// rejected duplicate apart from a real failure.
|
|
496
579
|
async failUnlessValidated(record, validation, reason, error, height) {
|
|
580
|
+
var _a;
|
|
497
581
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
498
582
|
const applicant = record.applicantParticipantId
|
|
499
583
|
? await this.agent.indexer.getParticipant(record.applicantParticipantId)
|
|
500
584
|
: undefined;
|
|
585
|
+
const failed = Object.assign(Object.assign({}, validation), { tx: Object.assign(Object.assign({}, validation.tx), { height, status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Failed, reason, error }) });
|
|
501
586
|
if ((applicant === null || applicant === void 0 ? void 0 : applicant.op_state) === 'VALIDATED') {
|
|
502
|
-
|
|
587
|
+
// the notification handler may have moved the flow while the tx and the entry were read
|
|
588
|
+
if (((_a = (await vtFlowApi.findById(record.id))) === null || _a === void 0 ? void 0 : _a.state) !== credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxSubmitted)
|
|
589
|
+
return;
|
|
590
|
+
if (reason === credo_ts_didcomm_vt_flow_1.VtFlowTxReason.TxFailed)
|
|
591
|
+
await vtFlowApi.recordValidation(record.id, failed);
|
|
592
|
+
await this.markValidated(record.id, applicant);
|
|
503
593
|
await this.continueAfterValidated(record.id);
|
|
504
594
|
return;
|
|
505
595
|
}
|
|
506
|
-
await vtFlowApi.recordValidation(record.id,
|
|
596
|
+
await vtFlowApi.recordValidation(record.id, failed, credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxFailed);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* [VSA-VTI-FLOW-OP-ISSUE]: moves the flow to VALIDATED and fills `validation` from the validated
|
|
600
|
+
* entry. Without the landed transaction, a recorded broadcast keeps its tx, and its submission
|
|
601
|
+
* unless that tx failed.
|
|
602
|
+
*/
|
|
603
|
+
async markValidated(recordId, entry, landed) {
|
|
604
|
+
var _a, _b, _c, _d, _e;
|
|
605
|
+
const vtFlowApi = this.resolveVtFlowApi();
|
|
606
|
+
if (markingValidated.has(recordId))
|
|
607
|
+
throw invalidState('the flow is already moving to VALIDATED');
|
|
608
|
+
markingValidated.add(recordId);
|
|
609
|
+
try {
|
|
610
|
+
const record = await vtFlowApi.findById(recordId);
|
|
611
|
+
if (!record)
|
|
612
|
+
throw new Error(`vt-flow record ${recordId} not found`);
|
|
613
|
+
if (!credo_ts_didcomm_vt_flow_1.VtFlowValidatedFromStates.has(record.state) &&
|
|
614
|
+
record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.TerminatedByValidator) {
|
|
615
|
+
throw invalidState(`the flow is '${record.state}', which does not precede VALIDATED`);
|
|
616
|
+
}
|
|
617
|
+
const recorded = record.validation;
|
|
618
|
+
const agentTx = landed && ((_a = recorded === null || recorded === void 0 ? void 0 : recorded.tx) === null || _a === void 0 ? void 0 : _a.hash) === landed.hash ? recorded.tx : undefined;
|
|
619
|
+
const validation = Object.assign(Object.assign(Object.assign({}, recorded), { decidedAt: (_c = (_b = recorded === null || recorded === void 0 ? void 0 : recorded.decidedAt) !== null && _b !== void 0 ? _b : landed === null || landed === void 0 ? void 0 : landed.timestamp) !== null && _c !== void 0 ? _c : entry.modified, submission: !landed && ((_d = recorded === null || recorded === void 0 ? void 0 : recorded.tx) === null || _d === void 0 ? void 0 : _d.hash) && recorded.tx.reason !== credo_ts_didcomm_vt_flow_1.VtFlowTxReason.TxFailed
|
|
620
|
+
? recorded.submission
|
|
621
|
+
: agentTx
|
|
622
|
+
? credo_ts_didcomm_vt_flow_1.VtFlowSubmission.Agent
|
|
623
|
+
: credo_ts_didcomm_vt_flow_1.VtFlowSubmission.Operator, validationFees: entry.validation_fees, issuanceFees: entry.issuance_fees, verificationFees: entry.verification_fees, issuanceFeeDiscount: entry.issuance_fee_discount, verificationFeeDiscount: entry.verification_fee_discount, effectiveUntil: (_e = entry.effective_until) !== null && _e !== void 0 ? _e : undefined }), (agentTx && {
|
|
624
|
+
tx: {
|
|
625
|
+
hash: agentTx.hash,
|
|
626
|
+
submittedAt: agentTx.submittedAt,
|
|
627
|
+
height: landed === null || landed === void 0 ? void 0 : landed.height,
|
|
628
|
+
status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Succeeded,
|
|
629
|
+
},
|
|
630
|
+
}));
|
|
631
|
+
return await vtFlowApi.recordValidation(recordId, validation, credo_ts_didcomm_vt_flow_1.VtFlowState.Validated);
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
markingValidated.delete(recordId);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/** rejectFlow ended the flow with its transaction in flight, and no newer flow of the applicant replaced it ([VSA-ADM-VT-FL-REJECT-2]). */
|
|
638
|
+
async isRejectedInFlight(record) {
|
|
639
|
+
if (record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.TerminatedByValidator || !record.validation)
|
|
640
|
+
return false;
|
|
641
|
+
if (!record.applicantParticipantId)
|
|
642
|
+
return false;
|
|
643
|
+
const [latest] = (await this.resolveVtFlowApi().findAllByQuery({
|
|
644
|
+
role: credo_ts_didcomm_vt_flow_1.VtFlowRole.Validator,
|
|
645
|
+
applicantParticipantId: record.applicantParticipantId,
|
|
646
|
+
})).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
647
|
+
return (latest === null || latest === void 0 ? void 0 : latest.id) === record.id;
|
|
507
648
|
}
|
|
508
649
|
async continueAfterValidated(recordId) {
|
|
650
|
+
var _a;
|
|
509
651
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
510
652
|
const record = await vtFlowApi.findById(recordId);
|
|
511
653
|
if (!record)
|
|
@@ -517,7 +659,14 @@ class VtFlowOrchestrator {
|
|
|
517
659
|
if (!participant)
|
|
518
660
|
throw new Error(`Applicant participant ${record.applicantParticipantId} not found`);
|
|
519
661
|
if (Number(participant.role) !== types_2.HOLDER_PARTICIPANT_TYPE)
|
|
520
|
-
return
|
|
662
|
+
return record;
|
|
663
|
+
const schema = await this.agent.indexer.getCredentialSchema(participant.schemaId);
|
|
664
|
+
const violations = (0, utils_1.schemaViolations)(JSON.parse(schema.json_schema), Object.assign({ id: participant.did }, ((_a = record.claims) !== null && _a !== void 0 ? _a : {})));
|
|
665
|
+
if (violations.length > 0) {
|
|
666
|
+
this.agent.config.logger.error(`[vt-flow] not offering the credential of flow ${recordId}: its claims do not satisfy the ` +
|
|
667
|
+
`json_schema of schema ${participant.schemaId}: ${violations.map(v => `${v.path} ${v.message}`.trim()).join(', ')}`);
|
|
668
|
+
return record.state === credo_ts_didcomm_vt_flow_1.VtFlowState.Validated ? vtFlowApi.markPendingClaims(recordId) : record;
|
|
669
|
+
}
|
|
521
670
|
return this.offerOnboardingCredential({ vtFlowRecordId: recordId, participant });
|
|
522
671
|
}
|
|
523
672
|
/** [VSA-ADM-VT-FL-VALIDATE-8]: at startup, resume every flow left in VALIDATION_TX_SUBMITTED. */
|
|
@@ -528,6 +677,20 @@ class VtFlowOrchestrator {
|
|
|
528
677
|
});
|
|
529
678
|
await Promise.all(pending.map(record => this.resolveValidationTx(record.id)));
|
|
530
679
|
}
|
|
680
|
+
/** [VSA-VTI-FLOW-ISSUE-1]: at startup, resume every anchoring left SUBMITTED through the stored issued credential. */
|
|
681
|
+
async resumeIssuanceSubmissions() {
|
|
682
|
+
const vtFlowApi = this.resolveVtFlowApi();
|
|
683
|
+
const offered = await vtFlowApi.findAllByQuery({
|
|
684
|
+
role: credo_ts_didcomm_vt_flow_1.VtFlowRole.Validator,
|
|
685
|
+
flowState: credo_ts_didcomm_vt_flow_1.VtFlowState.CredOffered,
|
|
686
|
+
});
|
|
687
|
+
await Promise.all(offered.map(({ id, issuance, credentialExchangeRecordId }) => {
|
|
688
|
+
var _a;
|
|
689
|
+
return ((_a = issuance === null || issuance === void 0 ? void 0 : issuance.tx) === null || _a === void 0 ? void 0 : _a.status) === credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Submitted && credentialExchangeRecordId
|
|
690
|
+
? vtFlowApi.issueCredentialForSession({ vtFlowRecordId: id, credentialExchangeRecordId })
|
|
691
|
+
: undefined;
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
531
694
|
async acceptCredential(input) {
|
|
532
695
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
533
696
|
const record = await vtFlowApi.findById(input.vtFlowRecordId);
|
|
@@ -603,6 +766,7 @@ class VtFlowOrchestrator {
|
|
|
603
766
|
context: input.credentialContext,
|
|
604
767
|
credentialSubject: Object.assign(Object.assign({}, input.claims), { id: input.subjectDid }),
|
|
605
768
|
credentialSchema: { id: jsonSchemaCredentialId, type: 'JsonSchemaCredential' },
|
|
769
|
+
validUntil: input.validUntil,
|
|
606
770
|
});
|
|
607
771
|
return (0, utils_1.toOfferedCredentialJson)(unsignedCredential);
|
|
608
772
|
}
|
|
@@ -620,7 +784,7 @@ class VtFlowOrchestrator {
|
|
|
620
784
|
}
|
|
621
785
|
return { digestAlgorithm: schema.digest_algorithm, ecsKey: await (0, vs_agent_model_1.classifyEcsSchema)(schema.json_schema) };
|
|
622
786
|
}
|
|
623
|
-
/** Fired after signing and before delivery
|
|
787
|
+
/** Fired after signing and before delivery. A failed anchoring comes back in `issuance`, and the credential stays undelivered. */
|
|
624
788
|
async onCredentialIssued(vtFlowRecordId, signedCredential) {
|
|
625
789
|
var _a, _b;
|
|
626
790
|
const chain = this.requireChain();
|
|
@@ -634,14 +798,17 @@ class VtFlowOrchestrator {
|
|
|
634
798
|
const issuer = await this.agent.indexer.getParticipant(record.issuerParticipantId);
|
|
635
799
|
const algorithm = await this.digestAlgorithmForSchema(issuer.schema_id);
|
|
636
800
|
const digest = (0, verre_1.computeCredentialDigestJCS)(signedCredential, algorithm);
|
|
637
|
-
|
|
801
|
+
const message = chain.createOrUpdateParticipantSessionMsg({
|
|
638
802
|
id: record.participantSessionId,
|
|
639
803
|
issuerParticipantId: record.issuerParticipantId,
|
|
640
804
|
agentParticipantId: Number((_a = record.agentParticipantId) !== null && _a !== void 0 ? _a : 0) || 0,
|
|
641
805
|
walletAgentParticipantId: Number((_b = record.walletAgentParticipantId) !== null && _b !== void 0 ? _b : 0) || 0,
|
|
642
806
|
digest,
|
|
643
807
|
});
|
|
644
|
-
return
|
|
808
|
+
return {
|
|
809
|
+
credentialDigest: digest,
|
|
810
|
+
issuance: { tx: await this.anchor(record, message, record.issuerParticipantId) },
|
|
811
|
+
};
|
|
645
812
|
}
|
|
646
813
|
async verifyOfferedCredential(vtFlowRecordId) {
|
|
647
814
|
var _a, _b;
|
|
@@ -676,6 +843,7 @@ class VtFlowOrchestrator {
|
|
|
676
843
|
throw new Error(`Issuer participant schema ${issuer.schema_id} does not match credential schema ${record.schemaId}`);
|
|
677
844
|
}
|
|
678
845
|
const credentialJson = await this.getReceivedCredentialJson(record.credentialExchangeRecordId);
|
|
846
|
+
await this.assertFlowCredentialFormat(credentialJson, issuer);
|
|
679
847
|
const { digestAlgorithm, ecsKey } = await this.credentialSchema(issuer.schema_id);
|
|
680
848
|
const digest = (0, verre_1.computeCredentialDigestJCS)(credentialJson, digestAlgorithm);
|
|
681
849
|
const anchored = await indexer.getDigest(digest);
|
|
@@ -685,6 +853,26 @@ class VtFlowOrchestrator {
|
|
|
685
853
|
if (ecsKey)
|
|
686
854
|
await vtFlowApi.setEcsSchemaKey(record.id, ecsKey);
|
|
687
855
|
}
|
|
856
|
+
/** [VSA-VTI-FLOW-FMT-1]. Credo already verified the proof and its assertionMethod key, for data model 2.0 only. */
|
|
857
|
+
async assertFlowCredentialFormat(credential, issuer) {
|
|
858
|
+
var _a, _b, _c;
|
|
859
|
+
if (!(0, utils_1.isVcdm2Credential)(credential)) {
|
|
860
|
+
throw new Error('Received credential is not a VC Data Model 2.0 credential');
|
|
861
|
+
}
|
|
862
|
+
const verificationMethod = (_a = credential.proof) === null || _a === void 0 ? void 0 : _a.verificationMethod;
|
|
863
|
+
if (!issuer.did || (verificationMethod === null || verificationMethod === void 0 ? void 0 : verificationMethod.split('#')[0]) !== issuer.did) {
|
|
864
|
+
throw new Error(`Proof verification method ${verificationMethod} is not one of the validator ${issuer.did}`);
|
|
865
|
+
}
|
|
866
|
+
const subjectId = (_b = credential.credentialSubject) === null || _b === void 0 ? void 0 : _b.id;
|
|
867
|
+
if (subjectId !== this.agent.did) {
|
|
868
|
+
throw new Error(`Credential subject ${subjectId} is not the applicant ${this.agent.did}`);
|
|
869
|
+
}
|
|
870
|
+
const schemaRef = (_c = credential.credentialSchema) === null || _c === void 0 ? void 0 : _c.id;
|
|
871
|
+
const jsonSchemaCredentialId = await this.resolveJsonSchemaCredentialId(String(issuer.schema_id));
|
|
872
|
+
if (schemaRef !== jsonSchemaCredentialId) {
|
|
873
|
+
throw new Error(`Credential schema ${schemaRef} is not the VTJSC ${jsonSchemaCredentialId} of the flow`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
688
876
|
async onCredentialRevoked(vtFlowRecordId) {
|
|
689
877
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
690
878
|
const record = await vtFlowApi.findById(vtFlowRecordId);
|
|
@@ -734,6 +922,45 @@ class VtFlowOrchestrator {
|
|
|
734
922
|
trustedEcosystemDids: this.agent.trustedEcosystemDids,
|
|
735
923
|
}, context);
|
|
736
924
|
}
|
|
925
|
+
// the indexer can still lag the applicant's own transaction, so wait for it as a tx lookup does
|
|
926
|
+
async checkParticipantId({ record }) {
|
|
927
|
+
if (!record.applicantParticipantId || !this.agent.did)
|
|
928
|
+
return false;
|
|
929
|
+
const deadline = Date.now() + TX_LOOKUP_TIMEOUT_MS;
|
|
930
|
+
let validatedAtFirstRead;
|
|
931
|
+
for (;;) {
|
|
932
|
+
const entry = await this.agent.indexer
|
|
933
|
+
.getParticipant(record.applicantParticipantId)
|
|
934
|
+
.catch(() => undefined);
|
|
935
|
+
validatedAtFirstRead !== null && validatedAtFirstRead !== void 0 ? validatedAtFirstRead : (validatedAtFirstRead = (entry === null || entry === void 0 ? void 0 : entry.op_state) === 'VALIDATED');
|
|
936
|
+
if (entry) {
|
|
937
|
+
if (entry.revoked || entry.slashed || entry.validator_participant_id == null)
|
|
938
|
+
return false;
|
|
939
|
+
const validator = await this.agent.indexer
|
|
940
|
+
.getParticipant(entry.validator_participant_id)
|
|
941
|
+
.catch(() => undefined);
|
|
942
|
+
if (validator && validator.did !== this.agent.did)
|
|
943
|
+
return false;
|
|
944
|
+
if (validator && entry.op_state === 'PENDING')
|
|
945
|
+
return true;
|
|
946
|
+
if (validator && entry.op_state === 'VALIDATED') {
|
|
947
|
+
const flow = await this.resolveVtFlowApi().findById(record.id);
|
|
948
|
+
// the flow has moved on, or another path is moving it to VALIDATED and continues it itself
|
|
949
|
+
if ((flow === null || flow === void 0 ? void 0 : flow.state) !== credo_ts_didcomm_vt_flow_1.VtFlowState.AwaitingOr || markingValidated.has(record.id))
|
|
950
|
+
return 'validated';
|
|
951
|
+
// VALIDATED at the first read can be the round before this request, which it does not settle
|
|
952
|
+
if (!validatedAtFirstRead) {
|
|
953
|
+
await this.markValidated(record.id, entry);
|
|
954
|
+
await this.continueAfterValidated(record.id);
|
|
955
|
+
return 'validated';
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (Date.now() >= deadline)
|
|
960
|
+
return false;
|
|
961
|
+
await new Promise(resolve => setTimeout(resolve, TX_LOOKUP_INTERVAL_MS));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
737
964
|
async publishCredentialAsLinkedVp(vtFlowRecordId) {
|
|
738
965
|
var _a;
|
|
739
966
|
if (!this.options.publicApiBaseUrl) {
|