@nodii/approval 0.0.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +40 -2
  2. package/dist/consumer.d.ts +19 -0
  3. package/dist/consumer.d.ts.map +1 -0
  4. package/dist/consumer.js +274 -0
  5. package/dist/consumer.js.map +1 -0
  6. package/dist/deferred-actions.d.ts +34 -0
  7. package/dist/deferred-actions.d.ts.map +1 -0
  8. package/dist/deferred-actions.js +126 -0
  9. package/dist/deferred-actions.js.map +1 -0
  10. package/dist/errors.d.ts +51 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +82 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/handlers.d.ts +7 -0
  15. package/dist/handlers.d.ts.map +1 -0
  16. package/dist/handlers.js +20 -0
  17. package/dist/handlers.js.map +1 -0
  18. package/dist/index.d.ts +13 -2
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +15 -1
  21. package/dist/index.js.map +1 -0
  22. package/dist/init.d.ts +21 -0
  23. package/dist/init.d.ts.map +1 -0
  24. package/dist/init.js +139 -0
  25. package/dist/init.js.map +1 -0
  26. package/dist/kinds.d.ts +20 -0
  27. package/dist/kinds.d.ts.map +1 -0
  28. package/dist/kinds.js +72 -0
  29. package/dist/kinds.js.map +1 -0
  30. package/dist/migrations.d.ts +27 -0
  31. package/dist/migrations.d.ts.map +1 -0
  32. package/dist/migrations.js +67 -0
  33. package/dist/migrations.js.map +1 -0
  34. package/dist/policies.d.ts +21 -0
  35. package/dist/policies.d.ts.map +1 -0
  36. package/dist/policies.js +99 -0
  37. package/dist/policies.js.map +1 -0
  38. package/dist/request.d.ts +3 -0
  39. package/dist/request.d.ts.map +1 -0
  40. package/dist/request.js +356 -0
  41. package/dist/request.js.map +1 -0
  42. package/dist/task-tracking-client.d.ts +39 -0
  43. package/dist/task-tracking-client.d.ts.map +1 -0
  44. package/dist/task-tracking-client.js +100 -0
  45. package/dist/task-tracking-client.js.map +1 -0
  46. package/dist/telemetry-shim.d.ts +35 -0
  47. package/dist/telemetry-shim.d.ts.map +1 -0
  48. package/dist/telemetry-shim.js +88 -0
  49. package/dist/telemetry-shim.js.map +1 -0
  50. package/dist/types.d.ts +279 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +18 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/validation.d.ts +5 -0
  55. package/dist/validation.d.ts.map +1 -0
  56. package/dist/validation.js +40 -0
  57. package/dist/validation.js.map +1 -0
  58. package/package.json +44 -3
  59. package/src/migrations/001-approval-policies.sql +27 -0
  60. package/src/migrations/002-deferred-actions.sql +41 -0
package/README.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @nodii/approval
2
2
 
3
- Placeholder package at v0.0.1. Real implementation ships at v0.1.0 per the locked feature_doc.
3
+ Approval library for the Nodii microservice stack. Implements the platform-wide
4
+ approval contract locked in `10-approval-doctrine.md`. Polyglot ship: TS + Python + Go
5
+ at v0.1.0.
4
6
 
