@verana-labs/credo-ts-didcomm-vt-flow 2.0.0-dev.8 → 2.0.0-dev.81

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 (60) hide show
  1. package/build/VtFlowApi.d.ts +20 -3
  2. package/build/VtFlowApi.js +123 -14
  3. package/build/VtFlowApi.js.map +1 -1
  4. package/build/VtFlowModule.js +27 -18
  5. package/build/VtFlowModule.js.map +1 -1
  6. package/build/VtFlowModuleConfig.d.ts +45 -3
  7. package/build/VtFlowModuleConfig.js +16 -1
  8. package/build/VtFlowModuleConfig.js.map +1 -1
  9. package/build/errors/VtFlowErrorCode.d.ts +13 -3
  10. package/build/errors/VtFlowErrorCode.js +62 -18
  11. package/build/errors/VtFlowErrorCode.js.map +1 -1
  12. package/build/errors/buildVtFlowProblemReport.d.ts +5 -2
  13. package/build/errors/buildVtFlowProblemReport.js +8 -3
  14. package/build/errors/buildVtFlowProblemReport.js.map +1 -1
  15. package/build/errors/index.d.ts +3 -3
  16. package/build/errors/index.js +4 -1
  17. package/build/errors/index.js.map +1 -1
  18. package/build/handlers/IssuanceRequestHandler.d.ts +2 -1
  19. package/build/handlers/IssuanceRequestHandler.js +11 -2
  20. package/build/handlers/IssuanceRequestHandler.js.map +1 -1
  21. package/build/handlers/OnboardingRequestHandler.d.ts +2 -1
  22. package/build/handlers/OnboardingRequestHandler.js +11 -2
  23. package/build/handlers/OnboardingRequestHandler.js.map +1 -1
  24. package/build/handlers/ProblemReportHandler.d.ts +10 -0
  25. package/build/handlers/ProblemReportHandler.js +17 -0
  26. package/build/handlers/ProblemReportHandler.js.map +1 -0
  27. package/build/handlers/index.d.ts +1 -0
  28. package/build/handlers/index.js +3 -1
  29. package/build/handlers/index.js.map +1 -1
  30. package/build/index.d.ts +5 -4
  31. package/build/index.js +12 -1
  32. package/build/index.js.map +1 -1
  33. package/build/messages/ValidatingMessage.d.ts +1 -1
  34. package/build/messages/ValidatingMessage.js +1 -1
  35. package/build/messages/ValidatingMessage.js.map +1 -1
  36. package/build/messages/VtFlowProblemReportMessage.d.ts +13 -0
  37. package/build/messages/VtFlowProblemReportMessage.js +53 -0
  38. package/build/messages/VtFlowProblemReportMessage.js.map +1 -0
  39. package/build/messages/VtFlowProtocol.d.ts +3 -1
  40. package/build/messages/VtFlowProtocol.js +4 -2
  41. package/build/messages/VtFlowProtocol.js.map +1 -1
  42. package/build/messages/index.d.ts +2 -1
  43. package/build/messages/index.js +4 -1
  44. package/build/messages/index.js.map +1 -1
  45. package/build/repository/VtFlowRecord.d.ts +25 -7
  46. package/build/repository/VtFlowRecord.js +10 -3
  47. package/build/repository/VtFlowRecord.js.map +1 -1
  48. package/build/services/VtFlowService.d.ts +65 -17
  49. package/build/services/VtFlowService.js +431 -87
  50. package/build/services/VtFlowService.js.map +1 -1
  51. package/build/types.d.ts +100 -5
  52. package/build/types.js +66 -2
  53. package/build/types.js.map +1 -1
  54. package/build/utils/index.d.ts +1 -0
  55. package/build/utils/index.js +6 -0
  56. package/build/utils/index.js.map +1 -0
  57. package/build/utils/peerAnchorDid.d.ts +2 -0
  58. package/build/utils/peerAnchorDid.js +8 -0
  59. package/build/utils/peerAnchorDid.js.map +1 -0
  60. package/package.json +5 -5
@@ -19,6 +19,7 @@ const VtFlowModuleConfig_1 = require("../VtFlowModuleConfig");
19
19
  const errors_1 = require("../errors");
20
20
  const messages_1 = require("../messages");
21
21
  const repository_1 = require("../repository");
22
+ const utils_1 = require("../utils");
22
23
  const types_1 = require("../types");
23
24
  /** Core state machine for vt-flow; each method performs one transition and returns the wire message without dispatching (dispatch lives in `VtFlowApi`). */
