@voyantjs/action-ledger 0.52.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.
@@ -0,0 +1,1025 @@
1
+ import { newId } from "@voyantjs/db";
2
+ import { and, desc, eq, gte, inArray, lt, lte, or, sql } from "drizzle-orm";
3
+ import { actionApprovals, actionDelegations, actionLedgerEntries, actionLedgerPayloads, actionLedgerRelayOutbox, actionMutationDetails, actionSensitiveReadDetails, } from "./schema.js";
4
+ const DEFAULT_LIST_LIMIT = 50;
5
+ const MAX_LIST_LIMIT = 200;
6
+ export class ActionLedgerIdempotencyConflictError extends Error {
7
+ existingActionId;
8
+ constructor(existingActionId) {
9
+ super("Action ledger idempotency key was reused with a different fingerprint");
10
+ this.name = "ActionLedgerIdempotencyConflictError";
11
+ this.existingActionId = existingActionId;
12
+ }
13
+ }
14
+ export class ActionApprovalDecisionConflictError extends Error {
15
+ approvalId;
16
+ currentStatus;
17
+ constructor(approvalId, currentStatus) {
18
+ super(`Action approval ${approvalId} is already ${currentStatus}`);
19
+ this.name = "ActionApprovalDecisionConflictError";
20
+ this.approvalId = approvalId;
21
+ this.currentStatus = currentStatus;
22
+ }
23
+ }
24
+ export class ActionApprovalDecisionStatusError extends Error {
25
+ status;
26
+ constructor(status) {
27
+ super(`Action approval decision status must be terminal, received ${status}`);
28
+ this.name = "ActionApprovalDecisionStatusError";
29
+ this.status = status;
30
+ }
31
+ }
32
+ export class ActionLedgerReversalTargetError extends Error {
33
+ actionId;
34
+ reason;
35
+ constructor(actionId, reason) {
36
+ super(`Action ledger entry ${actionId} cannot be reversed: ${reason}`);
37
+ this.name = "ActionLedgerReversalTargetError";
38
+ this.actionId = actionId;
39
+ this.reason = reason;
40
+ }
41
+ }
42
+ const approvalDecisionStatusValues = [
43
+ "approved",
44
+ "denied",
45
+ "expired",
46
+ "cancelled",
47
+ "superseded",
48
+ ];
49
+ const approvalDecisionStatusSet = new Set(approvalDecisionStatusValues);
50
+ export const actionLedgerService = {
51
+ async appendEntry(db, input) {
52
+ const existing = await findExistingIdempotentEntry(db, input);
53
+ if (existing) {
54
+ assertSameFingerprint(existing, input.idempotencyFingerprint ?? null);
55
+ return { entry: existing, replayed: true };
56
+ }
57
+ try {
58
+ return await withOptionalTransaction(db, (tx) => insertEntry(tx, input));
59
+ }
60
+ catch (error) {
61
+ const racedExisting = await findExistingIdempotentEntry(db, input);
62
+ if (racedExisting) {
63
+ assertSameFingerprint(racedExisting, input.idempotencyFingerprint ?? null);
64
+ return { entry: racedExisting, replayed: true };
65
+ }
66
+ throw error;
67
+ }
68
+ },
69
+ async requestApproval(db, input) {
70
+ return withOptionalTransaction(db, async (tx) => {
71
+ const approvalId = newId("action_approvals");
72
+ const requestedActionResult = await actionLedgerService.appendEntry(tx, {
73
+ ...input.requestedAction,
74
+ status: "awaiting_approval",
75
+ approvalId,
76
+ });
77
+ if (!requestedActionResult.entry.approvalId) {
78
+ throw new Error("Action approval requested action is missing its approval id");
79
+ }
80
+ const existingApproval = await findApprovalForRequestedAction(tx, requestedActionResult.entry.id);
81
+ if (existingApproval) {
82
+ return {
83
+ requestedAction: requestedActionResult.entry,
84
+ approval: existingApproval,
85
+ replayed: requestedActionResult.replayed,
86
+ };
87
+ }
88
+ const [approval] = await tx
89
+ .insert(actionApprovals)
90
+ .values({
91
+ id: requestedActionResult.entry.approvalId,
92
+ requestedActionId: requestedActionResult.entry.id,
93
+ status: "pending",
94
+ requestedByPrincipalId: input.approval.requestedByPrincipalId ?? requestedActionResult.entry.principalId,
95
+ assignedToPrincipalId: input.approval.assignedToPrincipalId ?? null,
96
+ delegatedFromPrincipalId: input.approval.delegatedFromPrincipalId ?? null,
97
+ policyName: input.approval.policyName,
98
+ policyVersion: input.approval.policyVersion,
99
+ targetSnapshotRef: input.approval.targetSnapshotRef ?? null,
100
+ riskSnapshot: input.approval.riskSnapshot ?? requestedActionResult.entry.evaluatedRisk,
101
+ reasonCode: input.approval.reasonCode ?? null,
102
+ expiresAt: input.approval.expiresAt ? parseCursorDate(input.approval.expiresAt) : null,
103
+ })
104
+ .returning();
105
+ if (!approval) {
106
+ throw new Error("Action approval insert did not return an approval");
107
+ }
108
+ return {
109
+ requestedAction: requestedActionResult.entry,
110
+ approval,
111
+ replayed: requestedActionResult.replayed,
112
+ };
113
+ });
114
+ },
115
+ async decideApproval(db, input) {
116
+ assertApprovalDecisionStatus(input.status);
117
+ return withOptionalTransaction(db, async (tx) => {
118
+ const approval = await findApprovalById(tx, input.id);
119
+ if (!approval)
120
+ return null;
121
+ if (approval.status !== "pending") {
122
+ throw new ActionApprovalDecisionConflictError(approval.id, approval.status);
123
+ }
124
+ const decidedAt = input.decidedAt ? parseCursorDate(input.decidedAt) : new Date();
125
+ const [updatedApproval] = await tx
126
+ .update(actionApprovals)
127
+ .set({
128
+ status: input.status,
129
+ decidedByPrincipalId: input.decidedByPrincipalId,
130
+ decidedAt,
131
+ })
132
+ .where(and(eq(actionApprovals.id, input.id), eq(actionApprovals.status, "pending")))
133
+ .returning();
134
+ if (!updatedApproval) {
135
+ const current = await findApprovalById(tx, input.id);
136
+ if (!current)
137
+ return null;
138
+ throw new ActionApprovalDecisionConflictError(current.id, current.status);
139
+ }
140
+ const decisionAction = await actionLedgerService.appendEntry(tx, {
141
+ ...input.decisionAction,
142
+ actionKind: input.status === "approved" ? "approve" : "reject",
143
+ status: input.status,
144
+ evaluatedRisk: input.decisionAction.evaluatedRisk ?? updatedApproval.riskSnapshot,
145
+ targetType: input.decisionAction.targetType ?? "action_approval",
146
+ targetId: input.decisionAction.targetId ?? updatedApproval.id,
147
+ causationActionId: updatedApproval.requestedActionId,
148
+ approvalId: updatedApproval.id,
149
+ });
150
+ return {
151
+ approval: updatedApproval,
152
+ decisionAction: decisionAction.entry,
153
+ };
154
+ });
155
+ },
156
+ async recordReversal(db, input) {
157
+ return withOptionalTransaction(db, async (tx) => {
158
+ const original = await actionLedgerService.getEntry(tx, input.originalActionId);
159
+ if (!original)
160
+ return null;
161
+ if (!original.mutationDetail) {
162
+ throw new ActionLedgerReversalTargetError(input.originalActionId, "missing_mutation_detail");
163
+ }
164
+ if (original.mutationDetail.reversalKind === "none") {
165
+ throw new ActionLedgerReversalTargetError(input.originalActionId, "not_reversible");
166
+ }
167
+ const reversalResult = await actionLedgerService.appendEntry(tx, {
168
+ ...input.reversalAction,
169
+ causationActionId: original.entry.id,
170
+ mutationDetail: {
171
+ commandInputRef: input.reversalAction.mutationDetail?.commandInputRef ?? null,
172
+ commandResultRef: input.reversalAction.mutationDetail?.commandResultRef ?? null,
173
+ summary: input.reversalAction.mutationDetail?.summary ?? null,
174
+ reversalKind: input.reversalAction.mutationDetail?.reversalKind ?? "none",
175
+ reversalCommandId: input.reversalAction.mutationDetail?.reversalCommandId ?? null,
176
+ reversalCommandVersion: input.reversalAction.mutationDetail?.reversalCommandVersion ?? null,
177
+ reversalArgsRef: input.reversalAction.mutationDetail?.reversalArgsRef ?? null,
178
+ reversalStateProjection: input.reversalAction.mutationDetail?.reversalStateProjection ?? null,
179
+ reversalOutcomeProjection: input.reversalAction.mutationDetail?.reversalOutcomeProjection ?? null,
180
+ reversesActionId: original.entry.id,
181
+ reversedByActionIdProjection: input.reversalAction.mutationDetail?.reversedByActionIdProjection ?? null,
182
+ },
183
+ });
184
+ const defaultProjection = input.reversalAction.status === "failed"
185
+ ? { reversalState: "failed", reversalOutcome: "failed" }
186
+ : { reversalState: "completed", reversalOutcome: "full" };
187
+ await tx
188
+ .update(actionMutationDetails)
189
+ .set({
190
+ reversalStateProjection: input.projection?.reversalState ?? defaultProjection.reversalState,
191
+ reversalOutcomeProjection: input.projection?.reversalOutcome ?? defaultProjection.reversalOutcome,
192
+ reversedByActionIdProjection: reversalResult.entry.id,
193
+ })
194
+ .where(eq(actionMutationDetails.actionId, original.entry.id));
195
+ return {
196
+ originalAction: original.entry,
197
+ originalMutationDetail: original.mutationDetail,
198
+ reversalAction: reversalResult.entry,
199
+ replayed: reversalResult.replayed,
200
+ };
201
+ });
202
+ },
203
+ async listEntries(db, input = {}) {
204
+ const limit = normalizeListLimit(input.limit);
205
+ const predicate = buildActionLedgerEntriesPredicate(input);
206
+ let query = db.select().from(actionLedgerEntries).$dynamic();
207
+ if (predicate) {
208
+ query = query.where(predicate);
209
+ }
210
+ const rows = await query
211
+ .orderBy(desc(actionLedgerEntries.occurredAt), desc(actionLedgerEntries.id))
212
+ .limit(limit + 1);
213
+ const entries = rows.slice(0, limit);
214
+ return {
215
+ entries,
216
+ nextCursor: rows.length > limit && entries.length > 0
217
+ ? toActionLedgerListCursor(entries[entries.length - 1])
218
+ : null,
219
+ };
220
+ },
221
+ async listRelayOutbox(db, input = {}) {
222
+ const limit = normalizeListLimit(input.limit);
223
+ const predicate = buildActionLedgerRelayOutboxPredicate(input);
224
+ let query = db.select().from(actionLedgerRelayOutbox).$dynamic();
225
+ if (predicate) {
226
+ query = query.where(predicate);
227
+ }
228
+ const rows = await query
229
+ .orderBy(desc(actionLedgerRelayOutbox.createdAt), desc(actionLedgerRelayOutbox.id))
230
+ .limit(limit + 1);
231
+ const visibleRows = rows.slice(0, limit);
232
+ return {
233
+ rows: visibleRows,
234
+ nextCursor: rows.length > limit && visibleRows.length > 0
235
+ ? toActionLedgerRelayOutboxListCursor(visibleRows[visibleRows.length - 1])
236
+ : null,
237
+ };
238
+ },
239
+ async listApprovals(db, input = {}) {
240
+ const limit = normalizeListLimit(input.limit);
241
+ const predicate = buildActionApprovalsPredicate(input);
242
+ let query = db.select().from(actionApprovals).$dynamic();
243
+ if (predicate) {
244
+ query = query.where(predicate);
245
+ }
246
+ const rows = await query
247
+ .orderBy(desc(actionApprovals.createdAt), desc(actionApprovals.id))
248
+ .limit(limit + 1);
249
+ const approvals = rows.slice(0, limit);
250
+ return {
251
+ approvals,
252
+ nextCursor: rows.length > limit && approvals.length > 0
253
+ ? toActionApprovalListCursor(approvals[approvals.length - 1])
254
+ : null,
255
+ };
256
+ },
257
+ async listDelegations(db, input = {}) {
258
+ const limit = normalizeListLimit(input.limit);
259
+ const predicate = buildActionDelegationsPredicate(input);
260
+ let query = db.select().from(actionDelegations).$dynamic();
261
+ if (predicate) {
262
+ query = query.where(predicate);
263
+ }
264
+ const rows = await query
265
+ .orderBy(desc(actionDelegations.createdAt), desc(actionDelegations.id))
266
+ .limit(limit + 1);
267
+ const delegations = rows.slice(0, limit);
268
+ return {
269
+ delegations,
270
+ nextCursor: rows.length > limit && delegations.length > 0
271
+ ? toActionDelegationListCursor(delegations[delegations.length - 1])
272
+ : null,
273
+ };
274
+ },
275
+ async getApproval(db, id) {
276
+ const [approval] = await db
277
+ .select()
278
+ .from(actionApprovals)
279
+ .where(eq(actionApprovals.id, id))
280
+ .limit(1);
281
+ if (!approval)
282
+ return null;
283
+ return {
284
+ approval,
285
+ requestedAction: await actionLedgerService.getEntry(db, approval.requestedActionId),
286
+ };
287
+ },
288
+ async validateApprovedAction(db, input) {
289
+ const result = await actionLedgerService.getApproval(db, input.approvalId);
290
+ if (!result) {
291
+ return { ok: false, reason: "not_found" };
292
+ }
293
+ if (result.approval.status !== "approved") {
294
+ return {
295
+ ok: false,
296
+ reason: "not_approved",
297
+ approval: result.approval,
298
+ status: result.approval.status,
299
+ };
300
+ }
301
+ const now = input.now ? parseCursorDate(input.now) : new Date();
302
+ if (result.approval.expiresAt && result.approval.expiresAt < now) {
303
+ return {
304
+ ok: false,
305
+ reason: "expired",
306
+ approval: result.approval,
307
+ };
308
+ }
309
+ const requestedAction = result.requestedAction?.entry;
310
+ if (!requestedAction ||
311
+ requestedAction.actionName !== input.actionName ||
312
+ requestedAction.actionVersion !== input.actionVersion ||
313
+ (input.requestedActionKind && requestedAction.actionKind !== input.requestedActionKind) ||
314
+ (input.requestedActionStatus &&
315
+ !(Array.isArray(input.requestedActionStatus)
316
+ ? input.requestedActionStatus
317
+ : [input.requestedActionStatus]).includes(requestedAction.status)) ||
318
+ requestedAction.targetType !== input.targetType ||
319
+ requestedAction.targetId !== input.targetId ||
320
+ requestedAction.routeOrToolName !== (input.routeOrToolName ?? null) ||
321
+ requestedAction.approvalId !== result.approval.id) {
322
+ return {
323
+ ok: false,
324
+ reason: "mismatched_action",
325
+ approval: result.approval,
326
+ requestedAction: requestedAction ?? undefined,
327
+ };
328
+ }
329
+ const existingExecution = await actionLedgerService.listEntries(db, {
330
+ actionName: input.actionName,
331
+ actionKind: input.executionActionKind,
332
+ targetType: input.targetType,
333
+ targetId: input.targetId,
334
+ causationActionId: requestedAction.id,
335
+ approvalId: result.approval.id,
336
+ status: input.executionStatus ?? "succeeded",
337
+ limit: 1,
338
+ });
339
+ if (existingExecution.entries.length > 0) {
340
+ return {
341
+ ok: false,
342
+ reason: "already_executed",
343
+ approval: result.approval,
344
+ requestedAction,
345
+ existingActionId: existingExecution.entries[0]?.id,
346
+ };
347
+ }
348
+ if (!requestedAction.idempotencyFingerprint) {
349
+ return {
350
+ ok: false,
351
+ reason: "missing_fingerprint",
352
+ approval: result.approval,
353
+ requestedAction,
354
+ };
355
+ }
356
+ if (input.idempotencyFingerprint !== requestedAction.idempotencyFingerprint) {
357
+ return {
358
+ ok: false,
359
+ reason: "fingerprint_mismatch",
360
+ approval: result.approval,
361
+ requestedAction,
362
+ };
363
+ }
364
+ if (input.principalType &&
365
+ input.principalId &&
366
+ (requestedAction.principalType !== input.principalType ||
367
+ requestedAction.principalId !== input.principalId)) {
368
+ return {
369
+ ok: false,
370
+ reason: "principal_mismatch",
371
+ approval: result.approval,
372
+ requestedAction,
373
+ };
374
+ }
375
+ return {
376
+ ok: true,
377
+ approval: result.approval,
378
+ requestedAction,
379
+ idempotencyFingerprint: requestedAction.idempotencyFingerprint,
380
+ };
381
+ },
382
+ async getDelegation(db, id) {
383
+ const [delegation] = await db
384
+ .select()
385
+ .from(actionDelegations)
386
+ .where(eq(actionDelegations.id, id))
387
+ .limit(1);
388
+ if (!delegation)
389
+ return null;
390
+ return { delegation };
391
+ },
392
+ async claimRelayOutbox(db, input = {}) {
393
+ const limit = normalizeListLimit(input.limit);
394
+ const dueAt = input.dueAt ? parseCursorDate(input.dueAt) : new Date();
395
+ const organizationId = input.organizationId ?? null;
396
+ const result = await db.execute(sql `
397
+ WITH due AS (
398
+ SELECT id
399
+ FROM action_ledger_outbox
400
+ WHERE relay_status IN ('pending', 'failed')
401
+ AND (${organizationId}::text IS NULL OR organization_id = ${organizationId})
402
+ AND (next_retry_at IS NULL OR next_retry_at <= ${dueAt})
403
+ ORDER BY created_at ASC, id ASC
404
+ LIMIT ${limit}
405
+ FOR UPDATE SKIP LOCKED
406
+ )
407
+ UPDATE action_ledger_outbox AS outbox
408
+ SET relay_status = 'processing',
409
+ attempt_count = outbox.attempt_count + 1,
410
+ last_error = NULL,
411
+ processed_at = NULL
412
+ FROM due
413
+ WHERE outbox.id = due.id
414
+ RETURNING
415
+ outbox.id,
416
+ outbox.action_id,
417
+ outbox.organization_id,
418
+ outbox.relay_status,
419
+ outbox.payload_ref,
420
+ outbox.attempt_count,
421
+ outbox.next_retry_at,
422
+ outbox.last_error,
423
+ outbox.created_at,
424
+ outbox.processed_at
425
+ `);
426
+ const rows = ("rows" in result ? result.rows : result);
427
+ return {
428
+ rows: rows.map(actionLedgerRelayOutboxFromSqlRow),
429
+ };
430
+ },
431
+ async markRelayOutboxSucceeded(db, input) {
432
+ const [row] = await db
433
+ .update(actionLedgerRelayOutbox)
434
+ .set({
435
+ relayStatus: "succeeded",
436
+ nextRetryAt: null,
437
+ lastError: null,
438
+ processedAt: input.processedAt ? parseCursorDate(input.processedAt) : new Date(),
439
+ })
440
+ .where(and(eq(actionLedgerRelayOutbox.id, input.id), eq(actionLedgerRelayOutbox.relayStatus, "processing")))
441
+ .returning();
442
+ return row ?? null;
443
+ },
444
+ async markRelayOutboxFailed(db, input) {
445
+ const deadLetter = input.deadLetter ?? false;
446
+ const [row] = await db
447
+ .update(actionLedgerRelayOutbox)
448
+ .set({
449
+ relayStatus: deadLetter ? "dead_letter" : "failed",
450
+ nextRetryAt: deadLetter
451
+ ? null
452
+ : input.nextRetryAt
453
+ ? parseCursorDate(input.nextRetryAt)
454
+ : null,
455
+ lastError: input.lastError,
456
+ processedAt: deadLetter
457
+ ? input.processedAt
458
+ ? parseCursorDate(input.processedAt)
459
+ : new Date()
460
+ : null,
461
+ })
462
+ .where(and(eq(actionLedgerRelayOutbox.id, input.id), eq(actionLedgerRelayOutbox.relayStatus, "processing")))
463
+ .returning();
464
+ return row ?? null;
465
+ },
466
+ async getEntry(db, id) {
467
+ const [entry] = await db
468
+ .select()
469
+ .from(actionLedgerEntries)
470
+ .where(eq(actionLedgerEntries.id, id))
471
+ .limit(1);
472
+ if (!entry)
473
+ return null;
474
+ const [[mutationDetail], [sensitiveReadDetail], payloads, relayOutbox] = await Promise.all([
475
+ db
476
+ .select()
477
+ .from(actionMutationDetails)
478
+ .where(eq(actionMutationDetails.actionId, id))
479
+ .limit(1),
480
+ db
481
+ .select()
482
+ .from(actionSensitiveReadDetails)
483
+ .where(eq(actionSensitiveReadDetails.actionId, id))
484
+ .limit(1),
485
+ db.select().from(actionLedgerPayloads).where(eq(actionLedgerPayloads.actionId, id)),
486
+ db.select().from(actionLedgerRelayOutbox).where(eq(actionLedgerRelayOutbox.actionId, id)),
487
+ ]);
488
+ return {
489
+ entry,
490
+ mutationDetail: mutationDetail ?? null,
491
+ sensitiveReadDetail: sensitiveReadDetail ?? null,
492
+ payloads,
493
+ relayOutbox,
494
+ };
495
+ },
496
+ };
497
+ const activeTransactionDbs = new WeakSet();
498
+ function withOptionalTransaction(db, callback) {
499
+ if (activeTransactionDbs.has(db)) {
500
+ return callback(db);
501
+ }
502
+ const maybeTransactional = db;
503
+ if (typeof maybeTransactional.transaction === "function") {
504
+ return maybeTransactional.transaction(async (tx) => {
505
+ activeTransactionDbs.add(tx);
506
+ try {
507
+ return await callback(tx);
508
+ }
509
+ finally {
510
+ activeTransactionDbs.delete(tx);
511
+ }
512
+ });
513
+ }
514
+ return callback(db);
515
+ }
516
+ function actionLedgerRelayOutboxFromSqlRow(row) {
517
+ return {
518
+ id: row.id,
519
+ actionId: row.action_id,
520
+ organizationId: row.organization_id,
521
+ relayStatus: row.relay_status,
522
+ payloadRef: row.payload_ref,
523
+ attemptCount: Number(row.attempt_count),
524
+ nextRetryAt: row.next_retry_at ? parseCursorDate(row.next_retry_at) : null,
525
+ lastError: row.last_error,
526
+ createdAt: parseCursorDate(row.created_at),
527
+ processedAt: row.processed_at ? parseCursorDate(row.processed_at) : null,
528
+ };
529
+ }
530
+ async function insertEntry(db, input) {
531
+ const { enqueueRelay, mutationDetail, payloads, sensitiveReadDetail, ...entryInput } = input;
532
+ const [entry] = await db
533
+ .insert(actionLedgerEntries)
534
+ .values({
535
+ ...entryInput,
536
+ occurredAt: input.occurredAt,
537
+ })
538
+ .returning();
539
+ if (!entry) {
540
+ throw new Error("Action ledger insert did not return an entry");
541
+ }
542
+ if (mutationDetail) {
543
+ await db.insert(actionMutationDetails).values({
544
+ actionId: entry.id,
545
+ ...mutationDetail,
546
+ });
547
+ }
548
+ if (sensitiveReadDetail) {
549
+ await db.insert(actionSensitiveReadDetails).values({
550
+ actionId: entry.id,
551
+ ...sensitiveReadDetail,
552
+ });
553
+ }
554
+ if (payloads && payloads.length > 0) {
555
+ await db.insert(actionLedgerPayloads).values(payloads.map((payload) => ({
556
+ actionId: entry.id,
557
+ ...payload,
558
+ })));
559
+ }
560
+ if (enqueueRelay) {
561
+ const payloadRef = typeof enqueueRelay === "object" ? enqueueRelay.payloadRef : null;
562
+ await db.insert(actionLedgerRelayOutbox).values({
563
+ actionId: entry.id,
564
+ organizationId: entry.organizationId,
565
+ payloadRef: payloadRef ?? null,
566
+ relayStatus: "pending",
567
+ });
568
+ }
569
+ return { entry, replayed: false };
570
+ }
571
+ async function findExistingIdempotentEntry(db, input) {
572
+ if (!input.idempotencyScope || !input.idempotencyKey)
573
+ return null;
574
+ const [existing] = await db
575
+ .select()
576
+ .from(actionLedgerEntries)
577
+ .where(and(eq(actionLedgerEntries.idempotencyScope, input.idempotencyScope), eq(actionLedgerEntries.actionName, input.actionName), eq(actionLedgerEntries.targetType, input.targetType), eq(actionLedgerEntries.targetId, input.targetId), eq(actionLedgerEntries.idempotencyKey, input.idempotencyKey)))
578
+ .limit(1);
579
+ return existing ?? null;
580
+ }
581
+ async function findApprovalForRequestedAction(db, requestedActionId) {
582
+ const [approval] = await db
583
+ .select()
584
+ .from(actionApprovals)
585
+ .where(eq(actionApprovals.requestedActionId, requestedActionId))
586
+ .limit(1);
587
+ return approval ?? null;
588
+ }
589
+ async function findApprovalById(db, id) {
590
+ const [approval] = await db
591
+ .select()
592
+ .from(actionApprovals)
593
+ .where(eq(actionApprovals.id, id))
594
+ .limit(1);
595
+ return approval ?? null;
596
+ }
597
+ function assertApprovalDecisionStatus(status) {
598
+ if (approvalDecisionStatusSet.has(status))
599
+ return;
600
+ throw new ActionApprovalDecisionStatusError(status);
601
+ }
602
+ function assertSameFingerprint(entry, fingerprint) {
603
+ if (entry.idempotencyFingerprint !== fingerprint) {
604
+ throw new ActionLedgerIdempotencyConflictError(entry.id);
605
+ }
606
+ }
607
+ function normalizeListLimit(limit) {
608
+ if (limit === undefined)
609
+ return DEFAULT_LIST_LIMIT;
610
+ if (!Number.isFinite(limit))
611
+ return DEFAULT_LIST_LIMIT;
612
+ return Math.min(Math.max(Math.trunc(limit), 1), MAX_LIST_LIMIT);
613
+ }
614
+ function toActionLedgerListCursor(entry) {
615
+ return {
616
+ occurredAt: serializeCursorDate(entry.occurredAt),
617
+ id: entry.id,
618
+ };
619
+ }
620
+ function toActionLedgerRelayOutboxListCursor(row) {
621
+ return {
622
+ createdAt: serializeCursorDate(row.createdAt),
623
+ id: row.id,
624
+ };
625
+ }
626
+ function toActionApprovalListCursor(row) {
627
+ return {
628
+ createdAt: serializeCursorDate(row.createdAt),
629
+ id: row.id,
630
+ };
631
+ }
632
+ function toActionDelegationListCursor(row) {
633
+ return {
634
+ createdAt: serializeCursorDate(row.createdAt),
635
+ id: row.id,
636
+ };
637
+ }
638
+ function serializeCursorDate(value) {
639
+ const date = value instanceof Date ? value : new Date(value);
640
+ if (Number.isNaN(date.getTime())) {
641
+ throw new Error("Action ledger cursor occurredAt must be a valid timestamp");
642
+ }
643
+ return date.toISOString();
644
+ }
645
+ function parseCursorDate(value) {
646
+ const date = value instanceof Date ? value : new Date(value);
647
+ if (Number.isNaN(date.getTime())) {
648
+ throw new Error("Action ledger cursor occurredAt must be a valid timestamp");
649
+ }
650
+ return date;
651
+ }
652
+ function riskCondition(value) {
653
+ if (value === undefined)
654
+ return undefined;
655
+ if (Array.isArray(value)) {
656
+ if (value.length === 0)
657
+ return undefined;
658
+ return inArray(actionLedgerEntries.evaluatedRisk, value);
659
+ }
660
+ return eq(actionLedgerEntries.evaluatedRisk, value);
661
+ }
662
+ function statusCondition(value) {
663
+ if (value === undefined)
664
+ return undefined;
665
+ if (Array.isArray(value)) {
666
+ if (value.length === 0)
667
+ return undefined;
668
+ return inArray(actionLedgerEntries.status, value);
669
+ }
670
+ return eq(actionLedgerEntries.status, value);
671
+ }
672
+ function approvalStatusCondition(value) {
673
+ if (value === undefined)
674
+ return undefined;
675
+ if (Array.isArray(value)) {
676
+ if (value.length === 0)
677
+ return undefined;
678
+ return inArray(actionApprovals.status, value);
679
+ }
680
+ return eq(actionApprovals.status, value);
681
+ }
682
+ function approvalRiskCondition(value) {
683
+ if (value === undefined)
684
+ return undefined;
685
+ if (Array.isArray(value)) {
686
+ if (value.length === 0)
687
+ return undefined;
688
+ return inArray(actionApprovals.riskSnapshot, value);
689
+ }
690
+ return eq(actionApprovals.riskSnapshot, value);
691
+ }
692
+ function reversalKindCondition(value) {
693
+ if (value === undefined)
694
+ return undefined;
695
+ if (Array.isArray(value)) {
696
+ if (value.length === 0)
697
+ return undefined;
698
+ return inArray(actionMutationDetails.reversalKind, value);
699
+ }
700
+ return eq(actionMutationDetails.reversalKind, value);
701
+ }
702
+ function reversalStateCondition(value) {
703
+ if (value === undefined)
704
+ return undefined;
705
+ if (Array.isArray(value)) {
706
+ if (value.length === 0)
707
+ return undefined;
708
+ return inArray(actionMutationDetails.reversalStateProjection, value);
709
+ }
710
+ return eq(actionMutationDetails.reversalStateProjection, value);
711
+ }
712
+ function reversalOutcomeCondition(value) {
713
+ if (value === undefined)
714
+ return undefined;
715
+ if (Array.isArray(value)) {
716
+ if (value.length === 0)
717
+ return undefined;
718
+ return inArray(actionMutationDetails.reversalOutcomeProjection, value);
719
+ }
720
+ return eq(actionMutationDetails.reversalOutcomeProjection, value);
721
+ }
722
+ function relayStatusCondition(value) {
723
+ if (value === undefined)
724
+ return undefined;
725
+ if (Array.isArray(value)) {
726
+ if (value.length === 0)
727
+ return undefined;
728
+ return inArray(actionLedgerRelayOutbox.relayStatus, value);
729
+ }
730
+ return eq(actionLedgerRelayOutbox.relayStatus, value);
731
+ }
732
+ function mutationDetailExists(condition) {
733
+ return sql `EXISTS (
734
+ SELECT 1
735
+ FROM ${actionMutationDetails}
736
+ WHERE ${actionMutationDetails.actionId} = ${actionLedgerEntries.id}
737
+ AND ${condition}
738
+ )`;
739
+ }
740
+ function sensitiveReadDetailExists(condition) {
741
+ return sql `EXISTS (
742
+ SELECT 1
743
+ FROM ${actionSensitiveReadDetails}
744
+ WHERE ${actionSensitiveReadDetails.actionId} = ${actionLedgerEntries.id}
745
+ AND ${condition}
746
+ )`;
747
+ }
748
+ function buildCursorCondition(cursor) {
749
+ const occurredAt = parseCursorDate(cursor.occurredAt);
750
+ const tieBreaker = and(eq(actionLedgerEntries.occurredAt, occurredAt), lt(actionLedgerEntries.id, cursor.id));
751
+ return or(lt(actionLedgerEntries.occurredAt, occurredAt), tieBreaker);
752
+ }
753
+ function buildRelayOutboxCursorCondition(cursor) {
754
+ const createdAt = parseCursorDate(cursor.createdAt);
755
+ const tieBreaker = and(eq(actionLedgerRelayOutbox.createdAt, createdAt), lt(actionLedgerRelayOutbox.id, cursor.id));
756
+ return or(lt(actionLedgerRelayOutbox.createdAt, createdAt), tieBreaker);
757
+ }
758
+ function buildApprovalCursorCondition(cursor) {
759
+ const createdAt = parseCursorDate(cursor.createdAt);
760
+ const tieBreaker = and(eq(actionApprovals.createdAt, createdAt), lt(actionApprovals.id, cursor.id));
761
+ return or(lt(actionApprovals.createdAt, createdAt), tieBreaker);
762
+ }
763
+ function buildDelegationCursorCondition(cursor) {
764
+ const createdAt = parseCursorDate(cursor.createdAt);
765
+ const tieBreaker = and(eq(actionDelegations.createdAt, createdAt), lt(actionDelegations.id, cursor.id));
766
+ return or(lt(actionDelegations.createdAt, createdAt), tieBreaker);
767
+ }
768
+ function buildActionDelegationsPredicate(input) {
769
+ const conditions = [];
770
+ if (input.rootPrincipalType) {
771
+ conditions.push(eq(actionDelegations.rootPrincipalType, input.rootPrincipalType));
772
+ }
773
+ if (input.rootPrincipalId) {
774
+ conditions.push(eq(actionDelegations.rootPrincipalId, input.rootPrincipalId));
775
+ }
776
+ if (input.parentPrincipalType) {
777
+ conditions.push(eq(actionDelegations.parentPrincipalType, input.parentPrincipalType));
778
+ }
779
+ if (input.parentPrincipalId) {
780
+ conditions.push(eq(actionDelegations.parentPrincipalId, input.parentPrincipalId));
781
+ }
782
+ if (input.childPrincipalType) {
783
+ conditions.push(eq(actionDelegations.childPrincipalType, input.childPrincipalType));
784
+ }
785
+ if (input.childPrincipalId) {
786
+ conditions.push(eq(actionDelegations.childPrincipalId, input.childPrincipalId));
787
+ }
788
+ if (input.grantSource)
789
+ conditions.push(eq(actionDelegations.grantSource, input.grantSource));
790
+ if (input.capabilityScopeRef) {
791
+ conditions.push(eq(actionDelegations.capabilityScopeRef, input.capabilityScopeRef));
792
+ }
793
+ if (input.budgetScopeRef) {
794
+ conditions.push(eq(actionDelegations.budgetScopeRef, input.budgetScopeRef));
795
+ }
796
+ if (input.expiresAtFrom) {
797
+ conditions.push(gte(actionDelegations.expiresAt, parseCursorDate(input.expiresAtFrom)));
798
+ }
799
+ if (input.expiresAtTo) {
800
+ conditions.push(lte(actionDelegations.expiresAt, parseCursorDate(input.expiresAtTo)));
801
+ }
802
+ if (input.createdAtFrom) {
803
+ conditions.push(gte(actionDelegations.createdAt, parseCursorDate(input.createdAtFrom)));
804
+ }
805
+ if (input.createdAtTo) {
806
+ conditions.push(lte(actionDelegations.createdAt, parseCursorDate(input.createdAtTo)));
807
+ }
808
+ if (input.cursor) {
809
+ conditions.push(buildDelegationCursorCondition(input.cursor));
810
+ }
811
+ if (conditions.length === 0)
812
+ return undefined;
813
+ if (conditions.length === 1)
814
+ return conditions[0];
815
+ return and(...conditions);
816
+ }
817
+ function buildActionApprovalsPredicate(input) {
818
+ const conditions = [];
819
+ if (input.requestedActionId) {
820
+ conditions.push(eq(actionApprovals.requestedActionId, input.requestedActionId));
821
+ }
822
+ const entryStatusCondition = approvalStatusCondition(input.status);
823
+ if (entryStatusCondition)
824
+ conditions.push(entryStatusCondition);
825
+ if (input.requestedByPrincipalId) {
826
+ conditions.push(eq(actionApprovals.requestedByPrincipalId, input.requestedByPrincipalId));
827
+ }
828
+ if (input.assignedToPrincipalId) {
829
+ conditions.push(eq(actionApprovals.assignedToPrincipalId, input.assignedToPrincipalId));
830
+ }
831
+ if (input.decidedByPrincipalId) {
832
+ conditions.push(eq(actionApprovals.decidedByPrincipalId, input.decidedByPrincipalId));
833
+ }
834
+ if (input.delegatedFromPrincipalId) {
835
+ conditions.push(eq(actionApprovals.delegatedFromPrincipalId, input.delegatedFromPrincipalId));
836
+ }
837
+ if (input.policyName)
838
+ conditions.push(eq(actionApprovals.policyName, input.policyName));
839
+ if (input.policyVersion)
840
+ conditions.push(eq(actionApprovals.policyVersion, input.policyVersion));
841
+ const riskSnapshotCondition = approvalRiskCondition(input.riskSnapshot);
842
+ if (riskSnapshotCondition)
843
+ conditions.push(riskSnapshotCondition);
844
+ if (input.reasonCode)
845
+ conditions.push(eq(actionApprovals.reasonCode, input.reasonCode));
846
+ if (input.expiresAtFrom) {
847
+ conditions.push(gte(actionApprovals.expiresAt, parseCursorDate(input.expiresAtFrom)));
848
+ }
849
+ if (input.expiresAtTo) {
850
+ conditions.push(lte(actionApprovals.expiresAt, parseCursorDate(input.expiresAtTo)));
851
+ }
852
+ if (input.decidedAtFrom) {
853
+ conditions.push(gte(actionApprovals.decidedAt, parseCursorDate(input.decidedAtFrom)));
854
+ }
855
+ if (input.decidedAtTo) {
856
+ conditions.push(lte(actionApprovals.decidedAt, parseCursorDate(input.decidedAtTo)));
857
+ }
858
+ if (input.createdAtFrom) {
859
+ conditions.push(gte(actionApprovals.createdAt, parseCursorDate(input.createdAtFrom)));
860
+ }
861
+ if (input.createdAtTo) {
862
+ conditions.push(lte(actionApprovals.createdAt, parseCursorDate(input.createdAtTo)));
863
+ }
864
+ if (input.cursor) {
865
+ conditions.push(buildApprovalCursorCondition(input.cursor));
866
+ }
867
+ if (conditions.length === 0)
868
+ return undefined;
869
+ if (conditions.length === 1)
870
+ return conditions[0];
871
+ return and(...conditions);
872
+ }
873
+ function buildActionLedgerRelayOutboxPredicate(input) {
874
+ const conditions = [];
875
+ if (input.actionId)
876
+ conditions.push(eq(actionLedgerRelayOutbox.actionId, input.actionId));
877
+ if (input.organizationId) {
878
+ conditions.push(eq(actionLedgerRelayOutbox.organizationId, input.organizationId));
879
+ }
880
+ const entryRelayStatusCondition = relayStatusCondition(input.relayStatus);
881
+ if (entryRelayStatusCondition)
882
+ conditions.push(entryRelayStatusCondition);
883
+ if (input.dueBefore) {
884
+ conditions.push(lte(actionLedgerRelayOutbox.nextRetryAt, parseCursorDate(input.dueBefore)));
885
+ }
886
+ if (input.createdAtFrom) {
887
+ conditions.push(gte(actionLedgerRelayOutbox.createdAt, parseCursorDate(input.createdAtFrom)));
888
+ }
889
+ if (input.createdAtTo) {
890
+ conditions.push(lte(actionLedgerRelayOutbox.createdAt, parseCursorDate(input.createdAtTo)));
891
+ }
892
+ if (input.processedAtFrom) {
893
+ conditions.push(gte(actionLedgerRelayOutbox.processedAt, parseCursorDate(input.processedAtFrom)));
894
+ }
895
+ if (input.processedAtTo) {
896
+ conditions.push(lte(actionLedgerRelayOutbox.processedAt, parseCursorDate(input.processedAtTo)));
897
+ }
898
+ if (input.cursor) {
899
+ conditions.push(buildRelayOutboxCursorCondition(input.cursor));
900
+ }
901
+ if (conditions.length === 0)
902
+ return undefined;
903
+ if (conditions.length === 1)
904
+ return conditions[0];
905
+ return and(...conditions);
906
+ }
907
+ function buildActionLedgerEntriesPredicate(input) {
908
+ const conditions = [];
909
+ if (input.actionName)
910
+ conditions.push(eq(actionLedgerEntries.actionName, input.actionName));
911
+ if (input.actionKind)
912
+ conditions.push(eq(actionLedgerEntries.actionKind, input.actionKind));
913
+ if (input.actorType)
914
+ conditions.push(eq(actionLedgerEntries.actorType, input.actorType));
915
+ if (input.principalType) {
916
+ conditions.push(eq(actionLedgerEntries.principalType, input.principalType));
917
+ }
918
+ if (input.principalId)
919
+ conditions.push(eq(actionLedgerEntries.principalId, input.principalId));
920
+ if (input.apiTokenId)
921
+ conditions.push(eq(actionLedgerEntries.apiTokenId, input.apiTokenId));
922
+ if (input.sessionId)
923
+ conditions.push(eq(actionLedgerEntries.sessionId, input.sessionId));
924
+ if (input.callerType)
925
+ conditions.push(eq(actionLedgerEntries.callerType, input.callerType));
926
+ if (input.organizationId) {
927
+ conditions.push(eq(actionLedgerEntries.organizationId, input.organizationId));
928
+ }
929
+ if (input.targetType)
930
+ conditions.push(eq(actionLedgerEntries.targetType, input.targetType));
931
+ if (input.targetId)
932
+ conditions.push(eq(actionLedgerEntries.targetId, input.targetId));
933
+ if (input.targetIds && input.targetIds.length > 0) {
934
+ conditions.push(inArray(actionLedgerEntries.targetId, input.targetIds));
935
+ }
936
+ if (input.routeOrToolName) {
937
+ conditions.push(eq(actionLedgerEntries.routeOrToolName, input.routeOrToolName));
938
+ }
939
+ if (input.workflowRunId) {
940
+ conditions.push(eq(actionLedgerEntries.workflowRunId, input.workflowRunId));
941
+ }
942
+ if (input.workflowStepId) {
943
+ conditions.push(eq(actionLedgerEntries.workflowStepId, input.workflowStepId));
944
+ }
945
+ if (input.correlationId) {
946
+ conditions.push(eq(actionLedgerEntries.correlationId, input.correlationId));
947
+ }
948
+ if (input.causationActionId) {
949
+ conditions.push(eq(actionLedgerEntries.causationActionId, input.causationActionId));
950
+ }
951
+ if (input.capabilityId)
952
+ conditions.push(eq(actionLedgerEntries.capabilityId, input.capabilityId));
953
+ if (input.capabilityVersion) {
954
+ conditions.push(eq(actionLedgerEntries.capabilityVersion, input.capabilityVersion));
955
+ }
956
+ if (input.authorizationSource) {
957
+ conditions.push(eq(actionLedgerEntries.authorizationSource, input.authorizationSource));
958
+ }
959
+ if (input.approvalId)
960
+ conditions.push(eq(actionLedgerEntries.approvalId, input.approvalId));
961
+ if (input.amendsActionId) {
962
+ conditions.push(eq(actionLedgerEntries.amendsActionId, input.amendsActionId));
963
+ }
964
+ if (input.idempotencyScope) {
965
+ conditions.push(eq(actionLedgerEntries.idempotencyScope, input.idempotencyScope));
966
+ }
967
+ if (input.idempotencyKey) {
968
+ conditions.push(eq(actionLedgerEntries.idempotencyKey, input.idempotencyKey));
969
+ }
970
+ const evaluatedRiskCondition = riskCondition(input.evaluatedRisk);
971
+ if (evaluatedRiskCondition)
972
+ conditions.push(evaluatedRiskCondition);
973
+ const entryStatusCondition = statusCondition(input.status);
974
+ if (entryStatusCondition)
975
+ conditions.push(entryStatusCondition);
976
+ const entryReversalKindCondition = reversalKindCondition(input.reversalKind);
977
+ if (entryReversalKindCondition) {
978
+ conditions.push(mutationDetailExists(entryReversalKindCondition));
979
+ }
980
+ const entryReversalStateCondition = reversalStateCondition(input.reversalState);
981
+ if (entryReversalStateCondition) {
982
+ conditions.push(mutationDetailExists(entryReversalStateCondition));
983
+ }
984
+ const entryReversalOutcomeCondition = reversalOutcomeCondition(input.reversalOutcome);
985
+ if (entryReversalOutcomeCondition) {
986
+ conditions.push(mutationDetailExists(entryReversalOutcomeCondition));
987
+ }
988
+ if (input.reversesActionId) {
989
+ conditions.push(mutationDetailExists(eq(actionMutationDetails.reversesActionId, input.reversesActionId)));
990
+ }
991
+ if (input.reversedByActionId) {
992
+ conditions.push(mutationDetailExists(eq(actionMutationDetails.reversedByActionIdProjection, input.reversedByActionId)));
993
+ }
994
+ if (input.sensitiveReasonCode) {
995
+ conditions.push(sensitiveReadDetailExists(eq(actionSensitiveReadDetails.reasonCode, input.sensitiveReasonCode)));
996
+ }
997
+ if (input.decisionPolicy) {
998
+ conditions.push(sensitiveReadDetailExists(eq(actionSensitiveReadDetails.decisionPolicy, input.decisionPolicy)));
999
+ }
1000
+ if (input.occurredAtFrom) {
1001
+ conditions.push(gte(actionLedgerEntries.occurredAt, parseCursorDate(input.occurredAtFrom)));
1002
+ }
1003
+ if (input.occurredAtTo) {
1004
+ conditions.push(lte(actionLedgerEntries.occurredAt, parseCursorDate(input.occurredAtTo)));
1005
+ }
1006
+ if (input.cursor) {
1007
+ conditions.push(buildCursorCondition(input.cursor));
1008
+ }
1009
+ if (conditions.length === 0)
1010
+ return undefined;
1011
+ if (conditions.length === 1)
1012
+ return conditions[0];
1013
+ return and(...conditions);
1014
+ }
1015
+ export const __test__ = {
1016
+ buildActionApprovalsPredicate,
1017
+ buildActionDelegationsPredicate,
1018
+ buildActionLedgerEntriesPredicate,
1019
+ buildActionLedgerRelayOutboxPredicate,
1020
+ normalizeListLimit,
1021
+ toActionApprovalListCursor,
1022
+ toActionDelegationListCursor,
1023
+ toActionLedgerListCursor,
1024
+ toActionLedgerRelayOutboxListCursor,
1025
+ };