@emilia-protocol/gate 0.13.0 → 0.15.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +89 -0
  3. package/aeb-consumption-store.js +3 -0
  4. package/dist/aeb-consumption-store.d.ts +80 -0
  5. package/dist/aeb-consumption-store.d.ts.map +1 -0
  6. package/dist/aeb-consumption-store.js +430 -0
  7. package/dist/aeb-consumption-store.js.map +1 -0
  8. package/dist/capability-receipt.d.ts +8 -0
  9. package/dist/capability-receipt.d.ts.map +1 -1
  10. package/dist/capability-receipt.js +17 -0
  11. package/dist/capability-receipt.js.map +1 -1
  12. package/dist/index.d.ts +13 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +17 -4
  15. package/dist/index.js.map +1 -1
  16. package/dist/proposal-to-effect-postgres.d.ts +129 -0
  17. package/dist/proposal-to-effect-postgres.d.ts.map +1 -0
  18. package/dist/proposal-to-effect-postgres.js +1511 -0
  19. package/dist/proposal-to-effect-postgres.js.map +1 -0
  20. package/dist/proposal-to-effect-status.d.ts +55 -0
  21. package/dist/proposal-to-effect-status.d.ts.map +1 -0
  22. package/dist/proposal-to-effect-status.js +280 -0
  23. package/dist/proposal-to-effect-status.js.map +1 -0
  24. package/dist/proposal-to-effect.d.ts +294 -0
  25. package/dist/proposal-to-effect.d.ts.map +1 -0
  26. package/dist/proposal-to-effect.js +970 -0
  27. package/dist/proposal-to-effect.js.map +1 -0
  28. package/dist/remedy-case-set-postgres.d.ts +73 -0
  29. package/dist/remedy-case-set-postgres.d.ts.map +1 -0
  30. package/dist/remedy-case-set-postgres.js +846 -0
  31. package/dist/remedy-case-set-postgres.js.map +1 -0
  32. package/dist/remedy-case-set.d.ts +53 -0
  33. package/dist/remedy-case-set.d.ts.map +1 -0
  34. package/dist/remedy-case-set.js +571 -0
  35. package/dist/remedy-case-set.js.map +1 -0
  36. package/dist/remedy-program-adapters.d.ts +137 -0
  37. package/dist/remedy-program-adapters.d.ts.map +1 -0
  38. package/dist/remedy-program-adapters.js +590 -0
  39. package/dist/remedy-program-adapters.js.map +1 -0
  40. package/dist/trust-program-revocation.js.map +1 -1
  41. package/package.json +280 -63
  42. package/proposal-to-effect-postgres.js +3 -0
  43. package/proposal-to-effect-status.js +2 -0
  44. package/proposal-to-effect.js +4 -0
  45. package/remedy-case-set-postgres.js +3 -0
  46. package/remedy-case-set.js +3 -0
  47. package/remedy-program-adapters.js +3 -0
  48. package/src/aeb-consumption-store.ts +521 -0
  49. package/src/capability-receipt.ts +17 -0
  50. package/src/index.ts +57 -3
  51. package/src/proposal-to-effect-postgres.ts +1740 -0
  52. package/src/proposal-to-effect-status.ts +357 -0
  53. package/src/proposal-to-effect.ts +1276 -0
  54. package/src/remedy-case-set-postgres.ts +1008 -0
  55. package/src/remedy-case-set.ts +603 -0
  56. package/src/remedy-program-adapters.ts +657 -0
  57. package/src/trust-program-revocation.ts +1 -1
