@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,74 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
/**
|
|
3
|
+
* Default retry configuration (env: `HTTP_CLIENT_MAX_RETRIES`, `HTTP_CLIENT_RETRY_DELAY_MS`).
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_MAX_RETRIES: number;
|
|
6
|
+
export declare const DEFAULT_RETRY_DELAY_MS: number;
|
|
7
|
+
export declare const DEFAULT_MAX_RATE_LIMIT_RETRIES: number;
|
|
8
|
+
/**
|
|
9
|
+
* Configuration for retry behavior.
|
|
10
|
+
*/
|
|
11
|
+
export interface RetryConfig {
|
|
12
|
+
/** Maximum retry attempts for transient failures (default: 2) */
|
|
13
|
+
maxRetries: number;
|
|
14
|
+
/** Base delay between retries in ms — doubles each attempt (default: 200) */
|
|
15
|
+
retryDelayMs: number;
|
|
16
|
+
/** Maximum retry attempts specifically for 429 rate limiting (default: 4) */
|
|
17
|
+
maxRateLimitRetries: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Result of a retry decision: whether to retry, and how long to wait.
|
|
21
|
+
*/
|
|
22
|
+
export interface RetryDecision {
|
|
23
|
+
/** Whether the request should be retried */
|
|
24
|
+
shouldRetry: boolean;
|
|
25
|
+
/** Delay in milliseconds before retrying (with jitter applied) */
|
|
26
|
+
delayMs: number;
|
|
27
|
+
/** Human-readable reason for the retry decision */
|
|
28
|
+
reason: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse a `Retry-After` header value into milliseconds.
|
|
32
|
+
* Supports numeric seconds (e.g. "5") and HTTP-date format.
|
|
33
|
+
* Returns `undefined` for missing or invalid values.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseRetryAfter(header: string | string[] | undefined): number | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Apply +/-25% random jitter to a delay to prevent thundering herd.
|
|
38
|
+
*/
|
|
39
|
+
export declare function addJitter(delay: number): number;
|
|
40
|
+
/**
|
|
41
|
+
* Calculate exponential backoff delay for a given attempt.
|
|
42
|
+
*
|
|
43
|
+
* @param baseDelay - Base delay in milliseconds
|
|
44
|
+
* @param attempt - Zero-based attempt number
|
|
45
|
+
* @returns Delay in milliseconds (without jitter)
|
|
46
|
+
*/
|
|
47
|
+
export declare function calculateBackoff(baseDelay: number, attempt: number): number;
|
|
48
|
+
/**
|
|
49
|
+
* Determine whether a status code represents a transient server error
|
|
50
|
+
* that is eligible for retry (502, 503, 504).
|
|
51
|
+
*/
|
|
52
|
+
export declare function isTransientStatusCode(statusCode: number): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Determine whether a status code indicates rate limiting (429).
|
|
55
|
+
*/
|
|
56
|
+
export declare function isRateLimited(statusCode: number): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Determine whether a failed response should be retried, and compute the delay.
|
|
59
|
+
*
|
|
60
|
+
* @param statusCode - HTTP status code of the response
|
|
61
|
+
* @param headers - Response headers (used to read Retry-After for 429)
|
|
62
|
+
* @param attempt - Zero-based attempt number
|
|
63
|
+
* @param config - Retry configuration
|
|
64
|
+
* @returns A RetryDecision indicating whether to retry and the delay
|
|
65
|
+
*/
|
|
66
|
+
export declare function getRetryDecision(statusCode: number, headers: http.IncomingHttpHeaders, attempt: number, config: RetryConfig): RetryDecision;
|
|
67
|
+
/**
|
|
68
|
+
* Determine whether a connection/timeout error should be retried, and compute the delay.
|
|
69
|
+
*
|
|
70
|
+
* @param attempt - Zero-based attempt number
|
|
71
|
+
* @param config - Retry configuration
|
|
72
|
+
* @returns A RetryDecision indicating whether to retry and the delay
|
|
73
|
+
*/
|
|
74
|
+
export declare function getErrorRetryDecision(attempt: number, config: RetryConfig): RetryDecision;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import * as http from 'http';
|
|
4
|
+
/**
|
|
5
|
+
* Default retry configuration (env: `HTTP_CLIENT_MAX_RETRIES`, `HTTP_CLIENT_RETRY_DELAY_MS`).
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_MAX_RETRIES = parseInt(process.env.HTTP_CLIENT_MAX_RETRIES || '2', 10);
|
|
8
|
+
export const DEFAULT_RETRY_DELAY_MS = parseInt(process.env.HTTP_CLIENT_RETRY_DELAY_MS || '200', 10);
|
|
9
|
+
export const DEFAULT_MAX_RATE_LIMIT_RETRIES = parseInt(process.env.HTTP_CLIENT_MAX_RATE_LIMIT_RETRIES || '4', 10);
|
|
10
|
+
/** Max Retry-After value we'll honor (60 seconds). */
|
|
11
|
+
const MAX_RETRY_AFTER_MS = 60_000;
|
|
12
|
+
/** HTTP status codes considered transient server errors eligible for retry. */
|
|
13
|
+
const TRANSIENT_STATUS_CODES = [502, 503, 504];
|
|
14
|
+
/**
|
|
15
|
+
* Parse a `Retry-After` header value into milliseconds.
|
|
16
|
+
* Supports numeric seconds (e.g. "5") and HTTP-date format.
|
|
17
|
+
* Returns `undefined` for missing or invalid values.
|
|
18
|
+
*/
|
|
19
|
+
export function parseRetryAfter(header) {
|
|
20
|
+
if (!header)
|
|
21
|
+
return undefined;
|
|
22
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
23
|
+
if (!value)
|
|
24
|
+
return undefined;
|
|
25
|
+
// Try numeric seconds first
|
|
26
|
+
const seconds = Number(value);
|
|
27
|
+
if (!isNaN(seconds) && seconds >= 0) {
|
|
28
|
+
return Math.min(seconds * 1000, MAX_RETRY_AFTER_MS);
|
|
29
|
+
}
|
|
30
|
+
// Try HTTP-date
|
|
31
|
+
const date = Date.parse(value);
|
|
32
|
+
if (!isNaN(date)) {
|
|
33
|
+
const delayMs = date - Date.now();
|
|
34
|
+
if (delayMs > 0)
|
|
35
|
+
return Math.min(delayMs, MAX_RETRY_AFTER_MS);
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Apply +/-25% random jitter to a delay to prevent thundering herd.
|
|
41
|
+
*/
|
|
42
|
+
export function addJitter(delay) {
|
|
43
|
+
const jitter = delay * 0.25 * (2 * Math.random() - 1); // -25% to +25%
|
|
44
|
+
return Math.max(0, Math.round(delay + jitter));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Calculate exponential backoff delay for a given attempt.
|
|
48
|
+
*
|
|
49
|
+
* @param baseDelay - Base delay in milliseconds
|
|
50
|
+
* @param attempt - Zero-based attempt number
|
|
51
|
+
* @returns Delay in milliseconds (without jitter)
|
|
52
|
+
*/
|
|
53
|
+
export function calculateBackoff(baseDelay, attempt) {
|
|
54
|
+
return baseDelay * Math.pow(2, attempt);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Determine whether a status code represents a transient server error
|
|
58
|
+
* that is eligible for retry (502, 503, 504).
|
|
59
|
+
*/
|
|
60
|
+
export function isTransientStatusCode(statusCode) {
|
|
61
|
+
return TRANSIENT_STATUS_CODES.includes(statusCode);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Determine whether a status code indicates rate limiting (429).
|
|
65
|
+
*/
|
|
66
|
+
export function isRateLimited(statusCode) {
|
|
67
|
+
return statusCode === 429;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Determine whether a failed response should be retried, and compute the delay.
|
|
71
|
+
*
|
|
72
|
+
* @param statusCode - HTTP status code of the response
|
|
73
|
+
* @param headers - Response headers (used to read Retry-After for 429)
|
|
74
|
+
* @param attempt - Zero-based attempt number
|
|
75
|
+
* @param config - Retry configuration
|
|
76
|
+
* @returns A RetryDecision indicating whether to retry and the delay
|
|
77
|
+
*/
|
|
78
|
+
export function getRetryDecision(statusCode, headers, attempt, config) {
|
|
79
|
+
// 429 rate limiting — use Retry-After or longer backoff (4x base)
|
|
80
|
+
if (isRateLimited(statusCode) && attempt < config.maxRateLimitRetries) {
|
|
81
|
+
const retryAfter = parseRetryAfter(headers['retry-after']);
|
|
82
|
+
const rawDelay = retryAfter ?? (config.retryDelayMs * 4 * Math.pow(2, attempt));
|
|
83
|
+
return {
|
|
84
|
+
shouldRetry: true,
|
|
85
|
+
delayMs: addJitter(rawDelay),
|
|
86
|
+
reason: 'Rate limited (429)',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// 5xx transient server errors — standard exponential backoff
|
|
90
|
+
if (isTransientStatusCode(statusCode) && attempt < config.maxRetries) {
|
|
91
|
+
const rawDelay = calculateBackoff(config.retryDelayMs, attempt);
|
|
92
|
+
return {
|
|
93
|
+
shouldRetry: true,
|
|
94
|
+
delayMs: addJitter(rawDelay),
|
|
95
|
+
reason: `Transient server error (${statusCode})`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { shouldRetry: false, delayMs: 0, reason: 'Not retryable' };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Determine whether a connection/timeout error should be retried, and compute the delay.
|
|
102
|
+
*
|
|
103
|
+
* @param attempt - Zero-based attempt number
|
|
104
|
+
* @param config - Retry configuration
|
|
105
|
+
* @returns A RetryDecision indicating whether to retry and the delay
|
|
106
|
+
*/
|
|
107
|
+
export function getErrorRetryDecision(attempt, config) {
|
|
108
|
+
if (attempt < config.maxRetries) {
|
|
109
|
+
const rawDelay = calculateBackoff(config.retryDelayMs, attempt);
|
|
110
|
+
return {
|
|
111
|
+
shouldRetry: true,
|
|
112
|
+
delayMs: addJitter(rawDelay),
|
|
113
|
+
reason: 'Connection or timeout error',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { shouldRetry: false, delayMs: 0, reason: 'Max retries exceeded' };
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmV0cnktc3RyYXRlZ3kuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvc2VydmljZXMvcmV0cnktc3RyYXRlZ3kudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQUV0QyxPQUFPLEtBQUssSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUU3Qjs7R0FFRztBQUNILE1BQU0sQ0FBQyxNQUFNLG1CQUFtQixHQUFHLFFBQVEsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLHVCQUF1QixJQUFJLEdBQUcsRUFBRSxFQUFFLENBQUMsQ0FBQztBQUM1RixNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQywwQkFBMEIsSUFBSSxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUM7QUFDcEcsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUcsUUFBUSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsa0NBQWtDLElBQUksR0FBRyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBRWxILHNEQUFzRDtBQUN0RCxNQUFNLGtCQUFrQixHQUFHLE1BQU0sQ0FBQztBQUVsQywrRUFBK0U7QUFDL0UsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxDQUFDLENBQUM7QUEwQi9DOzs7O0dBSUc7QUFDSCxNQUFNLFVBQVUsZUFBZSxDQUFDLE1BQXFDO0lBQ25FLElBQUksQ0FBQyxNQUFNO1FBQUUsT0FBTyxTQUFTLENBQUM7SUFDOUIsTUFBTSxLQUFLLEdBQUcsS0FBSyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7SUFDekQsSUFBSSxDQUFDLEtBQUs7UUFBRSxPQUFPLFNBQVMsQ0FBQztJQUU3Qiw0QkFBNEI7SUFDNUIsTUFBTSxPQUFPLEdBQUcsTUFB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RSxPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxlQUFlLEVBQUUsQ0FBQztBQUNyRSxDQUFDO0FBRUQ7Ozs7OztHQU1HO0FBQ0gsTUFBTSxVQUFVLHFCQUFxQixDQUNuQyxPQUFlLEVBQ2YsTUFBbUI7SUFFbkIsSUFBSSxPQUFPLEdBQUcsTUFBTSxDQUFDLFVBQVUsRUFBRSxDQUFDO1FBQ2hDLE1BQU0sUUFBUSxHQUFHLGdCQUFnQixDQUFDLE1BQU0sQ0FBQyxZQUFZLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFDaEUsT0FBTztZQUNMLFdBQVcsRUFBRSxJQUFJO1lBQ2pCLE9BQU8sRUFBRSxTQUFTLENBQUMsUUFBUSxDQUFDO1lBQzVCLE1BQU0sRUFBRSw2QkFBNkI7U0FDdEMsQ0FBQztJQUNKLENBQUM7SUFFRCxPQUFPLEVBQUUsV0FBVyxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxzQkFBc0IsRUFBRSxDQUFDO0FBQzVFLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuaW1wb3J0ICogYXMgaHR0cCBmcm9tICdodHRwJztcblxuLyoqXG4gKiBEZWZhdWx0IHJldHJ5IGNvbmZpZ3VyYXRpb24gKGVudjogYEhUVFBfQ0xJRU5UX01BWF9SRVRSSUVTYCwgYEhUVFBfQ0xJRU5UX1JFVFJZX0RFTEFZX01TYCkuXG4gKi9cbmV4cG9ydCBjb25zdCBERUZBVUxUX01BWF9SRVRSSUVTID0gcGFyc2VJbnQocHJvY2Vzcy5lbnYuSFRUUF9DTElFTlRfTUFYX1JFVFJJRVMgfHwgJzInLCAxMCk7XG5leHBvcnQgY29uc3QgREVGQVVMVF9SRVRSWV9ERUxBWV9NUyA9IHBhcnNlSW50KHByb2Nlc3MuZW52LkhUVFBfQ0xJRU5UX1JFVFJZX0RFTEFZX01TIHx8ICcyMDAnLCAxMCk7XG5leHBvcnQgY29uc3QgREVGQVVMVF9NQVhfUkFURV9MSU1JVF9SRVRSSUVTID0gcGFyc2VJbnQocHJvY2Vzcy5lbnYuSFRUUF9DTElFTlRfTUFYX1JBVEVfTElNSVRfUkVUUklFUyB8fCAnNCcsIDEwKTtcblxuLyoqIE1heCBSZXRyeS1BZnRlciB2YWx1ZSB3ZSdsbCBob25vciAoNjAgc2Vjb25kcykuICovXG5jb25zdCBNQVhfUkVUUllfQUZURVJfTVMgPSA2MF8wMDA7XG5cbi8qKiBIVFRQIHN0YXR1cyBjb2RlcyBjb25zaWRlcmVkIHRyYW5zaWVudCBzZXJ2ZXIgZXJyb3JzIGVsaWdpYmxlIGZvciByZXRyeS4gKi9cbmNvbnN0IFRSQU5TSUVOVF9TVEFUVVNfQ09ERVMgPSBbNTAyLCA1MDMsIDUwNF07XG5cbi8qKlxuICogQ29uZmlndXJhdGlvbiBmb3IgcmV0cnkgYmVoYXZpb3IuXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUmV0cnlDb25maWcge1xuICAvKiogTWF4aW11bSByZXRyeSBhdHRlbXB0cyBmb3IgdHJhbnNpZW50IGZhaWx1cmVzIChkZWZhdWx0OiAyKSAqL1xuICBtYXhSZXRyaWVzOiBudW1iZXI7XG4gIC8qKiBCYXNlIGRlbGF5IGJldHdlZW4gcmV0cmllcyBpbiBtcyDigJQgZG91YmxlcyBlYWNoIGF0dGVtcHQgKGRlZmF1bHQ6IDIwMCkgKi9cbiAgcmV0cnlEZWxheU1zOiBudW1iZXI7XG4gIC8qKiBNYXhpbXVtIHJldHJ5IGF0dGVtcHRzIHNwZWNpZmljYWxseSBmb3IgNDI5IHJhdGUgbGltaXRpbmcgKGRlZmF1bHQ6IDQpICovXG4gIG1heFJhdGVMaW1pdFJldHJpZXM6IG51bWJlcjtcbn1cblxuLyoqXG4gKiBSZXN1bHQgb2YgYSByZXRyeSBkZWNpc2lvbjogd2hldGhlciB0byByZXRyeSwgYW5kIGhvdyBsb25nIHRvIHdhaXQuXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUmV0cnlEZWNpc2lvbiB7XG4gIC8qKiBXaGV0aGVyIHRoZSByZXF1ZXN0IHNob3VsZCBiZSByZXRyaWVkICovXG4gIHNob3VsZFJldHJ5OiBib29sZWFuO1xuICAvKiogRGVsYXkgaW4gbWlsbGlzZWNvbmRzIGJlZm9yZSByZXRyeWluZyAod2l0aCBqaXR0ZXIgYXBwbGllZCkgKi9cbiAgZGVsYXlNczogbnVtYmVyO1xuICAvKiogSHVtYW4tcmVhZGFibGUgcmVhc29uIGZvciB0aGUgcmV0cnkgZGVjaXNpb24gKi9cbiAgcmVhc29uOiBzdHJpbmc7XG59XG5cbi8qKlxuICogUGFyc2UgYSBgUmV0cnktQWZ0ZXJgIGhlYWRlciB2YWx1ZSBpbnRvIG1pbGxpc2Vjb25kcy5cbiAqIFN1cHBvcnRzIG51bWVyaWMgc2Vjb25kcyAoZS5nLiBcIjVcIikgYW5kIEhUVFAtZGF0ZSBmb3JtYXQuXG4gKiBSZXR1cm5zIGB1bmRlZmluZWRgIGZvciBtaXNzaW5nIG9yIGludmFsaWQgdmFsdWVzLlxuICovXG5leHBvcnQgZnVuY3Rpb24gcGFyc2VSZXRyeUFmdGVyKGhlYWRlcjogc3RyaW5nIHwgc3RyaW5nW10gfCB1bmRlZmluZWQpOiBudW1iZXIgfCB1bmRlZmluZWQge1xuICBpZiAoIWhlYWRlcikgcmV0dXJuIHVuZGVmaW5lZDtcbiAgY29uc3QgdmFsdWUgPSBBcnJheS5pc0FycmF5KGhlYWRlcikgPyBoZWFkZXJbMF0gOiBoZWFkZXI7XG4gIGlmICghdmFsdWUpIHJldHVybiB1bmRlZmluZWQ7XG5cbiAgLy8gVHJ5IG51bWVyaWMgc2Vjb25kcyBmaXJzdFxuICBjb25zdCBzZWNvbmRzID0gTnVtYmVyKHZhbHVlKTtcbiAgaWYgKCFpc05hTihzZWNvbmRzKSAmJiBzZWNvbmRzID49IDApIHtcbiAgICByZXR1cm4gTWF0aC5taW4oc2Vjb25kcyAqIDEwMDAsIE1BWF9SRVRSWV9BRlRFUl9NUyk7XG4gIH1cblxuICAvLyBUcnkgSFRUUC1kYXRlXG4gIGNvbnN0IGRhdGUgPSBEYXRlLnBhcnNlKHZhbHVlKTtcbiAgaWYgKCFpc05hTihkYXRlKSkge1xuICAgIGNvbnN0IGRlbGF5TXMgPSBkYXRlIC0gRGF0ZS5ub3coKTtcbiAgICBpZiAoZGVsYXlNcyA+IDApIHJldHVybiBNYXRoLm1pbihkZWxheU1zLCBNQVhfUkVUUllfQUZURVJfTVMpO1xuICB9XG5cbiAgcmV0dXJuIHVuZGVmaW5lZDtcbn1cblxuLyoqXG4gKiBBcHBseSArLy0yNSUgcmFuZG9tIGppdHRlciB0byBhIGRlbGF5IHRvIHByZXZlbnQgdGh1bmRlcmluZyBoZXJkLlxuICovXG5leHBvcnQgZnVuY3Rpb24gYWRkSml0dGVyKGRlbGF5OiBudW1iZXIpOiBudW1iZXIge1xuICBjb25zdCBqaXR0ZXIgPSBkZWxheSAqIDAuMjUgKiAoMiAqIE1hdGgucmFuZG9tKCkgLSAxKTsgLy8gLTI1JSB0byArMjUlXG4gIHJldHVybiBNYXRoLm1heCgwLCBNYXRoLnJvdW5kKGRlbGF5ICsgaml0dGVyKSk7XG59XG5cbi8qKlxuICogQ2FsY3VsYXRlIGV4cG9uZW50aWFsIGJhY2tvZmYgZGVsYXkgZm9yIGEgZ2l2ZW4gYXR0ZW1wdC5cbiAqXG4gKiBAcGFyYW0gYmFzZURlbGF5IC0gQmFzZSBkZWxheSBpbiBtaWxsaXNlY29uZHNcbiAqIEBwYXJhbSBhdHRlbXB0IC0gWmVyby1iYXNlZCBhdHRlbXB0IG51bWJlclxuICogQHJldHVybnMgRGVsYXkgaW4gbWlsbGlzZWNvbmRzICh3aXRob3V0IGppdHRlcilcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGNhbGN1bGF0ZUJhY2tvZmYoYmFzZURlbGF5OiBudW1iZXIsIGF0dGVtcHQ6IG51bWJlcik6IG51bWJlciB7XG4gIHJldHVybiBiYXNlRGVsYXkgKiBNYXRoLnBvdygyLCBhdHRlbXB0KTtcbn1cblxuLyoqXG4gKiBEZXRlcm1pbmUgd2hldGhlciBhIHN0YXR1cyBjb2RlIHJlcHJlc2VudHMgYSB0cmFuc2llbnQgc2VydmVyIGVycm9yXG4gKiB0aGF0IGlzIGVsaWdpYmxlIGZvciByZXRyeSAoNTAyLCA1MDMsIDUwNCkuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBpc1RyYW5zaWVudFN0YXR1c0NvZGUoc3RhdHVzQ29kZTogbnVtYmVyKTogYm9vbGVhbiB7XG4gIHJldHVybiBUUkFOU0lFTlRfU1RBVFVTX0NPREVTLmluY2x1ZGVzKHN0YXR1c0NvZGUpO1xufVxuXG4vKipcbiAqIERldGVybWluZSB3aGV0aGVyIGEgc3RhdHVzIGNvZGUgaW5kaWNhdGVzIHJhdGUgbGltaXRpbmcgKDQyOSkuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBpc1JhdGVMaW1pdGVkKHN0YXR1c0NvZGU6IG51bWJlcik6IGJvb2xlYW4ge1xuICByZXR1cm4gc3RhdHVzQ29kZSA9PT0gNDI5O1xufVxuXG4vKipcbiAqIERldGVybWluZSB3aGV0aGVyIGEgZmFpbGVkIHJlc3BvbnNlIHNob3VsZCBiZSByZXRyaWVkLCBhbmQgY29tcHV0ZSB0aGUgZGVsYXkuXG4gKlxuICogQHBhcmFtIHN0YXR1c0NvZGUgLSBIVFRQIHN0YXR1cyBjb2RlIG9mIHRoZSByZXNwb25zZVxuICogQHBhcmFtIGhlYWRlcnMgLSBSZXNwb25zZSBoZWFkZXJzICh1c2VkIHRvIHJlYWQgUmV0cnktQWZ0ZXIgZm9yIDQyOSlcbiAqIEBwYXJhbSBhdHRlbXB0IC0gWmVyby1iYXNlZCBhdHRlbXB0IG51bWJlclxuICogQHBhcmFtIGNvbmZpZyAtIFJldHJ5IGNvbmZpZ3VyYXRpb25cbiAqIEByZXR1cm5zIEEgUmV0cnlEZWNpc2lvbiBpbmRpY2F0aW5nIHdoZXRoZXIgdG8gcmV0cnkgYW5kIHRoZSBkZWxheVxuICovXG5leHBvcnQgZnVuY3Rpb24gZ2V0UmV0cnlEZWNpc2lvbihcbiAgc3RhdHVzQ29kZTogbnVtYmVyLFxuICBoZWFkZXJzOiBodHRwLkluY29taW5nSHR0cEhlYWRlcnMsXG4gIGF0dGVtcHQ6IG51bWJlcixcbiAgY29uZmlnOiBSZXRyeUNvbmZpZyxcbik6IFJldHJ5RGVjaXNpb24ge1xuICAvLyA0MjkgcmF0ZSBsaW1pdGluZyDigJQgdXNlIFJldHJ5LUFmdGVyIG9yIGxvbmdlciBiYWNrb2ZmICg0eCBiYXNlKVxuICBpZiAoaXNSYXRlTGltaXRlZChzdGF0dXNDb2RlKSAmJiBhdHRlbXB0IDwgY29uZmlnLm1heFJhdGVMaW1pdFJldHJpZXMpIHtcbiAgICBjb25zdCByZXRyeUFmdGVyID0gcGFyc2VSZXRyeUFmdGVyKGhlYWRlcnNbJ3JldHJ5LWFmdGVyJ10pO1xuICAgIGNvbnN0IHJhd0RlbGF5ID0gcmV0cnlBZnRlciA/PyAoY29uZmlnLnJldHJ5RGVsYXlNcyAqIDQgKiBNYXRoLnBvdygyLCBhdHRlbXB0KSk7XG4gICAgcmV0dXJuIHtcbiAgICAgIHNob3VsZFJldHJ5OiB0cnVlLFxuICAgICAgZGVsYXlNczogYWRkSml0dGVyKHJhd0RlbGF5KSxcbiAgICAgIHJlYXNvbjogJ1JhdGUgbGltaXRlZCAoNDI5KScsXG4gICAgfTtcbiAgfVxuXG4gIC8vIDV4eCB0cmFuc2llbnQgc2VydmVyIGVycm9ycyDigJQgc3RhbmRhcmQgZXhwb25lbnRpYWwgYmFja29mZlxuICBpZiAoaXNUcmFuc2llbnRTdGF0dXNDb2RlKHN0YXR1c0NvZGUpICYmIGF0dGVtcHQgPCBjb25maWcubWF4UmV0cmllcykge1xuICAgIGNvbnN0IHJhd0RlbGF5ID0gY2FsY3VsYXRlQmFja29mZihjb25maWcucmV0cnlEZWxheU1zLCBhdHRlbXB0KTtcbiAgICByZXR1cm4ge1xuICAgICAgc2hvdWxkUmV0cnk6IHRydWUsXG4gICAgICBkZWxheU1zOiBhZGRKaXR0ZXIocmF3RGVsYXkpLFxuICAgICAgcmVhc29uOiBgVHJhbnNpZW50IHNlcnZlciBlcnJvciAoJHtzdGF0dXNDb2RlfSlgLFxuICAgIH07XG4gIH1cblxuICByZXR1cm4geyBzaG91bGRSZXRyeTogZmFsc2UsIGRlbGF5TXM6IDAsIHJlYXNvbjogJ05vdCByZXRyeWFibGUnIH07XG59XG5cbi8qKlxuICogRGV0ZXJtaW5lIHdoZXRoZXIgYSBjb25uZWN0aW9uL3RpbWVvdXQgZXJyb3Igc2hvdWxkIGJlIHJldHJpZWQsIGFuZCBjb21wdXRlIHRoZSBkZWxheS5cbiAqXG4gKiBAcGFyYW0gYXR0ZW1wdCAtIFplcm8tYmFzZWQgYXR0ZW1wdCBudW1iZXJcbiAqIEBwYXJhbSBjb25maWcgLSBSZXRyeSBjb25maWd1cmF0aW9uXG4gKiBAcmV0dXJucyBBIFJldHJ5RGVjaXNpb24gaW5kaWNhdGluZyB3aGV0aGVyIHRvIHJldHJ5IGFuZCB0aGUgZGVsYXlcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGdldEVycm9yUmV0cnlEZWNpc2lvbihcbiAgYXR0ZW1wdDogbnVtYmVyLFxuICBjb25maWc6IFJldHJ5Q29uZmlnLFxuKTogUmV0cnlEZWNpc2lvbiB7XG4gIGlmIChhdHRlbXB0IDwgY29uZmlnLm1heFJldHJpZXMpIHtcbiAgICBjb25zdCByYXdEZWxheSA9IGNhbGN1bGF0ZUJhY2tvZmYoY29uZmlnLnJldHJ5RGVsYXlNcywgYXR0ZW1wdCk7XG4gICAgcmV0dXJuIHtcbiAgICAgIHNob3VsZFJldHJ5OiB0cnVlLFxuICAgICAgZGVsYXlNczogYWRkSml0dGVyKHJhd0RlbGF5KSxcbiAgICAgIHJlYXNvbjogJ0Nvbm5lY3Rpb24gb3IgdGltZW91dCBlcnJvcicsXG4gICAgfTtcbiAgfVxuXG4gIHJldHVybiB7IHNob3VsZFJldHJ5OiBmYWxzZSwgZGVsYXlNczogMCwgcmVhc29uOiAnTWF4IHJldHJpZXMgZXhjZWVkZWQnIH07XG59XG4iXX0=
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type LockRedis } from './leader-lock.js';
|
|
2
|
+
/** Optional cross-pod single-runner lock for a scheduler's cycle. The redis
|
|
3
|
+
* client is resolved per cycle (it may be an async getter, e.g. a BullMQ
|
|
4
|
+
* `queue.client` promise). */
|
|
5
|
+
export interface SchedulerLock {
|
|
6
|
+
redis: () => LockRedis | Promise<LockRedis>;
|
|
7
|
+
key: string;
|
|
8
|
+
ttlMs: number;
|
|
9
|
+
}
|
|
10
|
+
export interface SchedulerOptions {
|
|
11
|
+
/** Log label (also the logger name). */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Interval between cycles (ms). */
|
|
14
|
+
intervalMs: number;
|
|
15
|
+
/** The work to run each cycle. Errors are caught + logged, never thrown. */
|
|
16
|
+
run: () => Promise<void>;
|
|
17
|
+
/** Run a cycle immediately when started. Default true. */
|
|
18
|
+
runOnStart?: boolean;
|
|
19
|
+
/** Delay before the first cycle (and before the interval begins), ms.
|
|
20
|
+
* Default 0. Lets a service wait for dependencies to come up. */
|
|
21
|
+
startupDelayMs?: number;
|
|
22
|
+
/** When set, each cycle runs only on the pod that wins this lock — so with
|
|
23
|
+
* multiple replicas only one runs per window. */
|
|
24
|
+
lock?: SchedulerLock;
|
|
25
|
+
}
|
|
26
|
+
export interface Scheduler {
|
|
27
|
+
/** Start the timer. Idempotent — repeated calls are no-ops. */
|
|
28
|
+
start(): void;
|
|
29
|
+
/** Stop the timer (and cancel a pending startup delay). Safe before start. */
|
|
30
|
+
stop(): void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A periodic background job: a single unref'd `setInterval` with start-once
|
|
34
|
+
* semantics, error isolation (a throwing cycle is logged, never crashes the
|
|
35
|
+
* loop), an optional startup delay, and an optional cross-pod leader lock.
|
|
36
|
+
*
|
|
37
|
+
* Replaces the hand-rolled timer/`unref`/`catch`/start-stop boilerplate that
|
|
38
|
+
* each scheduler (scan, digest, registry GC, billing lifecycle) repeated, and
|
|
39
|
+
* makes the leader lock available to all of them uniformly.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createScheduler(opts: SchedulerOptions): Scheduler;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { withLeaderLock } from './leader-lock.js';
|
|
4
|
+
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
const errorMessage = (e) => (e instanceof Error ? e.message : String(e));
|
|
6
|
+
/**
|
|
7
|
+
* A periodic background job: a single unref'd `setInterval` with start-once
|
|
8
|
+
* semantics, error isolation (a throwing cycle is logged, never crashes the
|
|
9
|
+
* loop), an optional startup delay, and an optional cross-pod leader lock.
|
|
10
|
+
*
|
|
11
|
+
* Replaces the hand-rolled timer/`unref`/`catch`/start-stop boilerplate that
|
|
12
|
+
* each scheduler (scan, digest, registry GC, billing lifecycle) repeated, and
|
|
13
|
+
* makes the leader lock available to all of them uniformly.
|
|
14
|
+
*/
|
|
15
|
+
export function createScheduler(opts) {
|
|
16
|
+
const log = createLogger(opts.name);
|
|
17
|
+
let interval = null;
|
|
18
|
+
let startup = null;
|
|
19
|
+
let stopped = false;
|
|
20
|
+
let running = false;
|
|
21
|
+
const cycle = async () => {
|
|
22
|
+
// Same-pod re-entrancy guard: setInterval doesn't await the async cycle, so
|
|
23
|
+
// a cycle slower than intervalMs would otherwise overlap ITSELF on this pod
|
|
24
|
+
// (double-processing, duplicate side effects). The optional leader lock only
|
|
25
|
+
// guards CROSS-pod overlap; this guards same-pod. A skipped tick just runs on
|
|
26
|
+
// the next interval.
|
|
27
|
+
if (running) {
|
|
28
|
+
log.debug('Cycle skipped — previous cycle still running (same-pod re-entrancy)');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
running = true;
|
|
32
|
+
try {
|
|
33
|
+
if (opts.lock) {
|
|
34
|
+
const redis = await opts.lock.redis();
|
|
35
|
+
const ran = await withLeaderLock(redis, opts.lock.key, opts.lock.ttlMs, opts.run);
|
|
36
|
+
if (!ran)
|
|
37
|
+
log.debug('Cycle skipped — another pod holds the lock');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
await opts.run();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
log.error('Scheduler cycle failed', { error: errorMessage(err) });
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
running = false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const begin = () => {
|
|
51
|
+
if (stopped)
|
|
52
|
+
return; // stop() called during the startup delay
|
|
53
|
+
if (opts.runOnStart !== false)
|
|
54
|
+
void cycle();
|
|
55
|
+
interval = setInterval(() => void cycle(), opts.intervalMs);
|
|
56
|
+
interval.unref();
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
start() {
|
|
60
|
+
if (interval || startup)
|
|
61
|
+
return;
|
|
62
|
+
stopped = false;
|
|
63
|
+
if (opts.startupDelayMs && opts.startupDelayMs > 0) {
|
|
64
|
+
startup = setTimeout(() => { startup = null; begin(); }, opts.startupDelayMs);
|
|
65
|
+
startup.unref();
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
begin();
|
|
69
|
+
}
|
|
70
|
+
log.info('Scheduler started', { intervalMs: opts.intervalMs, locked: !!opts.lock });
|
|
71
|
+
},
|
|
72
|
+
stop() {
|
|
73
|
+
stopped = true;
|
|
74
|
+
if (startup) {
|
|
75
|
+
clearTimeout(startup);
|
|
76
|
+
startup = null;
|
|
77
|
+
}
|
|
78
|
+
if (interval) {
|
|
79
|
+
clearInterval(interval);
|
|
80
|
+
interval = null;
|
|
81
|
+
}
|
|
82
|
+
log.info('Scheduler stopped');
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NoZWR1bGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3NlcnZpY2VzL3NjaGVkdWxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLE9BQU8sRUFBRSxjQUFjLEVBQWtCLE1BQU0sa0JBQWtCLENBQUM7QUFDbEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBRWxELE1BQU0sWUFBWSxHQUFHLENBQUMsQ0FBVSxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBbUNsRjs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sVUFBVSxlQUFlLENBQUMsSUFBc0I7SUFDcEQsTUFBTSxHQUFHLEdBQUcsWUFBWSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUNwQyxJQUFJLFFBQVEsR0FBMEMsSUFBSSxDQUFDO0lBQzNELElBQUksT0FBTyxHQUF5QyxJQUFJLENBQUM7SUFDekQsSUFBSSxPQUFPLEdBQUcsS0FBSyxDQUFDO0lBQ3BCLElBQUksT0FBTyxHQUFHLEtBQUssQ0FBQztJQUVwQixNQUFNLEtBQUssR0FBRyxLQUFLLElBQW1CLEVBQUU7UUFDdEMsNEVBQTRFO1FBQzVFLDRFQUE0RTtRQUM1RSw2RUFBNkU7UUFDN0UsOEVBQThFO1FBQzlFLHFCQUFxQjtRQUNyQixJQUFJLE9BQU8sRUFBRSxDQUFDO1lBQ1osR0FBRyxDQUFDLEtBQUssQ0FBQyxxRUFBcUUsQ0FBQyxDQUFDO1lBQ2pGLE9BQU87UUFDVCxDQUFDO1FBQ0QsT0FBTyxHQUFHLElBQUksQ0FBQztRQUNmLElBQUksQ0FBQztZQUNILElBQUksSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDO2dCQUNkLE1BQU0sS0FBSyxHQUFHLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztnQkFDdEMsTUFBTSxHQUFHLEdBQUcsTUFBTSxjQUFjLENBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQztnQkFDbEYsSUFBSSxDQUFDLEdBQUc7b0JBQUUsR0FBRyxDQUFDLEtBQUssQ0FBQyw0Q0FBNEMsQ0FBQyxDQUFDO1lBQ3BFLENBQUM7aUJBQU0sQ0FBQztnQkFDTixNQUFNLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztZQUNuQixDQUFDO1FBQ0gsQ0FBQztRQUFDLE9BQU8sR0FBRyxFQUFFLENBQUM7WUFDYixHQUFHLENBQUMsS0FBSyxDQUFDLHdCQUF3QixFQUFFLEVBQUUsS0FBSyxFQUFFLFlBQVksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDcEUsQ0FBQztnQkFBUyxDQUFDO1lBQ1QsT0FBTyxHQUFHLEtBQUssQ0FBQztRQUNsQixDQUFDO0lBQ0gsQ0FBQyxDQUFDO0lBRUYsTUFBTSxLQUFLLEdBQUcsR0FBUyxFQUFFO1FBQ3ZCLElBQUksT0FBTztZQUFFLE9BQU8sQ0FBQyx5Q0FBeUM7UUFDOUQsSUFBSSxJQUFJLENBQUMsVUFBVSxLQUFLLEtBQUs7WUFBRSxLQUFLLEtBQUssRUFBRSxDQUFDO1FBQzVDLFFBQVEsR0FBRyxXQUFXLENBQUMsR0FBRyxFQUFFLENBQUMsS0FBSyxLQUFLLEVBQUUsRUFBRSxJQUFJLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDNUQsUUFBUSxDQUFDLEtBQUssRUFBRSxDQUFDO0lBQ25CLENBQUMsQ0FBQztJQUVGLE9BQU87UUFDTCxLQUFLO1lBQ0gsSUFBSSxRQUFRLElBQUksT0FBTztnQkFBRSxPQUFPO1lBQ2hDLE9BQU8sR0FBRyxLQUFLLENBQUM7WUFDaEIsSUFBSSxJQUFJLENBQUMsY0FBYyxJQUFJLElBQUksQ0FBQyxjQUFjLEdBQUcsQ0FBQyxFQUFFLENBQUM7Z0JBQ25ELE9BQU8sR0FBRyxVQUFVLENBQUMsR0FBRyxFQUFFLEdBQUcsT0FBTyxHQUFHLElBQUksQ0FBQyxDQUFDLEtBQUssRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQyxjQUFjLENBQUMsQ0FBQztnQkFDOUUsT0FBTyxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQ2xCLENBQUM7aUJBQU0sQ0FBQztnQkFDTixLQUFLLEVBQUUsQ0FBQztZQUNWLENBQUM7WUFDRCxHQUFHLENBQUMsSUFBSSxDQUFDLG1CQUFtQixFQUFFLEVBQUUsVUFBVSxFQUFFLElBQUksQ0FBQyxVQUFVLEVBQUUsTUFBTSxFQUFFLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQztRQUN0RixDQUFDO1FBQ0QsSUFBSTtZQUNGLE9BQU8sR0FBRyxJQUFJLENBQUM7WUFDZixJQUFJLE9BQU8sRUFBRSxDQUFDO2dCQUFDLFlBQVksQ0FBQyxPQUFPLENBQUMsQ0FBQztnQkFBQyxPQUFPLEdBQUcsSUFBSSxDQUFDO1lBQUMsQ0FBQztZQUN2RCxJQUFJLFFBQVEsRUFBRSxDQUFDO2dCQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQztnQkFBQyxRQUFRLEdBQUcsSUFBSSxDQUFDO1lBQUMsQ0FBQztZQUMzRCxHQUFHLENBQUMsSUFBSSxDQUFDLG1CQUFtQixDQUFDLENBQUM7UUFDaEMsQ0FBQztLQUNGLENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IHdpdGhMZWFkZXJMb2NrLCB0eXBlIExvY2tSZWRpcyB9IGZyb20gJy4vbGVhZGVyLWxvY2suanMnO1xuaW1wb3J0IHsgY3JlYXRlTG9nZ2VyIH0gZnJvbSAnLi4vdXRpbHMvbG9nZ2VyLmpzJztcblxuY29uc3QgZXJyb3JNZXNzYWdlID0gKGU6IHVua25vd24pID0+IChlIGluc3RhbmNlb2YgRXJyb3IgPyBlLm1lc3NhZ2UgOiBTdHJpbmcoZSkpO1xuXG4vKiogT3B0aW9uYWwgY3Jvc3MtcG9kIHNpbmdsZS1ydW5uZXIgbG9jayBmb3IgYSBzY2hlZHVsZXIncyBjeWNsZS4gVGhlIHJlZGlzXG4gKiAgY2xpZW50IGlzIHJlc29sdmVkIHBlciBjeWNsZSAoaXQgbWF5IGJlIGFuIGFzeW5jIGdldHRlciwgZS5nLiBhIEJ1bGxNUVxuICogIGBxdWV1ZS5jbGllbnRgIHByb21pc2UpLiAqL1xuZXhwb3J0IGludGVyZmFjZSBTY2hlZHVsZXJMb2NrIHtcbiAgcmVkaXM6ICgpID0+IExvY2tSZWRpcyB8IFByb21pc2U8TG9ja1JlZGlzPjtcbiAga2V5OiBzdHJpbmc7XG4gIHR0bE1zOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgU2NoZWR1bGVyT3B0aW9ucyB7XG4gIC8qKiBMb2cgbGFiZWwgKGFsc28gdGhlIGxvZ2dlciBuYW1lKS4gKi9cbiAgbmFtZTogc3RyaW5nO1xuICAvKiogSW50ZXJ2YWwgYmV0d2VlbiBjeWNsZXMgKG1zKS4gKi9cbiAgaW50ZXJ2YWxNczogbnVtYmVyO1xuICAvKiogVGhlIHdvcmsgdG8gcnVuIGVhY2ggY3ljbGUuIEVycm9ycyBhcmUgY2F1Z2h0ICsgbG9nZ2VkLCBuZXZlciB0aHJvd24uICovXG4gIHJ1bjogKCkgPT4gUHJvbWlzZTx2b2lkPjtcbiAgLyoqIFJ1biBhIGN5Y2xlIGltbWVkaWF0ZWx5IHdoZW4gc3RhcnRlZC4gRGVmYXVsdCB0cnVlLiAqL1xuICBydW5PblN0YXJ0PzogYm9vbGVhbjtcbiAgLyoqIERlbGF5IGJlZm9yZSB0aGUgZmlyc3QgY3ljbGUgKGFuZCBiZWZvcmUgdGhlIGludGVydmFsIGJlZ2lucyksIG1zLlxuICAgKiAgRGVmYXVsdCAwLiBMZXRzIGEgc2VydmljZSB3YWl0IGZvciBkZXBlbmRlbmNpZXMgdG8gY29tZSB1cC4gKi9cbiAgc3RhcnR1cERlbGF5TXM/OiBudW1iZXI7XG4gIC8qKiBXaGVuIHNldCwgZWFjaCBjeWNsZSBydW5zIG9ubHkgb24gdGhlIHBvZCB0aGF0IHdpbnMgdGhpcyBsb2NrIOKAlCBzbyB3aXRoXG4gICAqICBtdWx0aXBsZSByZXBsaWNhcyBvbmx5IG9uZSBydW5zIHBlciB3aW5kb3cuICovXG4gIGxvY2s/OiBTY2hlZHVsZXJMb2NrO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFNjaGVkdWxlciB7XG4gIC8qKiBTdGFydCB0aGUgdGltZXIuIElkZW1wb3RlbnQg4oCUIHJlcGVhdGVkIGNhbGxzIGFyZSBuby1vcHMuICovXG4gIHN0YXJ0KCk6IHZvaWQ7XG4gIC8qKiBTdG9wIHRoZSB0aW1lciAoYW5kIGNhbmNlbCBhIHBlbmRpbmcgc3RhcnR1cCBkZWxheSkuIFNhZmUgYmVmb3JlIHN0YXJ0LiAqL1xuICBzdG9wKCk6IHZvaWQ7XG59XG5cbi8qKlxuICogQSBwZXJpb2RpYyBiYWNrZ3JvdW5kIGpvYjogYSBzaW5nbGUgdW5yZWYnZCBgc2V0SW50ZXJ2YWxgIHdpdGggc3RhcnQtb25jZVxuICogc2VtYW50aWNzLCBlcnJvciBpc29sYXRpb24gKGEgdGhyb3dpbmcgY3ljbGUgaXMgbG9nZ2VkLCBuZXZlciBjcmFzaGVzIHRoZVxuICogbG9vcCksIGFuIG9wdGlvbmFsIHN0YXJ0dXAgZGVsYXksIGFuZCBhbiBvcHRpb25hbCBjcm9zcy1wb2QgbGVhZGVyIGxvY2suXG4gKlxuICogUmVwbGFjZXMgdGhlIGhhbmQtcm9sbGVkIHRpbWVyL2B1bnJlZmAvYGNhdGNoYC9zdGFydC1zdG9wIGJvaWxlcnBsYXRlIHRoYXRcbiAqIGVhY2ggc2NoZWR1bGVyIChzY2FuLCBkaWdlc3QsIHJlZ2lzdHJ5IEdDLCBiaWxsaW5nIGxpZmVjeWNsZSkgcmVwZWF0ZWQsIGFuZFxuICogbWFrZXMgdGhlIGxlYWRlciBsb2NrIGF2YWlsYWJsZSB0byBhbGwgb2YgdGhlbSB1bmlmb3JtbHkuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVTY2hlZHVsZXIob3B0czogU2NoZWR1bGVyT3B0aW9ucyk6IFNjaGVkdWxlciB7XG4gIGNvbnN0IGxvZyA9IGNyZWF0ZUxvZ2dlcihvcHRzLm5hbWUpO1xuICBsZXQgaW50ZXJ2YWw6IFJldHVyblR5cGU8dHlwZW9mIHNldEludGVydmFsPiB8IG51bGwgPSBudWxsO1xuICBsZXQgc3RhcnR1cDogUmV0dXJuVHlwZTx0eXBlb2Ygc2V0VGltZW91dD4gfCBudWxsID0gbnVsbDtcbiAgbGV0IHN0b3BwZWQgPSBmYWxzZTtcbiAgbGV0IHJ1bm5pbmcgPSBmYWxzZTtcblxuICBjb25zdCBjeWNsZSA9IGFzeW5jICgpOiBQcm9taXNlPHZvaWQ+ID0+IHtcbiAgICAvLyBTYW1lLXBvZCByZS1lbnRyYW5jeSBndWFyZDogc2V0SW50ZXJ2YWwgZG9lc24ndCBhd2FpdCB0aGUgYXN5bmMgY3ljbGUsIHNvXG4gICAgLy8gYSBjeWNsZSBzbG93ZXIgdGhhbiBpbnRlcnZhbE1zIHdvdWxkIG90aGVyd2lzZSBvdmVybGFwIElUU0VMRiBvbiB0aGlzIHBvZFxuICAgIC8vIChkb3VibGUtcHJvY2Vzc2luZywgZHVwbGljYXRlIHNpZGUgZWZmZWN0cykuIFRoZSBvcHRpb25hbCBsZWFkZXIgbG9jayBvbmx5XG4gICAgLy8gZ3VhcmRzIENST1NTLXBvZCBvdmVybGFwOyB0aGlzIGd1YXJkcyBzYW1lLXBvZC4gQSBza2lwcGVkIHRpY2sganVzdCBydW5zIG9uXG4gICAgLy8gdGhlIG5leHQgaW50ZXJ2YWwuXG4gICAgaWYgKHJ1bm5pbmcpIHtcbiAgICAgIGxvZy5kZWJ1ZygnQ3ljbGUgc2tpcHBlZCDigJQgcHJldmlvdXMgY3ljbGUgc3RpbGwgcnVubmluZyAoc2FtZS1wb2QgcmUtZW50cmFuY3kpJyk7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIHJ1bm5pbmcgPSB0cnVlO1xuICAgIHRyeSB7XG4gICAgICBpZiAob3B0cy5sb2NrKSB7XG4gICAgICAgIGNvbnN0IHJlZGlzID0gYXdhaXQgb3B0cy5sb2NrLnJlZGlzKCk7XG4gICAgICAgIGNvbnN0IHJhbiA9IGF3YWl0IHdpdGhMZWFkZXJMb2NrKHJlZGlzLCBvcHRzLmxvY2sua2V5LCBvcHRzLmxvY2sudHRsTXMsIG9wdHMucnVuKTtcbiAgICAgICAgaWYgKCFyYW4pIGxvZy5kZWJ1ZygnQ3ljbGUgc2tpcHBlZCDigJQgYW5vdGhlciBwb2QgaG9sZHMgdGhlIGxvY2snKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGF3YWl0IG9wdHMucnVuKCk7XG4gICAgICB9XG4gICAgfSBjYXRjaCAoZXJyKSB7XG4gICAgICBsb2cuZXJyb3IoJ1NjaGVkdWxlciBjeWNsZSBmYWlsZWQnLCB7IGVycm9yOiBlcnJvck1lc3NhZ2UoZXJyKSB9KTtcbiAgICB9IGZpbmFsbHkge1xuICAgICAgcnVubmluZyA9IGZhbHNlO1xuICAgIH1cbiAgfTtcblxuICBjb25zdCBiZWdpbiA9ICgpOiB2b2lkID0+IHtcbiAgICBpZiAoc3RvcHBlZCkgcmV0dXJuOyAvLyBzdG9wKCkgY2FsbGVkIGR1cmluZyB0aGUgc3RhcnR1cCBkZWxheVxuICAgIGlmIChvcHRzLnJ1bk9uU3RhcnQgIT09IGZhbHNlKSB2b2lkIGN5Y2xlKCk7XG4gICAgaW50ZXJ2YWwgPSBzZXRJbnRlcnZhbCgoKSA9PiB2b2lkIGN5Y2xlKCksIG9wdHMuaW50ZXJ2YWxNcyk7XG4gICAgaW50ZXJ2YWwudW5yZWYoKTtcbiAgfTtcblxuICByZXR1cm4ge1xuICAgIHN0YXJ0KCk6IHZvaWQge1xuICAgICAgaWYgKGludGVydmFsIHx8IHN0YXJ0dXApIHJldHVybjtcbiAgICAgIHN0b3BwZWQgPSBmYWxzZTtcbiAgICAgIGlmIChvcHRzLnN0YXJ0dXBEZWxheU1zICYmIG9wdHMuc3RhcnR1cERlbGF5TXMgPiAwKSB7XG4gICAgICAgIHN0YXJ0dXAgPSBzZXRUaW1lb3V0KCgpID0+IHsgc3RhcnR1cCA9IG51bGw7IGJlZ2luKCk7IH0sIG9wdHMuc3RhcnR1cERlbGF5TXMpO1xuICAgICAgICBzdGFydHVwLnVucmVmKCk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBiZWdpbigpO1xuICAgICAgfVxuICAgICAgbG9nLmluZm8oJ1NjaGVkdWxlciBzdGFydGVkJywgeyBpbnRlcnZhbE1zOiBvcHRzLmludGVydmFsTXMsIGxvY2tlZDogISFvcHRzLmxvY2sgfSk7XG4gICAgfSxcbiAgICBzdG9wKCk6IHZvaWQge1xuICAgICAgc3RvcHBlZCA9IHRydWU7XG4gICAgICBpZiAoc3RhcnR1cCkgeyBjbGVhclRpbWVvdXQoc3RhcnR1cCk7IHN0YXJ0dXAgPSBudWxsOyB9XG4gICAgICBpZiAoaW50ZXJ2YWwpIHsgY2xlYXJJbnRlcnZhbChpbnRlcnZhbCk7IGludGVydmFsID0gbnVsbDsgfVxuICAgICAgbG9nLmluZm8oJ1NjaGVkdWxlciBzdG9wcGVkJyk7XG4gICAgfSxcbiAgfTtcbn1cbiJdfQ==
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RemoteAuditClient } from './remote-audit-client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wire the two boot-security concerns every stateless service sets up
|
|
4
|
+
* identically at startup:
|
|
5
|
+
* 1. forward denied (non-GET) authorizations to the shared `authz.denied`
|
|
6
|
+
* audit sink (`wireAuthzDenialAuditor`);
|
|
7
|
+
* 2. register the env-Redis token-revocation reader (fail-open) so
|
|
8
|
+
* `requireAuth` can reject a token behind the platform-published
|
|
9
|
+
* `tokenVersion` (`setTokenRevocationStore(createEnvRedisTokenRevocationStore())`).
|
|
10
|
+
*
|
|
11
|
+
* Collapses the two copy-pasted lines in each service's `index.ts` into one call.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: the PLUGIN service opts out — it shares its health-check Redis
|
|
14
|
+
* connection via `createRedisTokenRevocationStore(getHealthRedisConnection())`
|
|
15
|
+
* instead of the env-Redis store — so it wires these two concerns by hand.
|
|
16
|
+
*
|
|
17
|
+
* @param serviceName short service name minted into the `authz.denied` records
|
|
18
|
+
* @param getAuditClient lazy accessor for the service's RemoteAuditClient
|
|
19
|
+
*/
|
|
20
|
+
export declare function wireServiceSecurity(serviceName: string, getAuditClient: () => RemoteAuditClient): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { wireAuthzDenialAuditor } from './remote-audit-client.js';
|
|
4
|
+
import { createEnvRedisTokenRevocationStore } from './token-revocation.js';
|
|
5
|
+
import { setTokenRevocationStore } from '../middleware/auth.js';
|
|
6
|
+
/**
|
|
7
|
+
* Wire the two boot-security concerns every stateless service sets up
|
|
8
|
+
* identically at startup:
|
|
9
|
+
* 1. forward denied (non-GET) authorizations to the shared `authz.denied`
|
|
10
|
+
* audit sink (`wireAuthzDenialAuditor`);
|
|
11
|
+
* 2. register the env-Redis token-revocation reader (fail-open) so
|
|
12
|
+
* `requireAuth` can reject a token behind the platform-published
|
|
13
|
+
* `tokenVersion` (`setTokenRevocationStore(createEnvRedisTokenRevocationStore())`).
|
|
14
|
+
*
|
|
15
|
+
* Collapses the two copy-pasted lines in each service's `index.ts` into one call.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: the PLUGIN service opts out — it shares its health-check Redis
|
|
18
|
+
* connection via `createRedisTokenRevocationStore(getHealthRedisConnection())`
|
|
19
|
+
* instead of the env-Redis store — so it wires these two concerns by hand.
|
|
20
|
+
*
|
|
21
|
+
* @param serviceName short service name minted into the `authz.denied` records
|
|
22
|
+
* @param getAuditClient lazy accessor for the service's RemoteAuditClient
|
|
23
|
+
*/
|
|
24
|
+
export function wireServiceSecurity(serviceName, getAuditClient) {
|
|
25
|
+
wireAuthzDenialAuditor(serviceName, getAuditClient);
|
|
26
|
+
setTokenRevocationStore(createEnvRedisTokenRevocationStore());
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS1ib290LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3NlcnZpY2VzL3NlcnZpY2UtYm9vdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBR3RDLE9BQU8sRUFBRSxzQkFBc0IsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBQ2xFLE9BQU8sRUFBRSxrQ0FBa0MsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQzNFLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBRWhFOzs7Ozs7Ozs7Ozs7Ozs7OztHQWlCRztBQUNILE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxXQUFtQixFQUFFLGNBQXVDO0lBQzlGLHNCQUFzQixDQUFDLFdBQVcsRUFBRSxjQUFjLENBQUMsQ0FBQztJQUNwRCx1QkFBdUIsQ0FBQyxrQ0FBa0MsRUFBRSxDQUFDLENBQUM7QUFDaEUsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG5pbXBvcnQgdHlwZSB7IFJlbW90ZUF1ZGl0Q2xpZW50IH0gZnJvbSAnLi9yZW1vdGUtYXVkaXQtY2xpZW50LmpzJztcbmltcG9ydCB7IHdpcmVBdXRoekRlbmlhbEF1ZGl0b3IgfSBmcm9tICcuL3JlbW90ZS1hdWRpdC1jbGllbnQuanMnO1xuaW1wb3J0IHsgY3JlYXRlRW52UmVkaXNUb2tlblJldm9jYXRpb25TdG9yZSB9IGZyb20gJy4vdG9rZW4tcmV2b2NhdGlvbi5qcyc7XG5pbXBvcnQgeyBzZXRUb2tlblJldm9jYXRpb25TdG9yZSB9IGZyb20gJy4uL21pZGRsZXdhcmUvYXV0aC5qcyc7XG5cbi8qKlxuICogV2lyZSB0aGUgdHdvIGJvb3Qtc2VjdXJpdHkgY29uY2VybnMgZXZlcnkgc3RhdGVsZXNzIHNlcnZpY2Ugc2V0cyB1cFxuICogaWRlbnRpY2FsbHkgYXQgc3RhcnR1cDpcbiAqICAgMS4gZm9yd2FyZCBkZW5pZWQgKG5vbi1HRVQpIGF1dGhvcml6YXRpb25zIHRvIHRoZSBzaGFyZWQgYGF1dGh6LmRlbmllZGBcbiAqICAgICAgYXVkaXQgc2luayAoYHdpcmVBdXRoekRlbmlhbEF1ZGl0b3JgKTtcbiAqICAgMi4gcmVnaXN0ZXIgdGhlIGVudi1SZWRpcyB0b2tlbi1yZXZvY2F0aW9uIHJlYWRlciAoZmFpbC1vcGVuKSBzb1xuICogICAgICBgcmVxdWlyZUF1dGhgIGNhbiByZWplY3QgYSB0b2tlbiBiZWhpbmQgdGhlIHBsYXRmb3JtLXB1Ymxpc2hlZFxuICogICAgICBgdG9rZW5WZXJzaW9uYCAoYHNldFRva2VuUmV2b2NhdGlvblN0b3JlKGNyZWF0ZUVudlJlZGlzVG9rZW5SZXZvY2F0aW9uU3RvcmUoKSlgKS5cbiAqXG4gKiBDb2xsYXBzZXMgdGhlIHR3byBjb3B5LXBhc3RlZCBsaW5lcyBpbiBlYWNoIHNlcnZpY2UncyBgaW5kZXgudHNgIGludG8gb25lIGNhbGwuXG4gKlxuICogTk9URTogdGhlIFBMVUdJTiBzZXJ2aWNlIG9wdHMgb3V0IOKAlCBpdCBzaGFyZXMgaXRzIGhlYWx0aC1jaGVjayBSZWRpc1xuICogY29ubmVjdGlvbiB2aWEgYGNyZWF0ZVJlZGlzVG9rZW5SZXZvY2F0aW9uU3RvcmUoZ2V0SGVhbHRoUmVkaXNDb25uZWN0aW9uKCkpYFxuICogaW5zdGVhZCBvZiB0aGUgZW52LVJlZGlzIHN0b3JlIOKAlCBzbyBpdCB3aXJlcyB0aGVzZSB0d28gY29uY2VybnMgYnkgaGFuZC5cbiAqXG4gKiBAcGFyYW0gc2VydmljZU5hbWUgc2hvcnQgc2VydmljZSBuYW1lIG1pbnRlZCBpbnRvIHRoZSBgYXV0aHouZGVuaWVkYCByZWNvcmRzXG4gKiBAcGFyYW0gZ2V0QXVkaXRDbGllbnQgbGF6eSBhY2Nlc3NvciBmb3IgdGhlIHNlcnZpY2UncyBSZW1vdGVBdWRpdENsaWVudFxuICovXG5leHBvcnQgZnVuY3Rpb24gd2lyZVNlcnZpY2VTZWN1cml0eShzZXJ2aWNlTmFtZTogc3RyaW5nLCBnZXRBdWRpdENsaWVudDogKCkgPT4gUmVtb3RlQXVkaXRDbGllbnQpOiB2b2lkIHtcbiAgd2lyZUF1dGh6RGVuaWFsQXVkaXRvcihzZXJ2aWNlTmFtZSwgZ2V0QXVkaXRDbGllbnQpO1xuICBzZXRUb2tlblJldm9jYXRpb25TdG9yZShjcmVhdGVFbnZSZWRpc1Rva2VuUmV2b2NhdGlvblN0b3JlKCkpO1xufVxuIl19
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Result of {@link SseTicketStore.issue}. */
|
|
2
|
+
export type SseTicketIssueResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
ticket: string;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: 'total' | 'org';
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Store for short-lived, single-use SSE auth tickets. A client exchanges its JWT
|
|
11
|
+
* for a ticket (so the token never lands in an EventSource query string / access
|
|
12
|
+
* log), then redeems it on the SSE `GET`.
|
|
13
|
+
*
|
|
14
|
+
* The store is the multi-replica correctness boundary: a ticket minted on pod A
|
|
15
|
+
* MUST be redeemable on pod B. The Redis backend makes that true; the in-memory
|
|
16
|
+
* backend is a single-process fallback for dev / single-replica installs.
|
|
17
|
+
*/
|
|
18
|
+
export interface SseTicketStore {
|
|
19
|
+
/** Mint a ticket for `orgId`, or reject when a rate window is saturated. */
|
|
20
|
+
issue(orgId: string): Promise<SseTicketIssueResult>;
|
|
21
|
+
/** Atomically redeem a ticket exactly once; returns its org or null if invalid/expired/spent. */
|
|
22
|
+
consume(ticketId: string): Promise<{
|
|
23
|
+
orgId: string;
|
|
24
|
+
} | null>;
|
|
25
|
+
/** Release timers/resources. In-memory clears its sweep interval; Redis is a no-op. */
|
|
26
|
+
stop(): void;
|
|
27
|
+
}
|
|
28
|
+
/** Caps + TTL for the ticket store. */
|
|
29
|
+
export interface SseTicketStoreConfig {
|
|
30
|
+
/** Ticket lifetime — long enough for the client to open the EventSource. */
|
|
31
|
+
ttlMs: number;
|
|
32
|
+
/** Hard cap on tickets minted per TTL window across all orgs (abuse bound). */
|
|
33
|
+
maxTotal: number;
|
|
34
|
+
/** Per-org cap on tickets minted per TTL window (single-tenant abuse bound). */
|
|
35
|
+
maxPerOrg: number;
|
|
36
|
+
/**
|
|
37
|
+
* Optional Redis key namespace so independent channels (e.g. message
|
|
38
|
+
* notifications vs. reporting execution-status) don't share one ticket
|
|
39
|
+
* keyspace — a ticket minted for one channel must not be redeemable on
|
|
40
|
+
* another. Omit for the default (shared) namespace.
|
|
41
|
+
*/
|
|
42
|
+
keyPrefix?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build an {@link SseTicketStore} from the standard Redis env (`REDIS_URL` or
|
|
46
|
+
* `REDIS_HOST`/`REDIS_PORT`). Uses the Redis backend when configured
|
|
47
|
+
* (multi-replica safe), otherwise falls back to the in-memory single-process
|
|
48
|
+
* backend. A service opts in with one boot line:
|
|
49
|
+
* `const tickets = createEnvSseTicketStore({ ttlMs, maxTotal, maxPerOrg });`
|
|
50
|
+
*/
|
|
51
|
+
export declare function createEnvSseTicketStore(config: SseTicketStoreConfig): SseTicketStore;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { createEnvRedisClient } from './env-redis.js';
|
|
5
|
+
import { createLogger } from '../utils/logger.js';
|
|
6
|
+
const logger = createLogger('sse-ticket-store');
|
|
7
|
+
/** Redis key namespaces. Ticket → orgId; windowed issue counters bound abuse. */
|
|
8
|
+
const TICKET_KEY_PREFIX = 'sse:tk:';
|
|
9
|
+
const ORG_COUNT_PREFIX = 'sse:tkc:';
|
|
10
|
+
const TOTAL_COUNT_KEY = 'sse:tkc:__all__';
|
|
11
|
+
function newTicketId() {
|
|
12
|
+
return crypto.randomBytes(24).toString('base64url');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* In-memory backend — the original single-process behaviour. Counts LIVE
|
|
16
|
+
* (unexpired) tickets per org and evicts expired entries on a timer. Correct
|
|
17
|
+
* only within one process, so it is the fallback used when Redis isn't configured.
|
|
18
|
+
*/
|
|
19
|
+
function createInMemorySseTicketStore(config) {
|
|
20
|
+
const store = new Map();
|
|
21
|
+
const cleanup = setInterval(() => {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
for (const [id, t] of store)
|
|
24
|
+
if (now > t.expiresAt)
|
|
25
|
+
store.delete(id);
|
|
26
|
+
}, config.ttlMs);
|
|
27
|
+
cleanup.unref();
|
|
28
|
+
function countLiveForOrg(orgId, now) {
|
|
29
|
+
let count = 0;
|
|
30
|
+
for (const t of store.values())
|
|
31
|
+
if (t.orgId === orgId && t.expiresAt > now)
|
|
32
|
+
count++;
|
|
33
|
+
return count;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
async issue(orgId) {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
if (store.size >= config.maxTotal)
|
|
39
|
+
return { ok: false, reason: 'total' };
|
|
40
|
+
if (countLiveForOrg(orgId, now) >= config.maxPerOrg)
|
|
41
|
+
return { ok: false, reason: 'org' };
|
|
42
|
+
const ticket = newTicketId();
|
|
43
|
+
store.set(ticket, { orgId, expiresAt: now + config.ttlMs });
|
|
44
|
+
return { ok: true, ticket };
|
|
45
|
+
},
|
|
46
|
+
async consume(ticketId) {
|
|
47
|
+
const t = store.get(ticketId);
|
|
48
|
+
store.delete(ticketId); // single-use — consume immediately
|
|
49
|
+
if (!t || Date.now() > t.expiresAt)
|
|
50
|
+
return null;
|
|
51
|
+
return { orgId: t.orgId };
|
|
52
|
+
},
|
|
53
|
+
stop() { clearInterval(cleanup); },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Redis backend — multi-replica safe. Tickets live under a TTL'd key so any pod
|
|
58
|
+
* can redeem one; {@link SseTicketStore.consume} uses GETDEL so redemption is
|
|
59
|
+
* atomic (two pods can't both spend the same ticket). The per-org / global caps
|
|
60
|
+
* become windowed rate limits (max mints per TTL window) via INCR + first-hit
|
|
61
|
+
* EXPIRE — equivalent abuse bounding to the in-memory live-count, and it also
|
|
62
|
+
* caps churn.
|
|
63
|
+
*
|
|
64
|
+
* Fail-CLOSED by design (unlike the fail-open token-revocation reader): if Redis
|
|
65
|
+
* errors, `issue` returns a cap rejection and `consume` returns null, so a Redis
|
|
66
|
+
* outage degrades to "no live notifications" rather than handing out or
|
|
67
|
+
* accepting unvalidated tickets. SSE notifications are non-critical, so failing
|
|
68
|
+
* closed here costs only real-time delivery, never correctness.
|
|
69
|
+
*/
|
|
70
|
+
function createRedisSseTicketStore(redis, config) {
|
|
71
|
+
const ttlSec = Math.max(1, Math.ceil(config.ttlMs / 1000));
|
|
72
|
+
// Per-channel namespace so two services' ticket stores don't share keys.
|
|
73
|
+
const ns = config.keyPrefix ? `${config.keyPrefix}:` : '';
|
|
74
|
+
const ticketKey = (t) => `${ns}${TICKET_KEY_PREFIX}${t}`;
|
|
75
|
+
const orgCountKey = (o) => `${ns}${ORG_COUNT_PREFIX}${o}`;
|
|
76
|
+
const totalCountKey = `${ns}${TOTAL_COUNT_KEY}`;
|
|
77
|
+
// Windowed counter: INCR, and on the first hit of a window set the TTL so it
|
|
78
|
+
// auto-resets. Returns true while still within `limit` for this window.
|
|
79
|
+
async function withinWindow(key, limit) {
|
|
80
|
+
const n = await redis.incr(key);
|
|
81
|
+
if (n === 1)
|
|
82
|
+
await redis.expire(key, ttlSec);
|
|
83
|
+
return n <= limit;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
async issue(orgId) {
|
|
87
|
+
try {
|
|
88
|
+
if (!(await withinWindow(totalCountKey, config.maxTotal)))
|
|
89
|
+
return { ok: false, reason: 'total' };
|
|
90
|
+
if (!(await withinWindow(orgCountKey(orgId), config.maxPerOrg)))
|
|
91
|
+
return { ok: false, reason: 'org' };
|
|
92
|
+
const ticket = newTicketId();
|
|
93
|
+
await redis.set(ticketKey(ticket), orgId, 'EX', ttlSec);
|
|
94
|
+
return { ok: true, ticket };
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
logger.warn('SSE ticket issue failed (fail-closed)', { error: err instanceof Error ? err.message : String(err) });
|
|
98
|
+
return { ok: false, reason: 'total' };
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async consume(ticketId) {
|
|
102
|
+
try {
|
|
103
|
+
const orgId = await redis.getdel(ticketKey(ticketId));
|
|
104
|
+
return orgId ? { orgId } : null;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
logger.warn('SSE ticket consume failed (fail-closed)', { error: err instanceof Error ? err.message : String(err) });
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
stop() { },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build an {@link SseTicketStore} from the standard Redis env (`REDIS_URL` or
|
|
116
|
+
* `REDIS_HOST`/`REDIS_PORT`). Uses the Redis backend when configured
|
|
117
|
+
* (multi-replica safe), otherwise falls back to the in-memory single-process
|
|
118
|
+
* backend. A service opts in with one boot line:
|
|
119
|
+
* `const tickets = createEnvSseTicketStore({ ttlMs, maxTotal, maxPerOrg });`
|
|
120
|
+
*/
|
|
121
|
+
export function createEnvSseTicketStore(config) {
|
|
122
|
+
const redis = createEnvRedisClient('sse-ticket');
|
|
123
|
+
if (redis) {
|
|
124
|
+
logger.info('SSE ticket store: Redis backend (multi-replica)');
|
|
125
|
+
return createRedisSseTicketStore(redis, config);
|
|
126
|
+
}
|
|
127
|
+
logger.info('SSE ticket store: in-memory backend (single-process fallback)');
|
|
128
|
+
return createInMemorySseTicketStore(config);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3NlLXRpY2tldC1zdG9yZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9zZXJ2aWNlcy9zc2UtdGlja2V0LXN0b3JlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEMsT0FBTyxNQUFNLE1BQU0sUUFBUSxDQUFDO0FBRTVCLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ3RELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUVsRCxNQUFNLE1BQU0sR0FBRyxZQUFZLENBQUMsa0JBQWtCLENBQUMsQ0FBQztBQUVoRCxpRkFBaUY7QUFDakYsTUFBTSxpQkFBaUIsR0FBRyxTQUFTLENBQUM7QUFDcEMsTUFBTSxnQkFBZ0IsR0FBRyxVQUFVLENBQUM7QUFDcEMsTUFBTSxlQUFlLEdBQUcsaUJBQWlCLENBQUM7QUFrRDFDLFNBQVMsV0FBVztJQUNsQixPQUFPLE1BQU0sQ0FBQyxXQUFXLENBQUMsRUFBRSxDQUFDLENBQUMsUUFBUSxDQUFDLFdBQVcsQ0FBQyxDQUFDO0FBQ3RELENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsU0FBUyw0QkFBNEIsQ0FBQyxNQUE0QjtJQUVoRSxNQUFNLEtBQUssR0FBRyxJQUFJLEdBQUcsRUFBaUIsQ0FBQztJQUV2QyxNQUFNLE9BQU8sR0FBRyxXQUFXLENBQUMsR0FBRyxFQUFFO1FBQy9CLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztRQUN2QixLQUFLLE1BQU0sQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksS0FBSztZQUFFLElBQUksR0FBRyxHQUFHLENBQUMsQ0FBQyxTQUFTO2dCQUFFLEtBQUssQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDdkUsQ0FBQyxFQUFFLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUNqQixPQUFPLENBQUMsS0FBSyxFQUFF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QUM7Z0JBQ0gsTUFBTSxLQUFLLEdBQUcsTUFBTSxLQUFLLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDO2dCQUN0RCxPQUFPLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO1lBQ2xDLENBQUM7WUFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO2dCQUNiLE1BQU0sQ0FBQyxJQUFJLENBQUMseUNBQXlDLEVBQUUsRUFBRSxLQUFLLEVBQUUsR0FBRyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztnQkFDcEgsT0FBTyxJQUFJLENBQUM7WUFDZCxDQUFDO1FBQ0gsQ0FBQztRQUNELElBQUksS0FBMEUsQ0FBQztLQUNoRixDQUFDO0FBQ0osQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILE1BQU0sVUFBVSx1QkFBdUIsQ0FBQyxNQUE0QjtJQUNsRSxNQUFNLEtBQUssR0FBRyxvQkFBb0IsQ0FBVyxZQUFZLENBQUMsQ0FBQztJQUMzRCxJQUFJLEtBQUssRUFBRSxDQUFDO1FBQ1YsTUFBTSxDQUFDLElBQUksQ0FBQyxpREFBaUQsQ0FBQyxDQUFDO1FBQy9ELE9BQU8seUJBQXlCLENBQUMsS0FBSyxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQ2xELENBQUM7SUFDRCxNQUFNLENBQUMsSUFBSSxDQUFDLCtEQUErRCxDQUFDLENBQUM7SUFDN0UsT0FBTyw0QkFBNEIsQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUM5QyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCBjcnlwdG8gZnJvbSAnY3J5cHRvJztcblxuaW1wb3J0IHsgY3JlYXRlRW52UmVkaXNDbGllbnQgfSBmcm9tICcuL2Vudi1yZWRpcy5qcyc7XG5pbXBvcnQgeyBjcmVhdGVMb2dnZXIgfSBmcm9tICcuLi91dGlscy9sb2dnZXIuanMnO1xuXG5jb25zdCBsb2dnZXIgPSBjcmVhdGVMb2dnZXIoJ3NzZS10aWNrZXQtc3RvcmUnKTtcblxuLyoqIFJlZGlzIGtleSBuYW1lc3BhY2VzLiBUaWNrZXQg4oaSIG9yZ0lkOyB3aW5kb3dlZCBpc3N1ZSBjb3VudGVycyBib3VuZCBhYnVzZS4gKi9cbmNvbnN0IFRJQ0tFVF9LRVlfUFJFRklYID0gJ3NzZTp0azonO1xuY29uc3QgT1JHX0NPVU5UX1BSRUZJWCA9ICdzc2U6dGtjOic7XG5jb25zdCBUT1RBTF9DT1VOVF9LRVkgPSAnc3NlOnRrYzpfX2FsbF9fJztcblxuLyoqIFJlc3VsdCBvZiB7QGxpbmsgU3NlVGlja2V0U3RvcmUuaXNzdWV9LiAqL1xuZXhwb3J0IHR5cGUgU3NlVGlja2V0SXNzdWVSZXN1bHQgPVxuICB8IHsgb2s6IHRydWU7IHRpY2tldDogc3RyaW5nIH1cbiAgfCB7IG9rOiBmYWxzZTsgcmVhc29uOiAndG90YWwnIHwgJ29yZycgfTtcblxuLyoqXG4gKiBTdG9yZSBmb3Igc2hvcnQtbGl2ZWQsIHNpbmdsZS11c2UgU1NFIGF1dGggdGlja2V0cy4gQSBjbGllbnQgZXhjaGFuZ2VzIGl0cyBKV1RcbiAqIGZvciBhIHRpY2tldCAoc28gdGhlIHRva2VuIG5ldmVyIGxhbmRzIGluIGFuIEV2ZW50U291cmNlIHF1ZXJ5IHN0cmluZyAvIGFjY2Vzc1xuICogbG9nKSwgdGhlbiByZWRlZW1zIGl0IG9uIHRoZSBTU0UgYEdFVGAuXG4gKlxuICogVGhlIHN0b3JlIGlzIHRoZSBtdWx0aS1yZXBsaWNhIGNvcnJlY3RuZXNzIGJvdW5kYXJ5OiBhIHRpY2tldCBtaW50ZWQgb24gcG9kIEFcbiAqIE1VU1QgYmUgcmVkZWVtYWJsZSBvbiBwb2QgQi4gVGhlIFJlZGlzIGJhY2tlbmQgbWFrZXMgdGhhdCB0cnVlOyB0aGUgaW4tbWVtb3J5XG4gKiBiYWNrZW5kIGlzIGEgc2luZ2xlLXByb2Nlc3MgZmFsbGJhY2sgZm9yIGRldiAvIHNpbmdsZS1yZXBsaWNhIGluc3RhbGxzLlxuICovXG5leHBvcnQgaW50ZXJmYWNlIFNzZVRpY2tldFN0b3JlIHtcbiAgLyoqIE1pbnQgYSB0aWNrZXQgZm9yIGBvcmdJZGAsIG9yIHJlamVjdCB3aGVuIGEgcmF0ZSB3aW5kb3cgaXMgc2F0dXJhdGVkLiAqL1xuICBpc3N1ZShvcmdJZDogc3RyaW5nKTogUHJvbWlzZTxTc2VUaWNrZXRJc3N1ZVJlc3VsdD47XG4gIC8qKiBBdG9taWNhbGx5IHJlZGVlbSBhIHRpY2tldCBleGFjdGx5IG9uY2U7IHJldHVybnMgaXRzIG9yZyBvciBudWxsIGlmIGludmFsaWQvZXhwaXJlZC9zcGVudC4gKi9cbiAgY29uc3VtZSh0aWNrZXRJZDogc3RyaW5nKTogUHJvbWlzZTx7IG9yZ0lkOiBzdHJpbmcgfSB8IG51bGw+O1xuICAvKiogUmVsZWFzZSB0aW1lcnMvcmVzb3VyY2VzLiBJbi1tZW1vcnkgY2xlYXJzIGl0cyBzd2VlcCBpbnRlcnZhbDsgUmVkaXMgaXMgYSBuby1vcC4gKi9cbiAgc3RvcCgpOiB2b2lkO1xufVxuXG4vKiogQ2FwcyArIFRUTCBmb3IgdGhlIHRpY2tldCBzdG9yZS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgU3NlVGlja2V0U3RvcmVDb25maWcge1xuICAvKiogVGlja2V0IGxpZmV0aW1lIOKAlCBsb25nIGVub3VnaCBmb3IgdGhlIGNsaWVudCB0byBvcGVuIHRoZSBFdmVudFNvdXJjZS4gKi9cbiAgdHRsTXM6IG51bWJlcjtcbiAgLyoqIEhhcmQgY2FwIG9uIHRpY2tldHMgbWludGVkIHBlciBUVEwgd2luZG93IGFjcm9zcyBhbGwgb3JncyAoYWJ1c2UgYm91bmQpLiAqL1xuICBtYXhUb3RhbDogbnVtYmVyO1xuICAvKiogUGVyLW9yZyBjYXAgb24gdGlja2V0cyBtaW50ZWQgcGVyIFRUTCB3aW5kb3cgKHNpbmdsZS10ZW5hbnQgYWJ1c2UgYm91bmQpLiAqL1xuICBtYXhQZXJPcmc6IG51bWJlcjtcbiAgLyoqXG4gICAqIE9wdGlvbmFsIFJlZGlzIGtleSBuYW1lc3BhY2Ugc28gaW5kZXBlbmRlbnQgY2hhbm5lbHMgKGUuZy4gbWVzc2FnZVxuICAgKiBub3RpZmljYXRpb25zIHZzLiByZXBvcnRpbmcgZXhlY3V0aW9uLXN0YXR1cykgZG9uJ3Qgc2hhcmUgb25lIHRpY2tldFxuICAgKiBrZXlzcGFjZSDigJQgYSB0aWNrZXQgbWludGVkIGZvciBvbmUgY2hhbm5lbCBtdXN0IG5vdCBiZSByZWRlZW1hYmxlIG9uXG4gICAqIGFub3RoZXIuIE9taXQgZm9yIHRoZSBkZWZhdWx0IChzaGFyZWQpIG5hbWVzcGFjZS5cbiAgICovXG4gIGtleVByZWZpeD86IHN0cmluZztcbn1cblxuLyoqIE1pbmltYWwgaW9yZWRpcyBzdXJmYWNlIHRoaXMgc3RvcmUgbmVlZHMgKEdFVERFTCByZXF1aXJlcyBSZWRpcyDiiaUgNi4yKS4gKi9cbmludGVyZmFjZSBTc2VSZWRpcyB7XG4gIGluY3Ioa2V5OiBzdHJpbmcpOiBQcm9taXNlPG51bWJlcj47XG4gIGV4cGlyZShrZXk6IHN0cmluZywgc2Vjb25kczogbnVtYmVyKTogUHJvbWlzZTx1bmtub3duPjtcbiAgc2V0KGtleTogc3RyaW5nLCB2YWx1ZTogc3RyaW5nLCAuLi5hcmdzOiB1bmtub3duW10pOiBQcm9taXNlPHVua25vd24+O1xuICBnZXRkZWwoa2V5OiBzdHJpbmcpOiBQcm9taXNlPHN0cmluZyB8IG51bGw+O1xufVxuXG5mdW5jdGlvbiBuZXdUaWNrZXRJZCgpOiBzdHJpbmcge1xuICByZXR1cm4gY3J5cHRvLnJhbmRvbUJ5dGVzKDI0KS50b1N0cmluZygnYmFzZTY0dXJsJyk7XG59XG5cbi8qKlxuICogSW4tbWVtb3J5IGJhY2tlbmQg4oCUIHRoZSBvcmlnaW5hbCBzaW5nbGUtcHJvY2VzcyBiZWhhdmlvdXIuIENvdW50cyBMSVZFXG4gKiAodW5leHBpcmVkKSB0aWNrZXRzIHBlciBvcmcgYW5kIGV2aWN0cyBleHBpcmVkIGVudHJpZXMgb24gYSB0aW1lci4gQ29ycmVjdFxuICogb25seSB3aXRoaW4gb25lIHByb2Nlc3MsIHNvIGl0IGlzIHRoZSBmYWxsYmFjayB1c2VkIHdoZW4gUmVkaXMgaXNuJ3QgY29uZmlndXJlZC5cbiAqL1xuZnVuY3Rpb24gY3JlYXRlSW5NZW1vcnlTc2VUaWNrZXRTdG9yZShjb25maWc6IFNzZVRpY2tldFN0b3JlQ29uZmlnKTogU3NlVGlja2V0U3RvcmUge1xuICBpbnRlcmZhY2UgRW50cnkgeyBvcmdJZDogc3RyaW5nOyBleHBpcmVzQXQ6IG51bWJlciB9XG4gIGNvbnN0IHN0b3JlID0gbmV3IE1hcDxzdHJpbmcsIEVudHJ5PigpO1xuXG4gIGNvbnN0IGNsZWFudXAgPSBzZXRJbnRlcnZhbCgoKSA9PiB7XG4gICAgY29uc3Qgbm93ID0gRGF0ZS5ub3coKTtcbiAgICBmb3IgKGNvbnN0IFtpZCwgdF0gb2Ygc3RvcmUpIGlmIChub3cgPiB0LmV4cGlyZXNBdCkgc3RvcmUuZGVsZXRlKGlkKTtcbiAgfSwgY29uZmlnLnR0bE1zKTtcbiAgY2xlYW51cC51bnJlZigpO1xuXG4gIGZ1bmN0aW9uIGNvdW50TGl2ZUZvck9yZyhvcmdJZDogc3RyaW5nLCBub3c6IG51bWJlcik6IG51bWJlciB7XG4gICAgbGV0IGNvdW50ID0gMDtcbiAgICBmb3IgKGNvbnN0IHQgb2Ygc3RvcmUudmFsdWVzKCkpIGlmICh0Lm9yZ0lkID09PSBvcmdJZCAmJiB0LmV4cGlyZXNBdCA+IG5vdykgY291bnQrKztcbiAgICByZXR1cm4gY291bnQ7XG4gIH1cblxuICByZXR1cm4ge1xuICAgIGFzeW5jIGlzc3VlKG9yZ0lkOiBzdHJpbmcpOiBQcm9taXNlPFNzZVRpY2tldElzc3VlUmVzdWx0PiB7XG4gICAgICBjb25zdCBub3cgPSBEYXRlLm5vdygpO1xuICAgICAgaWYgKHN0b3JlLnNpemUgPj0gY29uZmlnLm1heFRvdGFsKSByZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogJ3RvdGFsJyB9O1xuICAgICAgaWYgKGNvdW50TGl2ZUZvck9yZyhvcmdJZCwgbm93KSA+PSBjb25maWcubWF4UGVyT3JnKSByZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogJ29yZycgfTtcbiAgICAgIGNvbnN0IHRpY2tldCA9IG5ld1RpY2tldElkKCk7XG4gICAgICBzdG9yZS5zZXQodGlja2V0LCB7IG9yZ0lkLCBleHBpcmVzQXQ6IG5vdyArIGNvbmZpZy50dGxNcyB9KTtcbiAgICAgIHJldHVybiB7IG9rOiB0cnVlLCB0aWNrZXQgfTtcbiAgICB9LFxuICAgIGFzeW5jIGNvbnN1bWUodGlja2V0SWQ6IHN0cmluZyk6IFByb21pc2U8eyBvcmdJZDogc3RyaW5nIH0gfCBudWxsPiB7XG4gICAgICBjb25zdCB0ID0gc3RvcmUuZ2V0KHRpY2tldElkKTtcbiAgICAgIHN0b3JlLmRlbGV0ZSh0aWNrZXRJZCk7IC8vIHNpbmdsZS11c2Ug4oCUIGNvbnN1bWUgaW1tZWRpYXRlbHlcbiAgICAgIGlmICghdCB8fCBEYXRlLm5vdygpID4gdC5leHBpcmVzQXQpIHJldHVybiBudWxsO1xuICAgICAgcmV0dXJuIHsgb3JnSWQ6IHQub3JnSWQgfTtcbiAgICB9LFxuICAgIHN0b3AoKTogdm9pZCB7IGNsZWFySW50ZXJ2YWwoY2xlYW51cCk7IH0sXG4gIH07XG59XG5cbi8qKlxuICogUmVkaXMgYmFja2VuZCDigJQgbXVsdGktcmVwbGljYSBzYWZlLiBUaWNrZXRzIGxpdmUgdW5kZXIgYSBUVEwnZCBrZXkgc28gYW55IHBvZFxuICogY2FuIHJlZGVlbSBvbmU7IHtAbGluayBTc2VUaWNrZXRTdG9yZS5jb25zdW1lfSB1c2VzIEdFVERFTCBzbyByZWRlbXB0aW9uIGlzXG4gKiBhdG9taWMgKHR3byBwb2RzIGNhbid0IGJvdGggc3BlbmQgdGhlIHNhbWUgdGlja2V0KS4gVGhlIHBlci1vcmcgLyBnbG9iYWwgY2Fwc1xuICogYmVjb21lIHdpbmRvd2VkIHJhdGUgbGltaXRzIChtYXggbWludHMgcGVyIFRUTCB3aW5kb3cpIHZpYSBJTkNSICsgZmlyc3QtaGl0XG4gKiBFWFBJUkUg4oCUIGVxdWl2YWxlbnQgYWJ1c2UgYm91bmRpbmcgdG8gdGhlIGluLW1lbW9yeSBsaXZlLWNvdW50LCBhbmQgaXQgYWxzb1xuICogY2FwcyBjaHVybi5cbiAqXG4gKiBGYWlsLUNMT1NFRCBieSBkZXNpZ24gKHVubGlrZSB0aGUgZmFpbC1vcGVuIHRva2VuLXJldm9jYXRpb24gcmVhZGVyKTogaWYgUmVkaXNcbiAqIGVycm9ycywgYGlzc3VlYCByZXR1cm5zIGEgY2FwIHJlamVjdGlvbiBhbmQgYGNvbnN1bWVgIHJldHVybnMgbnVsbCwgc28gYSBSZWRpc1xuICogb3V0YWdlIGRlZ3JhZGVzIHRvIFwibm8gbGl2ZSBub3RpZmljYXRpb25zXCIgcmF0aGVyIHRoYW4gaGFuZGluZyBvdXQgb3JcbiAqIGFjY2VwdGluZyB1bnZhbGlkYXRlZCB0aWNrZXRzLiBTU0Ugbm90aWZpY2F0aW9ucyBhcmUgbm9uLWNyaXRpY2FsLCBzbyBmYWlsaW5nXG4gKiBjbG9zZWQgaGVyZSBjb3N0cyBvbmx5IHJlYWwtdGltZSBkZWxpdmVyeSwgbmV2ZXIgY29ycmVjdG5lc3MuXG4gKi9cbmZ1bmN0aW9uIGNyZWF0ZVJlZGlzU3NlVGlja2V0U3RvcmUocmVkaXM6IFNzZVJlZGlzLCBjb25maWc6IFNzZVRpY2tldFN0b3JlQ29uZmlnKTogU3NlVGlja2V0U3RvcmUge1xuICBjb25zdCB0dGxTZWMgPSBNYXRoLm1heCgxLCBNYXRoLmNlaWwoY29uZmlnLnR0bE1zIC8gMTAwMCkpO1xuICAvLyBQZXItY2hhbm5lbCBuYW1lc3BhY2Ugc28gdHdvIHNlcnZpY2VzJyB0aWNrZXQgc3RvcmVzIGRvbid0IHNoYXJlIGtleXMuXG4gIGNvbnN0IG5zID0gY29uZmlnLmtleVByZWZpeCA/IGAke2NvbmZpZy5rZXlQcmVmaXh9OmAgOiAnJztcbiAgY29uc3QgdGlja2V0S2V5ID0gKHQ6IHN0cmluZykgPT4gYCR7bnN9JHtUSUNLRVRfS0VZX1BSRUZJWH0ke3R9YDtcbiAgY29uc3Qgb3JnQ291bnRLZXkgPSAobzogc3RyaW5nKSA9PiBgJHtuc30ke09SR19DT1VOVF9QUkVGSVh9JHtvfWA7XG4gIGNvbnN0IHRvdGFsQ291bnRLZXkgPSBgJHtuc30ke1RPVEFMX0NPVU5UX0tFWX1gO1xuXG4gIC8vIFdpbmRvd2VkIGNvdW50ZXI6IElOQ1IsIGFuZCBvbiB0aGUgZmlyc3QgaGl0IG9mIGEgd2luZG93IHNldCB0aGUgVFRMIHNvIGl0XG4gIC8vIGF1dG8tcmVzZXRzLiBSZXR1cm5zIHRydWUgd2hpbGUgc3RpbGwgd2l0aGluIGBsaW1pdGAgZm9yIHRoaXMgd2luZG93LlxuICBhc3luYyBmdW5jdGlvbiB3aXRoaW5XaW5kb3coa2V5OiBzdHJpbmcsIGxpbWl0OiBudW1iZXIpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgICBjb25zdCBuID0gYXdhaXQgcmVkaXMuaW5jcihrZXkpO1xuICAgIGlmIChuID09PSAxKSBhd2FpdCByZWRpcy5leHBpcmUoa2V5LCB0dGxTZWMpO1xuICAgIHJldHVybiBuIDw9IGxpbWl0O1xuICB9XG5cbiAgcmV0dXJuIHtcbiAgICBhc3luYyBpc3N1ZShvcmdJZDogc3RyaW5nKTogUHJvbWlzZTxTc2VUaWNrZXRJc3N1ZVJlc3VsdD4ge1xuICAgICAgdHJ5IHtcbiAgICAgICAgaWYgKCEoYXdhaXQgd2l0aGluV2luZG93KHRvdGFsQ291bnRLZXksIGNvbmZpZy5tYXhUb3RhbCkpKSByZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogJ3RvdGFsJyB9O1xuICAgICAgICBpZiAoIShhd2FpdCB3aXRoaW5XaW5kb3cob3JnQ291bnRLZXkob3JnSWQpLCBjb25maWcubWF4UGVyT3JnKSkpIHJldHVybiB7IG9rOiBmYWxzZSwgcmVhc29uOiAnb3JnJyB9O1xuICAgICAgICBjb25zdCB0aWNrZXQgPSBuZXdUaWNrZXRJZCgpO1xuICAgICAgICBhd2FpdCByZWRpcy5zZXQodGlja2V0S2V5KHRpY2tldCksIG9yZ0lkLCAnRVgnLCB0dGxTZWMpO1xuICAgICAgICByZXR1cm4geyBvazogdHJ1ZSwgdGlja2V0IH07XG4gICAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgbG9nZ2VyLndhcm4oJ1NTRSB0aWNrZXQgaXNzdWUgZmFpbGVkIChmYWlsLWNsb3NlZCknLCB7IGVycm9yOiBlcnIgaW5zdGFuY2VvZiBFcnJvciA/IGVyci5tZXNzYWdlIDogU3RyaW5nKGVycikgfSk7XG4gICAgICAgIHJldHVybiB7IG9rOiBmYWxzZSwgcmVhc29uOiAndG90YWwnIH07XG4gICAgICB9XG4gICAgfSxcbiAgICBhc3luYyBjb25zdW1lKHRpY2tldElkOiBzdHJpbmcpOiBQcm9taXNlPHsgb3JnSWQ6IHN0cmluZyB9IHwgbnVsbD4ge1xuICAgICAgdHJ5IHtcbiAgICAgICAgY29uc3Qgb3JnSWQgPSBhd2FpdCByZWRpcy5nZXRkZWwodGlja2V0S2V5KHRpY2tldElkKSk7XG4gICAgICAgIHJldHVybiBvcmdJZCA/IHsgb3JnSWQgfSA6IG51bGw7XG4gICAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgbG9nZ2VyLndhcm4oJ1NTRSB0aWNrZXQgY29uc3VtZSBmYWlsZWQgKGZhaWwtY2xvc2VkKScsIHsgZXJyb3I6IGVyciBpbnN0YW5jZW9mIEVycm9yID8gZXJyLm1lc3NhZ2UgOiBTdHJpbmcoZXJyKSB9KTtcbiAgICAgICAgcmV0dXJuIG51bGw7XG4gICAgICB9XG4gICAgfSxcbiAgICBzdG9wKCk6IHZvaWQgeyAvKiBSZWRpcyBrZXlzIGV4cGlyZSBvbiB0aGVpciBvd24gVFRMIOKAlCBub3RoaW5nIHRvIHJlbGVhc2UuICovIH0sXG4gIH07XG59XG5cbi8qKlxuICogQnVpbGQgYW4ge0BsaW5rIFNzZVRpY2tldFN0b3JlfSBmcm9tIHRoZSBzdGFuZGFyZCBSZWRpcyBlbnYgKGBSRURJU19VUkxgIG9yXG4gKiBgUkVESVNfSE9TVGAvYFJFRElTX1BPUlRgKS4gVXNlcyB0aGUgUmVkaXMgYmFja2VuZCB3aGVuIGNvbmZpZ3VyZWRcbiAqIChtdWx0aS1yZXBsaWNhIHNhZmUpLCBvdGhlcndpc2UgZmFsbHMgYmFjayB0byB0aGUgaW4tbWVtb3J5IHNpbmdsZS1wcm9jZXNzXG4gKiBiYWNrZW5kLiBBIHNlcnZpY2Ugb3B0cyBpbiB3aXRoIG9uZSBib290IGxpbmU6XG4gKiAgIGBjb25zdCB0aWNrZXRzID0gY3JlYXRlRW52U3NlVGlja2V0U3RvcmUoeyB0dGxNcywgbWF4VG90YWwsIG1heFBlck9yZyB9KTtgXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVFbnZTc2VUaWNrZXRTdG9yZShjb25maWc6IFNzZVRpY2tldFN0b3JlQ29uZmlnKTogU3NlVGlja2V0U3RvcmUge1xuICBjb25zdCByZWRpcyA9IGNyZWF0ZUVudlJlZGlzQ2xpZW50PFNzZVJlZGlzPignc3NlLXRpY2tldCcpO1xuICBpZiAocmVkaXMpIHtcbiAgICBsb2dnZXIuaW5mbygnU1NFIHRpY2tldCBzdG9yZTogUmVkaXMgYmFja2VuZCAobXVsdGktcmVwbGljYSknKTtcbiAgICByZXR1cm4gY3JlYXRlUmVkaXNTc2VUaWNrZXRTdG9yZShyZWRpcywgY29uZmlnKTtcbiAgfVxuICBsb2dnZXIuaW5mbygnU1NFIHRpY2tldCBzdG9yZTogaW4tbWVtb3J5IGJhY2tlbmQgKHNpbmdsZS1wcm9jZXNzIGZhbGxiYWNrKScpO1xuICByZXR1cm4gY3JlYXRlSW5NZW1vcnlTc2VUaWNrZXRTdG9yZShjb25maWcpO1xufVxuIl19
|