@pipeline-builder/api-core 0.0.1

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 (184) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +98 -0
  3. package/lib/constants/ai-providers.d.ts +52 -0
  4. package/lib/constants/ai-providers.js +129 -0
  5. package/lib/constants/http-status.d.ts +20 -0
  6. package/lib/constants/http-status.js +26 -0
  7. package/lib/constants/index.d.ts +3 -0
  8. package/lib/constants/index.js +6 -0
  9. package/lib/constants/time.d.ts +10 -0
  10. package/lib/constants/time.js +13 -0
  11. package/lib/errors/app-errors.d.ts +30 -0
  12. package/lib/errors/app-errors.js +53 -0
  13. package/lib/errors/index.d.ts +1 -0
  14. package/lib/errors/index.js +4 -0
  15. package/lib/helpers/access-helpers.d.ts +63 -0
  16. package/lib/helpers/access-helpers.js +82 -0
  17. package/lib/helpers/crud-helpers.d.ts +9 -0
  18. package/lib/helpers/crud-helpers.js +23 -0
  19. package/lib/helpers/index.d.ts +6 -0
  20. package/lib/helpers/index.js +9 -0
  21. package/lib/helpers/org-hierarchy-http.d.ts +74 -0
  22. package/lib/helpers/org-hierarchy-http.js +144 -0
  23. package/lib/helpers/org-hierarchy.d.ts +57 -0
  24. package/lib/helpers/org-hierarchy.js +110 -0
  25. package/lib/helpers/restore-helpers.d.ts +49 -0
  26. package/lib/helpers/restore-helpers.js +78 -0
  27. package/lib/helpers/sse-helpers.d.ts +20 -0
  28. package/lib/helpers/sse-helpers.js +45 -0
  29. package/lib/index.d.ts +58 -0
  30. package/lib/index.js +71 -0
  31. package/lib/middleware/auth.d.ts +208 -0
  32. package/lib/middleware/auth.js +588 -0
  33. package/lib/middleware/index.d.ts +3 -0
  34. package/lib/middleware/index.js +6 -0
  35. package/lib/middleware/mongo-sanitize.d.ts +32 -0
  36. package/lib/middleware/mongo-sanitize.js +100 -0
  37. package/lib/middleware/step-up.d.ts +45 -0
  38. package/lib/middleware/step-up.js +138 -0
  39. package/lib/openapi/extend-zod.d.ts +1 -0
  40. package/lib/openapi/extend-zod.js +6 -0
  41. package/lib/openapi/index.d.ts +2 -0
  42. package/lib/openapi/index.js +5 -0
  43. package/lib/openapi/registry.d.ts +17 -0
  44. package/lib/openapi/registry.js +38 -0
  45. package/lib/openapi/routes/ask-routes.d.ts +7 -0
  46. package/lib/openapi/routes/ask-routes.js +62 -0
  47. package/lib/openapi/routes/billing-routes.d.ts +1 -0
  48. package/lib/openapi/routes/billing-routes.js +66 -0
  49. package/lib/openapi/routes/image-registry-routes.d.ts +7 -0
  50. package/lib/openapi/routes/image-registry-routes.js +98 -0
  51. package/lib/openapi/routes/index.d.ts +5 -0
  52. package/lib/openapi/routes/index.js +28 -0
  53. package/lib/openapi/routes/message-routes.d.ts +1 -0
  54. package/lib/openapi/routes/message-routes.js +105 -0
  55. package/lib/openapi/routes/pipeline-routes.d.ts +1 -0
  56. package/lib/openapi/routes/pipeline-routes.js +105 -0
  57. package/lib/openapi/routes/pipeline-template-routes.d.ts +1 -0
  58. package/lib/openapi/routes/pipeline-template-routes.js +61 -0
  59. package/lib/openapi/routes/plugin-routes.d.ts +1 -0
  60. package/lib/openapi/routes/plugin-routes.js +96 -0
  61. package/lib/openapi/routes/quota-routes.d.ts +1 -0
  62. package/lib/openapi/routes/quota-routes.js +62 -0
  63. package/lib/openapi/schema-registry.d.ts +25 -0
  64. package/lib/openapi/schema-registry.js +91 -0
  65. package/lib/routes/health.d.ts +41 -0
  66. package/lib/routes/health.js +82 -0
  67. package/lib/routes/index.d.ts +1 -0
  68. package/lib/routes/index.js +4 -0
  69. package/lib/services/audit-spool.d.ts +80 -0
  70. package/lib/services/audit-spool.js +172 -0
  71. package/lib/services/cache-service.d.ts +124 -0
  72. package/lib/services/cache-service.js +276 -0
  73. package/lib/services/circuit-breaker.d.ts +45 -0
  74. package/lib/services/circuit-breaker.js +138 -0
  75. package/lib/services/compliance-client.d.ts +46 -0
  76. package/lib/services/compliance-client.js +128 -0
  77. package/lib/services/compliance-event-subscriber.d.ts +16 -0
  78. package/lib/services/compliance-event-subscriber.js +72 -0
  79. package/lib/services/durable-event-bus.d.ts +96 -0
  80. package/lib/services/durable-event-bus.js +204 -0
  81. package/lib/services/entity-events.d.ts +48 -0
  82. package/lib/services/entity-events.js +79 -0
  83. package/lib/services/env-redis.d.ts +44 -0
  84. package/lib/services/env-redis.js +94 -0
  85. package/lib/services/http-client.d.ts +143 -0
  86. package/lib/services/http-client.js +335 -0
  87. package/lib/services/index.d.ts +18 -0
  88. package/lib/services/index.js +20 -0
  89. package/lib/services/leader-lock.d.ts +45 -0
  90. package/lib/services/leader-lock.js +85 -0
  91. package/lib/services/quota.d.ts +130 -0
  92. package/lib/services/quota.js +264 -0
  93. package/lib/services/remote-audit-client.d.ts +160 -0
  94. package/lib/services/remote-audit-client.js +390 -0
  95. package/lib/services/retry-strategy.d.ts +74 -0
  96. package/lib/services/retry-strategy.js +118 -0
  97. package/lib/services/scheduler.d.ts +41 -0
  98. package/lib/services/scheduler.js +86 -0
  99. package/lib/services/service-boot.d.ts +20 -0
  100. package/lib/services/service-boot.js +28 -0
  101. package/lib/services/sse-ticket-store.d.ts +51 -0
  102. package/lib/services/sse-ticket-store.js +130 -0
  103. package/lib/services/token-revocation.d.ts +52 -0
  104. package/lib/services/token-revocation.js +120 -0
  105. package/lib/testing/tier-mock.d.ts +44 -0
  106. package/lib/testing/tier-mock.js +65 -0
  107. package/lib/types/audit-events.d.ts +69 -0
  108. package/lib/types/audit-events.js +4 -0
  109. package/lib/types/catalog-metadata.d.ts +36 -0
  110. package/lib/types/catalog-metadata.js +21 -0
  111. package/lib/types/common.d.ts +255 -0
  112. package/lib/types/common.js +41 -0
  113. package/lib/types/error-codes.d.ts +54 -0
  114. package/lib/types/error-codes.js +107 -0
  115. package/lib/types/feature-flags.d.ts +36 -0
  116. package/lib/types/feature-flags.js +128 -0
  117. package/lib/types/http.d.ts +27 -0
  118. package/lib/types/http.js +4 -0
  119. package/lib/types/index.d.ts +11 -0
  120. package/lib/types/index.js +14 -0
  121. package/lib/types/permissions.d.ts +65 -0
  122. package/lib/types/permissions.js +131 -0
  123. package/lib/types/pipeline-template.d.ts +30 -0
  124. package/lib/types/pipeline-template.js +13 -0
  125. package/lib/types/pipeline.d.ts +59 -0
  126. package/lib/types/pipeline.js +31 -0
  127. package/lib/types/quota-tiers.d.ts +79 -0
  128. package/lib/types/quota-tiers.js +221 -0
  129. package/lib/types/visibility.d.ts +20 -0
  130. package/lib/types/visibility.js +22 -0
  131. package/lib/utils/alias-resolver.d.ts +37 -0
  132. package/lib/utils/alias-resolver.js +68 -0
  133. package/lib/utils/audit.d.ts +15 -0
  134. package/lib/utils/audit.js +39 -0
  135. package/lib/utils/aws-scrub.d.ts +11 -0
  136. package/lib/utils/aws-scrub.js +67 -0
  137. package/lib/utils/compliance-attributes.d.ts +6 -0
  138. package/lib/utils/compliance-attributes.js +69 -0
  139. package/lib/utils/concurrency.d.ts +9 -0
  140. package/lib/utils/concurrency.js +29 -0
  141. package/lib/utils/crash-handlers.d.ts +18 -0
  142. package/lib/utils/crash-handlers.js +36 -0
  143. package/lib/utils/env.d.ts +28 -0
  144. package/lib/utils/env.js +54 -0
  145. package/lib/utils/headers.d.ts +18 -0
  146. package/lib/utils/headers.js +21 -0
  147. package/lib/utils/identity.d.ts +40 -0
  148. package/lib/utils/identity.js +51 -0
  149. package/lib/utils/index.d.ts +17 -0
  150. package/lib/utils/index.js +20 -0
  151. package/lib/utils/logger.d.ts +38 -0
  152. package/lib/utils/logger.js +174 -0
  153. package/lib/utils/metric-emitter.d.ts +36 -0
  154. package/lib/utils/metric-emitter.js +32 -0
  155. package/lib/utils/object.d.ts +13 -0
  156. package/lib/utils/object.js +18 -0
  157. package/lib/utils/params.d.ts +150 -0
  158. package/lib/utils/params.js +206 -0
  159. package/lib/utils/response.d.ts +117 -0
  160. package/lib/utils/response.js +223 -0
  161. package/lib/utils/safe-require.d.ts +13 -0
  162. package/lib/utils/safe-require.js +19 -0
  163. package/lib/utils/secret-encryption.d.ts +176 -0
  164. package/lib/utils/secret-encryption.js +374 -0
  165. package/lib/utils/ssrf.d.ts +30 -0
  166. package/lib/utils/ssrf.js +106 -0
  167. package/lib/validation/ai-schemas.d.ts +56 -0
  168. package/lib/validation/ai-schemas.js +78 -0
  169. package/lib/validation/common-schemas.d.ts +133 -0
  170. package/lib/validation/common-schemas.js +94 -0
  171. package/lib/validation/index.d.ts +7 -0
  172. package/lib/validation/index.js +10 -0
  173. package/lib/validation/message-schemas.d.ts +160 -0
  174. package/lib/validation/message-schemas.js +156 -0
  175. package/lib/validation/middleware.d.ts +72 -0
  176. package/lib/validation/middleware.js +65 -0
  177. package/lib/validation/pipeline-schemas.d.ts +181 -0
  178. package/lib/validation/pipeline-schemas.js +101 -0
  179. package/lib/validation/pipeline-template-schemas.d.ts +155 -0
  180. package/lib/validation/pipeline-template-schemas.js +66 -0
  181. package/lib/validation/plugin-schemas.d.ts +171 -0
  182. package/lib/validation/plugin-schemas.js +81 -0
  183. package/openapi.yaml +292 -0
  184. package/package.json +167 -0
