@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.
- package/LICENSE +202 -0
- package/README.md +98 -0
- package/lib/constants/ai-providers.d.ts +52 -0
- package/lib/constants/ai-providers.js +129 -0
- package/lib/constants/http-status.d.ts +20 -0
- package/lib/constants/http-status.js +26 -0
- package/lib/constants/index.d.ts +3 -0
- package/lib/constants/index.js +6 -0
- package/lib/constants/time.d.ts +10 -0
- package/lib/constants/time.js +13 -0
- package/lib/errors/app-errors.d.ts +30 -0
- package/lib/errors/app-errors.js +53 -0
- package/lib/errors/index.d.ts +1 -0
- package/lib/errors/index.js +4 -0
- package/lib/helpers/access-helpers.d.ts +63 -0
- package/lib/helpers/access-helpers.js +82 -0
- package/lib/helpers/crud-helpers.d.ts +9 -0
- package/lib/helpers/crud-helpers.js +23 -0
- package/lib/helpers/index.d.ts +6 -0
- package/lib/helpers/index.js +9 -0
- package/lib/helpers/org-hierarchy-http.d.ts +74 -0
- package/lib/helpers/org-hierarchy-http.js +144 -0
- package/lib/helpers/org-hierarchy.d.ts +57 -0
- package/lib/helpers/org-hierarchy.js +110 -0
- package/lib/helpers/restore-helpers.d.ts +49 -0
- package/lib/helpers/restore-helpers.js +78 -0
- package/lib/helpers/sse-helpers.d.ts +20 -0
- package/lib/helpers/sse-helpers.js +45 -0
- package/lib/index.d.ts +58 -0
- package/lib/index.js +71 -0
- package/lib/middleware/auth.d.ts +208 -0
- package/lib/middleware/auth.js +588 -0
- package/lib/middleware/index.d.ts +3 -0
- package/lib/middleware/index.js +6 -0
- package/lib/middleware/mongo-sanitize.d.ts +32 -0
- package/lib/middleware/mongo-sanitize.js +100 -0
- package/lib/middleware/step-up.d.ts +45 -0
- package/lib/middleware/step-up.js +138 -0
- package/lib/openapi/extend-zod.d.ts +1 -0
- package/lib/openapi/extend-zod.js +6 -0
- package/lib/openapi/index.d.ts +2 -0
- package/lib/openapi/index.js +5 -0
- package/lib/openapi/registry.d.ts +17 -0
- package/lib/openapi/registry.js +38 -0
- package/lib/openapi/routes/ask-routes.d.ts +7 -0
- package/lib/openapi/routes/ask-routes.js +62 -0
- package/lib/openapi/routes/billing-routes.d.ts +1 -0
- package/lib/openapi/routes/billing-routes.js +66 -0
- package/lib/openapi/routes/image-registry-routes.d.ts +7 -0
- package/lib/openapi/routes/image-registry-routes.js +98 -0
- package/lib/openapi/routes/index.d.ts +5 -0
- package/lib/openapi/routes/index.js +28 -0
- package/lib/openapi/routes/message-routes.d.ts +1 -0
- package/lib/openapi/routes/message-routes.js +105 -0
- package/lib/openapi/routes/pipeline-routes.d.ts +1 -0
- package/lib/openapi/routes/pipeline-routes.js +105 -0
- package/lib/openapi/routes/pipeline-template-routes.d.ts +1 -0
- package/lib/openapi/routes/pipeline-template-routes.js +61 -0
- package/lib/openapi/routes/plugin-routes.d.ts +1 -0
- package/lib/openapi/routes/plugin-routes.js +96 -0
- package/lib/openapi/routes/quota-routes.d.ts +1 -0
- package/lib/openapi/routes/quota-routes.js +62 -0
- package/lib/openapi/schema-registry.d.ts +25 -0
- package/lib/openapi/schema-registry.js +91 -0
- package/lib/routes/health.d.ts +41 -0
- package/lib/routes/health.js +82 -0
- package/lib/routes/index.d.ts +1 -0
- package/lib/routes/index.js +4 -0
- package/lib/services/audit-spool.d.ts +80 -0
- package/lib/services/audit-spool.js +172 -0
- package/lib/services/cache-service.d.ts +124 -0
- package/lib/services/cache-service.js +276 -0
- package/lib/services/circuit-breaker.d.ts +45 -0
- package/lib/services/circuit-breaker.js +138 -0
- package/lib/services/compliance-client.d.ts +46 -0
- package/lib/services/compliance-client.js +128 -0
- package/lib/services/compliance-event-subscriber.d.ts +16 -0
- package/lib/services/compliance-event-subscriber.js +72 -0
- package/lib/services/durable-event-bus.d.ts +96 -0
- package/lib/services/durable-event-bus.js +204 -0
- package/lib/services/entity-events.d.ts +48 -0
- package/lib/services/entity-events.js +79 -0
- package/lib/services/env-redis.d.ts +44 -0
- package/lib/services/env-redis.js +94 -0
- package/lib/services/http-client.d.ts +143 -0
- package/lib/services/http-client.js +335 -0
- package/lib/services/index.d.ts +18 -0
- package/lib/services/index.js +20 -0
- package/lib/services/leader-lock.d.ts +45 -0
- package/lib/services/leader-lock.js +85 -0
- package/lib/services/quota.d.ts +130 -0
- package/lib/services/quota.js +264 -0
- package/lib/services/remote-audit-client.d.ts +160 -0
- package/lib/services/remote-audit-client.js +390 -0
- package/lib/services/retry-strategy.d.ts +74 -0
- package/lib/services/retry-strategy.js +118 -0
- package/lib/services/scheduler.d.ts +41 -0
- package/lib/services/scheduler.js +86 -0
- package/lib/services/service-boot.d.ts +20 -0
- package/lib/services/service-boot.js +28 -0
- package/lib/services/sse-ticket-store.d.ts +51 -0
- package/lib/services/sse-ticket-store.js +130 -0
- package/lib/services/token-revocation.d.ts +52 -0
- package/lib/services/token-revocation.js +120 -0
- package/lib/testing/tier-mock.d.ts +44 -0
- package/lib/testing/tier-mock.js +65 -0
- package/lib/types/audit-events.d.ts +69 -0
- package/lib/types/audit-events.js +4 -0
- package/lib/types/catalog-metadata.d.ts +36 -0
- package/lib/types/catalog-metadata.js +21 -0
- package/lib/types/common.d.ts +255 -0
- package/lib/types/common.js +41 -0
- package/lib/types/error-codes.d.ts +54 -0
- package/lib/types/error-codes.js +107 -0
- package/lib/types/feature-flags.d.ts +36 -0
- package/lib/types/feature-flags.js +128 -0
- package/lib/types/http.d.ts +27 -0
- package/lib/types/http.js +4 -0
- package/lib/types/index.d.ts +11 -0
- package/lib/types/index.js +14 -0
- package/lib/types/permissions.d.ts +65 -0
- package/lib/types/permissions.js +131 -0
- package/lib/types/pipeline-template.d.ts +30 -0
- package/lib/types/pipeline-template.js +13 -0
- package/lib/types/pipeline.d.ts +59 -0
- package/lib/types/pipeline.js +31 -0
- package/lib/types/quota-tiers.d.ts +79 -0
- package/lib/types/quota-tiers.js +221 -0
- package/lib/types/visibility.d.ts +20 -0
- package/lib/types/visibility.js +22 -0
- package/lib/utils/alias-resolver.d.ts +37 -0
- package/lib/utils/alias-resolver.js +68 -0
- package/lib/utils/audit.d.ts +15 -0
- package/lib/utils/audit.js +39 -0
- package/lib/utils/aws-scrub.d.ts +11 -0
- package/lib/utils/aws-scrub.js +67 -0
- package/lib/utils/compliance-attributes.d.ts +6 -0
- package/lib/utils/compliance-attributes.js +69 -0
- package/lib/utils/concurrency.d.ts +9 -0
- package/lib/utils/concurrency.js +29 -0
- package/lib/utils/crash-handlers.d.ts +18 -0
- package/lib/utils/crash-handlers.js +36 -0
- package/lib/utils/env.d.ts +28 -0
- package/lib/utils/env.js +54 -0
- package/lib/utils/headers.d.ts +18 -0
- package/lib/utils/headers.js +21 -0
- package/lib/utils/identity.d.ts +40 -0
- package/lib/utils/identity.js +51 -0
- package/lib/utils/index.d.ts +17 -0
- package/lib/utils/index.js +20 -0
- package/lib/utils/logger.d.ts +38 -0
- package/lib/utils/logger.js +174 -0
- package/lib/utils/metric-emitter.d.ts +36 -0
- package/lib/utils/metric-emitter.js +32 -0
- package/lib/utils/object.d.ts +13 -0
- package/lib/utils/object.js +18 -0
- package/lib/utils/params.d.ts +150 -0
- package/lib/utils/params.js +206 -0
- package/lib/utils/response.d.ts +117 -0
- package/lib/utils/response.js +223 -0
- package/lib/utils/safe-require.d.ts +13 -0
- package/lib/utils/safe-require.js +19 -0
- package/lib/utils/secret-encryption.d.ts +176 -0
- package/lib/utils/secret-encryption.js +374 -0
- package/lib/utils/ssrf.d.ts +30 -0
- package/lib/utils/ssrf.js +106 -0
- package/lib/validation/ai-schemas.d.ts +56 -0
- package/lib/validation/ai-schemas.js +78 -0
- package/lib/validation/common-schemas.d.ts +133 -0
- package/lib/validation/common-schemas.js +94 -0
- package/lib/validation/index.d.ts +7 -0
- package/lib/validation/index.js +10 -0
- package/lib/validation/message-schemas.d.ts +160 -0
- package/lib/validation/message-schemas.js +156 -0
- package/lib/validation/middleware.d.ts +72 -0
- package/lib/validation/middleware.js +65 -0
- package/lib/validation/pipeline-schemas.d.ts +181 -0
- package/lib/validation/pipeline-schemas.js +101 -0
- package/lib/validation/pipeline-template-schemas.d.ts +155 -0
- package/lib/validation/pipeline-template-schemas.js +66 -0
- package/lib/validation/plugin-schemas.d.ts +171 -0
- package/lib/validation/plugin-schemas.js +81 -0
- package/openapi.yaml +292 -0
- 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tcGxpYW5jZS1jbGllbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvc2VydmljZXMvY29tcGxpYW5jZS1jbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQUV0QyxPQUFPLEVBQUUsa0JBQWtCLEVBQXVCLE1BQU0sa0JBQWtCLENBQUM7QUFFM0UsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRWxELE1BQU0sTUFBTSxHQUFHLFlBQVksQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBRWpEOzs7R0FHRztBQUNILE1BQU0saUJBQWlCLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsS0FBSyxNQUFNLENBQUM7QUFFbkU7Ozs7O0dBS0c7QUFDSCxNQUFNLG1CQUFtQixHQUFHLFFBQVEsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLDhCQUE4QixJQUFJLE1BQU0sRUFBRSxFQUFFLENBQUMsQ0FBQztBQW1FL0Y7OztHQUdHO0FBQ0gsU0FBUyxZQUFZLENBQUMsT0FBZ0M7SUFDcEQsTUFBTSxDQUFDLElBQUksQ0FBQywwREFBMEQsRUFBRSxPQUFPLENBQUMsQ0FBQztJQUNqRixPQUFPO1FBQ0wsTUFBTSxFQUFFLElBQUk7UUFDWixPQUFPLEVBQUUsS0FBSztRQUNkLFVBQVUsRUFBRSxFQUFFO1FBQ2QsUUFBUSxFQUFFLENBQUMsRUFBRSxNQUFNLEVBQUUsRUFBRSxFQUFFLFFBQVEsRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLEVBQUUsRUFBRSxRQUFRLEVBQUUsRUFBRSxFQUFFLGFBQWEsRUFBRSxJQUFJLEVBQUUsV0FBVyxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsU0FBUyxFQUFFLE9BQU8sRUFBRSxnREFBZ0QsRUFBRSxDQUFDO1FBQ3pMLGNBQWMsRUFBRSxDQUFDO1FBQ2pCLFlBQVksRUFBRSxDQUFDO1FBQ2YsaUJBQWlCLEVBQUUsRUFBRTtL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RUFBRSxNQUFNLElBQUksUUFBUSxFQUFFLEVBQUUsZ0JBQWdCLEVBQ2xGLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxRQUFRLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUM7UUFDM0QsQ0FBQztRQUVELGdCQUFnQixDQUFDLEtBQUssRUFBRSxVQUFVLEVBQUUsVUFBVSxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQUUsTUFBTTtZQUMxRSxPQUFPLGFBQWEsQ0FBQyxLQUFLLEVBQUUsVUFBVSxFQUFFLCtCQUErQixFQUNyRSxFQUFFLFVBQVUsRUFBRSxRQUFRLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxNQUFNLElBQUksUUFBUSxFQUFFLEVBQUUsa0JBQWtCLEVBQ3BGLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxVQUFVLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUM7UUFDN0QsQ0FBQztRQUVELFlBQVksQ0FBQyxLQUFLLEVBQUUsVUFBVSxFQUFFLFVBQVU7WUFDeEMsT0FBTyxhQUFhLENBQUMsS0FBSyxFQUFFLFVBQVUsRUFBRSxxQ0FBcUMsRUFDM0UsRUFBRSxVQUFVLEVBQUUsRUFBRSxjQUFjLEVBQUUsRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLFFBQVEsRUFBRSxNQUFNLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztRQUNuRixDQUFDO1FBRUQsY0FBYyxDQUFDLEtBQUssRUFBRSxVQUFVLEVBQUUsVUFBVTtZQUMxQyxPQUFPLGFBQWEsQ0FBQyxLQUFLLEVBQUUsVUFBVSxFQUFFLHVDQUF1QyxFQUM3RSxFQUFFLFVBQVUsRUFBRSxFQUFFLGdCQUFnQixFQUFFLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxVQUFVLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDdkYsQ0FBQztLQUNGLENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IEludGVybmFsSHR0cENsaWVudCwgdHlwZSBSZXF1ZXN0T3B0aW9ucyB9IGZyb20gJy4vaHR0cC1jbGllbnQuanMnO1xuaW1wb3J0IHR5cGUgeyBTZXJ2aWNlQ29uZmlnIH0gZnJvbSAnLi4vdHlwZXMvY29tbW9uLmpzJztcbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciB9IGZyb20gJy4uL3V0aWxzL2xvZ2dlci5qcyc7XG5cbmNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcignY29tcGxpYW5jZS1jbGllbnQnKTtcblxuLyoqXG4gKiBXaGVuIHRydWUsIGNvbXBsaWFuY2UgY2hlY2tzIGFyZSBieXBhc3NlZCBpZiB0aGUgc2VydmljZSBpcyB1bmF2YWlsYWJsZVxuICogKGZhaWwtb3BlbikuIERlZmF1bHQgaXMgZmFpbC1jbG9zZWQgKGVycm9ycyBwcm9wYWdhdGUgYW5kIGJsb2NrIHRoZSBvcGVyYXRpb24pLlxuICovXG5jb25zdCBDT01QTElBTkNFX0JZUEFTUyA9IHByb2Nlc3MuZW52LkNPTVBMSUFOQ0VfQllQQVNTID09PSAndHJ1ZSc7XG5cbi8qKlxuICogUGVyLWF0dGVtcHQgdGltZW91dCBmb3IgYSB2YWxpZGF0aW9uIFBPU1QgKGVudjogYENPTVBMSUFOQ0VfVkFMSURBVEVfVElNRU9VVF9NU2ApLlxuICogRGVsaWJlcmF0ZWx5IFRJR0hURVIgdGhhbiB0aGUgNXMgY2xpZW50IGRlZmF1bHQgc28gdGhlIGVuZm9yY2VkIHJldHJpZXMgKGJlbG93KVxuICogZml0IGluc2lkZSBhIGJvdW5kZWQgdG90YWwgYnVkZ2V0IHJhdGhlciB0aGFuIHN0YWNraW5nIHRocmVlIDVzIHN0YWxscyBvbnRvIGFcbiAqIHNpbmdsZSBpbnRlcmFjdGl2ZSByZXF1ZXN0LlxuICovXG5jb25zdCBWQUxJREFURV9USU1FT1VUX01TID0gcGFyc2VJbnQocHJvY2Vzcy5lbnYuQ09NUExJQU5DRV9WQUxJREFURV9USU1FT1VUX01TID8/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;
|