@happyvertical/smrt-sales 0.40.0 → 0.40.2
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/AGENTS.md +14 -6
- package/dist/agreements/collections/AgreementExecutionCollection.d.ts +106 -0
- package/dist/agreements/collections/AgreementExecutionCollection.d.ts.map +1 -0
- package/dist/agreements/collections/AgreementExecutionEventCollection.d.ts +16 -0
- package/dist/agreements/collections/AgreementExecutionEventCollection.d.ts.map +1 -0
- package/dist/agreements/collections/ExecutedAgreementCollection.d.ts +17 -0
- package/dist/agreements/collections/ExecutedAgreementCollection.d.ts.map +1 -0
- package/dist/agreements/index.d.ts +18 -0
- package/dist/agreements/index.d.ts.map +1 -0
- package/dist/agreements/models/AgreementExecution.d.ts +66 -0
- package/dist/agreements/models/AgreementExecution.d.ts.map +1 -0
- package/dist/agreements/models/AgreementExecutionEvent.d.ts +25 -0
- package/dist/agreements/models/AgreementExecutionEvent.d.ts.map +1 -0
- package/dist/agreements/models/ExecutedAgreement.d.ts +35 -0
- package/dist/agreements/models/ExecutedAgreement.d.ts.map +1 -0
- package/dist/agreements/services/AgreementExecutionService.d.ts +85 -0
- package/dist/agreements/services/AgreementExecutionService.d.ts.map +1 -0
- package/dist/agreements/types.d.ts +140 -0
- package/dist/agreements/types.d.ts.map +1 -0
- package/dist/agreements.d.ts +2 -0
- package/dist/agreements.d.ts.map +1 -0
- package/dist/agreements.js +3 -0
- package/dist/chunks/__smrt-register__-Cwn1Fb4I.js +6 -0
- package/dist/chunks/{__smrt-register__-DGR1u9K2.js.map → __smrt-register__-Cwn1Fb4I.js.map} +1 -1
- package/dist/chunks/agreements-DbzW1Pcq.js +1640 -0
- package/dist/chunks/agreements-DbzW1Pcq.js.map +1 -0
- package/dist/chunks/{referrals-CzkrPeKv.js → referrals-cYYg4YZw.js} +163 -40
- package/dist/chunks/referrals-cYYg4YZw.js.map +1 -0
- package/dist/commissions.js +1 -1
- package/dist/crm.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/manifest.json +1853 -99
- package/dist/referrals/collections/ReferralAgreementCollection.d.ts +0 -4
- package/dist/referrals/collections/ReferralAgreementCollection.d.ts.map +1 -1
- package/dist/referrals/index.d.ts +1 -0
- package/dist/referrals/index.d.ts.map +1 -1
- package/dist/referrals/models/ReferralAgreement.d.ts +8 -19
- package/dist/referrals/models/ReferralAgreement.d.ts.map +1 -1
- package/dist/referrals/services/ReferralAgreementExecutionService.d.ts +55 -0
- package/dist/referrals/services/ReferralAgreementExecutionService.d.ts.map +1 -0
- package/dist/referrals/types.d.ts +2 -4
- package/dist/referrals/types.d.ts.map +1 -1
- package/dist/referrals.js +3 -3
- package/dist/smrt-knowledge.json +780 -33
- package/dist/svelte/components/ExecutedAgreementsList.svelte +36 -10
- package/dist/svelte/components/ExecutedAgreementsList.svelte.d.ts +2 -0
- package/dist/svelte/components/ExecutedAgreementsList.svelte.d.ts.map +1 -1
- package/dist/svelte/types.d.ts +7 -3
- package/dist/svelte/types.d.ts.map +1 -1
- package/package.json +13 -7
- package/dist/chunks/__smrt-register__-DGR1u9K2.js +0 -6
- package/dist/chunks/referrals-CzkrPeKv.js.map +0 -1
|
@@ -0,0 +1,1640 @@
|
|
|
1
|
+
import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
|
|
2
|
+
import { TenantScoped, requireTenantId, tenantId } from "@happyvertical/smrt-tenancy";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
//#region src/agreements/types.ts
|
|
5
|
+
var AGREEMENT_EXECUTION_STATUSES = [
|
|
6
|
+
"prepared",
|
|
7
|
+
"sent",
|
|
8
|
+
"delivered",
|
|
9
|
+
"viewed",
|
|
10
|
+
"partially_signed",
|
|
11
|
+
"completed",
|
|
12
|
+
"declined",
|
|
13
|
+
"cancelled",
|
|
14
|
+
"expired",
|
|
15
|
+
"failed"
|
|
16
|
+
];
|
|
17
|
+
function sanitizeSignerIntent(signers) {
|
|
18
|
+
return signers.map((signer) => ({
|
|
19
|
+
name: signer.name,
|
|
20
|
+
email: signer.email,
|
|
21
|
+
...signer.role ? { role: signer.role } : {},
|
|
22
|
+
...signer.order !== void 0 ? { order: signer.order } : {},
|
|
23
|
+
authenticationMethod: signer.authentication?.method ?? "none"
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
function sanitizeSignerEvidence(signers) {
|
|
27
|
+
return signers.map((signer) => ({
|
|
28
|
+
...signer.id ? { id: signer.id } : {},
|
|
29
|
+
name: signer.name,
|
|
30
|
+
email: signer.email,
|
|
31
|
+
...signer.role ? { role: signer.role } : {},
|
|
32
|
+
...signer.order !== void 0 ? { order: signer.order } : {},
|
|
33
|
+
status: signer.status,
|
|
34
|
+
...signer.authenticationMethod ? { authenticationMethod: signer.authenticationMethod } : {},
|
|
35
|
+
...signer.viewed !== void 0 ? { viewed: signer.viewed } : {},
|
|
36
|
+
...signer.deliveryFailed !== void 0 ? { deliveryFailed: signer.deliveryFailed } : {}
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function coerceAgreementDate(value) {
|
|
40
|
+
if (value == null) return null;
|
|
41
|
+
if (value instanceof Date) return value;
|
|
42
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
43
|
+
const date = new Date(value);
|
|
44
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/agreements/models/AgreementExecution.ts
|
|
50
|
+
var __defProp$2 = Object.defineProperty;
|
|
51
|
+
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
52
|
+
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
53
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
54
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
55
|
+
if (kind && result) __defProp$2(target, key, result);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
var persistedExecutionIdentity = /* @__PURE__ */ new WeakMap();
|
|
59
|
+
var AgreementExecution = class extends SmrtObject {
|
|
60
|
+
tenantId = "";
|
|
61
|
+
provider = "";
|
|
62
|
+
/** Non-secret provider account/region reference used for operations. */
|
|
63
|
+
providerAccountRef = "";
|
|
64
|
+
/** SDK/SMRT secret-store reference only; never a credential value. */
|
|
65
|
+
credentialRef = "";
|
|
66
|
+
idempotencyKey = "";
|
|
67
|
+
sourceKind = "";
|
|
68
|
+
sourceId = "";
|
|
69
|
+
sourceVersion = 1;
|
|
70
|
+
sourceAssetId = "";
|
|
71
|
+
sourceSha256 = "";
|
|
72
|
+
sourceSizeBytes = 0;
|
|
73
|
+
requestIntentSha256 = "";
|
|
74
|
+
title = "";
|
|
75
|
+
signerIntent = "[]";
|
|
76
|
+
providerRequestId = "";
|
|
77
|
+
providerRequestKey = null;
|
|
78
|
+
status = "prepared";
|
|
79
|
+
expiresAt = null;
|
|
80
|
+
cancellationReason = "";
|
|
81
|
+
lastProviderEventAt = null;
|
|
82
|
+
lastReconciledAt = null;
|
|
83
|
+
completedAt = null;
|
|
84
|
+
effectiveFrom = null;
|
|
85
|
+
effectiveTo = null;
|
|
86
|
+
/**
|
|
87
|
+
* Opaque prior-evidence provenance copied into the immutable result.
|
|
88
|
+
* The authoritative relationship is on ExecutedAgreement; making this
|
|
89
|
+
* mutable orchestration hint a foreign key would create a registry cycle.
|
|
90
|
+
*/
|
|
91
|
+
supersedesExecutedAgreementId = "";
|
|
92
|
+
signedDocumentAssetId = "";
|
|
93
|
+
signedDocumentSha256 = "";
|
|
94
|
+
signedDocumentSizeBytes = 0;
|
|
95
|
+
signedDocumentMediaType = "";
|
|
96
|
+
signedDocumentFilename = "";
|
|
97
|
+
auditTrailAssetId = "";
|
|
98
|
+
auditTrailSha256 = "";
|
|
99
|
+
auditTrailSizeBytes = 0;
|
|
100
|
+
auditTrailMediaType = "";
|
|
101
|
+
auditTrailFilename = "";
|
|
102
|
+
attemptCount = 0;
|
|
103
|
+
/** Operation id holding the current provider-create lease. */
|
|
104
|
+
createLeaseId = "";
|
|
105
|
+
/** A crashed create attempt stops fencing recovery after this instant. */
|
|
106
|
+
createLeaseExpiresAt = null;
|
|
107
|
+
lastError = "";
|
|
108
|
+
metadata = "{}";
|
|
109
|
+
constructor(options = {}) {
|
|
110
|
+
super(options);
|
|
111
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
112
|
+
if (options.provider !== void 0) this.provider = options.provider;
|
|
113
|
+
if (options.providerAccountRef !== void 0) this.providerAccountRef = options.providerAccountRef;
|
|
114
|
+
if (options.credentialRef !== void 0) this.credentialRef = options.credentialRef;
|
|
115
|
+
if (options.idempotencyKey !== void 0) this.idempotencyKey = options.idempotencyKey;
|
|
116
|
+
if (options.sourceKind !== void 0) this.sourceKind = options.sourceKind;
|
|
117
|
+
if (options.sourceId !== void 0) this.sourceId = options.sourceId;
|
|
118
|
+
if (options.sourceVersion !== void 0) this.sourceVersion = options.sourceVersion;
|
|
119
|
+
if (options.sourceAssetId !== void 0) this.sourceAssetId = options.sourceAssetId;
|
|
120
|
+
if (options.sourceSha256 !== void 0) this.sourceSha256 = options.sourceSha256;
|
|
121
|
+
if (options.sourceSizeBytes !== void 0) this.sourceSizeBytes = options.sourceSizeBytes;
|
|
122
|
+
if (options.requestIntentSha256 !== void 0) this.requestIntentSha256 = options.requestIntentSha256;
|
|
123
|
+
if (options.title !== void 0) this.title = options.title;
|
|
124
|
+
if (options.signerIntent !== void 0) this.signerIntent = options.signerIntent;
|
|
125
|
+
if (options.providerRequestId !== void 0) this.providerRequestId = options.providerRequestId;
|
|
126
|
+
if (options.providerRequestKey !== void 0) this.providerRequestKey = options.providerRequestKey;
|
|
127
|
+
this.syncProviderRequestKey();
|
|
128
|
+
if (options.status !== void 0) this.status = options.status;
|
|
129
|
+
if (options.expiresAt !== void 0) this.expiresAt = coerceAgreementDate(options.expiresAt);
|
|
130
|
+
if (options.cancellationReason !== void 0) this.cancellationReason = options.cancellationReason;
|
|
131
|
+
if (options.lastProviderEventAt !== void 0) this.lastProviderEventAt = coerceAgreementDate(options.lastProviderEventAt);
|
|
132
|
+
if (options.lastReconciledAt !== void 0) this.lastReconciledAt = coerceAgreementDate(options.lastReconciledAt);
|
|
133
|
+
if (options.completedAt !== void 0) this.completedAt = coerceAgreementDate(options.completedAt);
|
|
134
|
+
if (options.effectiveFrom !== void 0) this.effectiveFrom = coerceAgreementDate(options.effectiveFrom);
|
|
135
|
+
if (options.effectiveTo !== void 0) this.effectiveTo = coerceAgreementDate(options.effectiveTo);
|
|
136
|
+
if (options.supersedesExecutedAgreementId !== void 0) this.supersedesExecutedAgreementId = options.supersedesExecutedAgreementId;
|
|
137
|
+
if (options.signedDocumentAssetId !== void 0) this.signedDocumentAssetId = options.signedDocumentAssetId;
|
|
138
|
+
if (options.signedDocumentSha256 !== void 0) this.signedDocumentSha256 = options.signedDocumentSha256;
|
|
139
|
+
if (options.signedDocumentSizeBytes !== void 0) this.signedDocumentSizeBytes = options.signedDocumentSizeBytes;
|
|
140
|
+
if (options.signedDocumentMediaType !== void 0) this.signedDocumentMediaType = options.signedDocumentMediaType;
|
|
141
|
+
if (options.signedDocumentFilename !== void 0) this.signedDocumentFilename = options.signedDocumentFilename;
|
|
142
|
+
if (options.auditTrailAssetId !== void 0) this.auditTrailAssetId = options.auditTrailAssetId;
|
|
143
|
+
if (options.auditTrailSha256 !== void 0) this.auditTrailSha256 = options.auditTrailSha256;
|
|
144
|
+
if (options.auditTrailSizeBytes !== void 0) this.auditTrailSizeBytes = options.auditTrailSizeBytes;
|
|
145
|
+
if (options.auditTrailMediaType !== void 0) this.auditTrailMediaType = options.auditTrailMediaType;
|
|
146
|
+
if (options.auditTrailFilename !== void 0) this.auditTrailFilename = options.auditTrailFilename;
|
|
147
|
+
if (options.attemptCount !== void 0) this.attemptCount = options.attemptCount;
|
|
148
|
+
if (options.createLeaseId !== void 0) this.createLeaseId = options.createLeaseId;
|
|
149
|
+
if (options.createLeaseExpiresAt !== void 0) this.createLeaseExpiresAt = coerceAgreementDate(options.createLeaseExpiresAt);
|
|
150
|
+
if (options.lastError !== void 0) this.lastError = options.lastError;
|
|
151
|
+
if (options.metadata !== void 0) this.metadata = options.metadata;
|
|
152
|
+
}
|
|
153
|
+
async initialize() {
|
|
154
|
+
await super.initialize();
|
|
155
|
+
this.expiresAt = coerceAgreementDate(this.expiresAt);
|
|
156
|
+
this.lastProviderEventAt = coerceAgreementDate(this.lastProviderEventAt);
|
|
157
|
+
this.lastReconciledAt = coerceAgreementDate(this.lastReconciledAt);
|
|
158
|
+
this.completedAt = coerceAgreementDate(this.completedAt);
|
|
159
|
+
this.effectiveFrom = coerceAgreementDate(this.effectiveFrom);
|
|
160
|
+
this.effectiveTo = coerceAgreementDate(this.effectiveTo);
|
|
161
|
+
this.createLeaseExpiresAt = coerceAgreementDate(this.createLeaseExpiresAt);
|
|
162
|
+
if (this.isPersisted) persistedExecutionIdentity.set(this, this.captureIdentity());
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
async save() {
|
|
166
|
+
const captured = persistedExecutionIdentity.get(this);
|
|
167
|
+
if (captured) this.assertBoundValueUnchanged("provider request", captured.providerRequestId, this.providerRequestId);
|
|
168
|
+
this.syncProviderRequestKey();
|
|
169
|
+
if (captured) {
|
|
170
|
+
if (captured.intent !== this.serializeIntent()) throw new Error(`AgreementExecution ${this.id ?? "<new>"}: execution identity is immutable`);
|
|
171
|
+
this.assertBoundValueUnchanged("provider request key", captured.providerRequestKey ?? "", this.providerRequestKey ?? "");
|
|
172
|
+
this.assertBoundValueUnchanged("signed document evidence", captured.signedDocumentEvidence, this.serializeSignedDocumentEvidence());
|
|
173
|
+
this.assertBoundValueUnchanged("audit trail evidence", captured.auditTrailEvidence, this.serializeAuditTrailEvidence());
|
|
174
|
+
}
|
|
175
|
+
const saved = await super.save();
|
|
176
|
+
persistedExecutionIdentity.set(this, this.captureIdentity());
|
|
177
|
+
return saved;
|
|
178
|
+
}
|
|
179
|
+
getSignerIntent() {
|
|
180
|
+
try {
|
|
181
|
+
const value = JSON.parse(this.signerIntent);
|
|
182
|
+
return Array.isArray(value) ? value : [];
|
|
183
|
+
} catch {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
captureIdentity() {
|
|
188
|
+
return {
|
|
189
|
+
intent: this.serializeIntent(),
|
|
190
|
+
providerRequestId: this.providerRequestId,
|
|
191
|
+
providerRequestKey: this.providerRequestKey,
|
|
192
|
+
signedDocumentEvidence: this.serializeSignedDocumentEvidence(),
|
|
193
|
+
auditTrailEvidence: this.serializeAuditTrailEvidence()
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
serializeIntent() {
|
|
197
|
+
return JSON.stringify({
|
|
198
|
+
tenantId: this.tenantId,
|
|
199
|
+
provider: this.provider,
|
|
200
|
+
providerAccountRef: this.providerAccountRef,
|
|
201
|
+
credentialRef: this.credentialRef,
|
|
202
|
+
idempotencyKey: this.idempotencyKey,
|
|
203
|
+
sourceKind: this.sourceKind,
|
|
204
|
+
sourceId: this.sourceId,
|
|
205
|
+
sourceVersion: this.sourceVersion,
|
|
206
|
+
sourceAssetId: this.sourceAssetId,
|
|
207
|
+
sourceSha256: this.sourceSha256,
|
|
208
|
+
sourceSizeBytes: this.sourceSizeBytes,
|
|
209
|
+
requestIntentSha256: this.requestIntentSha256,
|
|
210
|
+
title: this.title,
|
|
211
|
+
signerIntent: this.signerIntent,
|
|
212
|
+
effectiveFrom: this.effectiveFrom?.toISOString() ?? null,
|
|
213
|
+
effectiveTo: this.effectiveTo?.toISOString() ?? null,
|
|
214
|
+
supersedesExecutedAgreementId: this.supersedesExecutedAgreementId,
|
|
215
|
+
metadata: this.metadata
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
serializeSignedDocumentEvidence() {
|
|
219
|
+
if (!this.signedDocumentAssetId) return "";
|
|
220
|
+
return JSON.stringify({
|
|
221
|
+
assetId: this.signedDocumentAssetId,
|
|
222
|
+
sha256: this.signedDocumentSha256,
|
|
223
|
+
sizeBytes: this.signedDocumentSizeBytes,
|
|
224
|
+
mediaType: this.signedDocumentMediaType,
|
|
225
|
+
filename: this.signedDocumentFilename
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
serializeAuditTrailEvidence() {
|
|
229
|
+
if (!this.auditTrailAssetId) return "";
|
|
230
|
+
return JSON.stringify({
|
|
231
|
+
assetId: this.auditTrailAssetId,
|
|
232
|
+
sha256: this.auditTrailSha256,
|
|
233
|
+
sizeBytes: this.auditTrailSizeBytes,
|
|
234
|
+
mediaType: this.auditTrailMediaType,
|
|
235
|
+
filename: this.auditTrailFilename
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
assertBoundValueUnchanged(label, captured, current) {
|
|
239
|
+
if (captured && captured !== current) throw new Error(`AgreementExecution ${this.id ?? "<new>"}: ${label} is immutable once bound`);
|
|
240
|
+
}
|
|
241
|
+
syncProviderRequestKey() {
|
|
242
|
+
if (!this.providerRequestId) {
|
|
243
|
+
this.providerRequestKey = null;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!this.tenantId || !this.provider) throw new Error("AgreementExecution provider request binding requires tenant and provider");
|
|
247
|
+
const expected = `${this.tenantId}:${this.provider}:${this.providerRequestId}`;
|
|
248
|
+
if (this.providerRequestKey && this.providerRequestKey !== expected) throw new Error(`AgreementExecution ${this.id ?? "<new>"}: provider request key does not match its binding`);
|
|
249
|
+
this.providerRequestKey = expected;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
__decorateClass$2([tenantId()], AgreementExecution.prototype, "tenantId", 2);
|
|
253
|
+
__decorateClass$2([field({ required: true })], AgreementExecution.prototype, "provider", 2);
|
|
254
|
+
__decorateClass$2([field({ required: true })], AgreementExecution.prototype, "idempotencyKey", 2);
|
|
255
|
+
__decorateClass$2([field({ required: true })], AgreementExecution.prototype, "sourceKind", 2);
|
|
256
|
+
__decorateClass$2([field({ required: true })], AgreementExecution.prototype, "sourceId", 2);
|
|
257
|
+
__decorateClass$2([crossPackageRef("@happyvertical/smrt-assets:Asset")], AgreementExecution.prototype, "sourceAssetId", 2);
|
|
258
|
+
__decorateClass$2([field({
|
|
259
|
+
type: "text",
|
|
260
|
+
nullable: true,
|
|
261
|
+
unique: true
|
|
262
|
+
})], AgreementExecution.prototype, "providerRequestKey", 2);
|
|
263
|
+
__decorateClass$2([crossPackageRef("@happyvertical/smrt-assets:Asset")], AgreementExecution.prototype, "signedDocumentAssetId", 2);
|
|
264
|
+
__decorateClass$2([crossPackageRef("@happyvertical/smrt-assets:Asset")], AgreementExecution.prototype, "auditTrailAssetId", 2);
|
|
265
|
+
AgreementExecution = __decorateClass$2([TenantScoped({ mode: "required" }), smrt({
|
|
266
|
+
conflictColumns: ["tenant_id", "idempotency_key"],
|
|
267
|
+
api: false,
|
|
268
|
+
mcp: false,
|
|
269
|
+
cli: false
|
|
270
|
+
})], AgreementExecution);
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/agreements/collections/AgreementExecutionCollection.ts
|
|
273
|
+
var AgreementExecutionCollection = class extends SmrtCollection {
|
|
274
|
+
static _itemClass = AgreementExecution;
|
|
275
|
+
async findByIdempotencyKey(idempotencyKey) {
|
|
276
|
+
return (await this.list({
|
|
277
|
+
where: { idempotencyKey },
|
|
278
|
+
limit: 1
|
|
279
|
+
}))[0] ?? null;
|
|
280
|
+
}
|
|
281
|
+
async findByProviderRequest(provider, providerRequestId) {
|
|
282
|
+
return (await this.list({
|
|
283
|
+
where: {
|
|
284
|
+
provider,
|
|
285
|
+
providerRequestId
|
|
286
|
+
},
|
|
287
|
+
limit: 1
|
|
288
|
+
}))[0] ?? null;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Atomically claim an expired or cleared provider-create lease.
|
|
292
|
+
*
|
|
293
|
+
* The explicit tenant predicate and expected-state predicates make the raw
|
|
294
|
+
* compare-and-swap fail closed across workers. `RETURNING id` is used
|
|
295
|
+
* because not every database adapter reports affected-row counts reliably.
|
|
296
|
+
*/
|
|
297
|
+
async claimCreateAttempt(input) {
|
|
298
|
+
if (requireTenantId() !== input.tenantId) throw new Error("AgreementExecution create-lease tenant mismatch");
|
|
299
|
+
const claimed = (await this._db.query(`UPDATE ${this.tableName}
|
|
300
|
+
SET attempt_count = attempt_count + 1,
|
|
301
|
+
status = ?,
|
|
302
|
+
last_error = '',
|
|
303
|
+
create_lease_id = ?,
|
|
304
|
+
create_lease_expires_at = ?
|
|
305
|
+
WHERE id = ?
|
|
306
|
+
AND tenant_id = ?
|
|
307
|
+
AND attempt_count = ?
|
|
308
|
+
AND status = ?
|
|
309
|
+
AND COALESCE(last_error, '') = ?
|
|
310
|
+
AND COALESCE(create_lease_id, '') = ?
|
|
311
|
+
AND (provider_request_id IS NULL OR provider_request_id = '')
|
|
312
|
+
AND (create_lease_expires_at IS NULL OR create_lease_expires_at <= ?)
|
|
313
|
+
RETURNING id`, "prepared", input.operationId, input.leaseExpiresAt.toISOString(), input.executionId, input.tenantId, input.expectedAttemptCount, input.expectedStatus, input.expectedLastError, input.expectedLeaseId, input.now.toISOString())).rows[0];
|
|
314
|
+
if (typeof claimed?.id !== "string") return null;
|
|
315
|
+
return await this.get({ id: claimed.id });
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Persist a provider-create result only while the caller still owns its
|
|
319
|
+
* lease. This prevents a slow, expired attempt from overwriting a newer
|
|
320
|
+
* recovery attempt after returning from the provider.
|
|
321
|
+
*/
|
|
322
|
+
async completeCreateAttempt(input) {
|
|
323
|
+
this.assertCreateAttemptTenant(input.tenantId);
|
|
324
|
+
const providerRequestKey = `${input.tenantId}:${input.provider}:${input.providerRequestId}`;
|
|
325
|
+
const result = await this._db.query(`UPDATE ${this.tableName}
|
|
326
|
+
SET provider_request_id = ?,
|
|
327
|
+
provider_request_key = ?,
|
|
328
|
+
status = ?,
|
|
329
|
+
expires_at = ?,
|
|
330
|
+
last_error = '',
|
|
331
|
+
create_lease_id = '',
|
|
332
|
+
create_lease_expires_at = NULL
|
|
333
|
+
WHERE id = ?
|
|
334
|
+
AND tenant_id = ?
|
|
335
|
+
AND provider = ?
|
|
336
|
+
AND COALESCE(create_lease_id, '') = ?
|
|
337
|
+
AND (provider_request_id IS NULL OR provider_request_id = '')
|
|
338
|
+
RETURNING id`, input.providerRequestId, providerRequestKey, input.status, input.expiresAt?.toISOString() ?? null, input.executionId, input.tenantId, input.provider, input.operationId);
|
|
339
|
+
return await this.getReturnedExecution(result);
|
|
340
|
+
}
|
|
341
|
+
/** Record a create failure only while the caller still owns its lease. */
|
|
342
|
+
async failCreateAttempt(input) {
|
|
343
|
+
this.assertCreateAttemptTenant(input.tenantId);
|
|
344
|
+
const result = await this._db.query(`UPDATE ${this.tableName}
|
|
345
|
+
SET status = ?,
|
|
346
|
+
last_error = ?,
|
|
347
|
+
create_lease_id = '',
|
|
348
|
+
create_lease_expires_at = NULL
|
|
349
|
+
WHERE id = ?
|
|
350
|
+
AND tenant_id = ?
|
|
351
|
+
AND COALESCE(create_lease_id, '') = ?
|
|
352
|
+
AND (provider_request_id IS NULL OR provider_request_id = '')
|
|
353
|
+
RETURNING id`, "failed", input.lastError, input.executionId, input.tenantId, input.operationId);
|
|
354
|
+
return await this.getReturnedExecution(result);
|
|
355
|
+
}
|
|
356
|
+
/** Bind an adopted provider request without saving a stale object snapshot. */
|
|
357
|
+
async bindProviderRequest(input) {
|
|
358
|
+
this.assertCreateAttemptTenant(input.tenantId);
|
|
359
|
+
const providerRequestKey = `${input.tenantId}:${input.provider}:${input.providerRequestId}`;
|
|
360
|
+
const result = await this._db.query(`UPDATE ${this.tableName}
|
|
361
|
+
SET provider_request_id = ?,
|
|
362
|
+
provider_request_key = ?
|
|
363
|
+
WHERE id = ?
|
|
364
|
+
AND tenant_id = ?
|
|
365
|
+
AND provider = ?
|
|
366
|
+
AND (provider_request_id IS NULL OR provider_request_id = '' OR provider_request_id = ?)
|
|
367
|
+
RETURNING id`, input.providerRequestId, providerRequestKey, input.executionId, input.tenantId, input.provider, input.providerRequestId);
|
|
368
|
+
return await this.getReturnedExecution(result);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Apply provider lifecycle state only while status still matches the
|
|
372
|
+
* caller's snapshot and the candidate observation is not older than the
|
|
373
|
+
* persisted provider event. Service retries re-read the winner before
|
|
374
|
+
* deciding whether the candidate state remains monotonic.
|
|
375
|
+
*/
|
|
376
|
+
async compareAndSetLifecycle(input) {
|
|
377
|
+
this.assertCreateAttemptTenant(input.tenantId);
|
|
378
|
+
const result = await this._db.query(`UPDATE ${this.tableName}
|
|
379
|
+
SET status = ?,
|
|
380
|
+
expires_at = ?,
|
|
381
|
+
cancellation_reason = ?,
|
|
382
|
+
last_provider_event_at = ?,
|
|
383
|
+
last_reconciled_at = ?,
|
|
384
|
+
completed_at = ?,
|
|
385
|
+
last_error = ''
|
|
386
|
+
WHERE id = ?
|
|
387
|
+
AND tenant_id = ?
|
|
388
|
+
AND status = ?
|
|
389
|
+
AND (last_provider_event_at IS NULL OR last_provider_event_at <= ?)
|
|
390
|
+
RETURNING id`, input.status, input.expiresAt?.toISOString() ?? null, input.cancellationReason, input.lastProviderEventAt.toISOString(), input.lastReconciledAt?.toISOString() ?? null, input.completedAt?.toISOString() ?? null, input.executionId, input.tenantId, input.expectedStatus, input.lastProviderEventAt.toISOString());
|
|
391
|
+
return await this.getReturnedExecution(result);
|
|
392
|
+
}
|
|
393
|
+
/** Increment audit attempts without writing any lifecycle columns. */
|
|
394
|
+
async incrementAttemptCount(tenantId, executionId) {
|
|
395
|
+
this.assertCreateAttemptTenant(tenantId);
|
|
396
|
+
const result = await this._db.query(`UPDATE ${this.tableName}
|
|
397
|
+
SET attempt_count = attempt_count + 1
|
|
398
|
+
WHERE id = ?
|
|
399
|
+
AND tenant_id = ?
|
|
400
|
+
RETURNING id`, executionId, tenantId);
|
|
401
|
+
const execution = await this.getReturnedExecution(result);
|
|
402
|
+
if (!execution) throw new Error(`AgreementExecution '${executionId}' was not found`);
|
|
403
|
+
return execution;
|
|
404
|
+
}
|
|
405
|
+
/** Update failure diagnostics without writing a stale lifecycle snapshot. */
|
|
406
|
+
async updateLastError(input) {
|
|
407
|
+
this.assertCreateAttemptTenant(input.tenantId);
|
|
408
|
+
const result = await this._db.query(`UPDATE ${this.tableName}
|
|
409
|
+
SET last_error = ?
|
|
410
|
+
WHERE id = ?
|
|
411
|
+
AND tenant_id = ?
|
|
412
|
+
RETURNING id`, input.lastError, input.executionId, input.tenantId);
|
|
413
|
+
const execution = await this.getReturnedExecution(result);
|
|
414
|
+
if (!execution) throw new Error(`AgreementExecution '${input.executionId}' was not found`);
|
|
415
|
+
return execution;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Bind one immutable evidence artifact with a tenant-fenced compare-and-swap.
|
|
419
|
+
* Concurrent finalizers can only publish one asset identity for each kind.
|
|
420
|
+
*/
|
|
421
|
+
async bindEvidenceArtifact(input) {
|
|
422
|
+
this.assertCreateAttemptTenant(input.tenantId);
|
|
423
|
+
const columns = input.kind === "signed_document" ? {
|
|
424
|
+
assetId: "signed_document_asset_id",
|
|
425
|
+
sha256: "signed_document_sha256",
|
|
426
|
+
sizeBytes: "signed_document_size_bytes",
|
|
427
|
+
mediaType: "signed_document_media_type",
|
|
428
|
+
filename: "signed_document_filename"
|
|
429
|
+
} : {
|
|
430
|
+
assetId: "audit_trail_asset_id",
|
|
431
|
+
sha256: "audit_trail_sha256",
|
|
432
|
+
sizeBytes: "audit_trail_size_bytes",
|
|
433
|
+
mediaType: "audit_trail_media_type",
|
|
434
|
+
filename: "audit_trail_filename"
|
|
435
|
+
};
|
|
436
|
+
const result = await this._db.query(`UPDATE ${this.tableName}
|
|
437
|
+
SET ${columns.assetId} = ?,
|
|
438
|
+
${columns.sha256} = ?,
|
|
439
|
+
${columns.sizeBytes} = ?,
|
|
440
|
+
${columns.mediaType} = ?,
|
|
441
|
+
${columns.filename} = ?
|
|
442
|
+
WHERE id = ?
|
|
443
|
+
AND tenant_id = ?
|
|
444
|
+
AND provider_request_id IS NOT NULL
|
|
445
|
+
AND provider_request_id <> ''
|
|
446
|
+
AND ${columns.assetId} IS NULL
|
|
447
|
+
RETURNING id`, input.assetId, input.sha256, input.sizeBytes, input.mediaType, input.filename, input.executionId, input.tenantId);
|
|
448
|
+
return await this.getReturnedExecution(result);
|
|
449
|
+
}
|
|
450
|
+
async findBySource(sourceKind, sourceId) {
|
|
451
|
+
return await this.list({
|
|
452
|
+
where: {
|
|
453
|
+
sourceKind,
|
|
454
|
+
sourceId
|
|
455
|
+
},
|
|
456
|
+
orderBy: "source_version DESC"
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
assertCreateAttemptTenant(tenantId) {
|
|
460
|
+
if (requireTenantId() !== tenantId) throw new Error("AgreementExecution create-lease tenant mismatch");
|
|
461
|
+
}
|
|
462
|
+
async getReturnedExecution(result) {
|
|
463
|
+
const returned = result.rows?.[0];
|
|
464
|
+
if (typeof returned?.id !== "string") return null;
|
|
465
|
+
return await this.get({ id: returned.id });
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
//#endregion
|
|
469
|
+
//#region src/agreements/models/AgreementExecutionEvent.ts
|
|
470
|
+
var __defProp$1 = Object.defineProperty;
|
|
471
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
472
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
473
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
474
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
475
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
476
|
+
return result;
|
|
477
|
+
};
|
|
478
|
+
var persistedEventState = /* @__PURE__ */ new WeakMap();
|
|
479
|
+
var AgreementExecutionEvent = class extends SmrtObject {
|
|
480
|
+
tenantId = "";
|
|
481
|
+
executionId = "";
|
|
482
|
+
provider = "";
|
|
483
|
+
providerEventId = "";
|
|
484
|
+
eventOrigin = "provider_webhook";
|
|
485
|
+
operationId = "";
|
|
486
|
+
dedupeKey = "";
|
|
487
|
+
orderingKey = "";
|
|
488
|
+
eventType = "";
|
|
489
|
+
status = "prepared";
|
|
490
|
+
occurredAt = /* @__PURE__ */ new Date();
|
|
491
|
+
receivedAt = /* @__PURE__ */ new Date();
|
|
492
|
+
payloadSha256 = "";
|
|
493
|
+
signerEvidence = "[]";
|
|
494
|
+
payload = "{}";
|
|
495
|
+
constructor(options = {}) {
|
|
496
|
+
super(options);
|
|
497
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
498
|
+
if (options.executionId !== void 0) this.executionId = options.executionId;
|
|
499
|
+
if (options.provider !== void 0) this.provider = options.provider;
|
|
500
|
+
if (options.providerEventId !== void 0) this.providerEventId = options.providerEventId;
|
|
501
|
+
if (options.eventOrigin !== void 0) this.eventOrigin = options.eventOrigin;
|
|
502
|
+
if (options.operationId !== void 0) this.operationId = options.operationId;
|
|
503
|
+
if (options.dedupeKey !== void 0) this.dedupeKey = options.dedupeKey;
|
|
504
|
+
if (options.orderingKey !== void 0) this.orderingKey = options.orderingKey;
|
|
505
|
+
if (options.eventType !== void 0) this.eventType = options.eventType;
|
|
506
|
+
if (options.status !== void 0) this.status = options.status;
|
|
507
|
+
if (options.occurredAt !== void 0) this.occurredAt = coerceAgreementDate(options.occurredAt) ?? /* @__PURE__ */ new Date();
|
|
508
|
+
if (options.receivedAt !== void 0) this.receivedAt = coerceAgreementDate(options.receivedAt) ?? /* @__PURE__ */ new Date();
|
|
509
|
+
if (options.payloadSha256 !== void 0) this.payloadSha256 = options.payloadSha256;
|
|
510
|
+
if (options.signerEvidence !== void 0) this.signerEvidence = options.signerEvidence;
|
|
511
|
+
if (options.payload !== void 0) this.payload = options.payload;
|
|
512
|
+
}
|
|
513
|
+
async initialize() {
|
|
514
|
+
await super.initialize();
|
|
515
|
+
this.occurredAt = coerceAgreementDate(this.occurredAt) ?? /* @__PURE__ */ new Date();
|
|
516
|
+
this.receivedAt = coerceAgreementDate(this.receivedAt) ?? /* @__PURE__ */ new Date();
|
|
517
|
+
if (this.isPersisted) persistedEventState.set(this, this.serializeState());
|
|
518
|
+
return this;
|
|
519
|
+
}
|
|
520
|
+
async save() {
|
|
521
|
+
const captured = persistedEventState.get(this);
|
|
522
|
+
if (captured !== void 0 && captured !== this.serializeState()) throw new Error(`AgreementExecutionEvent ${this.id ?? "<new>"}: verified events are immutable`);
|
|
523
|
+
if (captured === void 0 && !this.isPersisted) this.requireInsertOnSave();
|
|
524
|
+
const result = await super.save();
|
|
525
|
+
persistedEventState.set(this, this.serializeState());
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
serializeState() {
|
|
529
|
+
return JSON.stringify({
|
|
530
|
+
tenantId: this.tenantId,
|
|
531
|
+
executionId: this.executionId,
|
|
532
|
+
provider: this.provider,
|
|
533
|
+
providerEventId: this.providerEventId,
|
|
534
|
+
eventOrigin: this.eventOrigin,
|
|
535
|
+
operationId: this.operationId,
|
|
536
|
+
dedupeKey: this.dedupeKey,
|
|
537
|
+
orderingKey: this.orderingKey,
|
|
538
|
+
eventType: this.eventType,
|
|
539
|
+
status: this.status,
|
|
540
|
+
occurredAt: this.occurredAt.toISOString(),
|
|
541
|
+
receivedAt: this.receivedAt.toISOString(),
|
|
542
|
+
payloadSha256: this.payloadSha256,
|
|
543
|
+
signerEvidence: this.signerEvidence,
|
|
544
|
+
payload: this.payload
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
__decorateClass$1([tenantId()], AgreementExecutionEvent.prototype, "tenantId", 2);
|
|
549
|
+
__decorateClass$1([foreignKey("AgreementExecution", { required: true })], AgreementExecutionEvent.prototype, "executionId", 2);
|
|
550
|
+
__decorateClass$1([field({ required: true })], AgreementExecutionEvent.prototype, "dedupeKey", 2);
|
|
551
|
+
AgreementExecutionEvent = __decorateClass$1([TenantScoped({ mode: "required" }), smrt({
|
|
552
|
+
conflictColumns: ["tenant_id", "dedupe_key"],
|
|
553
|
+
api: false,
|
|
554
|
+
mcp: false,
|
|
555
|
+
cli: false
|
|
556
|
+
})], AgreementExecutionEvent);
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/agreements/collections/AgreementExecutionEventCollection.ts
|
|
559
|
+
var AgreementExecutionEventCollection = class extends SmrtCollection {
|
|
560
|
+
static _itemClass = AgreementExecutionEvent;
|
|
561
|
+
async findByDedupeKey(dedupeKey) {
|
|
562
|
+
return (await this.list({
|
|
563
|
+
where: { dedupeKey },
|
|
564
|
+
limit: 1
|
|
565
|
+
}))[0] ?? null;
|
|
566
|
+
}
|
|
567
|
+
async recordVerified(options) {
|
|
568
|
+
if (!options.dedupeKey) throw new Error("AgreementExecutionEvent requires a dedupe key");
|
|
569
|
+
const occurredAt = coerceAgreementDate(options.occurredAt);
|
|
570
|
+
if (!occurredAt) throw new Error("AgreementExecutionEvent requires a valid occurredAt");
|
|
571
|
+
const receivedAt = coerceAgreementDate(options.receivedAt);
|
|
572
|
+
if (!receivedAt) throw new Error("AgreementExecutionEvent requires a valid receivedAt");
|
|
573
|
+
const existing = await this.findByDedupeKey(options.dedupeKey);
|
|
574
|
+
if (existing) {
|
|
575
|
+
this.assertSameVerifiedEvent(existing, options, occurredAt);
|
|
576
|
+
return {
|
|
577
|
+
event: existing,
|
|
578
|
+
created: false
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
const { occurredAt: _occurredAt, receivedAt: _receivedAt, ...rest } = options;
|
|
582
|
+
try {
|
|
583
|
+
return {
|
|
584
|
+
event: await this.create({
|
|
585
|
+
...rest,
|
|
586
|
+
occurredAt,
|
|
587
|
+
receivedAt,
|
|
588
|
+
_insertOnly: true
|
|
589
|
+
}),
|
|
590
|
+
created: true
|
|
591
|
+
};
|
|
592
|
+
} catch (error) {
|
|
593
|
+
const raced = await this.findByDedupeKey(options.dedupeKey);
|
|
594
|
+
if (!raced) throw error;
|
|
595
|
+
this.assertSameVerifiedEvent(raced, options, occurredAt);
|
|
596
|
+
return {
|
|
597
|
+
event: raced,
|
|
598
|
+
created: false
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
assertSameVerifiedEvent(existing, options, occurredAt) {
|
|
603
|
+
if (existing.executionId !== options.executionId || existing.provider !== options.provider || existing.providerEventId !== (options.providerEventId ?? "") || existing.eventOrigin !== (options.eventOrigin ?? "provider_webhook") || existing.operationId !== (options.operationId ?? "") || existing.orderingKey !== (options.orderingKey ?? "") || existing.eventType !== (options.eventType ?? "") || existing.status !== options.status || existing.occurredAt.toISOString() !== occurredAt.toISOString() || existing.payloadSha256 !== (options.payloadSha256 ?? "") || existing.signerEvidence !== (options.signerEvidence ?? "[]") || existing.payload !== (options.payload ?? "{}")) throw new Error(`AgreementExecutionEvent dedupe key '${options.dedupeKey}' collides with different verified evidence`);
|
|
604
|
+
}
|
|
605
|
+
async findByExecution(executionId) {
|
|
606
|
+
return await this.list({
|
|
607
|
+
where: { executionId },
|
|
608
|
+
orderBy: "occurred_at ASC"
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async findProviderEventsByExecution(executionId) {
|
|
612
|
+
return await this.list({
|
|
613
|
+
where: {
|
|
614
|
+
executionId,
|
|
615
|
+
eventOrigin: "provider_webhook"
|
|
616
|
+
},
|
|
617
|
+
orderBy: "occurred_at ASC"
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/agreements/models/ExecutedAgreement.ts
|
|
623
|
+
var __defProp = Object.defineProperty;
|
|
624
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
625
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
626
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
627
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
628
|
+
if (kind && result) __defProp(target, key, result);
|
|
629
|
+
return result;
|
|
630
|
+
};
|
|
631
|
+
var persistedAgreementState = /* @__PURE__ */ new WeakMap();
|
|
632
|
+
var ExecutedAgreement = class extends SmrtObject {
|
|
633
|
+
tenantId = "";
|
|
634
|
+
executionId = "";
|
|
635
|
+
sourceKind = "";
|
|
636
|
+
sourceId = "";
|
|
637
|
+
sourceVersion = 1;
|
|
638
|
+
sourceAssetId = "";
|
|
639
|
+
sourceSha256 = "";
|
|
640
|
+
sourceSizeBytes = 0;
|
|
641
|
+
signedDocumentAssetId = "";
|
|
642
|
+
signedDocumentSha256 = "";
|
|
643
|
+
signedDocumentSizeBytes = 0;
|
|
644
|
+
signedDocumentMediaType = "";
|
|
645
|
+
signedDocumentFilename = "";
|
|
646
|
+
auditTrailAssetId = "";
|
|
647
|
+
auditTrailSha256 = "";
|
|
648
|
+
auditTrailSizeBytes = 0;
|
|
649
|
+
auditTrailMediaType = "";
|
|
650
|
+
auditTrailFilename = "";
|
|
651
|
+
signerEvidence = "[]";
|
|
652
|
+
acceptedAt = /* @__PURE__ */ new Date();
|
|
653
|
+
effectiveFrom = null;
|
|
654
|
+
effectiveTo = null;
|
|
655
|
+
supersedesExecutedAgreementId = "";
|
|
656
|
+
metadata = "{}";
|
|
657
|
+
constructor(options = {}) {
|
|
658
|
+
super(options);
|
|
659
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
660
|
+
if (options.executionId !== void 0) this.executionId = options.executionId;
|
|
661
|
+
if (options.sourceKind !== void 0) this.sourceKind = options.sourceKind;
|
|
662
|
+
if (options.sourceId !== void 0) this.sourceId = options.sourceId;
|
|
663
|
+
if (options.sourceVersion !== void 0) this.sourceVersion = options.sourceVersion;
|
|
664
|
+
if (options.sourceAssetId !== void 0) this.sourceAssetId = options.sourceAssetId;
|
|
665
|
+
if (options.sourceSha256 !== void 0) this.sourceSha256 = options.sourceSha256;
|
|
666
|
+
if (options.sourceSizeBytes !== void 0) this.sourceSizeBytes = options.sourceSizeBytes;
|
|
667
|
+
if (options.signedDocumentAssetId !== void 0) this.signedDocumentAssetId = options.signedDocumentAssetId;
|
|
668
|
+
if (options.signedDocumentSha256 !== void 0) this.signedDocumentSha256 = options.signedDocumentSha256;
|
|
669
|
+
if (options.signedDocumentSizeBytes !== void 0) this.signedDocumentSizeBytes = options.signedDocumentSizeBytes;
|
|
670
|
+
if (options.signedDocumentMediaType !== void 0) this.signedDocumentMediaType = options.signedDocumentMediaType;
|
|
671
|
+
if (options.signedDocumentFilename !== void 0) this.signedDocumentFilename = options.signedDocumentFilename;
|
|
672
|
+
if (options.auditTrailAssetId !== void 0) this.auditTrailAssetId = options.auditTrailAssetId;
|
|
673
|
+
if (options.auditTrailSha256 !== void 0) this.auditTrailSha256 = options.auditTrailSha256;
|
|
674
|
+
if (options.auditTrailSizeBytes !== void 0) this.auditTrailSizeBytes = options.auditTrailSizeBytes;
|
|
675
|
+
if (options.auditTrailMediaType !== void 0) this.auditTrailMediaType = options.auditTrailMediaType;
|
|
676
|
+
if (options.auditTrailFilename !== void 0) this.auditTrailFilename = options.auditTrailFilename;
|
|
677
|
+
if (options.signerEvidence !== void 0) this.signerEvidence = options.signerEvidence;
|
|
678
|
+
if (options.acceptedAt !== void 0) this.acceptedAt = coerceAgreementDate(options.acceptedAt) ?? /* @__PURE__ */ new Date();
|
|
679
|
+
if (options.effectiveFrom !== void 0) this.effectiveFrom = coerceAgreementDate(options.effectiveFrom);
|
|
680
|
+
if (options.effectiveTo !== void 0) this.effectiveTo = coerceAgreementDate(options.effectiveTo);
|
|
681
|
+
if (options.supersedesExecutedAgreementId !== void 0) this.supersedesExecutedAgreementId = options.supersedesExecutedAgreementId;
|
|
682
|
+
if (options.metadata !== void 0) this.metadata = options.metadata;
|
|
683
|
+
}
|
|
684
|
+
async initialize() {
|
|
685
|
+
await super.initialize();
|
|
686
|
+
this.acceptedAt = coerceAgreementDate(this.acceptedAt) ?? /* @__PURE__ */ new Date();
|
|
687
|
+
this.effectiveFrom = coerceAgreementDate(this.effectiveFrom);
|
|
688
|
+
this.effectiveTo = coerceAgreementDate(this.effectiveTo);
|
|
689
|
+
if (this.isPersisted) persistedAgreementState.set(this, this.serializeState());
|
|
690
|
+
return this;
|
|
691
|
+
}
|
|
692
|
+
isEffectiveAt(at) {
|
|
693
|
+
return !(this.effectiveFrom && at < this.effectiveFrom || this.effectiveTo && at > this.effectiveTo);
|
|
694
|
+
}
|
|
695
|
+
async save() {
|
|
696
|
+
const captured = persistedAgreementState.get(this);
|
|
697
|
+
if (captured !== void 0 && captured !== this.serializeState()) throw new Error(`ExecutedAgreement ${this.id ?? "<new>"}: executed agreements are immutable; create a versioned amendment`);
|
|
698
|
+
if (captured === void 0 && !this.isPersisted) this.requireInsertOnSave();
|
|
699
|
+
const result = await super.save();
|
|
700
|
+
persistedAgreementState.set(this, this.serializeState());
|
|
701
|
+
return result;
|
|
702
|
+
}
|
|
703
|
+
serializeState() {
|
|
704
|
+
return JSON.stringify({
|
|
705
|
+
tenantId: this.tenantId,
|
|
706
|
+
executionId: this.executionId,
|
|
707
|
+
sourceKind: this.sourceKind,
|
|
708
|
+
sourceId: this.sourceId,
|
|
709
|
+
sourceVersion: this.sourceVersion,
|
|
710
|
+
sourceAssetId: this.sourceAssetId,
|
|
711
|
+
sourceSha256: this.sourceSha256,
|
|
712
|
+
sourceSizeBytes: this.sourceSizeBytes,
|
|
713
|
+
signedDocumentAssetId: this.signedDocumentAssetId,
|
|
714
|
+
signedDocumentSha256: this.signedDocumentSha256,
|
|
715
|
+
signedDocumentSizeBytes: this.signedDocumentSizeBytes,
|
|
716
|
+
signedDocumentMediaType: this.signedDocumentMediaType,
|
|
717
|
+
signedDocumentFilename: this.signedDocumentFilename,
|
|
718
|
+
auditTrailAssetId: this.auditTrailAssetId,
|
|
719
|
+
auditTrailSha256: this.auditTrailSha256,
|
|
720
|
+
auditTrailSizeBytes: this.auditTrailSizeBytes,
|
|
721
|
+
auditTrailMediaType: this.auditTrailMediaType,
|
|
722
|
+
auditTrailFilename: this.auditTrailFilename,
|
|
723
|
+
signerEvidence: this.signerEvidence,
|
|
724
|
+
acceptedAt: this.acceptedAt.toISOString(),
|
|
725
|
+
effectiveFrom: this.effectiveFrom?.toISOString() ?? null,
|
|
726
|
+
effectiveTo: this.effectiveTo?.toISOString() ?? null,
|
|
727
|
+
supersedesExecutedAgreementId: this.supersedesExecutedAgreementId,
|
|
728
|
+
metadata: this.metadata
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
__decorateClass([tenantId()], ExecutedAgreement.prototype, "tenantId", 2);
|
|
733
|
+
__decorateClass([foreignKey("AgreementExecution", { required: true })], ExecutedAgreement.prototype, "executionId", 2);
|
|
734
|
+
__decorateClass([field({ required: true })], ExecutedAgreement.prototype, "sourceKind", 2);
|
|
735
|
+
__decorateClass([field({ required: true })], ExecutedAgreement.prototype, "sourceId", 2);
|
|
736
|
+
__decorateClass([crossPackageRef("@happyvertical/smrt-assets:Asset")], ExecutedAgreement.prototype, "sourceAssetId", 2);
|
|
737
|
+
__decorateClass([crossPackageRef("@happyvertical/smrt-assets:Asset")], ExecutedAgreement.prototype, "signedDocumentAssetId", 2);
|
|
738
|
+
__decorateClass([crossPackageRef("@happyvertical/smrt-assets:Asset")], ExecutedAgreement.prototype, "auditTrailAssetId", 2);
|
|
739
|
+
__decorateClass([foreignKey("ExecutedAgreement")], ExecutedAgreement.prototype, "supersedesExecutedAgreementId", 2);
|
|
740
|
+
ExecutedAgreement = __decorateClass([TenantScoped({ mode: "required" }), smrt({
|
|
741
|
+
conflictColumns: ["tenant_id", "execution_id"],
|
|
742
|
+
api: { include: ["list", "get"] },
|
|
743
|
+
mcp: { include: ["list", "get"] },
|
|
744
|
+
cli: false
|
|
745
|
+
})], ExecutedAgreement);
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/agreements/collections/ExecutedAgreementCollection.ts
|
|
748
|
+
var ExecutedAgreementCollection = class extends SmrtCollection {
|
|
749
|
+
static _itemClass = ExecutedAgreement;
|
|
750
|
+
async findByExecution(executionId) {
|
|
751
|
+
return (await this.list({
|
|
752
|
+
where: { executionId },
|
|
753
|
+
limit: 1
|
|
754
|
+
}))[0] ?? null;
|
|
755
|
+
}
|
|
756
|
+
async createImmutable(options) {
|
|
757
|
+
if (!options.executionId) throw new Error("ExecutedAgreement requires an executionId");
|
|
758
|
+
this.assertCompleteEvidence(options);
|
|
759
|
+
const existing = await this.findByExecution(options.executionId);
|
|
760
|
+
if (existing) {
|
|
761
|
+
this.assertSameEvidence(existing, options);
|
|
762
|
+
return {
|
|
763
|
+
agreement: existing,
|
|
764
|
+
created: false
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
const { acceptedAt, effectiveFrom, effectiveTo, ...rest } = options;
|
|
768
|
+
try {
|
|
769
|
+
return {
|
|
770
|
+
agreement: await this.create({
|
|
771
|
+
...rest,
|
|
772
|
+
...acceptedAt !== void 0 ? { acceptedAt: coerceAgreementDate(acceptedAt) ?? /* @__PURE__ */ new Date() } : {},
|
|
773
|
+
...effectiveFrom !== void 0 ? { effectiveFrom: coerceAgreementDate(effectiveFrom) } : {},
|
|
774
|
+
...effectiveTo !== void 0 ? { effectiveTo: coerceAgreementDate(effectiveTo) } : {},
|
|
775
|
+
_insertOnly: true
|
|
776
|
+
}),
|
|
777
|
+
created: true
|
|
778
|
+
};
|
|
779
|
+
} catch (error) {
|
|
780
|
+
const raced = await this.findByExecution(options.executionId);
|
|
781
|
+
if (!raced) throw error;
|
|
782
|
+
this.assertSameEvidence(raced, options);
|
|
783
|
+
return {
|
|
784
|
+
agreement: raced,
|
|
785
|
+
created: false
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
assertSameEvidence(existing, options) {
|
|
790
|
+
const acceptedAt = coerceAgreementDate(options.acceptedAt);
|
|
791
|
+
const effectiveFrom = coerceAgreementDate(options.effectiveFrom);
|
|
792
|
+
const effectiveTo = coerceAgreementDate(options.effectiveTo);
|
|
793
|
+
if (existing.sourceKind !== options.sourceKind || existing.sourceId !== options.sourceId || existing.sourceVersion !== options.sourceVersion || existing.sourceAssetId !== options.sourceAssetId || existing.sourceSha256 !== (options.sourceSha256 ?? "") || existing.sourceSizeBytes !== options.sourceSizeBytes || existing.signedDocumentAssetId !== options.signedDocumentAssetId || existing.signedDocumentSha256 !== (options.signedDocumentSha256 ?? "") || existing.signedDocumentSizeBytes !== options.signedDocumentSizeBytes || existing.signedDocumentMediaType !== options.signedDocumentMediaType || existing.signedDocumentFilename !== options.signedDocumentFilename || existing.auditTrailAssetId !== options.auditTrailAssetId || existing.auditTrailSha256 !== (options.auditTrailSha256 ?? "") || existing.auditTrailSizeBytes !== options.auditTrailSizeBytes || existing.auditTrailMediaType !== options.auditTrailMediaType || existing.auditTrailFilename !== options.auditTrailFilename || existing.signerEvidence !== options.signerEvidence || existing.acceptedAt.toISOString() !== acceptedAt?.toISOString() || (existing.effectiveFrom?.toISOString() ?? null) !== (effectiveFrom?.toISOString() ?? null) || (existing.effectiveTo?.toISOString() ?? null) !== (effectiveTo?.toISOString() ?? null) || (existing.supersedesExecutedAgreementId ?? "") !== (options.supersedesExecutedAgreementId ?? "") || existing.metadata !== (options.metadata ?? "{}")) throw new Error(`ExecutedAgreement execution '${options.executionId}' collides with different immutable evidence`);
|
|
794
|
+
}
|
|
795
|
+
assertCompleteEvidence(options) {
|
|
796
|
+
for (const [label, value] of [
|
|
797
|
+
["sourceKind", options.sourceKind],
|
|
798
|
+
["sourceId", options.sourceId],
|
|
799
|
+
["sourceAssetId", options.sourceAssetId],
|
|
800
|
+
["sourceSha256", options.sourceSha256],
|
|
801
|
+
["signedDocumentAssetId", options.signedDocumentAssetId],
|
|
802
|
+
["signedDocumentSha256", options.signedDocumentSha256],
|
|
803
|
+
["signedDocumentMediaType", options.signedDocumentMediaType],
|
|
804
|
+
["signedDocumentFilename", options.signedDocumentFilename],
|
|
805
|
+
["auditTrailAssetId", options.auditTrailAssetId],
|
|
806
|
+
["auditTrailSha256", options.auditTrailSha256],
|
|
807
|
+
["auditTrailMediaType", options.auditTrailMediaType],
|
|
808
|
+
["auditTrailFilename", options.auditTrailFilename],
|
|
809
|
+
["signerEvidence", options.signerEvidence]
|
|
810
|
+
]) if (typeof value !== "string" || !value) throw new Error(`ExecutedAgreement requires ${label}`);
|
|
811
|
+
if (typeof options.sourceVersion !== "number" || !Number.isSafeInteger(options.sourceVersion) || options.sourceVersion < 1) throw new Error("ExecutedAgreement requires a positive sourceVersion");
|
|
812
|
+
for (const [label, value] of [
|
|
813
|
+
["sourceSizeBytes", options.sourceSizeBytes],
|
|
814
|
+
["signedDocumentSizeBytes", options.signedDocumentSizeBytes],
|
|
815
|
+
["auditTrailSizeBytes", options.auditTrailSizeBytes]
|
|
816
|
+
]) if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) throw new Error(`ExecutedAgreement requires a positive ${label}`);
|
|
817
|
+
if (!coerceAgreementDate(options.acceptedAt)) throw new Error("ExecutedAgreement requires a valid acceptedAt");
|
|
818
|
+
try {
|
|
819
|
+
const signers = JSON.parse(options.signerEvidence ?? "");
|
|
820
|
+
if (!Array.isArray(signers) || signers.length === 0 || signers.some((signer) => !signer || typeof signer !== "object" || signer.status !== "signed")) throw new Error("incomplete");
|
|
821
|
+
} catch {
|
|
822
|
+
throw new Error("ExecutedAgreement requires non-empty signed signerEvidence");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async findVersionsBySource(sourceKind, sourceId) {
|
|
826
|
+
return await this.list({
|
|
827
|
+
where: {
|
|
828
|
+
sourceKind,
|
|
829
|
+
sourceId
|
|
830
|
+
},
|
|
831
|
+
orderBy: "source_version DESC"
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
async effectiveForSource(sourceKind, sourceId, at = /* @__PURE__ */ new Date()) {
|
|
835
|
+
return (await this.findVersionsBySource(sourceKind, sourceId)).find((agreement) => agreement.isEffectiveAt(at)) ?? null;
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
//#endregion
|
|
839
|
+
//#region src/agreements/services/AgreementExecutionService.ts
|
|
840
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
841
|
+
"completed",
|
|
842
|
+
"declined",
|
|
843
|
+
"cancelled",
|
|
844
|
+
"expired",
|
|
845
|
+
"failed"
|
|
846
|
+
]);
|
|
847
|
+
var STATUS_PROGRESS = /* @__PURE__ */ new Map([
|
|
848
|
+
["prepared", 0],
|
|
849
|
+
["sent", 1],
|
|
850
|
+
["delivered", 2],
|
|
851
|
+
["viewed", 3],
|
|
852
|
+
["partially_signed", 4]
|
|
853
|
+
]);
|
|
854
|
+
var DEFAULT_CREATE_LEASE_DURATION_MS = 300 * 1e3;
|
|
855
|
+
var AgreementExecutionService = class {
|
|
856
|
+
constructor(deps) {
|
|
857
|
+
this.deps = deps;
|
|
858
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
859
|
+
this.createLeaseDurationMs = deps.createLeaseDurationMs ?? DEFAULT_CREATE_LEASE_DURATION_MS;
|
|
860
|
+
if (!Number.isFinite(this.createLeaseDurationMs) || this.createLeaseDurationMs <= 0) throw new Error("Agreement execution create lease must be positive");
|
|
861
|
+
}
|
|
862
|
+
deps;
|
|
863
|
+
now;
|
|
864
|
+
createLeaseDurationMs;
|
|
865
|
+
async createExecution(input) {
|
|
866
|
+
this.assertTenant(input.tenantId);
|
|
867
|
+
this.assertProviderCapabilities();
|
|
868
|
+
this.assertCreateInput(input);
|
|
869
|
+
const sourceBytes = await readByteSource(input.document.data);
|
|
870
|
+
if (sourceBytes.byteLength === 0) throw new Error("Agreement execution document must not be empty");
|
|
871
|
+
const sourceSha256 = sha256(sourceBytes);
|
|
872
|
+
const effectiveFrom = normalizeOptionalAgreementDate(input.effectiveFrom, "effectiveFrom");
|
|
873
|
+
const effectiveTo = normalizeOptionalAgreementDate(input.effectiveTo, "effectiveTo");
|
|
874
|
+
if (effectiveFrom && effectiveTo && effectiveTo < effectiveFrom) throw new Error("Agreement execution effectiveTo must not precede effectiveFrom");
|
|
875
|
+
const requestIntentSha256 = hashRequestIntent(input, this.deps.provider.capabilities.id, sourceSha256, effectiveFrom, effectiveTo);
|
|
876
|
+
const existing = await this.deps.executions.findByIdempotencyKey(input.idempotencyKey);
|
|
877
|
+
if (existing) {
|
|
878
|
+
this.assertSameIntent(existing, input, requestIntentSha256);
|
|
879
|
+
return await this.resumeOrReplayCreate(existing, input, sourceBytes);
|
|
880
|
+
}
|
|
881
|
+
const sourceAsset = await this.deps.assets.storeSourceAsset(input.document.name, sourceBytes, {
|
|
882
|
+
mimeType: input.document.mediaType,
|
|
883
|
+
typeSlug: "agreement-source",
|
|
884
|
+
sourceType: "agreement-execution",
|
|
885
|
+
externalId: input.idempotencyKey,
|
|
886
|
+
metadata: {
|
|
887
|
+
sourceKind: input.sourceKind,
|
|
888
|
+
sourceId: input.sourceId,
|
|
889
|
+
sourceVersion: input.sourceVersion,
|
|
890
|
+
sha256: sourceSha256
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
this.assertTenantValue(sourceAsset.tenantId, input.tenantId, "source asset");
|
|
894
|
+
if (!sourceAsset.id) throw new Error("Stored source Asset has no id");
|
|
895
|
+
const operationId = randomUUID();
|
|
896
|
+
const createLeaseExpiresAt = this.newCreateLeaseExpiry();
|
|
897
|
+
let execution;
|
|
898
|
+
try {
|
|
899
|
+
execution = await this.deps.executions.create({
|
|
900
|
+
tenantId: input.tenantId,
|
|
901
|
+
provider: this.deps.provider.capabilities.id,
|
|
902
|
+
providerAccountRef: input.providerAccountRef ?? "",
|
|
903
|
+
credentialRef: input.credentialRef ?? "",
|
|
904
|
+
idempotencyKey: input.idempotencyKey,
|
|
905
|
+
sourceKind: input.sourceKind,
|
|
906
|
+
sourceId: input.sourceId,
|
|
907
|
+
sourceVersion: input.sourceVersion,
|
|
908
|
+
sourceAssetId: sourceAsset.id,
|
|
909
|
+
sourceSha256,
|
|
910
|
+
sourceSizeBytes: sourceBytes.byteLength,
|
|
911
|
+
requestIntentSha256,
|
|
912
|
+
title: input.title,
|
|
913
|
+
signerIntent: JSON.stringify(sanitizeSignerIntent(input.signers)),
|
|
914
|
+
status: "prepared",
|
|
915
|
+
effectiveFrom,
|
|
916
|
+
effectiveTo,
|
|
917
|
+
supersedesExecutedAgreementId: input.supersedesExecutedAgreementId ?? "",
|
|
918
|
+
metadata: JSON.stringify(input.metadata ?? {}),
|
|
919
|
+
attemptCount: 1,
|
|
920
|
+
createLeaseId: operationId,
|
|
921
|
+
createLeaseExpiresAt,
|
|
922
|
+
_insertOnly: true
|
|
923
|
+
});
|
|
924
|
+
} catch (error) {
|
|
925
|
+
const raced = await this.deps.executions.findByIdempotencyKey(input.idempotencyKey);
|
|
926
|
+
if (!raced || raced.sourceAssetId !== sourceAsset.id) await this.removeStoredAsset(sourceAsset);
|
|
927
|
+
if (!raced) throw error;
|
|
928
|
+
this.assertSameIntent(raced, input, requestIntentSha256);
|
|
929
|
+
return await this.resumeOrReplayCreate(raced, input, sourceBytes);
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
await this.ensureAssetAssociation("AgreementExecution", execution.id ?? "", sourceAsset.id, "source_document");
|
|
933
|
+
} catch (error) {
|
|
934
|
+
await this.failOwnedCreateAttempt(execution, operationId, summarizeError(error, false));
|
|
935
|
+
throw error;
|
|
936
|
+
}
|
|
937
|
+
return await this.sendProviderRequest(execution, input, sourceBytes, operationId);
|
|
938
|
+
}
|
|
939
|
+
async sendProviderRequest(execution, input, sourceBytes, operationId) {
|
|
940
|
+
if (execution.createLeaseId !== operationId) throw new Error(`AgreementExecution ${execution.id}: provider-create lease is not owned by this operation`);
|
|
941
|
+
try {
|
|
942
|
+
await this.recordOperationAudit(execution, operationId, "create.started", "system", { attemptCount: execution.attemptCount });
|
|
943
|
+
} catch (error) {
|
|
944
|
+
await this.failOwnedCreateAttempt(execution, operationId, summarizeError(error, false));
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
947
|
+
let request;
|
|
948
|
+
let providerResponded = false;
|
|
949
|
+
try {
|
|
950
|
+
request = await this.deps.provider.createRequest({
|
|
951
|
+
tenantId: input.tenantId,
|
|
952
|
+
idempotencyKey: input.idempotencyKey,
|
|
953
|
+
title: input.title,
|
|
954
|
+
...input.message ? { message: input.message } : {},
|
|
955
|
+
documents: [{
|
|
956
|
+
name: input.document.name,
|
|
957
|
+
mediaType: input.document.mediaType,
|
|
958
|
+
data: sourceBytes
|
|
959
|
+
}],
|
|
960
|
+
signers: input.signers,
|
|
961
|
+
...input.signingOrder !== void 0 ? { signingOrder: input.signingOrder } : {},
|
|
962
|
+
...input.expiresInDays !== void 0 ? { expiresInDays: input.expiresInDays } : {},
|
|
963
|
+
metadata: {
|
|
964
|
+
...input.metadata ?? {},
|
|
965
|
+
smrtExecutionId: execution.id ?? "",
|
|
966
|
+
smrtSourceKind: input.sourceKind,
|
|
967
|
+
smrtSourceId: input.sourceId,
|
|
968
|
+
smrtSourceVersion: String(input.sourceVersion),
|
|
969
|
+
smrtSourceSha256: execution.sourceSha256
|
|
970
|
+
},
|
|
971
|
+
...input.signal ? { signal: input.signal } : {}
|
|
972
|
+
});
|
|
973
|
+
providerResponded = true;
|
|
974
|
+
this.assertProviderRequest(request, input.tenantId);
|
|
975
|
+
await this.assertProviderRequestAvailable(execution, request.id);
|
|
976
|
+
const completed = await this.deps.executions.completeCreateAttempt({
|
|
977
|
+
tenantId: input.tenantId,
|
|
978
|
+
executionId: execution.id ?? "",
|
|
979
|
+
operationId,
|
|
980
|
+
provider: execution.provider,
|
|
981
|
+
providerRequestId: request.id,
|
|
982
|
+
status: request.status,
|
|
983
|
+
expiresAt: request.expiresAt ?? null
|
|
984
|
+
});
|
|
985
|
+
if (completed) execution = completed;
|
|
986
|
+
else {
|
|
987
|
+
const latest = await this.requireExecution(execution.id ?? "");
|
|
988
|
+
if (latest.providerRequestId !== request.id) throw new Error(`AgreementExecution ${execution.id}: provider-create lease ownership changed before the provider result was persisted; reconcile the current attempt`);
|
|
989
|
+
execution = latest;
|
|
990
|
+
}
|
|
991
|
+
} catch (error) {
|
|
992
|
+
const mayHaveSucceeded = providerResponded || requestMayHaveSucceeded(error);
|
|
993
|
+
const lastError = summarizeError(error, mayHaveSucceeded);
|
|
994
|
+
execution = await this.failOwnedCreateAttempt(execution, operationId, lastError);
|
|
995
|
+
await this.recordOperationAudit(execution, operationId, mayHaveSucceeded ? "create.uncertain" : "create.failed", "provider_operation", { error: lastError });
|
|
996
|
+
throw error;
|
|
997
|
+
}
|
|
998
|
+
await this.recordOperationAudit(execution, operationId, "create.succeeded", "provider_operation", { providerRequestId: request.id });
|
|
999
|
+
return this.result(execution, execution.attemptCount > 1);
|
|
1000
|
+
}
|
|
1001
|
+
async resumeOrReplayCreate(execution, input, sourceBytes) {
|
|
1002
|
+
if (execution.providerRequestId) return this.result(execution, true);
|
|
1003
|
+
const now = this.now();
|
|
1004
|
+
if (this.isCreateLeaseActive(execution, now)) return this.result(execution, true);
|
|
1005
|
+
const abandonedCreate = this.isAbandonedCreate(execution);
|
|
1006
|
+
if (abandonedCreate && !this.deps.provider.capabilities.providerEnforcedIdempotency) throw new Error(`AgreementExecution ${execution.id}: the provider-create lease expired without a confirmed request id; reconcile or adopt the provider request before retrying`);
|
|
1007
|
+
if (!abandonedCreate && !this.canSafelyRetryCreate(execution)) throw new Error(`AgreementExecution ${execution.id}: the prior provider create has no confirmed request id; reconcile or adopt the provider request before retrying`);
|
|
1008
|
+
await this.ensureAssetAssociation("AgreementExecution", execution.id ?? "", execution.sourceAssetId, "source_document");
|
|
1009
|
+
const operationId = randomUUID();
|
|
1010
|
+
const claimed = await this.deps.executions.claimCreateAttempt({
|
|
1011
|
+
tenantId: input.tenantId,
|
|
1012
|
+
executionId: execution.id ?? "",
|
|
1013
|
+
expectedAttemptCount: execution.attemptCount,
|
|
1014
|
+
expectedLeaseId: execution.createLeaseId,
|
|
1015
|
+
expectedStatus: execution.status,
|
|
1016
|
+
expectedLastError: execution.lastError,
|
|
1017
|
+
operationId,
|
|
1018
|
+
leaseExpiresAt: this.newCreateLeaseExpiry(now),
|
|
1019
|
+
now
|
|
1020
|
+
});
|
|
1021
|
+
if (!claimed) {
|
|
1022
|
+
const latest = await this.requireExecution(execution.id ?? "");
|
|
1023
|
+
if (latest.providerRequestId || this.isCreateLeaseActive(latest, this.now())) return this.result(latest, true);
|
|
1024
|
+
throw new Error(`AgreementExecution ${execution.id}: provider-create state changed while claiming recovery; retry the operation`);
|
|
1025
|
+
}
|
|
1026
|
+
return await this.sendProviderRequest(claimed, input, sourceBytes, operationId);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Bind an operator-reconciled provider request after an uncertain create.
|
|
1030
|
+
* This is the safe recovery path for providers without atomic idempotency.
|
|
1031
|
+
*/
|
|
1032
|
+
async adoptProviderRequest(input) {
|
|
1033
|
+
this.assertTenant(input.tenantId);
|
|
1034
|
+
let execution = await this.requireExecution(input.executionId);
|
|
1035
|
+
if (execution.providerRequestId && execution.providerRequestId !== input.providerRequestId) throw new Error(`AgreementExecution ${execution.id} is already bound to a different provider request`);
|
|
1036
|
+
const started = await this.beginAuditedOperation(execution, "adopt", "operator");
|
|
1037
|
+
execution = started.execution;
|
|
1038
|
+
const { operationId } = started;
|
|
1039
|
+
try {
|
|
1040
|
+
const request = await this.deps.provider.getRequest({
|
|
1041
|
+
tenantId: input.tenantId,
|
|
1042
|
+
requestId: input.providerRequestId,
|
|
1043
|
+
...input.signal ? { signal: input.signal } : {}
|
|
1044
|
+
});
|
|
1045
|
+
this.assertProviderRequest(request, input.tenantId, input.providerRequestId);
|
|
1046
|
+
await this.assertProviderRequestAvailable(execution, request.id);
|
|
1047
|
+
const linkedExecutionId = request.metadata?.smrtExecutionId;
|
|
1048
|
+
if (linkedExecutionId && linkedExecutionId !== execution.id) throw new Error(`Provider request '${request.id}' belongs to a different agreement execution`);
|
|
1049
|
+
execution = await this.bindProviderRequest(execution, request.id);
|
|
1050
|
+
const observedAt = this.now();
|
|
1051
|
+
execution = await this.applyProviderState(execution, request, observedAt, { lastReconciledAt: observedAt });
|
|
1052
|
+
await this.completeAuditedOperation(execution, operationId, "adopt", { providerRequestId: request.id });
|
|
1053
|
+
return this.result(execution, false);
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
await this.failAuditedOperation(execution, operationId, "adopt", error, false);
|
|
1056
|
+
throw error;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
async reconcile(input) {
|
|
1060
|
+
this.assertTenant(input.tenantId);
|
|
1061
|
+
let execution = await this.requireExecution(input.executionId);
|
|
1062
|
+
this.requireProviderRequestId(execution);
|
|
1063
|
+
const started = await this.beginAuditedOperation(execution, "reconcile", "system");
|
|
1064
|
+
execution = started.execution;
|
|
1065
|
+
const { operationId } = started;
|
|
1066
|
+
try {
|
|
1067
|
+
const request = await this.deps.provider.getRequest({
|
|
1068
|
+
tenantId: input.tenantId,
|
|
1069
|
+
requestId: execution.providerRequestId,
|
|
1070
|
+
...input.signal ? { signal: input.signal } : {}
|
|
1071
|
+
});
|
|
1072
|
+
this.assertProviderRequest(request, input.tenantId, execution.providerRequestId);
|
|
1073
|
+
const observedAt = this.now();
|
|
1074
|
+
execution = await this.applyProviderState(execution, request, observedAt, { lastReconciledAt: observedAt });
|
|
1075
|
+
if (execution.status === "completed") await this.finalizeExecution(execution);
|
|
1076
|
+
await this.completeAuditedOperation(execution, operationId, "reconcile", { providerStatus: request.status });
|
|
1077
|
+
return this.result(execution, false);
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
await this.failAuditedOperation(execution, operationId, "reconcile", error, false);
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async cancel(input) {
|
|
1084
|
+
this.assertTenant(input.tenantId);
|
|
1085
|
+
if (!input.reason.trim()) throw new Error("Cancellation reason is required");
|
|
1086
|
+
let execution = await this.requireExecution(input.executionId);
|
|
1087
|
+
this.requireProviderRequestId(execution);
|
|
1088
|
+
if (execution.status === "cancelled") return this.result(execution, true);
|
|
1089
|
+
this.assertNonTerminalOperation(execution, "cancel");
|
|
1090
|
+
const started = await this.beginAuditedOperation(execution, "cancel", "operator", { reason: input.reason });
|
|
1091
|
+
execution = started.execution;
|
|
1092
|
+
const { operationId } = started;
|
|
1093
|
+
let providerResponded = false;
|
|
1094
|
+
try {
|
|
1095
|
+
this.assertNonTerminalOperation(execution, "cancel");
|
|
1096
|
+
const request = await this.deps.provider.cancelRequest({
|
|
1097
|
+
tenantId: input.tenantId,
|
|
1098
|
+
requestId: execution.providerRequestId,
|
|
1099
|
+
reason: input.reason,
|
|
1100
|
+
...input.signal ? { signal: input.signal } : {}
|
|
1101
|
+
});
|
|
1102
|
+
providerResponded = true;
|
|
1103
|
+
this.assertProviderRequest(request, input.tenantId, execution.providerRequestId);
|
|
1104
|
+
execution = await this.applyProviderState(execution, request, this.now(), { cancellationReason: input.reason });
|
|
1105
|
+
await this.completeAuditedOperation(execution, operationId, "cancel", { providerStatus: request.status });
|
|
1106
|
+
return this.result(execution, false);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
await this.failAuditedOperation(execution, operationId, "cancel", error, true, providerResponded);
|
|
1109
|
+
throw error;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
async extendExpiry(input) {
|
|
1113
|
+
this.assertTenant(input.tenantId);
|
|
1114
|
+
let execution = await this.requireExecution(input.executionId);
|
|
1115
|
+
this.requireProviderRequestId(execution);
|
|
1116
|
+
this.assertNonTerminalOperation(execution, "extend expiry for");
|
|
1117
|
+
const started = await this.beginAuditedOperation(execution, "extend_expiry", "operator", { expiresAt: normalizeRequiredAgreementDate(input.expiresAt).toISOString() });
|
|
1118
|
+
execution = started.execution;
|
|
1119
|
+
const { operationId } = started;
|
|
1120
|
+
let providerResponded = false;
|
|
1121
|
+
try {
|
|
1122
|
+
this.assertNonTerminalOperation(execution, "extend expiry for");
|
|
1123
|
+
const request = await this.deps.provider.extendExpiry({
|
|
1124
|
+
tenantId: input.tenantId,
|
|
1125
|
+
requestId: execution.providerRequestId,
|
|
1126
|
+
expiresAt: input.expiresAt,
|
|
1127
|
+
...input.warnPrior !== void 0 ? { warnPrior: input.warnPrior } : {},
|
|
1128
|
+
...input.signal ? { signal: input.signal } : {}
|
|
1129
|
+
});
|
|
1130
|
+
providerResponded = true;
|
|
1131
|
+
this.assertProviderRequest(request, input.tenantId, execution.providerRequestId);
|
|
1132
|
+
execution = await this.applyProviderState(execution, request, this.now());
|
|
1133
|
+
await this.completeAuditedOperation(execution, operationId, "extend_expiry", { expiresAt: request.expiresAt?.toISOString() ?? null });
|
|
1134
|
+
return this.result(execution, false);
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
await this.failAuditedOperation(execution, operationId, "extend_expiry", error, true, providerResponded);
|
|
1137
|
+
throw error;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
async ingestWebhook(input) {
|
|
1141
|
+
this.assertTenant(input.tenantId);
|
|
1142
|
+
const providerEvent = this.deps.provider.parseWebhook({
|
|
1143
|
+
payload: input.payload,
|
|
1144
|
+
signature: input.signature
|
|
1145
|
+
});
|
|
1146
|
+
this.assertTenantValue(providerEvent.tenantId, input.tenantId, "verified webhook");
|
|
1147
|
+
if (providerEvent.provider !== this.deps.provider.capabilities.id) throw new Error("Verified webhook provider does not match the adapter");
|
|
1148
|
+
let execution = await this.deps.executions.findByProviderRequest(providerEvent.provider, providerEvent.requestId);
|
|
1149
|
+
if (!execution) throw new Error(`No AgreementExecution is bound to provider request '${providerEvent.requestId}'`);
|
|
1150
|
+
const dedupeKey = `${input.tenantId}:${providerEvent.provider}:${providerEvent.replay.deduplicationKey}`;
|
|
1151
|
+
const recorded = await this.deps.events.recordVerified({
|
|
1152
|
+
tenantId: input.tenantId,
|
|
1153
|
+
executionId: execution.id ?? "",
|
|
1154
|
+
provider: providerEvent.provider,
|
|
1155
|
+
providerEventId: providerEvent.id,
|
|
1156
|
+
eventOrigin: "provider_webhook",
|
|
1157
|
+
dedupeKey,
|
|
1158
|
+
orderingKey: providerEvent.replay.orderingKey,
|
|
1159
|
+
eventType: providerEvent.type,
|
|
1160
|
+
status: providerEvent.status,
|
|
1161
|
+
occurredAt: providerEvent.createdAt,
|
|
1162
|
+
receivedAt: this.now(),
|
|
1163
|
+
payloadSha256: sha256(Buffer.from(input.payload, "utf8")),
|
|
1164
|
+
signerEvidence: JSON.stringify(sanitizeSignerEvidence(providerEvent.signers)),
|
|
1165
|
+
payload: input.payload
|
|
1166
|
+
});
|
|
1167
|
+
execution = await this.persistLifecycleState(execution, {
|
|
1168
|
+
status: providerEvent.status,
|
|
1169
|
+
observedAt: providerEvent.createdAt,
|
|
1170
|
+
completedAt: providerEvent.status === "completed" ? providerEvent.createdAt : void 0
|
|
1171
|
+
});
|
|
1172
|
+
let executedAgreement = null;
|
|
1173
|
+
if (execution.status === "completed") {
|
|
1174
|
+
const started = await this.beginAuditedOperation(execution, "finalize", "system", { providerEventId: providerEvent.id });
|
|
1175
|
+
execution = started.execution;
|
|
1176
|
+
const { operationId } = started;
|
|
1177
|
+
try {
|
|
1178
|
+
executedAgreement = await this.finalizeExecution(execution, providerEvent);
|
|
1179
|
+
await this.completeAuditedOperation(execution, operationId, "finalize", { executedAgreementId: executedAgreement.id ?? "" });
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
await this.failAuditedOperation(execution, operationId, "finalize", error, false);
|
|
1182
|
+
throw error;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
executionId: execution.id ?? "",
|
|
1187
|
+
eventId: recorded.event.id ?? "",
|
|
1188
|
+
replayed: !recorded.created,
|
|
1189
|
+
...executedAgreement?.id ? { executedAgreementId: executedAgreement.id } : {}
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
async finalizeExecution(execution, verifiedEvent) {
|
|
1193
|
+
this.assertTenant(execution.tenantId);
|
|
1194
|
+
const existing = await this.deps.executedAgreements.findByExecution(execution.id ?? "");
|
|
1195
|
+
if (existing) {
|
|
1196
|
+
await this.ensureExecutedAgreementAssociations(existing);
|
|
1197
|
+
return existing;
|
|
1198
|
+
}
|
|
1199
|
+
this.requireProviderRequestId(execution);
|
|
1200
|
+
const request = await this.deps.provider.getRequest({
|
|
1201
|
+
tenantId: execution.tenantId,
|
|
1202
|
+
requestId: execution.providerRequestId
|
|
1203
|
+
});
|
|
1204
|
+
this.assertProviderRequest(request, execution.tenantId, execution.providerRequestId);
|
|
1205
|
+
if (request.status !== "completed") throw new Error(`Provider request '${request.id}' is '${request.status}', not completed`);
|
|
1206
|
+
execution = (await this.ensureEvidenceArtifact(execution, "signed_document")).execution;
|
|
1207
|
+
const auditResult = await this.ensureEvidenceArtifact(execution, "audit_trail");
|
|
1208
|
+
execution = await this.requireExecution(auditResult.execution.id ?? "");
|
|
1209
|
+
const signed = this.requireStoredArtifactMetadata(execution, "signed_document");
|
|
1210
|
+
const audit = this.requireStoredArtifactMetadata(execution, "audit_trail");
|
|
1211
|
+
const signerEvidence = sanitizeSignerEvidence(request.signers);
|
|
1212
|
+
const acceptedAt = execution.completedAt ?? verifiedEvent?.createdAt ?? this.now();
|
|
1213
|
+
const created = await this.deps.executedAgreements.createImmutable({
|
|
1214
|
+
tenantId: execution.tenantId,
|
|
1215
|
+
executionId: execution.id ?? "",
|
|
1216
|
+
sourceKind: execution.sourceKind,
|
|
1217
|
+
sourceId: execution.sourceId,
|
|
1218
|
+
sourceVersion: execution.sourceVersion,
|
|
1219
|
+
sourceAssetId: execution.sourceAssetId,
|
|
1220
|
+
sourceSha256: execution.sourceSha256,
|
|
1221
|
+
sourceSizeBytes: execution.sourceSizeBytes,
|
|
1222
|
+
signedDocumentAssetId: signed.assetId,
|
|
1223
|
+
signedDocumentSha256: signed.sha256,
|
|
1224
|
+
signedDocumentSizeBytes: signed.sizeBytes,
|
|
1225
|
+
signedDocumentMediaType: signed.mediaType,
|
|
1226
|
+
signedDocumentFilename: signed.filename,
|
|
1227
|
+
auditTrailAssetId: audit.assetId,
|
|
1228
|
+
auditTrailSha256: audit.sha256,
|
|
1229
|
+
auditTrailSizeBytes: audit.sizeBytes,
|
|
1230
|
+
auditTrailMediaType: audit.mediaType,
|
|
1231
|
+
auditTrailFilename: audit.filename,
|
|
1232
|
+
signerEvidence: JSON.stringify(signerEvidence),
|
|
1233
|
+
acceptedAt,
|
|
1234
|
+
effectiveFrom: execution.effectiveFrom ?? acceptedAt,
|
|
1235
|
+
effectiveTo: execution.effectiveTo,
|
|
1236
|
+
supersedesExecutedAgreementId: execution.supersedesExecutedAgreementId,
|
|
1237
|
+
metadata: execution.metadata
|
|
1238
|
+
});
|
|
1239
|
+
await this.ensureExecutedAgreementAssociations(created.agreement);
|
|
1240
|
+
return created.agreement;
|
|
1241
|
+
}
|
|
1242
|
+
async ensureEvidenceArtifact(execution, kind) {
|
|
1243
|
+
const existing = this.getStoredArtifactMetadata(execution, kind);
|
|
1244
|
+
if (existing) {
|
|
1245
|
+
await this.ensureAssetAssociation("AgreementExecution", execution.id ?? "", existing.assetId, kind);
|
|
1246
|
+
return {
|
|
1247
|
+
execution,
|
|
1248
|
+
metadata: existing
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
const stored = await this.retrieveAndStoreArtifact(execution, kind);
|
|
1252
|
+
let bound;
|
|
1253
|
+
try {
|
|
1254
|
+
bound = await this.deps.executions.bindEvidenceArtifact({
|
|
1255
|
+
tenantId: execution.tenantId,
|
|
1256
|
+
executionId: execution.id ?? "",
|
|
1257
|
+
kind,
|
|
1258
|
+
...stored.metadata
|
|
1259
|
+
});
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
const latest = await this.requireExecution(execution.id ?? "");
|
|
1262
|
+
if (this.getStoredArtifactMetadata(latest, kind)?.assetId !== stored.asset.id) await this.removeStoredAsset(stored.asset);
|
|
1263
|
+
throw error;
|
|
1264
|
+
}
|
|
1265
|
+
if (!bound) {
|
|
1266
|
+
await this.removeStoredAsset(stored.asset);
|
|
1267
|
+
bound = await this.requireExecution(execution.id ?? "");
|
|
1268
|
+
}
|
|
1269
|
+
const metadata = this.requireStoredArtifactMetadata(bound, kind);
|
|
1270
|
+
await this.ensureAssetAssociation("AgreementExecution", bound.id ?? "", metadata.assetId, kind);
|
|
1271
|
+
return {
|
|
1272
|
+
execution: bound,
|
|
1273
|
+
metadata
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
async retrieveAndStoreArtifact(execution, kind) {
|
|
1277
|
+
const artifact = await this.deps.provider.downloadArtifact({
|
|
1278
|
+
tenantId: execution.tenantId,
|
|
1279
|
+
requestId: execution.providerRequestId,
|
|
1280
|
+
kind
|
|
1281
|
+
});
|
|
1282
|
+
this.assertArtifact(artifact, execution, kind);
|
|
1283
|
+
const bytes = await readReadableStream(artifact.stream);
|
|
1284
|
+
if (bytes.byteLength === 0) throw new Error(`Downloaded ${kind} artifact was empty`);
|
|
1285
|
+
const computed = sha256(bytes);
|
|
1286
|
+
if (computed !== (await artifact.sha256).toLowerCase()) throw new Error(`Downloaded ${kind} hash did not match the SDK evidence hash`);
|
|
1287
|
+
const asset = await this.deps.assets.storeSourceAsset(artifact.filename, bytes, {
|
|
1288
|
+
mimeType: artifact.mediaType,
|
|
1289
|
+
typeSlug: "agreement-evidence",
|
|
1290
|
+
sourceType: execution.provider,
|
|
1291
|
+
externalId: `${execution.providerRequestId}:${kind}`,
|
|
1292
|
+
metadata: {
|
|
1293
|
+
executionId: execution.id,
|
|
1294
|
+
provider: execution.provider,
|
|
1295
|
+
providerRequestId: execution.providerRequestId,
|
|
1296
|
+
artifactKind: kind,
|
|
1297
|
+
sha256: computed,
|
|
1298
|
+
retrievedAt: artifact.retrievedAt.toISOString()
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
this.assertTenantValue(asset.tenantId, execution.tenantId, `${kind} asset`);
|
|
1302
|
+
if (!asset.id) throw new Error(`Stored ${kind} Asset has no id`);
|
|
1303
|
+
return {
|
|
1304
|
+
asset,
|
|
1305
|
+
metadata: {
|
|
1306
|
+
assetId: asset.id,
|
|
1307
|
+
sha256: computed,
|
|
1308
|
+
sizeBytes: bytes.byteLength,
|
|
1309
|
+
filename: artifact.filename,
|
|
1310
|
+
mediaType: artifact.mediaType
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
getStoredArtifactMetadata(execution, kind) {
|
|
1315
|
+
const metadata = kind === "signed_document" ? {
|
|
1316
|
+
assetId: execution.signedDocumentAssetId,
|
|
1317
|
+
sha256: execution.signedDocumentSha256,
|
|
1318
|
+
sizeBytes: execution.signedDocumentSizeBytes,
|
|
1319
|
+
mediaType: execution.signedDocumentMediaType,
|
|
1320
|
+
filename: execution.signedDocumentFilename
|
|
1321
|
+
} : {
|
|
1322
|
+
assetId: execution.auditTrailAssetId,
|
|
1323
|
+
sha256: execution.auditTrailSha256,
|
|
1324
|
+
sizeBytes: execution.auditTrailSizeBytes,
|
|
1325
|
+
mediaType: execution.auditTrailMediaType,
|
|
1326
|
+
filename: execution.auditTrailFilename
|
|
1327
|
+
};
|
|
1328
|
+
if (!metadata.assetId) return null;
|
|
1329
|
+
if (!metadata.sha256 || !Number.isSafeInteger(metadata.sizeBytes) || metadata.sizeBytes < 1 || !metadata.mediaType || !metadata.filename) throw new Error(`AgreementExecution ${execution.id}: ${kind} evidence is incomplete`);
|
|
1330
|
+
return metadata;
|
|
1331
|
+
}
|
|
1332
|
+
requireStoredArtifactMetadata(execution, kind) {
|
|
1333
|
+
const metadata = this.getStoredArtifactMetadata(execution, kind);
|
|
1334
|
+
if (!metadata) throw new Error(`AgreementExecution ${execution.id}: ${kind} evidence was not bound`);
|
|
1335
|
+
return metadata;
|
|
1336
|
+
}
|
|
1337
|
+
async removeStoredAsset(asset) {
|
|
1338
|
+
await this.deps.assets.store.remove(asset);
|
|
1339
|
+
}
|
|
1340
|
+
async ensureExecutedAgreementAssociations(agreement) {
|
|
1341
|
+
if (!agreement.id) throw new Error("ExecutedAgreement has no id");
|
|
1342
|
+
await this.ensureAssetAssociation("ExecutedAgreement", agreement.id, agreement.sourceAssetId, "source_document");
|
|
1343
|
+
await this.ensureAssetAssociation("ExecutedAgreement", agreement.id, agreement.signedDocumentAssetId, "signed_document");
|
|
1344
|
+
await this.ensureAssetAssociation("ExecutedAgreement", agreement.id, agreement.auditTrailAssetId, "audit_trail");
|
|
1345
|
+
}
|
|
1346
|
+
async applyProviderState(execution, request, observedAt, options = {}) {
|
|
1347
|
+
return await this.persistLifecycleState(execution, {
|
|
1348
|
+
status: request.status,
|
|
1349
|
+
observedAt,
|
|
1350
|
+
enforceEventOrder: false,
|
|
1351
|
+
...request.expiresAt ? { expiresAt: request.expiresAt } : {},
|
|
1352
|
+
...options.cancellationReason !== void 0 ? { cancellationReason: options.cancellationReason } : {},
|
|
1353
|
+
...options.lastReconciledAt ? { lastReconciledAt: options.lastReconciledAt } : {},
|
|
1354
|
+
...request.status === "completed" ? { completedAt: observedAt } : {}
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
async persistLifecycleState(execution, input) {
|
|
1358
|
+
const executionId = execution.id ?? "";
|
|
1359
|
+
let current = await this.requireExecution(executionId);
|
|
1360
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
1361
|
+
if (input.enforceEventOrder !== false && current.lastProviderEventAt && input.observedAt < current.lastProviderEventAt) return current;
|
|
1362
|
+
if (!canAdvanceStatus(current.status, input.status)) return current;
|
|
1363
|
+
const persistedObservedAt = input.enforceEventOrder === false && current.lastProviderEventAt && current.lastProviderEventAt > input.observedAt ? current.lastProviderEventAt : input.observedAt;
|
|
1364
|
+
const updated = await this.deps.executions.compareAndSetLifecycle({
|
|
1365
|
+
tenantId: current.tenantId,
|
|
1366
|
+
executionId,
|
|
1367
|
+
expectedStatus: current.status,
|
|
1368
|
+
status: input.status,
|
|
1369
|
+
expiresAt: input.expiresAt ?? current.expiresAt,
|
|
1370
|
+
cancellationReason: input.cancellationReason ?? current.cancellationReason,
|
|
1371
|
+
lastProviderEventAt: persistedObservedAt,
|
|
1372
|
+
lastReconciledAt: input.lastReconciledAt ?? current.lastReconciledAt,
|
|
1373
|
+
completedAt: input.status === "completed" ? current.completedAt ?? input.completedAt ?? input.observedAt : current.completedAt
|
|
1374
|
+
});
|
|
1375
|
+
if (updated) return updated;
|
|
1376
|
+
current = await this.requireExecution(executionId);
|
|
1377
|
+
}
|
|
1378
|
+
throw new Error(`AgreementExecution ${executionId}: lifecycle state remained contended; retry the operation`);
|
|
1379
|
+
}
|
|
1380
|
+
assertProviderRequest(request, tenantId, expectedRequestId) {
|
|
1381
|
+
this.assertTenantValue(request.tenantId, tenantId, "provider request");
|
|
1382
|
+
if (request.provider !== this.deps.provider.capabilities.id) throw new Error("Provider request does not match the configured adapter");
|
|
1383
|
+
if (!request.id) throw new Error("Provider request has no id");
|
|
1384
|
+
if (expectedRequestId && request.id !== expectedRequestId) throw new Error(`Provider returned request '${request.id}' while '${expectedRequestId}' was requested`);
|
|
1385
|
+
}
|
|
1386
|
+
assertArtifact(artifact, execution, kind) {
|
|
1387
|
+
this.assertTenantValue(artifact.tenantId, execution.tenantId, kind);
|
|
1388
|
+
if (artifact.provider !== execution.provider || artifact.requestId !== execution.providerRequestId || artifact.kind !== kind) throw new Error(`Downloaded ${kind} does not match its execution`);
|
|
1389
|
+
}
|
|
1390
|
+
assertProviderCapabilities() {
|
|
1391
|
+
const caps = this.deps.provider.capabilities;
|
|
1392
|
+
if (!caps.supportsWebhooks || !caps.supportsCancellation || !caps.supportsExpiryExtension || !caps.supportsSignedDocument || !caps.supportsAuditTrail) throw new Error(`Signature provider '${caps.id}' lacks required agreement-execution capabilities`);
|
|
1393
|
+
}
|
|
1394
|
+
assertSameIntent(existing, input, requestIntentSha256) {
|
|
1395
|
+
if (existing.provider !== this.deps.provider.capabilities.id || existing.sourceKind !== input.sourceKind || existing.sourceId !== input.sourceId || existing.sourceVersion !== input.sourceVersion || existing.title !== input.title || existing.requestIntentSha256 !== requestIntentSha256) throw new Error(`Idempotency key '${input.idempotencyKey}' belongs to a different agreement execution`);
|
|
1396
|
+
}
|
|
1397
|
+
assertCreateInput(input) {
|
|
1398
|
+
for (const [label, value] of [
|
|
1399
|
+
["idempotencyKey", input.idempotencyKey],
|
|
1400
|
+
["sourceKind", input.sourceKind],
|
|
1401
|
+
["sourceId", input.sourceId],
|
|
1402
|
+
["title", input.title],
|
|
1403
|
+
["document.name", input.document.name],
|
|
1404
|
+
["document.mediaType", input.document.mediaType]
|
|
1405
|
+
]) if (!value.trim()) throw new Error(`Agreement execution ${label} is required`);
|
|
1406
|
+
if (!Number.isSafeInteger(input.sourceVersion) || input.sourceVersion < 1) throw new Error("Agreement execution sourceVersion must be a positive integer");
|
|
1407
|
+
if (input.expiresInDays !== void 0 && (!Number.isSafeInteger(input.expiresInDays) || input.expiresInDays < 1)) throw new Error("Agreement execution expiresInDays must be a positive integer");
|
|
1408
|
+
if (input.credentialRef && !/^[a-z][a-z0-9+.-]*:\/\//i.test(input.credentialRef)) throw new Error("Agreement execution credentialRef must be a secret-store reference URI");
|
|
1409
|
+
if (input.signers.length === 0) throw new Error("Agreement execution requires at least one signer");
|
|
1410
|
+
}
|
|
1411
|
+
async requireExecution(id) {
|
|
1412
|
+
const execution = await this.deps.executions.get({ id });
|
|
1413
|
+
if (!execution) throw new Error(`AgreementExecution '${id}' was not found`);
|
|
1414
|
+
return execution;
|
|
1415
|
+
}
|
|
1416
|
+
requireProviderRequestId(execution) {
|
|
1417
|
+
if (!execution.providerRequestId) throw new Error(`AgreementExecution ${execution.id}: provider request is not confirmed; reconcile or adopt it first`);
|
|
1418
|
+
}
|
|
1419
|
+
async assertProviderRequestAvailable(execution, providerRequestId) {
|
|
1420
|
+
const bound = await this.deps.executions.findByProviderRequest(execution.provider, providerRequestId);
|
|
1421
|
+
if (bound && bound.id !== execution.id) throw new Error(`Provider request '${providerRequestId}' is already bound to another AgreementExecution`);
|
|
1422
|
+
}
|
|
1423
|
+
async bindProviderRequest(execution, providerRequestId) {
|
|
1424
|
+
if (execution.providerRequestId === providerRequestId) return execution;
|
|
1425
|
+
const bound = await this.deps.executions.bindProviderRequest({
|
|
1426
|
+
tenantId: execution.tenantId,
|
|
1427
|
+
executionId: execution.id ?? "",
|
|
1428
|
+
provider: execution.provider,
|
|
1429
|
+
providerRequestId
|
|
1430
|
+
});
|
|
1431
|
+
if (!bound) {
|
|
1432
|
+
const current = await this.requireExecution(execution.id ?? "");
|
|
1433
|
+
if (current.providerRequestId === providerRequestId) return current;
|
|
1434
|
+
throw new Error(`AgreementExecution ${execution.id}: provider request binding changed concurrently`);
|
|
1435
|
+
}
|
|
1436
|
+
return bound;
|
|
1437
|
+
}
|
|
1438
|
+
canSafelyRetryCreate(execution) {
|
|
1439
|
+
if (execution.attemptCount === 0 && execution.status === "prepared") return true;
|
|
1440
|
+
try {
|
|
1441
|
+
return JSON.parse(execution.lastError).requestMayHaveSucceeded === false;
|
|
1442
|
+
} catch {
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
isAbandonedCreate(execution) {
|
|
1447
|
+
return execution.status === "prepared" && execution.attemptCount > 0 && !execution.providerRequestId && !execution.lastError;
|
|
1448
|
+
}
|
|
1449
|
+
isCreateLeaseActive(execution, at) {
|
|
1450
|
+
return Boolean(this.isAbandonedCreate(execution) && execution.createLeaseId && execution.createLeaseExpiresAt && execution.createLeaseExpiresAt.getTime() > at.getTime());
|
|
1451
|
+
}
|
|
1452
|
+
newCreateLeaseExpiry(from = this.now()) {
|
|
1453
|
+
return new Date(from.getTime() + this.createLeaseDurationMs);
|
|
1454
|
+
}
|
|
1455
|
+
async failOwnedCreateAttempt(execution, operationId, lastError) {
|
|
1456
|
+
return await this.deps.executions.failCreateAttempt({
|
|
1457
|
+
tenantId: execution.tenantId,
|
|
1458
|
+
executionId: execution.id ?? "",
|
|
1459
|
+
operationId,
|
|
1460
|
+
lastError
|
|
1461
|
+
}) ?? await this.requireExecution(execution.id ?? "");
|
|
1462
|
+
}
|
|
1463
|
+
async ensureAssetAssociation(ownerType, ownerId, assetId, role) {
|
|
1464
|
+
if (!ownerId || !assetId) throw new Error(`Cannot link ${role}: owner and Asset ids are required`);
|
|
1465
|
+
await this.deps.assets.associations.attach(`@happyvertical/smrt-sales:${ownerType}`, ownerId, assetId, { role });
|
|
1466
|
+
}
|
|
1467
|
+
async beginAuditedOperation(execution, operation, origin, metadata = {}) {
|
|
1468
|
+
const current = await this.deps.executions.incrementAttemptCount(execution.tenantId, execution.id ?? "");
|
|
1469
|
+
const operationId = randomUUID();
|
|
1470
|
+
try {
|
|
1471
|
+
await this.recordOperationAudit(current, operationId, `${operation}.started`, origin, {
|
|
1472
|
+
attemptCount: current.attemptCount,
|
|
1473
|
+
...metadata
|
|
1474
|
+
});
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
await this.deps.executions.updateLastError({
|
|
1477
|
+
tenantId: current.tenantId,
|
|
1478
|
+
executionId: current.id ?? "",
|
|
1479
|
+
lastError: summarizeError(error, false)
|
|
1480
|
+
});
|
|
1481
|
+
throw error;
|
|
1482
|
+
}
|
|
1483
|
+
return {
|
|
1484
|
+
execution: current,
|
|
1485
|
+
operationId
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
async completeAuditedOperation(execution, operationId, operation, metadata) {
|
|
1489
|
+
await this.recordOperationAudit(execution, operationId, `${operation}.succeeded`, "provider_operation", metadata);
|
|
1490
|
+
}
|
|
1491
|
+
async failAuditedOperation(execution, operationId, operation, error, mayMutateProvider, providerMutationConfirmed = false) {
|
|
1492
|
+
const current = await this.deps.executions.updateLastError({
|
|
1493
|
+
tenantId: execution.tenantId,
|
|
1494
|
+
executionId: execution.id ?? "",
|
|
1495
|
+
lastError: summarizeError(error)
|
|
1496
|
+
});
|
|
1497
|
+
const uncertain = mayMutateProvider && (providerMutationConfirmed || isPotentiallyUncertain(error));
|
|
1498
|
+
await this.recordOperationAudit(current, operationId, `${operation}.${uncertain ? "uncertain" : "failed"}`, "provider_operation", { error: current.lastError });
|
|
1499
|
+
}
|
|
1500
|
+
async recordOperationAudit(execution, operationId, eventType, eventOrigin, metadata) {
|
|
1501
|
+
const occurredAt = this.now();
|
|
1502
|
+
const payload = JSON.stringify(metadata);
|
|
1503
|
+
await this.deps.events.recordVerified({
|
|
1504
|
+
tenantId: execution.tenantId,
|
|
1505
|
+
executionId: execution.id ?? "",
|
|
1506
|
+
provider: execution.provider,
|
|
1507
|
+
providerEventId: "",
|
|
1508
|
+
eventOrigin,
|
|
1509
|
+
operationId,
|
|
1510
|
+
dedupeKey: `${execution.tenantId}:${execution.provider}:operation:${operationId}:${eventType}`,
|
|
1511
|
+
orderingKey: execution.providerRequestId || execution.id || operationId,
|
|
1512
|
+
eventType: `operation.${eventType}`,
|
|
1513
|
+
status: execution.status,
|
|
1514
|
+
occurredAt,
|
|
1515
|
+
receivedAt: occurredAt,
|
|
1516
|
+
payloadSha256: sha256(Buffer.from(payload, "utf8")),
|
|
1517
|
+
signerEvidence: "[]",
|
|
1518
|
+
payload
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
assertNonTerminalOperation(execution, operation) {
|
|
1522
|
+
if (TERMINAL_STATUSES.has(execution.status)) throw new Error(`Cannot ${operation} terminal AgreementExecution ${execution.id} in status '${execution.status}'`);
|
|
1523
|
+
}
|
|
1524
|
+
result(execution, replayed) {
|
|
1525
|
+
return {
|
|
1526
|
+
executionId: execution.id ?? "",
|
|
1527
|
+
...execution.providerRequestId ? { providerRequestId: execution.providerRequestId } : {},
|
|
1528
|
+
status: execution.status,
|
|
1529
|
+
replayed
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
assertTenant(expected) {
|
|
1533
|
+
this.assertTenantValue(requireTenantId(), expected, "tenant context");
|
|
1534
|
+
}
|
|
1535
|
+
assertTenantValue(actual, expected, context) {
|
|
1536
|
+
if (actual !== expected) throw new Error(`Agreement execution tenant mismatch for ${context}: expected '${expected}'`);
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
function sha256(data) {
|
|
1540
|
+
return createHash("sha256").update(data).digest("hex");
|
|
1541
|
+
}
|
|
1542
|
+
function hashRequestIntent(input, provider, sourceSha256, effectiveFrom, effectiveTo) {
|
|
1543
|
+
const sortedMetadata = Object.fromEntries(Object.entries(input.metadata ?? {}).sort(([left], [right]) => left.localeCompare(right)));
|
|
1544
|
+
return sha256(Buffer.from(JSON.stringify({
|
|
1545
|
+
provider,
|
|
1546
|
+
providerAccountRef: input.providerAccountRef ?? "",
|
|
1547
|
+
credentialRef: input.credentialRef ?? "",
|
|
1548
|
+
sourceKind: input.sourceKind,
|
|
1549
|
+
sourceId: input.sourceId,
|
|
1550
|
+
sourceVersion: input.sourceVersion,
|
|
1551
|
+
sourceSha256,
|
|
1552
|
+
documentName: input.document.name,
|
|
1553
|
+
documentMediaType: input.document.mediaType,
|
|
1554
|
+
title: input.title,
|
|
1555
|
+
messageSha256: input.message ? sha256(Buffer.from(input.message, "utf8")) : "",
|
|
1556
|
+
signers: input.signers.map((signer) => ({
|
|
1557
|
+
...sanitizeSignerIntent([signer])[0],
|
|
1558
|
+
privateMessageSha256: signer.privateMessage ? sha256(Buffer.from(signer.privateMessage, "utf8")) : "",
|
|
1559
|
+
phoneSha256: signer.authentication?.phone ? sha256(Buffer.from(`${signer.authentication.phone.countryCode}:${signer.authentication.phone.number}`, "utf8")) : "",
|
|
1560
|
+
identityVerification: signer.authentication?.identityVerification ?? null,
|
|
1561
|
+
fields: signer.fields.map((field) => ({
|
|
1562
|
+
id: field.id,
|
|
1563
|
+
type: field.type,
|
|
1564
|
+
page: field.page,
|
|
1565
|
+
bounds: field.bounds,
|
|
1566
|
+
required: field.required ?? true,
|
|
1567
|
+
valueSha256: field.value ? sha256(Buffer.from(field.value, "utf8")) : ""
|
|
1568
|
+
}))
|
|
1569
|
+
})),
|
|
1570
|
+
signingOrder: input.signingOrder ?? false,
|
|
1571
|
+
expiresInDays: input.expiresInDays ?? null,
|
|
1572
|
+
effectiveFrom: effectiveFrom?.toISOString() ?? null,
|
|
1573
|
+
effectiveTo: effectiveTo?.toISOString() ?? null,
|
|
1574
|
+
supersedesExecutedAgreementId: input.supersedesExecutedAgreementId ?? "",
|
|
1575
|
+
metadata: sortedMetadata
|
|
1576
|
+
}), "utf8"));
|
|
1577
|
+
}
|
|
1578
|
+
function normalizeOptionalAgreementDate(value, label) {
|
|
1579
|
+
if (value == null) return null;
|
|
1580
|
+
const date = coerceAgreementDate(value);
|
|
1581
|
+
if (!date) throw new Error(`Agreement execution ${label} is invalid`);
|
|
1582
|
+
return date;
|
|
1583
|
+
}
|
|
1584
|
+
function normalizeRequiredAgreementDate(value) {
|
|
1585
|
+
const date = coerceAgreementDate(value);
|
|
1586
|
+
if (!date) throw new Error("Agreement execution expiresAt is invalid");
|
|
1587
|
+
return date;
|
|
1588
|
+
}
|
|
1589
|
+
async function readByteSource(source) {
|
|
1590
|
+
if (source instanceof Uint8Array) return Buffer.from(source);
|
|
1591
|
+
if (isReadableStream(source)) return await readReadableStream(source);
|
|
1592
|
+
const chunks = [];
|
|
1593
|
+
for await (const chunk of source) chunks.push(chunk);
|
|
1594
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
|
1595
|
+
}
|
|
1596
|
+
async function readReadableStream(stream) {
|
|
1597
|
+
const reader = stream.getReader();
|
|
1598
|
+
const chunks = [];
|
|
1599
|
+
try {
|
|
1600
|
+
while (true) {
|
|
1601
|
+
const result = await reader.read();
|
|
1602
|
+
if (result.done) break;
|
|
1603
|
+
chunks.push(Buffer.from(result.value));
|
|
1604
|
+
}
|
|
1605
|
+
} finally {
|
|
1606
|
+
reader.releaseLock();
|
|
1607
|
+
}
|
|
1608
|
+
return Buffer.concat(chunks);
|
|
1609
|
+
}
|
|
1610
|
+
function isReadableStream(value) {
|
|
1611
|
+
return typeof value.getReader === "function";
|
|
1612
|
+
}
|
|
1613
|
+
function summarizeError(error, requestMayHaveSucceededOverride) {
|
|
1614
|
+
const candidate = error && typeof error === "object" ? error : {};
|
|
1615
|
+
return JSON.stringify({
|
|
1616
|
+
name: typeof candidate.name === "string" ? candidate.name : "Error",
|
|
1617
|
+
...typeof candidate.code === "string" ? { code: candidate.code } : {},
|
|
1618
|
+
...typeof candidate.status === "number" ? { status: candidate.status } : {},
|
|
1619
|
+
...typeof candidate.retryable === "boolean" ? { retryable: candidate.retryable } : {},
|
|
1620
|
+
...requestMayHaveSucceededOverride !== void 0 ? { requestMayHaveSucceeded: requestMayHaveSucceededOverride } : typeof candidate.requestMayHaveSucceeded === "boolean" ? { requestMayHaveSucceeded: candidate.requestMayHaveSucceeded } : {}
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
function requestMayHaveSucceeded(error) {
|
|
1624
|
+
return Boolean(error && typeof error === "object" && error.requestMayHaveSucceeded === true);
|
|
1625
|
+
}
|
|
1626
|
+
function isPotentiallyUncertain(error) {
|
|
1627
|
+
if (requestMayHaveSucceeded(error)) return true;
|
|
1628
|
+
return Boolean(error && typeof error === "object" && error.retryable === true);
|
|
1629
|
+
}
|
|
1630
|
+
function canAdvanceStatus(current, next) {
|
|
1631
|
+
if (current === next) return true;
|
|
1632
|
+
if (current === "failed") return true;
|
|
1633
|
+
if (TERMINAL_STATUSES.has(current)) return false;
|
|
1634
|
+
if (TERMINAL_STATUSES.has(next)) return true;
|
|
1635
|
+
return (STATUS_PROGRESS.get(next) ?? -1) >= (STATUS_PROGRESS.get(current) ?? -1);
|
|
1636
|
+
}
|
|
1637
|
+
//#endregion
|
|
1638
|
+
export { AgreementExecutionEvent as a, AGREEMENT_EXECUTION_STATUSES as c, sanitizeSignerIntent as d, AgreementExecutionEventCollection as i, coerceAgreementDate as l, ExecutedAgreementCollection as n, AgreementExecutionCollection as o, ExecutedAgreement as r, AgreementExecution as s, AgreementExecutionService as t, sanitizeSignerEvidence as u };
|
|
1639
|
+
|
|
1640
|
+
//# sourceMappingURL=agreements-DbzW1Pcq.js.map
|