@@ -0,0 +1,45 @@
1
+ export type CircuitState = 'closed' | 'open' | 'half-open';
2
+ export interface CircuitBreakerConfig {
3
+ /** Consecutive failures that trip the breaker from closed → open. */
4
+ failureThreshold: number;
5
+ /** How long to stay open before allowing a half-open probe (ms). */
6
+ cooldownMs: number;
7
+ /** Master switch — when false, allowRequest() is always true and no state moves. */
8
+ enabled: boolean;
9
+ }
10
+ export declare const DEFAULT_BREAKER_CONFIG: CircuitBreakerConfig;
11
+ /**
12
+ * A single target's breaker. Not concurrency-locked (JS is single-threaded per
13
+ * event loop); `probeInFlight` gates half-open to one in-flight probe so a burst
14
+ * during cooldown doesn't stampede the recovering downstream.
15
+ */
16
+ export declare class CircuitBreaker {
17
+ private readonly key;
18
+ private readonly config;
19
+ private state;
20
+ private consecutiveFailures;
21
+ private openedAt;
22
+ private probeInFlight;
23
+ constructor(key: string, config?: CircuitBreakerConfig);
24
+ getState(): CircuitState;
25
+ /**
26
+ * Whether a request may proceed. Transitions open→half-open once the cooldown
27
+ * elapses and lets exactly ONE probe through. Returns false when open (or a
28
+ * probe is already in flight) — the caller must fast-fail without any network.
29
+ */
30
+ allowRequest(): boolean;
31
+ /** A logical request succeeded (2xx/3xx/4xx that isn't a downstream fault). */
32
+ recordSuccess(): void;
33
+ /** A logical request failed (connection error, timeout, or 5xx response). */
34
+ recordFailure(): void;
35
+ private open;
36
+ }
37
+ /** Get (or lazily create) the shared breaker for a `host:port` target. */
38
+ export declare function getCircuitBreaker(key: string): CircuitBreaker;
39
+ /** Test helper: clear all breaker state so suites don't leak across each other. */
40
+ export declare function resetCircuitBreakers(): void;
41
+ /** Thrown by the client when a request is fast-failed because the breaker is open. */
42
+ export declare class CircuitOpenError extends Error {
43
+ readonly code = "CIRCUIT_OPEN";
44
+ constructor(target: string);
45
+ }
@@ -0,0 +1,138 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Per-target circuit breaker for service-to-service calls.
5
+ *
6
+ * The shared {@link InternalHttpClient} already has timeouts, a bounded socket
7
+ * pool, and bounded retry-with-jitter — but no load-shedding. Under a sustained
8
+ * downstream brownout every caller keeps paying `timeout × (maxRetries+1)` per
9
+ * request, saturating the socket pool and turning one slow dependency into a
10
+ * fleet-wide latency cascade (the classic retry storm). Worse for fail-CLOSED
11
+ * dependencies (compliance): a brownout there blocks plugin/pipeline creation
12
+ * with full retry latency on every request.
13
+ *
14
+ * This breaker short-circuits that: after `failureThreshold` consecutive
15
+ * failures to a target it OPENS and fast-fails (no network, no retries) for
16
+ * `cooldownMs`, then allows a single half-open probe. A fail-closed caller still
17
+ * fails closed — just immediately, instead of after the full retry budget; a
18
+ * fail-open caller (createSafeClient) gets its `null` immediately.
19
+ *
20
+ * State is keyed by `host:port` in a process-level registry so it is shared even
21
+ * when callers mint a fresh client per request — one bad target trips once, not
22
+ * once-per-client. Disable entirely with `S2S_BREAKER_ENABLED=false`.
23
+ */
24
+ import { envInt, envBool } from '../utils/env.js';
25
+ import { createLogger } from '../utils/logger.js';
26
+ import { emitCounter } from '../utils/metric-emitter.js';
27
+ const logger = createLogger('circuit-breaker');
28
+ export const DEFAULT_BREAKER_CONFIG = {
29
+ failureThreshold: envInt('S2S_BREAKER_THRESHOLD', 5, { min: 1 }),
30
+ cooldownMs: envInt('S2S_BREAKER_COOLDOWN_MS', 10000, { min: 1 }),
31
+ enabled: envBool('S2S_BREAKER_ENABLED', true),
32
+ };
33
+ /**
34
+ * A single target's breaker. Not concurrency-locked (JS is single-threaded per
35
+ * event loop); `probeInFlight` gates half-open to one in-flight probe so a burst
36
+ * during cooldown doesn't stampede the recovering downstream.
37
+ */
38
+ export class CircuitBreaker {
39
+ key;
40
+ config;
41
+ state = 'closed';
42
+ consecutiveFailures = 0;
43
+ openedAt = 0;
44
+ probeInFlight = false;
45
+ constructor(key, config = DEFAULT_BREAKER_CONFIG) {
46
+ this.key = key;
47
+ this.config = config;
48
+ }
49
+ getState() {
50
+ return this.state;
51
+ }
52
+ /**
53
+ * Whether a request may proceed. Transitions open→half-open once the cooldown
54
+ * elapses and lets exactly ONE probe through. Returns false when open (or a
55
+ * probe is already in flight) — the caller must fast-fail without any network.
56
+ */
57
+ allowRequest() {
58
+ if (!this.config.enabled)
59
+ return true;
60
+ if (this.state === 'closed')
61
+ return true;
62
+ if (this.state === 'open') {
63
+ if (Date.now() - this.openedAt >= this.config.cooldownMs) {
64
+ this.state = 'half-open';
65
+ this.probeInFlight = true;
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ // half-open: allow only a single probe at a time.
71
+ if (this.probeInFlight)
72
+ return false;
73
+ this.probeInFlight = true;
74
+ return true;
75
+ }
76
+ /** A logical request succeeded (2xx/3xx/4xx that isn't a downstream fault). */
77
+ recordSuccess() {
78
+ if (!this.config.enabled)
79
+ return;
80
+ this.consecutiveFailures = 0;
81
+ this.probeInFlight = false;
82
+ if (this.state !== 'closed') {
83
+ logger.info('circuit closed (downstream recovered)', { target: this.key });
84
+ this.state = 'closed';
85
+ }
86
+ }
87
+ /** A logical request failed (connection error, timeout, or 5xx response). */
88
+ recordFailure() {
89
+ if (!this.config.enabled)
90
+ return;
91
+ this.probeInFlight = false;
92
+ this.consecutiveFailures++;
93
+ // A failed half-open probe means the downstream is still bad — re-open.
94
+ if (this.state === 'half-open') {
95
+ this.open();
96
+ return;
97
+ }
98
+ if (this.state === 'closed' && this.consecutiveFailures >= this.config.failureThreshold) {
99
+ this.open();
100
+ }
101
+ }
102
+ open() {
103
+ const wasOpen = this.state === 'open';
104
+ this.state = 'open';
105
+ this.openedAt = Date.now();
106
+ if (!wasOpen) {
107
+ logger.warn('circuit opened — shedding load to target', {
108
+ target: this.key,
109
+ consecutiveFailures: this.consecutiveFailures,
110
+ cooldownMs: this.config.cooldownMs,
111
+ });
112
+ emitCounter('s2s_circuit_opened_total', { target: this.key });
113
+ }
114
+ }
115
+ }
116
+ const registry = new Map();
117
+ /** Get (or lazily create) the shared breaker for a `host:port` target. */
118
+ export function getCircuitBreaker(key) {
119
+ let breaker = registry.get(key);
120
+ if (!breaker) {
121
+ breaker = new CircuitBreaker(key);
122
+ registry.set(key, breaker);
123
+ }
124
+ return breaker;
125
+ }
126
+ /** Test helper: clear all breaker state so suites don't leak across each other. */
127
+ export function resetCircuitBreakers() {
128
+ registry.clear();
129
+ }
130
+ /** Thrown by the client when a request is fast-failed because the breaker is open. */
131
+ export class CircuitOpenError extends Error {
132
+ code = 'CIRCUIT_OPEN';
133
+ constructor(target) {
134
+ super(`Circuit open for ${target} — fast-failing to shed load`);
135
+ this.name = 'CircuitOpenError';
136
+ }
137
+ }
138
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2lyY3VpdC1icmVha2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3NlcnZpY2VzL2NpcmN1aXQtYnJlYWtlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDOzs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQW9CRztBQUVILE9BQU8sRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDbEQsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2xELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUV6RCxNQUFNLE1BQU0sR0FBRyxZQUFZLENBQUMsaUJBQWlCLENBQUMsQ0FBQztBQWEvQyxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBeUI7SUFDMUQsZ0JBQWdCLEVBQUUsTUFBTSxDQUFDLHVCQUF1QixFQUFFLENBQUMsRUFBRSxFQUFFLEdBQUcsRUFBRSxDQUFDLEVBQUUsQ0FBQztJQUNoRSxVQUFVLEVBQUUsTUFBTSxDQUFDLHlCQUF5QixFQUFFLEtBQUssRUFBRSxFQUFFLEdBQUcsRUFBRSxDQUFDLEVBQUUsQ0FBQztJQUNoRSxPQUFPLEVBQUUsT0FBTyxDQUFDLHFCQUFxQixFQUFFLElBQUksQ0FBQztDQUM5QyxDQUFDO0FBRUY7Ozs7R0FJRztBQUNILE1BQU0sT0FBTyxjQUFjO0lBT04sR0FBRztJQUNILE1BQU07SUFQakIsS0FBSyxHQUFpQixRQUFRLENBQUM7SUFDL0IsbUJBQW1CLEdBQUcsQ0FBQyxDQUFDO0lBQ3hCLFFBQVEsR0FBRyxDQUFDLENBQUM7SUFDYixhQUFhLEdBQUcsS0FBSyxDQUFDO0lBRTlCLFlBQ21CLEdBQVcsRUFDWCxNQUFNLEdBQXlCLHNCQUFzQjttQkFEckQsR0FBRztzQkFDSCxNQUFNO0lBQ3RCLENBQUM7SUFFSixRQUFRO1FBQ04sT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDO0lB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QyxHQUFXO0lBQzNDLElBQUksT0FBTyxHQUFHLFFBQVEsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDaEMsSUFBSSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ2IsT0FBTyxHQUFHLElBQUksY0FBYyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ2xDLFFBQVEsQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQzdCLENBQUM7SUFDRCxPQUFPLE9BQU8sQ0FBQztBQUNqQixDQUFDO0FBRUQsbUZBQW1GO0FBQ25GLE1BQU0sVUFBVSxvQkFBb0I7SUFDbEMsUUFBUSxDQUFDLEtBQUssRUFBRSxDQUFDO0FBQ25CLENBQUM7QUFFRCxzRkFBc0Y7QUFDdEYsTUFBTSxPQUFPLGdCQUFpQixTQUFRLEtBQUs7SUFDaEMsSUFBSSxHQUFHLGNBQWMsQ0FBQztJQUMvQixZQUFZLE1BQWM7UUFDeEIsS0FBSyxDQUFDLG9CQUFvQixNQUFNLDhCQUE4QixDQUFDLENBQUM7UUFDaEUsSUFBSSxDQUFDLElBQUksR0FBRyxrQkFBa0IsQ0FBQztJQUNqQyxDQUFDO0NBQ0YiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuLyoqXG4gKiBQZXItdGFyZ2V0IGNpcmN1aXQgYnJlYWtlciBmb3Igc2VydmljZS10by1zZXJ2aWNlIGNhbGxzLlxuICpcbiAqIFRoZSBzaGFyZWQge0BsaW5rIEludGVybmFsSHR0cENsaWVudH0gYWxyZWFkeSBoYXMgdGltZW91dHMsIGEgYm91bmRlZCBzb2NrZXRcbiAqIHBvb2wsIGFuZCBib3VuZGVkIHJldHJ5LXdpdGgtaml0dGVyIOKAlCBidXQgbm8gbG9hZC1zaGVkZGluZy4gVW5kZXIgYSBzdXN0YWluZWRcbiAqIGRvd25zdHJlYW0gYnJvd25vdXQgZXZlcnkgY2FsbGVyIGtlZXBzIHBheWluZyBgdGltZW91dCDDlyAobWF4UmV0cmllcysxKWAgcGVyXG4gKiByZXF1ZXN0LCBzYXR1cmF0aW5nIHRoZSBzb2NrZXQgcG9vbCBhbmQgdHVybmluZyBvbmUgc2xvdyBkZXBlbmRlbmN5IGludG8gYVxuICogZmxlZXQtd2lkZSBsYXRlbmN5IGNhc2NhZGUgKHRoZSBjbGFzc2ljIHJldHJ5IHN0b3JtKS4gV29yc2UgZm9yIGZhaWwtQ0xPU0VEXG4gKiBkZXBlbmRlbmNpZXMgKGNvbXBsaWFuY2UpOiBhIGJyb3dub3V0IHRoZXJlIGJsb2NrcyBwbHVnaW4vcGlwZWxpbmUgY3JlYXRpb25cbiAqIHdpdGggZnVsbCByZXRyeSBsYXRlbmN5IG9uIGV2ZXJ5IHJlcXVlc3QuXG4gKlxuICogVGhpcyBicmVha2VyIHNob3J0LWNpcmN1aXRzIHRoYXQ6IGFmdGVyIGBmYWlsdXJlVGhyZXNob2xkYCBjb25zZWN1dGl2ZVxuICogZmFpbHVyZXMgdG8gYSB0YXJnZXQgaXQgT1BFTlMgYW5kIGZhc3QtZmFpbHMgKG5vIG5ldHdvcmssIG5vIHJldHJpZXMpIGZvclxuICogYGNvb2xkb3duTXNgLCB0aGVuIGFsbG93cyBhIHNpbmdsZSBoYWxmLW9wZW4gcHJvYmUuIEEgZmFpbC1jbG9zZWQgY2FsbGVyIHN0aWxsXG4gKiBmYWlscyBjbG9zZWQg4oCUIGp1c3QgaW1tZWRpYXRlbHksIGluc3RlYWQgb2YgYWZ0ZXIgdGhlIGZ1bGwgcmV0cnkgYnVkZ2V0OyBhXG4gKiBmYWlsLW9wZW4gY2FsbGVyIChjcmVhdGVTYWZlQ2xpZW50KSBnZXRzIGl0cyBgbnVsbGAgaW1tZWRpYXRlbHkuXG4gKlxuICogU3RhdGUgaXMga2V5ZWQgYnkgYGhvc3Q6cG9ydGAgaW4gYSBwcm9jZXNzLWxldmVsIHJlZ2lzdHJ5IHNvIGl0IGlzIHNoYXJlZCBldmVuXG4gKiB3aGVuIGNhbGxlcnMgbWludCBhIGZyZXNoIGNsaWVudCBwZXIgcmVxdWVzdCDigJQgb25lIGJhZCB0YXJnZXQgdHJpcHMgb25jZSwgbm90XG4gKiBvbmNlLXBlci1jbGllbnQuIERpc2FibGUgZW50aXJlbHkgd2l0aCBgUzJTX0JSRUFLRVJfRU5BQkxFRD1mYWxzZWAuXG4gKi9cblxuaW1wb3J0IHsgZW52SW50LCBlbnZCb29sIH0gZnJvbSAnLi4vdXRpbHMvZW52LmpzJztcbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciB9IGZyb20gJy4uL3V0aWxzL2xvZ2dlci5qcyc7XG5pbXBvcnQgeyBlbWl0Q291bnRlciB9IGZyb20gJy4uL3V0aWxzL21ldHJpYy1lbWl0dGVyLmpzJztcblxuY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdjaXJjdWl0LWJyZWFrZXInKTtcblxuZXhwb3J0IHR5cGUgQ2lyY3VpdFN0YXRlID0gJ2Nsb3NlZCcgfCAnb3BlbicgfCAnaGFsZi1vcGVuJztcblxuZXhwb3J0IGludGVyZmFjZSBDaXJjdWl0QnJlYWtlckNvbmZpZyB7XG4gIC8qKiBDb25zZWN1dGl2ZSBmYWlsdXJlcyB0aGF0IHRyaXAgdGhlIGJyZWFrZXIgZnJvbSBjbG9zZWQg4oaSIG9wZW4uICovXG4gIGZhaWx1cmVUaHJlc2hvbGQ6IG51bWJlcjtcbiAgLyoqIEhvdyBsb25nIHRvIHN0YXkgb3BlbiBiZWZvcmUgYWxsb3dpbmcgYSBoYWxmLW9wZW4gcHJvYmUgKG1zKS4gKi9cbiAgY29vbGRvd25NczogbnVtYmVyO1xuICAvKiogTWFzdGVyIHN3aXRjaCDigJQgd2hlbiBmYWxzZSwgYWxsb3dSZXF1ZXN0KCkgaXMgYWx3YXlzIHRydWUgYW5kIG5vIHN0YXRlIG1vdmVzLiAqL1xuICBlbmFibGVkOiBib29sZWFuO1xufVxuXG5leHBvcnQgY29uc3QgREVGQVVMVF9CUkVBS0VSX0NPTkZJRzogQ2lyY3VpdEJyZWFrZXJDb25maWcgPSB7XG4gIGZhaWx1cmVUaHJlc2hvbGQ6IGVudkludCgnUzJTX0JSRUFLRVJfVEhSRVNIT0xEJywgNSwgeyBtaW46IDEgfSksXG4gIGNvb2xkb3duTXM6IGVudkludCgnUzJTX0JSRUFLRVJfQ09PTERPV05fTVMnLCAxMDAwMCwgeyBtaW46IDEgfSksXG4gIGVuYWJsZWQ6IGVudkJvb2woJ1MyU19CUkVBS0VSX0VOQUJMRUQnLCB0cnVlKSxcbn07XG5cbi8qKlxuICogQSBzaW5nbGUgdGFyZ2V0J3MgYnJlYWtlci4gTm90IGNvbmN1cnJlbmN5LWxvY2tlZCAoSlMgaXMgc2luZ2xlLXRocmVhZGVkIHBlclxuICogZXZlbnQgbG9vcCk7IGBwcm9iZUluRmxpZ2h0YCBnYXRlcyBoYWxmLW9wZW4gdG8gb25lIGluLWZsaWdodCBwcm9iZSBzbyBhIGJ1cnN0XG4gKiBkdXJpbmcgY29vbGRvd24gZG9lc24ndCBzdGFtcGVkZSB0aGUgcmVjb3ZlcmluZyBkb3duc3RyZWFtLlxuICovXG5leHBvcnQgY2xhc3MgQ2lyY3VpdEJyZWFrZXIge1xuICBwcml2YXRlIHN0YXRlOiBDaXJjdWl0U3RhdGUgPSAnY2xvc2VkJztcbiAgcHJpdmF0ZSBjb25zZWN1dGl2ZUZhaWx1cmVzID0gMDtcbiAgcHJpdmF0ZSBvcGVuZWRBdCA9IDA7XG4gIHByaXZhdGUgcHJvYmVJbkZsaWdodCA9IGZhbHNlO1xuXG4gIGNvbnN0cnVjdG9yKFxuICAgIHByaXZhdGUgcmVhZG9ubHkga2V5OiBzdHJpbmcsXG4gICAgcHJpdmF0ZSByZWFkb25seSBjb25maWc6IENpcmN1aXRCcmVha2VyQ29uZmlnID0gREVGQVVMVF9CUkVBS0VSX0NPTkZJRyxcbiAgKSB7fVxuXG4gIGdldFN0YXRlKCk6IENpcmN1aXRTdGF0ZSB7XG4gICAgcmV0dXJuIHRoaXMuc3RhdGU7XG4gIH1cblxuICAvKipcbiAgICogV2hldGhlciBhIHJlcXVlc3QgbWF5IHByb2NlZWQuIFRyYW5zaXRpb25zIG9wZW7ihpJoYWxmLW9wZW4gb25jZSB0aGUgY29vbGRvd25cbiAgICogZWxhcHNlcyBhbmQgbGV0cyBleGFjdGx5IE9ORSBwcm9iZSB0aHJvdWdoLiBSZXR1cm5zIGZhbHNlIHdoZW4gb3BlbiAob3IgYVxuICAgKiBwcm9iZSBpcyBhbHJlYWR5IGluIGZsaWdodCkg4oCUIHRoZSBjYWxsZXIgbXVzdCBmYXN0LWZhaWwgd2l0aG91dCBhbnkgbmV0d29yay5cbiAgICovXG4gIGFsbG93UmVxdWVzdCgpOiBib29sZWFuIHtcbiAgICBpZiAoIXRoaXMuY29uZmlnLmVuYWJsZWQpIHJldHVybiB0cnVlO1xuICAgIGlmICh0aGlzLnN0YXRlID09PSAnY2xvc2VkJykgcmV0dXJuIHRydWU7XG4gICAgaWYgKHRoaXMuc3RhdGUgPT09ICdvcGVuJykge1xuICAgICAgaWYgKERhdGUubm93KCkgLSB0aGlzLm9wZW5lZEF0ID49IHRoaXMuY29uZmlnLmNvb2xkb3duTXMpIHtcbiAgICAgICAgdGhpcy5zdGF0ZSA9ICdoYWxmLW9wZW4nO1xuICAgICAgICB0aGlzLnByb2JlSW5GbGlnaHQgPSB0cnVlO1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICAgIH1cbiAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgLy8gaGFsZi1vcGVuOiBhbGxvdyBvbmx5IGEgc2luZ2xlIHByb2JlIGF0IGEgdGltZS5cbiAgICBpZiAodGhpcy5wcm9iZUluRmxpZ2h0KSByZXR1cm4gZmFsc2U7XG4gICAgdGhpcy5wcm9iZUluRmxpZ2h0ID0gdHJ1ZTtcbiAgICByZXR1cm4gdHJ1ZTtcbiAgfVxuXG4gIC8qKiBBIGxvZ2ljYWwgcmVxdWVzdCBzdWNjZWVkZWQgKDJ4eC8zeHgvNHh4IHRoYXQgaXNuJ3QgYSBkb3duc3RyZWFtIGZhdWx0KS4gKi9cbiAgcmVjb3JkU3VjY2VzcygpOiB2b2lkIHtcbiAgICBpZiAoIXRoaXMuY29uZmlnLmVuYWJsZWQpIHJldHVybjtcbiAgICB0aGlzLmNvbnNlY3V0aXZlRmFpbHVyZXMgPSAwO1xuICAgIHRoaXMucHJvYmVJbkZsaWdodCA9IGZhbHNlO1xuICAgIGlmICh0aGlzLnN0YXRlICE9PSAnY2xvc2VkJykge1xuICAgICAgbG9nZ2VyLmluZm8oJ2NpcmN1aXQgY2xvc2VkIChkb3duc3RyZWFtIHJlY292ZXJlZCknLCB7IHRhcmdldDogdGhpcy5rZXkgfSk7XG4gICAgICB0aGlzLnN0YXRlID0gJ2Nsb3NlZCc7XG4gICAgfVxuICB9XG5cbiAgLyoqIEEgbG9naWNhbCByZXF1ZXN0IGZhaWxlZCAoY29ubmVjdGlvbiBlcnJvciwgdGltZW91dCwgb3IgNXh4IHJlc3BvbnNlKS4gKi9cbiAgcmVjb3JkRmFpbHVyZSgpOiB2b2lkIHtcbiAgICBpZiAoIXRoaXMuY29uZmlnLmVuYWJsZWQpIHJldHVybjtcbiAgICB0aGlzLnByb2JlSW5GbGlnaHQgPSBmYWxzZTtcbiAgICB0aGlzLmNvbnNlY3V0aXZlRmFpbHVyZXMrKztcbiAgICAvLyBBIGZhaWxlZCBoYWxmLW9wZW4gcHJvYmUgbWVhbnMgdGhlIGRvd25zdHJlYW0gaXMgc3RpbGwgYmFkIOKAlCByZS1vcGVuLlxuICAgIGlmICh0aGlzLnN0YXRlID09PSAnaGFsZi1vcGVuJykge1xuICAgICAgdGhpcy5vcGVuKCk7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmICh0aGlzLnN0YXRlID09PSAnY2xvc2VkJyAmJiB0aGlzLmNvbnNlY3V0aXZlRmFpbHVyZXMgPj0gdGhpcy5jb25maWcuZmFpbHVyZVRocmVzaG9sZCkge1xuICAgICAgdGhpcy5vcGVuKCk7XG4gICAgfVxuICB9XG5cbiAgcHJpdmF0ZSBvcGVuKCk6IHZvaWQge1xuICAgIGNvbnN0IHdhc09wZW4gPSB0aGlzLnN0YXRlID09PSAnb3Blbic7XG4gICAgdGhpcy5zdGF0ZSA9ICdvcGVuJztcbiAgICB0aGlzLm9wZW5lZEF0ID0gRGF0ZS5ub3coKTtcbiAgICBpZiAoIXdhc09wZW4pIHtcbiAgICAgIGxvZ2dlci53YXJuKCdjaXJjdWl0IG9wZW5lZCDigJQgc2hlZGRpbmcgbG9hZCB0byB0YXJnZXQnLCB7XG4gICAgICAgIHRhcmdldDogdGhpcy5rZXksXG4gICAgICAgIGNvbnNlY3V0aXZlRmFpbHVyZXM6IHRoaXMuY29uc2VjdXRpdmVGYWlsdXJlcyxcbiAgICAgICAgY29vbGRvd25NczogdGhpcy5jb25maWcuY29vbGRvd25NcyxcbiAgICAgIH0pO1xuICAgICAgZW1pdENvdW50ZXIoJ3Myc19jaXJjdWl0X29wZW5lZF90b3RhbCcsIHsgdGFyZ2V0OiB0aGlzLmtleSB9KTtcbiAgICB9XG4gIH1cbn1cblxuY29uc3QgcmVnaXN0cnkgPSBuZXcgTWFwPHN0cmluZywgQ2lyY3VpdEJyZWFrZXI+KCk7XG5cbi8qKiBHZXQgKG9yIGxhemlseSBjcmVhdGUpIHRoZSBzaGFyZWQgYnJlYWtlciBmb3IgYSBgaG9zdDpwb3J0YCB0YXJnZXQuICovXG5leHBvcnQgZnVuY3Rpb24gZ2V0Q2lyY3VpdEJyZWFrZXIoa2V5OiBzdHJpbmcpOiBDaXJjdWl0QnJlYWtlciB7XG4gIGxldCBicmVha2VyID0gcmVnaXN0cnkuZ2V0KGtleSk7XG4gIGlmICghYnJlYWtlcikge1xuICAgIGJyZWFrZXIgPSBuZXcgQ2lyY3VpdEJyZWFrZXIoa2V5KTtcbiAgICByZWdpc3RyeS5zZXQoa2V5LCBicmVha2VyKTtcbiAgfVxuICByZXR1cm4gYnJlYWtlcjtcbn1cblxuLyoqIFRlc3QgaGVscGVyOiBjbGVhciBhbGwgYnJlYWtlciBzdGF0ZSBzbyBzdWl0ZXMgZG9uJ3QgbGVhayBhY3Jvc3MgZWFjaCBvdGhlci4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZXNldENpcmN1aXRCcmVha2VycygpOiB2b2lkIHtcbiAgcmVnaXN0cnkuY2xlYXIoKTtcbn1cblxuLyoqIFRocm93biBieSB0aGUgY2xpZW50IHdoZW4gYSByZXF1ZXN0IGlzIGZhc3QtZmFpbGVkIGJlY2F1c2UgdGhlIGJyZWFrZXIgaXMgb3Blbi4gKi9cbmV4cG9ydCBjbGFzcyBDaXJjdWl0T3BlbkVycm9yIGV4dGVuZHMgRXJyb3Ige1xuICByZWFkb25seSBjb2RlID0gJ0NJUkNVSVRfT1BFTic7XG4gIGNvbnN0cnVjdG9yKHRhcmdldDogc3RyaW5nKSB7XG4gICAgc3VwZXIoYENpcmN1aXQgb3BlbiBmb3IgJHt0YXJnZXR9IOKAlCBmYXN0LWZhaWxpbmcgdG8gc2hlZCBsb2FkYCk7XG4gICAgdGhpcy5uYW1lID0gJ0NpcmN1aXRPcGVuRXJyb3InO1xuICB9XG59XG4iXX0=
@@ -0,0 +1,46 @@
1
+ import type { ServiceConfig } from '../types/common.js';
2
+ /**
3
+ * Result of a compliance validation check.
4
+ */
5
+ export interface ComplianceCheckResult {
6
+ passed: boolean;
7
+ violations: ComplianceViolation[];
8
+ warnings: ComplianceViolation[];
9
+ blocked: boolean;
10
+ rulesEvaluated: number;
11
+ rulesSkipped: number;
12
+ exemptionsApplied: string[];
13
+ }
14
+ export interface ComplianceViolation {
15
+ ruleId: string;
16
+ ruleName: string;
17
+ policyId?: string | null;
18
+ field: string;
19
+ operator: string;
20
+ expectedValue: unknown;
21
+ actualValue: unknown;
22
+ severity: string;
23
+ message: string;
24
+ }
25
+ /**
26
+ * Compliance service client interface.
27
+ * Uses fail-closed design: errors propagate to block the operation.
28
+ */
29
+ export interface ComplianceClient {
30
+ /** Validate plugin attributes against org rules. Throws on service error (fail-closed). */
31
+ validatePlugin(orgId: string, attributes: Record<string, unknown>, authHeader: string, entityId?: string, entityName?: string, action?: string): Promise<ComplianceCheckResult>;
32
+ /** Validate pipeline attributes against org rules. Throws on service error (fail-closed). */
33
+ validatePipeline(orgId: string, attributes: Record<string, unknown>, authHeader: string, entityId?: string, entityName?: string, action?: string): Promise<ComplianceCheckResult>;
34
+ /** Pre-flight check for plugin (no audit, no notification). */
35
+ dryRunPlugin(orgId: string, attributes: Record<string, unknown>, authHeader: string): Promise<ComplianceCheckResult>;
36
+ /** Pre-flight check for pipeline (no audit, no notification). */
37
+ dryRunPipeline(orgId: string, attributes: Record<string, unknown>, authHeader: string): Promise<ComplianceCheckResult>;
38
+ }
39
+ /**
40
+ * Create a compliance client.
41
+ *
42
+ * IMPORTANT: This client is fail-closed — if the compliance service is
43
+ * unreachable or returns an error, the error propagates and the calling
44
+ * operation (plugin upload, pipeline create) is rejected.
45
+ */
46
+ export declare function createComplianceClient(config?: Partial<ServiceConfig>): ComplianceClient;
@@ -0,0 +1,128 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { InternalHttpClient } from './http-client.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+ const logger = createLogger('compliance-client');
6
+ /**
7
+ * When true, compliance checks are bypassed if the service is unavailable
8
+ * (fail-open). Default is fail-closed (errors propagate and block the operation).
9
+ */
10
+ const COMPLIANCE_BYPASS = process.env.COMPLIANCE_BYPASS === 'true';
11
+ /**
12
+ * Per-attempt timeout for a validation POST (env: `COMPLIANCE_VALIDATE_TIMEOUT_MS`).
13
+ * Deliberately TIGHTER than the 5s client default so the enforced retries (below)
14
+ * fit inside a bounded total budget rather than stacking three 5s stalls onto a
15
+ * single interactive request.
16
+ */
17
+ const VALIDATE_TIMEOUT_MS = parseInt(process.env.COMPLIANCE_VALIDATE_TIMEOUT_MS ?? '4000', 10);
18
+ /**
19
+ * Return a pass-through result when COMPLIANCE_BYPASS is enabled and the
20
+ * compliance service is unreachable.
21
+ */
22
+ function bypassResult(context) {
23
+ logger.warn('COMPLIANCE_BYPASS: Service unavailable, allowing request', context);
24
+ return {
25
+ passed: true,
26
+ blocked: false,
27
+ violations: [],
28
+ warnings: [{ ruleId: '', ruleName: '', field: '', operator: '', expectedValue: null, actualValue: null, severity: 'warning', message: 'Compliance check skipped (service unavailable)' }],
29
+ rulesEvaluated: 0,
30
+ rulesSkipped: 0,
31
+ exemptionsApplied: [],
32
+ };
33
+ }
34
+ /**
35
+ * Build common request headers for compliance calls.
36
+ */
37
+ function buildHeaders(orgId, authHeader) {
38
+ const headers = {
39
+ 'x-org-id': orgId,
40
+ };
41
+ // NOTE: the old `x-internal-service: true` header was removed — it is trusted
42
+ // by nothing (it was spoofable); compliance routes authenticate the caller via
43
+ // the `Authorization` service JWT (`requireServicePrincipal`).
44
+ if (authHeader)
45
+ headers.Authorization = authHeader;
46
+ return headers;
47
+ }
48
+ /**
49
+ * Request options for a compliance validation POST.
50
+ *
51
+ * These calls are side-effect-free (they evaluate rules and return a verdict;
52
+ * any audit/notification the compliance service writes is derived, not a mutation
53
+ * the caller depends on being once-only), so they are marked `idempotent: true`.
54
+ * That lets the HTTP client auto-retry a transient 503 / connection reset /
55
+ * timeout instead of surfacing it as a hard failure — which, under the
56
+ * fail-closed default, would wrongly REJECT a legitimate plugin upload / pipeline
57
+ * create just because compliance briefly stalled. Paired with the tighter
58
+ * per-attempt timeout so the retries stay within a bounded total budget.
59
+ */
60
+ function validateOptions(orgId, authHeader) {
61
+ return {
62
+ headers: buildHeaders(orgId, authHeader),
63
+ idempotent: true,
64
+ timeout: VALIDATE_TIMEOUT_MS,
65
+ };
66
+ }
67
+ /**
68
+ * Create a compliance client.
69
+ *
70
+ * IMPORTANT: This client is fail-closed — if the compliance service is
71
+ * unreachable or returns an error, the error propagates and the calling
72
+ * operation (plugin upload, pipeline create) is rejected.
73
+ */
74
+ export function createComplianceClient(config) {
75
+ const serviceConfig = {
76
+ host: config?.host ?? process.env.COMPLIANCE_SERVICE_HOST ?? 'compliance',
77
+ port: config?.port ?? parseInt(process.env.COMPLIANCE_SERVICE_PORT ?? '3000', 10),
78
+ };
79
+ const client = new InternalHttpClient(serviceConfig);
80
+ /**
81
+ * Validate response shape + status. The HTTP client doesn't throw on non-2xx
82
+ * (it just returns the body), so without this an auth failure / route miss
83
+ * silently produced `response.body.data === undefined` and the caller
84
+ * crashed on `.blocked`. Now non-2xx surfaces as a thrown error that the
85
+ * outer try/catch can turn into either fail-closed (default) or
86
+ * COMPLIANCE_BYPASS (fail-open).
87
+ */
88
+ function unwrap(response, op) {
89
+ if (response.statusCode < 200 || response.statusCode >= 300) {
90
+ const msg = response.body?.message ?? `HTTP ${response.statusCode}`;
91
+ throw new Error(`compliance ${op} failed: ${msg}`);
92
+ }
93
+ if (!response.body?.data) {
94
+ throw new Error(`compliance ${op} returned no data (status ${response.statusCode})`);
95
+ }
96
+ return response.body.data;
97
+ }
98
+ // Shared body for all four validate/dry-run calls: POST → unwrap, and on a
99
+ // transport/HTTP failure either return a fail-open bypass result (when
100
+ // COMPLIANCE_BYPASS) or rethrow. The four methods differ ONLY in path, request
101
+ // body, op label, and bypass context.
102
+ async function runValidation(orgId, authHeader, path, body, op, bypassCtx) {
103
+ try {
104
+ const response = await client.post(path, body, validateOptions(orgId, authHeader));
105
+ return unwrap(response, op);
106
+ }
107
+ catch (error) {
108
+ if (COMPLIANCE_BYPASS)
109
+ return bypassResult(bypassCtx);
110
+ throw error;
111
+ }
112
+ }
113
+ return {
114
+ validatePlugin(orgId, attributes, authHeader, entityId, entityName, action) {
115
+ return runValidation(orgId, authHeader, '/compliance/validate/plugin', { attributes, entityId, entityName, action: action ?? 'upload' }, 'validatePlugin', { orgId, entityType: 'plugin', entityId, entityName });
116
+ },
117
+ validatePipeline(orgId, attributes, authHeader, entityId, entityName, action) {
118
+ return runValidation(orgId, authHeader, '/compliance/validate/pipeline', { attributes, entityId, entityName, action: action ?? 'create' }, 'validatePipeline', { orgId, entityType: 'pipeline', entityId, entityName });
119
+ },
120
+ dryRunPlugin(orgId, attributes, authHeader) {
121
+ return runValidation(orgId, authHeader, '/compliance/validate/plugin/dry-run', { attributes }, 'dryRunPlugin', { orgId, entityType: 'plugin', dryRun: true });
122
+ },
123
+ dryRunPipeline(orgId, attributes, authHeader) {
124
+ return runValidation(orgId, authHeader, '/compliance/validate/pipeline/dry-run', { attributes }, 'dryRunPipeline', { orgId, entityType: 'pipeline', dryRun: true });
125
+ },
126
+ };
127
+ }
128
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tcGxpYW5jZS1jbGllbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvc2VydmljZXMvY29tcGxpYW5jZS1jbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQUV0QyxPQUFPLEVBQUUsa0JBQWtCLEVBQXVCLE1BQU0sa0JBQWtCLENBQUM7QUFFM0UsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRWxELE1BQU0sTUFBTSxHQUFHLFlBQVksQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBRWpEOzs7R0FHRztBQUNILE1BQU0saUJBQWlCLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsS0FBSyxNQUFNLENBQUM7QUFFbkU7Ozs7O0dBS0c7QUFDSCxNQUFNLG1CQUFtQixHQUFHLFFBQVEsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLDhCQUE4QixJQUFJLE1BQU0sRUFBRSxFQUFFLENBQUMsQ0FBQztBQW1FL0Y7OztHQUdHO0FBQ0gsU0FBUyxZQUFZLENBQUMsT0FBZ0M7SUFDcEQsTUFBTSxDQUFDLElBQUksQ0FBQywwREFBMEQsRUFBRSxPQUFPLENBQUMsQ0FBQztJQUNqRixPQUFPO1FBQ0wsTUFBTSxFQUFFLElBQUk7UUFDWixPQUFPLEVBQUUsS0FBSztRQUNkLFVBQVUsRUFBRSxFQUFFO1FBQ2QsUUFBUSxFQUFFLENBQUMsRUFBRSxNQUFNLEVBQUUsRUFBRSxFQUFFLFFBQVEsRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLEVBQUUsRUFBRSxRQUFRLEVBQUUsRUFBRSxFQUFFLGFBQWEsRUFBRSxJQUFJLEVBQUUsV0FBVyxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsU0FBUyxFQUFFLE9BQU8sRUFBRSxnREFBZ0QsRUFBRSxDQUFDO1FBQ3pMLGNBQWMsRUFBRSxDQUFDO1FBQ2pCLFlBQVksRUFBRSxDQUFDO1FBQ2YsaUJBQWlCLEVBQUUsRUFBRTtLQUN0QixDQUFDO0FBQ0osQ0FBQztBQUVEOztHQUVHO0FBQ0gsU0FBUyxZQUFZLENBQUMsS0FBYSxFQUFFLFVBQWtCO0lBQ3JELE1BQU0sT0FBTyxHQUEyQjtRQUN0QyxVQUFVLEVBQUUsS0FBSztLQUNsQixDQUFDO0lBQ0YsOEVBQThFO0lBQzlFLCtFQUErRTtJQUMvRSwrREFBK0Q7SUFDL0QsSUFBSSxVQUFVO1FBQUUsT0FBTyxDQUFDLGFBQWEsR0FBRyxVQUFVLENBQUM7SUFDbkQsT0FBTyxPQUFPLENBQUM7QUFDakIsQ0FBQztBQUVEOzs7Ozs7Ozs7OztHQVdHO0FBQ0gsU0FBUyxlQUFlLENBQUMsS0FBYSxFQUFFLFVBQWtCO0lBQ3hELE9BQU87UUFDTCxPQUFPLEVBQUUsWUFBWSxDQUFDLEtBQUssRUFBRSxVQUFVLENBQUM7UUFDeEMsVUFBVSxFQUFFLElBQUk7UUFDaEIsT0FBTyxFQUFFLG1CQUFtQjtLQUM3QixDQUFDO0FBQ0osQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILE1BQU0sVUFBVSxzQkFBc0IsQ0FBQyxNQUErQjtJQUNwRSxNQUFNLGFBQWEsR0FBa0I7UUFDbkMsSUFBSSxFQUFFLE1BQU0sRUFBRSxJQUFJLElBQUksT0FBTyxDQUFDLEdBQUcsQ0FBQyx1QkFBdUIsSUFBSSxZQUFZO1FBQ3pFLElBQUksRUFBRSxNQUFNLEVBQUUsSUFBSSxJQUFJLFFBQVEsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLHVCQUF1QixJQUFJLE1BQU0sRUFBRSxFQUFFLENBQUM7S0FDbEYsQ0FBQztJQUVGLE1BQU0sTUFBTSxHQUFHLElBQUksa0JBQWtCLENBQUMsYUFBYSxDQUFDLENBQUM7SUFFckQ7Ozs7Ozs7T0FPRztJQUNILFNBQVMsTUFBTSxDQUNiLFFBQTZHLEVBQzdHLEVBQVU7UUFFVixJQUFJLFFBQVEsQ0FBQyxVQUFVLEdBQUcsR0FBRyxJQUFJLFFBQVEsQ0FBQyxVQUFVLElBQUksR0FBRyxFQUFFLENBQUM7WUFDNUQsTUFBTSxHQUFHLEdBQUcsUUFBUSxDQUFDLElBQUksRUFBRSxPQUFPLElBQUksUUFBUSxRQUFRLENBQUMsVUFBVSxFQUFFLENBQUM7WUFDcEUsTUFBTSxJQUFJLEtBQUssQ0FBQyxjQUFjLEVBQUUsWUFBWSxHQUFHLEVBQUUsQ0FBQyxDQUFDO1FBQ3JELENBQUM7UUFDRCxJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRSxJQUFJLEVBQUUsQ0FBQztZQUN6QixNQUFNLElBQUksS0FBSyxDQUFDLGNBQWMsRUFBRSw2QkFBNkIsUUFBUSxDQUFDLFVBQVUsR0FBRyxDQUFDLENBQUM7UUFDdkYsQ0FBQztRQUNELE9BQU8sUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7SUFDNUIsQ0FBQztJQUVELDJFQUEyRTtJQUMzRSx1RUFBdUU7SUFDdkUsK0VBQStFO0lBQy9FLHNDQUFzQztJQUN0QyxLQUFLLFVBQVUsYUFBYSxDQUMxQixLQUFhLEVBQ2IsVUFBa0IsRUFDbEIsSUFBWSxFQUNaLElBQTZCLEVBQzdCLEVBQVUsRUFDVixTQUFrQztRQUVsQyxJQUFJLENBQUM7WUFDSCxNQUFNLFFBQVEsR0FBRyxNQUFNLE1BQU0sQ0FBQyxJQUFJLENBQ2hDLElBQUksRUFBRSxJQUFJLEVBQUUsZUFBZSxDQUFDLEtBQUssRUFBRSxVQUFVLENBQUMsQ0FDL0MsQ0FBQztZQUNGLE9BQU8sTUFBTSxDQUFDLFFBQVEsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUM5QixDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLElBQUksaUJBQWlCO2dCQUFFLE9BQU8sWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDO1lBQ3RELE1BQU0sS0FBSyxDQUFDO1FBQ2QsQ0FBQztJQUNILENBQUM7SUFFRCxPQUFPO1FBQ0wsY0FBYyxDQUFDLEtBQUssRUFBRSxVQUFVLEVBQUUsVUFBVSxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQUUsTUFBTTtZQUN4RSxPQUFPLGFBQWEsQ0FBQyxLQUFLLEVBQUUsVUFBVSxFQUFFLDZCQUE2QixFQUNuRSxFQUFFLFVBQVUsRUFBRSxRQUFRLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxNQUFNLElBQUksUUFBUSxFQUFFLEVBQUUsZ0JBQWdCLEVBQ2xGLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUM7UUFDM0QsQ0FBQztRQUVELGdCQUFnQixDQUFDLEtBQUssRUFBRSxVQUFVLEVBQUUsVUFBVSxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQUUsTUFBTTtZQUMxRSxPQUFPLGFBQWEsQ0FBQyxLQUFLLEVBQUUsVUFBVSxFQUFFLCtCQUErQixFQUNyRSxFQUFFLFVBQVUsRUFBRSxRQUFRLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxNQUFNLElBQUksUUFBUSxFQUFFLEVBQUUsa0JBQWtCLEVBQ3BGLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxVQUFVLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUM7UUFDN0QsQ0FBQztRQUVELFlBQVksQ0FBQyxLQUFLLEVBQUUsVUFBVSxFQUFFLFVBQVU7WUFDeEMsT0FBTyxhQUFhLENBQUMsS0FBSyxFQUFFLFVBQVUsRUFBRSxxQ0FBcUMsRUFDM0UsRUFBRSxVQUFVLEVBQUUsRUFBRSxjQUFjLEVBQUUsRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLFFBQVEsRUFBRSxNQUFNLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztRQUNuRixDQUFDO1FBRUQsY0FBYyxDQUFDLEtBQUssRUFBRSxVQUFVLEVBQUUsVUFBVTtZQUMxQyxPQUFPLGFBQWEsQ0FBQyxLQUFLLEVBQUUsVUFBVSxFQUFFLHVDQUF1QyxFQUM3RSxFQUFFLFVBQVUsRUFBRSxFQUFFLGdCQUFnQixFQUFFLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxVQUFVLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDdkYsQ0FBQztLQUNGLENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IEludGVybmFsSHR0cENsaWVudCwgdHlwZSBSZXF1ZXN0T3B0aW9ucyB9IGZyb20gJy4vaHR0cC1jbGllbnQuanMnO1xuaW1wb3J0IHR5cGUgeyBTZXJ2aWNlQ29uZmlnIH0gZnJvbSAnLi4vdHlwZXMvY29tbW9uLmpzJztcbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciB9IGZyb20gJy4uL3V0aWxzL2xvZ2dlci5qcyc7XG5cbmNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcignY29tcGxpYW5jZS1jbGllbnQnKTtcblxuLyoqXG4gKiBXaGVuIHRydWUsIGNvbXBsaWFuY2UgY2hlY2tzIGFyZSBieXBhc3NlZCBpZiB0aGUgc2VydmljZSBpcyB1bmF2YWlsYWJsZVxuICogKGZhaWwtb3BlbikuIERlZmF1bHQgaXMgZmFpbC1jbG9zZWQgKGVycm9ycyBwcm9wYWdhdGUgYW5kIGJsb2NrIHRoZSBvcGVyYXRpb24pLlxuICovXG5jb25zdCBDT01QTElBTkNFX0JZUEFTUyA9IHByb2Nlc3MuZW52LkNPTVBMSUFOQ0VfQllQQVNTID09PSAndHJ1ZSc7XG5cbi8qKlxuICogUGVyLWF0dGVtcHQgdGltZW91dCBmb3IgYSB2YWxpZGF0aW9uIFBPU1QgKGVudjogYENPTVBMSUFOQ0VfVkFMSURBVEVfVElNRU9VVF9NU2ApLlxuICogRGVsaWJlcmF0ZWx5IFRJR0hURVIgdGhhbiB0aGUgNXMgY2xpZW50IGRlZmF1bHQgc28gdGhlIGVuZm9yY2VkIHJldHJpZXMgKGJlbG93KVxuICogZml0IGluc2lkZSBhIGJvdW5kZWQgdG90YWwgYnVkZ2V0IHJhdGhlciB0aGFuIHN0YWNraW5nIHRocmVlIDVzIHN0YWxscyBvbnRvIGFcbiAqIHNpbmdsZSBpbnRlcmFjdGl2ZSByZXF1ZXN0LlxuICovXG5jb25zdCBWQUxJREFURV9USU1FT1VUX01TID0gcGFyc2VJbnQocHJvY2Vzcy5lbnYuQ09NUExJQU5DRV9WQUxJREFURV9USU1FT1VUX01TID8/ICc0MDAwJywgMTApO1xuXG4vKipcbiAqIFJlc3VsdCBvZiBhIGNvbXBsaWFuY2UgdmFsaWRhdGlvbiBjaGVjay5cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBDb21wbGlhbmNlQ2hlY2tSZXN1bHQge1xuICBwYXNzZWQ6IGJvb2xlYW47XG4gIHZpb2xhdGlvbnM6IENvbXBsaWFuY2VWaW9sYXRpb25bXTtcbiAgd2FybmluZ3M6IENvbXBsaWFuY2VWaW9sYXRpb25bXTtcbiAgYmxvY2tlZDogYm9vbGVhbjtcbiAgcnVsZXNFdmFsdWF0ZWQ6IG51bWJlcjtcbiAgcnVsZXNTa2lwcGVkOiBudW1iZXI7XG4gIGV4ZW1wdGlvbnNBcHBsaWVkOiBzdHJpbmdbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb21wbGlhbmNlVmlvbGF0aW9uIHtcbiAgcnVsZUlkOiBzdHJpbmc7XG4gIHJ1bGVOYW1lOiBzdHJpbmc7XG4gIHBvbGljeUlkPzogc3RyaW5nIHwgbnVsbDtcbiAgZmllbGQ6IHN0cmluZztcbiAgb3BlcmF0b3I6IHN0cmluZztcbiAgZXhwZWN0ZWRWYWx1ZTogdW5rbm93bjtcbiAgYWN0dWFsVmFsdWU6IHVua25vd247XG4gIHNldmVyaXR5OiBzdHJpbmc7XG4gIG1lc3NhZ2U6IHN0cmluZztcbn1cblxuLyoqXG4gKiBDb21wbGlhbmNlIHNlcnZpY2UgY2xpZW50IGludGVyZmFjZS5cbiAqIFVzZXMgZmFpbC1jbG9zZWQgZGVzaWduOiBlcnJvcnMgcHJvcGFnYXRlIHRvIGJsb2NrIHRoZSBvcGVyYXRpb24uXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgQ29tcGxpYW5jZUNsaWVudCB7XG4gIC8qKiBWYWxpZGF0ZSBwbHVnaW4gYXR0cmlidXRlcyBhZ2FpbnN0IG9yZyBydWxlcy4gVGhyb3dzIG9uIHNlcnZpY2UgZXJyb3IgKGZhaWwtY2xvc2VkKS4gKi9cbiAgdmFsaWRhdGVQbHVnaW4oXG4gICAgb3JnSWQ6IHN0cmluZyxcbiAgICBhdHRyaWJ1dGVzOiBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPixcbiAgICBhdXRoSGVhZGVyOiBzdHJpbmcsXG4gICAgZW50aXR5SWQ/OiBzdHJpbmcsXG4gICAgZW50aXR5TmFtZT86IHN0cmluZyxcbiAgICBhY3Rpb24/OiBzdHJpbmcsXG4gICk6IFByb21pc2U8Q29tcGxpYW5jZUNoZWNrUmVzdWx0PjtcblxuICAvKiogVmFsaWRhdGUgcGlwZWxpbmUgYXR0cmlidXRlcyBhZ2FpbnN0IG9yZyBydWxlcy4gVGhyb3dzIG9uIHNlcnZpY2UgZXJyb3IgKGZhaWwtY2xvc2VkKS4gKi9cbiAgdmFsaWRhdGVQaXBlbGluZShcbiAgICBvcmdJZDogc3RyaW5nLFxuICAgIGF0dHJpYnV0ZXM6IFJlY29yZDxzdHJpbmcsIHVua25vd24+LFxuICAgIGF1dGhIZWFkZXI6IHN0cmluZyxcbiAgICBlbnRpdHlJZD86IHN0cmluZyxcbiAgICBlbnRpdHlOYW1lPzogc3RyaW5nLFxuICAgIGFjdGlvbj86IHN0cmluZyxcbiAgKTogUHJvbWlzZTxDb21wbGlhbmNlQ2hlY2tSZXN1bHQ+O1xuXG4gIC8qKiBQcmUtZmxpZ2h0IGNoZWNrIGZvciBwbHVnaW4gKG5vIGF1ZGl0LCBubyBub3RpZmljYXRpb24pLiAqL1xuICBkcnlSdW5QbHVnaW4oXG4gICAgb3JnSWQ6IHN0cmluZyxcbiAgICBhdHRyaWJ1dGVzOiBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPixcbiAgICBhdXRoSGVhZGVyOiBzdHJpbmcsXG4gICk6IFByb21pc2U8Q29tcGxpYW5jZUNoZWNrUmVzdWx0PjtcblxuICAvKiogUHJlLWZsaWdodCBjaGVjayBmb3IgcGlwZWxpbmUgKG5vIGF1ZGl0LCBubyBub3RpZmljYXRpb24pLiAqL1xuICBkcnlSdW5QaXBlbGluZShcbiAgICBvcmdJZDogc3RyaW5nLFxuICAgIGF0dHJpYnV0ZXM6IFJlY29yZDxzdHJpbmcsIHVua25vd24+LFxuICAgIGF1dGhIZWFkZXI6IHN0cmluZyxcbiAgKTogUHJvbWlzZTxDb21wbGlhbmNlQ2hlY2tSZXN1bHQ+O1xufVxuXG4vKipcbiAqIFJldHVybiBhIHBhc3MtdGhyb3VnaCByZXN1bHQgd2hlbiBDT01QTElBTkNFX0JZUEFTUyBpcyBlbmFibGVkIGFuZCB0aGVcbiAqIGNvbXBsaWFuY2Ugc2VydmljZSBpcyB1bnJlYWNoYWJsZS5cbiAqL1xuZnVuY3Rpb24gYnlwYXNzUmVzdWx0KGNvbnRleHQ6IFJlY29yZDxzdHJpbmcsIHVua25vd24+KTogQ29tcGxpYW5jZUNoZWNrUmVzdWx0IHtcbiAgbG9nZ2VyLndhcm4oJ0NPTVBMSUFOQ0VfQllQQVNTOiBTZXJ2aWNlIHVuYXZhaWxhYmxlLCBhbGxvd2luZyByZXF1ZXN0JywgY29udGV4dCk7XG4gIHJldHVybiB7XG4gICAgcGFzc2VkOiB0cnVlLFxuICAgIGJsb2NrZWQ6IGZhbHNlLFxuICAgIHZpb2xhdGlvbnM6IFtdLFxuICAgIHdhcm5pbmdzOiBbeyBydWxlSWQ6ICcnLCBydWxlTmFtZTogJycsIGZpZWxkOiAnJywgb3BlcmF0b3I6ICcnLCBleHBlY3RlZFZhbHVlOiBudWxsLCBhY3R1YWxWYWx1ZTogbnVsbCwgc2V2ZXJpdHk6ICd3YXJuaW5nJywgbWVzc2FnZTogJ0NvbXBsaWFuY2UgY2hlY2sgc2tpcHBlZCAoc2VydmljZSB1bmF2YWlsYWJsZSknIH1dLFxuICAgIHJ1bGVzRXZhbHVhdGVkOiAwLFxuICAgIHJ1bGVzU2tpcHBlZDogMCxcbiAgICBleGVtcHRpb25zQXBwbGllZDogW10sXG4gIH07XG59XG5cbi8qKlxuICogQnVpbGQgY29tbW9uIHJlcXVlc3QgaGVhZGVycyBmb3IgY29tcGxpYW5jZSBjYWxscy5cbiAqL1xuZnVuY3Rpb24gYnVpbGRIZWFkZXJzKG9yZ0lkOiBzdHJpbmcsIGF1dGhIZWFkZXI6IHN0cmluZyk6IFJlY29yZDxzdHJpbmcsIHN0cmluZz4ge1xuICBjb25zdCBoZWFkZXJzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0ge1xuICAgICd4LW9yZy1pZCc6IG9yZ0lkLFxuICB9O1xuICAvLyBOT1RFOiB0aGUgb2xkIGB4LWludGVybmFsLXNlcnZpY2U6IHRydWVgIGhlYWRlciB3YXMgcmVtb3ZlZCDigJQgaXQgaXMgdHJ1c3RlZFxuICAvLyBieSBub3RoaW5nIChpdCB3YXMgc3Bvb2ZhYmxlKTsgY29tcGxpYW5jZSByb3V0ZXMgYXV0aGVudGljYXRlIHRoZSBjYWxsZXIgdmlhXG4gIC8vIHRoZSBgQXV0aG9yaXphdGlvbmAgc2VydmljZSBKV1QgKGByZXF1aXJlU2VydmljZVByaW5jaXBhbGApLlxuICBpZiAoYXV0aEhlYWRlcikgaGVhZGVycy5BdXRob3JpemF0aW9uID0gYXV0aEhlYWRlcjtcbiAgcmV0dXJuIGhlYWRlcnM7XG59XG5cbi8qKlxuICogUmVxdWVzdCBvcHRpb25zIGZvciBhIGNvbXBsaWFuY2UgdmFsaWRhdGlvbiBQT1NULlxuICpcbiAqIFRoZXNlIGNhbGxzIGFyZSBzaWRlLWVmZmVjdC1mcmVlICh0aGV5IGV2YWx1YXRlIHJ1bGVzIGFuZCByZXR1cm4gYSB2ZXJkaWN0O1xuICogYW55IGF1ZGl0L25vdGlmaWNhdGlvbiB0aGUgY29tcGxpYW5jZSBzZXJ2aWNlIHdyaXRlcyBpcyBkZXJpdmVkLCBub3QgYSBtdXRhdGlvblxuICogdGhlIGNhbGxlciBkZXBlbmRzIG9uIGJlaW5nIG9uY2Utb25seSksIHNvIHRoZXkgYXJlIG1hcmtlZCBgaWRlbXBvdGVudDogdHJ1ZWAuXG4gKiBUaGF0IGxldHMgdGhlIEhUVFAgY2xpZW50IGF1dG8tcmV0cnkgYSB0cmFuc2llbnQgNTAzIC8gY29ubmVjdGlvbiByZXNldCAvXG4gKiB0aW1lb3V0IGluc3RlYWQgb2Ygc3VyZmFjaW5nIGl0IGFzIGEgaGFyZCBmYWlsdXJlIOKAlCB3aGljaCwgdW5kZXIgdGhlXG4gKiBmYWlsLWNsb3NlZCBkZWZhdWx0LCB3b3VsZCB3cm9uZ2x5IFJFSkVDVCBhIGxlZ2l0aW1hdGUgcGx1Z2luIHVwbG9hZCAvIHBpcGVsaW5lXG4gKiBjcmVhdGUganVzdCBiZWNhdXNlIGNvbXBsaWFuY2UgYnJpZWZseSBzdGFsbGVkLiBQYWlyZWQgd2l0aCB0aGUgdGlnaHRlclxuICogcGVyLWF0dGVtcHQgdGltZW91dCBzbyB0aGUgcmV0cmllcyBzdGF5IHdpdGhpbiBhIGJvdW5kZWQgdG90YWwgYnVkZ2V0LlxuICovXG5mdW5jdGlvbiB2YWxpZGF0ZU9wdGlvbnMob3JnSWQ6IHN0cmluZywgYXV0aEhlYWRlcjogc3RyaW5nKTogUmVxdWVzdE9wdGlvbnMge1xuICByZXR1cm4ge1xuICAgIGhlYWRlcnM6IGJ1aWxkSGVhZGVycyhvcmdJZCwgYXV0aEhlYWRlciksXG4gICAgaWRlbXBvdGVudDogdHJ1ZSxcbiAgICB0aW1lb3V0OiBWQUxJREFURV9USU1FT1VUX01TLFxuICB9O1xufVxuXG4vKipcbiAqIENyZWF0ZSBhIGNvbXBsaWFuY2UgY2xpZW50LlxuICpcbiAqIElNUE9SVEFOVDogVGhpcyBjbGllbnQgaXMgZmFpbC1jbG9zZWQg4oCUIGlmIHRoZSBjb21wbGlhbmNlIHNlcnZpY2UgaXNcbiAqIHVucmVhY2hhYmxlIG9yIHJldHVybnMgYW4gZXJyb3IsIHRoZSBlcnJvciBwcm9wYWdhdGVzIGFuZCB0aGUgY2FsbGluZ1xuICogb3BlcmF0aW9uIChwbHVnaW4gdXBsb2FkLCBwaXBlbGluZSBjcmVhdGUpIGlzIHJlamVjdGVkLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlQ29tcGxpYW5jZUNsaWVudChjb25maWc/OiBQYXJ0aWFsPFNlcnZpY2VDb25maWc+KTogQ29tcGxpYW5jZUNsaWVudCB7XG4gIGNvbnN0IHNlcnZpY2VDb25maWc6IFNlcnZpY2VDb25maWcgPSB7XG4gICAgaG9zdDogY29uZmlnPy5ob3N0ID8/IHByb2Nlc3MuZW52LkNPTVBMSUFOQ0VfU0VSVklDRV9IT1NUID8/ICdjb21wbGlhbmNlJyxcbiAgICBwb3J0OiBjb25maWc/LnBvcnQgPz8gcGFyc2VJbnQocHJvY2Vzcy5lbnYuQ09NUExJQU5DRV9TRVJWSUNFX1BPUlQgPz8gJzMwMDAnLCAxMCksXG4gIH07XG5cbiAgY29uc3QgY2xpZW50ID0gbmV3IEludGVybmFsSHR0cENsaWVudChzZXJ2aWNlQ29uZmlnKTtcblxuICAvKipcbiAgICogVmFsaWRhdGUgcmVzcG9uc2Ugc2hhcGUgKyBzdGF0dXMuIFRoZSBIVFRQIGNsaWVudCBkb2Vzbid0IHRocm93IG9uIG5vbi0yeHhcbiAgICogKGl0IGp1c3QgcmV0dXJucyB0aGUgYm9keSksIHNvIHdpdGhvdXQgdGhpcyBhbiBhdXRoIGZhaWx1cmUgLyByb3V0ZSBtaXNzXG4gICAqIHNpbGVudGx5IHByb2R1Y2VkIGByZXNwb25zZS5ib2R5LmRhdGEgPT09IHVuZGVmaW5lZGAgYW5kIHRoZSBjYWxsZXJcbiAgICogY3Jhc2hlZCBvbiBgLmJsb2NrZWRgLiBOb3cgbm9uLTJ4eCBzdXJmYWNlcyBhcyBhIHRocm93biBlcnJvciB0aGF0IHRoZVxuICAgKiBvdXRlciB0cnkvY2F0Y2ggY2FuIHR1cm4gaW50byBlaXRoZXIgZmFpbC1jbG9zZWQgKGRlZmF1bHQpIG9yXG4gICAqIENPTVBMSUFOQ0VfQllQQVNTIChmYWlsLW9wZW4pLlxuICAgKi9cbiAgZnVuY3Rpb24gdW53cmFwKFxuICAgIHJlc3BvbnNlOiB7IHN0YXR1c0NvZGU6IG51bWJlcjsgYm9keTogeyBzdWNjZXNzPzogYm9vbGVhbjsgZGF0YT86IENvbXBsaWFuY2VDaGVja1Jlc3VsdDsgbWVzc2FnZT86IHN0cmluZyB9IH0sXG4gICAgb3A6IHN0cmluZyxcbiAgKTogQ29tcGxpYW5jZUNoZWNrUmVzdWx0IHtcbiAgICBpZiAocmVzcG9uc2Uuc3RhdHVzQ29kZSA8IDIwMCB8fCByZXNwb25zZS5zdGF0dXNDb2RlID49IDMwMCkge1xuICAgICAgY29uc3QgbXNnID0gcmVzcG9uc2UuYm9keT8ubWVzc2FnZSA/PyBgSFRUUCAke3Jlc3BvbnNlLnN0YXR1c0NvZGV9YDtcbiAgICAgIHRocm93IG5ldyBFcnJvcihgY29tcGxpYW5jZSAke29wfSBmYWlsZWQ6ICR7bXNnfWApO1xuICAgIH1cbiAgICBpZiAoIXJlc3BvbnNlLmJvZHk/LmRhdGEpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcihgY29tcGxpYW5jZSAke29wfSByZXR1cm5lZCBubyBkYXRhIChzdGF0dXMgJHtyZXNwb25zZS5zdGF0dXNDb2RlfSlgKTtcbiAgICB9XG4gICAgcmV0dXJuIHJlc3BvbnNlLmJvZHkuZGF0YTtcbiAgfVxuXG4gIC8vIFNoYXJlZCBib2R5IGZvciBhbGwgZm91ciB2YWxpZGF0ZS9kcnktcnVuIGNhbGxzOiBQT1NUIOKGkiB1bndyYXAsIGFuZCBvbiBhXG4gIC8vIHRyYW5zcG9ydC9IVFRQIGZhaWx1cmUgZWl0aGVyIHJldHVybiBhIGZhaWwtb3BlbiBieXBhc3MgcmVzdWx0ICh3aGVuXG4gIC8vIENPTVBMSUFOQ0VfQllQQVNTKSBvciByZXRocm93LiBUaGUgZm91ciBtZXRob2RzIGRpZmZlciBPTkxZIGluIHBhdGgsIHJlcXVlc3RcbiAgLy8gYm9keSwgb3AgbGFiZWwsIGFuZCBieXBhc3MgY29udGV4dC5cbiAgYXN5bmMgZnVuY3Rpb24gcnVuVmFsaWRhdGlvbihcbiAgICBvcmdJZDogc3RyaW5nLFxuICAgIGF1dGhIZWFkZXI6IHN0cmluZyxcbiAgICBwYXRoOiBzdHJpbmcsXG4gICAgYm9keTogUmVjb3JkPHN0cmluZywgdW5rbm93bj4sXG4gICAgb3A6IHN0cmluZyxcbiAgICBieXBhc3NDdHg6IFJlY29yZDxzdHJpbmcsIHVua25vd24+LFxuICApOiBQcm9taXNlPENvbXBsaWFuY2VDaGVja1Jlc3VsdD4ge1xuICAgIHRyeSB7XG4gICAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGNsaWVudC5wb3N0PHsgc3VjY2VzczogYm9vbGVhbjsgZGF0YTogQ29tcGxpYW5jZUNoZWNrUmVzdWx0OyBtZXNzYWdlPzogc3RyaW5nIH0+KFxuICAgICAgICBwYXRoLCBib2R5LCB2YWxpZGF0ZU9wdGlvbnMob3JnSWQsIGF1dGhIZWFkZXIpLFxuICAgICAgKTtcbiAgICAgIHJldHVybiB1bndyYXAocmVzcG9uc2UsIG9wKTtcbiAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgaWYgKENPTVBMSUFOQ0VfQllQQVNTKSByZXR1cm4gYnlwYXNzUmVzdWx0KGJ5cGFzc0N0eCk7XG4gICAgICB0aHJvdyBlcnJvcjtcbiAgICB9XG4gIH1cblxuICByZXR1cm4ge1xuICAgIHZhbGlkYXRlUGx1Z2luKG9yZ0lkLCBhdHRyaWJ1dGVzLCBhdXRoSGVhZGVyLCBlbnRpdHlJZCwgZW50aXR5TmFtZSwgYWN0aW9uKSB7XG4gICAgICByZXR1cm4gcnVuVmFsaWRhdGlvbihvcmdJZCwgYXV0aEhlYWRlciwgJy9jb21wbGlhbmNlL3ZhbGlkYXRlL3BsdWdpbicsXG4gICAgICAgIHsgYXR0cmlidXRlcywgZW50aXR5SWQsIGVudGl0eU5hbWUsIGFjdGlvbjogYWN0aW9uID8/ICd1cGxvYWQnIH0sICd2YWxpZGF0ZVBsdWdpbicsXG4gICAgICAgIHsgb3JnSWQsIGVudGl0eVR5cGU6ICdwbHVnaW4nLCBlbnRpdHlJZCwgZW50aXR5TmFtZSB9KTtcbiAgICB9LFxuXG4gICAgdmFsaWRhdGVQaXBlbGluZShvcmdJZCwgYXR0cmlidXRlcywgYXV0aEhlYWRlciwgZW50aXR5SWQsIGVudGl0eU5hbWUsIGFjdGlvbikge1xuICAgICAgcmV0dXJuIHJ1blZhbGlkYXRpb24ob3JnSWQsIGF1dGhIZWFkZXIsICcvY29tcGxpYW5jZS92YWxpZGF0ZS9waXBlbGluZScsXG4gICAgICAgIHsgYXR0cmlidXRlcywgZW50aXR5SWQsIGVudGl0eU5hbWUsIGFjdGlvbjogYWN0aW9uID8/ICdjcmVhdGUnIH0sICd2YWxpZGF0ZVBpcGVsaW5lJyxcbiAgICAgICAgeyBvcmdJZCwgZW50aXR5VHlwZTogJ3BpcGVsaW5lJywgZW50aXR5SWQsIGVudGl0eU5hbWUgfSk7XG4gICAgfSxcblxuICAgIGRyeVJ1blBsdWdpbihvcmdJZCwgYXR0cmlidXRlcywgYXV0aEhlYWRlcikge1xuICAgICAgcmV0dXJuIHJ1blZhbGlkYXRpb24ob3JnSWQsIGF1dGhIZWFkZXIsICcvY29tcGxpYW5jZS92YWxpZGF0ZS9wbHVnaW4vZHJ5LXJ1bicsXG4gICAgICAgIHsgYXR0cmlidXRlcyB9LCAnZHJ5UnVuUGx1Z2luJywgeyBvcmdJZCwgZW50aXR5VHlwZTogJ3BsdWdpbicsIGRyeVJ1bjogdHJ1ZSB9KTtcbiAgICB9LFxuXG4gICAgZHJ5UnVuUGlwZWxpbmUob3JnSWQsIGF0dHJpYnV0ZXMsIGF1dGhIZWFkZXIpIHtcbiAgICAgIHJldHVybiBydW5WYWxpZGF0aW9uKG9yZ0lkLCBhdXRoSGVhZGVyLCAnL2NvbXBsaWFuY2UvdmFsaWRhdGUvcGlwZWxpbmUvZHJ5LXJ1bicsXG4gICAgICAgIHsgYXR0cmlidXRlcyB9LCAnZHJ5UnVuUGlwZWxpbmUnLCB7IG9yZ0lkLCBlbnRpdHlUeXBlOiAncGlwZWxpbmUnLCBkcnlSdW46IHRydWUgfSk7XG4gICAgfSxcbiAgfTtcbn1cbiJdfQ==
@@ -0,0 +1,16 @@
1
+ import { type ServiceConfig } from '../types/common.js';
2
+ /**
3
+ * Registers an entity event subscriber that forwards events to the compliance service.
4
+ *
5
+ * Call this at service startup (in index.ts) to enable automatic compliance
6
+ * notification on entity mutations. Events are fire-and-forget — failures
7
+ * are logged but never block the original request.
8
+ *
9
+ * @param config - Optional service config override (defaults to COMPLIANCE_SERVICE_HOST/PORT env vars)
10
+ * @param serviceName - Service identifier baked into the signed JWT's `sub`
11
+ * (e.g. 'pipeline', 'plugin'). The compliance route requires a service
12
+ * principal — a spoofable `x-internal-service: true` header is no longer
13
+ * accepted. Defaults to a generic 'entity-events' when unspecified, but
14
+ * callers should pass their service name for accurate audit trails.
15
+ */
16
+ export declare function registerComplianceEventSubscriber(config?: Partial<ServiceConfig>, serviceName?: string): void;
@@ -0,0 +1,72 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { entityEvents } from './entity-events.js';
4
+ import { InternalHttpClient } from './http-client.js';
5
+ import { getServiceAuthHeader } from '../middleware/auth.js';
6
+ import {} from '../types/common.js';
7
+ import { createLogger } from '../utils/logger.js';
8
+ import { emitCounter } from '../utils/metric-emitter.js';
9
+ const logger = createLogger('compliance-events');
10
+ /**
11
+ * Registers an entity event subscriber that forwards events to the compliance service.
12
+ *
13
+ * Call this at service startup (in index.ts) to enable automatic compliance
14
+ * notification on entity mutations. Events are fire-and-forget — failures
15
+ * are logged but never block the original request.
16
+ *
17
+ * @param config - Optional service config override (defaults to COMPLIANCE_SERVICE_HOST/PORT env vars)
18
+ * @param serviceName - Service identifier baked into the signed JWT's `sub`
19
+ * (e.g. 'pipeline', 'plugin'). The compliance route requires a service
20
+ * principal — a spoofable `x-internal-service: true` header is no longer
21
+ * accepted. Defaults to a generic 'entity-events' when unspecified, but
22
+ * callers should pass their service name for accurate audit trails.
23
+ */
24
+ export function registerComplianceEventSubscriber(config, serviceName = 'entity-events') {
25
+ const serviceConfig = {
26
+ host: config?.host ?? process.env.COMPLIANCE_SERVICE_HOST ?? 'compliance',
27
+ port: config?.port ?? parseInt(process.env.COMPLIANCE_SERVICE_PORT ?? '3000', 10),
28
+ };
29
+ const client = new InternalHttpClient(serviceConfig);
30
+ const subscriber = {
31
+ async onEntityEvent(event) {
32
+ try {
33
+ // Mint a per-event service JWT scoped to the event's org so the
34
+ // compliance route's `runWithTenantContext` sees the right tenant
35
+ // GUC. The compliance side enforces `requireAuth` +
36
+ // `requireServicePrincipal` — the previous `x-internal-service`
37
+ // header is no longer sufficient (and was spoofable).
38
+ //
39
+ // This HTTP notify is the SINGLE delivery channel for post-mutation
40
+ // re-validation (primary enforcement is the fail-CLOSED live
41
+ // validate path). A stable per-event `Idempotency-Key` makes the POST
42
+ // retry-safe in the http-client, so a transient 5xx/timeout is retried
43
+ // rather than dropped on the first attempt; compliance re-evaluation is
44
+ // naturally idempotent, so a duplicate delivery is harmless.
45
+ await client.post('/compliance/events/entity', event, {
46
+ headers: {
47
+ 'Authorization': getServiceAuthHeader({ serviceName, orgId: event.orgId, role: 'member' }),
48
+ 'Idempotency-Key': `${event.target}:${event.entityId}:${event.eventType}:${event.timestamp.toISOString()}`,
49
+ },
50
+ });
51
+ }
52
+ catch (err) {
53
+ // Fire-and-forget: log + a drop metric so sustained loss is alertable
54
+ // (operators can't act on a warn line alone), then swallow — compliance
55
+ // notification is non-fatal to the originating mutation.
56
+ emitCounter('compliance_event_drop_total', { service: serviceName, target: event.target });
57
+ logger.warn('Failed to notify compliance service of entity event', {
58
+ target: event.target,
59
+ eventType: event.eventType,
60
+ entityId: event.entityId,
61
+ error: err instanceof Error ? err.message : String(err),
62
+ });
63
+ }
64
+ },
65
+ };
66
+ entityEvents.subscribe(subscriber);
67
+ logger.info('Compliance event subscriber registered', {
68
+ host: serviceConfig.host,
69
+ port: serviceConfig.port,
70
+ });
71
+ }
72
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tcGxpYW5jZS1ldmVudC1zdWJzY3JpYmVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3NlcnZpY2VzL2NvbXBsaWFuY2UtZXZlbnQtc3Vic2NyaWJlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLE9BQU8sRUFBRSxZQUFZLEVBQWdELE1BQU0sb0JBQW9CLENBQUM7QUFDaEcsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDdEQsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDN0QsT0FBTyxFQUFzQixNQUFNLG9CQUFvQixDQUFDO0FBQ3hELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNsRCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFFekQsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFFakQ7Ozs7Ozs7Ozs7Ozs7R0FhRztBQUNILE1BQU0sVUFBVSxpQ0FBaUMsQ0FDL0MsTUFBK0IsRUFDL0IsV0FBVyxHQUFXLGVBQWU7SUFFckMsTUFBTSxhQUFhLEdBQWtCO1FBQ25DLElBQUksRUFBRSxNQUFNLEVBQUUsSUFBSSxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsdUJBQXVCLElBQUksWUFBWTtRQUN6RSxJQUFJLEVBQUUsTUFBTSxFQUFFLElBQUksSUFBSSxRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyx1QkFBdUIsSUFBSSxNQUFNLEVBQUUsRUFBRSxDQUFDO0tBQ2xGLENBQUM7SUFFRixNQUFNLE1BQU0sR0FBRyxJQUFJLGtCQUFrQixDQUFDLGFBQWEsQ0FBQyxDQUFDO0lBRXJELE1BQU0sVUFBVSxHQUEwQjtRQUN4QyxLQUFLLENBQUMsYUFBYSxDQUFDLEtBQWtCO1lBQ3BDLElBQUksQ0FBQztnQkFDSCxnRUFBZ0U7Z0JBQ2hFLGtFQUFrRTtnQkFDbEUsb0RBQW9EO2dCQUNwRCxnRUFBZ0U7Z0JBQ2hFLHNEQUFzRDtnQkFDdEQsRUFBRTtnQkFDRixvRUFBb0U7Z0JBQ3BFLDZEQUE2RDtnQkFDN0Qsc0VBQXNFO2dCQUN0RSx1RUFBdUU7Z0JBQ3ZFLHdFQUF3RTtnQkFDeEUsNkRBQTZEO2dCQUM3RCxNQUFNLE1BQU0sQ0FBQyxJQUFJLENBQUMsMkJBQTJCLEVBQUUsS0FBSyxFQUFFO29CQUNwRCxPQUFPLEVBQUU7d0JBQ1AsZUFBZSxFQUFFLG9CQUFvQixDQUFDLEVBQUUsV0FBVyxFQUFFLEtBQUssRUFBRSxLQUFLLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsQ0FBQzt3QkFDMUYsaUJBQWlCLEVBQUUsR0FBRyxLQUFLLENBQUMsTUFBTSxJQUFJLEtBQUssQ0FBQyxRQUFRLElBQUksS0FBSyxDQUFDLFNBQVMsSUFBSSxLQUFLLENBQUMsU0FBUyxDQUFDLFdBQVcsRUFBRSxFQUFFO3FCQUMzRztpQkFDRixDQUFDLENBQUM7WUFDTCxDQUFDO1lBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztnQkFDYixzRUFBc0U7Z0JBQ3RFLHdFQUF3RTtnQkFDeEUseURBQXlEO2dCQUN6RCxXQUFXLENBQUMsNkJBQTZCLEVBQUUsRUFBRSxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sRUFBRSxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztnQkFDM0YsTUFBTSxDQUFDLElBQUksQ0FBQyxxREFBcUQsRUFBRTtvQkFDakUsTUFBTSxFQUFFLEtBQUssQ0FBQyxNQUFNO29CQUNwQixTQUFTLEVBQUUsS0FBSyxDQUFDLFNBQVM7b0JBQzFCLFFBQVEsRUFBRSxLQUFLLENBQUMsUUFBUTtvQkFDeEIsS0FBSyxFQUFFLEdBQUcsWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUM7aUJBQ3hELENBQUMsQ0FBQztZQUNMLENBQUM7UUFDSCxDQUFDO0tBQ0YsQ0FBQztJQUVGLFlBQVksQ0FBQyxTQUFTLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDbkMsTUFBTSxDQUFDLElBQUksQ0FBQyx3Q0FBd0MsRUFBRTtRQUNwRCxJQUFJLEVBQUUsYUFBYSxDQUFDLElBQUk7UUFDeEIsSUFBSSxFQUFFLGFBQWEsQ0FBQyxJQUFJO0tBQ3pCLENBQUMsQ0FBQztBQUNMLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuaW1wb3J0IHsgZW50aXR5RXZlbnRzLCB0eXBlIEVudGl0eUV2ZW50LCB0eXBlIEVudGl0eUV2ZW50U3Vic2NyaWJlciB9IGZyb20gJy4vZW50aXR5LWV2ZW50cy5qcyc7XG5pbXBvcnQgeyBJbnRlcm5hbEh0dHBDbGllbnQgfSBmcm9tICcuL2h0dHAtY2xpZW50LmpzJztcbmltcG9ydCB7IGdldFNlcnZpY2VBdXRoSGVhZGVyIH0gZnJvbSAnLi4vbWlkZGxld2FyZS9hdXRoLmpzJztcbmltcG9ydCB7IHR5cGUgU2VydmljZUNvbmZpZyB9IGZyb20gJy4uL3R5cGVzL2NvbW1vbi5qcyc7XG5pbXBvcnQgeyBjcmVhdGVMb2dnZXIgfSBmcm9tICcuLi91dGlscy9sb2dnZXIuanMnO1xuaW1wb3J0IHsgZW1pdENvdW50ZXIgfSBmcm9tICcuLi91dGlscy9tZXRyaWMtZW1pdHRlci5qcyc7XG5cbmNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcignY29tcGxpYW5jZS1ldmVudHMnKTtcblxuLyoqXG4gKiBSZWdpc3RlcnMgYW4gZW50aXR5IGV2ZW50IHN1YnNjcmliZXIgdGhhdCBmb3J3YXJkcyBldmVudHMgdG8gdGhlIGNvbXBsaWFuY2Ugc2VydmljZS5cbiAqXG4gKiBDYWxsIHRoaXMgYXQgc2VydmljZSBzdGFydHVwIChpbiBpbmRleC50cykgdG8gZW5hYmxlIGF1dG9tYXRpYyBjb21wbGlhbmNlXG4gKiBub3RpZmljYXRpb24gb24gZW50aXR5IG11dGF0aW9ucy4gRXZlbnRzIGFyZSBmaXJlLWFuZC1mb3JnZXQg4oCUIGZhaWx1cmVzXG4gKiBhcmUgbG9nZ2VkIGJ1dCBuZXZlciBibG9jayB0aGUgb3JpZ2luYWwgcmVxdWVzdC5cbiAqXG4gKiBAcGFyYW0gY29uZmlnIC0gT3B0aW9uYWwgc2VydmljZSBjb25maWcgb3ZlcnJpZGUgKGRlZmF1bHRzIHRvIENPTVBMSUFOQ0VfU0VSVklDRV9IT1NUL1BPUlQgZW52IHZhcnMpXG4gKiBAcGFyYW0gc2VydmljZU5hbWUgLSBTZXJ2aWNlIGlkZW50aWZpZXIgYmFrZWQgaW50byB0aGUgc2lnbmVkIEpXVCdzIGBzdWJgXG4gKiAgIChlLmcuICdwaXBlbGluZScsICdwbHVnaW4nKS4gVGhlIGNvbXBsaWFuY2Ugcm91dGUgcmVxdWlyZXMgYSBzZXJ2aWNlXG4gKiAgIHByaW5jaXBhbCDigJQgYSBzcG9vZmFibGUgYHgtaW50ZXJuYWwtc2VydmljZTogdHJ1ZWAgaGVhZGVyIGlzIG5vIGxvbmdlclxuICogICBhY2NlcHRlZC4gRGVmYXVsdHMgdG8gYSBnZW5lcmljICdlbnRpdHktZXZlbnRzJyB3aGVuIHVuc3BlY2lmaWVkLCBidXRcbiAqICAgY2FsbGVycyBzaG91bGQgcGFzcyB0aGVpciBzZXJ2aWNlIG5hbWUgZm9yIGFjY3VyYXRlIGF1ZGl0IHRyYWlscy5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlZ2lzdGVyQ29tcGxpYW5jZUV2ZW50U3Vic2NyaWJlcihcbiAgY29uZmlnPzogUGFydGlhbDxTZXJ2aWNlQ29uZmlnPixcbiAgc2VydmljZU5hbWU6IHN0cmluZyA9ICdlbnRpdHktZXZlbnRzJyxcbik6IHZvaWQge1xuICBjb25zdCBzZXJ2aWNlQ29uZmlnOiBTZXJ2aWNlQ29uZmlnID0ge1xuICAgIGhvc3Q6IGNvbmZpZz8uaG9zdCA/PyBwcm9jZXNzLmVudi5DT01QTElBTkNFX1NFUlZJQ0VfSE9TVCA/PyAnY29tcGxpYW5jZScsXG4gICAgcG9ydDogY29uZmlnPy5wb3J0ID8/IHBhcnNlSW50KHByb2Nlc3MuZW52LkNPTVBMSUFOQ0VfU0VSVklDRV9QT1JUID8/ICczMDAwJywgMTApLFxuICB9O1xuXG4gIGNvbnN0IGNsaWVudCA9IG5ldyBJbnRlcm5hbEh0dHBDbGllbnQoc2VydmljZUNvbmZpZyk7XG5cbiAgY29uc3Qgc3Vic2NyaWJlcjogRW50aXR5RXZlbnRTdWJzY3JpYmVyID0ge1xuICAgIGFzeW5jIG9uRW50aXR5RXZlbnQoZXZlbnQ6IEVudGl0eUV2ZW50KTogUHJvbWlzZTx2b2lkPiB7XG4gICAgICB0cnkge1xuICAgICAgICAvLyBNaW50IGEgcGVyLWV2ZW50IHNlcnZpY2UgSldUIHNjb3BlZCB0byB0aGUgZXZlbnQncyBvcmcgc28gdGhlXG4gICAgICAgIC8vIGNvbXBsaWFuY2Ugcm91dGUncyBgcnVuV2l0aFRlbmFudENvbnRleHRgIHNlZXMgdGhlIHJpZ2h0IHRlbmFudFxuICAgICAgICAvLyBHVUMuIFRoZSBjb21wbGlhbmNlIHNpZGUgZW5mb3JjZXMgYHJlcXVpcmVBdXRoYCArXG4gICAgICAgIC8vIGByZXF1aXJlU2VydmljZVByaW5jaXBhbGAg4oCUIHRoZSBwcmV2aW91cyBgeC1pbnRlcm5hbC1zZXJ2aWNlYFxuICAgICAgICAvLyBoZWFkZXIgaXMgbm8gbG9uZ2VyIHN1ZmZpY2llbnQgKGFuZCB3YXMgc3Bvb2ZhYmxlKS5cbiAgICAgICAgLy9cbiAgICAgICAgLy8gVGhpcyBIVFRQIG5vdGlmeSBpcyB0aGUgU0lOR0xFIGRlbGl2ZXJ5IGNoYW5uZWwgZm9yIHBvc3QtbXV0YXRpb25cbiAgICAgICAgLy8gcmUtdmFsaWRhdGlvbiAocHJpbWFyeSBlbmZvcmNlbWVudCBpcyB0aGUgZmFpbC1DTE9TRUQgbGl2ZVxuICAgICAgICAvLyB2YWxpZGF0ZSBwYXRoKS4gQSBzdGFibGUgcGVyLWV2ZW50IGBJZGVtcG90ZW5jeS1LZXlgIG1ha2VzIHRoZSBQT1NUXG4gICAgICAgIC8vIHJldHJ5LXNhZmUgaW4gdGhlIGh0dHAtY2xpZW50LCBzbyBhIHRyYW5zaWVudCA1eHgvdGltZW91dCBpcyByZXRyaWVkXG4gICAgICAgIC8vIHJhdGhlciB0aGFuIGRyb3BwZWQgb24gdGhlIGZpcnN0IGF0dGVtcHQ7IGNvbXBsaWFuY2UgcmUtZXZhbHVhdGlvbiBpc1xuICAgICAgICAvLyBuYXR1cmFsbHkgaWRlbXBvdGVudCwgc28gYSBkdXBsaWNhdGUgZGVsaXZlcnkgaXMgaGFybWxlc3MuXG4gICAgICAgIGF3YWl0IGNsaWVudC5wb3N0KCcvY29tcGxpYW5jZS9ldmVudHMvZW50aXR5JywgZXZlbnQsIHtcbiAgICAgICAgICBoZWFkZXJzOiB7XG4gICAgICAgICAgICAnQXV0aG9yaXphdGlvbic6IGdldFNlcnZpY2VBdXRoSGVhZGVyKHsgc2VydmljZU5hbWUsIG9yZ0lkOiBldmVudC5vcmdJZCwgcm9sZTogJ21lbWJlcicgfSksXG4gICAgICAgICAgICAnSWRlbXBvdGVuY3ktS2V5JzogYCR7ZXZlbnQudGFyZ2V0fToke2V2ZW50LmVudGl0eUlkfToke2V2ZW50LmV2ZW50VHlwZX06JHtldmVudC50aW1lc3RhbXAudG9JU09TdHJpbmcoKX1gLFxuICAgICAgICAgIH0sXG4gICAgICAgIH0pO1xuICAgICAgfSBjYXRjaCAoZXJyKSB7XG4gICAgICAgIC8vIEZpcmUtYW5kLWZvcmdldDogbG9nICsgYSBkcm9wIG1ldHJpYyBzbyBzdXN0YWluZWQgbG9zcyBpcyBhbGVydGFibGVcbiAgICAgICAgLy8gKG9wZXJhdG9ycyBjYW4ndCBhY3Qgb24gYSB3YXJuIGxpbmUgYWxvbmUpLCB0aGVuIHN3YWxsb3cg4oCUIGNvbXBsaWFuY2VcbiAgICAgICAgLy8gbm90aWZpY2F0aW9uIGlzIG5vbi1mYXRhbCB0byB0aGUgb3JpZ2luYXRpbmcgbXV0YXRpb24uXG4gICAgICAgIGVtaXRDb3VudGVyKCdjb21wbGlhbmNlX2V2ZW50X2Ryb3BfdG90YWwnLCB7IHNlcnZpY2U6IHNlcnZpY2VOYW1lLCB0YXJnZXQ6IGV2ZW50LnRhcmdldCB9KTtcbiAgICAgICAgbG9nZ2VyLndhcm4oJ0ZhaWxlZCB0byBub3RpZnkgY29tcGxpYW5jZSBzZXJ2aWNlIG9mIGVudGl0eSBldmVudCcsIHtcbiAgICAgICAgICB0YXJnZXQ6IGV2ZW50LnRhcmdldCxcbiAgICAgICAgICBldmVudFR5cGU6IGV2ZW50LmV2ZW50VHlwZSxcbiAgICAgICAgICBlbnRpdHlJZDogZXZlbnQuZW50aXR5SWQsXG4gICAgICAgICAgZXJyb3I6IGVyciBpbnN0YW5jZW9mIEVycm9yID8gZXJyLm1lc3NhZ2UgOiBTdHJpbmcoZXJyKSxcbiAgICAgICAgfSk7XG4gICAgICB9XG4gICAgfSxcbiAgfTtcblxuICBlbnRpdHlFdmVudHMuc3Vic2NyaWJlKHN1YnNjcmliZXIpO1xuICBsb2dnZXIuaW5mbygnQ29tcGxpYW5jZSBldmVudCBzdWJzY3JpYmVyIHJlZ2lzdGVyZWQnLCB7XG4gICAgaG9zdDogc2VydmljZUNvbmZpZy5ob3N0LFxuICAgIHBvcnQ6IHNlcnZpY2VDb25maWcucG9ydCxcbiAgfSk7XG59XG4iXX0=
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Durable, at-least-once event backbone (Redis Streams).
3
+ *
4
+ * The platform's cross-service signalling today is point-to-point HTTP plus
5
+ * polling reconcilers plus an IN-PROCESS best-effort emitter ({@link entityEvents}).
6
+ * There is no durable bus, so a signal produced on one pod is either delivered
7
+ * synchronously over HTTP (coupling + retry-storm risk) or lost if the consumer
8
+ * is momentarily down (the reconcilers exist to paper over exactly that).
9
+ *
10
+ * This is that missing bus, as a reusable primitive:
11
+ * - **publish(topic, payload)** → `XADD` onto a bounded (`MAXLEN ~`) per-topic
12
+ * stream. Durable across restarts, shared by all replicas.
13
+ * - **subscribe({topic, group, consumer, handler})** → a `XREADGROUP` consumer.
14
+ * Consumer GROUPS give at-least-once: each group gets every message once, a
15
+ * message stays pending until the handler `XACK`s it, and `XAUTOCLAIM`
16
+ * reclaims messages stranded by a crashed consumer. Multiple groups on one
17
+ * topic = independent fan-out (e.g. quota-sync AND audit both react).
18
+ *
19
+ * FAIL-SAFE + OPT-IN, exactly like the audit spool: {@link createEnvRedisDurableEventBus}
20
+ * returns `null` when Redis isn't configured, so a service without it simply
21
+ * doesn't use the bus (keeps today's behavior). A publish failure is dropped-with-
22
+ * -metric, never thrown; a handler throw leaves the message un-acked for redelivery.
23
+ *
24
+ * Migrating a specific reconciler (e.g. billing→quota entitlement sync) onto this
25
+ * is deliberately follow-on work — this lands the backbone + an additive entity-
26
+ * event bridge; the reconcilers stay as the belt-and-suspenders backstop until a
27
+ * flow is fully cut over.
28
+ */
29
+ import type { EntityEventSubscriber } from './entity-events.js';
30
+ /** A delivered event: its stream id, topic, and decoded payload. */
31
+ export interface EventEnvelope<T = unknown> {
32
+ /** Redis Stream entry id (`<ms>-<seq>`) — also the idempotency handle. */
33
+ id: string;
34
+ topic: string;
35
+ payload: T;
36
+ }
37
+ export interface SubscribeOptions<T = unknown> {
38
+ /** Topic (stream) to consume. */
39
+ topic: string;
40
+ /** Consumer group — one delivery per group; distinct groups fan out. */
41
+ group: string;
42
+ /** This consumer's id within the group (e.g. `${service}-${pid}`). */
43
+ consumer: string;
44
+ /** Async handler. Resolves ⇒ the message is acked; throws ⇒ left pending for redelivery. */
45
+ handler: (env: EventEnvelope<T>) => Promise<void>;
46
+ /** Max messages per read (default 16). */
47
+ batchSize?: number;
48
+ /** XREADGROUP block time in ms (default 5000). */
49
+ blockMs?: number;
50
+ /** A pending message idle this long (ms) is reclaimed from a dead consumer (default 60000). */
51
+ minIdleMs?: number;
52
+ }
53
+ export interface EventSubscription {
54
+ /** Stop the consumer loop. Resolves once the in-flight read returns. */
55
+ stop(): Promise<void>;
56
+ }
57
+ export interface DurableEventBus {
58
+ /** Publish a payload to a topic. Returns the stream id, or null on failure (dropped, never throws). */
59
+ publish<T>(topic: string, payload: T): Promise<string | null>;
60
+ /** Start a consumer-group reader for a topic. Fire-and-forget loop; call `stop()` to end it. */
61
+ subscribe<T>(opts: SubscribeOptions<T>): EventSubscription;
62
+ }
63
+ /**
64
+ * The subset of ioredis' Stream API the bus uses. Kept minimal so a test can
65
+ * supply an in-memory fake and so merely importing this never hard-depends on
66
+ * ioredis (constructed via the shared env helper).
67
+ */
68
+ export interface RedisStreamClient {
69
+ xadd(key: string, ...args: (string | number)[]): Promise<string | null>;
70
+ xgroup(...args: (string | number)[]): Promise<unknown>;
71
+ xreadgroup(...args: (string | number)[]): Promise<unknown>;
72
+ xack(key: string, group: string, ...ids: string[]): Promise<number>;
73
+ xautoclaim(...args: (string | number)[]): Promise<unknown>;
74
+ }
75
+ /** Build a durable event bus over a Redis Streams client. */
76
+ export declare function createRedisDurableEventBus(redis: RedisStreamClient, opts?: {
77
+ maxLen?: number;
78
+ }): DurableEventBus;
79
+ /**
80
+ * Build a durable event bus from the ambient Redis env via the shared
81
+ * `createEnvRedisClient`. Returns `null` when Redis is not configured — the
82
+ * caller then runs without a bus (today's HTTP/reconciler behavior), never
83
+ * crashing. Opt in with a single boot line where a service wants durable events.
84
+ */
85
+ export declare function createEnvRedisDurableEventBus(opts?: {
86
+ maxLen?: number;
87
+ }): DurableEventBus | null;
88
+ /** Topic the entity-event bridge publishes to. */
89
+ export declare const ENTITY_EVENT_TOPIC = "entity-events";
90
+ /**
91
+ * An {@link EntityEventSubscriber} that forwards each entity event to the durable
92
+ * bus. `bus.publish` is fail-safe (drops-with-metric, never throws), matching the
93
+ * emitter's fire-and-forget contract. Note `EntityEvent.timestamp` (a Date)
94
+ * serializes to an ISO string on the wire — a consumer re-hydrates as needed.
95
+ */
96
+ export declare function createEntityEventBusPublisher(bus: DurableEventBus): EntityEventSubscriber;