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,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 };
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var ApprovalError = class extends Error {
5
+ constructor(message, code) {
6
+ super(message);
7
+ this.code = code;
8
+ this.name = "ApprovalError";
9
+ }
10
+ toJSON() {
11
+ return { code: this.code, message: this.message, name: this.name };
12
+ }
13
+ toHttpStatus() {
14
+ const map = {
15
+ NOT_FOUND: 404,
16
+ CONFLICT: 409,
17
+ FORBIDDEN: 403,
18
+ VALIDATION: 422,
19
+ TEMPLATE_NOT_FOUND: 404
20
+ };
21
+ return map[this.code] ?? 500;
22
+ }
23
+ };
24
+ var ApprovalConflictError = class extends ApprovalError {
25
+ constructor(instanceId) {
26
+ super(
27
+ `Concurrent modification detected on instance "${instanceId}". The record was updated by another process. Please retry.`,
28
+ "CONFLICT"
29
+ );
30
+ this.name = "ApprovalConflictError";
31
+ }
32
+ };
33
+
34
+ // src/adapters/MemoryAdapter.ts
35
+ function deepClone(value) {
36
+ return JSON.parse(JSON.stringify(value));
37
+ }
38
+ function reviveDates(instance) {
39
+ return {
40
+ ...instance,
41
+ createdAt: new Date(instance.createdAt),
42
+ updatedAt: new Date(instance.updatedAt),
43
+ expiresAt: instance.expiresAt ? new Date(instance.expiresAt) : void 0,
44
+ slaDeadlineAt: instance.slaDeadlineAt ? new Date(instance.slaDeadlineAt) : void 0,
45
+ slaBreachedAt: instance.slaBreachedAt ? new Date(instance.slaBreachedAt) : void 0,
46
+ auditLog: instance.auditLog.map((e) => ({ ...e, timestamp: new Date(e.timestamp) })),
47
+ levels: instance.levels.map((l) => {
48
+ const level = { ...l };
49
+ if (l.escalationDueAt) level.escalationDueAt = new Date(l.escalationDueAt);
50
+ if (l.delegatedUntil) level.delegatedUntil = new Date(l.delegatedUntil);
51
+ return level;
52
+ })
53
+ };
54
+ }
55
+ function applyFilter(instance, filter) {
56
+ if (filter.status && instance.status !== filter.status) return false;
57
+ if (filter.documentType && instance.documentType !== filter.documentType) return false;
58
+ if (filter.submittedBy && instance.submittedBy !== filter.submittedBy) return false;
59
+ if (filter.fromDate && instance.createdAt < filter.fromDate) return false;
60
+ if (filter.toDate && instance.createdAt > filter.toDate) return false;
61
+ return true;
62
+ }
63
+ var MemoryAdapter = class {
64
+ constructor() {
65
+ // keyed by `${tenantId}:${template.name}`
66
+ this.templates = /* @__PURE__ */ new Map();
67
+ // keyed by `${tenantId}:${instance.id}`
68
+ this.instances = /* @__PURE__ */ new Map();
69
+ }
70
+ async saveTemplate(template) {
71
+ this.templates.set(`${template.tenantId}:${template.name}`, deepClone(template));
72
+ }
73
+ async getTemplate(tenantId, name) {
74
+ return deepClone(this.templates.get(`${tenantId}:${name}`) ?? null);
75
+ }
76
+ async listTemplates(tenantId) {
77
+ const result = [];
78
+ for (const [key, template] of this.templates) {
79
+ if (key.startsWith(`${tenantId}:`)) {
80
+ result.push(deepClone(template));
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+ async saveInstance(instance) {
86
+ this.instances.set(`${instance.tenantId}:${instance.id}`, deepClone(instance));
87
+ }
88
+ async updateInstance(instance, expectedVersion) {
89
+ const key = `${instance.tenantId}:${instance.id}`;
90
+ const stored = this.instances.get(key);
91
+ if (!stored) throw new ApprovalConflictError(instance.id);
92
+ if (stored.version !== expectedVersion) throw new ApprovalConflictError(instance.id);
93
+ const updated = deepClone(instance);
94
+ updated.version = expectedVersion + 1;
95
+ this.instances.set(key, updated);
96
+ }
97
+ async getInstance(tenantId, id) {
98
+ const raw = this.instances.get(`${tenantId}:${id}`);
99
+ if (!raw) return null;
100
+ return reviveDates(deepClone(raw));
101
+ }
102
+ async getInstancesByApprover(tenantId, approverId, opts) {
103
+ const all = [...this.instances.values()].filter((i) => {
104
+ if (i.tenantId !== tenantId || i.status !== "pending") return false;
105
+ const currentLevel = i.levels.find((l) => l.level === i.currentLevel);
106
+ return currentLevel?.approverIds.includes(approverId) ?? false;
107
+ });
108
+ return paginate(all.map((i) => reviveDates(deepClone(i))), opts);
109
+ }
110
+ async getInstancesByFilter(tenantId, filter, opts) {
111
+ const all = [...this.instances.values()].filter(
112
+ (i) => i.tenantId === tenantId && applyFilter(i, filter)
113
+ );
114
+ return paginate(all.map((i) => reviveDates(deepClone(i))), opts);
115
+ }
116
+ async getOverdueInstances(tenantId, asOf) {
117
+ return [...this.instances.values()].filter((i) => {
118
+ if (i.tenantId !== tenantId || i.status !== "pending") return false;
119
+ const currentLevel = i.levels.find((l) => l.level === i.currentLevel);
120
+ const hasOverdueEscalation = currentLevel?.escalationDueAt != null && new Date(currentLevel.escalationDueAt) <= asOf;
121
+ const isExpired = i.expiresAt != null && new Date(i.expiresAt) <= asOf;
122
+ const hasSLABreach = i.slaDeadlineAt != null && new Date(i.slaDeadlineAt) <= asOf && !i.slaBreachedAt;
123
+ const hasDelegationExpiry = i.levels.some(
124
+ (l) => l.status === "pending" && l.delegatedUntil != null && new Date(l.delegatedUntil) <= asOf && l.delegatedFrom != null
125
+ );
126
+ return hasOverdueEscalation || isExpired || hasSLABreach || hasDelegationExpiry;
127
+ }).map((i) => reviveDates(deepClone(i)));
128
+ }
129
+ async getInstancesByCursor(tenantId, filter, opts) {
130
+ const all = [...this.instances.values()].filter((i) => i.tenantId === tenantId && applyFilter(i, filter)).map((i) => reviveDates(deepClone(i))).sort((a, b) => {
131
+ const ta = a.updatedAt.getTime();
132
+ const tb = b.updatedAt.getTime();
133
+ return ta !== tb ? ta - tb : a.id.localeCompare(b.id);
134
+ });
135
+ const { cursor, limit, direction = "forward" } = opts;
136
+ let startIdx = 0;
137
+ if (cursor) {
138
+ const [ts, id] = decodeCursor(cursor);
139
+ const idx = all.findIndex(
140
+ (i) => i.updatedAt.getTime() > ts || i.updatedAt.getTime() === ts && i.id > id
141
+ );
142
+ startIdx = idx === -1 ? all.length : idx;
143
+ }
144
+ if (direction === "backward" && startIdx > 0) {
145
+ startIdx = Math.max(0, startIdx - limit - 1);
146
+ }
147
+ const slice = all.slice(startIdx, startIdx + limit);
148
+ const hasMore = startIdx + limit < all.length;
149
+ const nextCursor = hasMore ? encodeCursor(slice[slice.length - 1]) : void 0;
150
+ const prevCursor = startIdx > 0 ? encodeCursor(all[startIdx - 1]) : void 0;
151
+ return { items: slice, nextCursor, prevCursor, hasMore };
152
+ }
153
+ async getIdempotentInstance(tenantId, idempotencyKey) {
154
+ for (const instance of this.instances.values()) {
155
+ if (instance.tenantId === tenantId && instance.idempotencyKey === idempotencyKey) {
156
+ return reviveDates(deepClone(instance));
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+ async appendAuditEntry(tenantId, instanceId, entry) {
162
+ const key = `${tenantId}:${instanceId}`;
163
+ const instance = this.instances.get(key);
164
+ if (!instance) return;
165
+ instance.auditLog.push(deepClone(entry));
166
+ instance.updatedAt = new Date(entry.timestamp);
167
+ }
168
+ /** Test helper — total stored instances across all tenants. */
169
+ get size() {
170
+ return this.instances.size;
171
+ }
172
+ };
173
+ function paginate(items, opts) {
174
+ const total = items.length;
175
+ if (!opts) return { items, total };
176
+ return { items: items.slice(opts.offset, opts.offset + opts.limit), total };
177
+ }
178
+ function encodeCursor(instance) {
179
+ return Buffer.from(`${instance.updatedAt.getTime()}|${instance.id}`).toString("base64");
180
+ }
181
+ function decodeCursor(cursor) {
182
+ const decoded = Buffer.from(cursor, "base64").toString("utf8");
183
+ const pipeIdx = decoded.indexOf("|");
184
+ return [Number(decoded.slice(0, pipeIdx)), decoded.slice(pipeIdx + 1)];
185
+ }
186
+
187
+ exports.MemoryAdapter = MemoryAdapter;
188
+ //# sourceMappingURL=MemoryAdapter.cjs.map
189
+ //# sourceMappingURL=MemoryAdapter.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/adapters/MemoryAdapter.ts"],"names":[],"mappings":"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","file":"MemoryAdapter.cjs","sourcesContent":["export class ApprovalError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n ) {\n super(message);\n this.name = 'ApprovalError';\n }\n\n toJSON(): { code: string; message: string; name: string } {\n return { code: this.code, message: this.message, name: this.name };\n }\n\n toHttpStatus(): number {\n const map: Record<string, number> = {\n NOT_FOUND: 404,\n CONFLICT: 409,\n FORBIDDEN: 403,\n VALIDATION: 422,\n TEMPLATE_NOT_FOUND: 404,\n };\n return map[this.code] ?? 500;\n }\n}\n\nexport class ApprovalNotFoundError extends ApprovalError {\n constructor(resource: string, id: string) {\n super(`${resource} \"${id}\" not found.`, 'NOT_FOUND');\n this.name = 'ApprovalNotFoundError';\n }\n}\n\nexport class ApprovalConflictError extends ApprovalError {\n constructor(instanceId: string) {\n super(\n `Concurrent modification detected on instance \"${instanceId}\". The record was updated by another process. Please retry.`,\n 'CONFLICT',\n );\n this.name = 'ApprovalConflictError';\n }\n}\n\nexport class ApprovalForbiddenError extends ApprovalError {\n constructor(message: string) {\n super(message, 'FORBIDDEN');\n this.name = 'ApprovalForbiddenError';\n }\n}\n\nexport class ApprovalValidationError extends ApprovalError {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message, 'VALIDATION');\n this.name = 'ApprovalValidationError';\n }\n}\n\nexport class ApprovalTemplateNotFoundError extends ApprovalError {\n constructor(name: string) {\n super(`Template \"${name}\" not found.`, 'TEMPLATE_NOT_FOUND');\n this.name = 'ApprovalTemplateNotFoundError';\n }\n}\n","import type {\n IStorageAdapter,\n PaginationOpts,\n PaginatedResult,\n InstanceFilter,\n CursorPaginationOpts,\n CursorPaginatedResult,\n} from './IStorageAdapter.js';\nimport type { ApprovalTemplate, ApprovalInstance, AuditEntry } from '../types/index.js';\nimport { ApprovalConflictError } from '../errors.js';\n\nfunction deepClone<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T;\n}\n\nfunction reviveDates(instance: ApprovalInstance): ApprovalInstance {\n return {\n ...instance,\n createdAt: new Date(instance.createdAt),\n updatedAt: new Date(instance.updatedAt),\n expiresAt: instance.expiresAt ? new Date(instance.expiresAt) : undefined,\n slaDeadlineAt: instance.slaDeadlineAt ? new Date(instance.slaDeadlineAt) : undefined,\n slaBreachedAt: instance.slaBreachedAt ? new Date(instance.slaBreachedAt) : undefined,\n auditLog: instance.auditLog.map((e) => ({ ...e, timestamp: new Date(e.timestamp) })),\n levels: instance.levels.map((l) => {\n const level: typeof l = { ...l };\n if (l.escalationDueAt) level.escalationDueAt = new Date(l.escalationDueAt);\n if (l.delegatedUntil) level.delegatedUntil = new Date(l.delegatedUntil);\n return level;\n }),\n };\n}\n\nfunction applyFilter(instance: ApprovalInstance, filter: InstanceFilter): boolean {\n if (filter.status && instance.status !== filter.status) return false;\n if (filter.documentType && instance.documentType !== filter.documentType) return false;\n if (filter.submittedBy && instance.submittedBy !== filter.submittedBy) return false;\n if (filter.fromDate && instance.createdAt < filter.fromDate) return false;\n if (filter.toDate && instance.createdAt > filter.toDate) return false;\n return true;\n}\n\nexport class MemoryAdapter implements IStorageAdapter {\n // keyed by `${tenantId}:${template.name}`\n private templates = new Map<string, ApprovalTemplate>();\n // keyed by `${tenantId}:${instance.id}`\n private instances = new Map<string, ApprovalInstance>();\n\n async saveTemplate(template: ApprovalTemplate): Promise<void> {\n this.templates.set(`${template.tenantId}:${template.name}`, deepClone(template));\n }\n\n async getTemplate(tenantId: string, name: string): Promise<ApprovalTemplate | null> {\n return deepClone(this.templates.get(`${tenantId}:${name}`) ?? null);\n }\n\n async listTemplates(tenantId: string): Promise<ApprovalTemplate[]> {\n const result: ApprovalTemplate[] = [];\n for (const [key, template] of this.templates) {\n if (key.startsWith(`${tenantId}:`)) {\n result.push(deepClone(template));\n }\n }\n return result;\n }\n\n async saveInstance(instance: ApprovalInstance): Promise<void> {\n this.instances.set(`${instance.tenantId}:${instance.id}`, deepClone(instance));\n }\n\n async updateInstance(instance: ApprovalInstance, expectedVersion: number): Promise<void> {\n const key = `${instance.tenantId}:${instance.id}`;\n const stored = this.instances.get(key);\n if (!stored) throw new ApprovalConflictError(instance.id);\n if (stored.version !== expectedVersion) throw new ApprovalConflictError(instance.id);\n const updated = deepClone(instance);\n updated.version = expectedVersion + 1;\n this.instances.set(key, updated);\n }\n\n async getInstance(tenantId: string, id: string): Promise<ApprovalInstance | null> {\n const raw = this.instances.get(`${tenantId}:${id}`);\n if (!raw) return null;\n return reviveDates(deepClone(raw));\n }\n\n async getInstancesByApprover(\n tenantId: string,\n approverId: string,\n opts?: PaginationOpts,\n ): Promise<PaginatedResult<ApprovalInstance>> {\n const all = [...this.instances.values()].filter((i) => {\n if (i.tenantId !== tenantId || i.status !== 'pending') return false;\n // Use .find() by level number, not array index (level numbers may not be consecutive)\n const currentLevel = i.levels.find((l) => l.level === i.currentLevel);\n return currentLevel?.approverIds.includes(approverId) ?? false;\n });\n return paginate(all.map((i) => reviveDates(deepClone(i))), opts);\n }\n\n async getInstancesByFilter(\n tenantId: string,\n filter: InstanceFilter,\n opts?: PaginationOpts,\n ): Promise<PaginatedResult<ApprovalInstance>> {\n const all = [...this.instances.values()].filter(\n (i) => i.tenantId === tenantId && applyFilter(i, filter),\n );\n return paginate(all.map((i) => reviveDates(deepClone(i))), opts);\n }\n\n async getOverdueInstances(tenantId: string, asOf: Date): Promise<ApprovalInstance[]> {\n return [...this.instances.values()]\n .filter((i) => {\n if (i.tenantId !== tenantId || i.status !== 'pending') return false;\n // Escalation overdue\n const currentLevel = i.levels.find((l) => l.level === i.currentLevel);\n const hasOverdueEscalation =\n currentLevel?.escalationDueAt != null && new Date(currentLevel.escalationDueAt) <= asOf;\n // Instance deadline expired\n const isExpired = i.expiresAt != null && new Date(i.expiresAt) <= asOf;\n // SLA breach (not yet recorded)\n const hasSLABreach =\n i.slaDeadlineAt != null && new Date(i.slaDeadlineAt) <= asOf && !i.slaBreachedAt;\n // Delegation expiry on any pending level\n const hasDelegationExpiry = i.levels.some(\n (l) =>\n l.status === 'pending' &&\n l.delegatedUntil != null &&\n new Date(l.delegatedUntil) <= asOf &&\n l.delegatedFrom != null,\n );\n return hasOverdueEscalation || isExpired || hasSLABreach || hasDelegationExpiry;\n })\n .map((i) => reviveDates(deepClone(i)));\n }\n\n async getInstancesByCursor(\n tenantId: string,\n filter: InstanceFilter,\n opts: CursorPaginationOpts,\n ): Promise<CursorPaginatedResult<ApprovalInstance>> {\n const all = [...this.instances.values()]\n .filter((i) => i.tenantId === tenantId && applyFilter(i, filter))\n .map((i) => reviveDates(deepClone(i)))\n .sort((a, b) => {\n const ta = a.updatedAt.getTime();\n const tb = b.updatedAt.getTime();\n return ta !== tb ? ta - tb : a.id.localeCompare(b.id);\n });\n\n const { cursor, limit, direction = 'forward' } = opts;\n let startIdx = 0;\n\n if (cursor) {\n const [ts, id] = decodeCursor(cursor);\n const idx = all.findIndex(\n (i) => i.updatedAt.getTime() > ts || (i.updatedAt.getTime() === ts && i.id > id),\n );\n startIdx = idx === -1 ? all.length : idx;\n }\n\n if (direction === 'backward' && startIdx > 0) {\n startIdx = Math.max(0, startIdx - limit - 1);\n }\n\n const slice = all.slice(startIdx, startIdx + limit);\n const hasMore = startIdx + limit < all.length;\n const nextCursor = hasMore ? encodeCursor(slice[slice.length - 1]!) : undefined;\n const prevCursor = startIdx > 0 ? encodeCursor(all[startIdx - 1]!) : undefined;\n\n return { items: slice, nextCursor, prevCursor, hasMore };\n }\n\n async getIdempotentInstance(\n tenantId: string,\n idempotencyKey: string,\n ): Promise<ApprovalInstance | null> {\n for (const instance of this.instances.values()) {\n if (instance.tenantId === tenantId && instance.idempotencyKey === idempotencyKey) {\n return reviveDates(deepClone(instance));\n }\n }\n return null;\n }\n\n async appendAuditEntry(tenantId: string, instanceId: string, entry: AuditEntry): Promise<void> {\n const key = `${tenantId}:${instanceId}`;\n const instance = this.instances.get(key);\n if (!instance) return;\n instance.auditLog.push(deepClone(entry));\n instance.updatedAt = new Date(entry.timestamp);\n }\n\n /** Test helper — total stored instances across all tenants. */\n get size(): number {\n return this.instances.size;\n }\n}\n\nfunction paginate<T>(items: T[], opts?: PaginationOpts): PaginatedResult<T> {\n const total = items.length;\n if (!opts) return { items, total };\n return { items: items.slice(opts.offset, opts.offset + opts.limit), total };\n}\n\nfunction encodeCursor(instance: ApprovalInstance): string {\n return Buffer.from(`${instance.updatedAt.getTime()}|${instance.id}`).toString('base64');\n}\n\nfunction decodeCursor(cursor: string): [number, string] {\n const decoded = Buffer.from(cursor, 'base64').toString('utf8');\n const pipeIdx = decoded.indexOf('|');\n return [Number(decoded.slice(0, pipeIdx)), decoded.slice(pipeIdx + 1)];\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import { I as IStorageAdapter, A as ApprovalTemplate, a as ApprovalInstance, P as PaginationOpts, b as PaginatedResult, c as InstanceFilter, C as CursorPaginationOpts, e as CursorPaginatedResult, d as AuditEntry } from '../IStorageAdapter-RAiLF8bc.cjs';
2
+
3
+ declare class MemoryAdapter implements IStorageAdapter {
4
+ private templates;
5
+ private instances;
6
+ saveTemplate(template: ApprovalTemplate): Promise<void>;
7
+ getTemplate(tenantId: string, name: string): Promise<ApprovalTemplate | null>;
8
+ listTemplates(tenantId: string): Promise<ApprovalTemplate[]>;
9
+ saveInstance(instance: ApprovalInstance): Promise<void>;
10
+ updateInstance(instance: ApprovalInstance, expectedVersion: number): Promise<void>;
11
+ getInstance(tenantId: string, id: string): Promise<ApprovalInstance | null>;
12
+ getInstancesByApprover(tenantId: string, approverId: string, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
13
+ getInstancesByFilter(tenantId: string, filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
14
+ getOverdueInstances(tenantId: string, asOf: Date): Promise<ApprovalInstance[]>;
15
+ getInstancesByCursor(tenantId: string, filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
16
+ getIdempotentInstance(tenantId: string, idempotencyKey: string): Promise<ApprovalInstance | null>;
17
+ appendAuditEntry(tenantId: string, instanceId: string, entry: AuditEntry): Promise<void>;
18
+ /** Test helper — total stored instances across all tenants. */
19
+ get size(): number;
20
+ }
21
+
22
+ export { MemoryAdapter };
@@ -0,0 +1,22 @@
1
+ import { I as IStorageAdapter, A as ApprovalTemplate, a as ApprovalInstance, P as PaginationOpts, b as PaginatedResult, c as InstanceFilter, C as CursorPaginationOpts, e as CursorPaginatedResult, d as AuditEntry } from '../IStorageAdapter-RAiLF8bc.js';
2
+
3
+ declare class MemoryAdapter implements IStorageAdapter {
4
+ private templates;
5
+ private instances;
6
+ saveTemplate(template: ApprovalTemplate): Promise<void>;
7
+ getTemplate(tenantId: string, name: string): Promise<ApprovalTemplate | null>;
8
+ listTemplates(tenantId: string): Promise<ApprovalTemplate[]>;
9
+ saveInstance(instance: ApprovalInstance): Promise<void>;
10
+ updateInstance(instance: ApprovalInstance, expectedVersion: number): Promise<void>;
11
+ getInstance(tenantId: string, id: string): Promise<ApprovalInstance | null>;
12
+ getInstancesByApprover(tenantId: string, approverId: string, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
13
+ getInstancesByFilter(tenantId: string, filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
14
+ getOverdueInstances(tenantId: string, asOf: Date): Promise<ApprovalInstance[]>;
15
+ getInstancesByCursor(tenantId: string, filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
16
+ getIdempotentInstance(tenantId: string, idempotencyKey: string): Promise<ApprovalInstance | null>;
17
+ appendAuditEntry(tenantId: string, instanceId: string, entry: AuditEntry): Promise<void>;
18
+ /** Test helper — total stored instances across all tenants. */
19
+ get size(): number;
20
+ }
21
+
22
+ export { MemoryAdapter };
@@ -0,0 +1,187 @@
1
+ // src/errors.ts
2
+ var ApprovalError = class extends Error {
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "ApprovalError";
7
+ }
8
+ toJSON() {
9
+ return { code: this.code, message: this.message, name: this.name };
10
+ }
11
+ toHttpStatus() {
12
+ const map = {
13
+ NOT_FOUND: 404,
14
+ CONFLICT: 409,
15
+ FORBIDDEN: 403,
16
+ VALIDATION: 422,
17
+ TEMPLATE_NOT_FOUND: 404
18
+ };
19
+ return map[this.code] ?? 500;
20
+ }
21
+ };
22
+ var ApprovalConflictError = class extends ApprovalError {
23
+ constructor(instanceId) {
24
+ super(
25
+ `Concurrent modification detected on instance "${instanceId}". The record was updated by another process. Please retry.`,
26
+ "CONFLICT"
27
+ );
28
+ this.name = "ApprovalConflictError";
29
+ }
30
+ };
31
+
32
+ // src/adapters/MemoryAdapter.ts
33
+ function deepClone(value) {
34
+ return JSON.parse(JSON.stringify(value));
35
+ }
36
+ function reviveDates(instance) {
37
+ return {
38
+ ...instance,
39
+ createdAt: new Date(instance.createdAt),
40
+ updatedAt: new Date(instance.updatedAt),
41
+ expiresAt: instance.expiresAt ? new Date(instance.expiresAt) : void 0,
42
+ slaDeadlineAt: instance.slaDeadlineAt ? new Date(instance.slaDeadlineAt) : void 0,
43
+ slaBreachedAt: instance.slaBreachedAt ? new Date(instance.slaBreachedAt) : void 0,
44
+ auditLog: instance.auditLog.map((e) => ({ ...e, timestamp: new Date(e.timestamp) })),
45
+ levels: instance.levels.map((l) => {
46
+ const level = { ...l };
47
+ if (l.escalationDueAt) level.escalationDueAt = new Date(l.escalationDueAt);
48
+ if (l.delegatedUntil) level.delegatedUntil = new Date(l.delegatedUntil);
49
+ return level;
50
+ })
51
+ };
52
+ }
53
+ function applyFilter(instance, filter) {
54
+ if (filter.status && instance.status !== filter.status) return false;
55
+ if (filter.documentType && instance.documentType !== filter.documentType) return false;
56
+ if (filter.submittedBy && instance.submittedBy !== filter.submittedBy) return false;
57
+ if (filter.fromDate && instance.createdAt < filter.fromDate) return false;
58
+ if (filter.toDate && instance.createdAt > filter.toDate) return false;
59
+ return true;
60
+ }
61
+ var MemoryAdapter = class {
62
+ constructor() {
63
+ // keyed by `${tenantId}:${template.name}`
64
+ this.templates = /* @__PURE__ */ new Map();
65
+ // keyed by `${tenantId}:${instance.id}`
66
+ this.instances = /* @__PURE__ */ new Map();
67
+ }
68
+ async saveTemplate(template) {
69
+ this.templates.set(`${template.tenantId}:${template.name}`, deepClone(template));
70
+ }
71
+ async getTemplate(tenantId, name) {
72
+ return deepClone(this.templates.get(`${tenantId}:${name}`) ?? null);
73
+ }
74
+ async listTemplates(tenantId) {
75
+ const result = [];
76
+ for (const [key, template] of this.templates) {
77
+ if (key.startsWith(`${tenantId}:`)) {
78
+ result.push(deepClone(template));
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ async saveInstance(instance) {
84
+ this.instances.set(`${instance.tenantId}:${instance.id}`, deepClone(instance));
85
+ }
86
+ async updateInstance(instance, expectedVersion) {
87
+ const key = `${instance.tenantId}:${instance.id}`;
88
+ const stored = this.instances.get(key);
89
+ if (!stored) throw new ApprovalConflictError(instance.id);
90
+ if (stored.version !== expectedVersion) throw new ApprovalConflictError(instance.id);
91
+ const updated = deepClone(instance);
92
+ updated.version = expectedVersion + 1;
93
+ this.instances.set(key, updated);
94
+ }
95
+ async getInstance(tenantId, id) {
96
+ const raw = this.instances.get(`${tenantId}:${id}`);
97
+ if (!raw) return null;
98
+ return reviveDates(deepClone(raw));
99
+ }
100
+ async getInstancesByApprover(tenantId, approverId, opts) {
101
+ const all = [...this.instances.values()].filter((i) => {
102
+ if (i.tenantId !== tenantId || i.status !== "pending") return false;
103
+ const currentLevel = i.levels.find((l) => l.level === i.currentLevel);
104
+ return currentLevel?.approverIds.includes(approverId) ?? false;
105
+ });
106
+ return paginate(all.map((i) => reviveDates(deepClone(i))), opts);
107
+ }
108
+ async getInstancesByFilter(tenantId, filter, opts) {
109
+ const all = [...this.instances.values()].filter(
110
+ (i) => i.tenantId === tenantId && applyFilter(i, filter)
111
+ );
112
+ return paginate(all.map((i) => reviveDates(deepClone(i))), opts);
113
+ }
114
+ async getOverdueInstances(tenantId, asOf) {
115
+ return [...this.instances.values()].filter((i) => {
116
+ if (i.tenantId !== tenantId || i.status !== "pending") return false;
117
+ const currentLevel = i.levels.find((l) => l.level === i.currentLevel);
118
+ const hasOverdueEscalation = currentLevel?.escalationDueAt != null && new Date(currentLevel.escalationDueAt) <= asOf;
119
+ const isExpired = i.expiresAt != null && new Date(i.expiresAt) <= asOf;
120
+ const hasSLABreach = i.slaDeadlineAt != null && new Date(i.slaDeadlineAt) <= asOf && !i.slaBreachedAt;
121
+ const hasDelegationExpiry = i.levels.some(
122
+ (l) => l.status === "pending" && l.delegatedUntil != null && new Date(l.delegatedUntil) <= asOf && l.delegatedFrom != null
123
+ );
124
+ return hasOverdueEscalation || isExpired || hasSLABreach || hasDelegationExpiry;
125
+ }).map((i) => reviveDates(deepClone(i)));
126
+ }
127
+ async getInstancesByCursor(tenantId, filter, opts) {
128
+ const all = [...this.instances.values()].filter((i) => i.tenantId === tenantId && applyFilter(i, filter)).map((i) => reviveDates(deepClone(i))).sort((a, b) => {
129
+ const ta = a.updatedAt.getTime();
130
+ const tb = b.updatedAt.getTime();
131
+ return ta !== tb ? ta - tb : a.id.localeCompare(b.id);
132
+ });
133
+ const { cursor, limit, direction = "forward" } = opts;
134
+ let startIdx = 0;
135
+ if (cursor) {
136
+ const [ts, id] = decodeCursor(cursor);
137
+ const idx = all.findIndex(
138
+ (i) => i.updatedAt.getTime() > ts || i.updatedAt.getTime() === ts && i.id > id
139
+ );
140
+ startIdx = idx === -1 ? all.length : idx;
141
+ }
142
+ if (direction === "backward" && startIdx > 0) {
143
+ startIdx = Math.max(0, startIdx - limit - 1);
144
+ }
145
+ const slice = all.slice(startIdx, startIdx + limit);
146
+ const hasMore = startIdx + limit < all.length;
147
+ const nextCursor = hasMore ? encodeCursor(slice[slice.length - 1]) : void 0;
148
+ const prevCursor = startIdx > 0 ? encodeCursor(all[startIdx - 1]) : void 0;
149
+ return { items: slice, nextCursor, prevCursor, hasMore };
150
+ }
151
+ async getIdempotentInstance(tenantId, idempotencyKey) {
152
+ for (const instance of this.instances.values()) {
153
+ if (instance.tenantId === tenantId && instance.idempotencyKey === idempotencyKey) {
154
+ return reviveDates(deepClone(instance));
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+ async appendAuditEntry(tenantId, instanceId, entry) {
160
+ const key = `${tenantId}:${instanceId}`;
161
+ const instance = this.instances.get(key);
162
+ if (!instance) return;
163
+ instance.auditLog.push(deepClone(entry));
164
+ instance.updatedAt = new Date(entry.timestamp);
165
+ }
166
+ /** Test helper — total stored instances across all tenants. */
167
+ get size() {
168
+ return this.instances.size;
169
+ }
170
+ };
171
+ function paginate(items, opts) {
172
+ const total = items.length;
173
+ if (!opts) return { items, total };
174
+ return { items: items.slice(opts.offset, opts.offset + opts.limit), total };
175
+ }
176
+ function encodeCursor(instance) {
177
+ return Buffer.from(`${instance.updatedAt.getTime()}|${instance.id}`).toString("base64");
178
+ }
179
+ function decodeCursor(cursor) {
180
+ const decoded = Buffer.from(cursor, "base64").toString("utf8");
181
+ const pipeIdx = decoded.indexOf("|");
182
+ return [Number(decoded.slice(0, pipeIdx)), decoded.slice(pipeIdx + 1)];
183
+ }
184
+
185
+ export { MemoryAdapter };
186
+ //# sourceMappingURL=MemoryAdapter.js.map
187
+ //# sourceMappingURL=MemoryAdapter.js.map