hierarchical-approval 0.1.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 +968 -0
- package/dist/ApprovalEngine-BcnLzfAU.d.cts +426 -0
- package/dist/ApprovalEngine-DdZtyeB5.d.ts +426 -0
- package/dist/IStorageAdapter-RAiLF8bc.d.cts +192 -0
- package/dist/IStorageAdapter-RAiLF8bc.d.ts +192 -0
- package/dist/adapters/MemoryAdapter.cjs +189 -0
- package/dist/adapters/MemoryAdapter.cjs.map +1 -0
- package/dist/adapters/MemoryAdapter.d.cts +22 -0
- package/dist/adapters/MemoryAdapter.d.ts +22 -0
- package/dist/adapters/MemoryAdapter.js +187 -0
- package/dist/adapters/MemoryAdapter.js.map +1 -0
- package/dist/adapters/PostgresAdapter.cjs +468 -0
- package/dist/adapters/PostgresAdapter.cjs.map +1 -0
- package/dist/adapters/PostgresAdapter.d.cts +45 -0
- package/dist/adapters/PostgresAdapter.d.ts +45 -0
- package/dist/adapters/PostgresAdapter.js +466 -0
- package/dist/adapters/PostgresAdapter.js.map +1 -0
- package/dist/index.cjs +1759 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +1742 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.cjs +1797 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +24 -0
- package/dist/testing.d.ts +24 -0
- package/dist/testing.js +1790 -0
- package/dist/testing.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { a as ApprovalInstance, d as AuditEntry, g as ApprovalLevelInstance, I as IStorageAdapter, q as ResolverFn, j as ApprovalTemplateConfig, A as ApprovalTemplate, m as AuditContext, h as ApprovalMode, P as PaginationOpts, b as PaginatedResult, c as InstanceFilter, C as CursorPaginationOpts, e as CursorPaginatedResult } from './IStorageAdapter-RAiLF8bc.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
interface ApprovalEvent {
|
|
5
|
+
instanceId: string;
|
|
6
|
+
documentId: string;
|
|
7
|
+
documentType: string;
|
|
8
|
+
timestamp: Date;
|
|
9
|
+
}
|
|
10
|
+
interface SubmittedEvent extends ApprovalEvent {
|
|
11
|
+
submittedBy: string;
|
|
12
|
+
currentApprovers: string[];
|
|
13
|
+
}
|
|
14
|
+
interface ApprovedEvent extends ApprovalEvent {
|
|
15
|
+
approverId: string;
|
|
16
|
+
level: number;
|
|
17
|
+
comment?: string;
|
|
18
|
+
isFinal: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface RejectedEvent extends ApprovalEvent {
|
|
21
|
+
approverId: string;
|
|
22
|
+
level: number;
|
|
23
|
+
reason: string;
|
|
24
|
+
returnTo: 'originator' | 'previous' | null;
|
|
25
|
+
}
|
|
26
|
+
interface DelegatedEvent extends ApprovalEvent {
|
|
27
|
+
fromApprover: string;
|
|
28
|
+
toApprover: string;
|
|
29
|
+
level: number;
|
|
30
|
+
reason: string;
|
|
31
|
+
}
|
|
32
|
+
interface EscalatedEvent extends ApprovalEvent {
|
|
33
|
+
level: number;
|
|
34
|
+
escalatedTo: string;
|
|
35
|
+
}
|
|
36
|
+
interface CancelledEvent extends ApprovalEvent {
|
|
37
|
+
cancelledBy: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
}
|
|
40
|
+
interface LevelAdvancedEvent extends ApprovalEvent {
|
|
41
|
+
fromLevel: number;
|
|
42
|
+
toLevel: number;
|
|
43
|
+
newApprovers: string[];
|
|
44
|
+
}
|
|
45
|
+
interface ResubmittedEvent extends ApprovalEvent {
|
|
46
|
+
resubmittedBy: string;
|
|
47
|
+
originalInstanceId: string;
|
|
48
|
+
}
|
|
49
|
+
interface OverriddenEvent extends ApprovalEvent {
|
|
50
|
+
overriddenBy: string;
|
|
51
|
+
justification: string;
|
|
52
|
+
}
|
|
53
|
+
interface ExpiredEvent extends ApprovalEvent {
|
|
54
|
+
deadlineAction: 'cancel' | 'reject';
|
|
55
|
+
}
|
|
56
|
+
interface SlaBreachedEvent extends ApprovalEvent {
|
|
57
|
+
slaDeadlineAt: Date;
|
|
58
|
+
}
|
|
59
|
+
interface ApprovalEventMap {
|
|
60
|
+
'approval:submitted': SubmittedEvent;
|
|
61
|
+
'approval:approved': ApprovedEvent;
|
|
62
|
+
'approval:rejected': RejectedEvent;
|
|
63
|
+
'approval:delegated': DelegatedEvent;
|
|
64
|
+
'approval:escalated': EscalatedEvent;
|
|
65
|
+
'approval:cancelled': CancelledEvent;
|
|
66
|
+
'approval:completed': ApprovalInstance;
|
|
67
|
+
'approval:level_advanced': LevelAdvancedEvent;
|
|
68
|
+
'approval:resubmitted': ResubmittedEvent;
|
|
69
|
+
'approval:overridden': OverriddenEvent;
|
|
70
|
+
'approval:expired': ExpiredEvent;
|
|
71
|
+
'approval:sla_breached': SlaBreachedEvent;
|
|
72
|
+
}
|
|
73
|
+
type ApprovalEventName = keyof ApprovalEventMap;
|
|
74
|
+
interface HistoryEntry extends AuditEntry {
|
|
75
|
+
instanceId: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
79
|
+
templateName: z.ZodString;
|
|
80
|
+
documentId: z.ZodString;
|
|
81
|
+
documentType: z.ZodString;
|
|
82
|
+
submittedBy: z.ZodString;
|
|
83
|
+
data: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
84
|
+
metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
85
|
+
expiresAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
86
|
+
deadlineAction: z.ZodOptional<z.ZodEnum<{
|
|
87
|
+
cancel: "cancel";
|
|
88
|
+
reject: "reject";
|
|
89
|
+
}>>;
|
|
90
|
+
}, z.core.$strip>;
|
|
91
|
+
declare const ApproveOptionsSchema: z.ZodObject<{
|
|
92
|
+
approverId: z.ZodString;
|
|
93
|
+
comment: z.ZodOptional<z.ZodString>;
|
|
94
|
+
}, z.core.$strip>;
|
|
95
|
+
declare const RejectOptionsSchema: z.ZodObject<{
|
|
96
|
+
approverId: z.ZodString;
|
|
97
|
+
reason: z.ZodString;
|
|
98
|
+
returnTo: z.ZodOptional<z.ZodEnum<{
|
|
99
|
+
originator: "originator";
|
|
100
|
+
previous: "previous";
|
|
101
|
+
}>>;
|
|
102
|
+
}, z.core.$strip>;
|
|
103
|
+
declare const DelegateOptionsSchema: z.ZodObject<{
|
|
104
|
+
fromApprover: z.ZodString;
|
|
105
|
+
toApprover: z.ZodString;
|
|
106
|
+
reason: z.ZodString;
|
|
107
|
+
until: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
108
|
+
}, z.core.$strip>;
|
|
109
|
+
declare const CancelOptionsSchema: z.ZodObject<{
|
|
110
|
+
cancelledBy: z.ZodString;
|
|
111
|
+
reason: z.ZodString;
|
|
112
|
+
}, z.core.$strip>;
|
|
113
|
+
declare const EscalateOptionsSchema: z.ZodObject<{
|
|
114
|
+
escalatedBy: z.ZodString;
|
|
115
|
+
}, z.core.$strip>;
|
|
116
|
+
declare const ResubmitOptionsSchema: z.ZodObject<{
|
|
117
|
+
resubmittedBy: z.ZodString;
|
|
118
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
119
|
+
updatedData: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
120
|
+
}, z.core.$strip>;
|
|
121
|
+
declare const AddCommentOptionsSchema: z.ZodObject<{
|
|
122
|
+
actorId: z.ZodString;
|
|
123
|
+
comment: z.ZodString;
|
|
124
|
+
}, z.core.$strip>;
|
|
125
|
+
declare const OverrideOptionsSchema: z.ZodObject<{
|
|
126
|
+
overriddenBy: z.ZodString;
|
|
127
|
+
justification: z.ZodString;
|
|
128
|
+
}, z.core.$strip>;
|
|
129
|
+
type SubmitOptions = z.infer<typeof SubmitOptionsSchema>;
|
|
130
|
+
type ApproveOptions = z.infer<typeof ApproveOptionsSchema>;
|
|
131
|
+
type RejectOptions = z.infer<typeof RejectOptionsSchema>;
|
|
132
|
+
type DelegateOptions = z.infer<typeof DelegateOptionsSchema>;
|
|
133
|
+
type CancelOptions = z.infer<typeof CancelOptionsSchema>;
|
|
134
|
+
type EscalateOptions = z.infer<typeof EscalateOptionsSchema>;
|
|
135
|
+
type ResubmitOptions = z.infer<typeof ResubmitOptionsSchema>;
|
|
136
|
+
type AddCommentOptions = z.infer<typeof AddCommentOptionsSchema>;
|
|
137
|
+
type OverrideOptions = z.infer<typeof OverrideOptionsSchema>;
|
|
138
|
+
|
|
139
|
+
interface Logger {
|
|
140
|
+
info(msg: string, context?: Record<string, unknown>): void;
|
|
141
|
+
warn(msg: string, context?: Record<string, unknown>): void;
|
|
142
|
+
error(msg: string, err?: unknown, context?: Record<string, unknown>): void;
|
|
143
|
+
debug(msg: string, context?: Record<string, unknown>): void;
|
|
144
|
+
}
|
|
145
|
+
declare const noopLogger: Logger;
|
|
146
|
+
|
|
147
|
+
interface Clock {
|
|
148
|
+
now(): Date;
|
|
149
|
+
}
|
|
150
|
+
declare const systemClock: Clock;
|
|
151
|
+
|
|
152
|
+
type IdGeneratorFn = (prefix: 'inst' | 'tpl') => string;
|
|
153
|
+
declare const defaultIdGenerator: IdGeneratorFn;
|
|
154
|
+
|
|
155
|
+
interface OrgProvider {
|
|
156
|
+
getUsersByRole(role: string, tenantId?: string): Promise<string[]> | string[];
|
|
157
|
+
/** Optional: resolve users by department name. */
|
|
158
|
+
getUsersByDepartment?(dept: string, tenantId?: string): Promise<string[]> | string[];
|
|
159
|
+
/** Optional: resolve the direct manager of a user. */
|
|
160
|
+
getManagerOf?(userId: string, tenantId?: string): Promise<string | null> | string | null;
|
|
161
|
+
/** Optional: resolve the skip-level manager of a user. */
|
|
162
|
+
getSkipLevelManagerOf?(userId: string, tenantId?: string): Promise<string | null> | string | null;
|
|
163
|
+
/** Optional: resolve users matching a custom attribute/value pair. */
|
|
164
|
+
getUsersByAttribute?(attr: string, value: unknown, tenantId?: string): Promise<string[]> | string[];
|
|
165
|
+
}
|
|
166
|
+
type ApproverResolverFn = (config: Record<string, unknown>, ctx: {
|
|
167
|
+
submittedBy: string;
|
|
168
|
+
data: Record<string, unknown>;
|
|
169
|
+
orgProvider?: OrgProvider;
|
|
170
|
+
}) => Promise<string[]> | string[];
|
|
171
|
+
|
|
172
|
+
type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
|
|
173
|
+
|
|
174
|
+
declare class ApprovalError extends Error {
|
|
175
|
+
readonly code: string;
|
|
176
|
+
constructor(message: string, code: string);
|
|
177
|
+
toJSON(): {
|
|
178
|
+
code: string;
|
|
179
|
+
message: string;
|
|
180
|
+
name: string;
|
|
181
|
+
};
|
|
182
|
+
toHttpStatus(): number;
|
|
183
|
+
}
|
|
184
|
+
declare class ApprovalNotFoundError extends ApprovalError {
|
|
185
|
+
constructor(resource: string, id: string);
|
|
186
|
+
}
|
|
187
|
+
declare class ApprovalConflictError extends ApprovalError {
|
|
188
|
+
constructor(instanceId: string);
|
|
189
|
+
}
|
|
190
|
+
declare class ApprovalForbiddenError extends ApprovalError {
|
|
191
|
+
constructor(message: string);
|
|
192
|
+
}
|
|
193
|
+
declare class ApprovalValidationError extends ApprovalError {
|
|
194
|
+
readonly cause?: unknown | undefined;
|
|
195
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
196
|
+
}
|
|
197
|
+
declare class ApprovalTemplateNotFoundError extends ApprovalError {
|
|
198
|
+
constructor(name: string);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface NotificationEvent {
|
|
202
|
+
type: ApprovalEventName;
|
|
203
|
+
instanceId: string;
|
|
204
|
+
documentId: string;
|
|
205
|
+
documentType: string;
|
|
206
|
+
timestamp: Date;
|
|
207
|
+
/** Current-level approver IDs; empty for non-level events (cancelled, expired, etc.). */
|
|
208
|
+
recipients: string[];
|
|
209
|
+
templateName: string;
|
|
210
|
+
tenantId: string;
|
|
211
|
+
payload: ApprovalEventMap[ApprovalEventName];
|
|
212
|
+
}
|
|
213
|
+
interface INotificationAdapter {
|
|
214
|
+
/** Called after every emitted approval event. Must not throw — errors are logged and swallowed. */
|
|
215
|
+
notify(event: NotificationEvent): Promise<void>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface IAuditAdapter {
|
|
219
|
+
/**
|
|
220
|
+
* Called after every state-mutating operation, in addition to the primary storage adapter.
|
|
221
|
+
* Intended for write-once sinks: Kafka, S3, CloudTrail, WORM stores.
|
|
222
|
+
* Must not throw — errors are logged and swallowed.
|
|
223
|
+
*/
|
|
224
|
+
append(tenantId: string, instanceId: string, entry: AuditEntry, instance: Readonly<ApprovalInstance>): Promise<void>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.overridden' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
228
|
+
interface IMetricsAdapter {
|
|
229
|
+
/** Increment a counter. Synchronous — never awaited. */
|
|
230
|
+
increment(metric: MetricName, labels?: Record<string, string>): void;
|
|
231
|
+
/** Record a timing measurement in milliseconds. Synchronous — never awaited. */
|
|
232
|
+
timing(metric: MetricName, durationMs: number, labels?: Record<string, string>): void;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface ISchedulerAdapter {
|
|
236
|
+
/**
|
|
237
|
+
* Schedule a one-shot callback to run at the given date.
|
|
238
|
+
* Returns an opaque handle that can be passed to cancel().
|
|
239
|
+
*/
|
|
240
|
+
scheduleAt(id: string, runAt: Date, callback: () => Promise<void>): Promise<string>;
|
|
241
|
+
/** Cancel a previously scheduled callback by its handle. */
|
|
242
|
+
cancel(handle: string): Promise<void>;
|
|
243
|
+
/** Gracefully shut down the scheduler and release resources. */
|
|
244
|
+
shutdown(): Promise<void>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
interface AuthorizationContext {
|
|
248
|
+
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment';
|
|
249
|
+
actorId: string;
|
|
250
|
+
instance: Readonly<ApprovalInstance>;
|
|
251
|
+
level?: Readonly<ApprovalLevelInstance>;
|
|
252
|
+
opts: Record<string, unknown>;
|
|
253
|
+
}
|
|
254
|
+
interface IAuthorizationPolicy {
|
|
255
|
+
/**
|
|
256
|
+
* Return undefined to allow the operation.
|
|
257
|
+
* Return a non-empty string to deny — the engine throws ApprovalForbiddenError(message).
|
|
258
|
+
* Throwing ApprovalForbiddenError directly is also permitted.
|
|
259
|
+
*/
|
|
260
|
+
authorize(ctx: AuthorizationContext): Promise<string | undefined> | string | undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface OperationContext<T = unknown> {
|
|
264
|
+
operation: string;
|
|
265
|
+
instanceId?: string;
|
|
266
|
+
actorId?: string;
|
|
267
|
+
tenantId: string;
|
|
268
|
+
input: T;
|
|
269
|
+
}
|
|
270
|
+
interface IOperationMiddleware {
|
|
271
|
+
/** Runs after authorization and input validation, before state mutations. */
|
|
272
|
+
before?(ctx: OperationContext): Promise<void> | void;
|
|
273
|
+
/** Runs after successful completion of the operation. */
|
|
274
|
+
after?(ctx: OperationContext, result: ApprovalInstance | void): Promise<void> | void;
|
|
275
|
+
/** Runs when an ApprovalError is thrown. Does not suppress the error. */
|
|
276
|
+
onError?(ctx: OperationContext, error: ApprovalError): Promise<void> | void;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
interface ValidationResult {
|
|
280
|
+
valid: boolean;
|
|
281
|
+
errors: Array<{
|
|
282
|
+
field: string;
|
|
283
|
+
message: string;
|
|
284
|
+
}>;
|
|
285
|
+
}
|
|
286
|
+
interface CanApproveResult {
|
|
287
|
+
eligible: boolean;
|
|
288
|
+
reason?: 'not_an_approver' | 'already_acted' | 'self_approval' | 'wrong_status' | 'delegated_away';
|
|
289
|
+
}
|
|
290
|
+
interface PreviewChainLevel {
|
|
291
|
+
level: number;
|
|
292
|
+
name: string;
|
|
293
|
+
resolvedApprovers: string[];
|
|
294
|
+
mode: ApprovalMode;
|
|
295
|
+
}
|
|
296
|
+
interface PreviewResult {
|
|
297
|
+
levels: PreviewChainLevel[];
|
|
298
|
+
/** Indices (0-based) of conditions that fired for this data. */
|
|
299
|
+
conditionsApplied: number[];
|
|
300
|
+
}
|
|
301
|
+
interface BulkResult {
|
|
302
|
+
succeeded: ApprovalInstance[];
|
|
303
|
+
failed: Array<{
|
|
304
|
+
instanceId: string;
|
|
305
|
+
error: ApprovalError;
|
|
306
|
+
}>;
|
|
307
|
+
total: number;
|
|
308
|
+
}
|
|
309
|
+
interface HealthResult {
|
|
310
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
311
|
+
adapter: 'connected' | 'error';
|
|
312
|
+
pendingCount: number;
|
|
313
|
+
overdueCount: number;
|
|
314
|
+
escalationRunning: boolean;
|
|
315
|
+
lastTickAt?: Date;
|
|
316
|
+
}
|
|
317
|
+
interface RetryPolicy {
|
|
318
|
+
maxAttempts: number;
|
|
319
|
+
baseDelayMs: number;
|
|
320
|
+
maxDelayMs?: number;
|
|
321
|
+
jitter?: boolean;
|
|
322
|
+
}
|
|
323
|
+
type IdempotencyKeyFn = (tenantId: string, documentType: string, documentId: string, templateName: string, data: Record<string, unknown>) => string;
|
|
324
|
+
interface ApprovalEngineOptions {
|
|
325
|
+
adapter: IStorageAdapter;
|
|
326
|
+
tenantId?: string;
|
|
327
|
+
orgProvider?: OrgProvider;
|
|
328
|
+
logger?: Logger;
|
|
329
|
+
escalationPollIntervalMs?: number;
|
|
330
|
+
/** Maximum number of instances allowed in a single bulk operation. Default: 200. */
|
|
331
|
+
maxBulkItems?: number;
|
|
332
|
+
/** Injectable clock — defaults to system clock. Enables deterministic tests and custom time sources. */
|
|
333
|
+
clock?: Clock;
|
|
334
|
+
/** Custom ID generator for instances and templates. Defaults to timestamp+random. */
|
|
335
|
+
generateId?: IdGeneratorFn;
|
|
336
|
+
/** Custom optimistic locking retry policy. */
|
|
337
|
+
retryPolicy?: RetryPolicy;
|
|
338
|
+
/** Custom idempotency key derivation function. Default: SHA-256 of tenant+documentType+documentId+templateName. */
|
|
339
|
+
idempotencyKeyFn?: IdempotencyKeyFn;
|
|
340
|
+
/** Notification adapter called after every approval event. */
|
|
341
|
+
notificationAdapter?: INotificationAdapter;
|
|
342
|
+
/** Separate append-only audit sink (Kafka, S3, CloudTrail). Called alongside storage adapter. */
|
|
343
|
+
auditAdapter?: IAuditAdapter;
|
|
344
|
+
/** Metrics adapter for Prometheus / Datadog / OpenTelemetry. */
|
|
345
|
+
metricsAdapter?: IMetricsAdapter;
|
|
346
|
+
/** Custom scheduler adapter (BullMQ, Temporal, cron). Replaces built-in setInterval polling. */
|
|
347
|
+
schedulerAdapter?: ISchedulerAdapter;
|
|
348
|
+
/** Authorization policy called before every mutating operation. */
|
|
349
|
+
authorizationPolicy?: IAuthorizationPolicy;
|
|
350
|
+
/** Middleware chain: before/after/onError hooks for every operation. */
|
|
351
|
+
middleware?: IOperationMiddleware[];
|
|
352
|
+
}
|
|
353
|
+
declare class ApprovalEngine {
|
|
354
|
+
private readonly opts;
|
|
355
|
+
private readonly bus;
|
|
356
|
+
private readonly registry;
|
|
357
|
+
private readonly resolver;
|
|
358
|
+
private readonly escalation;
|
|
359
|
+
private readonly tenantId;
|
|
360
|
+
private readonly logger;
|
|
361
|
+
private readonly clock;
|
|
362
|
+
private readonly generateId;
|
|
363
|
+
private readonly maxBulkItems;
|
|
364
|
+
private readonly retryPolicy;
|
|
365
|
+
private readonly idempotencyKeyFn;
|
|
366
|
+
constructor(opts: ApprovalEngineOptions);
|
|
367
|
+
on<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
|
|
368
|
+
off<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
|
|
369
|
+
registerResolver(name: string, fn: ResolverFn): void;
|
|
370
|
+
registerApproverType(typeName: string, fn: ApproverResolverFn): void;
|
|
371
|
+
registerConditionOperator(name: string, fn: ConditionOperatorFn): void;
|
|
372
|
+
/** Validate a template config without persisting. Synchronous; never throws. */
|
|
373
|
+
validateTemplate(config: ApprovalTemplateConfig): ValidationResult;
|
|
374
|
+
defineTemplate(config: ApprovalTemplateConfig): Promise<string>;
|
|
375
|
+
/** Update an existing template, incrementing its version. In-flight instances are protected by their templateSnapshot. */
|
|
376
|
+
updateTemplate(config: ApprovalTemplateConfig): Promise<string>;
|
|
377
|
+
getTemplate(name: string): Promise<ApprovalTemplate>;
|
|
378
|
+
listTemplates(): Promise<ApprovalTemplate[]>;
|
|
379
|
+
submit(raw: SubmitOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
380
|
+
approve(instanceId: string, raw: ApproveOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
381
|
+
reject(instanceId: string, raw: RejectOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
382
|
+
delegate(instanceId: string, raw: DelegateOptions, auditCtx?: AuditContext): Promise<void>;
|
|
383
|
+
cancel(instanceId: string, raw: CancelOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
384
|
+
escalate(instanceId: string, raw: EscalateOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
385
|
+
/** Add a comment to an instance without approving or rejecting. */
|
|
386
|
+
addComment(instanceId: string, raw: AddCommentOptions, auditCtx?: AuditContext): Promise<void>;
|
|
387
|
+
/** Resubmit a rejected instance, creating a new linked instance from level 1. */
|
|
388
|
+
resubmit(instanceId: string, raw: ResubmitOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
389
|
+
/** Preview the resolved approval chain for a template and document data, without creating an instance. */
|
|
390
|
+
previewApprovalChain(templateName: string, data: Record<string, unknown>, submittedBy: string): Promise<PreviewResult>;
|
|
391
|
+
/** Check whether a user is eligible to approve a specific instance. Never throws. */
|
|
392
|
+
canApprove(instanceId: string, userId: string): Promise<CanApproveResult>;
|
|
393
|
+
/** Emergency bypass — completes the instance as 'approved', skipping remaining levels. Requires template.allowOverride = true. */
|
|
394
|
+
override(instanceId: string, raw: OverrideOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
395
|
+
/** Approve multiple instances in one call. Never throws — failures collected in result.failed. */
|
|
396
|
+
bulkApprove(instanceIds: string[], raw: ApproveOptions, auditCtx?: AuditContext): Promise<BulkResult>;
|
|
397
|
+
/** Reject multiple instances in one call. Never throws — failures collected in result.failed. */
|
|
398
|
+
bulkReject(instanceIds: string[], raw: RejectOptions, auditCtx?: AuditContext): Promise<BulkResult>;
|
|
399
|
+
getInstance(instanceId: string): Promise<ApprovalInstance>;
|
|
400
|
+
getPendingFor(approverId: string, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
401
|
+
queryInstances(filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
402
|
+
queryInstancesByCursor(filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
|
|
403
|
+
getHistory(instanceId: string): Promise<AuditEntry[]>;
|
|
404
|
+
getCurrentApprovers(instanceId: string): Promise<string[]>;
|
|
405
|
+
/** Check adapter connectivity and escalation scheduler health. */
|
|
406
|
+
healthCheck(): Promise<HealthResult>;
|
|
407
|
+
shutdown(): Promise<void>;
|
|
408
|
+
private escalateInternal;
|
|
409
|
+
private expireInstance;
|
|
410
|
+
private markSlaBreached;
|
|
411
|
+
private revertDelegation;
|
|
412
|
+
/** Read-modify-write with optimistic locking retry. */
|
|
413
|
+
private withOptimisticRetry;
|
|
414
|
+
private requireInstance;
|
|
415
|
+
private currentLevelInstance;
|
|
416
|
+
private findNextLevel;
|
|
417
|
+
private findPreviousLevel;
|
|
418
|
+
private guardBulkSize;
|
|
419
|
+
private runAuthorizationPolicy;
|
|
420
|
+
private runMiddlewareBefore;
|
|
421
|
+
private runMiddlewareAfter;
|
|
422
|
+
private notifyAdapters;
|
|
423
|
+
private runExternalAudit;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export { type SubmitOptions as $, type AddCommentOptions as A, type BulkResult as B, type Clock as C, type DelegateOptions as D, type EscalateOptions as E, type IOperationMiddleware as F, type ISchedulerAdapter as G, type HealthResult as H, type IAuditAdapter as I, type IdGeneratorFn as J, type IdempotencyKeyFn as K, type Logger as L, type LevelAdvancedEvent as M, type MetricName as N, type NotificationEvent as O, type OperationContext as P, type OrgProvider as Q, type OverriddenEvent as R, type OverrideOptions as S, type PreviewChainLevel as T, type PreviewResult as U, type RejectOptions as V, type RejectedEvent as W, type ResubmitOptions as X, type ResubmittedEvent as Y, type RetryPolicy as Z, type SlaBreachedEvent as _, ApprovalConflictError as a, type SubmittedEvent as a0, type ValidationResult as a1, defaultIdGenerator as a2, noopLogger as a3, systemClock as a4, ApprovalEngine as b, type ApprovalEngineOptions as c, ApprovalError as d, type ApprovalEvent as e, type ApprovalEventMap as f, type ApprovalEventName as g, ApprovalForbiddenError as h, ApprovalNotFoundError as i, ApprovalTemplateNotFoundError as j, ApprovalValidationError as k, type ApproveOptions as l, type ApprovedEvent as m, type ApproverResolverFn as n, type AuthorizationContext as o, type CanApproveResult as p, type CancelOptions as q, type CancelledEvent as r, type ConditionOperatorFn as s, type DelegatedEvent as t, type EscalatedEvent as u, type ExpiredEvent as v, type HistoryEntry as w, type IAuthorizationPolicy as x, type IMetricsAdapter as y, type INotificationAdapter as z };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
type ApproverConfig = {
|
|
2
|
+
type: 'user';
|
|
3
|
+
userId: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: 'role';
|
|
6
|
+
role: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'dynamic';
|
|
9
|
+
resolver: string;
|
|
10
|
+
}
|
|
11
|
+
/** Custom approver type registered via engine.registerApproverType(). */
|
|
12
|
+
| {
|
|
13
|
+
type: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
interface ResolvedApprover {
|
|
17
|
+
userId: string;
|
|
18
|
+
source: ApproverConfig;
|
|
19
|
+
}
|
|
20
|
+
type ResolverFn = (submittedBy: string, data: Record<string, unknown>) => Promise<string> | string;
|
|
21
|
+
|
|
22
|
+
type ApprovalMode = 'all' | 'any' | 'majority';
|
|
23
|
+
/** Built-in operators. Use engine.registerConditionOperator() to add custom ones. */
|
|
24
|
+
type ConditionOperator = '>' | '<' | '>=' | '<=' | '==' | '!=' | 'in' | 'not_in' | (string & {});
|
|
25
|
+
interface Condition {
|
|
26
|
+
field: string;
|
|
27
|
+
operator: ConditionOperator;
|
|
28
|
+
value: unknown;
|
|
29
|
+
}
|
|
30
|
+
interface ApprovalLevelConfig {
|
|
31
|
+
level: number;
|
|
32
|
+
name: string;
|
|
33
|
+
approvers: ApproverConfig[];
|
|
34
|
+
mode: ApprovalMode;
|
|
35
|
+
escalationAfterDays?: number;
|
|
36
|
+
}
|
|
37
|
+
interface ConditionRule {
|
|
38
|
+
when: Condition | Condition[];
|
|
39
|
+
addLevels?: ApprovalLevelConfig[];
|
|
40
|
+
skipLevels?: number[];
|
|
41
|
+
}
|
|
42
|
+
interface EscalationConfig {
|
|
43
|
+
afterDays: number;
|
|
44
|
+
escalateTo: ApproverConfig;
|
|
45
|
+
}
|
|
46
|
+
interface ApprovalTemplateConfig {
|
|
47
|
+
name: string;
|
|
48
|
+
documentType: string;
|
|
49
|
+
levels: ApprovalLevelConfig[];
|
|
50
|
+
conditions?: ConditionRule[];
|
|
51
|
+
escalation?: EscalationConfig;
|
|
52
|
+
/** Overall SLA for the entire workflow in days. Emits approval:sla_breached when elapsed. */
|
|
53
|
+
slaDeadlineDays?: number;
|
|
54
|
+
/** Allow emergency override (bypass remaining levels). Must be true to use engine.override(). */
|
|
55
|
+
allowOverride?: boolean;
|
|
56
|
+
}
|
|
57
|
+
interface ApprovalTemplate extends ApprovalTemplateConfig {
|
|
58
|
+
id: string;
|
|
59
|
+
tenantId: string;
|
|
60
|
+
createdAt: Date;
|
|
61
|
+
/** Starts at 1; incremented on each call to engine.updateTemplate(). */
|
|
62
|
+
version: number;
|
|
63
|
+
/** ID of the previous version of this template, for audit trail. */
|
|
64
|
+
previousVersionId?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'cancelled' | 'expired';
|
|
68
|
+
type LevelStatus = 'waiting' | 'pending' | 'approved' | 'rejected' | 'skipped';
|
|
69
|
+
type AuditAction = 'submitted' | 'approved' | 'rejected' | 'delegated' | 'escalated' | 'cancelled' | 'level_advanced' | 'commented' | 'resubmitted' | 'overridden' | 'expired';
|
|
70
|
+
interface AuditEntry {
|
|
71
|
+
action: AuditAction;
|
|
72
|
+
actorId: string;
|
|
73
|
+
actorRole?: string;
|
|
74
|
+
actorIp?: string;
|
|
75
|
+
actorUserAgent?: string;
|
|
76
|
+
level: number;
|
|
77
|
+
timestamp: Date;
|
|
78
|
+
traceId?: string;
|
|
79
|
+
comment?: string;
|
|
80
|
+
reason?: string;
|
|
81
|
+
delegateTo?: string;
|
|
82
|
+
oldValue?: Record<string, unknown>;
|
|
83
|
+
newValue?: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
/** Context injected by the caller on each mutating operation (for SOX/SOC2 compliance). */
|
|
86
|
+
interface AuditContext {
|
|
87
|
+
actorRole?: string;
|
|
88
|
+
actorIp?: string;
|
|
89
|
+
actorUserAgent?: string;
|
|
90
|
+
traceId?: string;
|
|
91
|
+
}
|
|
92
|
+
interface ApprovalLevelInstance {
|
|
93
|
+
level: number;
|
|
94
|
+
name: string;
|
|
95
|
+
mode: ApprovalMode;
|
|
96
|
+
approverConfigs: ApproverConfig[];
|
|
97
|
+
approverIds: string[];
|
|
98
|
+
approvedBy: string[];
|
|
99
|
+
rejectedBy: string[];
|
|
100
|
+
status: LevelStatus;
|
|
101
|
+
escalationDueAt?: Date;
|
|
102
|
+
escalationAfterDays?: number;
|
|
103
|
+
/** Set when a delegation has a time limit — the original approver is restored when this date passes. */
|
|
104
|
+
delegatedUntil?: Date;
|
|
105
|
+
/** The approver who delegated away from this slot; used to revert when delegatedUntil expires. */
|
|
106
|
+
delegatedFrom?: string;
|
|
107
|
+
/** The delegate who received this slot; used to revert when delegatedUntil expires. */
|
|
108
|
+
delegatedTo?: string;
|
|
109
|
+
}
|
|
110
|
+
/** Snapshot of template configuration captured at submit time to insulate in-flight instances from template updates. */
|
|
111
|
+
interface TemplateSnapshot {
|
|
112
|
+
escalation?: EscalationConfig;
|
|
113
|
+
slaDeadlineDays?: number;
|
|
114
|
+
allowOverride?: boolean;
|
|
115
|
+
}
|
|
116
|
+
interface ApprovalInstance {
|
|
117
|
+
id: string;
|
|
118
|
+
tenantId: string;
|
|
119
|
+
templateId: string;
|
|
120
|
+
templateName: string;
|
|
121
|
+
documentId: string;
|
|
122
|
+
documentType: string;
|
|
123
|
+
submittedBy: string;
|
|
124
|
+
status: ApprovalStatus;
|
|
125
|
+
currentLevel: number;
|
|
126
|
+
version: number;
|
|
127
|
+
idempotencyKey?: string;
|
|
128
|
+
levels: ApprovalLevelInstance[];
|
|
129
|
+
auditLog: AuditEntry[];
|
|
130
|
+
data: Record<string, unknown>;
|
|
131
|
+
metadata: Record<string, unknown>;
|
|
132
|
+
createdAt: Date;
|
|
133
|
+
updatedAt: Date;
|
|
134
|
+
/** Snapshot of template config at submit time — prevents template changes from affecting in-flight instances. */
|
|
135
|
+
templateSnapshot?: TemplateSnapshot;
|
|
136
|
+
/** ID of the rejected instance this was resubmitted from. */
|
|
137
|
+
parentInstanceId?: string;
|
|
138
|
+
/** Auto-cancel or auto-reject if not resolved by this time. */
|
|
139
|
+
expiresAt?: Date;
|
|
140
|
+
/** What happens when expiresAt is reached (default: 'cancel'). */
|
|
141
|
+
deadlineAction?: 'cancel' | 'reject';
|
|
142
|
+
/** Set from template.slaDeadlineDays at submit time; breached when passed without resolution. */
|
|
143
|
+
slaDeadlineAt?: Date;
|
|
144
|
+
/** Timestamp when the SLA deadline was first breached; set by the scheduler. */
|
|
145
|
+
slaBreachedAt?: Date;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface PaginationOpts {
|
|
149
|
+
limit: number;
|
|
150
|
+
offset: number;
|
|
151
|
+
}
|
|
152
|
+
interface PaginatedResult<T> {
|
|
153
|
+
items: T[];
|
|
154
|
+
total: number;
|
|
155
|
+
}
|
|
156
|
+
/** Opaque cursor: base64(updatedAt_iso:id). Use the value from nextCursor/prevCursor. */
|
|
157
|
+
interface CursorPaginationOpts {
|
|
158
|
+
cursor?: string;
|
|
159
|
+
limit: number;
|
|
160
|
+
direction?: 'forward' | 'backward';
|
|
161
|
+
}
|
|
162
|
+
interface CursorPaginatedResult<T> {
|
|
163
|
+
items: T[];
|
|
164
|
+
nextCursor?: string;
|
|
165
|
+
prevCursor?: string;
|
|
166
|
+
hasMore: boolean;
|
|
167
|
+
}
|
|
168
|
+
interface InstanceFilter {
|
|
169
|
+
status?: ApprovalStatus;
|
|
170
|
+
documentType?: string;
|
|
171
|
+
submittedBy?: string;
|
|
172
|
+
fromDate?: Date;
|
|
173
|
+
toDate?: Date;
|
|
174
|
+
}
|
|
175
|
+
interface IStorageAdapter {
|
|
176
|
+
saveTemplate(template: ApprovalTemplate): Promise<void>;
|
|
177
|
+
getTemplate(tenantId: string, name: string): Promise<ApprovalTemplate | null>;
|
|
178
|
+
listTemplates(tenantId: string): Promise<ApprovalTemplate[]>;
|
|
179
|
+
saveInstance(instance: ApprovalInstance): Promise<void>;
|
|
180
|
+
/** Conditional update — throws ApprovalConflictError if stored version !== expectedVersion. */
|
|
181
|
+
updateInstance(instance: ApprovalInstance, expectedVersion: number): Promise<void>;
|
|
182
|
+
getInstance(tenantId: string, id: string): Promise<ApprovalInstance | null>;
|
|
183
|
+
getInstancesByApprover(tenantId: string, approverId: string, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
184
|
+
getInstancesByFilter(tenantId: string, filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
185
|
+
/** Optional cursor-based pagination — more efficient than offset at scale. */
|
|
186
|
+
getInstancesByCursor?(tenantId: string, filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
|
|
187
|
+
getOverdueInstances(tenantId: string, asOf: Date): Promise<ApprovalInstance[]>;
|
|
188
|
+
getIdempotentInstance(tenantId: string, idempotencyKey: string): Promise<ApprovalInstance | null>;
|
|
189
|
+
appendAuditEntry(tenantId: string, instanceId: string, entry: AuditEntry): Promise<void>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export type { ApprovalTemplate as A, CursorPaginationOpts as C, EscalationConfig as E, IStorageAdapter as I, LevelStatus as L, PaginationOpts as P, ResolvedApprover as R, TemplateSnapshot as T, ApprovalInstance as a, PaginatedResult as b, InstanceFilter as c, AuditEntry as d, CursorPaginatedResult as e, ApprovalLevelConfig as f, ApprovalLevelInstance as g, ApprovalMode as h, ApprovalStatus as i, ApprovalTemplateConfig as j, ApproverConfig as k, AuditAction as l, AuditContext as m, Condition as n, ConditionOperator as o, ConditionRule as p, ResolverFn as q };
|