24
25
  let VtFlowService = class VtFlowService {
@@ -31,7 +32,7 @@ let VtFlowService = class VtFlowService {
31
32
  /** Applicant-side OnboardingProcess: build the outbound `onboarding-request` and persist a record in `OR_SENT`. */
32
33
  async createOnboardingProcessRecord(agentContext, params) {
33
34
  const message = new messages_1.OnboardingRequestMessage({
34
- participantId: params.participantId,
35
+ participantId: params.applicantParticipantId,
35
36
  participantSessionId: params.participantSessionId,
36
37
  agentParticipantId: params.agentParticipantId,
37
38
  walletAgentParticipantId: params.walletAgentParticipantId,
@@ -40,7 +41,7 @@ let VtFlowService = class VtFlowService {
40
41
  message.setThread({ threadId: message.id });
41
42
  const existing = await this.repository.findByParticipantSessionId(agentContext, params.participantSessionId, types_1.VtFlowRole.Applicant);
42
43
  if (existing) {
43
- if (existing.state !== types_1.VtFlowState.Completed && existing.state !== types_1.VtFlowState.CredRevoked) {
44
+ if (!(0, types_1.isVtFlowRenewable)(existing)) {
44
45
  throw new core_1.CredoError(`vt-flow: participant_session_id '${params.participantSessionId}' already belongs to a flow in state ${existing.state}; use a new session id`);
45
46
  }
46
47
  existing.connectionId = params.connectionId;
@@ -59,7 +60,10 @@ let VtFlowService = class VtFlowService {
59
60
  variant: types_1.VtFlowVariant.OnboardingProcess,
60
61
  agentParticipantId: params.agentParticipantId,
61
62
  walletAgentParticipantId: params.walletAgentParticipantId,
62
- participantId: params.participantId,
63
+ applicantParticipantId: params.applicantParticipantId,
64
+ applicantParticipantRole: params.applicantParticipantRole,
65
+ validatorParticipantId: params.validatorParticipantId,
66
+ schemaId: params.schemaId,
63
67
  claims: params.claims,
64
68
  });
65
69
  await this.repository.save(agentContext, record);
@@ -92,46 +96,109 @@ let VtFlowService = class VtFlowService {
92
96
  this.emitStateChanged(agentContext, record, null);
93
97
  return { message, record };
94
98
  }
99
+ async reattachOnboardingProcessRecord(agentContext, params) {
100
+ const record = await this.repository.getById(agentContext, params.vtFlowRecordId);
101
+ record.assertRole(types_1.VtFlowRole.Applicant);
102
+ record.assertVariant(types_1.VtFlowVariant.OnboardingProcess);
103
+ if ((0, types_1.isVtFlowTerminalState)(record.state) ||
104
+ record.state === types_1.VtFlowState.Completed ||
105
+ record.state === types_1.VtFlowState.CredRevoked) {
106
+ throw new core_1.CredoError(`vt-flow: flow '${record.id}' in state ${record.state} cannot be re-attached`);
107
+ }
108
+ if (!record.applicantParticipantId)
109
+ throw new core_1.CredoError(`vt-flow: flow '${record.id}' has no participant_id`);
110
+ const message = new messages_1.OnboardingRequestMessage({
111
+ participantId: record.applicantParticipantId,
112
+ participantSessionId: record.participantSessionId,
113
+ agentParticipantId: record.agentParticipantId,
114
+ walletAgentParticipantId: record.walletAgentParticipantId,
115
+ claims: record.claims,
116
+ proofsAttach: record.proofsAttach,
117
+ });
118
+ message.setThread({ threadId: record.threadId });
119
+ record.connectionId = params.connectionId;
120
+ if (record.state === types_1.VtFlowState.CredOffered) {
121
+ await this.releaseCredentialExchange(agentContext, record);
122
+ await this.updateState(agentContext, record, types_1.VtFlowState.Validating);
123
+ }
124
+ else {
125
+ await this.repository.update(agentContext, record);
126
+ }
127
+ return { message, record };
128
+ }
129
+ async releaseCredentialExchange(agentContext, record) {
130
+ if (record.credentialExchangeRecordId) {
131
+ const exchanges = agentContext.dependencyManager.resolve(didcomm_1.DidCommCredentialExchangeRepository);
132
+ const stale = await exchanges.findById(agentContext, record.credentialExchangeRecordId);
133
+ if (stale) {
134
+ stale.parentThreadId = undefined;
135
+ await exchanges.update(agentContext, stale);
136
+ }
137
+ }
138
+ record.credentialExchangeRecordId = undefined;
139
+ record.subprotocolThid = undefined;
140
+ record.issuance = undefined;
141
+ }
95
142
  /** Validator-side OnboardingProcess: create or re-attach (by `participant_session_id`) a record in `AWAITING_OR` from an inbound `onboarding-request`. */
96
143
  async processReceiveOnboardingRequest(messageContext) {
97
144
  const { message, agentContext } = messageContext;
98
145
  const connection = messageContext.assertReadyConnection();
99
- await this.checkIsVerifiableService(agentContext, connection);
100
146
  const existing = await this.repository.findByParticipantSessionId(agentContext, message.participantSessionId, types_1.VtFlowRole.Validator);
147
+ const record = existing !== null && existing !== void 0 ? existing : new repository_1.VtFlowRecord({
148
+ threadId: message.threadId,
149
+ participantSessionId: message.participantSessionId,
150
+ connectionId: connection.id,
151
+ role: types_1.VtFlowRole.Validator,
152
+ state: types_1.VtFlowState.AwaitingOr,
153
+ variant: types_1.VtFlowVariant.OnboardingProcess,
154
+ agentParticipantId: message.agentParticipantId,
155
+ walletAgentParticipantId: message.walletAgentParticipantId,
156
+ applicantParticipantId: message.participantId,
157
+ claims: message.claims,
158
+ proofsAttach: message.proofsAttach,
159
+ });
160
+ await this.checkRequestFromVerifiableService(agentContext, connection, record, !existing, {
161
+ participantId: message.participantId,
162
+ });
101
163
  if (existing) {
102
164
  if ((0, types_1.isVtFlowTerminalState)(existing.state)) {
103
- throw new core_1.CredoError(`vt-flow: participant_session_id '${message.participantSessionId}' collides with a terminated flow`);
165
+ throw new errors_1.VtFlowError(errors_1.VtFlowErrorCode.InvalidParticipantSessionId, `vt-flow: participant_session_id '${message.participantSessionId}' collides with a terminated flow`);
166
+ }
167
+ if (existing.applicantParticipantId !== message.participantId) {
168
+ throw new errors_1.VtFlowError(errors_1.VtFlowErrorCode.InvalidParticipantSessionId, `vt-flow: participant_id '${message.participantId}' does not match the flow of participant_session_id '${message.participantSessionId}'`);
104
169
  }
105
170
  await this.assertSamePeer(agentContext, existing, connection);
171
+ const reconnected = existing.connectionTerminated;
106
172
  existing.connectionId = connection.id;
107
- existing.threadId = message.threadId;
108
- if (message.claims)
109
- existing.claims = message.claims;
110
- if (message.proofsAttach)
111
- existing.proofsAttach = message.proofsAttach;
112
- if (existing.state === types_1.VtFlowState.Completed || existing.state === types_1.VtFlowState.CredRevoked) {
173
+ existing.connectionTerminated = undefined;
174
+ if (existing.threadId !== message.threadId) {
175
+ existing.threadId = message.threadId;
176
+ if (message.claims)
177
+ existing.claims = message.claims;
178
+ if (message.proofsAttach)
179
+ existing.proofsAttach = message.proofsAttach;
180
+ }
181
+ if ((0, types_1.isVtFlowRenewable)(existing)) {
113
182
  // A finished flow re-entered with a new OR is a renewal (VSA-VTI-FLOW-OP-RENEW): re-run it.
114
- existing.oobLinkUrl = undefined;
183
+ existing.oobLink = undefined;
184
+ existing.validation = undefined;
185
+ existing.issuance = undefined;
115
186
  await this.updateState(agentContext, existing, types_1.VtFlowState.AwaitingOr);
116
187
  }
188
+ else if (existing.state === types_1.VtFlowState.CredOffered) {
189
+ await this.releaseCredentialExchange(agentContext, existing);
190
+ await this.updateState(agentContext, existing, types_1.VtFlowState.Validated);
191
+ }
117
192
  else {
118
193
  await this.repository.update(agentContext, existing);
194
+ // [VSA-VTI-FLOW-OP-OR] checks every request, and [VSA-ADM-VT-FL-REJECT-2] issuance waits for this reconnection
195
+ if (existing.state === types_1.VtFlowState.AwaitingOr ||
196
+ (reconnected && existing.state === types_1.VtFlowState.Validated)) {
197
+ this.emitStateChanged(agentContext, existing, existing.state);
198
+ }
119
199
  }
120
200
  return existing;
121
201
  }
122
- const record = new repository_1.VtFlowRecord({
123
- threadId: message.threadId,
124
- participantSessionId: message.participantSessionId,
125
- connectionId: connection.id,
126
- role: types_1.VtFlowRole.Validator,
127
- state: types_1.VtFlowState.AwaitingOr,
128
- variant: types_1.VtFlowVariant.OnboardingProcess,
129
- agentParticipantId: message.agentParticipantId,
130
- walletAgentParticipantId: message.walletAgentParticipantId,
131
- participantId: message.participantId,
132
- claims: message.claims,
133
- proofsAttach: message.proofsAttach,
134
- });
135
202
  await this.repository.save(agentContext, record);
136
203
  this.emitStateChanged(agentContext, record, null);
137
204
  return record;
@@ -140,19 +207,8 @@ let VtFlowService = class VtFlowService {
140
207
  async processReceiveIssuanceRequest(messageContext) {
141
208
  const { message, agentContext } = messageContext;
142
209
  const connection = messageContext.assertReadyConnection();
143
- await this.checkIsVerifiableService(agentContext, connection);
144
210
  const existing = await this.repository.findByParticipantSessionId(agentContext, message.participantSessionId, types_1.VtFlowRole.Validator);
145
- if (existing) {
146
- if ((0, types_1.isVtFlowTerminalState)(existing.state)) {
147
- throw new core_1.CredoError(`vt-flow: participant_session_id '${message.participantSessionId}' collides with a terminated flow`);
148
- }
149
- await this.assertSamePeer(agentContext, existing, connection);
150
- existing.connectionId = connection.id;
151
- existing.threadId = message.threadId;
152
- await this.repository.update(agentContext, existing);
153
- return existing;
154
- }
155
- const record = new repository_1.VtFlowRecord({
211
+ const record = existing !== null && existing !== void 0 ? existing : new repository_1.VtFlowRecord({
156
212
  threadId: message.threadId,
157
213
  participantSessionId: message.participantSessionId,
158
214
  connectionId: connection.id,
@@ -164,20 +220,61 @@ let VtFlowService = class VtFlowService {
164
220
  schemaId: message.schemaId,
165
221
  claims: message.claims,
166
222
  });
223
+ await this.checkRequestFromVerifiableService(agentContext, connection, record, !existing, {
224
+ schemaId: message.schemaId,
225
+ });
226
+ if (existing) {
227
+ if ((0, types_1.isVtFlowTerminalState)(existing.state)) {
228
+ throw new errors_1.VtFlowError(errors_1.VtFlowErrorCode.InvalidParticipantSessionId, `vt-flow: participant_session_id '${message.participantSessionId}' collides with a terminated flow`);
229
+ }
230
+ if (existing.variant !== types_1.VtFlowVariant.DirectIssuance || existing.schemaId !== message.schemaId) {
231
+ throw new errors_1.VtFlowError(errors_1.VtFlowErrorCode.InvalidParticipantSessionId, `vt-flow: schema_id '${message.schemaId}' does not match the flow of participant_session_id '${message.participantSessionId}'`);
232
+ }
233
+ await this.assertSamePeer(agentContext, existing, connection);
234
+ existing.connectionId = connection.id;
235
+ existing.threadId = message.threadId;
236
+ if (existing.state === types_1.VtFlowState.CredOffered) {
237
+ await this.releaseCredentialExchange(agentContext, existing);
238
+ await this.updateState(agentContext, existing, types_1.VtFlowState.Validating);
239
+ }
240
+ else {
241
+ await this.repository.update(agentContext, existing);
242
+ }
243
+ return existing;
244
+ }
167
245
  await this.repository.save(agentContext, record);
168
246
  this.emitStateChanged(agentContext, record, null);
169
247
  return record;
170
248
  }
171
- /** Applicant-side inbound `oob-link`; transitions the session to `OOB_PENDING`. */
249
+ /** Applicant-side inbound `oob-link`; `OR_SENT`, `IR_SENT`, `VALIDATING` or `OOB_PENDING` => `OOB_PENDING`. */
172
250
  async processReceiveOobLink(messageContext) {
251
+ var _a;
173
252
  const { message, agentContext } = messageContext;
174
253
  messageContext.assertReadyConnection();
175
254
  const record = await this.getByThreadId(agentContext, message.threadId);
176
255
  record.assertRole(types_1.VtFlowRole.Applicant);
256
+ record.assertState([
257
+ types_1.VtFlowState.OrSent,
258
+ types_1.VtFlowState.IrSent,
259
+ types_1.VtFlowState.Validating,
260
+ types_1.VtFlowState.OobPending,
261
+ ]);
262
+ record.oobLink = {
263
+ url: message.url,
264
+ description: message.description,
265
+ expiresAt: (_a = message.expiresTime) === null || _a === void 0 ? void 0 : _a.toISOString(),
266
+ at: new Date().toISOString(),
267
+ };
268
+ this.appendMessage(record, {
269
+ type: types_1.VtFlowMessageType.OobLink,
270
+ text: message.description,
271
+ at: record.oobLink.at,
272
+ url: message.url,
273
+ });
177
274
  await this.updateState(agentContext, record, types_1.VtFlowState.OobPending);
178
275
  return record;
179
276
  }
180
- /** Applicant-side inbound `validating`; OnboardingProcess applicant transitions `OR_SENT => VALIDATING`. */
277
+ /** Applicant-side inbound `validating`; `OR_SENT`, `IR_SENT` or `OOB_PENDING` => `VALIDATING`. */
181
278
  async processReceiveValidating(messageContext) {
182
279
  var _a;
183
280
  const { message, agentContext } = messageContext;
@@ -185,8 +282,16 @@ let VtFlowService = class VtFlowService {
185
282
  const record = await this.getByThreadId(agentContext, message.threadId);
186
283
  this.logger.debug(`[vt-flow] validating received for session ${record.threadId}: ${(_a = message.comment) !== null && _a !== void 0 ? _a : '(no comment)'}`);
187
284
  if (record.role === types_1.VtFlowRole.Applicant &&
188
- record.variant === types_1.VtFlowVariant.OnboardingProcess &&
189
- record.state === types_1.VtFlowState.OrSent) {
285
+ (message.comment || record.state === types_1.VtFlowState.OobPending)) {
286
+ this.appendMessage(record, {
287
+ type: types_1.VtFlowMessageType.Validating,
288
+ text: message.comment,
289
+ at: new Date().toISOString(),
290
+ });
291
+ await this.updateRecord(agentContext, record);
292
+ }
293
+ if (record.role === types_1.VtFlowRole.Applicant &&
294
+ [types_1.VtFlowState.OrSent, types_1.VtFlowState.IrSent, types_1.VtFlowState.OobPending].includes(record.state)) {
190
295
  await this.updateState(agentContext, record, types_1.VtFlowState.Validating);
191
296
  }
192
297
  return record;
@@ -208,14 +313,26 @@ let VtFlowService = class VtFlowService {
208
313
  }
209
314
  return record;
210
315
  }
211
- /** `AWAITING_OR => VALIDATING`; caller is expected to have verified participant/agent/wallet IDs on-chain. */
316
+ /** `AWAITING_OR => VALIDATING` once the `checkParticipantId` hook accepts the `participant_id`, with no transition and no message once it reports the request settled by a `VALIDATED` entry; caller is expected to have verified agent/wallet IDs on-chain. */
212
317
  async acceptOnboardingRequest(agentContext, recordId) {
213
- const record = await this.repository.getById(agentContext, recordId);
318
+ let record = await this.repository.getById(agentContext, recordId);
214
319
  record.assertRole(types_1.VtFlowRole.Validator);
215
320
  record.assertState(types_1.VtFlowState.AwaitingOr);
216
321
  record.assertVariant(types_1.VtFlowVariant.OnboardingProcess);
322
+ const { checkParticipantId } = this.config;
323
+ if (checkParticipantId) {
324
+ const checked = await checkParticipantId({ agentContext, record });
325
+ if (!checked) {
326
+ throw new errors_1.VtFlowError(errors_1.VtFlowErrorCode.InvalidParticipantId, `vt-flow: participant_id '${record.applicantParticipantId}' is not a PENDING entry that names this agent as its validator`);
327
+ }
328
+ // the check can wait for the indexer while the flow moves on
329
+ record = await this.repository.getById(agentContext, recordId);
330
+ if (checked === 'validated')
331
+ return { record };
332
+ record.assertState(types_1.VtFlowState.AwaitingOr);
333
+ }
217
334
  await this.updateState(agentContext, record, types_1.VtFlowState.Validating);
218
- return record;
335
+ return { record, message: new messages_1.ValidatingMessage({ threadId: record.threadId }) };
219
336
  }
220
337
  /** DirectIssuance: `AWAITING_IR => VALIDATING`; transient before `CRED_OFFERED`. */
221
338
  async acceptIssuanceRequest(agentContext, recordId) {
@@ -224,23 +341,44 @@ let VtFlowService = class VtFlowService {
224
341
  record.assertState(types_1.VtFlowState.AwaitingIr);
225
342
  record.assertVariant(types_1.VtFlowVariant.DirectIssuance);
226
343
  await this.updateState(agentContext, record, types_1.VtFlowState.Validating);
227
- return record;
344
+ return { record, message: new messages_1.ValidatingMessage({ threadId: record.threadId }) };
228
345
  }
229
- /** Reject with a problem-report; transitions to `TERMINATED_BY_{role}` and marks the connection as `TERMINATED`. */
346
+ /** Reject with a problem-report; a fatal code moves the sender to the Error Codes state, a retryable one returns a validator to `AWAITING_OR` or `AWAITING_IR`. */
230
347
  async rejectRequest(agentContext, recordId, params) {
231
348
  var _a;
232
349
  const record = await this.repository.getById(agentContext, recordId);
350
+ if ((0, types_1.isVtFlowTerminalState)(record.state)) {
351
+ throw new core_1.CredoError(`vt-flow: flow '${record.id}' in state ${record.state} cannot be rejected`);
352
+ }
233
353
  const problemReport = (0, errors_1.buildVtFlowProblemReport)({
234
354
  code: params.code,
235
355
  threadId: record.threadId,
236
356
  enDescription: params.enDescription,
237
357
  fixHintEn: params.fixHintEn,
238
358
  });
239
- const nextState = record.role === types_1.VtFlowRole.Validator
240
- ? types_1.VtFlowState.TerminatedByValidator
241
- : types_1.VtFlowState.TerminatedByApplicant;
242
- record.errorMessage = (_a = params.enDescription) !== null && _a !== void 0 ? _a : params.code;
243
- await this.updateState(agentContext, record, nextState);
359
+ const info = errors_1.VT_FLOW_ERROR_INFO[params.code];
360
+ const receiverRole = record.role === types_1.VtFlowRole.Validator ? types_1.VtFlowRole.Applicant : types_1.VtFlowRole.Validator;
361
+ // a fatal code lands both parties in the state the table gives the receiver
362
+ const fatalState = this.resolveErrorFlowState(info.flowState, receiverRole, errors_1.whoRetriesMap[info.whoRetries]);
363
+ const retryState = record.variant === types_1.VtFlowVariant.OnboardingProcess ? types_1.VtFlowState.AwaitingOr : types_1.VtFlowState.AwaitingIr;
364
+ if (!fatalState && record.role === types_1.VtFlowRole.Validator) {
365
+ record.assertState([
366
+ types_1.VtFlowState.AwaitingOr,
367
+ types_1.VtFlowState.AwaitingIr,
368
+ types_1.VtFlowState.Validating,
369
+ types_1.VtFlowState.OobPending,
370
+ ]);
371
+ }
372
+ if (record.role === types_1.VtFlowRole.Validator) {
373
+ this.appendMessage(record, {
374
+ type: types_1.VtFlowMessageType.ProblemReport,
375
+ text: problemReport.description.en,
376
+ at: new Date().toISOString(),
377
+ });
378
+ }
379
+ if (fatalState)
380
+ record.errorMessage = (_a = params.enDescription) !== null && _a !== void 0 ? _a : params.code;
381
+ await this.updateState(agentContext, record, fatalState !== null && fatalState !== void 0 ? fatalState : (record.role === types_1.VtFlowRole.Validator ? retryState : record.state));
244
382
  return { record, problemReport };
245
383
  }
246
384
  /** Applicant-side termination producing a problem-report and landing in `TERMINATED_BY_APPLICANT`. */
@@ -264,6 +402,17 @@ let VtFlowService = class VtFlowService {
264
402
  var _a, _b;
265
403
  const record = await this.repository.getById(agentContext, recordId);
266
404
  record.assertRole(types_1.VtFlowRole.Validator);
405
+ record.assertState([
406
+ types_1.VtFlowState.AwaitingOr,
407
+ types_1.VtFlowState.AwaitingIr,
408
+ types_1.VtFlowState.Validating,
409
+ types_1.VtFlowState.OobPending,
410
+ types_1.VtFlowState.AwaitingValidationTx,
411
+ types_1.VtFlowState.ValidationTxFailed,
412
+ // VtFlowModule ends the flow here when auto-issue throws. A failed anchoring stays in CRED_OFFERED instead
413
+ types_1.VtFlowState.CredOffered,
414
+ types_1.VtFlowState.Completed,
415
+ ]);
267
416
  const code = (_a = params.code) !== null && _a !== void 0 ? _a : errors_1.VtFlowErrorCode.SessionTerminated;
268
417
  const problemReport = (0, errors_1.buildVtFlowProblemReport)({
269
418
  code,
@@ -272,6 +421,12 @@ let VtFlowService = class VtFlowService {
272
421
  fixHintEn: params.fixHintEn,
273
422
  });
274
423
  record.errorMessage = (_b = params.enDescription) !== null && _b !== void 0 ? _b : code;
424
+ this.appendMessage(record, {
425
+ type: types_1.VtFlowMessageType.ProblemReport,
426
+ text: problemReport.description.en,
427
+ at: new Date().toISOString(),
428
+ });
429
+ record.connectionTerminated = true;
275
430
  await this.updateState(agentContext, record, types_1.VtFlowState.TerminatedByValidator);
276
431
  return { record, problemReport };
277
432
  }
@@ -288,62 +443,120 @@ let VtFlowService = class VtFlowService {
288
443
  await this.updateState(agentContext, record, params.state);
289
444
  return { record, problemReport };
290
445
  }
291
- /** Build an `oob-link`; non-terminal records transition to `OOB_PENDING`. */
446
+ /** Build an `oob-link` from `VALIDATING` or `OOB_PENDING`; the flow moves to or stays in `OOB_PENDING`. */
292
447
  async sendOobLinkForSession(agentContext, recordId, params) {
448
+ var _a;
293
449
  const record = await this.repository.getById(agentContext, recordId);
294
450
  record.assertRole(types_1.VtFlowRole.Validator);
295
- record.assertState([
296
- types_1.VtFlowState.AwaitingOr,
297
- types_1.VtFlowState.AwaitingIr,
298
- types_1.VtFlowState.Validating,
299
- types_1.VtFlowState.OobPending,
300
- types_1.VtFlowState.Completed,
301
- ]);
451
+ record.assertState([types_1.VtFlowState.Validating, types_1.VtFlowState.OobPending]);
302
452
  const message = new messages_1.OobLinkMessage({
303
453
  threadId: record.threadId,
304
454
  url: params.url,
305
455
  description: params.description,
306
456
  expiresTime: params.expiresTime,
307
457
  });
308
- record.oobLinkUrl = params.url;
309
- if (record.state !== types_1.VtFlowState.Completed) {
310
- await this.updateState(agentContext, record, types_1.VtFlowState.OobPending);
311
- }
312
- else {
313
- await this.updateRecord(agentContext, record);
314
- }
458
+ record.oobLink = {
459
+ url: params.url,
460
+ description: params.description,
461
+ expiresAt: (_a = params.expiresTime) === null || _a === void 0 ? void 0 : _a.toISOString(),
462
+ at: new Date().toISOString(),
463
+ };
464
+ this.appendMessage(record, {
465
+ type: types_1.VtFlowMessageType.OobLink,
466
+ text: params.description,
467
+ at: record.oobLink.at,
468
+ url: params.url,
469
+ });
470
+ await this.updateState(agentContext, record, types_1.VtFlowState.OobPending);
315
471
  return { record, message };
316
472
  }
317
- /** Build a `validating` informational message; no state change on the Validator side. */
473
+ /** `OOB_PENDING` => `VALIDATING`, with the `validating` that tells the applicant its out-of-band step is done. */
318
474
  async sendValidatingForSession(agentContext, recordId, params = {}) {
319
475
  const record = await this.repository.getById(agentContext, recordId);
320
476
  record.assertRole(types_1.VtFlowRole.Validator);
477
+ record.assertState(types_1.VtFlowState.OobPending);
321
478
  const message = new messages_1.ValidatingMessage({
322
479
  threadId: record.threadId,
323
480
  comment: params.comment,
324
481
  });
482
+ this.appendMessage(record, {
483
+ type: types_1.VtFlowMessageType.Validating,
484
+ text: params.comment,
485
+ at: new Date().toISOString(),
486
+ });
487
+ await this.updateState(agentContext, record, types_1.VtFlowState.Validating);
325
488
  return { record, message };
326
489
  }
327
- /** `VALIDATING => VALIDATED`; call after `SetParticipantOPtoValidated` lands on-chain. */
490
+ /** Record the validation decision or its transaction outcome, moving the flow when `state` is given. */
491
+ async recordValidation(agentContext, recordId, validation, state) {
492
+ const record = await this.repository.getById(agentContext, recordId);
493
+ record.assertRole(types_1.VtFlowRole.Validator);
494
+ record.validation = validation;
495
+ if (state)
496
+ await this.updateState(agentContext, record, state);
497
+ else
498
+ await this.updateRecord(agentContext, record);
499
+ return record;
500
+ }
501
+ /** Record the outcome of the `CreateOrUpdateParticipantSession` transaction that anchors the issued credential. */
502
+ async recordIssuance(agentContext, recordId, issuance) {
503
+ const record = await this.repository.getById(agentContext, recordId);
504
+ record.assertRole(types_1.VtFlowRole.Validator);
505
+ record.issuance = issuance;
506
+ await this.updateRecord(agentContext, record);
507
+ return record;
508
+ }
509
+ /** `VALIDATED` => `VALIDATED_PENDING_CLAIMS`, when the claims fail the schema at offer time ([VSA-VTI-FLOW-OP-ISSUE-4]). */
510
+ async markPendingClaims(agentContext, recordId) {
511
+ const record = await this.repository.getById(agentContext, recordId);
512
+ record.assertRole(types_1.VtFlowRole.Validator);
513
+ record.assertState(types_1.VtFlowState.Validated);
514
+ await this.updateState(agentContext, record, types_1.VtFlowState.ValidatedPendingClaims);
515
+ return record;
516
+ }
517
+ /** Validator-side states preceding `VALIDATED` => `VALIDATED`; call after `SetParticipantOPtoValidated` lands on-chain. */
328
518
  async markValidated(agentContext, recordId) {
329
519
  const record = await this.repository.getById(agentContext, recordId);
330
520
  record.assertRole(types_1.VtFlowRole.Validator);
331
- record.assertState([types_1.VtFlowState.Validating, types_1.VtFlowState.OobPending]);
521
+ record.assertState([...types_1.VtFlowValidatedFromStates]);
332
522
  record.assertVariant(types_1.VtFlowVariant.OnboardingProcess);
333
523
  await this.updateState(agentContext, record, types_1.VtFlowState.Validated);
334
524
  return record;
335
525
  }
336
- /** Link a Credo exchange record to the session and transition to `CRED_OFFERED`. */
337
- async attachCredentialExchangeRecord(agentContext, recordId, credentialExchangeRecord, credentialDigest, issuerParticipantId) {
526
+ /**
527
+ * Close an onboarding process that carries no credential exchange. Only a HOLDER receives a
528
+ * credential; for every other role the chain records the outcome with
529
+ * SetParticipantOPToValidated, and nothing else follows. Both sides use this, so it asserts no
530
+ * role: the validator calls it after it validates, and the applicant reaches it from the chain
531
+ * event.
532
+ */
533
+ async markCompleted(agentContext, recordId) {
338
534
  const record = await this.repository.getById(agentContext, recordId);
535
+ record.assertVariant(types_1.VtFlowVariant.OnboardingProcess);
536
+ if (record.state === types_1.VtFlowState.Completed)
537
+ return record;
538
+ if ((0, types_1.isVtFlowTerminalState)(record.state)) {
539
+ throw new core_1.CredoError(`VtFlow record is in terminal state '${record.state}'; cannot complete it.`);
540
+ }
541
+ await this.updateState(agentContext, record, types_1.VtFlowState.Completed);
542
+ return record;
543
+ }
544
+ /** Validator states that an `offer-credential` moves to `CRED_OFFERED`; from `COMPLETED` it starts a new subprotocol run. */
545
+ assertCanOfferCredential(record) {
339
546
  record.assertRole(types_1.VtFlowRole.Validator);
340
547
  record.assertState([
341
548
  types_1.VtFlowState.Validated,
342
- types_1.VtFlowState.Validating,
343
- types_1.VtFlowState.OobPending,
549
+ types_1.VtFlowState.ValidatedPendingClaims,
344
550
  types_1.VtFlowState.Completed,
551
+ ...(record.variant === types_1.VtFlowVariant.DirectIssuance ? [types_1.VtFlowState.Validating] : []),
345
552
  ]);
553
+ }
554
+ /** Link a Credo exchange record to the session and transition to `CRED_OFFERED`. */
555
+ async attachCredentialExchangeRecord(agentContext, recordId, credentialExchangeRecord, credentialDigest, issuerParticipantId, credentialId) {
556
+ const record = await this.repository.getById(agentContext, recordId);
557
+ this.assertCanOfferCredential(record);
346
558
  record.credentialExchangeRecordId = credentialExchangeRecord.id;
559
+ record.credentialId = credentialId;
347
560
  if (credentialDigest)
348
561
  record.credentialDigest = credentialDigest;
349
562
  if (issuerParticipantId !== undefined)
@@ -352,6 +565,13 @@ let VtFlowService = class VtFlowService {
352
565
  await this.updateState(agentContext, record, types_1.VtFlowState.CredOffered);
353
566
  return record;
354
567
  }
568
+ async setEcsSchemaKey(agentContext, recordId, ecsSchemaKey) {
569
+ const record = await this.repository.getById(agentContext, recordId);
570
+ record.assertRole(types_1.VtFlowRole.Applicant);
571
+ record.ecsSchemaKey = ecsSchemaKey;
572
+ await this.repository.update(agentContext, record);
573
+ return record;
574
+ }
355
575
  async setCredentialDigest(agentContext, recordId, credentialDigest) {
356
576
  const record = await this.repository.getById(agentContext, recordId);
357
577
  record.assertRole(types_1.VtFlowRole.Validator);
@@ -380,7 +600,16 @@ let VtFlowService = class VtFlowService {
380
600
  async updateClaims(agentContext, recordId, claims) {
381
601
  const record = await this.repository.getById(agentContext, recordId);
382
602
  record.assertRole(types_1.VtFlowRole.Validator);
383
- record.assertState([types_1.VtFlowState.Validating, types_1.VtFlowState.CredRevoked]);
603
+ record.assertState([
604
+ types_1.VtFlowState.AwaitingOr,
605
+ types_1.VtFlowState.OobPending,
606
+ types_1.VtFlowState.Validating,
607
+ types_1.VtFlowState.AwaitingValidationTx,
608
+ types_1.VtFlowState.ValidationTxSubmitted,
609
+ types_1.VtFlowState.ValidationTxFailed,
610
+ types_1.VtFlowState.ValidatedPendingClaims,
611
+ types_1.VtFlowState.CredRevoked,
612
+ ]);
384
613
  record.claims = claims;
385
614
  await this.updateRecord(agentContext, record);
386
615
  return record;
@@ -401,6 +630,11 @@ let VtFlowService = class VtFlowService {
401
630
  }
402
631
  break;
403
632
  case didcomm_1.DidCommCredentialState.OfferReceived:
633
+ // the offer can beat the chain notification, and an Onboarding Process offers only from VALIDATED
634
+ if (record.variant === types_1.VtFlowVariant.OnboardingProcess &&
635
+ [types_1.VtFlowState.OrSent, types_1.VtFlowState.Validating, types_1.VtFlowState.OobPending].includes(record.state)) {
636
+ await this.updateState(agentContext, record, types_1.VtFlowState.Validated);
637
+ }
404
638
  if (record.state !== types_1.VtFlowState.CredOffered) {
405
639
  await this.updateState(agentContext, record, types_1.VtFlowState.CredOffered);
406
640
  }
@@ -414,6 +648,7 @@ let VtFlowService = class VtFlowService {
414
648
  }
415
649
  break;
416
650
  case didcomm_1.DidCommCredentialState.Abandoned:
651
+ case didcomm_1.DidCommCredentialState.Declined:
417
652
  if (record.state !== types_1.VtFlowState.Error) {
418
653
  record.errorMessage = (_a = record.errorMessage) !== null && _a !== void 0 ? _a : 'Subprotocol abandoned';
419
654
  await this.updateState(agentContext, record, types_1.VtFlowState.Error);
@@ -448,24 +683,111 @@ let VtFlowService = class VtFlowService {
448
683
  /** Transition the record, persist it, and emit `VtFlowStateChanged`. */
449
684
  async updateState(agentContext, record, newState) {
450
685
  const previousState = record.state;
451
- if (previousState === newState)
686
+ if (previousState === newState) {
687
+ await this.repository.update(agentContext, record);
452
688
  return;
689
+ }
690
+ if (previousState === types_1.VtFlowState.OobPending)
691
+ record.oobLink = undefined;
453
692
  record.state = newState;
454
693
  await this.repository.update(agentContext, record);
455
694
  this.emitStateChanged(agentContext, record, previousState);
456
695
  }
696
+ /**
697
+ * Inbound adopted `problem-report`. The Error Codes table gives the state the receiving party
698
+ * moves to, so both sides run this same mapping. A retryable code records the message and leaves
699
+ * the flow where it is.
700
+ */
701
+ async processReceiveProblemReport(messageContext) {
702
+ var _a, _b, _c, _d, _e, _f, _g;
703
+ const { message, agentContext } = messageContext;
704
+ const record = await this.repository.findByThreadId(agentContext, message.threadId);
705
+ if (!record)
706
+ return undefined;
707
+ if ((0, types_1.isVtFlowTerminalState)(record.state))
708
+ return record;
709
+ const code = (_a = message.description) === null || _a === void 0 ? void 0 : _a.code;
710
+ const info = code && (0, errors_1.isVtFlowErrorCode)(code) ? errors_1.VT_FLOW_ERROR_INFO[code] : undefined;
711
+ if (record.role === types_1.VtFlowRole.Applicant) {
712
+ this.appendMessage(record, {
713
+ type: types_1.VtFlowMessageType.ProblemReport,
714
+ text: (_d = (_c = (_b = message.description) === null || _b === void 0 ? void 0 : _b.en) !== null && _c !== void 0 ? _c : code) !== null && _d !== void 0 ? _d : 'problem-report',
715
+ at: new Date().toISOString(),
716
+ });
717
+ }
718
+ const target = info &&
719
+ this.resolveErrorFlowState(info.flowState, record.role, (_e = message.whoRetries) !== null && _e !== void 0 ? _e : errors_1.whoRetriesMap[info.whoRetries]);
720
+ if (!target) {
721
+ await this.updateRecord(agentContext, record);
722
+ return record;
723
+ }
724
+ record.errorMessage = (_g = (_f = message.description) === null || _f === void 0 ? void 0 : _f.en) !== null && _g !== void 0 ? _g : code;
725
+ await this.updateState(agentContext, record, target);
726
+ return record;
727
+ }
728
+ /** `terminated-by-sender` is the peer's termination, so the receiver lands in the state named after the sender. */
729
+ resolveErrorFlowState(flowState, receiverRole, whoRetries) {
730
+ if (flowState === 'unchanged')
731
+ return undefined;
732
+ if (flowState === 'unchanged-when-you') {
733
+ return whoRetries === didcomm_1.WhoRetriesStatus.You ? undefined : types_1.VtFlowState.Error;
734
+ }
735
+ if (flowState === 'error-when-fatal') {
736
+ return whoRetries === didcomm_1.WhoRetriesStatus.None ? types_1.VtFlowState.Error : undefined;
737
+ }
738
+ if (flowState !== 'terminated-by-sender')
739
+ return flowState;
740
+ return receiverRole === types_1.VtFlowRole.Applicant
741
+ ? types_1.VtFlowState.TerminatedByValidator
742
+ : types_1.VtFlowState.TerminatedByApplicant;
743
+ }
744
+ /** Append to `messages[]`, which a validator fills with what it sent and an applicant with what it received. */
745
+ appendMessage(record, message) {
746
+ var _a;
747
+ record.messages = [...((_a = record.messages) !== null && _a !== void 0 ? _a : []), message];
748
+ }
457
749
  /** Persist record changes without state-transition semantics (no event emitted). */
458
750
  async updateRecord(agentContext, record) {
459
751
  await this.repository.update(agentContext, record);
460
752
  }
461
753
  /** participant_session_ids are public on-chain, so only the original peer may re-attach a flow with one. */
462
- async assertSamePeer(agentContext, record, connection) {
754
+ async isSamePeer(agentContext, record, connection) {
463
755
  if (record.connectionId === connection.id)
464
- return;
756
+ return true;
465
757
  const connectionRepository = agentContext.dependencyManager.resolve(didcomm_1.DidCommConnectionRepository);
466
758
  const previous = await connectionRepository.findById(agentContext, record.connectionId);
467
- if ((previous === null || previous === void 0 ? void 0 : previous.theirDid) && connection.theirDid && previous.theirDid !== connection.theirDid) {
468
- throw new core_1.CredoError(`vt-flow: connection '${connection.id}' peer does not match the original connection of participant_session_id '${record.participantSessionId}'`);
759
+ return !((previous === null || previous === void 0 ? void 0 : previous.theirDid) && connection.theirDid && previous.theirDid !== connection.theirDid);
760
+ }
761
+ async assertSamePeer(agentContext, record, connection) {
762
+ if (!(await this.isSamePeer(agentContext, record, connection))) {
763
+ throw new errors_1.VtFlowError(errors_1.VtFlowErrorCode.InvalidParticipantSessionId, `vt-flow: connection '${connection.id}' peer does not match the original connection of participant_session_id '${record.participantSessionId}'`);
764
+ }
765
+ }
766
+ /** VS-CONN-VS failure is fatal, so it ends the flow of the request in ERROR unless that flow has ended or belongs to another peer. */
767
+ async checkRequestFromVerifiableService(agentContext, connection, record, isNew, purpose) {
768
+ try {
769
+ await this.checkIsVerifiableService(agentContext, connection, purpose);
770
+ }
771
+ catch (error) {
772
+ if (error instanceof errors_1.VtFlowError &&
773
+ (isNew ||
774
+ (!(0, types_1.isVtFlowTerminalState)(record.state) && (await this.isSamePeer(agentContext, record, connection))))) {
775
+ record.errorMessage = error.message;
776
+ this.appendMessage(record, {
777
+ type: types_1.VtFlowMessageType.ProblemReport,
778
+ text: (0, errors_1.defaultEnglishDescription)(error.code),
779
+ at: new Date().toISOString(),
780
+ });
781
+ if (isNew) {
782
+ record.state = types_1.VtFlowState.Error;
783
+ await this.repository.save(agentContext, record);
784
+ this.emitStateChanged(agentContext, record, null);
785
+ }
786
+ else {
787
+ await this.updateState(agentContext, record, types_1.VtFlowState.Error);
788
+ }
789
+ }
790
+ throw error;
469
791
  }
470
792
  }
471
793
  emitStateChanged(agentContext, record, previousState) {
@@ -489,25 +811,47 @@ let VtFlowService = class VtFlowService {
489
811
  buildProblemReport(options) {
490
812
  return (0, errors_1.buildVtFlowProblemReport)(options);
491
813
  }
492
- /** Spec Verifiable Service Identity Check: invokes the caller-provided VS-CONN-VS hook. Throws `vt-flow.not-a-verifiable-service` when the peer fails the check. When no hook is configured, logs a warning and permits. */
493
- async checkIsVerifiableService(agentContext, connection) {
494
- const peerDid = connection.theirDid;
495
- if (!peerDid)
814
+ /** Spec Verifiable Service Identity Check: invokes the caller-provided VS-CONN-VS hook. Throws `vt-flow.not-a-verifiable-service` when the peer fails the check. When no hook is configured, logs a warning and permits. A `purpose` is passed on the Validator side only, and lets a rejected peer through the ECS issuance exemption. */
815
+ async checkIsVerifiableService(agentContext, connection, purpose) {
816
+ const peerDid = (0, utils_1.peerAnchorDid)(connection);
817
+ if (!connection.theirDid || !peerDid) {
496
818
  throw new core_1.CredoError(`vt-flow: ready connection '${connection.id}' has no theirDid`);
819
+ }
497
820
  const hook = this.config.assertVerifiableService;
498
821
  if (!hook) {
499
822
  this.logger.warn(`[vt-flow] assertVerifiableService hook not configured; skipping VS-CONN-VS check for peer '${peerDid}'. Configure VtFlowModuleConfig.assertVerifiableService for spec-conformant trust resolution.`);
500
823
  return;
501
824
  }
502
825
  let permitted = false;
826
+ let failure;
503
827
  try {
504
- permitted = await hook({ agentContext, peerDid, connectionId: connection.id });
828
+ permitted = await hook({
829
+ agentContext,
830
+ peerDid,
831
+ connectionId: connection.id,
832
+ });
505
833
  }
506
834
  catch (error) {
507
- throw new core_1.CredoError(`vt-flow.not-a-verifiable-service: peer '${peerDid}' failed VS-CONN-VS check (${error.message})`);
835
+ failure = error.message;
836
+ }
837
+ if (permitted)
838
+ return;
839
+ if (purpose && (await this.isExemptFromVsConnVs(agentContext, connection, peerDid, purpose))) {
840
+ this.logger.info(`[vt-flow] peer '${peerDid}' is not (yet) a Verifiable Service, but the request qualifies for the VS-CONN-VS ECS issuance exemption`);
841
+ return;
842
+ }
843
+ throw new errors_1.VtFlowError(errors_1.VtFlowErrorCode.NotAVerifiableService, `vt-flow.not-a-verifiable-service: peer '${peerDid}' failed VS-CONN-VS check${failure ? ` (${failure})` : ''}`);
844
+ }
845
+ async isExemptFromVsConnVs(agentContext, connection, peerDid, purpose) {
846
+ const exemption = this.config.checkEcsIssuanceExemption;
847
+ if (!exemption)
848
+ return false;
849
+ try {
850
+ return await exemption({ agentContext, peerDid, connectionId: connection.id, purpose });
508
851
  }
509
- if (!permitted) {
510
- throw new core_1.CredoError(`vt-flow.not-a-verifiable-service: peer '${peerDid}' failed VS-CONN-VS check`);
852
+ catch (error) {
853
+ this.logger.warn(`[vt-flow] VS-CONN-VS exemption check failed for peer '${peerDid}': ${error.message}`);
854
+ return false;
511
855
  }
512
856
  }
513
857
  };