@verana-labs/vs-agent-sdk 2.0.0-dev.7 → 2.0.0-dev.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/build/adminApi/AdminApiError.d.ts +28 -0
- package/build/adminApi/AdminApiError.js +60 -0
- package/build/adminApi/AdminApiError.js.map +1 -0
- package/build/adminApi/didcommModules.d.ts +2 -0
- package/build/adminApi/didcommModules.js +8 -0
- package/build/adminApi/didcommModules.js.map +1 -0
- package/build/adminApi/index.d.ts +4 -0
- package/build/adminApi/index.js +21 -0
- package/build/adminApi/index.js.map +1 -0
- package/build/adminApi/moduleEndpoint.d.ts +8 -0
- package/build/adminApi/moduleEndpoint.js +28 -0
- package/build/adminApi/moduleEndpoint.js.map +1 -0
- package/build/adminApi/pagination/PageDto.d.ts +7 -0
- package/build/adminApi/pagination/PageDto.js +50 -0
- package/build/adminApi/pagination/PageDto.js.map +1 -0
- package/build/adminApi/pagination/PaginationQueryDto.d.ts +11 -0
- package/build/adminApi/pagination/PaginationQueryDto.js +46 -0
- package/build/adminApi/pagination/PaginationQueryDto.js.map +1 -0
- package/build/adminApi/pagination/cursor.d.ts +28 -0
- package/build/adminApi/pagination/cursor.js +64 -0
- package/build/adminApi/pagination/cursor.js.map +1 -0
- package/build/adminApi/pagination/index.d.ts +4 -0
- package/build/adminApi/pagination/index.js +21 -0
- package/build/adminApi/pagination/index.js.map +1 -0
- package/build/adminApi/pagination/paginate.d.ts +27 -0
- package/build/adminApi/pagination/paginate.js +77 -0
- package/build/adminApi/pagination/paginate.js.map +1 -0
- package/build/agent/VsAgent.d.ts +17 -34
- package/build/agent/VsAgent.js +75 -205
- package/build/agent/VsAgent.js.map +1 -1
- package/build/agent/createVsAgent.d.ts +5 -6
- package/build/agent/createVsAgent.js +3 -5
- package/build/agent/createVsAgent.js.map +1 -1
- package/build/blockchain/AnonCredsTrustService.d.ts +62 -0
- package/build/blockchain/AnonCredsTrustService.js +256 -0
- package/build/blockchain/AnonCredsTrustService.js.map +1 -0
- package/build/blockchain/AuthorizationService.d.ts +5 -3
- package/build/blockchain/AuthorizationService.js +17 -29
- package/build/blockchain/AuthorizationService.js.map +1 -1
- package/build/blockchain/IndexerWebSocketService.d.ts +7 -1
- package/build/blockchain/IndexerWebSocketService.js +68 -36
- package/build/blockchain/IndexerWebSocketService.js.map +1 -1
- package/build/blockchain/VeranaChainService.d.ts +26 -14
- package/build/blockchain/VeranaChainService.js +139 -127
- package/build/blockchain/VeranaChainService.js.map +1 -1
- package/build/blockchain/VeranaIndexerService.d.ts +14 -1
- package/build/blockchain/VeranaIndexerService.js +122 -8
- package/build/blockchain/VeranaIndexerService.js.map +1 -1
- package/build/blockchain/handlers/authorizationHandlers.js +2 -2
- package/build/blockchain/handlers/authorizationHandlers.js.map +1 -1
- package/build/blockchain/handlers/defaultHandlers.js +6 -1
- package/build/blockchain/handlers/defaultHandlers.js.map +1 -1
- package/build/blockchain/handlers/index.d.ts +1 -0
- package/build/blockchain/handlers/index.js +1 -0
- package/build/blockchain/handlers/index.js.map +1 -1
- package/build/blockchain/handlers/selfIssuanceHandlers.d.ts +13 -0
- package/build/blockchain/handlers/selfIssuanceHandlers.js +76 -0
- package/build/blockchain/handlers/selfIssuanceHandlers.js.map +1 -0
- package/build/blockchain/handlers/stateMutations.d.ts +20 -3
- package/build/blockchain/handlers/stateMutations.js +286 -41
- package/build/blockchain/handlers/stateMutations.js.map +1 -1
- package/build/blockchain/index.d.ts +2 -0
- package/build/blockchain/index.js +2 -0
- package/build/blockchain/index.js.map +1 -1
- package/build/blockchain/indexerMappers.d.ts +3 -0
- package/build/blockchain/indexerMappers.js +53 -0
- package/build/blockchain/indexerMappers.js.map +1 -0
- package/build/blockchain/types.d.ts +13 -58
- package/build/blockchain/types.js.map +1 -1
- package/build/bootstrap/EcsBootstrapService.d.ts +14 -3
- package/build/bootstrap/EcsBootstrapService.js +178 -79
- package/build/bootstrap/EcsBootstrapService.js.map +1 -1
- package/build/connections/ParentConnectionModule.d.ts +20 -0
- package/build/connections/ParentConnectionModule.js +56 -0
- package/build/connections/ParentConnectionModule.js.map +1 -0
- package/build/credentials/FullTailsFileService.d.ts +0 -1
- package/build/credentials/FullTailsFileService.js +0 -49
- package/build/credentials/FullTailsFileService.js.map +1 -1
- package/build/did/CachedDocumentLoader.d.ts +3 -0
- package/build/did/CachedDocumentLoader.js +7 -1
- package/build/did/CachedDocumentLoader.js.map +1 -1
- package/build/did/CachedWebDidResolver.d.ts +0 -4
- package/build/did/CachedWebDidResolver.js +1 -5
- package/build/did/CachedWebDidResolver.js.map +1 -1
- package/build/did/artifactServices.d.ts +12 -0
- package/build/did/artifactServices.js +48 -0
- package/build/did/artifactServices.js.map +1 -0
- package/build/did/legacyDidWeb.d.ts +6 -4
- package/build/did/legacyDidWeb.js +17 -14
- package/build/did/legacyDidWeb.js.map +1 -1
- package/build/did/publicDid.d.ts +4 -0
- package/build/did/publicDid.js +19 -0
- package/build/did/publicDid.js.map +1 -0
- package/build/events/BaseMessageEvents.js +139 -5
- package/build/events/BaseMessageEvents.js.map +1 -1
- package/build/events/ConnectionEvents.js +129 -82
- package/build/events/ConnectionEvents.js.map +1 -1
- package/build/events/VsAgentEvents.d.ts +11 -9
- package/build/events/VsAgentEvents.js +8 -1
- package/build/events/VsAgentEvents.js.map +1 -1
- package/build/events/VtFlowEvents.d.ts +1 -1
- package/build/events/VtFlowEvents.js +29 -17
- package/build/events/VtFlowEvents.js.map +1 -1
- package/build/index.d.ts +7 -1
- package/build/index.js +15 -2
- package/build/index.js.map +1 -1
- package/build/plugins/setupBaseDidComm.js +9 -12
- package/build/plugins/setupBaseDidComm.js.map +1 -1
- package/build/transports/VsAgentWsInboundTransport.d.ts +1 -1
- package/build/transports/VsAgentWsInboundTransport.js +20 -14
- package/build/transports/VsAgentWsInboundTransport.js.map +1 -1
- package/build/types.d.ts +7 -13
- package/build/types.js +1 -2
- package/build/types.js.map +1 -1
- package/build/utils/agent.d.ts +29 -4
- package/build/utils/agent.js +91 -22
- package/build/utils/agent.js.map +1 -1
- package/build/utils/didReadiness.d.ts +9 -0
- package/build/utils/didReadiness.js +42 -0
- package/build/utils/didReadiness.js.map +1 -0
- package/build/utils/ecsClaims.d.ts +47 -0
- package/build/utils/ecsClaims.js +120 -0
- package/build/utils/ecsClaims.js.map +1 -0
- package/build/utils/ecsService.d.ts +8 -0
- package/build/utils/ecsService.js +23 -0
- package/build/utils/ecsService.js.map +1 -0
- package/build/utils/index.d.ts +4 -0
- package/build/utils/index.js +4 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/selfIssuedEcsCredential.d.ts +8 -0
- package/build/utils/selfIssuedEcsCredential.js +158 -0
- package/build/utils/selfIssuedEcsCredential.js.map +1 -0
- package/build/utils/setupSelfTr.d.ts +34 -66
- package/build/utils/setupSelfTr.js +90 -294
- package/build/utils/setupSelfTr.js.map +1 -1
- package/build/utils/trustCredentialStore.d.ts +67 -9
- package/build/utils/trustCredentialStore.js +296 -111
- package/build/utils/trustCredentialStore.js.map +1 -1
- package/build/utils/util.d.ts +10 -0
- package/build/utils/util.js +16 -0
- package/build/utils/util.js.map +1 -1
- package/build/utils/vcdm2.d.ts +34 -0
- package/build/utils/vcdm2.js +60 -0
- package/build/utils/vcdm2.js.map +1 -0
- package/build/utils/vtjscResolver.d.ts +18 -0
- package/build/utils/vtjscResolver.js +54 -0
- package/build/utils/vtjscResolver.js.map +1 -0
- package/build/vtFlow/VtFlowOrchestrator.d.ts +107 -14
- package/build/vtFlow/VtFlowOrchestrator.js +734 -109
- package/build/vtFlow/VtFlowOrchestrator.js.map +1 -1
- package/build/vtFlow/assertVerifiableService.d.ts +1 -0
- package/build/vtFlow/assertVerifiableService.js +45 -5
- package/build/vtFlow/assertVerifiableService.js.map +1 -1
- package/build/vtFlow/ecsIssuanceExemption.d.ts +12 -0
- package/build/vtFlow/ecsIssuanceExemption.js +84 -0
- package/build/vtFlow/ecsIssuanceExemption.js.map +1 -0
- package/build/vtFlow/index.d.ts +1 -0
- package/build/vtFlow/index.js +1 -0
- package/build/vtFlow/index.js.map +1 -1
- package/package.json +17 -12
- package/build/did/migrateWebVhLog.d.ts +0 -34
- package/build/did/migrateWebVhLog.js +0 -230
- package/build/did/migrateWebVhLog.js.map +0 -1
- package/build/did/migrateWebVhVersionTime.d.ts +0 -9
- package/build/did/migrateWebVhVersionTime.js +0 -191
- package/build/did/migrateWebVhVersionTime.js.map +0 -1
|
@@ -3,10 +3,46 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.VtFlowOrchestrator = void 0;
|
|
4
4
|
const core_1 = require("@credo-ts/core");
|
|
5
5
|
const credo_ts_didcomm_vt_flow_1 = require("@verana-labs/credo-ts-didcomm-vt-flow");
|
|
6
|
+
const verana_types_1 = require("@verana-labs/verana-types");
|
|
6
7
|
const verre_1 = require("@verana-labs/verre");
|
|
8
|
+
const vs_agent_model_1 = require("@verana-labs/vs-agent-model");
|
|
9
|
+
const adminApi_1 = require("../adminApi");
|
|
10
|
+
const ecsIssuanceExemption_1 = require("./ecsIssuanceExemption");
|
|
7
11
|
const types_1 = require("../blockchain/types");
|
|
8
12
|
const types_2 = require("../types");
|
|
9
13
|
const utils_1 = require("../utils");
|
|
14
|
+
// The chain carries a fee discount as an integer from 0 to 10000. The API and the indexer carry it
|
|
15
|
+
// as a decimal from 0 to 1, so only the message is scaled.
|
|
16
|
+
const DISCOUNT_SCALE = 10000;
|
|
17
|
+
const TX_LOOKUP_TIMEOUT_MS = 60000;
|
|
18
|
+
const TX_LOOKUP_INTERVAL_MS = 3000;
|
|
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();
|
|
22
|
+
const FEE_KEYS = ['validationFees', 'issuanceFees', 'verificationFees'];
|
|
23
|
+
const DISCOUNT_KEYS = ['issuanceFeeDiscount', 'verificationFeeDiscount'];
|
|
24
|
+
function invalidInput(message) {
|
|
25
|
+
return new adminApi_1.AdminApiError(adminApi_1.AdminApiErrorCode.InvalidInput, 400, message);
|
|
26
|
+
}
|
|
27
|
+
function invalidState(message) {
|
|
28
|
+
return new adminApi_1.AdminApiError(adminApi_1.AdminApiErrorCode.InvalidState, 409, message);
|
|
29
|
+
}
|
|
30
|
+
// AUTHZ-CHECK-3 step 1. A record's expiration is only its budget clock, not a validity window.
|
|
31
|
+
function isActiveParticipant(participant) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (!participant.effective_from || Date.parse(participant.effective_from) > now)
|
|
34
|
+
return false;
|
|
35
|
+
if (participant.effective_until && Date.parse(participant.effective_until) <= now)
|
|
36
|
+
return false;
|
|
37
|
+
return !participant.revoked && !participant.slashed;
|
|
38
|
+
}
|
|
39
|
+
function sameTerm(key, given, onEntry) {
|
|
40
|
+
const scale = DISCOUNT_KEYS.includes(key) ? DISCOUNT_SCALE : 1;
|
|
41
|
+
return Math.round(given * scale) === Math.round(onEntry * scale);
|
|
42
|
+
}
|
|
43
|
+
function errorMessage(error) {
|
|
44
|
+
return error instanceof Error ? error.message : String(error);
|
|
45
|
+
}
|
|
10
46
|
class VtFlowOrchestrator {
|
|
11
47
|
constructor(agent, options = {}) {
|
|
12
48
|
this.agent = agent;
|
|
@@ -14,10 +50,10 @@ class VtFlowOrchestrator {
|
|
|
14
50
|
}
|
|
15
51
|
async startOnboardingProcess(input) {
|
|
16
52
|
var _a, _b, _c, _d;
|
|
17
|
-
|
|
53
|
+
this.requireChain();
|
|
18
54
|
if (!this.agent.did)
|
|
19
55
|
throw new Error('Agent has no public DID');
|
|
20
|
-
const holderParticipant = await
|
|
56
|
+
const holderParticipant = await this.agent.indexer.findParticipant(input.applicantParticipantId);
|
|
21
57
|
if (!holderParticipant) {
|
|
22
58
|
throw new Error(`Applicant participant ${input.applicantParticipantId} not found on chain`);
|
|
23
59
|
}
|
|
@@ -37,7 +73,7 @@ class VtFlowOrchestrator {
|
|
|
37
73
|
if (!holderParticipant.validatorParticipantId) {
|
|
38
74
|
throw new Error(`Applicant participant ${input.applicantParticipantId} has no validator_participant_id`);
|
|
39
75
|
}
|
|
40
|
-
const validatorParticipant = await
|
|
76
|
+
const validatorParticipant = await this.agent.indexer.findParticipant(Number(holderParticipant.validatorParticipantId));
|
|
41
77
|
if (!(validatorParticipant === null || validatorParticipant === void 0 ? void 0 : validatorParticipant.did)) {
|
|
42
78
|
throw new Error(`Validator participant ${holderParticipant.validatorParticipantId} not resolvable`);
|
|
43
79
|
}
|
|
@@ -47,46 +83,58 @@ class VtFlowOrchestrator {
|
|
|
47
83
|
// Renewals resend the OR with the same session id so the validator re-attaches the finished flow.
|
|
48
84
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
49
85
|
const [latest] = (await vtFlowApi.findAllByQuery({
|
|
50
|
-
|
|
86
|
+
applicantParticipantId: String(holderParticipant.id),
|
|
51
87
|
role: credo_ts_didcomm_vt_flow_1.VtFlowRole.Applicant,
|
|
52
88
|
flowVariant: credo_ts_didcomm_vt_flow_1.VtFlowVariant.OnboardingProcess,
|
|
53
89
|
}))
|
|
54
90
|
.filter(record => !(0, credo_ts_didcomm_vt_flow_1.isVtFlowTerminalState)(record.state))
|
|
55
91
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
56
|
-
|
|
57
|
-
this.agent.config.logger.info(`[vt-flow] onboarding flow ${latest.id} for participant ${holderParticipant.id} is already in progress (state ${latest.state}); not resending`);
|
|
58
|
-
return latest;
|
|
59
|
-
}
|
|
60
|
-
const existing = latest;
|
|
92
|
+
const running = latest !== undefined && !(0, credo_ts_didcomm_vt_flow_1.isVtFlowRenewable)(latest);
|
|
61
93
|
let connectionId;
|
|
62
|
-
if (
|
|
63
|
-
const connection = await this.agent.didcomm.connections.findById(
|
|
64
|
-
if (connection
|
|
94
|
+
if (latest) {
|
|
95
|
+
const connection = await this.agent.didcomm.connections.findById(latest.connectionId);
|
|
96
|
+
if (connection && (0, utils_1.isUsableConnectionTo)(connection, validatorParticipant.did))
|
|
65
97
|
connectionId = connection.id;
|
|
66
98
|
}
|
|
99
|
+
const undelivered = running && latest.state === credo_ts_didcomm_vt_flow_1.VtFlowState.OrSent && holderParticipant.opState === types_1.ValidationState.PENDING;
|
|
100
|
+
if (running && connectionId && !undelivered) {
|
|
101
|
+
this.agent.config.logger.info(`[vt-flow] onboarding flow ${latest.id} for participant ${holderParticipant.id} is in progress (state ${latest.state}) on an open connection, not resending`);
|
|
102
|
+
return latest;
|
|
103
|
+
}
|
|
67
104
|
if (!connectionId) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
74
|
-
if (!connectionRecord)
|
|
75
|
-
throw new Error('Failed to establish DIDComm connection to validator');
|
|
76
|
-
const ready = await this.agent.didcomm.connections.returnWhenIsConnected(connectionRecord.id);
|
|
77
|
-
connectionId = ready.id;
|
|
105
|
+
connectionId = await (0, utils_1.connectToPublicDid)(this.agent, validatorParticipant.did);
|
|
106
|
+
}
|
|
107
|
+
if (running) {
|
|
108
|
+
this.agent.config.logger.info(`[vt-flow] resending the onboarding request of flow ${latest.id} (state ${latest.state}) on ${connectionId === latest.connectionId ? 'its' : 'a new'} connection`);
|
|
109
|
+
return vtFlowApi.resendOnboardingRequest({ vtFlowRecordId: latest.id, connectionId });
|
|
78
110
|
}
|
|
79
111
|
return vtFlowApi.sendOnboardingRequest({
|
|
80
112
|
connectionId,
|
|
81
|
-
participantSessionId: (_b = (_a = input.participantSessionId) !== null && _a !== void 0 ? _a :
|
|
82
|
-
|
|
113
|
+
participantSessionId: (_b = (_a = input.participantSessionId) !== null && _a !== void 0 ? _a : latest === null || latest === void 0 ? void 0 : latest.participantSessionId) !== null && _b !== void 0 ? _b : core_1.utils.uuid(),
|
|
114
|
+
applicantParticipantId: String(holderParticipant.id),
|
|
115
|
+
applicantParticipantRole: Number(holderParticipant.role),
|
|
116
|
+
validatorParticipantId: String(holderParticipant.validatorParticipantId),
|
|
117
|
+
schemaId: String(holderParticipant.schemaId),
|
|
83
118
|
agentParticipantId: String((_c = this.options.agentParticipantId) !== null && _c !== void 0 ? _c : 0),
|
|
84
119
|
walletAgentParticipantId: String((_d = this.options.walletAgentParticipantId) !== null && _d !== void 0 ? _d : 0),
|
|
85
120
|
claims: input.claims,
|
|
86
121
|
});
|
|
87
122
|
}
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Validate an onboarding process: record the outcome on-chain, accept the onboarding request and
|
|
125
|
+
* move the record to VALIDATED. It sends no credential. Only a HOLDER takes part in a credential
|
|
126
|
+
* exchange; an ISSUER, a VERIFIER or a grantor joins the Ecosystem, and SetParticipantOPToValidated
|
|
127
|
+
* is the whole of it. The caller decides what follows, from the role of the returned participant.
|
|
128
|
+
*
|
|
129
|
+
* For a HOLDER it builds the credential before the chain write, and returns it for
|
|
130
|
+
* offerOnboardingCredential. Claims that do not satisfy the schema, or a schema fetch that fails,
|
|
131
|
+
* must stop the process while the chain holds no outcome yet and the caller can repeat the call.
|
|
132
|
+
*
|
|
133
|
+
* A record that is already VALIDATED and has no credential exchange keeps the chain outcome and
|
|
134
|
+
* only builds the credential again. A repeat call thus re-drives an offer that did not complete.
|
|
135
|
+
*/
|
|
136
|
+
async validateOnboardingProcess(input) {
|
|
137
|
+
var _a, _b;
|
|
90
138
|
const chain = this.requireChain();
|
|
91
139
|
if (!this.agent.did)
|
|
92
140
|
throw new Error('Agent has no public DID');
|
|
@@ -99,43 +147,550 @@ class VtFlowOrchestrator {
|
|
|
99
147
|
if (record.variant !== credo_ts_didcomm_vt_flow_1.VtFlowVariant.OnboardingProcess) {
|
|
100
148
|
throw new Error(`Record variant is '${record.variant}', expected OnboardingProcess`);
|
|
101
149
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!
|
|
112
|
-
throw new Error(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
150
|
+
const resumeOffer = record.state === credo_ts_didcomm_vt_flow_1.VtFlowState.Validated && !record.credentialExchangeRecordId;
|
|
151
|
+
if (record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.AwaitingOr && !resumeOffer) {
|
|
152
|
+
throw new Error(`Record state is '${record.state}', expected '${credo_ts_didcomm_vt_flow_1.VtFlowState.AwaitingOr}', or ` +
|
|
153
|
+
`'${credo_ts_didcomm_vt_flow_1.VtFlowState.Validated}' with no credential exchange`);
|
|
154
|
+
}
|
|
155
|
+
if (!record.applicantParticipantId)
|
|
156
|
+
throw new Error('Record has no applicantParticipantId');
|
|
157
|
+
const participantId = Number(record.applicantParticipantId);
|
|
158
|
+
const participant = await this.agent.indexer.findParticipant(participantId);
|
|
159
|
+
if (!participant)
|
|
160
|
+
throw new Error(`Applicant participant ${participantId} not found on chain`);
|
|
161
|
+
if (!participant.did)
|
|
162
|
+
throw new Error('Applicant participant has no DID');
|
|
163
|
+
const credential = Number(participant.role) === types_2.HOLDER_PARTICIPANT_TYPE
|
|
164
|
+
? await this.buildCredential({
|
|
165
|
+
credentialSchemaId: (_a = input.credentialSchemaId) !== null && _a !== void 0 ? _a : String(participant.schemaId),
|
|
166
|
+
subjectDid: participant.did,
|
|
167
|
+
claims: ((_b = record.claims) !== null && _b !== void 0 ? _b : {}),
|
|
168
|
+
credentialType: input.credentialType,
|
|
169
|
+
credentialContext: input.credentialContext,
|
|
170
|
+
})
|
|
171
|
+
: undefined;
|
|
172
|
+
if (resumeOffer)
|
|
173
|
+
return { record, participant, credential };
|
|
120
174
|
// op_summary_digest (MOD-PP-MSG-3) digests the applicant's submission, not the credential
|
|
121
|
-
await chain.setParticipantOPToValidated({
|
|
122
|
-
id: holderParticipantId,
|
|
123
|
-
corporation: holderParticipant.corporation,
|
|
124
|
-
});
|
|
175
|
+
await chain.setParticipantOPToValidated({ id: participantId, corporation: participant.corporation });
|
|
125
176
|
await vtFlowApi.acceptOnboardingRequest(record.id);
|
|
126
|
-
await vtFlowApi.markValidated(record.id);
|
|
177
|
+
const validated = await vtFlowApi.markValidated(record.id);
|
|
178
|
+
return { record: validated, participant, credential };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Offer the credential of an onboarding process to its applicant. Call it only for a HOLDER, and
|
|
182
|
+
* only after validateOnboardingProcess has moved the record to VALIDATED. Give it the credential
|
|
183
|
+
* that validateOnboardingProcess built, so that the process does not build the credential twice.
|
|
184
|
+
*/
|
|
185
|
+
async offerOnboardingCredential(input) {
|
|
186
|
+
var _a, _b, _c, _d, _e;
|
|
187
|
+
this.requireChain();
|
|
188
|
+
const vtFlowApi = this.resolveVtFlowApi();
|
|
189
|
+
const record = await vtFlowApi.findById(input.vtFlowRecordId);
|
|
190
|
+
if (!record)
|
|
191
|
+
throw new Error(`vt-flow record ${input.vtFlowRecordId} not found`);
|
|
192
|
+
if (!record.applicantParticipantId)
|
|
193
|
+
throw new Error('Record has no applicantParticipantId');
|
|
194
|
+
const participant = (_a = input.participant) !== null && _a !== void 0 ? _a : (await this.agent.indexer.findParticipant(Number(record.applicantParticipantId)));
|
|
195
|
+
if (!(participant === null || participant === void 0 ? void 0 : participant.did))
|
|
196
|
+
throw new Error('Applicant participant has no DID');
|
|
197
|
+
const unsignedCredentialJson = (_b = input.credential) !== null && _b !== void 0 ? _b : (await this.buildCredential({
|
|
198
|
+
credentialSchemaId: (_c = input.credentialSchemaId) !== null && _c !== void 0 ? _c : String(participant.schemaId),
|
|
199
|
+
subjectDid: participant.did,
|
|
200
|
+
claims: ((_d = record.claims) !== null && _d !== void 0 ? _d : {}),
|
|
201
|
+
credentialType: input.credentialType,
|
|
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,
|
|
205
|
+
}));
|
|
206
|
+
// W3C Data Integrity attachment format (RFC 0809). The applicant DID is the credential subject
|
|
207
|
+
// and the exchange runs over its authenticated connection, so no extra binding is required. The
|
|
208
|
+
// cryptosuite is the issuer's choice and is applied when the credential is issued.
|
|
127
209
|
const { record: offered } = await vtFlowApi.offerCredentialForSession({
|
|
128
210
|
vtFlowRecordId: record.id,
|
|
129
|
-
issuerParticipantId: Number(
|
|
211
|
+
issuerParticipantId: Number(participant.validatorParticipantId),
|
|
130
212
|
credentialFormats: {
|
|
131
|
-
|
|
132
|
-
credential: unsignedCredentialJson,
|
|
133
|
-
options: { proofType: 'Ed25519Signature2020', proofPurpose: 'assertionMethod' },
|
|
134
|
-
},
|
|
213
|
+
dataIntegrity: { credential: unsignedCredentialJson, bindingRequired: false },
|
|
135
214
|
},
|
|
136
215
|
});
|
|
137
216
|
return offered;
|
|
138
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Close an onboarding process that carries no credential exchange, on the validator side. The
|
|
220
|
+
* applicant reaches the same state from the SetParticipantOPToValidated chain event.
|
|
221
|
+
*/
|
|
222
|
+
async completeOnboardingProcess(vtFlowRecordId) {
|
|
223
|
+
return this.resolveVtFlowApi().markCompleted(vtFlowRecordId);
|
|
224
|
+
}
|
|
225
|
+
/** [VSA-ADM-VT-FL-VALIDATE] */
|
|
226
|
+
async validateFlow(input) {
|
|
227
|
+
var _a, _b;
|
|
228
|
+
this.requireChain();
|
|
229
|
+
const vtFlowApi = this.resolveVtFlowApi();
|
|
230
|
+
const record = await vtFlowApi.findById(input.vtFlowRecordId);
|
|
231
|
+
if (!record) {
|
|
232
|
+
throw new adminApi_1.AdminApiError(adminApi_1.AdminApiErrorCode.UnknownId, 404, `no flow with id "${input.vtFlowRecordId}"`);
|
|
233
|
+
}
|
|
234
|
+
if (record.role !== credo_ts_didcomm_vt_flow_1.VtFlowRole.Validator)
|
|
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
|
+
}
|
|
244
|
+
if (record.variant === credo_ts_didcomm_vt_flow_1.VtFlowVariant.DirectIssuance)
|
|
245
|
+
return this.validateDirectIssuance(record);
|
|
246
|
+
if (!record.applicantParticipantId)
|
|
247
|
+
throw invalidState('the flow names no applicant participant');
|
|
248
|
+
const applicant = await this.agent.indexer.getParticipant(record.applicantParticipantId);
|
|
249
|
+
const entryValidated = applicant.op_state === 'VALIDATED';
|
|
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
|
+
}
|
|
256
|
+
const terms = this.validationTerms(input, applicant, record);
|
|
257
|
+
await this.assertClaimsAndTerm(record, applicant, terms);
|
|
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);
|
|
261
|
+
if (record.state === credo_ts_didcomm_vt_flow_1.VtFlowState.OobPending)
|
|
262
|
+
await this.sendValidating(record);
|
|
263
|
+
if (record.state === credo_ts_didcomm_vt_flow_1.VtFlowState.ValidatedPendingClaims)
|
|
264
|
+
return this.continueAfterValidated(record.id);
|
|
265
|
+
if (entryValidated) {
|
|
266
|
+
if (record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.Validated)
|
|
267
|
+
await this.markValidated(record.id, applicant);
|
|
268
|
+
return this.continueAfterValidated(record.id);
|
|
269
|
+
}
|
|
270
|
+
const validation = Object.assign({ decidedAt: new Date().toISOString(), submission: await this.submissionPath(applicant) }, terms);
|
|
271
|
+
if (validation.submission === credo_ts_didcomm_vt_flow_1.VtFlowSubmission.Operator) {
|
|
272
|
+
return vtFlowApi.recordValidation(record.id, validation, credo_ts_didcomm_vt_flow_1.VtFlowState.AwaitingValidationTx);
|
|
273
|
+
}
|
|
274
|
+
return this.submitValidation(record.id, applicant, validation);
|
|
275
|
+
}
|
|
276
|
+
// A Direct Issuance flow has no entry to validate, so no transaction: check the claims, then offer.
|
|
277
|
+
async validateDirectIssuance(record) {
|
|
278
|
+
if (record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.Validating && record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.OobPending) {
|
|
279
|
+
throw invalidState(`the flow is '${record.state}', which validate does not accept`);
|
|
280
|
+
}
|
|
281
|
+
if (!record.schemaId)
|
|
282
|
+
throw invalidState('the flow names no credential schema');
|
|
283
|
+
const connection = await this.agent.didcomm.connections.findById(record.connectionId);
|
|
284
|
+
if (!(connection === null || connection === void 0 ? void 0 : connection.theirDid))
|
|
285
|
+
throw invalidState('the flow connection has no peer DID');
|
|
286
|
+
const schema = await this.agent.indexer.getCredentialSchema(record.schemaId);
|
|
287
|
+
this.assertClaims(schema.json_schema, connection.theirDid, record.claims);
|
|
288
|
+
const vtFlowApi = this.resolveVtFlowApi();
|
|
289
|
+
if (record.state === credo_ts_didcomm_vt_flow_1.VtFlowState.OobPending)
|
|
290
|
+
await this.sendValidating(record);
|
|
291
|
+
const offer = await this.buildDirectIssuanceOffer(record.id);
|
|
292
|
+
if (!offer)
|
|
293
|
+
throw invalidState('the agent holds no active ISSUER participant for the schema of the flow');
|
|
294
|
+
const { record: offered } = await vtFlowApi.offerCredentialForSession(Object.assign({ vtFlowRecordId: record.id }, offer));
|
|
295
|
+
return offered;
|
|
296
|
+
}
|
|
297
|
+
// [VSA-ADM-VT-FL-START]: checked before the flow moves, as a failed send would leave it in VALIDATING
|
|
298
|
+
async sendValidating(record) {
|
|
299
|
+
const connection = await this.agent.didcomm.connections.findById(record.connectionId);
|
|
300
|
+
if (!(connection === null || connection === void 0 ? void 0 : connection.isReady))
|
|
301
|
+
throw invalidState('the flow connection is not ESTABLISHED');
|
|
302
|
+
await this.resolveVtFlowApi().sendValidating(record.id);
|
|
303
|
+
}
|
|
304
|
+
assertClaims(jsonSchema, subjectDid, claims) {
|
|
305
|
+
const violations = (0, utils_1.schemaViolations)(JSON.parse(jsonSchema), Object.assign({ id: subjectDid }, (claims !== null && claims !== void 0 ? claims : {})));
|
|
306
|
+
if (violations.length > 0) {
|
|
307
|
+
throw new adminApi_1.AdminApiError(adminApi_1.AdminApiErrorCode.InvalidClaims, 422, 'the claim set does not satisfy the json_schema', { violations });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
assertValidateState(record, entryValidated) {
|
|
311
|
+
const accepted = [
|
|
312
|
+
credo_ts_didcomm_vt_flow_1.VtFlowState.Validating,
|
|
313
|
+
credo_ts_didcomm_vt_flow_1.VtFlowState.OobPending,
|
|
314
|
+
credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxFailed,
|
|
315
|
+
credo_ts_didcomm_vt_flow_1.VtFlowState.ValidatedPendingClaims,
|
|
316
|
+
];
|
|
317
|
+
// Once the entry is VALIDATED on chain any validator-side state resumes into issuance, which is
|
|
318
|
+
// how a backend that disabled the default notification handler drives the flow ([VSA-ADM-VT-FL-VALIDATE-11]).
|
|
319
|
+
const acceptedOnceValidated = [
|
|
320
|
+
credo_ts_didcomm_vt_flow_1.VtFlowState.AwaitingOr,
|
|
321
|
+
credo_ts_didcomm_vt_flow_1.VtFlowState.AwaitingValidationTx,
|
|
322
|
+
credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxSubmitted,
|
|
323
|
+
credo_ts_didcomm_vt_flow_1.VtFlowState.Validated,
|
|
324
|
+
];
|
|
325
|
+
if (accepted.includes(record.state))
|
|
326
|
+
return;
|
|
327
|
+
if (entryValidated && acceptedOnceValidated.includes(record.state))
|
|
328
|
+
return;
|
|
329
|
+
throw invalidState(`the flow is '${record.state}', which validate does not accept`);
|
|
330
|
+
}
|
|
331
|
+
validationTerms(input, applicant, record) {
|
|
332
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
333
|
+
for (const key of FEE_KEYS) {
|
|
334
|
+
const fee = input[key];
|
|
335
|
+
if (fee !== undefined && !(Number.isInteger(fee) && fee >= 0)) {
|
|
336
|
+
throw invalidInput(`${key} must be a non-negative integer`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const key of DISCOUNT_KEYS) {
|
|
340
|
+
const discount = input[key];
|
|
341
|
+
if (discount !== undefined && !(discount >= 0 && discount <= 1)) {
|
|
342
|
+
throw invalidInput(`${key} must be between 0 and 1`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (input.effectiveUntil !== undefined && Number.isNaN(Date.parse(input.effectiveUntil))) {
|
|
346
|
+
throw invalidInput('effectiveUntil must be an ISO 8601 datetime');
|
|
347
|
+
}
|
|
348
|
+
const onEntry = {
|
|
349
|
+
validationFees: (_a = applicant.validation_fees) !== null && _a !== void 0 ? _a : 0,
|
|
350
|
+
issuanceFees: (_b = applicant.issuance_fees) !== null && _b !== void 0 ? _b : 0,
|
|
351
|
+
verificationFees: (_c = applicant.verification_fees) !== null && _c !== void 0 ? _c : 0,
|
|
352
|
+
issuanceFeeDiscount: (_d = applicant.issuance_fee_discount) !== null && _d !== void 0 ? _d : 0,
|
|
353
|
+
verificationFeeDiscount: (_e = applicant.verification_fee_discount) !== null && _e !== void 0 ? _e : 0,
|
|
354
|
+
};
|
|
355
|
+
const renewal = !!applicant.effective_from;
|
|
356
|
+
const recorded = record.state === credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxFailed ? record.validation : undefined;
|
|
357
|
+
const terms = {
|
|
358
|
+
effectiveUntil: (_f = input.effectiveUntil) !== null && _f !== void 0 ? _f : recorded === null || recorded === void 0 ? void 0 : recorded.effectiveUntil,
|
|
359
|
+
opSummaryDigest: (_g = input.opSummaryDigest) !== null && _g !== void 0 ? _g : recorded === null || recorded === void 0 ? void 0 : recorded.opSummaryDigest,
|
|
360
|
+
};
|
|
361
|
+
for (const key of [...FEE_KEYS, ...DISCOUNT_KEYS]) {
|
|
362
|
+
const given = input[key];
|
|
363
|
+
if (renewal) {
|
|
364
|
+
if (given !== undefined && !sameTerm(key, given, onEntry[key])) {
|
|
365
|
+
throw invalidInput(`${key} differs from the entry, and a renewal keeps the agreed value`);
|
|
366
|
+
}
|
|
367
|
+
terms[key] = onEntry[key];
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
terms[key] = (_h = given !== null && given !== void 0 ? given : recorded === null || recorded === void 0 ? void 0 : recorded[key]) !== null && _h !== void 0 ? _h : 0;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return terms;
|
|
374
|
+
}
|
|
375
|
+
async assertClaimsAndTerm(record, applicant, terms) {
|
|
376
|
+
var _a;
|
|
377
|
+
if (applicant.role !== types_1.ParticipantRole.Holder)
|
|
378
|
+
return;
|
|
379
|
+
if (!applicant.did)
|
|
380
|
+
throw invalidState('the applicant entry has no DID to issue to');
|
|
381
|
+
const schema = await this.agent.indexer.getCredentialSchema(applicant.schema_id);
|
|
382
|
+
this.assertClaims(schema.json_schema, applicant.did, record.claims);
|
|
383
|
+
// An ECS Organization or Persona credential needs a validUntil. With a validity period of 0 the
|
|
384
|
+
// VPR sets no effective_until, so the call has to carry one.
|
|
385
|
+
const needsValidUntil = (0, vs_agent_model_1.ecsRequiresValidUntil)(await (0, vs_agent_model_1.classifyEcsSchema)(schema.json_schema));
|
|
386
|
+
if (needsValidUntil && ((_a = schema.holder_validation_validity_period) !== null && _a !== void 0 ? _a : 0) === 0 && !terms.effectiveUntil) {
|
|
387
|
+
throw invalidInput('this schema has no validity period, so effectiveUntil is required');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async submissionPath(applicant) {
|
|
391
|
+
const authorization = this.agent.authorizationService;
|
|
392
|
+
if (!authorization || applicant.validator_participant_id == null)
|
|
393
|
+
return credo_ts_didcomm_vt_flow_1.VtFlowSubmission.Operator;
|
|
394
|
+
const validator = await this.agent.indexer
|
|
395
|
+
.getParticipant(applicant.validator_participant_id)
|
|
396
|
+
.catch(() => undefined);
|
|
397
|
+
if (!validator || !isActiveParticipant(validator))
|
|
398
|
+
return credo_ts_didcomm_vt_flow_1.VtFlowSubmission.Operator;
|
|
399
|
+
await authorization.refreshForOperator().catch(() => undefined);
|
|
400
|
+
const grant = authorization.getVsOperatorAuthorizationRecord(validator.id);
|
|
401
|
+
return (grant === null || grant === void 0 ? void 0 : grant.msgTypes.includes(verana_types_1.veranaTypeUrls.MsgSetParticipantOPToValidated))
|
|
402
|
+
? credo_ts_didcomm_vt_flow_1.VtFlowSubmission.Agent
|
|
403
|
+
: credo_ts_didcomm_vt_flow_1.VtFlowSubmission.Operator;
|
|
404
|
+
}
|
|
405
|
+
async submitValidation(recordId, applicant, validation) {
|
|
406
|
+
var _a, _b;
|
|
407
|
+
const chain = this.requireChain();
|
|
408
|
+
const vtFlowApi = this.resolveVtFlowApi();
|
|
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);
|
|
410
|
+
const message = chain.setParticipantOPToValidatedMsg({
|
|
411
|
+
id: applicant.id,
|
|
412
|
+
effectiveUntil: validation.effectiveUntil ? new Date(validation.effectiveUntil) : undefined,
|
|
413
|
+
validationFees: validation.validationFees,
|
|
414
|
+
issuanceFees: validation.issuanceFees,
|
|
415
|
+
verificationFees: validation.verificationFees,
|
|
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),
|
|
418
|
+
opSummaryDigest: validation.opSummaryDigest,
|
|
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
|
+
});
|
|
448
|
+
let allowance;
|
|
449
|
+
try {
|
|
450
|
+
allowance = granter ? await chain.feeAllowance(granter, FEE_DENOM) : undefined;
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
|
|
454
|
+
}
|
|
455
|
+
if (granter && !allowance) {
|
|
456
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.FeegrantExpired, 'the Corporation grants the agent no active fee allowance');
|
|
457
|
+
}
|
|
458
|
+
let fee;
|
|
459
|
+
try {
|
|
460
|
+
fee = await chain.estimateFee([message], granter);
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
|
|
464
|
+
}
|
|
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');
|
|
466
|
+
if (granter && allowance) {
|
|
467
|
+
if (!allowance.unlimited && allowance.remaining < amount) {
|
|
468
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.FeegrantExhausted, `the fee allowance has ${allowance.remaining}${FEE_DENOM} left`);
|
|
469
|
+
}
|
|
470
|
+
let corporation;
|
|
471
|
+
try {
|
|
472
|
+
corporation = BigInt((await chain.getAccountBalance(granter, FEE_DENOM)).amount);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
|
|
476
|
+
}
|
|
477
|
+
if (corporation < amount) {
|
|
478
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.InsufficientFundsCorporation, `the Corporation holds ${corporation}${FEE_DENOM}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
let own;
|
|
483
|
+
try {
|
|
484
|
+
own = BigInt((await chain.getBalance(FEE_DENOM)).amount);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.PreflightError, errorMessage(error));
|
|
488
|
+
}
|
|
489
|
+
if (own < amount) {
|
|
490
|
+
return failed(credo_ts_didcomm_vt_flow_1.VtFlowTxReason.InsufficientFundsAgent, `the agent account holds ${own}${FEE_DENOM}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
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
|
+
});
|
|
521
|
+
}
|
|
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));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// [VSA-ADM-VT-FL-VALIDATE-8]. Leaving VALIDATION_TX_SUBMITTED ends the loop, so the notification
|
|
538
|
+
// handler moving the flow first wins the race.
|
|
539
|
+
async resolveValidationTx(recordId) {
|
|
540
|
+
var _a, _b, _c;
|
|
541
|
+
const chain = this.requireChain();
|
|
542
|
+
const vtFlowApi = this.resolveVtFlowApi();
|
|
543
|
+
let landed;
|
|
544
|
+
for (;;) {
|
|
545
|
+
const record = await vtFlowApi.findById(recordId);
|
|
546
|
+
const validation = record === null || record === void 0 ? void 0 : record.validation;
|
|
547
|
+
const hash = (_a = validation === null || validation === void 0 ? void 0 : validation.tx) === null || _a === void 0 ? void 0 : _a.hash;
|
|
548
|
+
if (!(record === null || record === void 0 ? void 0 : record.applicantParticipantId) || !validation || !hash)
|
|
549
|
+
return;
|
|
550
|
+
if (record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxSubmitted)
|
|
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
|
+
}
|
|
562
|
+
}
|
|
563
|
+
else if (tx) {
|
|
564
|
+
return this.failUnlessValidated(record, validation, credo_ts_didcomm_vt_flow_1.VtFlowTxReason.TxFailed, tx.rawLog, tx.height);
|
|
565
|
+
}
|
|
566
|
+
const submittedAt = (_c = (_b = validation.tx) === null || _b === void 0 ? void 0 : _b.submittedAt) !== null && _c !== void 0 ? _c : validation.decidedAt;
|
|
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
|
+
}
|
|
572
|
+
return this.failUnlessValidated(record, validation, credo_ts_didcomm_vt_flow_1.VtFlowTxReason.TxNotFound, 'not found 60 seconds after the broadcast');
|
|
573
|
+
}
|
|
574
|
+
await new Promise(resolve => setTimeout(resolve, TX_LOOKUP_INTERVAL_MS));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// The VPR has no distinct error for a second SetParticipantOPtoValidated, so the entry tells a
|
|
578
|
+
// rejected duplicate apart from a real failure.
|
|
579
|
+
async failUnlessValidated(record, validation, reason, error, height) {
|
|
580
|
+
var _a;
|
|
581
|
+
const vtFlowApi = this.resolveVtFlowApi();
|
|
582
|
+
const applicant = record.applicantParticipantId
|
|
583
|
+
? await this.agent.indexer.getParticipant(record.applicantParticipantId)
|
|
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 }) });
|
|
586
|
+
if ((applicant === null || applicant === void 0 ? void 0 : applicant.op_state) === 'VALIDATED') {
|
|
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);
|
|
593
|
+
await this.continueAfterValidated(record.id);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
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;
|
|
648
|
+
}
|
|
649
|
+
async continueAfterValidated(recordId) {
|
|
650
|
+
var _a;
|
|
651
|
+
const vtFlowApi = this.resolveVtFlowApi();
|
|
652
|
+
const record = await vtFlowApi.findById(recordId);
|
|
653
|
+
if (!record)
|
|
654
|
+
throw new Error(`vt-flow record ${recordId} not found`);
|
|
655
|
+
if (record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.Validated && record.state !== credo_ts_didcomm_vt_flow_1.VtFlowState.ValidatedPendingClaims) {
|
|
656
|
+
return record;
|
|
657
|
+
}
|
|
658
|
+
const participant = await this.agent.indexer.findParticipant(Number(record.applicantParticipantId));
|
|
659
|
+
if (!participant)
|
|
660
|
+
throw new Error(`Applicant participant ${record.applicantParticipantId} not found`);
|
|
661
|
+
if (Number(participant.role) !== types_2.HOLDER_PARTICIPANT_TYPE)
|
|
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
|
+
}
|
|
670
|
+
return this.offerOnboardingCredential({ vtFlowRecordId: recordId, participant });
|
|
671
|
+
}
|
|
672
|
+
/** [VSA-ADM-VT-FL-VALIDATE-8]: at startup, resume every flow left in VALIDATION_TX_SUBMITTED. */
|
|
673
|
+
async resumeValidationSubmissions() {
|
|
674
|
+
const pending = await this.resolveVtFlowApi().findAllByQuery({
|
|
675
|
+
role: credo_ts_didcomm_vt_flow_1.VtFlowRole.Validator,
|
|
676
|
+
flowState: credo_ts_didcomm_vt_flow_1.VtFlowState.ValidationTxSubmitted,
|
|
677
|
+
});
|
|
678
|
+
await Promise.all(pending.map(record => this.resolveValidationTx(record.id)));
|
|
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
|
+
}
|
|
139
694
|
async acceptCredential(input) {
|
|
140
695
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
141
696
|
const record = await vtFlowApi.findById(input.vtFlowRecordId);
|
|
@@ -154,7 +709,7 @@ class VtFlowOrchestrator {
|
|
|
154
709
|
const chain = this.requireChain();
|
|
155
710
|
if (!this.agent.did)
|
|
156
711
|
throw new Error('Agent has no public DID');
|
|
157
|
-
const indexer = this.
|
|
712
|
+
const indexer = this.agent.indexer;
|
|
158
713
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
159
714
|
const record = await vtFlowApi.findById(vtFlowRecordId);
|
|
160
715
|
if (!record)
|
|
@@ -185,49 +740,51 @@ class VtFlowOrchestrator {
|
|
|
185
740
|
});
|
|
186
741
|
return {
|
|
187
742
|
credentialFormats: {
|
|
188
|
-
|
|
189
|
-
credential: unsignedCredentialJson,
|
|
190
|
-
options: { proofType: 'Ed25519Signature2020', proofPurpose: 'assertionMethod' },
|
|
191
|
-
},
|
|
743
|
+
dataIntegrity: { credential: unsignedCredentialJson, bindingRequired: false },
|
|
192
744
|
},
|
|
193
745
|
issuerParticipantId: issuer.id,
|
|
194
746
|
};
|
|
195
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* Builds the unsigned VC Data Model 2.0 credential that goes into the offer. RFC 0809 derives the
|
|
750
|
+
* advertised data model version from its context, and the issuer secures it with a
|
|
751
|
+
* DataIntegrityProof once the applicant requests it.
|
|
752
|
+
*/
|
|
196
753
|
async buildCredential(input) {
|
|
197
|
-
var _a
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
throw new Error(`No stored VTJSC found for ${schemaRef}`);
|
|
207
|
-
const { data } = entry;
|
|
208
|
-
const unsignedCredential = (0, utils_1.createCredential)({
|
|
754
|
+
var _a;
|
|
755
|
+
const jsonSchemaCredentialId = await this.resolveJsonSchemaCredentialId(input.credentialSchemaId);
|
|
756
|
+
// A credential whose claims don't satisfy the schema's required fields is not a valid
|
|
757
|
+
// instance of that credential type — reject it here rather than issue an empty shell.
|
|
758
|
+
const schema = await this.agent.indexer.getCredentialSchema(input.credentialSchemaId);
|
|
759
|
+
(0, utils_1.validateSchema)(JSON.parse(schema.json_schema), Object.assign({ id: input.subjectDid }, input.claims));
|
|
760
|
+
// Data model 2.0 holds the claims on the credential subject itself; the applicant DID wins over
|
|
761
|
+
// any `id` claim so the credential can never name a different subject
|
|
762
|
+
const unsignedCredential = (0, utils_1.createW3cV2Credential)({
|
|
209
763
|
id: `${this.agent.did}#${core_1.utils.uuid()}`,
|
|
210
764
|
type: (_a = input.credentialType) !== null && _a !== void 0 ? _a : ['VerifiableCredential', 'VerifiableTrustCredential'],
|
|
211
765
|
issuer: this.agent.did,
|
|
212
|
-
|
|
766
|
+
context: input.credentialContext,
|
|
767
|
+
credentialSubject: Object.assign(Object.assign({}, input.claims), { id: input.subjectDid }),
|
|
768
|
+
credentialSchema: { id: jsonSchemaCredentialId, type: 'JsonSchemaCredential' },
|
|
769
|
+
validUntil: input.validUntil,
|
|
213
770
|
});
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
type: 'JsonSchemaCredential',
|
|
219
|
-
};
|
|
220
|
-
return core_1.JsonTransformer.toJSON(unsignedCredential);
|
|
771
|
+
return (0, utils_1.toOfferedCredentialJson)(unsignedCredential);
|
|
772
|
+
}
|
|
773
|
+
async resolveJsonSchemaCredentialId(credentialSchemaId) {
|
|
774
|
+
return (0, utils_1.resolveJsonSchemaCredentialId)(this.agent, this.agent.indexer, credentialSchemaId, this.requireChain().getChainId);
|
|
221
775
|
}
|
|
222
776
|
/** Per the spec the algorithm comes from the schema, never from the digest value. */
|
|
223
777
|
async digestAlgorithmForSchema(schemaId) {
|
|
224
|
-
|
|
778
|
+
return (await this.credentialSchema(schemaId)).digestAlgorithm;
|
|
779
|
+
}
|
|
780
|
+
async credentialSchema(schemaId) {
|
|
781
|
+
const schema = await this.agent.indexer.getCredentialSchema(schemaId);
|
|
225
782
|
if (!schema.digest_algorithm) {
|
|
226
783
|
throw new Error(`Credential schema ${schemaId} has no digest_algorithm`);
|
|
227
784
|
}
|
|
228
|
-
return schema.digest_algorithm;
|
|
785
|
+
return { digestAlgorithm: schema.digest_algorithm, ecsKey: await (0, vs_agent_model_1.classifyEcsSchema)(schema.json_schema) };
|
|
229
786
|
}
|
|
230
|
-
/** Fired after signing and before delivery
|
|
787
|
+
/** Fired after signing and before delivery. A failed anchoring comes back in `issuance`, and the credential stays undelivered. */
|
|
231
788
|
async onCredentialIssued(vtFlowRecordId, signedCredential) {
|
|
232
789
|
var _a, _b;
|
|
233
790
|
const chain = this.requireChain();
|
|
@@ -238,17 +795,20 @@ class VtFlowOrchestrator {
|
|
|
238
795
|
if (record.issuerParticipantId == null) {
|
|
239
796
|
throw new Error(`vt-flow record ${vtFlowRecordId} has no issuerParticipantId to anchor against`);
|
|
240
797
|
}
|
|
241
|
-
const issuer = await this.
|
|
798
|
+
const issuer = await this.agent.indexer.getParticipant(record.issuerParticipantId);
|
|
242
799
|
const algorithm = await this.digestAlgorithmForSchema(issuer.schema_id);
|
|
243
800
|
const digest = (0, verre_1.computeCredentialDigestJCS)(signedCredential, algorithm);
|
|
244
|
-
|
|
801
|
+
const message = chain.createOrUpdateParticipantSessionMsg({
|
|
245
802
|
id: record.participantSessionId,
|
|
246
803
|
issuerParticipantId: record.issuerParticipantId,
|
|
247
804
|
agentParticipantId: Number((_a = record.agentParticipantId) !== null && _a !== void 0 ? _a : 0) || 0,
|
|
248
805
|
walletAgentParticipantId: Number((_b = record.walletAgentParticipantId) !== null && _b !== void 0 ? _b : 0) || 0,
|
|
249
806
|
digest,
|
|
250
807
|
});
|
|
251
|
-
return
|
|
808
|
+
return {
|
|
809
|
+
credentialDigest: digest,
|
|
810
|
+
issuance: { tx: await this.anchor(record, message, record.issuerParticipantId) },
|
|
811
|
+
};
|
|
252
812
|
}
|
|
253
813
|
async verifyOfferedCredential(vtFlowRecordId) {
|
|
254
814
|
var _a, _b;
|
|
@@ -261,7 +821,7 @@ class VtFlowOrchestrator {
|
|
|
261
821
|
if (!record.credentialExchangeRecordId) {
|
|
262
822
|
throw new Error('Record has no credentialExchangeRecordId; nothing to verify');
|
|
263
823
|
}
|
|
264
|
-
const indexer = this.
|
|
824
|
+
const indexer = this.agent.indexer;
|
|
265
825
|
const session = await indexer.getParticipantSession(record.participantSessionId);
|
|
266
826
|
if (!session) {
|
|
267
827
|
throw new Error(`ParticipantSession ${record.participantSessionId} not found on indexer`);
|
|
@@ -282,13 +842,36 @@ class VtFlowOrchestrator {
|
|
|
282
842
|
if (record.schemaId != null && Number(issuer.schema_id) !== Number(record.schemaId)) {
|
|
283
843
|
throw new Error(`Issuer participant schema ${issuer.schema_id} does not match credential schema ${record.schemaId}`);
|
|
284
844
|
}
|
|
285
|
-
const credentialJson = await this.
|
|
286
|
-
|
|
287
|
-
const
|
|
845
|
+
const credentialJson = await this.getReceivedCredentialJson(record.credentialExchangeRecordId);
|
|
846
|
+
await this.assertFlowCredentialFormat(credentialJson, issuer);
|
|
847
|
+
const { digestAlgorithm, ecsKey } = await this.credentialSchema(issuer.schema_id);
|
|
848
|
+
const digest = (0, verre_1.computeCredentialDigestJCS)(credentialJson, digestAlgorithm);
|
|
288
849
|
const anchored = await indexer.getDigest(digest);
|
|
289
850
|
if (!anchored) {
|
|
290
851
|
throw new Error(`Credential digest ${digest} is not anchored on-chain`);
|
|
291
852
|
}
|
|
853
|
+
if (ecsKey)
|
|
854
|
+
await vtFlowApi.setEcsSchemaKey(record.id, ecsKey);
|
|
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
|
+
}
|
|
292
875
|
}
|
|
293
876
|
async onCredentialRevoked(vtFlowRecordId) {
|
|
294
877
|
const vtFlowApi = this.resolveVtFlowApi();
|
|
@@ -297,7 +880,7 @@ class VtFlowOrchestrator {
|
|
|
297
880
|
return;
|
|
298
881
|
if (!this.options.publicApiBaseUrl || !record.credentialExchangeRecordId)
|
|
299
882
|
return;
|
|
300
|
-
const credentialId = await (0, utils_1.removeStoredTrustCredential)(this.agent,
|
|
883
|
+
const credentialId = await (0, utils_1.removeStoredTrustCredential)(this.agent, record.credentialExchangeRecordId);
|
|
301
884
|
if (credentialId) {
|
|
302
885
|
this.agent.config.logger.info(`[vt-flow] removed revoked credential and its linked VP (${credentialId})`);
|
|
303
886
|
}
|
|
@@ -318,26 +901,68 @@ class VtFlowOrchestrator {
|
|
|
318
901
|
const chain = this.agent.veranaChain;
|
|
319
902
|
if (!chain || !chain.autoTriggerResolverEnabled)
|
|
320
903
|
return;
|
|
321
|
-
if (record.
|
|
904
|
+
if (record.applicantParticipantId == null)
|
|
322
905
|
return;
|
|
323
|
-
await chain.triggerResolver(Number(record.
|
|
906
|
+
await chain.triggerResolver(Number(record.applicantParticipantId));
|
|
324
907
|
}
|
|
325
|
-
|
|
326
|
-
|
|
908
|
+
/** The signed credential as the issuer attached it, which is the exact JSON its digest covers. */
|
|
909
|
+
async getReceivedCredentialJson(credentialExchangeRecordId) {
|
|
910
|
+
var _a, _b;
|
|
327
911
|
const formatData = await this.agent.didcomm.credentials.getFormatData(credentialExchangeRecordId);
|
|
328
|
-
const credentialJson = (_a = formatData.credential) === null || _a === void 0 ? void 0 : _a.
|
|
912
|
+
const credentialJson = (_b = (_a = formatData.credential) === null || _a === void 0 ? void 0 : _a.dataIntegrity) === null || _b === void 0 ? void 0 : _b.credential;
|
|
329
913
|
if (!credentialJson)
|
|
330
|
-
throw new Error('
|
|
914
|
+
throw new Error('Received credential has no data integrity credential body');
|
|
331
915
|
return credentialJson;
|
|
332
916
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
917
|
+
// VS-CONN-VS gate: consulted only after trust resolution rejected the peer.
|
|
918
|
+
async checkEcsIssuanceExemption(context) {
|
|
919
|
+
return (0, ecsIssuanceExemption_1.isEcsIssuanceExempt)({
|
|
920
|
+
indexer: this.agent.indexer,
|
|
921
|
+
agent: this.agent,
|
|
922
|
+
trustedEcosystemDids: this.agent.trustedEcosystemDids,
|
|
923
|
+
}, context);
|
|
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));
|
|
336
962
|
}
|
|
337
|
-
return this.options.indexer;
|
|
338
963
|
}
|
|
339
964
|
async publishCredentialAsLinkedVp(vtFlowRecordId) {
|
|
340
|
-
var _a
|
|
965
|
+
var _a;
|
|
341
966
|
if (!this.options.publicApiBaseUrl) {
|
|
342
967
|
throw new Error('publicApiBaseUrl is required to publish a LinkedVerifiablePresentation');
|
|
343
968
|
}
|
|
@@ -350,18 +975,18 @@ class VtFlowOrchestrator {
|
|
|
350
975
|
if (!record.credentialExchangeRecordId) {
|
|
351
976
|
throw new Error(`vt-flow record ${vtFlowRecordId} has no credentialExchangeRecordId`);
|
|
352
977
|
}
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
if (!jsonld) {
|
|
356
|
-
throw new Error(`No jsonld credential in exchange ${record.credentialExchangeRecordId}`);
|
|
357
|
-
}
|
|
358
|
-
const schemaRef = (_b = jsonld.credentialSchema) === null || _b === void 0 ? void 0 : _b.id;
|
|
978
|
+
const credentialJson = await this.getReceivedCredentialJson(record.credentialExchangeRecordId);
|
|
979
|
+
const schemaRef = (_a = credentialJson.credentialSchema) === null || _a === void 0 ? void 0 : _a.id;
|
|
359
980
|
const schemaBaseId = schemaRef ? this.extractSchemaBaseId(schemaRef) : undefined;
|
|
360
981
|
if (!schemaBaseId) {
|
|
361
982
|
throw new Error(`Cannot publish Linked VP: credential has no extractable schema base id (credentialSchema.id=${schemaRef !== null && schemaRef !== void 0 ? schemaRef : 'undefined'})`);
|
|
362
983
|
}
|
|
363
|
-
|
|
364
|
-
|
|
984
|
+
// A data model 2.0 credential arrives secured with a DataIntegrityProof; a validator still on
|
|
985
|
+
// data model 1.1 sends a linked data proof, and both are published the same way
|
|
986
|
+
const credential = (0, utils_1.isVcdm2Credential)(credentialJson)
|
|
987
|
+
? core_1.W3cV2DataIntegrityVerifiableCredential.fromObject(credentialJson)
|
|
988
|
+
: core_1.JsonTransformer.fromJSON(credentialJson, core_1.W3cJsonLdVerifiableCredential);
|
|
989
|
+
await (0, utils_1.createVtc)(this.agent, this.options.publicApiBaseUrl, record.ecsSchemaKey ? (0, utils_1.linkedVpSchemaId)(record.ecsSchemaKey) : schemaBaseId, credential);
|
|
365
990
|
}
|
|
366
991
|
extractSchemaBaseId(jscUrl) {
|
|
367
992
|
var _a;
|