@verana-labs/credo-ts-didcomm-vt-flow 2.0.0-dev.7 → 2.0.0-dev.70

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 (54) hide show
  1. package/build/VtFlowApi.d.ts +16 -3
  2. package/build/VtFlowApi.js +51 -11
  3. package/build/VtFlowApi.js.map +1 -1
  4. package/build/VtFlowModule.js +17 -13
  5. package/build/VtFlowModule.js.map +1 -1
  6. package/build/VtFlowModuleConfig.d.ts +27 -3
  7. package/build/VtFlowModuleConfig.js +10 -1
  8. package/build/VtFlowModuleConfig.js.map +1 -1
  9. package/build/errors/VtFlowErrorCode.d.ts +7 -3
  10. package/build/errors/VtFlowErrorCode.js +52 -17
  11. package/build/errors/VtFlowErrorCode.js.map +1 -1
  12. package/build/errors/buildVtFlowProblemReport.d.ts +4 -2
  13. package/build/errors/buildVtFlowProblemReport.js +7 -3
  14. package/build/errors/buildVtFlowProblemReport.js.map +1 -1
  15. package/build/errors/index.d.ts +2 -2
  16. package/build/errors/index.js +2 -1
  17. package/build/errors/index.js.map +1 -1
  18. package/build/handlers/ProblemReportHandler.d.ts +10 -0
  19. package/build/handlers/ProblemReportHandler.js +17 -0
  20. package/build/handlers/ProblemReportHandler.js.map +1 -0
  21. package/build/handlers/index.d.ts +1 -0
  22. package/build/handlers/index.js +3 -1
  23. package/build/handlers/index.js.map +1 -1
  24. package/build/index.d.ts +5 -4
  25. package/build/index.js +11 -1
  26. package/build/index.js.map +1 -1
  27. package/build/messages/ValidatingMessage.d.ts +1 -1
  28. package/build/messages/ValidatingMessage.js +1 -1
  29. package/build/messages/ValidatingMessage.js.map +1 -1
  30. package/build/messages/VtFlowProblemReportMessage.d.ts +13 -0
  31. package/build/messages/VtFlowProblemReportMessage.js +53 -0
  32. package/build/messages/VtFlowProblemReportMessage.js.map +1 -0
  33. package/build/messages/VtFlowProtocol.d.ts +3 -1
  34. package/build/messages/VtFlowProtocol.js +4 -2
  35. package/build/messages/VtFlowProtocol.js.map +1 -1
  36. package/build/messages/index.d.ts +2 -1
  37. package/build/messages/index.js +4 -1
  38. package/build/messages/index.js.map +1 -1
  39. package/build/repository/VtFlowRecord.d.ts +21 -7
  40. package/build/repository/VtFlowRecord.js +9 -3
  41. package/build/repository/VtFlowRecord.js.map +1 -1
  42. package/build/services/VtFlowService.d.ts +55 -14
  43. package/build/services/VtFlowService.js +280 -50
  44. package/build/services/VtFlowService.js.map +1 -1
  45. package/build/types.d.ts +94 -4
  46. package/build/types.js +55 -2
  47. package/build/types.js.map +1 -1
  48. package/build/utils/index.d.ts +1 -0
  49. package/build/utils/index.js +6 -0
  50. package/build/utils/index.js.map +1 -0
  51. package/build/utils/peerAnchorDid.d.ts +2 -0
  52. package/build/utils/peerAnchorDid.js +8 -0
  53. package/build/utils/peerAnchorDid.js.map +1 -0
  54. package/package.json +5 -5
@@ -1,15 +1,18 @@
1
1
  import type { AgentContext, Logger, Query, QueryOptions } from '@credo-ts/core';
2
2
  import type { DidCommConnectionRecord, DidCommCredentialExchangeRecord, DidCommInboundMessageContext } from '@credo-ts/didcomm';
3
3
  import { EventEmitter } from '@credo-ts/core';
4
- import { VtFlowModuleConfig } from '../VtFlowModuleConfig';
4
+ import { VtFlowModuleConfig, type VtFlowRequestPurpose } from '../VtFlowModuleConfig';
5
5
  import { type BuildVtFlowProblemReportOptions, VtFlowErrorCode, buildVtFlowProblemReport } from '../errors';
6
- import { CredentialStateChangeMessage, type CredentialStateChangeMessageOptions, IssuanceRequestMessage, OnboardingRequestMessage, OobLinkMessage, type OobLinkMessageOptions, ValidatingMessage, type ValidatingMessageOptions } from '../messages';
6
+ import { CredentialStateChangeMessage, type CredentialStateChangeMessageOptions, IssuanceRequestMessage, OnboardingRequestMessage, OobLinkMessage, type OobLinkMessageOptions, VtFlowProblemReportMessage, ValidatingMessage, type ValidatingMessageOptions } from '../messages';
7
7
  import { VtFlowRecord, VtFlowRepository } from '../repository';
