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.
@@ -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.cjs';
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 };