@emilia-protocol/sdk 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/client.d.ts +35 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +160 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +263 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +167 -16
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +202 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
- package/src/client.ts +210 -2
- package/src/index.ts +440 -40
- package/src/types.ts +219 -0
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// -- Params -----------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
export interface EPClientOptions {
|
|
6
|
-
baseUrl
|
|
6
|
+
baseUrl?: string;
|
|
7
7
|
apiKey?: string;
|
|
8
8
|
timeout?: number;
|
|
9
9
|
retries?: number;
|
|
@@ -119,6 +119,155 @@ export interface CreateSuppressionParams {
|
|
|
119
119
|
expires_at: string;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// -- v1 Guard Params --------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
export type GuardActionType =
|
|
125
|
+
| 'benefit_bank_account_change'
|
|
126
|
+
| 'benefit_address_change'
|
|
127
|
+
| 'caseworker_override'
|
|
128
|
+
| 'vendor_bank_account_change'
|
|
129
|
+
| 'beneficiary_creation'
|
|
130
|
+
| 'large_payment_release'
|
|
131
|
+
| 'ai_agent_payment_action';
|
|
132
|
+
|
|
133
|
+
export type GuardDecision = 'allow' | 'observe' | 'allow_with_signoff' | 'deny';
|
|
134
|
+
export type GuardEnforcementMode = 'observe' | 'warn' | 'enforce';
|
|
135
|
+
|
|
136
|
+
export interface GuardQuorumPolicy {
|
|
137
|
+
mode?: 'threshold' | 'all';
|
|
138
|
+
required: number;
|
|
139
|
+
approvers: Array<{ role: string; approver: string }>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface CreateTrustReceiptParams {
|
|
143
|
+
organizationId?: string;
|
|
144
|
+
actionType: GuardActionType | string;
|
|
145
|
+
targetResourceId: string;
|
|
146
|
+
policyId?: string;
|
|
147
|
+
enforcementMode?: GuardEnforcementMode;
|
|
148
|
+
beforeState?: Record<string, unknown>;
|
|
149
|
+
afterState?: Record<string, unknown>;
|
|
150
|
+
targetChangedFields?: string[];
|
|
151
|
+
amount?: number;
|
|
152
|
+
currency?: string;
|
|
153
|
+
riskFlags?: string[];
|
|
154
|
+
actorRole?: string;
|
|
155
|
+
actorDepartment?: string;
|
|
156
|
+
businessHours?: boolean;
|
|
157
|
+
velocitySameActor24h?: number;
|
|
158
|
+
priorDenialsActor30d?: number;
|
|
159
|
+
priorChangesTarget30d?: number;
|
|
160
|
+
destinationAgeDays?: number;
|
|
161
|
+
quorumPolicy?: GuardQuorumPolicy;
|
|
162
|
+
metadata?: Record<string, unknown>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface TrustReceipt {
|
|
166
|
+
receipt_id: string;
|
|
167
|
+
decision: GuardDecision;
|
|
168
|
+
observed_decision?: GuardDecision | null;
|
|
169
|
+
policy_id: string;
|
|
170
|
+
policy_hash: string;
|
|
171
|
+
action_hash: string;
|
|
172
|
+
before_state_hash?: string | null;
|
|
173
|
+
after_state_hash?: string | null;
|
|
174
|
+
nonce: string;
|
|
175
|
+
expires_at: string;
|
|
176
|
+
signoff_required: boolean;
|
|
177
|
+
signoff_request_id?: string | null;
|
|
178
|
+
risk_flags?: string[];
|
|
179
|
+
receipt_status: string;
|
|
180
|
+
enforcement_mode: string;
|
|
181
|
+
reasons?: string[];
|
|
182
|
+
canonical_action: Record<string, unknown>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface TrustReceiptState {
|
|
186
|
+
receipt_id: string;
|
|
187
|
+
organization_id: string;
|
|
188
|
+
action_type: string;
|
|
189
|
+
decision: GuardDecision;
|
|
190
|
+
enforcement_mode: string;
|
|
191
|
+
policy_id: string;
|
|
192
|
+
policy_hash: string;
|
|
193
|
+
action_hash: string;
|
|
194
|
+
expires_at: string;
|
|
195
|
+
signoff_required: boolean;
|
|
196
|
+
receipt_status: string;
|
|
197
|
+
signoff_key_class?: string | null;
|
|
198
|
+
timeline_event_count: number;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface RequestSignoffParams {
|
|
202
|
+
receiptId: string;
|
|
203
|
+
approverId?: string;
|
|
204
|
+
expiresInMinutes?: number;
|
|
205
|
+
comment?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface SignoffRequest {
|
|
209
|
+
signoff_id?: string;
|
|
210
|
+
receipt_id: string;
|
|
211
|
+
action_hash: string;
|
|
212
|
+
initiator_id: string;
|
|
213
|
+
approver_id?: string;
|
|
214
|
+
expires_at: string;
|
|
215
|
+
status: string;
|
|
216
|
+
quorum?: { mode: string; required: number; count: number };
|
|
217
|
+
signoffs?: Array<{ signoff_id: string; role?: string; approver_id: string }>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface ConsumeTrustReceiptResult {
|
|
221
|
+
receipt_id: string;
|
|
222
|
+
status: string;
|
|
223
|
+
consumed_at: string;
|
|
224
|
+
consumed_by_system: string;
|
|
225
|
+
execution_reference_id?: string | null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface ExecutionAttestation {
|
|
229
|
+
receipt_id: string;
|
|
230
|
+
status: string;
|
|
231
|
+
binding_status: string;
|
|
232
|
+
executed_action_hash: string;
|
|
233
|
+
approved_action_hash: string;
|
|
234
|
+
execution_integrity: Record<string, unknown>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface TrustReceiptEvidence {
|
|
238
|
+
document: Record<string, unknown> | null;
|
|
239
|
+
public_key: string | null;
|
|
240
|
+
signed: boolean;
|
|
241
|
+
verify_with: string;
|
|
242
|
+
receipt_id: string;
|
|
243
|
+
organization_id: string;
|
|
244
|
+
issued_at: string;
|
|
245
|
+
expires_at: string;
|
|
246
|
+
schema_version: string;
|
|
247
|
+
[key: string]: unknown;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface RequireReceiptParams extends CreateTrustReceiptParams {
|
|
251
|
+
executingSystem: string;
|
|
252
|
+
executionReferenceId?: string;
|
|
253
|
+
approverId?: string;
|
|
254
|
+
signoffComment?: string;
|
|
255
|
+
signoffExpiresInMinutes?: number;
|
|
256
|
+
onSignoffRequired?: (ctx: { client: EPClient; receipt: TrustReceipt; signoff?: SignoffRequest }) => Promise<void | boolean | { approved?: boolean }>;
|
|
257
|
+
executedAction?: Record<string, unknown> | ((ctx: { receipt: TrustReceipt; result: unknown }) => Record<string, unknown>);
|
|
258
|
+
executionId?: string | ((result: unknown) => string | undefined);
|
|
259
|
+
fetchEvidence?: boolean;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface RequireReceiptResult<T> {
|
|
263
|
+
result: T;
|
|
264
|
+
receipt: TrustReceipt;
|
|
265
|
+
signoff?: SignoffRequest;
|
|
266
|
+
consume: ConsumeTrustReceiptResult;
|
|
267
|
+
execution: ExecutionAttestation;
|
|
268
|
+
evidence?: TrustReceiptEvidence;
|
|
269
|
+
}
|
|
270
|
+
|
|
122
271
|
// -- Eye Responses ----------------------------------------------------------
|
|
123
272
|
|
|
124
273
|
export interface ObservationResponse {
|
|
@@ -198,14 +347,21 @@ export interface SimulatePolicyParams {
|
|
|
198
347
|
|
|
199
348
|
export interface RolloutPolicyParams {
|
|
200
349
|
policyId: string;
|
|
201
|
-
|
|
202
|
-
|
|
350
|
+
/** Policy version number to roll out (resolved against handshake_policies). */
|
|
351
|
+
version: number;
|
|
352
|
+
/** Target environment, e.g. "production" or "staging". */
|
|
353
|
+
environment: string;
|
|
354
|
+
strategy?: 'immediate' | 'canary';
|
|
355
|
+
/** Traffic percentage (1–99) for canary rollouts. */
|
|
356
|
+
canaryPct?: number;
|
|
203
357
|
}
|
|
204
358
|
|
|
205
359
|
export interface DiffPolicyVersionsParams {
|
|
206
360
|
policyId: string;
|
|
207
|
-
|
|
208
|
-
|
|
361
|
+
/** First version number to compare. */
|
|
362
|
+
v1: number;
|
|
363
|
+
/** Second version number to compare. */
|
|
364
|
+
v2: number;
|
|
209
365
|
}
|
|
210
366
|
|
|
211
367
|
// -- Responses --------------------------------------------------------------
|
|
@@ -463,34 +619,72 @@ export interface PolicySimulationResult {
|
|
|
463
619
|
}
|
|
464
620
|
|
|
465
621
|
export interface PolicyRolloutResult {
|
|
466
|
-
|
|
622
|
+
rollout_id: string;
|
|
623
|
+
/** handshake_policies policy_id of the rolled-out version row. */
|
|
624
|
+
policy_id: string;
|
|
625
|
+
policy_key: string;
|
|
626
|
+
version: number;
|
|
627
|
+
environment: string;
|
|
467
628
|
strategy: string;
|
|
468
|
-
percentage: number;
|
|
469
629
|
status: string;
|
|
470
|
-
|
|
630
|
+
canary_pct: number | null;
|
|
631
|
+
initiated_at: string;
|
|
632
|
+
tenant_id: string;
|
|
471
633
|
}
|
|
472
634
|
|
|
635
|
+
/**
|
|
636
|
+
* A policy version is a handshake_policies row. Versions sharing a policy_key
|
|
637
|
+
* form the version history of a single policy.
|
|
638
|
+
*/
|
|
473
639
|
export interface PolicyVersion {
|
|
474
|
-
|
|
475
|
-
|
|
640
|
+
policy_id: string;
|
|
641
|
+
policy_key: string;
|
|
476
642
|
version: number;
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
643
|
+
name: string;
|
|
644
|
+
mode: string;
|
|
645
|
+
status: string;
|
|
646
|
+
rules: Record<string, unknown>;
|
|
647
|
+
created_at: string;
|
|
648
|
+
updated_at: string;
|
|
480
649
|
}
|
|
481
650
|
|
|
482
|
-
export interface
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
651
|
+
export interface PolicyVersionsResult {
|
|
652
|
+
policy_id: string;
|
|
653
|
+
policy_key: string;
|
|
654
|
+
versions: PolicyVersion[];
|
|
655
|
+
count: number;
|
|
656
|
+
tenant_id: string;
|
|
487
657
|
}
|
|
488
658
|
|
|
659
|
+
/** A single classified field change between two policy versions' rules. */
|
|
489
660
|
export interface PolicyChange {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
661
|
+
path: string;
|
|
662
|
+
before: unknown;
|
|
663
|
+
after: unknown;
|
|
664
|
+
risk: 'loosening' | 'tightening' | 'neutral';
|
|
665
|
+
rationale: string;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Semantic diff of v1.rules vs v2.rules. */
|
|
669
|
+
export interface PolicyRulesDiff {
|
|
670
|
+
changes: PolicyChange[];
|
|
671
|
+
risk: 'loosening' | 'tightening' | 'neutral';
|
|
672
|
+
summary: {
|
|
673
|
+
loosening: number;
|
|
674
|
+
tightening: number;
|
|
675
|
+
neutral: number;
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export interface PolicyDiff {
|
|
680
|
+
policy_id: string;
|
|
681
|
+
policy_key: string;
|
|
682
|
+
/** The lower-numbered handshake_policies version row. */
|
|
683
|
+
v1: PolicyVersion;
|
|
684
|
+
/** The higher-numbered handshake_policies version row. */
|
|
685
|
+
v2: PolicyVersion;
|
|
686
|
+
diff: PolicyRulesDiff;
|
|
687
|
+
tenant_id: string;
|
|
494
688
|
}
|
|
495
689
|
|
|
496
690
|
// -- Error ------------------------------------------------------------------
|
|
@@ -594,25 +788,43 @@ export class EPCloudClient {
|
|
|
594
788
|
}, true);
|
|
595
789
|
}
|
|
596
790
|
|
|
597
|
-
/**
|
|
598
|
-
|
|
791
|
+
/**
|
|
792
|
+
* Roll out a specific policy version to an environment. The version is
|
|
793
|
+
* resolved against handshake_policies by the policy's policy_key. Immediate
|
|
794
|
+
* rollouts supersede the prior active rollout for that (policy_key,
|
|
795
|
+
* environment); canary rollouts coexist (canaryPct in 1–99).
|
|
796
|
+
*/
|
|
797
|
+
async rolloutPolicy(
|
|
798
|
+
policyId: string,
|
|
799
|
+
version: number,
|
|
800
|
+
environment: string,
|
|
801
|
+
strategy: 'immediate' | 'canary' = 'immediate',
|
|
802
|
+
canaryPct?: number,
|
|
803
|
+
): Promise<PolicyRolloutResult> {
|
|
599
804
|
return this._request<PolicyRolloutResult>('POST', `/api/cloud/policies/${encodeURIComponent(policyId)}/rollout`, {
|
|
805
|
+
version,
|
|
806
|
+
environment,
|
|
600
807
|
strategy,
|
|
601
|
-
|
|
808
|
+
...(strategy === 'canary' && canaryPct !== undefined ? { canary_pct: canaryPct } : {}),
|
|
602
809
|
}, true);
|
|
603
810
|
}
|
|
604
811
|
|
|
605
|
-
/**
|
|
606
|
-
|
|
607
|
-
|
|
812
|
+
/**
|
|
813
|
+
* List all versions of a policy. Versions are the handshake_policies rows
|
|
814
|
+
* sharing the policy's policy_key, returned newest-first inside an envelope.
|
|
815
|
+
*/
|
|
816
|
+
async getPolicyVersions(policyId: string): Promise<PolicyVersionsResult> {
|
|
817
|
+
return this._request<PolicyVersionsResult>('GET', `/api/cloud/policies/${encodeURIComponent(policyId)}/versions`, undefined, true);
|
|
608
818
|
}
|
|
609
819
|
|
|
610
|
-
/**
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
820
|
+
/**
|
|
821
|
+
* Diff two versions of a policy to see what changed between their rules.
|
|
822
|
+
* v1 and v2 are version numbers; the response includes both version rows and
|
|
823
|
+
* a semantic diff classified as loosening / tightening / neutral.
|
|
824
|
+
*/
|
|
825
|
+
async diffPolicyVersions(policyId: string, v1: number, v2: number): Promise<PolicyDiff> {
|
|
826
|
+
const qs = this._toQs({ v1: String(v1), v2: String(v2) });
|
|
827
|
+
return this._request<PolicyDiff>('GET', `/api/cloud/policies/${encodeURIComponent(policyId)}/diff${qs}`, undefined, true);
|
|
616
828
|
}
|
|
617
829
|
|
|
618
830
|
/** @internal Build query string from SignoffFilters. */
|
|
@@ -646,6 +858,31 @@ export class EPCloudClient {
|
|
|
646
858
|
|
|
647
859
|
// -- Client -----------------------------------------------------------------
|
|
648
860
|
|
|
861
|
+
function trustReceiptBody(params: CreateTrustReceiptParams): Record<string, unknown> {
|
|
862
|
+
return {
|
|
863
|
+
organization_id: params.organizationId,
|
|
864
|
+
action_type: params.actionType,
|
|
865
|
+
target_resource_id: params.targetResourceId,
|
|
866
|
+
policy_id: params.policyId,
|
|
867
|
+
enforcement_mode: params.enforcementMode,
|
|
868
|
+
before_state: params.beforeState,
|
|
869
|
+
after_state: params.afterState,
|
|
870
|
+
target_changed_fields: params.targetChangedFields,
|
|
871
|
+
amount: params.amount,
|
|
872
|
+
currency: params.currency,
|
|
873
|
+
risk_flags: params.riskFlags,
|
|
874
|
+
actor_role: params.actorRole,
|
|
875
|
+
actor_department: params.actorDepartment,
|
|
876
|
+
business_hours: params.businessHours,
|
|
877
|
+
velocity_same_actor_24h: params.velocitySameActor24h,
|
|
878
|
+
prior_denials_actor_30d: params.priorDenialsActor30d,
|
|
879
|
+
prior_changes_target_30d: params.priorChangesTarget30d,
|
|
880
|
+
destination_age_days: params.destinationAgeDays,
|
|
881
|
+
quorum_policy: params.quorumPolicy,
|
|
882
|
+
metadata: params.metadata,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
649
886
|
export class EPClient {
|
|
650
887
|
private readonly baseUrl: string;
|
|
651
888
|
private readonly apiKey: string;
|
|
@@ -655,9 +892,15 @@ export class EPClient {
|
|
|
655
892
|
/** Cloud-specific endpoints (dashboards, analytics, audit, policy management). */
|
|
656
893
|
public readonly cloud: EPCloudClient;
|
|
657
894
|
|
|
658
|
-
constructor(options: EPClientOptions) {
|
|
659
|
-
|
|
660
|
-
|
|
895
|
+
constructor(options: EPClientOptions = {}) {
|
|
896
|
+
const env = typeof process !== 'undefined' && process.env ? process.env : {};
|
|
897
|
+
// Trim trailing slashes without a regex — avoids the polynomial-backtracking
|
|
898
|
+
// CodeQL flags on /\/+$/ for long all-slash inputs.
|
|
899
|
+
const rawBase = options.baseUrl ?? env.EP_BASE_URL ?? 'https://emiliaprotocol.ai';
|
|
900
|
+
let baseEnd = rawBase.length;
|
|
901
|
+
while (baseEnd > 0 && rawBase.charCodeAt(baseEnd - 1) === 47 /* '/' */) baseEnd--;
|
|
902
|
+
this.baseUrl = rawBase.slice(0, baseEnd);
|
|
903
|
+
this.apiKey = options.apiKey ?? env.EP_API_KEY ?? '';
|
|
661
904
|
this.timeout = options.timeout ?? 10_000;
|
|
662
905
|
this.retries = options.retries ?? 2;
|
|
663
906
|
|
|
@@ -674,7 +917,7 @@ export class EPClient {
|
|
|
674
917
|
const url = `${this.baseUrl}${path}`;
|
|
675
918
|
const headers: Record<string, string> = {
|
|
676
919
|
'Content-Type': 'application/json',
|
|
677
|
-
'User-Agent': '@emilia-protocol/sdk/0.
|
|
920
|
+
'User-Agent': '@emilia-protocol/sdk/0.10.0',
|
|
678
921
|
};
|
|
679
922
|
if (auth && this.apiKey) {
|
|
680
923
|
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
@@ -694,8 +937,18 @@ export class EPClient {
|
|
|
694
937
|
const data: unknown = await res.json().catch(() => undefined);
|
|
695
938
|
if (!res.ok) {
|
|
696
939
|
const p = data as Record<string, unknown> | undefined;
|
|
697
|
-
const msg = typeof p?.['error'] === 'string'
|
|
698
|
-
|
|
940
|
+
const msg = typeof p?.['error'] === 'string'
|
|
941
|
+
? p['error']
|
|
942
|
+
: typeof p?.['detail'] === 'string'
|
|
943
|
+
? p['detail']
|
|
944
|
+
: typeof p?.['title'] === 'string'
|
|
945
|
+
? p['title']
|
|
946
|
+
: `EP API error: ${res.status}`;
|
|
947
|
+
const code = typeof p?.['code'] === 'string'
|
|
948
|
+
? p['code']
|
|
949
|
+
: typeof p?.['type'] === 'string'
|
|
950
|
+
? p['type'].split('/').pop()
|
|
951
|
+
: undefined;
|
|
699
952
|
throw new EPError(msg, res.status, code);
|
|
700
953
|
}
|
|
701
954
|
return data as T;
|
|
@@ -798,6 +1051,153 @@ export class EPClient {
|
|
|
798
1051
|
);
|
|
799
1052
|
}
|
|
800
1053
|
|
|
1054
|
+
// ------------------------------------------------------------------
|
|
1055
|
+
// v1 Trust Receipt Enforcement
|
|
1056
|
+
// ------------------------------------------------------------------
|
|
1057
|
+
|
|
1058
|
+
/** Create a v1 pre-action trust receipt for a high-risk mutation. */
|
|
1059
|
+
async createTrustReceipt(params: CreateTrustReceiptParams): Promise<TrustReceipt> {
|
|
1060
|
+
return this.request<TrustReceipt>('POST', '/api/v1/trust-receipts', trustReceiptBody(params), true);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** Read current receipt state from the append-only v1 audit timeline. */
|
|
1064
|
+
async getTrustReceipt(receiptId: string): Promise<TrustReceiptState> {
|
|
1065
|
+
return this.request<TrustReceiptState>(
|
|
1066
|
+
'GET',
|
|
1067
|
+
`/api/v1/trust-receipts/${encodeURIComponent(receiptId)}`,
|
|
1068
|
+
undefined,
|
|
1069
|
+
true,
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/** Request human signoff for a receipt that requires approval. */
|
|
1074
|
+
async requestSignoff(params: RequestSignoffParams): Promise<SignoffRequest> {
|
|
1075
|
+
return this.request<SignoffRequest>('POST', '/api/v1/signoffs/request', {
|
|
1076
|
+
receipt_id: params.receiptId,
|
|
1077
|
+
approver_id: params.approverId,
|
|
1078
|
+
expires_in_minutes: params.expiresInMinutes,
|
|
1079
|
+
comment: params.comment,
|
|
1080
|
+
}, true);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/** Consume a receipt before mutation. If this fails, do not execute the write. */
|
|
1084
|
+
async consumeTrustReceipt(
|
|
1085
|
+
receiptId: string,
|
|
1086
|
+
params: { actionHash: string; executingSystem: string; executionReferenceId?: string },
|
|
1087
|
+
): Promise<ConsumeTrustReceiptResult> {
|
|
1088
|
+
return this.request<ConsumeTrustReceiptResult>(
|
|
1089
|
+
'POST',
|
|
1090
|
+
`/api/v1/trust-receipts/${encodeURIComponent(receiptId)}/consume`,
|
|
1091
|
+
{
|
|
1092
|
+
action_hash: params.actionHash,
|
|
1093
|
+
executing_system: params.executingSystem,
|
|
1094
|
+
execution_reference_id: params.executionReferenceId,
|
|
1095
|
+
},
|
|
1096
|
+
true,
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/** Emit the post-mutation execution attestation bound to the consumed receipt. */
|
|
1101
|
+
async attestExecution(
|
|
1102
|
+
receiptId: string,
|
|
1103
|
+
params: {
|
|
1104
|
+
executedAction: Record<string, unknown>;
|
|
1105
|
+
executingSystem: string;
|
|
1106
|
+
executionId?: string;
|
|
1107
|
+
executedAt?: string;
|
|
1108
|
+
},
|
|
1109
|
+
): Promise<ExecutionAttestation> {
|
|
1110
|
+
return this.request<ExecutionAttestation>(
|
|
1111
|
+
'POST',
|
|
1112
|
+
`/api/v1/trust-receipts/${encodeURIComponent(receiptId)}/execution`,
|
|
1113
|
+
{
|
|
1114
|
+
executed_action: params.executedAction,
|
|
1115
|
+
executing_system: params.executingSystem,
|
|
1116
|
+
execution_id: params.executionId,
|
|
1117
|
+
executed_at: params.executedAt,
|
|
1118
|
+
},
|
|
1119
|
+
true,
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/** Fetch the signed evidence packet, when the receipt is in a signable state. */
|
|
1124
|
+
async getTrustReceiptEvidence(receiptId: string): Promise<TrustReceiptEvidence> {
|
|
1125
|
+
return this.request<TrustReceiptEvidence>(
|
|
1126
|
+
'GET',
|
|
1127
|
+
`/api/v1/trust-receipts/${encodeURIComponent(receiptId)}/evidence`,
|
|
1128
|
+
undefined,
|
|
1129
|
+
true,
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Wrap a dangerous mutation in the v1 receipt lifecycle.
|
|
1135
|
+
* The mutation runs only after create + consume succeed.
|
|
1136
|
+
*/
|
|
1137
|
+
async requireReceipt<T>(
|
|
1138
|
+
params: RequireReceiptParams,
|
|
1139
|
+
mutate: (ctx: { receipt: TrustReceipt; consume: ConsumeTrustReceiptResult }) => Promise<T>,
|
|
1140
|
+
): Promise<RequireReceiptResult<T>> {
|
|
1141
|
+
const receipt = await this.createTrustReceipt(params);
|
|
1142
|
+
if (receipt.decision === 'deny' || receipt.receipt_status === 'denied') {
|
|
1143
|
+
throw new EPError('EMILIA denied the action before execution', 403, 'receipt_denied');
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
let signoff: SignoffRequest | undefined;
|
|
1147
|
+
if (receipt.signoff_required) {
|
|
1148
|
+
if (!params.approverId && !params.quorumPolicy) {
|
|
1149
|
+
throw new EPError('Receipt requires signoff; pass approverId or quorumPolicy', 409, 'missing_approver_id');
|
|
1150
|
+
}
|
|
1151
|
+
signoff = await this.requestSignoff({
|
|
1152
|
+
receiptId: receipt.receipt_id,
|
|
1153
|
+
approverId: params.approverId,
|
|
1154
|
+
expiresInMinutes: params.signoffExpiresInMinutes,
|
|
1155
|
+
comment: params.signoffComment,
|
|
1156
|
+
});
|
|
1157
|
+
if (!params.onSignoffRequired) {
|
|
1158
|
+
throw new EPError('Receipt requires human signoff before the mutation can run', 409, 'signoff_required');
|
|
1159
|
+
}
|
|
1160
|
+
const signoffResult = await params.onSignoffRequired({ client: this, receipt, signoff });
|
|
1161
|
+
if (signoffResult === false || (typeof signoffResult === 'object' && signoffResult?.approved === false)) {
|
|
1162
|
+
throw new EPError('Human signoff was not approved', 403, 'signoff_rejected');
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const consume = await this.consumeTrustReceipt(receipt.receipt_id, {
|
|
1167
|
+
actionHash: receipt.action_hash,
|
|
1168
|
+
executingSystem: params.executingSystem,
|
|
1169
|
+
executionReferenceId: params.executionReferenceId,
|
|
1170
|
+
});
|
|
1171
|
+
const result = await mutate({ receipt, consume });
|
|
1172
|
+
const executedAction = typeof params.executedAction === 'function'
|
|
1173
|
+
? params.executedAction({ receipt, result })
|
|
1174
|
+
: params.executedAction ?? receipt.canonical_action;
|
|
1175
|
+
const executionId = typeof params.executionId === 'function'
|
|
1176
|
+
? params.executionId(result)
|
|
1177
|
+
: params.executionId;
|
|
1178
|
+
const execution = await this.attestExecution(receipt.receipt_id, {
|
|
1179
|
+
executedAction,
|
|
1180
|
+
executingSystem: params.executingSystem,
|
|
1181
|
+
executionId,
|
|
1182
|
+
});
|
|
1183
|
+
const evidence = params.fetchEvidence
|
|
1184
|
+
? await this.getTrustReceiptEvidence(receipt.receipt_id)
|
|
1185
|
+
: undefined;
|
|
1186
|
+
|
|
1187
|
+
return { result, receipt, signoff, consume, execution, evidence };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** Wrap an existing async function with requireReceipt(). */
|
|
1191
|
+
withReceipt<TArgs extends unknown[], TResult>(
|
|
1192
|
+
params: RequireReceiptParams | ((...args: TArgs) => RequireReceiptParams),
|
|
1193
|
+
mutate: (...args: TArgs) => Promise<TResult>,
|
|
1194
|
+
): (...args: TArgs) => Promise<RequireReceiptResult<TResult>> {
|
|
1195
|
+
return async (...args: TArgs) => {
|
|
1196
|
+
const resolved = typeof params === 'function' ? params(...args) : params;
|
|
1197
|
+
return this.requireReceipt(resolved, () => mutate(...args));
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
|
|
801
1201
|
// ------------------------------------------------------------------
|
|
802
1202
|
// Signoff extension
|
|
803
1203
|
// ------------------------------------------------------------------
|