@@ -0,0 +1,970 @@
1
+ // @ts-nocheck
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Proposal-to-Effect is a product orchestration profile over existing EMILIA
5
+ * artifacts. A proposal is deliberately NOT a bearer authorization object.
6
+ * Authority remains in EP-RECEIPT-v1 and the relying party's pinned AEB
7
+ * requirement; consequence custody remains in Gate and its durable stores.
8
+ */
9
+ import crypto from 'node:crypto';
10
+ import { beginReceiptApproval, pollReceiptApproval, approvalActionHash, validateApprovalAuthorization, validateCaidSelector, validateRequiredFields, } from '@emilia-protocol/require-receipt/acquisition';
11
+ import { aebReservationKey, authorizeAebExecutionDurable, digestAeb, pinnedConfigDigest, reconcileAebExecutionDurable, verifyAebEvaluation, } from '@emilia-protocol/verify/aeb-adapter-contract';
12
+ import { actionDigest as aecActionDigest } from '@emilia-protocol/verify/evidence-chain';
13
+ export const PROPOSAL_TO_EFFECT_VERSION = 'EMILIA-PROPOSAL-TO-EFFECT-v1';
14
+ const CAID_PATTERN = /^caid:1:[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*\.[1-9][0-9]*:[a-z0-9]+(?:-[a-z0-9]+)*:[A-Za-z0-9_-]{43}$/;
15
+ const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{2,255}$/;
16
+ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
17
+ const PROPOSAL_INTEGRITY_DOMAIN = `${PROPOSAL_TO_EFFECT_VERSION}:INTEGRITY\0`;
18
+ function isPlainObject(value) {
19
+ if (!value || typeof value !== 'object' || Array.isArray(value))
20
+ return false;
21
+ const prototype = Object.getPrototypeOf(value);
22
+ return prototype === Object.prototype || prototype === null;
23
+ }
24
+ function clone(value) {
25
+ return structuredClone(value);
26
+ }
27
+ function assertIdentifier(value, name) {
28
+ if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
29
+ throw new Error(`${name}_invalid`);
30
+ }
31
+ }
32
+ function validDigest(value) {
33
+ return typeof value === 'string' && DIGEST_PATTERN.test(value);
34
+ }
35
+ function canonicalInstant(value) {
36
+ if (typeof value !== 'string')
37
+ return NaN;
38
+ const parsed = Date.parse(value);
39
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value)
40
+ return NaN;
41
+ return parsed;
42
+ }
43
+ function exactKeys(value, keys) {
44
+ if (!isPlainObject(value))
45
+ return false;
46
+ const actual = Object.keys(value).sort();
47
+ const expected = [...keys].sort();
48
+ return actual.length === expected.length && expected.every((key, index) => key === actual[index]);
49
+ }
50
+ function secureConsequenceStore(store) {
51
+ return store !== null && typeof store === 'object'
52
+ && store.durable === true && store.ownershipFenced === true
53
+ && store.compareAndSwap === true && store.atomicEvidenceBinding === true
54
+ && typeof store.reserve === 'function' && typeof store.transition === 'function'
55
+ && typeof store.reconcile === 'function' && typeof store.read === 'function';
56
+ }
57
+ function normalizedCurrentStatus(value) {
58
+ return exactKeys(value, [
59
+ 'checked_at', 'expires_at', 'revocation_checked', 'revoked', 'consumed',
60
+ ...(isPlainObject(value) && Object.hasOwn(value, 'unavailable') ? ['unavailable'] : []),
61
+ ]) && typeof value.checked_at === 'string' && Number.isFinite(Date.parse(value.checked_at))
62
+ && typeof value.expires_at === 'string' && Number.isFinite(Date.parse(value.expires_at))
63
+ && typeof value.revocation_checked === 'boolean' && typeof value.revoked === 'boolean'
64
+ && typeof value.consumed === 'boolean'
65
+ && (value.unavailable === undefined || typeof value.unavailable === 'boolean');
66
+ }
67
+ function assertProfile(profile) {
68
+ if (!isPlainObject(profile))
69
+ throw new Error('proposal_profile_invalid');
70
+ assertIdentifier(profile.id, 'proposal_profile_id');
71
+ assertIdentifier(profile.action_type, 'proposal_action_type');
72
+ assertIdentifier(profile.aeb_requirement_ref, 'proposal_aeb_requirement_ref');
73
+ if (!isPlainObject(profile.selector) || Object.keys(profile.selector).length === 0) {
74
+ throw new Error('proposal_selector_invalid');
75
+ }
76
+ const requiredFields = validateRequiredFields(profile.required_fields);
77
+ if (!requiredFields.ok)
78
+ throw new Error(requiredFields.reason);
79
+ if (!Number.isSafeInteger(profile.ttl_sec) || profile.ttl_sec <= 0 || profile.ttl_sec > 86_400) {
80
+ throw new Error('proposal_ttl_invalid');
81
+ }
82
+ if (typeof profile.canonicalize_action !== 'function') {
83
+ throw new Error('proposal_canonicalizer_required');
84
+ }
85
+ const authorization = validateApprovalAuthorization(profile.authorization);
86
+ if (!authorization.ok)
87
+ throw new Error(authorization.reason);
88
+ if (profile.caid_selector) {
89
+ const caidSelector = validateCaidSelector(profile.caid_selector);
90
+ if (!caidSelector.ok)
91
+ throw new Error(caidSelector.reason);
92
+ }
93
+ }
94
+ function canonicalizeForProfile(profile, input) {
95
+ const normalized = profile.canonicalize_action(clone(input));
96
+ if (!isPlainObject(normalized) || !isPlainObject(normalized.action)
97
+ || typeof normalized.caid !== 'string' || !CAID_PATTERN.test(normalized.caid)) {
98
+ throw new Error('proposal_canonicalization_invalid');
99
+ }
100
+ if (normalized.action.action_type !== profile.action_type) {
101
+ throw new Error('proposal_action_type_mismatch');
102
+ }
103
+ for (const field of profile.required_fields) {
104
+ if (!Object.hasOwn(normalized.action, field) || normalized.action[field] === undefined) {
105
+ throw new Error(`proposal_required_field_missing:${field}`);
106
+ }
107
+ }
108
+ if (profile.caid_selector) {
109
+ const field = profile.caid_selector.field;
110
+ if (typeof field !== 'string' || normalized.action[field] !== normalized.caid) {
111
+ throw new Error(`proposal_caid_binding_invalid:${field}`);
112
+ }
113
+ }
114
+ // Both digest functions reject unsupported/non-canonical JSON values.
115
+ approvalActionHash(normalized.action);
116
+ digestAeb(normalized.action);
117
+ return { action: clone(normalized.action), caid: normalized.caid };
118
+ }
119
+ function assertSameObject(left, right, reason) {
120
+ if (digestAeb(left) !== digestAeb(right))
121
+ throw new Error(reason);
122
+ }
123
+ function exactProposalKeys(proposal) {
124
+ const expected = [
125
+ '@version', 'action', 'action_digest', 'aeb', 'aeb_action_digest', 'authorization',
126
+ 'caid', 'challenge', 'consequence', 'created_at', 'expires_at', 'initiator_id',
127
+ 'integrity', 'operation_id', 'profile_id', 'proposal_id',
128
+ ].sort();
129
+ const actual = Object.keys(proposal).sort();
130
+ return expected.length === actual.length && expected.every((key, index) => key === actual[index]);
131
+ }
132
+ function proposalAdmissibility(proposal, record) {
133
+ return {
134
+ admissibility_profile: { id: `aeb:${proposal.aeb.requirement_ref}`, version: '1' },
135
+ profile_hash: proposal.aeb.pinned_config_digest,
136
+ verdict: 'admissible',
137
+ replay_digest: record.evidence_digest,
138
+ challenge_id: proposal.proposal_id,
139
+ aeb_evaluation_digest: digestAeb(record),
140
+ };
141
+ }
142
+ function refusal(reason, extra = {}) {
143
+ return { ok: false, reason, ...extra };
144
+ }
145
+ export function proposalToEffectConsumptionNonce(operationId, pinnedConfigDigest) {
146
+ assertIdentifier(operationId, 'proposal_operation_id');
147
+ if (typeof pinnedConfigDigest !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(pinnedConfigDigest)) {
148
+ throw new Error('proposal_aeb_pin_invalid');
149
+ }
150
+ return digestAeb({
151
+ domain: PROPOSAL_TO_EFFECT_VERSION,
152
+ operation_id: operationId,
153
+ pinned_config_digest: pinnedConfigDigest,
154
+ });
155
+ }
156
+ function expectedAebCompositionDigest(proposal) {
157
+ return `sha256:${aecActionDigest({
158
+ caid: proposal.caid,
159
+ normalized_action_digest: proposal.aeb_action_digest,
160
+ })}`;
161
+ }
162
+ export function createProposalToEffect(options) {
163
+ if (!options?.gate || typeof options.gate.check !== 'function' || typeof options.gate.run !== 'function') {
164
+ throw new Error('proposal_gate_required');
165
+ }
166
+ if (!isPlainObject(options.profiles) || Object.keys(options.profiles).length === 0) {
167
+ throw new Error('proposal_profiles_required');
168
+ }
169
+ for (const [id, profile] of Object.entries(options.profiles)) {
170
+ assertProfile(profile);
171
+ if (id !== profile.id)
172
+ throw new Error('proposal_profile_registry_mismatch');
173
+ }
174
+ if (!options.aeb?.config || !options.aeb.adapters || !options.aeb.store
175
+ || typeof options.aeb.resolve_artifacts !== 'function'
176
+ || typeof options.aeb.currentStatusResolver !== 'function'
177
+ || typeof options.aeb.statusVerifier !== 'function'
178
+ || typeof options.aeb.verify_provider_evidence !== 'function') {
179
+ throw new Error('proposal_aeb_configuration_required');
180
+ }
181
+ if (!(options.proposal_integrity?.hmac_sha256_key instanceof Uint8Array)
182
+ || options.proposal_integrity.hmac_sha256_key.byteLength < 32) {
183
+ throw new Error('proposal_integrity_configuration_required');
184
+ }
185
+ if (!options.consequence)
186
+ throw new Error('proposal_consequence_configuration_required');
187
+ assertIdentifier(options.consequence.tenant_id, 'proposal_tenant_id');
188
+ assertIdentifier(options.consequence.provider_id, 'proposal_provider_id');
189
+ assertIdentifier(options.consequence.provider_account_id, 'proposal_provider_account_id');
190
+ assertIdentifier(options.consequence.environment, 'proposal_environment');
191
+ assertIdentifier(options.consequence.executor_id, 'proposal_executor_id');
192
+ if (!secureConsequenceStore(options.consequence.store)
193
+ || (options.consequence.create_attempt_id !== undefined
194
+ && typeof options.consequence.create_attempt_id !== 'function')) {
195
+ throw new Error('proposal_consequence_store_required');
196
+ }
197
+ const now = options.now ?? Date.now;
198
+ const configDigest = pinnedConfigDigest(options.aeb.config);
199
+ const integrityKey = Buffer.from(options.proposal_integrity.hmac_sha256_key);
200
+ const consequenceContext = Object.freeze({
201
+ tenant_id: options.consequence.tenant_id,
202
+ provider_id: options.consequence.provider_id,
203
+ provider_account_id: options.consequence.provider_account_id,
204
+ environment: options.consequence.environment,
205
+ executor_id: options.consequence.executor_id,
206
+ });
207
+ // Active owner capabilities never become enumerable response/error data.
208
+ // The same-process service can recover a handle from the exact object; after
209
+ // restart it must use the durable store's separately authorized recovery API.
210
+ const reconciliationHandles = new WeakMap();
211
+ function rememberReconciliationHandle(target, reference) {
212
+ reconciliationHandles.set(target, reference);
213
+ return target;
214
+ }
215
+ function getReconciliationHandle(target) {
216
+ const reference = reconciliationHandles.get(target);
217
+ return reference ? clone(reference) : null;
218
+ }
219
+ function currentTime() {
220
+ const value = now();
221
+ if (!Number.isSafeInteger(value) || !Number.isFinite(new Date(value).getTime())) {
222
+ throw new Error('proposal_time_invalid');
223
+ }
224
+ return value;
225
+ }
226
+ async function reconcileAebWithRecovery(key, outcome, authorization) {
227
+ let result = await reconcileAebExecutionDurable(options.aeb.store, key, outcome);
228
+ const recoveryStore = options.aeb.store;
229
+ if (result.state === 'RECONCILIATION_REQUIRED'
230
+ && authorization !== undefined
231
+ && typeof recoveryStore.claimReservation === 'function'
232
+ && await recoveryStore.claimReservation(key, authorization).catch(() => false)) {
233
+ result = await reconcileAebExecutionDurable(options.aeb.store, key, outcome);
234
+ }
235
+ return result;
236
+ }
237
+ function requestDigestFor(proposal) {
238
+ return digestAeb({
239
+ domain: `${PROPOSAL_TO_EFFECT_VERSION}:REQUEST`,
240
+ ...consequenceContext,
241
+ proposal_id: proposal.proposal_id,
242
+ operation_id: proposal.operation_id,
243
+ initiator_id: proposal.initiator_id,
244
+ profile_id: proposal.profile_id,
245
+ action: proposal.action,
246
+ action_digest: proposal.action_digest,
247
+ aeb_action_digest: proposal.aeb_action_digest,
248
+ caid: proposal.caid,
249
+ created_at: proposal.created_at,
250
+ expires_at: proposal.expires_at,
251
+ challenge: proposal.challenge,
252
+ authorization: proposal.authorization,
253
+ aeb: proposal.aeb,
254
+ });
255
+ }
256
+ function integrityMac(unsignedProposal) {
257
+ return crypto.createHmac('sha256', integrityKey)
258
+ .update(PROPOSAL_INTEGRITY_DOMAIN)
259
+ .update(digestAeb(unsignedProposal))
260
+ .digest('base64url');
261
+ }
262
+ function proposalIntegrityValid(proposal) {
263
+ if (!exactKeys(proposal.integrity, ['alg', 'value'])
264
+ || proposal.integrity.alg !== 'HMAC-SHA256'
265
+ || typeof proposal.integrity.value !== 'string'
266
+ || !/^[A-Za-z0-9_-]{43}$/.test(proposal.integrity.value))
267
+ return false;
268
+ const unsigned = clone(proposal);
269
+ delete unsigned.integrity;
270
+ const actual = Buffer.from(proposal.integrity.value, 'base64url');
271
+ const expected = Buffer.from(integrityMac(unsigned), 'base64url');
272
+ return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
273
+ }
274
+ function profileFor(id) {
275
+ if (typeof id !== 'string' || !options.profiles[id])
276
+ throw new Error('proposal_profile_not_pinned');
277
+ return options.profiles[id];
278
+ }
279
+ function prepare(input) {
280
+ assertIdentifier(input?.proposal_id, 'proposal_id');
281
+ assertIdentifier(input?.operation_id, 'proposal_operation_id');
282
+ assertIdentifier(input?.initiator_id, 'proposal_initiator_id');
283
+ const profile = profileFor(input?.profile_id);
284
+ const normalized = canonicalizeForProfile(profile, input.action);
285
+ const createdAtMs = currentTime();
286
+ const actionDigest = approvalActionHash(normalized.action);
287
+ const base = {
288
+ '@version': PROPOSAL_TO_EFFECT_VERSION,
289
+ proposal_id: input.proposal_id,
290
+ operation_id: input.operation_id,
291
+ initiator_id: input.initiator_id,
292
+ profile_id: profile.id,
293
+ action: normalized.action,
294
+ action_digest: actionDigest,
295
+ aeb_action_digest: digestAeb(normalized.action),
296
+ caid: normalized.caid,
297
+ created_at: new Date(createdAtMs).toISOString(),
298
+ expires_at: new Date(createdAtMs + profile.ttl_sec * 1000).toISOString(),
299
+ challenge: {
300
+ action: profile.action_type,
301
+ action_hash: actionDigest,
302
+ required_fields: [...profile.required_fields],
303
+ ...(profile.caid_selector ? { caid_selector: clone(profile.caid_selector) } : {}),
304
+ },
305
+ authorization: clone(profile.authorization),
306
+ aeb: {
307
+ requirement_ref: profile.aeb_requirement_ref,
308
+ pinned_config_digest: configDigest,
309
+ consumption_nonce: proposalToEffectConsumptionNonce(input.operation_id, configDigest),
310
+ },
311
+ };
312
+ const unsigned = {
313
+ ...base,
314
+ consequence: {
315
+ ...consequenceContext,
316
+ request_digest: requestDigestFor(base),
317
+ },
318
+ };
319
+ return clone({
320
+ ...unsigned,
321
+ integrity: { alg: 'HMAC-SHA256', value: integrityMac(unsigned) },
322
+ });
323
+ }
324
+ function verifyProposal(input, { allowExpired = false } = {}) {
325
+ if (!isPlainObject(input) || !exactProposalKeys(input)
326
+ || input['@version'] !== PROPOSAL_TO_EFFECT_VERSION) {
327
+ throw new Error('proposal_shape_invalid');
328
+ }
329
+ if (!proposalIntegrityValid(input))
330
+ throw new Error('proposal_integrity_invalid');
331
+ const proposal = input;
332
+ assertIdentifier(proposal.proposal_id, 'proposal_id');
333
+ assertIdentifier(proposal.operation_id, 'proposal_operation_id');
334
+ assertIdentifier(proposal.initiator_id, 'proposal_initiator_id');
335
+ const profile = profileFor(proposal.profile_id);
336
+ const normalized = canonicalizeForProfile(profile, proposal.action);
337
+ if (normalized.caid !== proposal.caid)
338
+ throw new Error('proposal_caid_mismatch');
339
+ assertSameObject(normalized.action, proposal.action, 'proposal_action_not_canonical');
340
+ if (approvalActionHash(normalized.action) !== proposal.action_digest) {
341
+ throw new Error('proposal_action_digest_mismatch');
342
+ }
343
+ if (digestAeb(normalized.action) !== proposal.aeb_action_digest) {
344
+ throw new Error('proposal_aeb_action_digest_mismatch');
345
+ }
346
+ const createdAtMs = canonicalInstant(proposal.created_at);
347
+ const expiresAtMs = canonicalInstant(proposal.expires_at);
348
+ if (!Number.isFinite(createdAtMs) || !Number.isFinite(expiresAtMs))
349
+ throw new Error('proposal_time_invalid');
350
+ if (expiresAtMs - createdAtMs !== profile.ttl_sec * 1000)
351
+ throw new Error('proposal_ttl_mismatch');
352
+ const decisionTime = currentTime();
353
+ if (createdAtMs > decisionTime)
354
+ throw new Error('proposal_created_in_future');
355
+ if (!allowExpired && decisionTime >= expiresAtMs)
356
+ throw new Error('proposal_expired');
357
+ assertSameObject(proposal.authorization, profile.authorization, 'proposal_authorization_mismatch');
358
+ if (!isPlainObject(proposal.aeb)
359
+ || Object.keys(proposal.aeb).sort().join(',') !== 'consumption_nonce,pinned_config_digest,requirement_ref'
360
+ || proposal.aeb?.requirement_ref !== profile.aeb_requirement_ref
361
+ || proposal.aeb?.pinned_config_digest !== configDigest) {
362
+ throw new Error('proposal_aeb_pin_mismatch');
363
+ }
364
+ if (proposal.aeb.consumption_nonce !== proposalToEffectConsumptionNonce(proposal.operation_id, configDigest)) {
365
+ throw new Error('proposal_aeb_nonce_mismatch');
366
+ }
367
+ if (!exactKeys(proposal.consequence, [
368
+ 'tenant_id', 'provider_id', 'provider_account_id', 'environment', 'executor_id', 'request_digest',
369
+ ]) || proposal.consequence.tenant_id !== consequenceContext.tenant_id
370
+ || proposal.consequence.provider_id !== consequenceContext.provider_id
371
+ || proposal.consequence.provider_account_id !== consequenceContext.provider_account_id
372
+ || proposal.consequence.environment !== consequenceContext.environment
373
+ || proposal.consequence.executor_id !== consequenceContext.executor_id
374
+ || proposal.consequence.request_digest !== requestDigestFor(proposal)) {
375
+ throw new Error('proposal_consequence_binding_mismatch');
376
+ }
377
+ const expectedChallenge = {
378
+ action: profile.action_type,
379
+ action_hash: proposal.action_digest,
380
+ required_fields: [...profile.required_fields],
381
+ ...(profile.caid_selector ? { caid_selector: clone(profile.caid_selector) } : {}),
382
+ };
383
+ assertSameObject(proposal.challenge, expectedChallenge, 'proposal_challenge_mismatch');
384
+ return { proposal: clone(proposal), profile };
385
+ }
386
+ async function verifyEvaluation(proposal, evaluation) {
387
+ if (!isPlainObject(evaluation))
388
+ return { valid: false, reason: 'aeb_evaluation_missing', record: null };
389
+ const record = evaluation;
390
+ if (record.operation_id !== proposal.operation_id
391
+ || record.consumption_nonce !== proposal.aeb.consumption_nonce
392
+ || record.initiator_id !== proposal.initiator_id
393
+ || record.executor_id !== proposal.consequence.executor_id
394
+ || record.requirement_ref !== proposal.aeb.requirement_ref
395
+ || record.caid !== proposal.caid) {
396
+ return { valid: false, reason: 'aeb_evaluation_binding_mismatch', record };
397
+ }
398
+ if (record.verdict === 'SATISFIED'
399
+ && record.composition?.action_digest !== expectedAebCompositionDigest(proposal)) {
400
+ return { valid: false, reason: 'aeb_evaluation_binding_mismatch', record };
401
+ }
402
+ let artifacts;
403
+ try {
404
+ artifacts = await options.aeb.resolve_artifacts({ proposal: clone(proposal), evaluation: clone(record) });
405
+ }
406
+ catch {
407
+ return { valid: false, reason: 'aeb_artifact_resolution_failed', record };
408
+ }
409
+ const decisionNow = new Date(currentTime()).toISOString();
410
+ const currentStatuses = {};
411
+ for (const leg of record.legs ?? []) {
412
+ try {
413
+ const statusArtifact = await options.aeb.currentStatusResolver({
414
+ proposal: clone(proposal),
415
+ evaluation: clone(record),
416
+ leg: clone(leg),
417
+ });
418
+ if (statusArtifact === undefined || statusArtifact === null) {
419
+ return { valid: false, reason: 'aeb_current_status_refused', record };
420
+ }
421
+ const status = await options.aeb.statusVerifier({
422
+ status_artifact: clone(statusArtifact),
423
+ expected: {
424
+ tenant_id: proposal.consequence.tenant_id,
425
+ executor_id: proposal.consequence.executor_id,
426
+ operation_id: proposal.operation_id,
427
+ caid: proposal.caid,
428
+ artifact_ref: leg.artifact_ref,
429
+ evidence_digest: leg.evidence_digest,
430
+ replay_unit: leg.replay_unit,
431
+ },
432
+ now: decisionNow,
433
+ });
434
+ if (!status?.valid || status.outcome !== 'current_not_revoked'
435
+ || !normalizedCurrentStatus(status.status)
436
+ || status.status.revocation_checked !== true || status.status.revoked !== false
437
+ || status.status.consumed !== false || status.status.unavailable === true) {
438
+ return { valid: false, reason: 'aeb_current_status_refused', record };
439
+ }
440
+ currentStatuses[leg.artifact_ref] = clone(status.status);
441
+ }
442
+ catch {
443
+ return { valid: false, reason: 'aeb_current_status_refused', record };
444
+ }
445
+ }
446
+ if (record.legs.length === 0 || Object.keys(currentStatuses).length !== record.legs.length) {
447
+ return { valid: false, reason: 'aeb_current_status_refused', record };
448
+ }
449
+ const verificationOptions = {
450
+ mode: 'execution',
451
+ config: options.aeb.config,
452
+ adapters: options.aeb.adapters,
453
+ artifacts,
454
+ expected_action: clone(proposal.action),
455
+ executor_id: proposal.consequence.executor_id,
456
+ current_statuses: currentStatuses,
457
+ now: decisionNow,
458
+ };
459
+ const checked = verifyAebEvaluation(record, verificationOptions);
460
+ if (!checked.valid || checked.execution_authorizing !== true
461
+ || record.verdict !== 'SATISFIED'
462
+ || record.authority_constraints?.one_time_consumption !== true) {
463
+ return { valid: false, reason: 'aeb_evaluation_refused', record, checked };
464
+ }
465
+ return { valid: true, reason: null, record, checked };
466
+ }
467
+ function gateInput(proposal, profile, receipt, record) {
468
+ return {
469
+ selector: {
470
+ ...clone(profile.selector),
471
+ operation_id: proposal.operation_id,
472
+ initiator_id: proposal.initiator_id,
473
+ aeb_requirement_ref: proposal.aeb.requirement_ref,
474
+ tenant_id: proposal.consequence.tenant_id,
475
+ provider_id: proposal.consequence.provider_id,
476
+ provider_account_id: proposal.consequence.provider_account_id,
477
+ environment: proposal.consequence.environment,
478
+ executor_id: proposal.consequence.executor_id,
479
+ request_digest: proposal.consequence.request_digest,
480
+ },
481
+ receipt,
482
+ observedAction: clone(proposal.action),
483
+ admissibility: proposalAdmissibility(proposal, record),
484
+ };
485
+ }
486
+ async function execute(input, effect) {
487
+ if (typeof effect !== 'function')
488
+ throw new Error('proposal_effect_required');
489
+ const { proposal, profile } = verifyProposal(input?.proposal);
490
+ const evaluation = await verifyEvaluation(proposal, input?.evaluation);
491
+ if (!evaluation.valid || !evaluation.record) {
492
+ return refusal(evaluation.reason || 'aeb_evaluation_refused', { aeb: evaluation.checked ?? null });
493
+ }
494
+ if (!evaluation.checked?.valid || evaluation.checked.execution_authorizing !== true) {
495
+ return refusal('aeb_execution_verification_required', { aeb: evaluation.checked ?? null });
496
+ }
497
+ const preparedGateInput = gateInput(proposal, profile, input.receipt, evaluation.record);
498
+ const preflight = await options.gate.check({ ...preparedGateInput, consumptionMode: 'none' });
499
+ if (preflight.allow !== true) {
500
+ return refusal(preflight.reason || 'gate_refused', { authorization: preflight });
501
+ }
502
+ if (preflight.reason === 'not_guarded' || preflight.requirement?.receipt_required !== true) {
503
+ return refusal('gate_profile_not_receipt_guarded', { authorization: preflight });
504
+ }
505
+ const reservation = await authorizeAebExecutionDurable(evaluation.record, {
506
+ verification: evaluation.checked,
507
+ local_authorization: true,
508
+ store: options.aeb.store,
509
+ });
510
+ if (!reservation.invoke_allowed || !reservation.reservation_key) {
511
+ return refusal(reservation.reason === 'consumption_conflict' ? 'aeb_consumption_conflict' : reservation.reason, { aeb: reservation });
512
+ }
513
+ const key = reservation.reservation_key;
514
+ let attemptId;
515
+ try {
516
+ attemptId = options.consequence.create_attempt_id
517
+ ? await options.consequence.create_attempt_id({
518
+ tenant_id: proposal.consequence.tenant_id,
519
+ request_digest: proposal.consequence.request_digest,
520
+ })
521
+ : `attempt:${crypto.randomUUID()}`;
522
+ assertIdentifier(attemptId, 'proposal_attempt_id');
523
+ }
524
+ catch {
525
+ await options.aeb.store.release(key).catch(() => false);
526
+ return refusal('consequence_attempt_allocation_failed');
527
+ }
528
+ const binding = {
529
+ tenant_id: proposal.consequence.tenant_id,
530
+ provider_id: proposal.consequence.provider_id,
531
+ provider_account_id: proposal.consequence.provider_account_id,
532
+ environment: proposal.consequence.environment,
533
+ attempt_id: attemptId,
534
+ request_digest: proposal.consequence.request_digest,
535
+ };
536
+ let reservedAttempt;
537
+ try {
538
+ reservedAttempt = await options.consequence.store.reserve(clone(binding));
539
+ }
540
+ catch {
541
+ await options.aeb.store.release(key).catch(() => false);
542
+ return refusal('consequence_attempt_store_unavailable');
543
+ }
544
+ if (!reservedAttempt?.reserved || typeof reservedAttempt.owner !== 'string'
545
+ || reservedAttempt.owner.length < 1 || reservedAttempt.owner.length > 1024) {
546
+ await options.aeb.store.release(key).catch(() => false);
547
+ const reason = reservedAttempt.reserved === false
548
+ ? reservedAttempt.reason : 'consequence_attempt_conflict';
549
+ return refusal(reason);
550
+ }
551
+ const attempt = {
552
+ ...binding,
553
+ owner: reservedAttempt.owner,
554
+ };
555
+ const attemptRef = {
556
+ tenant_id: attempt.tenant_id,
557
+ attempt_id: attempt.attempt_id,
558
+ owner: attempt.owner,
559
+ };
560
+ const attemptCustody = { state: 'RESERVED' };
561
+ const transition = async (change) => {
562
+ try {
563
+ const changed = await options.consequence.store.transition({
564
+ ...attemptRef,
565
+ ...change,
566
+ });
567
+ if (changed === true)
568
+ attemptCustody.state = change.next_state;
569
+ return changed === true;
570
+ }
571
+ catch {
572
+ return false;
573
+ }
574
+ };
575
+ if (!await transition({ expected_state: 'RESERVED', next_state: 'INVOKING' })) {
576
+ await options.aeb.store.release(key).catch(() => false);
577
+ return refusal('consequence_attempt_transition_conflict');
578
+ }
579
+ let callbackEntered = false;
580
+ const freeze = async () => (attemptCustody.state === 'INDETERMINATE'
581
+ || (attemptCustody.state === 'INVOKING'
582
+ && await transition({ expected_state: 'INVOKING', next_state: 'INDETERMINATE' })));
583
+ const attachAttempt = (thrown, outcome) => {
584
+ const error = thrown && (typeof thrown === 'object' || typeof thrown === 'function')
585
+ ? thrown : new Error(String(thrown));
586
+ error.proposalToEffect = {
587
+ ...(isPlainObject(error.proposalToEffect) ? error.proposalToEffect : {}),
588
+ ...(outcome ? { outcome } : {}),
589
+ reservation_key: key,
590
+ attempt: clone(binding),
591
+ attempt_state: attemptCustody.state,
592
+ };
593
+ return rememberReconciliationHandle(error, attemptRef);
594
+ };
595
+ try {
596
+ const result = await options.gate.run(preparedGateInput, async (authorization) => {
597
+ callbackEntered = true;
598
+ return effect({
599
+ action: clone(proposal.action),
600
+ proposal: clone(proposal),
601
+ authorization: clone(authorization),
602
+ attempt: clone(binding),
603
+ });
604
+ });
605
+ if (!await freeze()) {
606
+ throw attachAttempt(new Error('consequence_attempt_freeze_failed'), 'indeterminate');
607
+ }
608
+ if (result?.ok !== true) {
609
+ if (callbackEntered) {
610
+ return rememberReconciliationHandle(refusal(result?.authorization?.reason || result?.reason || 'gate_refused', {
611
+ authorization: result?.authorization ?? null,
612
+ consequence: { state: 'INDETERMINATE', attempt: clone(binding) },
613
+ }), attemptRef);
614
+ }
615
+ if (!await transition({ expected_state: 'INDETERMINATE', next_state: 'RELEASED' })) {
616
+ return rememberReconciliationHandle(refusal('consequence_attempt_transition_conflict', {
617
+ consequence: { state: 'INDETERMINATE', attempt: clone(binding) },
618
+ }), attemptRef);
619
+ }
620
+ await options.aeb.store.release(key).catch(() => false);
621
+ return refusal(result?.authorization?.reason || result?.reason || 'gate_refused', {
622
+ authorization: result?.authorization ?? null,
623
+ consequence: { state: 'RELEASED', attempt: clone(binding) },
624
+ });
625
+ }
626
+ if (!callbackEntered) {
627
+ if (await transition({ expected_state: 'INDETERMINATE', next_state: 'RELEASED' })) {
628
+ await options.aeb.store.release(key).catch(() => false);
629
+ }
630
+ const response = refusal('gate_effect_not_invoked', {
631
+ consequence: { state: attemptCustody.state, attempt: clone(binding) },
632
+ });
633
+ return attemptCustody.state === 'INDETERMINATE'
634
+ ? rememberReconciliationHandle(response, attemptRef) : response;
635
+ }
636
+ const committed = await reconcileAebWithRecovery(key, 'COMMITTED');
637
+ if (committed.state !== 'CONSUMED') {
638
+ const error = new Error('aeb_consumption_commit_failed');
639
+ error.code = 'EMILIA_PROPOSAL_TO_EFFECT_COMMIT_FAILED';
640
+ throw attachAttempt(error, 'indeterminate');
641
+ }
642
+ if (!await transition({ expected_state: 'INDETERMINATE', next_state: 'COMMITTED' })) {
643
+ throw attachAttempt(new Error('consequence_attempt_commit_failed'), 'executed');
644
+ }
645
+ return {
646
+ ...result,
647
+ proposal: clone(proposal),
648
+ aeb: committed,
649
+ consequence: { state: 'COMMITTED', attempt: clone(binding) },
650
+ };
651
+ }
652
+ catch (error) {
653
+ const outcome = error?.proposalToEffect?.outcome ?? error?.emiliaGateOutcome?.outcome;
654
+ if (attemptCustody.state === 'INVOKING')
655
+ await freeze();
656
+ if (attemptCustody.state === 'INDETERMINATE' && outcome === 'executed') {
657
+ const committed = await reconcileAebWithRecovery(key, 'COMMITTED');
658
+ if (committed.state === 'CONSUMED') {
659
+ await transition({ expected_state: 'INDETERMINATE', next_state: 'COMMITTED' });
660
+ }
661
+ }
662
+ else if (attemptCustody.state === 'INDETERMINATE' && !callbackEntered && outcome !== 'indeterminate') {
663
+ if (await transition({ expected_state: 'INDETERMINATE', next_state: 'RELEASED' })) {
664
+ await options.aeb.store.release(key).catch(() => false);
665
+ }
666
+ }
667
+ throw attachAttempt(error, callbackEntered ? (outcome || 'indeterminate') : outcome);
668
+ }
669
+ }
670
+ async function reconcile(input) {
671
+ const { proposal } = verifyProposal(input?.proposal, { allowExpired: true });
672
+ if (!isPlainObject(input?.evaluation))
673
+ return refusal('aeb_evaluation_missing');
674
+ const record = input.evaluation;
675
+ if (record.operation_id !== proposal.operation_id
676
+ || record.consumption_nonce !== proposal.aeb.consumption_nonce
677
+ || record.initiator_id !== proposal.initiator_id
678
+ || record.executor_id !== proposal.consequence.executor_id
679
+ || record.requirement_ref !== proposal.aeb.requirement_ref
680
+ || record.caid !== proposal.caid) {
681
+ return refusal('aeb_evaluation_binding_mismatch');
682
+ }
683
+ if (record.composition?.action_digest !== expectedAebCompositionDigest(proposal)) {
684
+ return refusal('aeb_evaluation_binding_mismatch');
685
+ }
686
+ let artifacts;
687
+ try {
688
+ artifacts = await options.aeb.resolve_artifacts({ proposal: clone(proposal), evaluation: clone(record) });
689
+ }
690
+ catch {
691
+ return refusal('aeb_artifact_resolution_failed');
692
+ }
693
+ const historicalOptions = {
694
+ mode: 'historical',
695
+ config: options.aeb.config,
696
+ adapters: options.aeb.adapters,
697
+ artifacts,
698
+ expected_action: clone(proposal.action),
699
+ executor_id: proposal.consequence.executor_id,
700
+ };
701
+ const historical = verifyAebEvaluation(record, historicalOptions);
702
+ if (!historical.valid)
703
+ return refusal('aeb_evaluation_refused', { aeb: historical });
704
+ if (record.verdict !== 'SATISFIED'
705
+ || record.authority_constraints?.one_time_consumption !== true) {
706
+ return refusal('aeb_evaluation_refused', { aeb: historical });
707
+ }
708
+ if (!isPlainObject(input?.attempt)
709
+ || input.attempt.tenant_id !== proposal.consequence.tenant_id
710
+ || typeof input.attempt.attempt_id !== 'string'
711
+ || !IDENTIFIER_PATTERN.test(input.attempt.attempt_id)
712
+ || typeof input.attempt.owner !== 'string'
713
+ || input.attempt.owner.length < 1 || input.attempt.owner.length > 1024) {
714
+ return refusal('consequence_attempt_binding_mismatch');
715
+ }
716
+ const attemptRef = {
717
+ tenant_id: proposal.consequence.tenant_id,
718
+ attempt_id: input.attempt.attempt_id,
719
+ owner: input.attempt.owner,
720
+ };
721
+ const publicAttempt = {
722
+ tenant_id: proposal.consequence.tenant_id,
723
+ provider_id: proposal.consequence.provider_id,
724
+ provider_account_id: proposal.consequence.provider_account_id,
725
+ environment: proposal.consequence.environment,
726
+ attempt_id: attemptRef.attempt_id,
727
+ request_digest: proposal.consequence.request_digest,
728
+ };
729
+ let provider;
730
+ try {
731
+ provider = await options.aeb.verify_provider_evidence({
732
+ evidence: clone(input.provider_evidence),
733
+ expected: {
734
+ operation_id: proposal.operation_id,
735
+ caid: proposal.caid,
736
+ action_digest: proposal.aeb_action_digest,
737
+ tenant_id: proposal.consequence.tenant_id,
738
+ request_digest: proposal.consequence.request_digest,
739
+ provider_id: proposal.consequence.provider_id,
740
+ provider_account_id: proposal.consequence.provider_account_id,
741
+ environment: proposal.consequence.environment,
742
+ attempt_id: attemptRef.attempt_id,
743
+ },
744
+ });
745
+ }
746
+ catch {
747
+ return refusal('provider_evidence_unverified');
748
+ }
749
+ const observedAtMs = typeof provider?.observed_at === 'string'
750
+ ? Date.parse(provider.observed_at) : NaN;
751
+ const proposalCreatedAtMs = Date.parse(proposal.created_at);
752
+ if (!provider?.valid || !['COMMITTED', 'NOT_COMMITTED', 'ESCALATED'].includes(provider.outcome ?? '')
753
+ || typeof provider.evidence_id !== 'string' || !IDENTIFIER_PATTERN.test(provider.evidence_id)
754
+ || !Number.isFinite(observedAtMs) || observedAtMs < proposalCreatedAtMs
755
+ || observedAtMs > currentTime()
756
+ || !validDigest(provider.evidence_digest)) {
757
+ return refusal(provider?.reason || 'provider_evidence_unverified');
758
+ }
759
+ if (provider.tenant_id !== proposal.consequence.tenant_id
760
+ || provider.request_digest !== proposal.consequence.request_digest
761
+ || provider.provider_id !== proposal.consequence.provider_id
762
+ || provider.provider_account_id !== proposal.consequence.provider_account_id
763
+ || provider.environment !== proposal.consequence.environment
764
+ || provider.attempt_id !== attemptRef.attempt_id
765
+ || provider.operation_id !== proposal.operation_id
766
+ || provider.caid !== proposal.caid
767
+ || provider.action_digest !== proposal.aeb_action_digest) {
768
+ return refusal('provider_evidence_binding_mismatch');
769
+ }
770
+ const providerOutcome = provider.outcome;
771
+ const evidence = {
772
+ operation_id: proposal.operation_id,
773
+ caid: proposal.caid,
774
+ action_digest: proposal.aeb_action_digest,
775
+ tenant_id: proposal.consequence.tenant_id,
776
+ provider_id: proposal.consequence.provider_id,
777
+ provider_account_id: proposal.consequence.provider_account_id,
778
+ environment: proposal.consequence.environment,
779
+ attempt_id: attemptRef.attempt_id,
780
+ request_digest: proposal.consequence.request_digest,
781
+ evidence_id: provider.evidence_id,
782
+ observed_at: provider.observed_at,
783
+ outcome: providerOutcome,
784
+ evidence_digest: provider.evidence_digest,
785
+ };
786
+ const terminalState = providerOutcome === 'COMMITTED'
787
+ ? 'COMMITTED' : providerOutcome === 'NOT_COMMITTED' ? 'RELEASED' : 'ESCALATED';
788
+ const key = aebReservationKey(record);
789
+ // For a committed real-world effect, consume replay authority before the
790
+ // attempt becomes terminal. A failed AEB commit leaves the attempt
791
+ // INDETERMINATE and therefore repairable, never terminal split-brain.
792
+ const preTerminalAeb = providerOutcome === 'COMMITTED'
793
+ ? await reconcileAebWithRecovery(key, 'COMMITTED', input.aeb_recovery_authorization)
794
+ : null;
795
+ if (providerOutcome === 'COMMITTED' && preTerminalAeb?.state !== 'CONSUMED') {
796
+ return refusal('aeb_consumption_reconciliation_failed', {
797
+ state: 'INDETERMINATE',
798
+ consequence: { state: 'INDETERMINATE', attempt: clone(publicAttempt) },
799
+ aeb: preTerminalAeb,
800
+ });
801
+ }
802
+ let attemptReconciled = false;
803
+ try {
804
+ attemptReconciled = await options.consequence.store.reconcile({
805
+ ...attemptRef,
806
+ expected_state: 'INDETERMINATE',
807
+ next_state: terminalState,
808
+ evidence: clone(evidence),
809
+ });
810
+ }
811
+ catch {
812
+ return refusal('consequence_attempt_store_unavailable');
813
+ }
814
+ if (!attemptReconciled)
815
+ return refusal('consequence_attempt_not_indeterminate');
816
+ const reconciled = providerOutcome === 'ESCALATED'
817
+ ? { state: 'RECONCILIATION_REQUIRED', retry_allowed: false, reason: 'execution_escalated' }
818
+ : providerOutcome === 'COMMITTED'
819
+ ? preTerminalAeb
820
+ : await reconcileAebWithRecovery(key, providerOutcome, input.aeb_recovery_authorization);
821
+ if (providerOutcome !== 'ESCALATED' && reconciled.state === 'RECONCILIATION_REQUIRED') {
822
+ return refusal('aeb_consumption_reconciliation_failed', {
823
+ state: terminalState,
824
+ consequence: { state: terminalState, attempt: clone(publicAttempt) },
825
+ aeb: reconciled,
826
+ });
827
+ }
828
+ return {
829
+ ok: true,
830
+ state: terminalState,
831
+ outcome: providerOutcome,
832
+ evidence_digest: provider.evidence_digest,
833
+ reservation_key: key,
834
+ consequence: { state: terminalState, attempt: clone(publicAttempt) },
835
+ aeb: reconciled,
836
+ };
837
+ }
838
+ /**
839
+ * Converge legacy or crash-window terminal consequence state with the AEB
840
+ * reservation. This never invokes an effect and trusts only the signed
841
+ * proposal/evaluation plus the durable consequence store's terminal state.
842
+ */
843
+ async function repairAeb(input) {
844
+ const { proposal } = verifyProposal(input?.proposal, { allowExpired: true });
845
+ if (!isPlainObject(input?.evaluation))
846
+ return refusal('aeb_evaluation_missing');
847
+ const record = input.evaluation;
848
+ if (record.operation_id !== proposal.operation_id
849
+ || record.consumption_nonce !== proposal.aeb.consumption_nonce
850
+ || record.initiator_id !== proposal.initiator_id
851
+ || record.executor_id !== proposal.consequence.executor_id
852
+ || record.requirement_ref !== proposal.aeb.requirement_ref
853
+ || record.caid !== proposal.caid
854
+ || record.composition?.action_digest !== expectedAebCompositionDigest(proposal)) {
855
+ return refusal('aeb_evaluation_binding_mismatch');
856
+ }
857
+ let artifacts;
858
+ try {
859
+ artifacts = await options.aeb.resolve_artifacts({ proposal: clone(proposal), evaluation: clone(record) });
860
+ }
861
+ catch {
862
+ return refusal('aeb_artifact_resolution_failed');
863
+ }
864
+ const historicalOptions = {
865
+ mode: 'historical',
866
+ config: options.aeb.config,
867
+ adapters: options.aeb.adapters,
868
+ artifacts,
869
+ expected_action: clone(proposal.action),
870
+ executor_id: proposal.consequence.executor_id,
871
+ };
872
+ const historical = verifyAebEvaluation(record, historicalOptions);
873
+ if (!historical.valid || record.verdict !== 'SATISFIED'
874
+ || record.authority_constraints?.one_time_consumption !== true) {
875
+ return refusal('aeb_evaluation_refused', { aeb: historical });
876
+ }
877
+ if (!exactKeys(input?.attempt, [
878
+ 'tenant_id', 'provider_id', 'provider_account_id', 'environment',
879
+ 'attempt_id', 'request_digest',
880
+ ])) {
881
+ return refusal('consequence_attempt_binding_mismatch');
882
+ }
883
+ const attempt = input.attempt;
884
+ if (attempt.tenant_id !== proposal.consequence.tenant_id
885
+ || attempt.provider_id !== proposal.consequence.provider_id
886
+ || attempt.provider_account_id !== proposal.consequence.provider_account_id
887
+ || attempt.environment !== proposal.consequence.environment
888
+ || attempt.request_digest !== proposal.consequence.request_digest
889
+ || typeof attempt.attempt_id !== 'string' || !IDENTIFIER_PATTERN.test(attempt.attempt_id)) {
890
+ return refusal('consequence_attempt_binding_mismatch');
891
+ }
892
+ let snapshot;
893
+ try {
894
+ snapshot = await options.consequence.store.read(clone(attempt));
895
+ }
896
+ catch {
897
+ return refusal('consequence_attempt_store_unavailable');
898
+ }
899
+ if (!snapshot)
900
+ return refusal('consequence_attempt_not_found');
901
+ if (!['COMMITTED', 'RELEASED', 'ESCALATED'].includes(snapshot.state)) {
902
+ return refusal('consequence_attempt_not_terminal', { state: snapshot.state });
903
+ }
904
+ if (snapshot.state === 'ESCALATED') {
905
+ return {
906
+ ok: true,
907
+ state: 'ESCALATED',
908
+ consequence: { state: 'ESCALATED', attempt: clone(attempt) },
909
+ aeb: { state: 'RECONCILIATION_REQUIRED', retry_allowed: false, reason: 'execution_escalated' },
910
+ };
911
+ }
912
+ const key = aebReservationKey(record);
913
+ const outcome = snapshot.state === 'COMMITTED' ? 'COMMITTED' : 'NOT_COMMITTED';
914
+ const repaired = await reconcileAebWithRecovery(key, outcome, input.aeb_recovery_authorization);
915
+ const expectedState = outcome === 'COMMITTED' ? 'CONSUMED' : 'AVAILABLE';
916
+ if (repaired.state !== expectedState) {
917
+ return refusal('aeb_consumption_repair_failed', {
918
+ state: snapshot.state,
919
+ consequence: { state: snapshot.state, attempt: clone(attempt) },
920
+ aeb: repaired,
921
+ });
922
+ }
923
+ return {
924
+ ok: true,
925
+ state: snapshot.state,
926
+ consequence: { state: snapshot.state, attempt: clone(attempt) },
927
+ reservation_key: key,
928
+ aeb: repaired,
929
+ };
930
+ }
931
+ async function beginApproval(input) {
932
+ const { proposal, profile } = verifyProposal(input?.proposal);
933
+ return beginReceiptApproval({
934
+ authorization: proposal.authorization,
935
+ trustedAuthorization: profile.authorization,
936
+ challenge: proposal.challenge,
937
+ action: proposal.action,
938
+ approver_id: input.approver_id,
939
+ idempotency_key: input.idempotency_key,
940
+ requesterAuthorization: input.requester_authorization,
941
+ fetchImpl: input.fetch_impl,
942
+ });
943
+ }
944
+ async function pollApproval(input) {
945
+ const { proposal, profile } = verifyProposal(input?.proposal, { allowExpired: true });
946
+ return pollReceiptApproval({
947
+ authorization: proposal.authorization,
948
+ trustedAuthorization: profile.authorization,
949
+ request_id: input.request_id,
950
+ poll_token: input.poll_token,
951
+ fetchImpl: input.fetch_impl,
952
+ });
953
+ }
954
+ return Object.freeze({
955
+ prepare,
956
+ verifyProposal,
957
+ beginApproval,
958
+ pollApproval,
959
+ execute,
960
+ reconcile,
961
+ repairAeb,
962
+ getReconciliationHandle,
963
+ });
964
+ }
965
+ export default {
966
+ PROPOSAL_TO_EFFECT_VERSION,
967
+ proposalToEffectConsumptionNonce,
968
+ createProposalToEffect,
969
+ };
970
+ //# sourceMappingURL=proposal-to-effect.js.map