@pipeline-builder/api-core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +98 -0
  3. package/lib/constants/ai-providers.d.ts +52 -0
  4. package/lib/constants/ai-providers.js +129 -0
  5. package/lib/constants/http-status.d.ts +20 -0
  6. package/lib/constants/http-status.js +26 -0
  7. package/lib/constants/index.d.ts +3 -0
  8. package/lib/constants/index.js +6 -0
  9. package/lib/constants/time.d.ts +10 -0
  10. package/lib/constants/time.js +13 -0
  11. package/lib/errors/app-errors.d.ts +30 -0
  12. package/lib/errors/app-errors.js +53 -0
  13. package/lib/errors/index.d.ts +1 -0
  14. package/lib/errors/index.js +4 -0
  15. package/lib/helpers/access-helpers.d.ts +63 -0
  16. package/lib/helpers/access-helpers.js +82 -0
  17. package/lib/helpers/crud-helpers.d.ts +9 -0
  18. package/lib/helpers/crud-helpers.js +23 -0
  19. package/lib/helpers/index.d.ts +6 -0
  20. package/lib/helpers/index.js +9 -0
  21. package/lib/helpers/org-hierarchy-http.d.ts +74 -0
  22. package/lib/helpers/org-hierarchy-http.js +144 -0
  23. package/lib/helpers/org-hierarchy.d.ts +57 -0
  24. package/lib/helpers/org-hierarchy.js +110 -0
  25. package/lib/helpers/restore-helpers.d.ts +49 -0
  26. package/lib/helpers/restore-helpers.js +78 -0
  27. package/lib/helpers/sse-helpers.d.ts +20 -0
  28. package/lib/helpers/sse-helpers.js +45 -0
  29. package/lib/index.d.ts +58 -0
  30. package/lib/index.js +71 -0
  31. package/lib/middleware/auth.d.ts +208 -0
  32. package/lib/middleware/auth.js +588 -0
  33. package/lib/middleware/index.d.ts +3 -0
  34. package/lib/middleware/index.js +6 -0
  35. package/lib/middleware/mongo-sanitize.d.ts +32 -0
  36. package/lib/middleware/mongo-sanitize.js +100 -0
  37. package/lib/middleware/step-up.d.ts +45 -0
  38. package/lib/middleware/step-up.js +138 -0
  39. package/lib/openapi/extend-zod.d.ts +1 -0
  40. package/lib/openapi/extend-zod.js +6 -0
  41. package/lib/openapi/index.d.ts +2 -0
  42. package/lib/openapi/index.js +5 -0
  43. package/lib/openapi/registry.d.ts +17 -0
  44. package/lib/openapi/registry.js +38 -0
  45. package/lib/openapi/routes/ask-routes.d.ts +7 -0
  46. package/lib/openapi/routes/ask-routes.js +62 -0
  47. package/lib/openapi/routes/billing-routes.d.ts +1 -0
  48. package/lib/openapi/routes/billing-routes.js +66 -0
  49. package/lib/openapi/routes/image-registry-routes.d.ts +7 -0
  50. package/lib/openapi/routes/image-registry-routes.js +98 -0
  51. package/lib/openapi/routes/index.d.ts +5 -0
  52. package/lib/openapi/routes/index.js +28 -0
  53. package/lib/openapi/routes/message-routes.d.ts +1 -0
  54. package/lib/openapi/routes/message-routes.js +105 -0
  55. package/lib/openapi/routes/pipeline-routes.d.ts +1 -0
  56. package/lib/openapi/routes/pipeline-routes.js +105 -0
  57. package/lib/openapi/routes/pipeline-template-routes.d.ts +1 -0
  58. package/lib/openapi/routes/pipeline-template-routes.js +61 -0
  59. package/lib/openapi/routes/plugin-routes.d.ts +1 -0
  60. package/lib/openapi/routes/plugin-routes.js +96 -0
  61. package/lib/openapi/routes/quota-routes.d.ts +1 -0
  62. package/lib/openapi/routes/quota-routes.js +62 -0
  63. package/lib/openapi/schema-registry.d.ts +25 -0
  64. package/lib/openapi/schema-registry.js +91 -0
  65. package/lib/routes/health.d.ts +41 -0
  66. package/lib/routes/health.js +82 -0
  67. package/lib/routes/index.d.ts +1 -0
  68. package/lib/routes/index.js +4 -0
  69. package/lib/services/audit-spool.d.ts +80 -0
  70. package/lib/services/audit-spool.js +172 -0
  71. package/lib/services/cache-service.d.ts +124 -0
  72. package/lib/services/cache-service.js +276 -0
  73. package/lib/services/circuit-breaker.d.ts +45 -0
  74. package/lib/services/circuit-breaker.js +138 -0
  75. package/lib/services/compliance-client.d.ts +46 -0
  76. package/lib/services/compliance-client.js +128 -0
  77. package/lib/services/compliance-event-subscriber.d.ts +16 -0
  78. package/lib/services/compliance-event-subscriber.js +72 -0
  79. package/lib/services/durable-event-bus.d.ts +96 -0
  80. package/lib/services/durable-event-bus.js +204 -0
  81. package/lib/services/entity-events.d.ts +48 -0
  82. package/lib/services/entity-events.js +79 -0
  83. package/lib/services/env-redis.d.ts +44 -0
  84. package/lib/services/env-redis.js +94 -0
  85. package/lib/services/http-client.d.ts +143 -0
  86. package/lib/services/http-client.js +335 -0
  87. package/lib/services/index.d.ts +18 -0
  88. package/lib/services/index.js +20 -0
  89. package/lib/services/leader-lock.d.ts +45 -0
  90. package/lib/services/leader-lock.js +85 -0
  91. package/lib/services/quota.d.ts +130 -0
  92. package/lib/services/quota.js +264 -0
  93. package/lib/services/remote-audit-client.d.ts +160 -0
  94. package/lib/services/remote-audit-client.js +390 -0
  95. package/lib/services/retry-strategy.d.ts +74 -0
  96. package/lib/services/retry-strategy.js +118 -0
  97. package/lib/services/scheduler.d.ts +41 -0
  98. package/lib/services/scheduler.js +86 -0
  99. package/lib/services/service-boot.d.ts +20 -0
  100. package/lib/services/service-boot.js +28 -0
  101. package/lib/services/sse-ticket-store.d.ts +51 -0
  102. package/lib/services/sse-ticket-store.js +130 -0
  103. package/lib/services/token-revocation.d.ts +52 -0
  104. package/lib/services/token-revocation.js +120 -0
  105. package/lib/testing/tier-mock.d.ts +44 -0
  106. package/lib/testing/tier-mock.js +65 -0
  107. package/lib/types/audit-events.d.ts +69 -0
  108. package/lib/types/audit-events.js +4 -0
  109. package/lib/types/catalog-metadata.d.ts +36 -0
  110. package/lib/types/catalog-metadata.js +21 -0
  111. package/lib/types/common.d.ts +255 -0
  112. package/lib/types/common.js +41 -0
  113. package/lib/types/error-codes.d.ts +54 -0
  114. package/lib/types/error-codes.js +107 -0
  115. package/lib/types/feature-flags.d.ts +36 -0
  116. package/lib/types/feature-flags.js +128 -0
  117. package/lib/types/http.d.ts +27 -0
  118. package/lib/types/http.js +4 -0
  119. package/lib/types/index.d.ts +11 -0
  120. package/lib/types/index.js +14 -0
  121. package/lib/types/permissions.d.ts +65 -0
  122. package/lib/types/permissions.js +131 -0
  123. package/lib/types/pipeline-template.d.ts +30 -0
  124. package/lib/types/pipeline-template.js +13 -0
  125. package/lib/types/pipeline.d.ts +59 -0
  126. package/lib/types/pipeline.js +31 -0
  127. package/lib/types/quota-tiers.d.ts +79 -0
  128. package/lib/types/quota-tiers.js +221 -0
  129. package/lib/types/visibility.d.ts +20 -0
  130. package/lib/types/visibility.js +22 -0
  131. package/lib/utils/alias-resolver.d.ts +37 -0
  132. package/lib/utils/alias-resolver.js +68 -0
  133. package/lib/utils/audit.d.ts +15 -0
  134. package/lib/utils/audit.js +39 -0
  135. package/lib/utils/aws-scrub.d.ts +11 -0
  136. package/lib/utils/aws-scrub.js +67 -0
  137. package/lib/utils/compliance-attributes.d.ts +6 -0
  138. package/lib/utils/compliance-attributes.js +69 -0
  139. package/lib/utils/concurrency.d.ts +9 -0
  140. package/lib/utils/concurrency.js +29 -0
  141. package/lib/utils/crash-handlers.d.ts +18 -0
  142. package/lib/utils/crash-handlers.js +36 -0
  143. package/lib/utils/env.d.ts +28 -0
  144. package/lib/utils/env.js +54 -0
  145. package/lib/utils/headers.d.ts +18 -0
  146. package/lib/utils/headers.js +21 -0
  147. package/lib/utils/identity.d.ts +40 -0
  148. package/lib/utils/identity.js +51 -0
  149. package/lib/utils/index.d.ts +17 -0
  150. package/lib/utils/index.js +20 -0
  151. package/lib/utils/logger.d.ts +38 -0
  152. package/lib/utils/logger.js +174 -0
  153. package/lib/utils/metric-emitter.d.ts +36 -0
  154. package/lib/utils/metric-emitter.js +32 -0
  155. package/lib/utils/object.d.ts +13 -0
  156. package/lib/utils/object.js +18 -0
  157. package/lib/utils/params.d.ts +150 -0
  158. package/lib/utils/params.js +206 -0
  159. package/lib/utils/response.d.ts +117 -0
  160. package/lib/utils/response.js +223 -0
  161. package/lib/utils/safe-require.d.ts +13 -0
  162. package/lib/utils/safe-require.js +19 -0
  163. package/lib/utils/secret-encryption.d.ts +176 -0
  164. package/lib/utils/secret-encryption.js +374 -0
  165. package/lib/utils/ssrf.d.ts +30 -0
  166. package/lib/utils/ssrf.js +106 -0
  167. package/lib/validation/ai-schemas.d.ts +56 -0
  168. package/lib/validation/ai-schemas.js +78 -0
  169. package/lib/validation/common-schemas.d.ts +133 -0
  170. package/lib/validation/common-schemas.js +94 -0
  171. package/lib/validation/index.d.ts +7 -0
  172. package/lib/validation/index.js +10 -0
  173. package/lib/validation/message-schemas.d.ts +160 -0
  174. package/lib/validation/message-schemas.js +156 -0
  175. package/lib/validation/middleware.d.ts +72 -0
  176. package/lib/validation/middleware.js +65 -0
  177. package/lib/validation/pipeline-schemas.d.ts +181 -0
  178. package/lib/validation/pipeline-schemas.js +101 -0
  179. package/lib/validation/pipeline-template-schemas.d.ts +155 -0
  180. package/lib/validation/pipeline-template-schemas.js +66 -0
  181. package/lib/validation/plugin-schemas.d.ts +171 -0
  182. package/lib/validation/plugin-schemas.js +81 -0
  183. package/openapi.yaml +292 -0
  184. package/package.json +167 -0
@@ -0,0 +1,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