5
- **Spec**: `https://planning.dev.nucleus-cloud.in/api/v1/feature-docs?serviceId=nodii-libs&docKey=approval`
7
+ ## Public surface (v0.1.0)
8
+
9
+ - `defineApprovalKinds(serviceName, defs)` — type-safe kind registry per spec § 5.3.
10
+ - `initApproval({...})` — boot-time wiring (pg, redis, telemetry, task-tracking).
11
+ - `bindApprovalHandlers({...})` — register `onApproved` / `onRejected` / `onExpired`.
12
+ - `requestApproval({...})` — the canonical 7-step request sequence per spec § 5.2.
13
+ - `startApprovalConsumer({...})` / `stopApprovalConsumer({...})` — BullMQ-leased
14
+ completion-event consumer per spec § 5.8.
15
+ - `getApprovalMigrationSQL(serviceName)` — emit per-service `<service>_approval_policies`
16
+ + `<service>_deferred_actions` migration SQL per spec § 5.4 / § 5.6.
17
+
18
+ Spec: planning hub `feature_doc` `docKey=approval`. See
19
+ [`10-approval-doctrine.md`](../../../nodii-planning-hub/docs/planning/global/10-approval-doctrine.md)
20
+ for the canonical contract.
21
+
22
+ ## Boot
23
+
24
+ ```typescript
25
+ import { initApproval, startApprovalConsumer } from "@nodii/approval";
26
+ import Redis from "ioredis";
27
+ import postgres from "postgres";
28
+
29
+ const pgPool = postgres(process.env.DATABASE_URL!);
30
+ const redis = new Redis(process.env.REDIS_URL!);
31
+
32
+ await initApproval({
33
+ serviceName: "billing",
34
+ pgPool,
35
+ redis,
36
+ taskServiceUrl: process.env.TASK_TRACKING_GRPC_URL!,
37
+ });
38
+
39
+ await startApprovalConsumer({ serviceName: "billing" });
40
+ ```
41
+
42
+ See `tests/integration.test.ts` for an end-to-end example that wires the
43
+ library against postgres + redis + an in-process task-tracking gRPC stub.
@@ -0,0 +1,19 @@
1
+ import { Worker } from "bullmq";
2
+ export interface StartApprovalConsumerOpts {
3
+ /** Defaults to `<serviceName>-approval-completions`. */
4
+ queueName?: string;
5
+ /** Defaults to `cfg.consumerMaxRetries`. */
6
+ maxAttempts?: number;
7
+ }
8
+ export interface ApprovalConsumerHandle {
9
+ worker: Worker;
10
+ close(timeoutMs?: number): Promise<void>;
11
+ }
12
+ export declare function startApprovalConsumer(opts?: StartApprovalConsumerOpts): Promise<ApprovalConsumerHandle>;
13
+ export declare function stopApprovalConsumer(opts?: {
14
+ timeoutMs?: number;
15
+ }): Promise<void>;
16
+ export declare function defaultQueueName(serviceName: string): string;
17
+ /** Test-only reset — closes the active worker if any. */
18
+ export declare function _resetConsumerForTests(): void;
19
+ //# sourceMappingURL=consumer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../src/consumer.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,MAAM,EAAY,MAAM,QAAQ,CAAC;AAmB1C,MAAM,WAAW,yBAAyB;IACxC,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAID,wBAAsB,qBAAqB,CACzC,IAAI,GAAE,yBAA8B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAyDjC;AAED,wBAAsB,oBAAoB,CACxC,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAChC,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5D;AAkPD,yDAAyD;AACzD,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
@@ -0,0 +1,274 @@
1
+ // BullMQ-leased completion-event consumer per spec § 5.8.
2
+ //
3
+ // Subscribes to `tasks.task.completed.v1` and dispatches the registered
4
+ // handler for each approval task that targets this service. Apply-tx
5
+ // wraps status-update + handler invocation + audit emit + dedupe write
6
+ // in one transaction.
7
+ //
8
+ // v0.1.0 scope: the consumer is a BullMQ Worker. Adopting services
9
+ // publish completion events into the `tasks.task.completed.v1` queue
10
+ // (either via a fan-out from task-tracking's outbox drainer, or via a
11
+ // direct BullMQ producer in integration tests). We do NOT own the
12
+ // publisher side — task-tracking does.
13
+ import { Worker } from "bullmq";
14
+ import { applyDecision, findById, findByTaskId, markExecuted, markFailed, } from "./deferred-actions";
15
+ import { DeferredActionNotFound } from "./errors";
16
+ import { requireApproval, requireHandlers } from "./init";
17
+ import { emitAudit, inc, observe } from "./telemetry-shim";
18
+ let CURRENT = null;
19
+ export async function startApprovalConsumer(opts = {}) {
20
+ if (CURRENT) {
21
+ return CURRENT;
22
+ }
23
+ const cfg = requireApproval();
24
+ const queue = opts.queueName ?? defaultQueueName(cfg.serviceName);
25
+ const maxAttempts = opts.maxAttempts ?? cfg.consumerMaxRetries;
26
+ // BullMQ's Connection type is pinned to its own ioredis version; the
27
+ // workspace may resolve a different patch. Cast through `unknown` to
28
+ // accept the user-supplied Redis as long as it has the same structural
29
+ // surface BullMQ uses internally.
30
+ const worker = new Worker(queue, async (job) => {
31
+ await consumeCompletionEvent(cfg, job);
32
+ }, {
33
+ connection: cfg.redis,
34
+ concurrency: 1, // single-flight per service per spec § 5.8 lease semantics
35
+ lockDuration: cfg.consumerLeaseSeconds * 1000,
36
+ autorun: true,
37
+ removeOnComplete: { count: 1000 },
38
+ removeOnFail: { count: 1000 },
39
+ settings: {
40
+ backoffStrategy: (attempts) => Math.min(60_000, 2 ** attempts * 250),
41
+ },
42
+ });
43
+ worker.on("failed", (job, err) => {
44
+ cfg.logger.warn("approval.consumer.job_failed", {
45
+ job_id: job?.id,
46
+ attempts: job?.attemptsMade,
47
+ max_attempts: maxAttempts,
48
+ error: err.message,
49
+ });
50
+ });
51
+ CURRENT = {
52
+ worker,
53
+ async close(timeoutMs = 30_000) {
54
+ try {
55
+ await Promise.race([
56
+ worker.close(),
57
+ new Promise((resolve) => setTimeout(resolve, timeoutMs).unref()),
58
+ ]);
59
+ }
60
+ finally {
61
+ CURRENT = null;
62
+ }
63
+ },
64
+ };
65
+ return CURRENT;
66
+ }
67
+ export async function stopApprovalConsumer(opts = {}) {
68
+ if (!CURRENT)
69
+ return;
70
+ await CURRENT.close(opts.timeoutMs);
71
+ }
72
+ export function defaultQueueName(serviceName) {
73
+ return `${serviceName}-approval-completions`;
74
+ }
75
+ async function consumeCompletionEvent(cfg, job) {
76
+ const event = job.data;
77
+ const startMs = Date.now();
78
+ // Dedupe via consumed_events.
79
+ const already = await isAlreadyConsumed(cfg, event.eventId, event.topic);
80
+ if (already) {
81
+ cfg.logger.info("approval.consumer.dedupe_skip", {
82
+ event_id: event.eventId,
83
+ });
84
+ return;
85
+ }
86
+ // Filter to approval kinds for this service only.
87
+ if (!event.kind.startsWith(`approval:${cfg.serviceName}.`)) {
88
+ await markConsumed(cfg, event.eventId, event.topic);
89
+ return;
90
+ }
91
+ // Resolve the deferred-action row.
92
+ const pg = cfg.pgPool;
93
+ let row = await findByTaskId(cfg, pg, event.taskId);
94
+ if (!row) {
95
+ const fallbackId = typeof event.metadata?.deferredActionId === "string"
96
+ ? event.metadata.deferredActionId
97
+ : null;
98
+ if (fallbackId) {
99
+ row = await findById(cfg, pg, fallbackId);
100
+ }
101
+ }
102
+ if (!row) {
103
+ // No row found → no tenant id available. Pass null rather than the
104
+ // literal string "unknown" so downstream audit-chain queries
105
+ // filtering by tenant_id don't mistake this for a real tenant value.
106
+ emitAudit({
107
+ tenantId: null,
108
+ action: `${cfg.serviceName}.approval.completion_unknown`,
109
+ metadata: { task_id: event.taskId, event_id: event.eventId },
110
+ });
111
+ inc({
112
+ name: "approval_completion_unknown_total",
113
+ family: "async_worker",
114
+ labels: { service: cfg.serviceName },
115
+ });
116
+ await markConsumed(cfg, event.eventId, event.topic);
117
+ throw new DeferredActionNotFound(event.taskId);
118
+ }
119
+ // Apply tx: status update + handler dispatch + audit + dedupe insert.
120
+ // If no handler is registered for this kind, treat it as a terminal
121
+ // failure — mark the row failed, write the dedupe row to stop BullMQ
122
+ // from retrying forever, and emit the matching audit + metric.
123
+ const handlers = requireHandlers();
124
+ const handlerSet = handlers.get(row.kind);
125
+ if (!handlerSet) {
126
+ const reason = `HandlerNotRegistered: no handler for kind '${row.kind}'`;
127
+ await markFailed(cfg, pg, row.id, reason);
128
+ emitAudit({
129
+ tenantId: row.tenant_id,
130
+ action: `${cfg.serviceName}.approval.execution_failed`,
131
+ targetKind: "deferred_action",
132
+ targetId: row.id,
133
+ metadata: { kind: row.kind, error: reason },
134
+ });
135
+ inc({
136
+ name: "approval_handler_failures_total",
137
+ family: "async_worker",
138
+ labels: {
139
+ service: cfg.serviceName,
140
+ kind: row.kind,
141
+ decision: event.outcome,
142
+ },
143
+ });
144
+ await markConsumed(cfg, event.eventId, event.topic);
145
+ // Terminal — return so BullMQ stops retrying (matches the retry-
146
+ // exhaustion branch below). Operator recovery via RetryApproval RPC.
147
+ cfg.logger.error("approval.consumer.handler_not_registered", {
148
+ kind: row.kind,
149
+ row_id: row.id,
150
+ });
151
+ return;
152
+ }
153
+ const ctx = {
154
+ tenantId: row.tenant_id,
155
+ deferredActionId: row.id,
156
+ deferredActionPayload: row.deferred_action_payload,
157
+ requesterMembershipId: row.requester_membership_id,
158
+ approverMembershipId: event.completedByMembershipId,
159
+ approverNote: event.completionNote,
160
+ approverKind: "user",
161
+ kind: row.kind,
162
+ metadata: row.metadata,
163
+ };
164
+ try {
165
+ await pg.begin(async (tx) => {
166
+ const updated = await applyDecision(cfg, tx, row.id, event.outcome, event.completedByMembershipId, event.completionNote);
167
+ // Idempotency guard: if 0 rows updated, the row already moved past
168
+ // `pending` — a prior duplicate completion event already ran the
169
+ // handler. Skip rather than re-fire (consumed_events dedupe +
170
+ // status='pending' guard = exactly-once handler invocation).
171
+ if (updated === 0) {
172
+ cfg.logger.info("approval.consumer.duplicate_completion_skipped", {
173
+ row_id: row.id,
174
+ event_id: event.eventId,
175
+ task_id: event.taskId,
176
+ });
177
+ await tx.unsafe(`INSERT INTO consumed_events (event_id, topic) VALUES ($1, $2)
178
+ ON CONFLICT (event_id, topic) DO NOTHING`, [event.eventId, event.topic]);
179
+ return;
180
+ }
181
+ if (event.outcome === "approved") {
182
+ await handlerSet.onApproved(ctx);
183
+ }
184
+ else if (event.outcome === "rejected") {
185
+ await handlerSet.onRejected(ctx);
186
+ }
187
+ else {
188
+ await handlerSet.onExpired(ctx);
189
+ }
190
+ await markExecuted(cfg, tx, row.id);
191
+ const auditAction = event.outcome === "approved"
192
+ ? `${cfg.serviceName}.approval.executed`
193
+ : `${cfg.serviceName}.approval.aborted`;
194
+ emitAudit({
195
+ tenantId: row.tenant_id,
196
+ action: auditAction,
197
+ targetKind: "deferred_action",
198
+ targetId: row.id,
199
+ // Event metadata first; lib-mandated fields last so they win.
200
+ metadata: {
201
+ ...event.metadata,
202
+ kind: row.kind,
203
+ outcome: event.outcome,
204
+ approver_membership_id: event.completedByMembershipId,
205
+ },
206
+ });
207
+ await tx.unsafe(`INSERT INTO consumed_events (event_id, topic) VALUES ($1, $2)
208
+ ON CONFLICT (event_id, topic) DO NOTHING`, [event.eventId, event.topic]);
209
+ });
210
+ }
211
+ catch (handlerErr) {
212
+ // Apply tx rolled back here — status reverts to whatever was before
213
+ // applyDecision ran, consumed_events row was NOT written, and the
214
+ // handler failure needs separate handling. Enforce consumerMaxRetries
215
+ // at this layer (BullMQ's per-job `attempts` is producer-controlled,
216
+ // so we cap on attemptsMade ourselves).
217
+ const reason = handlerErr instanceof Error ? handlerErr.message : String(handlerErr);
218
+ const attemptsMade = job.attemptsMade ?? 0;
219
+ const exhausted = attemptsMade + 1 >= cfg.consumerMaxRetries;
220
+ if (exhausted) {
221
+ // Terminal failure — mark + dedupe + don't re-throw so BullMQ stops.
222
+ await markFailed(cfg, pg, row.id, reason);
223
+ await markConsumed(cfg, event.eventId, event.topic);
224
+ emitAudit({
225
+ tenantId: row.tenant_id,
226
+ action: `${cfg.serviceName}.approval.execution_failed`,
227
+ targetKind: "deferred_action",
228
+ targetId: row.id,
229
+ metadata: { kind: row.kind, error: reason, attempts: attemptsMade + 1 },
230
+ });
231
+ inc({
232
+ name: "approval_handler_failures_total",
233
+ family: "async_worker",
234
+ labels: {
235
+ service: cfg.serviceName,
236
+ kind: row.kind,
237
+ decision: event.outcome,
238
+ },
239
+ });
240
+ return; // do not re-throw — operator-recoverable via RetryApproval RPC
241
+ }
242
+ // Transient — re-throw so BullMQ retries with backoff. The status is
243
+ // already rolled back by the apply tx; next attempt re-enters cleanly.
244
+ throw handlerErr;
245
+ }
246
+ observe({
247
+ name: "approval_execution_duration_seconds",
248
+ family: "async_worker",
249
+ labels: { service: cfg.serviceName, kind: row.kind },
250
+ value: (Date.now() - startMs) / 1000,
251
+ });
252
+ if (event.publishedAt) {
253
+ const lag = (Date.now() - new Date(event.publishedAt).getTime()) / 1000;
254
+ observe({
255
+ name: "approval_consumer_lag_seconds",
256
+ family: "async_worker",
257
+ labels: { service: cfg.serviceName },
258
+ value: lag,
259
+ });
260
+ }
261
+ }
262
+ async function isAlreadyConsumed(cfg, eventId, topic) {
263
+ const rows = await cfg.pgPool.unsafe("SELECT 1 FROM consumed_events WHERE event_id = $1 AND topic = $2 LIMIT 1", [eventId, topic]);
264
+ return Array.isArray(rows) && rows.length > 0;
265
+ }
266
+ async function markConsumed(cfg, eventId, topic) {
267
+ await cfg.pgPool.unsafe(`INSERT INTO consumed_events (event_id, topic) VALUES ($1, $2)
268
+ ON CONFLICT (event_id, topic) DO NOTHING`, [eventId, topic]);
269
+ }
270
+ /** Test-only reset — closes the active worker if any. */
271
+ export function _resetConsumerForTests() {
272
+ CURRENT = null;
273
+ }
274
+ //# sourceMappingURL=consumer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer.js","sourceRoot":"","sources":["../src/consumer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { DeferredActionRow, ResolvedApprovalConfig, SqlExecutor, SqlTxExecutor } from "./types";
2
+ export interface InsertDeferredActionInput {
3
+ tenantId: string;
4
+ kind: string;
5
+ requesterMembershipId: string;
6
+ deferredActionPayload: Record<string, unknown>;
7
+ idempotencyKey: string;
8
+ metadata: Record<string, unknown>;
9
+ auditCorrelationId: string | null;
10
+ }
11
+ /** Insert a fresh `pending` deferred-action row. Returns the inserted row.
12
+ * Used by `requestApproval` step [5]. */
13
+ export declare function insertDeferredAction(cfg: ResolvedApprovalConfig, tx: SqlTxExecutor, input: InsertDeferredActionInput): Promise<DeferredActionRow>;
14
+ /** Update the row's `task_id` after task-tracking returns. */
15
+ export declare function setTaskId(cfg: ResolvedApprovalConfig, tx: SqlTxExecutor, rowId: string, taskId: string): Promise<void>;
16
+ /** Look up a deferred-action row by task_id. Returns null if not found. */
17
+ export declare function findByTaskId(cfg: ResolvedApprovalConfig, pg: SqlExecutor, taskId: string): Promise<DeferredActionRow | null>;
18
+ /** Look up a deferred-action row by id. */
19
+ export declare function findById(cfg: ResolvedApprovalConfig, pg: SqlExecutor, id: string): Promise<DeferredActionRow | null>;
20
+ /** Atomic transition to `approved` | `rejected` | `expired` with decision
21
+ * metadata. Used by the completion-event consumer.
22
+ *
23
+ * Returns the number of rows updated. The caller short-circuits when 0,
24
+ * which happens if a duplicate completion event with a different event_id
25
+ * but the same task_id is delivered — the first event already moved the
26
+ * row past `pending`, and the handler must NOT re-fire. (Per
27
+ * consumed_events dedupe + this WHERE guard, the handler runs exactly
28
+ * once even under producer-side double-publish.) */
29
+ export declare function applyDecision(cfg: ResolvedApprovalConfig, tx: SqlTxExecutor, rowId: string, newStatus: "approved" | "rejected" | "expired", approverMembershipId: string | null, approverNote: string | null): Promise<number>;
30
+ /** Final transition to `executed` (handler ran clean). */
31
+ export declare function markExecuted(cfg: ResolvedApprovalConfig, tx: SqlTxExecutor, rowId: string): Promise<void>;
32
+ /** Final transition to `failed` (handler threw post-retries). */
33
+ export declare function markFailed(cfg: ResolvedApprovalConfig, tx: SqlTxExecutor, rowId: string, reason: string): Promise<void>;
34
+ //# sourceMappingURL=deferred-actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deferred-actions.d.ts","sourceRoot":"","sources":["../src/deferred-actions.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,iBAAiB,EACjB,sBAAsB,EACtB,WAAW,EACX,aAAa,EACd,MAAM,SAAS,CAAC;AAMjB,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;0CAC0C;AAC1C,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,sBAAsB,EAC3B,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAmB5B;AAED,8DAA8D;AAC9D,wBAAsB,SAAS,CAC7B,GAAG,EAAE,sBAAsB,EAC3B,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,2EAA2E;AAC3E,wBAAsB,YAAY,CAChC,GAAG,EAAE,sBAAsB,EAC3B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAQnC;AAED,2CAA2C;AAC3C,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,sBAAsB,EAC3B,EAAE,EAAE,WAAW,EACf,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAOnC;AAED;;;;;;;;qDAQqD;AACrD,wBAAsB,aAAa,CACjC,GAAG,EAAE,sBAAsB,EAC3B,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,EAC9C,oBAAoB,EAAE,MAAM,GAAG,IAAI,EACnC,YAAY,EAAE,MAAM,GAAG,IAAI,GAC1B,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAED,0DAA0D;AAC1D,wBAAsB,YAAY,CAChC,GAAG,EAAE,sBAAsB,EAC3B,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,iEAAiE;AACjE,wBAAsB,UAAU,CAC9B,GAAG,EAAE,sBAAsB,EAC3B,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAMf"}
@@ -0,0 +1,126 @@
1
+ // DB CRUD for `<service>_deferred_actions` per spec § 5.6.
2
+ //
3
+ // All queries go through the postgres.js handle (raw mode) OR the db-rls
4
+ // `withSystemContext` wrapper (RLS-aware mode). The library defers tenant-
5
+ // scoping to db-rls when wired; in raw mode, callers must ensure tenant
6
+ // isolation themselves (only acceptable for platform-tenant scope per
7
+ // 08-rls-doctrine § 4).
8
+ function tableName(serviceName) {
9
+ return `${serviceName}_deferred_actions`;
10
+ }
11
+ /** Insert a fresh `pending` deferred-action row. Returns the inserted row.
12
+ * Used by `requestApproval` step [5]. */
13
+ export async function insertDeferredAction(cfg, tx, input) {
14
+ const tbl = tableName(cfg.serviceName);
15
+ const rows = await tx.unsafe(`INSERT INTO ${tbl}
16
+ (tenant_id, kind, requester_membership_id, deferred_action_payload,
17
+ status, idempotency_key, metadata, audit_correlation_id)
18
+ VALUES ($1, $2, $3, $4, 'pending', $5, $6, $7)
19
+ RETURNING *`, [
20
+ input.tenantId,
21
+ input.kind,
22
+ input.requesterMembershipId,
23
+ JSON.stringify(input.deferredActionPayload),
24
+ input.idempotencyKey,
25
+ JSON.stringify(input.metadata),
26
+ input.auditCorrelationId,
27
+ ]);
28
+ return rowFromSql(rows[0]);
29
+ }
30
+ /** Update the row's `task_id` after task-tracking returns. */
31
+ export async function setTaskId(cfg, tx, rowId, taskId) {
32
+ const tbl = tableName(cfg.serviceName);
33
+ await tx.unsafe(`UPDATE ${tbl} SET task_id = $1 WHERE id = $2`, [
34
+ taskId,
35
+ rowId,
36
+ ]);
37
+ }
38
+ /** Look up a deferred-action row by task_id. Returns null if not found. */
39
+ export async function findByTaskId(cfg, pg, taskId) {
40
+ const tbl = tableName(cfg.serviceName);
41
+ const rows = await pg.unsafe(`SELECT * FROM ${tbl} WHERE task_id = $1 LIMIT 1`, [taskId]);
42
+ if (!Array.isArray(rows) || rows.length === 0)
43
+ return null;
44
+ return rowFromSql(rows[0]);
45
+ }
46
+ /** Look up a deferred-action row by id. */
47
+ export async function findById(cfg, pg, id) {
48
+ const tbl = tableName(cfg.serviceName);
49
+ const rows = await pg.unsafe(`SELECT * FROM ${tbl} WHERE id = $1 LIMIT 1`, [
50
+ id,
51
+ ]);
52
+ if (!Array.isArray(rows) || rows.length === 0)
53
+ return null;
54
+ return rowFromSql(rows[0]);
55
+ }
56
+ /** Atomic transition to `approved` | `rejected` | `expired` with decision
57
+ * metadata. Used by the completion-event consumer.
58
+ *
59
+ * Returns the number of rows updated. The caller short-circuits when 0,
60
+ * which happens if a duplicate completion event with a different event_id
61
+ * but the same task_id is delivered — the first event already moved the
62
+ * row past `pending`, and the handler must NOT re-fire. (Per
63
+ * consumed_events dedupe + this WHERE guard, the handler runs exactly
64
+ * once even under producer-side double-publish.) */
65
+ export async function applyDecision(cfg, tx, rowId, newStatus, approverMembershipId, approverNote) {
66
+ const tbl = tableName(cfg.serviceName);
67
+ const result = await tx.unsafe(`UPDATE ${tbl}
68
+ SET status = $1, approver_membership_id = $2, approver_note = $3, decided_at = now()
69
+ WHERE id = $4 AND status = 'pending'`, [newStatus, approverMembershipId, approverNote, rowId]);
70
+ // postgres.js returns an array-shaped result with a `.count` property
71
+ // for non-returning UPDATEs.
72
+ const count = typeof result.count === "number"
73
+ ? result.count
74
+ : Array.isArray(result)
75
+ ? result.length
76
+ : 0;
77
+ return count;
78
+ }
79
+ /** Final transition to `executed` (handler ran clean). */
80
+ export async function markExecuted(cfg, tx, rowId) {
81
+ const tbl = tableName(cfg.serviceName);
82
+ await tx.unsafe(`UPDATE ${tbl} SET status = 'executed', executed_at = now() WHERE id = $1`, [rowId]);
83
+ }
84
+ /** Final transition to `failed` (handler threw post-retries). */
85
+ export async function markFailed(cfg, tx, rowId, reason) {
86
+ const tbl = tableName(cfg.serviceName);
87
+ await tx.unsafe(`UPDATE ${tbl} SET status = 'failed', failed_reason = $1, executed_at = now() WHERE id = $2`, [reason, rowId]);
88
+ }
89
+ function rowFromSql(r) {
90
+ return {
91
+ id: String(r.id),
92
+ tenant_id: String(r.tenant_id),
93
+ kind: String(r.kind),
94
+ requester_membership_id: String(r.requester_membership_id),
95
+ deferred_action_payload: parseJsonb(r.deferred_action_payload),
96
+ status: r.status,
97
+ task_id: r.task_id == null ? null : String(r.task_id),
98
+ approver_membership_id: r.approver_membership_id == null
99
+ ? null
100
+ : String(r.approver_membership_id),
101
+ approver_note: r.approver_note == null ? null : String(r.approver_note),
102
+ requested_at: String(r.requested_at ?? ""),
103
+ decided_at: r.decided_at == null ? null : String(r.decided_at),
104
+ executed_at: r.executed_at == null ? null : String(r.executed_at),
105
+ failed_reason: r.failed_reason == null ? null : String(r.failed_reason),
106
+ idempotency_key: String(r.idempotency_key),
107
+ audit_correlation_id: r.audit_correlation_id == null ? null : String(r.audit_correlation_id),
108
+ metadata: parseJsonb(r.metadata) ?? {},
109
+ };
110
+ }
111
+ function parseJsonb(v) {
112
+ if (v == null)
113
+ return {};
114
+ if (typeof v === "string") {
115
+ try {
116
+ return JSON.parse(v);
117
+ }
118
+ catch {
119
+ return {};
120
+ }
121
+ }
122
+ if (typeof v === "object")
123
+ return v;
124
+ return {};
125
+ }
126
+ //# sourceMappingURL=deferred-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deferred-actions.js","sourceRoot":"","sources":["../src/deferred-actions.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,EAAE;AACF,yEAAyE;AACzE,2EAA2E;AAC3E,wEAAwE;AACxE,sEAAsE;AACtE,wBAAwB;AASxB,SAAS,SAAS,CAAC,WAAmB;IACpC,OAAO,GAAG,WAAW,mBAAmB,CAAC;AAC3C,CAAC;AAYD;0CAC0C;AAC1C,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,GAA2B,EAC3B,EAAiB,EACjB,KAAgC;IAEhC,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,MAAM,CAC1B,eAAe,GAAG;;;;iBAIL,EACb;QACE,KAAK,CAAC,QAAQ;QACd,KAAK,CAAC,IAAI;QACV,KAAK,CAAC,qBAAqB;QAC3B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,qBAAqB,CAAC;QAC3C,KAAK,CAAC,cAAc;QACpB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC9B,KAAK,CAAC,kBAAkB;KACzB,CACF,CAAC;IACF,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAA4B,CAAC,CAAC;AACxD,CAAC;AAED,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAA2B,EAC3B,EAAiB,EACjB,KAAa,EACb,MAAc;IAEd,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvC,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,GAAG,iCAAiC,EAAE;QAC9D,MAAM;QACN,KAAK;KACN,CAAC,CAAC;AACL,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAA2B,EAC3B,EAAe,EACf,MAAc;IAEd,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,MAAM,CAC1B,iBAAiB,GAAG,6BAA6B,EACjD,CAAC,MAAM,CAAC,CACT,CAAC;IACF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3D,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAA4B,CAAC,CAAC;AACxD,CAAC;AAED,2CAA2C;AAC3C,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,GAA2B,EAC3B,EAAe,EACf,EAAU;IAEV,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,iBAAiB,GAAG,wBAAwB,EAAE;QACzE,EAAE;KACH,CAAC,CAAC;IACH,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3D,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAA4B,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;qDAQqD;AACrD,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAA2B,EAC3B,EAAiB,EACjB,KAAa,EACb,SAA8C,EAC9C,oBAAmC,EACnC,YAA2B;IAE3B,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAC5B,UAAU,GAAG;;2CAE0B,EACvC,CAAC,SAAS,EAAE,oBAAoB,EAAE,YAAY,EAAE,KAAK,CAAC,CACvD,CAAC;IACF,sEAAsE;IACtE,6BAA6B;IAC7B,MAAM,KAAK,GACT,OAAQ,MAA6B,CAAC,KAAK,KAAK,QAAQ;QACtD,CAAC,CAAE,MAA4B,CAAC,KAAK;QACrC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;YACrB,CAAC,CAAC,MAAM,CAAC,MAAM;YACf,CAAC,CAAC,CAAC,CAAC;IACV,OAAO,KAAK,CAAC;AACf,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAA2B,EAC3B,EAAiB,EACjB,KAAa;IAEb,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvC,MAAM,EAAE,CAAC,MAAM,CACb,UAAU,GAAG,6DAA6D,EAC1E,CAAC,KAAK,CAAC,CACR,CAAC;AACJ,CAAC;AAED,iEAAiE;AACjE,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,GAA2B,EAC3B,EAAiB,EACjB,KAAa,EACb,MAAc;IAEd,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvC,MAAM,EAAE,CAAC,MAAM,CACb,UAAU,GAAG,+EAA+E,EAC5F,CAAC,MAAM,EAAE,KAAK,CAAC,CAChB,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,CAA0B;IAC5C,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9B,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QACpB,uBAAuB,EAAE,MAAM,CAAC,CAAC,CAAC,uBAAuB,CAAC;QAC1D,uBAAuB,EAAE,UAAU,CAAC,CAAC,CAAC,uBAAuB,CAAC;QAC9D,MAAM,EAAE,CAAC,CAAC,MAAqC;QAC/C,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;QACrD,sBAAsB,EACpB,CAAC,CAAC,sBAAsB,IAAI,IAAI;YAC9B,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,sBAAsB,CAAC;QACtC,aAAa,EAAE,CAAC,CAAC,aAAa,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC;QACvE,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC,YAAY,IAAI,EAAE,CAAC;QAC1C,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;QAC9D,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC;QACjE,aAAa,EAAE,CAAC,CAAC,aAAa,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC;QACvE,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC;QAC1C,oBAAoB,EAClB,CAAC,CAAC,oBAAoB,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC;QACxE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE;KACvC,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,CAAU;IAC5B,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC;IACzB,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAA4B,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAA4B,CAAC;IAC/D,OAAO,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,51 @@
1
+ export declare class ApprovalError extends Error {
2
+ readonly name: string;
3
+ }
4
+ export declare class ApprovalNotInitialized extends ApprovalError {
5
+ readonly name = "ApprovalNotInitialized";
6
+ constructor();
7
+ }
8
+ export declare class UnknownApprovalKind extends ApprovalError {
9
+ readonly name = "UnknownApprovalKind";
10
+ constructor(kind: string, registered: readonly string[]);
11
+ }
12
+ export declare class HandlerForUnregisteredKind extends ApprovalError {
13
+ readonly name = "HandlerForUnregisteredKind";
14
+ constructor(kind: string, registered: readonly string[]);
15
+ }
16
+ export declare class HandlerNotRegistered extends ApprovalError {
17
+ readonly name = "HandlerNotRegistered";
18
+ constructor(kind: string);
19
+ }
20
+ export declare class IdempotencyKeyRequired extends ApprovalError {
21
+ readonly name = "IdempotencyKeyRequired";
22
+ constructor();
23
+ }
24
+ export declare class ReservedServiceNameViolation extends ApprovalError {
25
+ readonly name = "ReservedServiceNameViolation";
26
+ constructor(serviceName: string, reserved: readonly string[]);
27
+ }
28
+ export declare class InvalidApprovalSegment extends ApprovalError {
29
+ readonly which: "resource" | "verb";
30
+ readonly value: string;
31
+ readonly name = "InvalidApprovalSegment";
32
+ constructor(which: "resource" | "verb", value: string, msg: string);
33
+ }
34
+ export declare class ApproverRoleNotInCatalog extends ApprovalError {
35
+ readonly name = "ApproverRoleNotInCatalog";
36
+ constructor(approverRole: string, kindName: string, knownRoles: readonly string[]);
37
+ }
38
+ export declare class TaskCreationFailed extends ApprovalError {
39
+ readonly cause: unknown;
40
+ readonly name = "TaskCreationFailed";
41
+ constructor(cause: unknown, msg: string);
42
+ }
43
+ export declare class DeferredActionNotFound extends ApprovalError {
44
+ readonly name = "DeferredActionNotFound";
45
+ constructor(taskId: string);
46
+ }
47
+ export declare class TenantPolicyKindUnknown extends ApprovalError {
48
+ readonly name = "TenantPolicyKindUnknown";
49
+ constructor(kind: string);
50
+ }
51
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAMA,qBAAa,aAAc,SAAQ,KAAK;IACtC,SAAkB,IAAI,EAAE,MAAM,CAAmB;CAClD;AAED,qBAAa,sBAAuB,SAAQ,aAAa;IACvD,SAAkB,IAAI,4BAA4B;;CAOnD;AAED,qBAAa,mBAAoB,SAAQ,aAAa;IACpD,SAAkB,IAAI,yBAAyB;gBACnC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAKxD;AAED,qBAAa,0BAA2B,SAAQ,aAAa;IAC3D,SAAkB,IAAI,gCAAgC;gBAC1C,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAKxD;AAED,qBAAa,oBAAqB,SAAQ,aAAa;IACrD,SAAkB,IAAI,0BAA0B;gBACpC,IAAI,EAAE,MAAM;CAKzB;AAED,qBAAa,sBAAuB,SAAQ,aAAa;IACvD,SAAkB,IAAI,4BAA4B;;CAMnD;AAED,qBAAa,4BAA6B,SAAQ,aAAa;IAC7D,SAAkB,IAAI,kCAAkC;gBAC5C,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE;CAK7D;AAED,qBAAa,sBAAuB,SAAQ,aAAa;aAGrC,KAAK,EAAE,UAAU,GAAG,MAAM;aAC1B,KAAK,EAAE,MAAM;IAH/B,SAAkB,IAAI,4BAA4B;gBAEhC,KAAK,EAAE,UAAU,GAAG,MAAM,EAC1B,KAAK,EAAE,MAAM,EAC7B,GAAG,EAAE,MAAM;CAId;AAED,qBAAa,wBAAyB,SAAQ,aAAa;IACzD,SAAkB,IAAI,8BAA8B;gBAElD,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,SAAS,MAAM,EAAE;CAMhC;AAED,qBAAa,kBAAmB,SAAQ,aAAa;aAGjC,KAAK,EAAE,OAAO;IAFhC,SAAkB,IAAI,wBAAwB;gBAE5B,KAAK,EAAE,OAAO,EAC9B,GAAG,EAAE,MAAM;CAId;AAED,qBAAa,sBAAuB,SAAQ,aAAa;IACvD,SAAkB,IAAI,4BAA4B;gBACtC,MAAM,EAAE,MAAM;CAG3B;AAED,qBAAa,uBAAwB,SAAQ,aAAa;IACxD,SAAkB,IAAI,6BAA6B;gBACvC,IAAI,EAAE,MAAM;CAKzB"}