@verana-labs/vs-agent-sdk 2.0.0-dev.70 → 2.0.0-dev.73

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.
Files changed (44) hide show
  1. package/build/adminApi/AdminApiError.d.ts +1 -0
  2. package/build/adminApi/AdminApiError.js +1 -0
  3. package/build/adminApi/AdminApiError.js.map +1 -1
  4. package/build/blockchain/VeranaChainService.d.ts +1 -0
  5. package/build/blockchain/VeranaChainService.js +16 -14
  6. package/build/blockchain/VeranaChainService.js.map +1 -1
  7. package/build/blockchain/handlers/defaultHandlers.js +6 -3
  8. package/build/blockchain/handlers/defaultHandlers.js.map +1 -1
  9. package/build/blockchain/handlers/selfIssuanceHandlers.js +7 -2
  10. package/build/blockchain/handlers/selfIssuanceHandlers.js.map +1 -1
  11. package/build/blockchain/handlers/stateMutations.d.ts +14 -10
  12. package/build/blockchain/handlers/stateMutations.js +56 -28
  13. package/build/blockchain/handlers/stateMutations.js.map +1 -1
  14. package/build/bootstrap/EcsBootstrapService.js +13 -4
  15. package/build/bootstrap/EcsBootstrapService.js.map +1 -1
  16. package/build/events/VtFlowEvents.js +3 -0
  17. package/build/events/VtFlowEvents.js.map +1 -1
  18. package/build/types.d.ts +0 -13
  19. package/build/types.js +1 -2
  20. package/build/types.js.map +1 -1
  21. package/build/utils/index.d.ts +1 -0
  22. package/build/utils/index.js +1 -0
  23. package/build/utils/index.js.map +1 -1
  24. package/build/utils/selfIssuedEcsCredential.d.ts +1 -1
  25. package/build/utils/selfIssuedEcsCredential.js +13 -6
  26. package/build/utils/selfIssuedEcsCredential.js.map +1 -1
  27. package/build/utils/setupSelfTr.js +3 -1
  28. package/build/utils/setupSelfTr.js.map +1 -1
  29. package/build/utils/trustCredentialStore.d.ts +12 -1
  30. package/build/utils/trustCredentialStore.js +37 -10
  31. package/build/utils/trustCredentialStore.js.map +1 -1
  32. package/build/utils/typeMetadata.d.ts +19 -0
  33. package/build/utils/typeMetadata.js +22 -0
  34. package/build/utils/typeMetadata.js.map +1 -0
  35. package/build/utils/util.d.ts +9 -0
  36. package/build/utils/util.js +12 -4
  37. package/build/utils/util.js.map +1 -1
  38. package/build/utils/vcdm2.d.ts +1 -1
  39. package/build/utils/vcdm2.js +3 -5
  40. package/build/utils/vcdm2.js.map +1 -1
  41. package/build/vtFlow/VtFlowOrchestrator.d.ts +26 -4
  42. package/build/vtFlow/VtFlowOrchestrator.js +277 -50
  43. package/build/vtFlow/VtFlowOrchestrator.js.map +1 -1
  44. package/package.json +4 -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.assertValidateState(record, entryValidated);
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
- if (record.state === credo_ts_didcomm_vt_flow_1.VtFlowState.AwaitingOr)
243
- await vtFlowApi.acceptOnboardingRequest(record.id);
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 vtFlowApi.markValidated(record.id);
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 ecsKey = await (0, vs_agent_model_1.classifyEcsSchema)(schema.json_schema);
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, _c, _d, _e;
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(((_b = validation.issuanceFeeDiscount) !== null && _b !== void 0 ? _b : 0) * DISCOUNT_SCALE),
405
- verificationFeeDiscount: Math.round(((_c = validation.verificationFeeDiscount) !== null && _c !== void 0 ? _c : 0) * DISCOUNT_SCALE),
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 fail(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
453
+ return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
414
454
  }
415
455
  if (granter && !allowance) {
416
- return fail(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.FeegrantExpired, 'the Corporation grants the agent no active fee allowance');
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 fail(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
463
+ return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
424
464
  }
425
- const amount = BigInt((_e = (_d = fee.amount.find(coin => coin.denom === FEE_DENOM)) === null || _d === void 0 ? void 0 : _d.amount) !== null && _e !== void 0 ? _e : '0');
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 fail(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.FeegrantExhausted, `the fee allowance has ${allowance.remaining}${FEE_DENOM} left`);
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 fail(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
475
+ return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
436
476
  }
437
477
  if (corporation < amount) {
438
- return fail(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.InsufficientFundsCorporation, `the Corporation holds ${corporation}${FEE_DENOM}`);
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 fail(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
487
+ return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
448
488
  }
449
489
  if (own < amount) {
450
- return fail(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.InsufficientFundsAgent, `the agent account holds ${own}${FEE_DENOM}`);
490
+ return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.InsufficientFundsAgent, `the agent account holds ${own}${FEE_DENOM}`);
451
491
  }
452
492
  }
453
- let hash;
454
- try {
455
- hash = await chain.broadcastWithoutWaiting([message], fee);
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
- catch (error) {
458
- return fail(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.BroadcastError, errorMessage(error));
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 || !validation || !hash || record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxSubmitted)
548
+ if (!(record === null || record === void 0 ? void 0 : record.applicantParticipantId) || !validation || !hash)
477
549
  return;
478
- const tx = await chain.findTx(hash).catch(() => undefined);
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
- await vtFlowApi.markValidated(record.id);
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, Object.assign(Object.assign({}, validation), { tx: Object.assign(Object.assign({}, validation.tx), { height, status: credo_ts_didcomm_vt_flow_1.VtFlowTxStatus.Failed, reason, error }) }), credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxFailed);
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 this.completeOnboardingProcess(recordId);
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, so a failure here must abort the issuance. */
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
- await chain.createOrUpdateParticipantSession({
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 digest;
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) {