8
- import { VtFlowState } from '../types';
8
+ import { type VtFlowMessage, type VtFlowValidation, VtFlowState } from '../types';
9
9
  export interface CreateOnboardingRequestParams {
10
10
  connectionId: string;
11
11
  participantSessionId: string;
12
- participantId: string;
12
+ applicantParticipantId: string;
13
+ applicantParticipantRole?: number;
14
+ validatorParticipantId?: string;
15
+ schemaId?: string;
13
16
  agentParticipantId: string;
14
17
  walletAgentParticipantId: string;
15
18
  claims?: Record<string, unknown>;
@@ -50,20 +53,34 @@ export declare class VtFlowService {
50
53
  message: IssuanceRequestMessage;
51
54
  record: VtFlowRecord;
52
55
  }>;
56
+ reattachOnboardingProcessRecord(agentContext: AgentContext, params: {
57
+ vtFlowRecordId: string;
58
+ connectionId: string;
59
+ }): Promise<{
60
+ message: OnboardingRequestMessage;
61
+ record: VtFlowRecord;
62
+ }>;
63
+ private releaseCredentialExchange;
53
64
  /** Validator-side OnboardingProcess: create or re-attach (by `participant_session_id`) a record in `AWAITING_OR` from an inbound `onboarding-request`. */
54
65
  processReceiveOnboardingRequest(messageContext: DidCommInboundMessageContext<OnboardingRequestMessage>): Promise<VtFlowRecord>;
55
66
  /** Validator-side DirectIssuance counterpart of `processReceiveOnboardingRequest`, landing the record in `AWAITING_IR`. */
56
67
  processReceiveIssuanceRequest(messageContext: DidCommInboundMessageContext<IssuanceRequestMessage>): Promise<VtFlowRecord>;
57
- /** Applicant-side inbound `oob-link`; transitions the session to `OOB_PENDING`. */
68
+ /** Applicant-side inbound `oob-link`; `OR_SENT`, `IR_SENT`, `VALIDATING` or `OOB_PENDING` => `OOB_PENDING`. */
58
69
  processReceiveOobLink(messageContext: DidCommInboundMessageContext<OobLinkMessage>): Promise<VtFlowRecord>;
59
- /** Applicant-side inbound `validating`; OnboardingProcess applicant transitions `OR_SENT => VALIDATING`. */
70
+ /** Applicant-side inbound `validating`; `OR_SENT`, `IR_SENT` or `OOB_PENDING` => `VALIDATING`. */
60
71
  processReceiveValidating(messageContext: DidCommInboundMessageContext<ValidatingMessage>): Promise<VtFlowRecord>;
61
72
  /** Applicant-side inbound `credential-state-change`; v1.0 handles `REVOKED` only. */
62
73
  processReceiveCredentialStateChange(messageContext: DidCommInboundMessageContext<CredentialStateChangeMessage>): Promise<VtFlowRecord | null>;
63
74
  /** `AWAITING_OR => VALIDATING`; caller is expected to have verified participant/agent/wallet IDs on-chain. */
64
- acceptOnboardingRequest(agentContext: AgentContext, recordId: string): Promise<VtFlowRecord>;
75
+ acceptOnboardingRequest(agentContext: AgentContext, recordId: string): Promise<{
76
+ record: VtFlowRecord;
77
+ message: ValidatingMessage;
78
+ }>;
65
79
  /** DirectIssuance: `AWAITING_IR => VALIDATING`; transient before `CRED_OFFERED`. */
66
- acceptIssuanceRequest(agentContext: AgentContext, recordId: string): Promise<VtFlowRecord>;
80
+ acceptIssuanceRequest(agentContext: AgentContext, recordId: string): Promise<{
81
+ record: VtFlowRecord;
82
+ message: ValidatingMessage;
83
+ }>;
67
84
  /** Reject with a problem-report; transitions to `TERMINATED_BY_{role}` and marks the connection as `TERMINATED`. */
68
85
  rejectRequest(agentContext: AgentContext, recordId: string, params: RejectRequestParams): Promise<{
69
86
  record: VtFlowRecord;
@@ -88,20 +105,33 @@ export declare class VtFlowService {
88
105
  record: VtFlowRecord;
89
106
  problemReport: ReturnType<typeof buildVtFlowProblemReport>;
90
107
  }>;
91
- /** Build an `oob-link`; non-terminal records transition to `OOB_PENDING`. */
108
+ /** Build an `oob-link` from `VALIDATING` or `OOB_PENDING`; the flow moves to or stays in `OOB_PENDING`. */
92
109
  sendOobLinkForSession(agentContext: AgentContext, recordId: string, params: SendOobLinkParams): Promise<{
93
110
  record: VtFlowRecord;
94
111
  message: OobLinkMessage;
95
112
  }>;
96
- /** Build a `validating` informational message; no state change on the Validator side. */
113
+ /** `OOB_PENDING` => `VALIDATING`, with the `validating` that tells the applicant its out-of-band step is done. */
97
114
  sendValidatingForSession(agentContext: AgentContext, recordId: string, params?: SendValidatingParams): Promise<{
98
115
  record: VtFlowRecord;
99
116
  message: ValidatingMessage;
100
117
  }>;
101
- /** `VALIDATING => VALIDATED`; call after `SetParticipantOPtoValidated` lands on-chain. */
118
+ /** Record the validation decision or its transaction outcome, moving the flow when `state` is given. */
119
+ recordValidation(agentContext: AgentContext, recordId: string, validation: VtFlowValidation, state?: VtFlowState): Promise<VtFlowRecord>;
120
+ /** Validator-side states preceding `VALIDATED` => `VALIDATED`; call after `SetParticipantOPtoValidated` lands on-chain. */
102
121
  markValidated(agentContext: AgentContext, recordId: string): Promise<VtFlowRecord>;
122
+ /**
123
+ * Close an onboarding process that carries no credential exchange. Only a HOLDER receives a
124
+ * credential; for every other role the chain records the outcome with
125
+ * SetParticipantOPToValidated, and nothing else follows. Both sides use this, so it asserts no
126
+ * role: the validator calls it after it validates, and the applicant reaches it from the chain
127
+ * event.
128
+ */
129
+ markCompleted(agentContext: AgentContext, recordId: string): Promise<VtFlowRecord>;
130
+ /** Validator states that an `offer-credential` moves to `CRED_OFFERED`; from `COMPLETED` it starts a new subprotocol run. */
131
+ assertCanOfferCredential(record: VtFlowRecord): void;
103
132
  /** Link a Credo exchange record to the session and transition to `CRED_OFFERED`. */
104
133
  attachCredentialExchangeRecord(agentContext: AgentContext, recordId: string, credentialExchangeRecord: DidCommCredentialExchangeRecord, credentialDigest?: string, issuerParticipantId?: number): Promise<VtFlowRecord>;
134
+ setEcsSchemaKey(agentContext: AgentContext, recordId: string, ecsSchemaKey: string): Promise<VtFlowRecord>;
105
135
  setCredentialDigest(agentContext: AgentContext, recordId: string, credentialDigest: string): Promise<VtFlowRecord>;
106
136
  /** Validator-side `credential-state-change`; `COMPLETED => CRED_REVOKED`, re-notify allowed from `CRED_REVOKED`. */
107
137
  notifyCredentialStateChange(agentContext: AgentContext, recordId: string, params: NotifyCredentialStateChangeParams): Promise<{
@@ -119,6 +149,16 @@ export declare class VtFlowService {
119
149
  findAllByQuery(agentContext: AgentContext, query: Query<VtFlowRecord>, queryOptions?: QueryOptions): Promise<VtFlowRecord[]>;
120
150
  /** Transition the record, persist it, and emit `VtFlowStateChanged`. */
121
151
  updateState(agentContext: AgentContext, record: VtFlowRecord, newState: VtFlowState): Promise<void>;
152
+ /**
153
+ * Inbound adopted `problem-report`. The Error Codes table gives the state the receiving party
154
+ * moves to, so both sides run this same mapping. A retryable code records the message and leaves
155
+ * the flow where it is.
156
+ */
157
+ processReceiveProblemReport(messageContext: DidCommInboundMessageContext<VtFlowProblemReportMessage>): Promise<VtFlowRecord | undefined>;
158
+ /** `terminated-by-sender` is the peer's termination, so the receiver lands in the state named after the sender. */
159
+ private resolveErrorFlowState;
160
+ /** Append to `messages[]`, which a validator fills with what it sent and an applicant with what it received. */
161
+ appendMessage(record: VtFlowRecord, message: VtFlowMessage): void;
122
162
  /** Persist record changes without state-transition semantics (no event emitted). */
123
163
  updateRecord(agentContext: AgentContext, record: VtFlowRecord): Promise<void>;
124
164
  /** participant_session_ids are public on-chain, so only the original peer may re-attach a flow with one. */
@@ -126,7 +166,8 @@ export declare class VtFlowService {
126
166
  private emitStateChanged;
127
167
  getModuleConfig(): VtFlowModuleConfig;
128
168
  getLogger(): Logger;
129
- buildProblemReport(options: BuildVtFlowProblemReportOptions): import("@credo-ts/didcomm").DidCommProblemReportMessage;
130
- /** 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. */
131
- checkIsVerifiableService(agentContext: AgentContext, connection: DidCommConnectionRecord): Promise<void>;
169
+ buildProblemReport(options: BuildVtFlowProblemReportOptions): VtFlowProblemReportMessage;
170
+ /** 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. */
171
+ checkIsVerifiableService(agentContext: AgentContext, connection: DidCommConnectionRecord, purpose?: VtFlowRequestPurpose): Promise<void>;
172
+ private isExemptFromVsConnVs;
132
173
  }
@@ -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,
@@ -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,28 +96,79 @@ 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
+ }
95
141
  /** Validator-side OnboardingProcess: create or re-attach (by `participant_session_id`) a record in `AWAITING_OR` from an inbound `onboarding-request`. */
96
142
  async processReceiveOnboardingRequest(messageContext) {
97
143
  const { message, agentContext } = messageContext;
98
144
  const connection = messageContext.assertReadyConnection();
99
- await this.checkIsVerifiableService(agentContext, connection);
145
+ await this.checkIsVerifiableService(agentContext, connection, { participantId: message.participantId });
100
146
  const existing = await this.repository.findByParticipantSessionId(agentContext, message.participantSessionId, types_1.VtFlowRole.Validator);
101
147
  if (existing) {
102
148
  if ((0, types_1.isVtFlowTerminalState)(existing.state)) {
103
149
  throw new core_1.CredoError(`vt-flow: participant_session_id '${message.participantSessionId}' collides with a terminated flow`);
104
150
  }
151
+ if (existing.applicantParticipantId !== message.participantId) {
152
+ throw new core_1.CredoError(`vt-flow: participant_id '${message.participantId}' does not match the flow of participant_session_id '${message.participantSessionId}'`);
153
+ }
105
154
  await this.assertSamePeer(agentContext, existing, connection);
106
155
  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;
156
+ if (existing.threadId !== message.threadId) {
157
+ existing.threadId = message.threadId;
158
+ if (message.claims)
159
+ existing.claims = message.claims;
160
+ if (message.proofsAttach)
161
+ existing.proofsAttach = message.proofsAttach;
162
+ }
112
163
  if (existing.state === types_1.VtFlowState.Completed || existing.state === types_1.VtFlowState.CredRevoked) {
113
164
  // A finished flow re-entered with a new OR is a renewal (VSA-VTI-FLOW-OP-RENEW): re-run it.
114
- existing.oobLinkUrl = undefined;
165
+ existing.oobLink = undefined;
115
166
  await this.updateState(agentContext, existing, types_1.VtFlowState.AwaitingOr);
116
167
  }
168
+ else if (existing.state === types_1.VtFlowState.CredOffered) {
169
+ await this.releaseCredentialExchange(agentContext, existing);
170
+ await this.updateState(agentContext, existing, types_1.VtFlowState.Validated);
171
+ }
117
172
  else {
118
173
  await this.repository.update(agentContext, existing);
119
174
  }
@@ -128,7 +183,7 @@ let VtFlowService = class VtFlowService {
128
183
  variant: types_1.VtFlowVariant.OnboardingProcess,
129
184
  agentParticipantId: message.agentParticipantId,
130
185
  walletAgentParticipantId: message.walletAgentParticipantId,
131
- participantId: message.participantId,
186
+ applicantParticipantId: message.participantId,
132
187
  claims: message.claims,
133
188
  proofsAttach: message.proofsAttach,
134
189
  });
@@ -140,16 +195,25 @@ let VtFlowService = class VtFlowService {
140
195
  async processReceiveIssuanceRequest(messageContext) {
141
196
  const { message, agentContext } = messageContext;
142
197
  const connection = messageContext.assertReadyConnection();
143
- await this.checkIsVerifiableService(agentContext, connection);
198
+ await this.checkIsVerifiableService(agentContext, connection, { schemaId: message.schemaId });
144
199
  const existing = await this.repository.findByParticipantSessionId(agentContext, message.participantSessionId, types_1.VtFlowRole.Validator);
145
200
  if (existing) {
146
201
  if ((0, types_1.isVtFlowTerminalState)(existing.state)) {
147
202
  throw new core_1.CredoError(`vt-flow: participant_session_id '${message.participantSessionId}' collides with a terminated flow`);
148
203
  }
204
+ if (existing.variant !== types_1.VtFlowVariant.DirectIssuance || existing.schemaId !== message.schemaId) {
205
+ throw new core_1.CredoError(`vt-flow: schema_id '${message.schemaId}' does not match the flow of participant_session_id '${message.participantSessionId}'`);
206
+ }
149
207
  await this.assertSamePeer(agentContext, existing, connection);
150
208
  existing.connectionId = connection.id;
151
209
  existing.threadId = message.threadId;
152
- await this.repository.update(agentContext, existing);
210
+ if (existing.state === types_1.VtFlowState.CredOffered) {
211
+ await this.releaseCredentialExchange(agentContext, existing);
212
+ await this.updateState(agentContext, existing, types_1.VtFlowState.Validating);
213
+ }
214
+ else {
215
+ await this.repository.update(agentContext, existing);
216
+ }
153
217
  return existing;
154
218
  }
155
219
  const record = new repository_1.VtFlowRecord({
@@ -168,25 +232,51 @@ let VtFlowService = class VtFlowService {
168
232
  this.emitStateChanged(agentContext, record, null);
169
233
  return record;
170
234
  }
171
- /** Applicant-side inbound `oob-link`; transitions the session to `OOB_PENDING`. */
235
+ /** Applicant-side inbound `oob-link`; `OR_SENT`, `IR_SENT`, `VALIDATING` or `OOB_PENDING` => `OOB_PENDING`. */
172
236
  async processReceiveOobLink(messageContext) {
237
+ var _a;
173
238
  const { message, agentContext } = messageContext;
174
239
  messageContext.assertReadyConnection();
175
240
  const record = await this.getByThreadId(agentContext, message.threadId);
176
241
  record.assertRole(types_1.VtFlowRole.Applicant);
242
+ record.assertState([
243
+ types_1.VtFlowState.OrSent,
244
+ types_1.VtFlowState.IrSent,
245
+ types_1.VtFlowState.Validating,
246
+ types_1.VtFlowState.OobPending,
247
+ ]);
248
+ record.oobLink = {
249
+ url: message.url,
250
+ description: message.description,
251
+ expiresAt: (_a = message.expiresTime) === null || _a === void 0 ? void 0 : _a.toISOString(),
252
+ at: new Date().toISOString(),
253
+ };
254
+ this.appendMessage(record, {
255
+ type: types_1.VtFlowMessageType.OobLink,
256
+ text: message.description,
257
+ at: record.oobLink.at,
258
+ url: message.url,
259
+ });
177
260
  await this.updateState(agentContext, record, types_1.VtFlowState.OobPending);
178
261
  return record;
179
262
  }
180
- /** Applicant-side inbound `validating`; OnboardingProcess applicant transitions `OR_SENT => VALIDATING`. */
263
+ /** Applicant-side inbound `validating`; `OR_SENT`, `IR_SENT` or `OOB_PENDING` => `VALIDATING`. */
181
264
  async processReceiveValidating(messageContext) {
182
265
  var _a;
183
266
  const { message, agentContext } = messageContext;
184
267
  messageContext.assertReadyConnection();
185
268
  const record = await this.getByThreadId(agentContext, message.threadId);
186
269
  this.logger.debug(`[vt-flow] validating received for session ${record.threadId}: ${(_a = message.comment) !== null && _a !== void 0 ? _a : '(no comment)'}`);
270
+ if (record.role === types_1.VtFlowRole.Applicant && message.comment) {
271
+ this.appendMessage(record, {
272
+ type: types_1.VtFlowMessageType.Validating,
273
+ text: message.comment,
274
+ at: new Date().toISOString(),
275
+ });
276
+ await this.updateRecord(agentContext, record);
277
+ }
187
278
  if (record.role === types_1.VtFlowRole.Applicant &&
188
- record.variant === types_1.VtFlowVariant.OnboardingProcess &&
189
- record.state === types_1.VtFlowState.OrSent) {
279
+ [types_1.VtFlowState.OrSent, types_1.VtFlowState.IrSent, types_1.VtFlowState.OobPending].includes(record.state)) {
190
280
  await this.updateState(agentContext, record, types_1.VtFlowState.Validating);
191
281
  }
192
282
  return record;
@@ -215,7 +305,7 @@ let VtFlowService = class VtFlowService {
215
305
  record.assertState(types_1.VtFlowState.AwaitingOr);
216
306
  record.assertVariant(types_1.VtFlowVariant.OnboardingProcess);
217
307
  await this.updateState(agentContext, record, types_1.VtFlowState.Validating);
218
- return record;
308
+ return { record, message: new messages_1.ValidatingMessage({ threadId: record.threadId }) };
219
309
  }
220
310
  /** DirectIssuance: `AWAITING_IR => VALIDATING`; transient before `CRED_OFFERED`. */
221
311
  async acceptIssuanceRequest(agentContext, recordId) {
@@ -224,7 +314,7 @@ let VtFlowService = class VtFlowService {
224
314
  record.assertState(types_1.VtFlowState.AwaitingIr);
225
315
  record.assertVariant(types_1.VtFlowVariant.DirectIssuance);
226
316
  await this.updateState(agentContext, record, types_1.VtFlowState.Validating);
227
- return record;
317
+ return { record, message: new messages_1.ValidatingMessage({ threadId: record.threadId }) };
228
318
  }
229
319
  /** Reject with a problem-report; transitions to `TERMINATED_BY_{role}` and marks the connection as `TERMINATED`. */
230
320
  async rejectRequest(agentContext, recordId, params) {
@@ -264,6 +354,17 @@ let VtFlowService = class VtFlowService {
264
354
  var _a, _b;
265
355
  const record = await this.repository.getById(agentContext, recordId);
266
356
  record.assertRole(types_1.VtFlowRole.Validator);
357
+ record.assertState([
358
+ types_1.VtFlowState.AwaitingOr,
359
+ types_1.VtFlowState.AwaitingIr,
360
+ types_1.VtFlowState.Validating,
361
+ types_1.VtFlowState.OobPending,
362
+ types_1.VtFlowState.AwaitingValidationTx,
363
+ types_1.VtFlowState.ValidationTxFailed,
364
+ // VtFlowModule ends the flow here when auto-issue fails, until verana-labs/vs-agent#738
365
+ types_1.VtFlowState.CredOffered,
366
+ types_1.VtFlowState.Completed,
367
+ ]);
267
368
  const code = (_a = params.code) !== null && _a !== void 0 ? _a : errors_1.VtFlowErrorCode.SessionTerminated;
268
369
  const problemReport = (0, errors_1.buildVtFlowProblemReport)({
269
370
  code,
@@ -288,61 +389,104 @@ let VtFlowService = class VtFlowService {
288
389
  await this.updateState(agentContext, record, params.state);
289
390
  return { record, problemReport };
290
391
  }
291
- /** Build an `oob-link`; non-terminal records transition to `OOB_PENDING`. */
392
+ /** Build an `oob-link` from `VALIDATING` or `OOB_PENDING`; the flow moves to or stays in `OOB_PENDING`. */
292
393
  async sendOobLinkForSession(agentContext, recordId, params) {
394
+ var _a;
293
395
  const record = await this.repository.getById(agentContext, recordId);
294
396
  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
- ]);
397
+ record.assertState([types_1.VtFlowState.Validating, types_1.VtFlowState.OobPending]);
302
398
  const message = new messages_1.OobLinkMessage({
303
399
  threadId: record.threadId,
304
400
  url: params.url,
305
401
  description: params.description,
306
402
  expiresTime: params.expiresTime,
307
403
  });
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
- }
404
+ record.oobLink = {
405
+ url: params.url,
406
+ description: params.description,
407
+ expiresAt: (_a = params.expiresTime) === null || _a === void 0 ? void 0 : _a.toISOString(),
408
+ at: new Date().toISOString(),
409
+ };
410
+ this.appendMessage(record, {
411
+ type: types_1.VtFlowMessageType.OobLink,
412
+ text: params.description,
413
+ at: record.oobLink.at,
414
+ url: params.url,
415
+ });
416
+ await this.updateState(agentContext, record, types_1.VtFlowState.OobPending);
315
417
  return { record, message };
316
418
  }
317
- /** Build a `validating` informational message; no state change on the Validator side. */
419
+ /** `OOB_PENDING` => `VALIDATING`, with the `validating` that tells the applicant its out-of-band step is done. */
318
420
  async sendValidatingForSession(agentContext, recordId, params = {}) {
319
421
  const record = await this.repository.getById(agentContext, recordId);
320
422
  record.assertRole(types_1.VtFlowRole.Validator);
423
+ record.assertState(types_1.VtFlowState.OobPending);
321
424
  const message = new messages_1.ValidatingMessage({
322
425
  threadId: record.threadId,
323
426
  comment: params.comment,
324
427
  });
428
+ if (params.comment) {
429
+ this.appendMessage(record, {
430
+ type: types_1.VtFlowMessageType.Validating,
431
+ text: params.comment,
432
+ at: new Date().toISOString(),
433
+ });
434
+ }
435
+ await this.updateState(agentContext, record, types_1.VtFlowState.Validating);
325
436
  return { record, message };
326
437
  }
327
- /** `VALIDATING => VALIDATED`; call after `SetParticipantOPtoValidated` lands on-chain. */
438
+ /** Record the validation decision or its transaction outcome, moving the flow when `state` is given. */
439
+ async recordValidation(agentContext, recordId, validation, state) {
440
+ const record = await this.repository.getById(agentContext, recordId);
441
+ record.assertRole(types_1.VtFlowRole.Validator);
442
+ record.validation = validation;
443
+ if (state)
444
+ await this.updateState(agentContext, record, state);
445
+ else
446
+ await this.updateRecord(agentContext, record);
447
+ return record;
448
+ }
449
+ /** Validator-side states preceding `VALIDATED` => `VALIDATED`; call after `SetParticipantOPtoValidated` lands on-chain. */
328
450
  async markValidated(agentContext, recordId) {
329
451
  const record = await this.repository.getById(agentContext, recordId);
330
452
  record.assertRole(types_1.VtFlowRole.Validator);
331
- record.assertState([types_1.VtFlowState.Validating, types_1.VtFlowState.OobPending]);
453
+ record.assertState([...types_1.VtFlowValidatedFromStates]);
332
454
  record.assertVariant(types_1.VtFlowVariant.OnboardingProcess);
333
455
  await this.updateState(agentContext, record, types_1.VtFlowState.Validated);
334
456
  return record;
335
457
  }
336
- /** Link a Credo exchange record to the session and transition to `CRED_OFFERED`. */
337
- async attachCredentialExchangeRecord(agentContext, recordId, credentialExchangeRecord, credentialDigest, issuerParticipantId) {
458
+ /**
459
+ * Close an onboarding process that carries no credential exchange. Only a HOLDER receives a
460
+ * credential; for every other role the chain records the outcome with
461
+ * SetParticipantOPToValidated, and nothing else follows. Both sides use this, so it asserts no
462
+ * role: the validator calls it after it validates, and the applicant reaches it from the chain
463
+ * event.
464
+ */
465
+ async markCompleted(agentContext, recordId) {
338
466
  const record = await this.repository.getById(agentContext, recordId);
467
+ record.assertVariant(types_1.VtFlowVariant.OnboardingProcess);
468
+ if (record.state === types_1.VtFlowState.Completed)
469
+ return record;
470
+ if ((0, types_1.isVtFlowTerminalState)(record.state)) {
471
+ throw new core_1.CredoError(`VtFlow record is in terminal state '${record.state}'; cannot complete it.`);
472
+ }
473
+ await this.updateState(agentContext, record, types_1.VtFlowState.Completed);
474
+ return record;
475
+ }
476
+ /** Validator states that an `offer-credential` moves to `CRED_OFFERED`; from `COMPLETED` it starts a new subprotocol run. */
477
+ assertCanOfferCredential(record) {
339
478
  record.assertRole(types_1.VtFlowRole.Validator);
340
479
  record.assertState([
341
480
  types_1.VtFlowState.Validated,
342
- types_1.VtFlowState.Validating,
343
- types_1.VtFlowState.OobPending,
481
+ types_1.VtFlowState.ValidatedPendingClaims,
344
482
  types_1.VtFlowState.Completed,
483
+ ...(record.variant === types_1.VtFlowVariant.DirectIssuance ? [types_1.VtFlowState.Validating] : []),
345
484
  ]);
485
+ }
486
+ /** Link a Credo exchange record to the session and transition to `CRED_OFFERED`. */
487
+ async attachCredentialExchangeRecord(agentContext, recordId, credentialExchangeRecord, credentialDigest, issuerParticipantId) {
488
+ const record = await this.repository.getById(agentContext, recordId);
489
+ this.assertCanOfferCredential(record);
346
490
  record.credentialExchangeRecordId = credentialExchangeRecord.id;
347
491
  if (credentialDigest)
348
492
  record.credentialDigest = credentialDigest;
@@ -352,6 +496,13 @@ let VtFlowService = class VtFlowService {
352
496
  await this.updateState(agentContext, record, types_1.VtFlowState.CredOffered);
353
497
  return record;
354
498
  }
499
+ async setEcsSchemaKey(agentContext, recordId, ecsSchemaKey) {
500
+ const record = await this.repository.getById(agentContext, recordId);
501
+ record.assertRole(types_1.VtFlowRole.Applicant);
502
+ record.ecsSchemaKey = ecsSchemaKey;
503
+ await this.repository.update(agentContext, record);
504
+ return record;
505
+ }
355
506
  async setCredentialDigest(agentContext, recordId, credentialDigest) {
356
507
  const record = await this.repository.getById(agentContext, recordId);
357
508
  record.assertRole(types_1.VtFlowRole.Validator);
@@ -380,7 +531,7 @@ let VtFlowService = class VtFlowService {
380
531
  async updateClaims(agentContext, recordId, claims) {
381
532
  const record = await this.repository.getById(agentContext, recordId);
382
533
  record.assertRole(types_1.VtFlowRole.Validator);
383
- record.assertState([types_1.VtFlowState.Validating, types_1.VtFlowState.CredRevoked]);
534
+ record.assertState([types_1.VtFlowState.AwaitingOr, types_1.VtFlowState.Validating, types_1.VtFlowState.CredRevoked]);
384
535
  record.claims = claims;
385
536
  await this.updateRecord(agentContext, record);
386
537
  return record;
@@ -448,12 +599,69 @@ let VtFlowService = class VtFlowService {
448
599
  /** Transition the record, persist it, and emit `VtFlowStateChanged`. */
449
600
  async updateState(agentContext, record, newState) {
450
601
  const previousState = record.state;
451
- if (previousState === newState)
602
+ if (previousState === newState) {
603
+ await this.repository.update(agentContext, record);
452
604
  return;
605
+ }
606
+ if (previousState === types_1.VtFlowState.OobPending)
607
+ record.oobLink = undefined;
453
608
  record.state = newState;
454
609
  await this.repository.update(agentContext, record);
455
610
  this.emitStateChanged(agentContext, record, previousState);
456
611
  }
612
+ /**
613
+ * Inbound adopted `problem-report`. The Error Codes table gives the state the receiving party
614
+ * moves to, so both sides run this same mapping. A retryable code records the message and leaves
615
+ * the flow where it is.
616
+ */
617
+ async processReceiveProblemReport(messageContext) {
618
+ var _a, _b, _c, _d, _e, _f, _g;
619
+ const { message, agentContext } = messageContext;
620
+ const record = await this.repository.findByThreadId(agentContext, message.threadId);
621
+ if (!record)
622
+ return undefined;
623
+ if ((0, types_1.isVtFlowTerminalState)(record.state))
624
+ return record;
625
+ const code = (_a = message.description) === null || _a === void 0 ? void 0 : _a.code;
626
+ const info = code && (0, errors_1.isVtFlowErrorCode)(code) ? errors_1.VT_FLOW_ERROR_INFO[code] : undefined;
627
+ if (record.role === types_1.VtFlowRole.Applicant) {
628
+ this.appendMessage(record, {
629
+ type: types_1.VtFlowMessageType.ProblemReport,
630
+ 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',
631
+ at: new Date().toISOString(),
632
+ });
633
+ }
634
+ const target = info &&
635
+ this.resolveErrorFlowState(info.flowState, record.role, (_e = message.whoRetries) !== null && _e !== void 0 ? _e : errors_1.whoRetriesMap[info.whoRetries]);
636
+ if (!target) {
637
+ await this.updateRecord(agentContext, record);
638
+ return record;
639
+ }
640
+ record.errorMessage = (_g = (_f = message.description) === null || _f === void 0 ? void 0 : _f.en) !== null && _g !== void 0 ? _g : code;
641
+ await this.updateState(agentContext, record, target);
642
+ return record;
643
+ }
644
+ /** `terminated-by-sender` is the peer's termination, so the receiver lands in the state named after the sender. */
645
+ resolveErrorFlowState(flowState, receiverRole, whoRetries) {
646
+ if (flowState === 'unchanged')
647
+ return undefined;
648
+ if (flowState === 'unchanged-when-you') {
649
+ return whoRetries === didcomm_1.WhoRetriesStatus.You ? undefined : types_1.VtFlowState.Error;
650
+ }
651
+ if (flowState === 'error-when-fatal') {
652
+ return whoRetries === didcomm_1.WhoRetriesStatus.None ? types_1.VtFlowState.Error : undefined;
653
+ }
654
+ if (flowState !== 'terminated-by-sender')
655
+ return flowState;
656
+ return receiverRole === types_1.VtFlowRole.Applicant
657
+ ? types_1.VtFlowState.TerminatedByValidator
658
+ : types_1.VtFlowState.TerminatedByApplicant;
659
+ }
660
+ /** Append to `messages[]`, which a validator fills with what it sent and an applicant with what it received. */
661
+ appendMessage(record, message) {
662
+ var _a;
663
+ record.messages = [...((_a = record.messages) !== null && _a !== void 0 ? _a : []), message];
664
+ }
457
665
  /** Persist record changes without state-transition semantics (no event emitted). */
458
666
  async updateRecord(agentContext, record) {
459
667
  await this.repository.update(agentContext, record);
@@ -489,25 +697,47 @@ let VtFlowService = class VtFlowService {
489
697
  buildProblemReport(options) {
490
698
  return (0, errors_1.buildVtFlowProblemReport)(options);
491
699
  }
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)
700
+ /** 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. */
701
+ async checkIsVerifiableService(agentContext, connection, purpose) {
702
+ const peerDid = (0, utils_1.peerAnchorDid)(connection);
703
+ if (!connection.theirDid || !peerDid) {
496
704
  throw new core_1.CredoError(`vt-flow: ready connection '${connection.id}' has no theirDid`);
705
+ }
497
706
  const hook = this.config.assertVerifiableService;
498
707
  if (!hook) {
499
708
  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
709
  return;
501
710
  }
502
711
  let permitted = false;
712
+ let failure;
503
713
  try {
504
- permitted = await hook({ agentContext, peerDid, connectionId: connection.id });
714
+ permitted = await hook({
715
+ agentContext,
716
+ peerDid,
717
+ connectionId: connection.id,
718
+ });
505
719
  }
506
720
  catch (error) {
507
- throw new core_1.CredoError(`vt-flow.not-a-verifiable-service: peer '${peerDid}' failed VS-CONN-VS check (${error.message})`);
721
+ failure = error.message;
508
722
  }
509
- if (!permitted) {
510
- throw new core_1.CredoError(`vt-flow.not-a-verifiable-service: peer '${peerDid}' failed VS-CONN-VS check`);
723
+ if (permitted)
724
+ return;
725
+ if (purpose && (await this.isExemptFromVsConnVs(agentContext, connection, peerDid, purpose))) {
726
+ 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`);
727
+ return;
728
+ }
729
+ throw new core_1.CredoError(`vt-flow.not-a-verifiable-service: peer '${peerDid}' failed VS-CONN-VS check${failure ? ` (${failure})` : ''}`);
730
+ }
731
+ async isExemptFromVsConnVs(agentContext, connection, peerDid, purpose) {
732
+ const exemption = this.config.checkEcsIssuanceExemption;
733
+ if (!exemption)
734
+ return false;
735
+ try {
736
+ return await exemption({ agentContext, peerDid, connectionId: connection.id, purpose });
737
+ }
738
+ catch (error) {
739
+ this.logger.warn(`[vt-flow] VS-CONN-VS exemption check failed for peer '${peerDid}': ${error.message}`);
740
+ return false;
511
741
  }
512
742
  }
513
743
  };