@pipeline-builder/api-core 4.1.2 → 4.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/lib/errors/app-errors.d.ts +0 -4
- package/lib/errors/app-errors.js +1 -8
- package/lib/helpers/restore-helpers.d.ts +3 -3
- package/lib/helpers/restore-helpers.js +4 -4
- package/lib/helpers/sse-helpers.d.ts +8 -1
- package/lib/helpers/sse-helpers.js +11 -3
- package/lib/middleware/auth.d.ts +174 -43
- package/lib/middleware/auth.js +384 -179
- package/lib/middleware/index.d.ts +1 -0
- package/lib/middleware/index.js +2 -1
- package/lib/middleware/route-table.d.ts +104 -0
- package/lib/middleware/route-table.js +230 -0
- package/lib/middleware/step-up.d.ts +48 -16
- package/lib/middleware/step-up.js +74 -36
- package/lib/services/api-key-exchange.d.ts +25 -0
- package/lib/services/api-key-exchange.js +219 -0
- package/lib/services/audit-spool.d.ts +2 -3
- package/lib/services/audit-spool.js +3 -4
- package/lib/services/cache-service.d.ts +65 -21
- package/lib/services/cache-service.js +173 -78
- package/lib/services/compliance-event-subscriber.d.ts +5 -4
- package/lib/services/compliance-event-subscriber.js +8 -6
- package/lib/services/durable-event-bus.d.ts +22 -39
- package/lib/services/durable-event-bus.js +109 -47
- package/lib/services/env-redis.d.ts +114 -35
- package/lib/services/env-redis.js +215 -73
- package/lib/services/http-client.d.ts +10 -15
- package/lib/services/http-client.js +32 -27
- package/lib/services/index.d.ts +3 -0
- package/lib/services/index.js +4 -1
- package/lib/services/jwks-cache.d.ts +102 -0
- package/lib/services/jwks-cache.js +222 -0
- package/lib/services/leader-lock.d.ts +6 -3
- package/lib/services/leader-lock.js +20 -5
- package/lib/services/quota.d.ts +44 -10
- package/lib/services/quota.js +95 -51
- package/lib/services/remote-audit-client.d.ts +1 -1
- package/lib/services/remote-audit-client.js +32 -1
- package/lib/services/scheduler.d.ts +2 -3
- package/lib/services/scheduler.js +2 -2
- package/lib/services/service-boot.d.ts +4 -2
- package/lib/services/service-boot.js +7 -3
- package/lib/services/service-keys.d.ts +83 -0
- package/lib/services/service-keys.js +354 -0
- package/lib/services/sse-ticket-store.d.ts +55 -22
- package/lib/services/sse-ticket-store.js +122 -79
- package/lib/services/token-revocation.d.ts +1 -1
- package/lib/services/token-revocation.js +31 -19
- package/lib/testing/route-coverage.d.ts +64 -0
- package/lib/testing/route-coverage.js +183 -0
- package/lib/testing/service-tokens.d.ts +66 -0
- package/lib/testing/service-tokens.js +137 -0
- package/lib/testing/tier-mock.d.ts +2 -2
- package/lib/testing/tier-mock.js +3 -3
- package/lib/testing/user-tokens.d.ts +75 -0
- package/lib/testing/user-tokens.js +115 -0
- package/lib/types/catalog-metadata.d.ts +0 -2
- package/lib/types/catalog-metadata.js +1 -3
- package/lib/types/common.d.ts +103 -4
- package/lib/types/common.js +11 -1
- package/lib/types/error-codes.d.ts +15 -0
- package/lib/types/error-codes.js +22 -1
- package/lib/types/permissions.d.ts +35 -1
- package/lib/types/permissions.js +70 -2
- package/lib/utils/api-key.d.ts +34 -0
- package/lib/utils/api-key.js +75 -0
- package/lib/utils/env.d.ts +0 -2
- package/lib/utils/env.js +1 -6
- package/lib/utils/index.d.ts +3 -0
- package/lib/utils/index.js +4 -1
- package/lib/utils/jwk.d.ts +98 -0
- package/lib/utils/jwk.js +139 -0
- package/lib/utils/logger.d.ts +4 -1
- package/lib/utils/logger.js +1 -22
- package/lib/utils/secret-encryption.d.ts +7 -2
- package/lib/utils/secret-encryption.js +47 -4
- package/lib/utils/secret-rotation.d.ts +17 -0
- package/lib/utils/secret-rotation.js +54 -0
- package/lib/validation/common-schemas.js +5 -4
- package/package.json +20 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { JWKS_PATH, isJwksDocument, publicKeyFromJwk, } from '../utils/jwk.js';
|
|
4
|
+
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
import { emitCounter } from '../utils/metric-emitter.js';
|
|
6
|
+
const logger = createLogger('jwks-cache');
|
|
7
|
+
/** How long a fetched key set is served before a refresh is due. */
|
|
8
|
+
export const JWKS_REFRESH_INTERVAL_MS = 600_000; // 10 minutes
|
|
9
|
+
/** How long a fetch FAILURE suppresses further attempts. */
|
|
10
|
+
export const JWKS_NEGATIVE_TTL_MS = 10_000;
|
|
11
|
+
/** Minimum gap between unknown-`kid` refetches, so forged kids can't stampede platform. */
|
|
12
|
+
export const UNKNOWN_KID_REFETCH_COOLDOWN_MS = 30_000;
|
|
13
|
+
/** The key set could not be obtained — the token's validity is UNKNOWN (503-shaped). */
|
|
14
|
+
export class JwksUnavailableError extends Error {
|
|
15
|
+
constructor(message = 'Token signing keys are temporarily unavailable') {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'JwksUnavailableError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The key set was obtained and does NOT contain the token's `kid` (401-shaped). */
|
|
21
|
+
export class UnknownKidError extends Error {
|
|
22
|
+
constructor(kid) {
|
|
23
|
+
super(`No published signing key for kid ${kid}`);
|
|
24
|
+
this.name = 'UnknownKidError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A key set with the freshness bookkeeping the contract above needs. Deliberately
|
|
29
|
+
* transport-agnostic: the CLI and the Lambda hand it their own fetcher.
|
|
30
|
+
*/
|
|
31
|
+
export class JwksCache {
|
|
32
|
+
options;
|
|
33
|
+
keys = new Map();
|
|
34
|
+
fetchedAt = 0;
|
|
35
|
+
failedUntil = 0;
|
|
36
|
+
lastUnknownKidFetchAt = 0;
|
|
37
|
+
inFlight;
|
|
38
|
+
refreshIntervalMs;
|
|
39
|
+
negativeTtlMs;
|
|
40
|
+
unknownKidCooldownMs;
|
|
41
|
+
source;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
this.refreshIntervalMs = options.refreshIntervalMs ?? JWKS_REFRESH_INTERVAL_MS;
|
|
45
|
+
this.negativeTtlMs = options.negativeTtlMs ?? JWKS_NEGATIVE_TTL_MS;
|
|
46
|
+
this.unknownKidCooldownMs = options.unknownKidCooldownMs ?? UNKNOWN_KID_REFETCH_COOLDOWN_MS;
|
|
47
|
+
this.source = options.source ?? 'platform';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The verification key for `kid`.
|
|
51
|
+
*
|
|
52
|
+
* @throws {JwksUnavailableError} the key set could not be obtained at all.
|
|
53
|
+
* @throws {UnknownKidError} the key set is in hand but has no such key.
|
|
54
|
+
*/
|
|
55
|
+
async getKey(kid) {
|
|
56
|
+
await this.ensureFresh();
|
|
57
|
+
const key = this.keys.get(kid);
|
|
58
|
+
if (key)
|
|
59
|
+
return key;
|
|
60
|
+
// Unknown kid: platform may have JUST rotated. Refetch once (rate-limited),
|
|
61
|
+
// then decide. A cached-but-stale set that still lacks the kid after the
|
|
62
|
+
// refetch is a real rejection, not a freshness problem.
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
if (now - this.lastUnknownKidFetchAt >= this.unknownKidCooldownMs) {
|
|
65
|
+
this.lastUnknownKidFetchAt = now;
|
|
66
|
+
emitCounter('jwks_unknown_kid_refetch_total', { source: this.source });
|
|
67
|
+
await this.refresh({ force: true }).catch(() => { });
|
|
68
|
+
const refreshed = this.keys.get(kid);
|
|
69
|
+
if (refreshed)
|
|
70
|
+
return refreshed;
|
|
71
|
+
}
|
|
72
|
+
if (this.keys.size === 0)
|
|
73
|
+
throw new JwksUnavailableError();
|
|
74
|
+
throw new UnknownKidError(kid);
|
|
75
|
+
}
|
|
76
|
+
/** Every currently-trusted `kid`. Exposed for diagnostics and tests. */
|
|
77
|
+
knownKids() {
|
|
78
|
+
return [...this.keys.keys()];
|
|
79
|
+
}
|
|
80
|
+
/** Drop everything, so the next lookup refetches. */
|
|
81
|
+
reset() {
|
|
82
|
+
this.keys.clear();
|
|
83
|
+
this.fetchedAt = 0;
|
|
84
|
+
this.failedUntil = 0;
|
|
85
|
+
this.lastUnknownKidFetchAt = 0;
|
|
86
|
+
this.inFlight = undefined;
|
|
87
|
+
}
|
|
88
|
+
/** Refetch when the set is missing or past its refresh interval. */
|
|
89
|
+
async ensureFresh() {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const stale = now - this.fetchedAt >= this.refreshIntervalMs;
|
|
92
|
+
if (this.keys.size > 0 && !stale)
|
|
93
|
+
return;
|
|
94
|
+
if (now < this.failedUntil) {
|
|
95
|
+
// Inside the negative-cache window. Serve a key set we already hold
|
|
96
|
+
// (public keys don't expire); with nothing in hand, fail closed.
|
|
97
|
+
if (this.keys.size > 0)
|
|
98
|
+
return;
|
|
99
|
+
throw new JwksUnavailableError();
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await this.refresh({ force: false });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (this.keys.size > 0) {
|
|
106
|
+
logger.warn('JWKS refresh failed; continuing on the cached key set', {
|
|
107
|
+
source: this.source,
|
|
108
|
+
error: error instanceof Error ? error.message : String(error),
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
throw new JwksUnavailableError();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** One real fetch, shared by every concurrent caller. */
|
|
116
|
+
refresh(opts) {
|
|
117
|
+
if (this.inFlight)
|
|
118
|
+
return this.inFlight;
|
|
119
|
+
if (!opts.force && Date.now() < this.failedUntil)
|
|
120
|
+
return Promise.reject(new JwksUnavailableError());
|
|
121
|
+
const promise = this.performFetch().finally(() => { this.inFlight = undefined; });
|
|
122
|
+
this.inFlight = promise;
|
|
123
|
+
return promise;
|
|
124
|
+
}
|
|
125
|
+
async performFetch() {
|
|
126
|
+
let document;
|
|
127
|
+
try {
|
|
128
|
+
document = await this.options.fetch();
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.failedUntil = Date.now() + this.negativeTtlMs;
|
|
132
|
+
emitCounter('jwks_fetch_total', { source: this.source, result: 'error' });
|
|
133
|
+
logger.warn('JWKS fetch failed', {
|
|
134
|
+
source: this.source,
|
|
135
|
+
error: error instanceof Error ? error.message : String(error),
|
|
136
|
+
});
|
|
137
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
138
|
+
}
|
|
139
|
+
const keys = new Map();
|
|
140
|
+
for (const jwk of document.keys) {
|
|
141
|
+
if (!jwk?.kid)
|
|
142
|
+
continue;
|
|
143
|
+
try {
|
|
144
|
+
keys.set(jwk.kid, publicKeyFromJwk(jwk));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
// One unusable entry must not discard the rest of the set.
|
|
148
|
+
logger.warn('Skipping unusable JWKS entry', { kid: jwk.kid, error: error instanceof Error ? error.message : String(error) });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (keys.size === 0) {
|
|
152
|
+
this.failedUntil = Date.now() + this.negativeTtlMs;
|
|
153
|
+
emitCounter('jwks_fetch_total', { source: this.source, result: 'empty' });
|
|
154
|
+
throw new Error('JWKS document contained no usable keys');
|
|
155
|
+
}
|
|
156
|
+
this.keys = keys;
|
|
157
|
+
this.fetchedAt = Date.now();
|
|
158
|
+
this.failedUntil = 0;
|
|
159
|
+
emitCounter('jwks_fetch_total', { source: this.source, result: 'success' });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Fetch platform's JWKS over the in-cluster address every other caller uses
|
|
164
|
+
* (`PLATFORM_SERVICE_HOST` / `PLATFORM_SERVICE_PORT`), or the absolute
|
|
165
|
+
* `PLATFORM_JWKS_URL` when one is configured (the CLI and the events Lambda run
|
|
166
|
+
* outside the cluster).
|
|
167
|
+
*
|
|
168
|
+
* Unauthenticated by design: the key set is public, and requiring a credential
|
|
169
|
+
* to fetch the keys that verify credentials is a bootstrap cycle.
|
|
170
|
+
*/
|
|
171
|
+
export async function fetchPlatformJwks() {
|
|
172
|
+
const absolute = process.env.PLATFORM_JWKS_URL;
|
|
173
|
+
const timeout = parseInt(process.env.JWKS_FETCH_TIMEOUT_MS || '3000', 10);
|
|
174
|
+
if (absolute)
|
|
175
|
+
return fetchJwksFromUrl(absolute, timeout);
|
|
176
|
+
// Loaded LAZILY, on the first fetch. A static import would put the HTTP client
|
|
177
|
+
// (and platform's address) into the import graph of every module that merely
|
|
178
|
+
// uses `requireAuth` — the same reason `api-key-exchange` is lazy, and what
|
|
179
|
+
// keeps partial `http-client.js` mocks in the service suites working.
|
|
180
|
+
const { InternalHttpClient } = await import('./http-client.js');
|
|
181
|
+
const client = new InternalHttpClient({
|
|
182
|
+
host: process.env.PLATFORM_SERVICE_HOST || 'platform',
|
|
183
|
+
port: parseInt(process.env.PLATFORM_SERVICE_PORT || '3000', 10),
|
|
184
|
+
timeout,
|
|
185
|
+
});
|
|
186
|
+
const res = await client.get(JWKS_PATH, { idempotent: true });
|
|
187
|
+
if (res.statusCode !== 200 || !isJwksDocument(res.body)) {
|
|
188
|
+
throw new Error(`Unexpected JWKS response (status ${res.statusCode})`);
|
|
189
|
+
}
|
|
190
|
+
return res.body;
|
|
191
|
+
}
|
|
192
|
+
/** Fetch a JWKS document from an absolute URL (outside-the-cluster verifiers). */
|
|
193
|
+
export async function fetchJwksFromUrl(url, timeoutMs = 3000) {
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch(url, { signal: controller.signal, headers: { accept: 'application/json' } });
|
|
198
|
+
if (!res.ok)
|
|
199
|
+
throw new Error(`JWKS fetch returned ${res.status}`);
|
|
200
|
+
const body = await res.json();
|
|
201
|
+
if (!isJwksDocument(body))
|
|
202
|
+
throw new Error('JWKS response was not a key set');
|
|
203
|
+
return body;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
let platformJwks;
|
|
210
|
+
/** The process-wide cache of platform's published signing keys. */
|
|
211
|
+
export function platformJwksCache() {
|
|
212
|
+
platformJwks ??= new JwksCache({ fetch: fetchPlatformJwks, source: 'platform' });
|
|
213
|
+
return platformJwks;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Replace the process-wide cache — used by tests, and by any host that must
|
|
217
|
+
* supply its own fetcher. Pass `undefined` to drop back to the default.
|
|
218
|
+
*/
|
|
219
|
+
export function setPlatformJwksCache(cache) {
|
|
220
|
+
platformJwks = cache;
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiandrcy1jYWNoZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9zZXJ2aWNlcy9qd2tzLWNhY2hlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUE0QnRDLE9BQU8sRUFDTCxTQUFTLEVBQ1QsY0FBYyxFQUNkLGdCQUFnQixHQUdqQixNQUFNLGlCQUFpQixDQUFDO0FBQ3pCLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNsRCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFFekQsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLFlBQVksQ0FBQyxDQUFDO0FBRTFDLG9FQUFvRTtBQUNwRSxNQUFNLENBQUMsTUFBTSx3QkFBd0IsR0FBRyxPQUFPLENBQUMsQ0FBQyxhQUFhO0FBRTlELDREQUE0RDtBQUM1RCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxNQUFNLENBQUM7QUFFM0MsMkZBQTJGO0FBQzNGLE1BQU0sQ0FBQyxNQUFNLCtCQUErQixHQUFHLE1BQU0sQ0FBQztBQUV0RCx3RkFBd0Y7QUFDeEYsTUFBTSxPQUFPLG9CQUFxQixTQUFRLEtBQUs7SUFDN0MsWUFBWSxPQUFPLEdBQUcsZ0RBQWdEO1FBQ3BFLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUNmLElBQUksQ0FBQyxJQUFJLEdBQUcsc0JBQXNCLENBQUM7SUFDckMsQ0FBQztDQUNGO0FBRUQsb0ZBQW9GO0FBQ3BGLE1BQU0sT0FBTyxlQUFnQixTQUFRLEtBQUs7SUFDeEMsWUFBWSxHQUFXO1FBQ3JCLEtBQUssQ0FBQyxvQ0FBb0MsR0FBRyxFQUFFLENBQUMsQ0FBQztRQUNqRCxJQUFJLENBQUMsSUFBSSxHQUFHLGlCQUFpQixDQUFDO0lBQ2hDLENBQUM7Q0FDRjtBQWNEOzs7R0FHRztBQUNILE1BQU0sT0FBTyxTQUFTO0lBWVMsT0FBTztJQVg1QixJQUFJLEdBQUcsSUFB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RyxFQUFFLENBQUMsVUFBVSxDQUFDLEtBQUssRUFBRSxFQUFFLFNBQVMsQ0FBQyxDQUFDO0lBQzlELElBQUksQ0FBQztRQUNILE1BQU0sR0FBRyxHQUFHLE1BQU0sS0FBSyxDQUFDLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRSxVQUFVLENBQUMsTUFBTSxFQUFFLE9BQU8sRUFBRSxFQUFFLE1BQU0sRUFBRSxrQkFBa0IsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUNyRyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUU7WUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHVCQUF1QixHQUFHLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztRQUNsRSxNQUFNLElBQUksR0FBWSxNQUFNLEdBQUcsQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUN2QyxJQUFJLENBQUMsY0FBYyxDQUFDLElBQUksQ0FBQztZQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsaUNBQWlDLENBQUMsQ0FBQztRQUM5RSxPQUFPLElBQUksQ0FBQztJQUNkLENBQUM7WUFBUyxDQUFDO1FBQ1QsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3RCLENBQUM7QUFDSCxDQUFDO0FBRUQsSUFBSSxZQUFtQyxDQUFDO0FBRXhDLG1FQUFtRTtBQUNuRSxNQUFNLFVBQVUsaUJBQWlCO0lBQy9CLFlBQVksS0FBSyxJQUFJLFNBQVMsQ0FBQyxFQUFFLEtBQUssRUFBRSxpQkFBaUIsRUFBRSxNQUFNLEVBQUUsVUFBVSxFQUFFLENBQUMsQ0FBQztJQUNqRixPQUFPLFlBQVksQ0FBQztBQUN0QixDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLG9CQUFvQixDQUFDLEtBQTRCO0lBQy9ELFlBQVksR0FBRyxLQUFLLENBQUM7QUFDdkIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG4vKipcbiAqIFRoZSBKV0tTIGNhY2hlIGV2ZXJ5IHZlcmlmaWVyIGluIHRoZSBmbGVldCBzaGFyZXMuXG4gKlxuICogUGxhdGZvcm0gc2lnbnMgdXNlciB0b2tlbnMgd2l0aCBFUzI1NiBhbmQgcHVibGlzaGVzIHRoZSBwdWJsaWMgaGFsdmVzIGF0XG4gKiBgLy53ZWxsLWtub3duL2p3a3MuanNvbmAuIFZlcmlmaWVycyBtdXN0IG5vdCBmZXRjaCB0aGF0IHBlciByZXF1ZXN0IChwbGF0Zm9ybVxuICogd291bGQgbGFuZCBvbiB0aGUgaG90IHBhdGggb2YgdGhlIHdob2xlIGZsZWV0KSBub3IgY2FjaGUgaXQgZm9yZXZlciAoYSByb3RhdGVkXG4gKiBrZXkgd291bGQgbmV2ZXIgYmUgcGlja2VkIHVwKSwgc28gdGhlIGNvbnRyYWN0IGlzIGZpeGVkIGhlcmUgYW5kIHJldXNlZCBieVxuICogYXBpLWNvcmUncyBgcmVxdWlyZUF1dGhgLCBpbWFnZS1yZWdpc3RyeSdzIGF1dGggcmVzb2x2ZXIgYW5kIHRoZSBDTEk6XG4gKlxuICogLSAqKlJlZnJlc2ggZXZlcnkgMTAgbWludXRlcy4qKiBBIGtleSByb3RhdGVkIGluIGF0IHBsYXRmb3JtIGlzIHRydXN0ZWQgZmxlZXQtXG4gKiAgIHdpZGUgd2l0aGluIHRoYXQgd2luZG93IHdpdGhvdXQgYW55b25lIHJlc3RhcnRpbmcuXG4gKiAtICoqUmVmZXRjaCBvbmNlIG9uIGFuIHVua25vd24gYGtpZGAuKiogQSBicmFuZC1uZXcgc2lnbmluZyBrZXkgaXMgcGlja2VkIHVwXG4gKiAgIGltbWVkaWF0ZWx5IGluc3RlYWQgb2YgNDAxLWluZyBldmVyeSByZXF1ZXN0IHVudGlsIHRoZSBuZXh0IHJlZnJlc2guIFJhdGUtXG4gKiAgIGxpbWl0ZWQgKHtAbGluayBVTktOT1dOX0tJRF9SRUZFVENIX0NPT0xET1dOX01TfSkgc28gYSBmbG9vZCBvZiBmb3JnZWQgYGtpZGBzXG4gKiAgIGNhbid0IHR1cm4gaW50byBhIGZldGNoIHN0b3JtIGFnYWluc3QgcGxhdGZvcm0uXG4gKiAtICoqTmVnYXRpdmUtY2FjaGUgZmFpbHVyZXMgYnJpZWZseS4qKiBBIHBsYXRmb3JtIG91dGFnZSBkb2Vzbid0IGJlY29tZSBhXG4gKiAgIGZldGNoLXBlci1yZXF1ZXN0IGFtcGxpZmllci5cbiAqIC0gKipGYWlsIGNsb3NlZC4qKiBBbiB1bnZlcmlmaWFibGUgdG9rZW4gaXMgbm90IGFuIGlkZW50aXR5OiB3aGVuIHRoZSBrZXkgc2V0XG4gKiAgIGNhbm5vdCBiZSBvYnRhaW5lZCB0aGUgdmVyaWZpZXIgdGhyb3dzIHtAbGluayBKd2tzVW5hdmFpbGFibGVFcnJvcn0gYW5kIHRoZVxuICogICBjYWxsZXIgYW5zd2VycyA0MDEvNTAzIOKAlCBpdCBORVZFUiBmYWxscyB0aHJvdWdoIHRvIFwiYWxsb3dcIi5cbiAqICAgQSBrZXkgc2V0IGFscmVhZHkgaW4gaGFuZCBpcyBzdGlsbCB1c2VkIHdoaWxlIGEgcmVmcmVzaCBpcyBmYWlsaW5nICh0aGUga2V5c1xuICogICBhcmUgcHVibGljIGFuZCBkb24ndCBleHBpcmU7IHJlZnVzaW5nIGV2ZXJ5IHJlcXVlc3QgYmVjYXVzZSBwbGF0Zm9ybSBibGlwcGVkXG4gKiAgIHdvdWxkIGJlIGEgc2VsZi1pbmZsaWN0ZWQgb3V0YWdlKSwgd2hpY2ggaXMgd2h5IHRoZSBuZWdhdGl2ZSBjYWNoZSBleGlzdHMuXG4gKi9cblxuaW1wb3J0IHR5cGUgeyBLZXlPYmplY3QgfSBmcm9tICdjcnlwdG8nO1xuaW1wb3J0IHtcbiAgSldLU19QQVRILFxuICBpc0p3a3NEb2N1bWVudCxcbiAgcHVibGljS2V5RnJvbUp3ayxcbiAgdHlwZSBKd2tzRG9jdW1lbnQsXG4gIHR5cGUgUHVibGljSndrLFxufSBmcm9tICcuLi91dGlscy9qd2suanMnO1xuaW1wb3J0IHsgY3JlYXRlTG9nZ2VyIH0gZnJvbSAnLi4vdXRpbHMvbG9nZ2VyLmpzJztcbmltcG9ydCB7IGVtaXRDb3VudGVyIH0gZnJvbSAnLi4vdXRpbHMvbWV0cmljLWVtaXR0ZXIuanMnO1xuXG5jb25zdCBsb2dnZXIgPSBjcmVhdGVMb2dnZXIoJ2p3a3MtY2FjaGUnKTtcblxuLyoqIEhvdyBsb25nIGEgZmV0Y2hlZCBrZXkgc2V0IGlzIHNlcnZlZCBiZWZvcmUgYSByZWZyZXNoIGlzIGR1ZS4gKi9cbmV4cG9ydCBjb25zdCBKV0tTX1JFRlJFU0hfSU5URVJWQUxfTVMgPSA2MDBfMDAwOyAvLyAxMCBtaW51dGVzXG5cbi8qKiBIb3cgbG9uZyBhIGZldGNoIEZBSUxVUkUgc3VwcHJlc3NlcyBmdXJ0aGVyIGF0dGVtcHRzLiAqL1xuZXhwb3J0IGNvbnN0IEpXS1NfTkVHQVRJVkVfVFRMX01TID0gMTBfMDAwO1xuXG4vKiogTWluaW11bSBnYXAgYmV0d2VlbiB1bmtub3duLWBraWRgIHJlZmV0Y2hlcywgc28gZm9yZ2VkIGtpZHMgY2FuJ3Qgc3RhbXBlZGUgcGxhdGZvcm0uICovXG5leHBvcnQgY29uc3QgVU5LTk9XTl9LSURfUkVGRVRDSF9DT09MRE9XTl9NUyA9IDMwXzAwMDtcblxuLyoqIFRoZSBrZXkgc2V0IGNvdWxkIG5vdCBiZSBvYnRhaW5lZCDigJQgdGhlIHRva2VuJ3MgdmFsaWRpdHkgaXMgVU5LTk9XTiAoNTAzLXNoYXBlZCkuICovXG5leHBvcnQgY2xhc3MgSndrc1VuYXZhaWxhYmxlRXJyb3IgZXh0ZW5kcyBFcnJvciB7XG4gIGNvbnN0cnVjdG9yKG1lc3NhZ2UgPSAnVG9rZW4gc2lnbmluZyBrZXlzIGFyZSB0ZW1wb3JhcmlseSB1bmF2YWlsYWJsZScpIHtcbiAgICBzdXBlcihtZXNzYWdlKTtcbiAgICB0aGlzLm5hbWUgPSAnSndrc1VuYXZhaWxhYmxlRXJyb3InO1xuICB9XG59XG5cbi8qKiBUaGUga2V5IHNldCB3YXMgb2J0YWluZWQgYW5kIGRvZXMgTk9UIGNvbnRhaW4gdGhlIHRva2VuJ3MgYGtpZGAgKDQwMS1zaGFwZWQpLiAqL1xuZXhwb3J0IGNsYXNzIFVua25vd25LaWRFcnJvciBleHRlbmRzIEVycm9yIHtcbiAgY29uc3RydWN0b3Ioa2lkOiBzdHJpbmcpIHtcbiAgICBzdXBlcihgTm8gcHVibGlzaGVkIHNpZ25pbmcga2V5IGZvciBraWQgJHtraWR9YCk7XG4gICAgdGhpcy5uYW1lID0gJ1Vua25vd25LaWRFcnJvcic7XG4gIH1cbn1cblxuLyoqIEZldGNoZXMgdGhlIHJhdyBKV0tTIGRvY3VtZW50LiBSZWplY3RzIG9uIGFueSB0cmFuc3BvcnQvc2hhcGUgZmFpbHVyZS4gKi9cbmV4cG9ydCB0eXBlIEp3a3NGZXRjaGVyID0gKCkgPT4gUHJvbWlzZTxKd2tzRG9jdW1lbnQ+O1xuXG5leHBvcnQgaW50ZXJmYWNlIEp3a3NDYWNoZU9wdGlvbnMge1xuICBmZXRjaDogSndrc0ZldGNoZXI7XG4gIC8qKiBGb3IgbWV0cmljcy9sb2dnaW5nIChlLmcuIGAncGxhdGZvcm0nYCkuICovXG4gIHNvdXJjZT86IHN0cmluZztcbiAgcmVmcmVzaEludGVydmFsTXM/OiBudW1iZXI7XG4gIG5lZ2F0aXZlVHRsTXM/OiBudW1iZXI7XG4gIHVua25vd25LaWRDb29sZG93bk1zPzogbnVtYmVyO1xufVxuXG4vKipcbiAqIEEga2V5IHNldCB3aXRoIHRoZSBmcmVzaG5lc3MgYm9va2tlZXBpbmcgdGhlIGNvbnRyYWN0IGFib3ZlIG5lZWRzLiBEZWxpYmVyYXRlbHlcbiAqIHRyYW5zcG9ydC1hZ25vc3RpYzogdGhlIENMSSBhbmQgdGhlIExhbWJkYSBoYW5kIGl0IHRoZWlyIG93biBmZXRjaGVyLlxuICovXG5leHBvcnQgY2xhc3MgSndrc0NhY2hlIHtcbiAgcHJpdmF0ZSBrZXlzID0gbmV3IE1hcDxzdHJpbmcsIEtleU9iamVjdD4oKTtcbiAgcHJpdmF0ZSBmZXRjaGVkQXQgPSAwO1xuICBwcml2YXRlIGZhaWxlZFVudGlsID0gMDtcbiAgcHJpdmF0ZSBsYXN0VW5rbm93bktpZEZldGNoQXQgPSAwO1xuICBwcml2YXRlIGluRmxpZ2h0OiBQcm9taXNlPHZvaWQ+IHwgdW5kZWZpbmVkO1xuXG4gIHByaXZhdGUgcmVhZG9ubHkgcmVmcmVzaEludGVydmFsTXM6IG51bWJlcjtcbiAgcHJpdmF0ZSByZWFkb25seSBuZWdhdGl2ZVR0bE1zOiBudW1iZXI7XG4gIHByaXZhdGUgcmVhZG9ubHkgdW5rbm93bktpZENvb2xkb3duTXM6IG51bWJlcjtcbiAgcHJpdmF0ZSByZWFkb25seSBzb3VyY2U6IHN0cmluZztcblxuICBjb25zdHJ1Y3Rvcihwcml2YXRlIHJlYWRvbmx5IG9wdGlvbnM6IEp3a3NDYWNoZU9wdGlvbnMpIHtcbiAgICB0aGlzLnJlZnJlc2hJbnRlcnZhbE1zID0gb3B0aW9ucy5yZWZyZXNoSW50ZXJ2YWxNcyA/PyBKV0tTX1JFRlJFU0hfSU5URVJWQUxfTVM7XG4gICAgdGhpcy5uZWdhdGl2ZVR0bE1zID0gb3B0aW9ucy5uZWdhdGl2ZVR0bE1zID8/IEpXS1NfTkVHQVRJVkVfVFRMX01TO1xuICAgIHRoaXMudW5rbm93bktpZENvb2xkb3duTXMgPSBvcHRpb25zLnVua25vd25LaWRDb29sZG93bk1zID8/IFVOS05PV05fS0lEX1JFRkVUQ0hfQ09PTERPV05fTVM7XG4gICAgdGhpcy5zb3VyY2UgPSBvcHRpb25zLnNvdXJjZSA/PyAncGxhdGZvcm0nO1xuICB9XG5cbiAgLyoqXG4gICAqIFRoZSB2ZXJpZmljYXRpb24ga2V5IGZvciBga2lkYC5cbiAgICpcbiAgICogQHRocm93cyB7Sndrc1VuYXZhaWxhYmxlRXJyb3J9IHRoZSBrZXkgc2V0IGNvdWxkIG5vdCBiZSBvYnRhaW5lZCBhdCBhbGwuXG4gICAqIEB0aHJvd3Mge1Vua25vd25LaWRFcnJvcn0gdGhlIGtleSBzZXQgaXMgaW4gaGFuZCBidXQgaGFzIG5vIHN1Y2gga2V5LlxuICAgKi9cbiAgYXN5bmMgZ2V0S2V5KGtpZDogc3RyaW5nKTogUHJvbWlzZTxLZXlPYmplY3Q+IHtcbiAgICBhd2FpdCB0aGlzLmVuc3VyZUZyZXNoKCk7XG4gICAgY29uc3Qga2V5ID0gdGhpcy5rZXlzLmdldChraWQpO1xuICAgIGlmIChrZXkpIHJldHVybiBrZXk7XG5cbiAgICAvLyBVbmtub3duIGtpZDogcGxhdGZvcm0gbWF5IGhhdmUgSlVTVCByb3RhdGVkLiBSZWZldGNoIG9uY2UgKHJhdGUtbGltaXRlZCksXG4gICAgLy8gdGhlbiBkZWNpZGUuIEEgY2FjaGVkLWJ1dC1zdGFsZSBzZXQgdGhhdCBzdGlsbCBsYWNrcyB0aGUga2lkIGFmdGVyIHRoZVxuICAgIC8vIHJlZmV0Y2ggaXMgYSByZWFsIHJlamVjdGlvbiwgbm90IGEgZnJlc2huZXNzIHByb2JsZW0uXG4gICAgY29uc3Qgbm93ID0gRGF0ZS5ub3coKTtcbiAgICBpZiAobm93IC0gdGhpcy5sYXN0VW5rbm93bktpZEZldGNoQXQgPj0gdGhpcy51bmtub3duS2lkQ29vbGRvd25Ncykge1xuICAgICAgdGhpcy5sYXN0VW5rbm93bktpZEZldGNoQXQgPSBub3c7XG4gICAgICBlbWl0Q291bnRlcignandrc191bmtub3duX2tpZF9yZWZldGNoX3RvdGFsJywgeyBzb3VyY2U6IHRoaXMuc291cmNlIH0pO1xuICAgICAgYXdhaXQgdGhpcy5yZWZyZXNoKHsgZm9yY2U6IHRydWUgfSkuY2F0Y2goKCkgPT4geyAvKiBoYW5kbGVkIGJlbG93IGJ5IHRoZSBtaXNzICovIH0pO1xuICAgICAgY29uc3QgcmVmcmVzaGVkID0gdGhpcy5rZXlzLmdldChraWQpO1xuICAgICAgaWYgKHJlZnJlc2hlZCkgcmV0dXJuIHJlZnJlc2hlZDtcbiAgICB9XG4gICAgaWYgKHRoaXMua2V5cy5zaXplID09PSAwKSB0aHJvdyBuZXcgSndrc1VuYXZhaWxhYmxlRXJyb3IoKTtcbiAgICB0aHJvdyBuZXcgVW5rbm93bktpZEVycm9yKGtpZCk7XG4gIH1cblxuICAvKiogRXZlcnkgY3VycmVudGx5LXRydXN0ZWQgYGtpZGAuIEV4cG9zZWQgZm9yIGRpYWdub3N0aWNzIGFuZCB0ZXN0cy4gKi9cbiAga25vd25LaWRzKCk6IHN0cmluZ1tdIHtcbiAgICByZXR1cm4gWy4uLnRoaXMua2V5cy5rZXlzKCldO1xuICB9XG5cbiAgLyoqIERyb3AgZXZlcnl0aGluZywgc28gdGhlIG5leHQgbG9va3VwIHJlZmV0Y2hlcy4gKi9cbiAgcmVzZXQoKTogdm9pZCB7XG4gICAgdGhpcy5rZXlzLmNsZWFyKCk7XG4gICAgdGhpcy5mZXRjaGVkQXQgPSAwO1xuICAgIHRoaXMuZmFpbGVkVW50aWwgPSAwO1xuICAgIHRoaXMubGFzdFVua25vd25LaWRGZXRjaEF0ID0gMDtcbiAgICB0aGlzLmluRmxpZ2h0ID0gdW5kZWZpbmVkO1xuICB9XG5cbiAgLyoqIFJlZmV0Y2ggd2hlbiB0aGUgc2V0IGlzIG1pc3Npbmcgb3IgcGFzdCBpdHMgcmVmcmVzaCBpbnRlcnZhbC4gKi9cbiAgcHJpdmF0ZSBhc3luYyBlbnN1cmVGcmVzaCgpOiBQcm9taXNlPHZvaWQ+IHtcbiAgICBjb25zdCBub3cgPSBEYXRlLm5vdygpO1xuICAgIGNvbnN0IHN0YWxlID0gbm93IC0gdGhpcy5mZXRjaGVkQXQgPj0gdGhpcy5yZWZyZXNoSW50ZXJ2YWxNcztcbiAgICBpZiAodGhpcy5rZXlzLnNpemUgPiAwICYmICFzdGFsZSkgcmV0dXJuO1xuXG4gICAgaWYgKG5vdyA8IHRoaXMuZmFpbGVkVW50aWwpIHtcbiAgICAgIC8vIEluc2lkZSB0aGUgbmVnYXRpdmUtY2FjaGUgd2luZG93LiBTZXJ2ZSBhIGtleSBzZXQgd2UgYWxyZWFkeSBob2xkXG4gICAgICAvLyAocHVibGljIGtleXMgZG9uJ3QgZXhwaXJlKTsgd2l0aCBub3RoaW5nIGluIGhhbmQsIGZhaWwgY2xvc2VkLlxuICAgICAgaWYgKHRoaXMua2V5cy5zaXplID4gMCkgcmV0dXJuO1xuICAgICAgdGhyb3cgbmV3IEp3a3NVbmF2YWlsYWJsZUVycm9yKCk7XG4gICAgfVxuXG4gICAgdHJ5IHtcbiAgICAgIGF3YWl0IHRoaXMucmVmcmVzaCh7IGZvcmNlOiBmYWxzZSB9KTtcbiAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgaWYgKHRoaXMua2V5cy5zaXplID4gMCkge1xuICAgICAgICBsb2dnZXIud2FybignSldLUyByZWZyZXNoIGZhaWxlZDsgY29udGludWluZyBvbiB0aGUgY2FjaGVkIGtleSBzZXQnLCB7XG4gICAgICAgICAgc291cmNlOiB0aGlzLnNvdXJjZSxcbiAgICAgICAgICBlcnJvcjogZXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IGVycm9yLm1lc3NhZ2UgOiBTdHJpbmcoZXJyb3IpLFxuICAgICAgICB9KTtcbiAgICAgICAgcmV0dXJuO1xuICAgICAgfVxuICAgICAgdGhyb3cgbmV3IEp3a3NVbmF2YWlsYWJsZUVycm9yKCk7XG4gICAgfVxuICB9XG5cbiAgLyoqIE9uZSByZWFsIGZldGNoLCBzaGFyZWQgYnkgZXZlcnkgY29uY3VycmVudCBjYWxsZXIuICovXG4gIHByaXZhdGUgcmVmcmVzaChvcHRzOiB7IGZvcmNlOiBib29sZWFuIH0pOiBQcm9taXNlPHZvaWQ+IHtcbiAgICBpZiAodGhpcy5pbkZsaWdodCkgcmV0dXJuIHRoaXMuaW5GbGlnaHQ7XG4gICAgaWYgKCFvcHRzLmZvcmNlICYmIERhdGUubm93KCkgPCB0aGlzLmZhaWxlZFVudGlsKSByZXR1cm4gUHJvbWlzZS5yZWplY3QobmV3IEp3a3NVbmF2YWlsYWJsZUVycm9yKCkpO1xuICAgIGNvbnN0IHByb21pc2UgPSB0aGlzLnBlcmZvcm1GZXRjaCgpLmZpbmFsbHkoKCkgPT4geyB0aGlzLmluRmxpZ2h0ID0gdW5kZWZpbmVkOyB9KTtcbiAgICB0aGlzLmluRmxpZ2h0ID0gcHJvbWlzZTtcbiAgICByZXR1cm4gcHJvbWlzZTtcbiAgfVxuXG4gIHByaXZhdGUgYXN5bmMgcGVyZm9ybUZldGNoKCk6IFByb21pc2U8dm9pZD4ge1xuICAgIGxldCBkb2N1bWVudDogSndrc0RvY3VtZW50O1xuICAgIHRyeSB7XG4gICAgICBkb2N1bWVudCA9IGF3YWl0IHRoaXMub3B0aW9ucy5mZXRjaCgpO1xuICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICB0aGlzLmZhaWxlZFVudGlsID0gRGF0ZS5ub3coKSArIHRoaXMubmVnYXRpdmVUdGxNcztcbiAgICAgIGVtaXRDb3VudGVyKCdqd2tzX2ZldGNoX3RvdGFsJywgeyBzb3VyY2U6IHRoaXMuc291cmNlLCByZXN1bHQ6ICdlcnJvcicgfSk7XG4gICAgICBsb2dnZXIud2FybignSldLUyBmZXRjaCBmYWlsZWQnLCB7XG4gICAgICAgIHNvdXJjZTogdGhpcy5zb3VyY2UsXG4gICAgICAgIGVycm9yOiBlcnJvciBpbnN0YW5jZW9mIEVycm9yID8gZXJyb3IubWVzc2FnZSA6IFN0cmluZyhlcnJvciksXG4gICAgICB9KTtcbiAgICAgIHRocm93IGVycm9yIGluc3RhbmNlb2YgRXJyb3IgPyBlcnJvciA6IG5ldyBFcnJvcihTdHJpbmcoZXJyb3IpKTtcbiAgICB9XG5cbiAgICBjb25zdCBrZXlzID0gbmV3IE1hcDxzdHJpbmcsIEtleU9iamVjdD4oKTtcbiAgICBmb3IgKGNvbnN0IGp3ayBvZiBkb2N1bWVudC5rZXlzIGFzIFB1YmxpY0p3a1tdKSB7XG4gICAgICBpZiAoIWp3az8ua2lkKSBjb250aW51ZTtcbiAgICAgIHRyeSB7XG4gICAgICAgIGtleXMuc2V0KGp3ay5raWQsIHB1YmxpY0tleUZyb21Kd2soandrKSk7XG4gICAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgICAvLyBPbmUgdW51c2FibGUgZW50cnkgbXVzdCBub3QgZGlzY2FyZCB0aGUgcmVzdCBvZiB0aGUgc2V0LlxuICAgICAgICBsb2dnZXIud2FybignU2tpcHBpbmcgdW51c2FibGUgSldLUyBlbnRyeScsIHsga2lkOiBqd2sua2lkLCBlcnJvcjogZXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IGVycm9yLm1lc3NhZ2UgOiBTdHJpbmcoZXJyb3IpIH0pO1xuICAgICAgfVxuICAgIH1cbiAgICBpZiAoa2V5cy5zaXplID09PSAwKSB7XG4gICAgICB0aGlzLmZhaWxlZFVudGlsID0gRGF0ZS5ub3coKSArIHRoaXMubmVnYXRpdmVUdGxNcztcbiAgICAgIGVtaXRDb3VudGVyKCdqd2tzX2ZldGNoX3RvdGFsJywgeyBzb3VyY2U6IHRoaXMuc291cmNlLCByZXN1bHQ6ICdlbXB0eScgfSk7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoJ0pXS1MgZG9jdW1lbnQgY29udGFpbmVkIG5vIHVzYWJsZSBrZXlzJyk7XG4gICAgfVxuXG4gICAgdGhpcy5rZXlzID0ga2V5cztcbiAgICB0aGlzLmZldGNoZWRBdCA9IERhdGUubm93KCk7XG4gICAgdGhpcy5mYWlsZWRVbnRpbCA9IDA7XG4gICAgZW1pdENvdW50ZXIoJ2p3a3NfZmV0Y2hfdG90YWwnLCB7IHNvdXJjZTogdGhpcy5zb3VyY2UsIHJlc3VsdDogJ3N1Y2Nlc3MnIH0pO1xuICB9XG59XG5cbi8qKlxuICogRmV0Y2ggcGxhdGZvcm0ncyBKV0tTIG92ZXIgdGhlIGluLWNsdXN0ZXIgYWRkcmVzcyBldmVyeSBvdGhlciBjYWxsZXIgdXNlc1xuICogKGBQTEFURk9STV9TRVJWSUNFX0hPU1RgIC8gYFBMQVRGT1JNX1NFUlZJQ0VfUE9SVGApLCBvciB0aGUgYWJzb2x1dGVcbiAqIGBQTEFURk9STV9KV0tTX1VSTGAgd2hlbiBvbmUgaXMgY29uZmlndXJlZCAodGhlIENMSSBhbmQgdGhlIGV2ZW50cyBMYW1iZGEgcnVuXG4gKiBvdXRzaWRlIHRoZSBjbHVzdGVyKS5cbiAqXG4gKiBVbmF1dGhlbnRpY2F0ZWQgYnkgZGVzaWduOiB0aGUga2V5IHNldCBpcyBwdWJsaWMsIGFuZCByZXF1aXJpbmcgYSBjcmVkZW50aWFsXG4gKiB0byBmZXRjaCB0aGUga2V5cyB0aGF0IHZlcmlmeSBjcmVkZW50aWFscyBpcyBhIGJvb3RzdHJhcCBjeWNsZS5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGZldGNoUGxhdGZvcm1Kd2tzKCk6IFByb21pc2U8Sndrc0RvY3VtZW50PiB7XG4gIGNvbnN0IGFic29sdXRlID0gcHJvY2Vzcy5lbnYuUExBVEZPUk1fSldLU19VUkw7XG4gIGNvbnN0IHRpbWVvdXQgPSBwYXJzZUludChwcm9jZXNzLmVudi5KV0tTX0ZFVENIX1RJTUVPVVRfTVMgfHwgJzMwMDAnLCAxMCk7XG4gIGlmIChhYnNvbHV0ZSkgcmV0dXJuIGZldGNoSndrc0Zyb21VcmwoYWJzb2x1dGUsIHRpbWVvdXQpO1xuXG4gIC8vIExvYWRlZCBMQVpJTFksIG9uIHRoZSBmaXJzdCBmZXRjaC4gQSBzdGF0aWMgaW1wb3J0IHdvdWxkIHB1dCB0aGUgSFRUUCBjbGllbnRcbiAgLy8gKGFuZCBwbGF0Zm9ybSdzIGFkZHJlc3MpIGludG8gdGhlIGltcG9ydCBncmFwaCBvZiBldmVyeSBtb2R1bGUgdGhhdCBtZXJlbHlcbiAgLy8gdXNlcyBgcmVxdWlyZUF1dGhgIOKAlCB0aGUgc2FtZSByZWFzb24gYGFwaS1rZXktZXhjaGFuZ2VgIGlzIGxhenksIGFuZCB3aGF0XG4gIC8vIGtlZXBzIHBhcnRpYWwgYGh0dHAtY2xpZW50LmpzYCBtb2NrcyBpbiB0aGUgc2VydmljZSBzdWl0ZXMgd29ya2luZy5cbiAgY29uc3QgeyBJbnRlcm5hbEh0dHBDbGllbnQgfSA9IGF3YWl0IGltcG9ydCgnLi9odHRwLWNsaWVudC5qcycpO1xuICBjb25zdCBjbGllbnQgPSBuZXcgSW50ZXJuYWxIdHRwQ2xpZW50KHtcbiAgICBob3N0OiBwcm9jZXNzLmVudi5QTEFURk9STV9TRVJWSUNFX0hPU1QgfHwgJ3BsYXRmb3JtJyxcbiAgICBwb3J0OiBwYXJzZUludChwcm9jZXNzLmVudi5QTEFURk9STV9TRVJWSUNFX1BPUlQgfHwgJzMwMDAnLCAxMCksXG4gICAgdGltZW91dCxcbiAgfSk7XG4gIGNvbnN0IHJlcyA9IGF3YWl0IGNsaWVudC5nZXQ8dW5rbm93bj4oSldLU19QQVRILCB7IGlkZW1wb3RlbnQ6IHRydWUgfSk7XG4gIGlmIChyZXMuc3RhdHVzQ29kZSAhPT0gMjAwIHx8ICFpc0p3a3NEb2N1bWVudChyZXMuYm9keSkpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYFVuZXhwZWN0ZWQgSldLUyByZXNwb25zZSAoc3RhdHVzICR7cmVzLnN0YXR1c0NvZGV9KWApO1xuICB9XG4gIHJldHVybiByZXMuYm9keTtcbn1cblxuLyoqIEZldGNoIGEgSldLUyBkb2N1bWVudCBmcm9tIGFuIGFic29sdXRlIFVSTCAob3V0c2lkZS10aGUtY2x1c3RlciB2ZXJpZmllcnMpLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGZldGNoSndrc0Zyb21VcmwodXJsOiBzdHJpbmcsIHRpbWVvdXRNcyA9IDMwMDApOiBQcm9taXNlPEp3a3NEb2N1bWVudD4ge1xuICBjb25zdCBjb250cm9sbGVyID0gbmV3IEFib3J0Q29udHJvbGxlcigpO1xuICBjb25zdCB0aW1lciA9IHNldFRpbWVvdXQoKCkgPT4gY29udHJvbGxlci5hYm9ydCgpLCB0aW1lb3V0TXMpO1xuICB0cnkge1xuICAgIGNvbnN0IHJlcyA9IGF3YWl0IGZldGNoKHVybCwgeyBzaWduYWw6IGNvbnRyb2xsZXIuc2lnbmFsLCBoZWFkZXJzOiB7IGFjY2VwdDogJ2FwcGxpY2F0aW9uL2pzb24nIH0gfSk7XG4gICAgaWYgKCFyZXMub2spIHRocm93IG5ldyBFcnJvcihgSldLUyBmZXRjaCByZXR1cm5lZCAke3Jlcy5zdGF0dXN9YCk7XG4gICAgY29uc3QgYm9keTogdW5rbm93biA9IGF3YWl0IHJlcy5qc29uKCk7XG4gICAgaWYgKCFpc0p3a3NEb2N1bWVudChib2R5KSkgdGhyb3cgbmV3IEVycm9yKCdKV0tTIHJlc3BvbnNlIHdhcyBub3QgYSBrZXkgc2V0Jyk7XG4gICAgcmV0dXJuIGJvZHk7XG4gIH0gZmluYWxseSB7XG4gICAgY2xlYXJUaW1lb3V0KHRpbWVyKTtcbiAgfVxufVxuXG5sZXQgcGxhdGZvcm1Kd2tzOiBKd2tzQ2FjaGUgfCB1bmRlZmluZWQ7XG5cbi8qKiBUaGUgcHJvY2Vzcy13aWRlIGNhY2hlIG9mIHBsYXRmb3JtJ3MgcHVibGlzaGVkIHNpZ25pbmcga2V5cy4gKi9cbmV4cG9ydCBmdW5jdGlvbiBwbGF0Zm9ybUp3a3NDYWNoZSgpOiBKd2tzQ2FjaGUge1xuICBwbGF0Zm9ybUp3a3MgPz89IG5ldyBKd2tzQ2FjaGUoeyBmZXRjaDogZmV0Y2hQbGF0Zm9ybUp3a3MsIHNvdXJjZTogJ3BsYXRmb3JtJyB9KTtcbiAgcmV0dXJuIHBsYXRmb3JtSndrcztcbn1cblxuLyoqXG4gKiBSZXBsYWNlIHRoZSBwcm9jZXNzLXdpZGUgY2FjaGUg4oCUIHVzZWQgYnkgdGVzdHMsIGFuZCBieSBhbnkgaG9zdCB0aGF0IG11c3RcbiAqIHN1cHBseSBpdHMgb3duIGZldGNoZXIuIFBhc3MgYHVuZGVmaW5lZGAgdG8gZHJvcCBiYWNrIHRvIHRoZSBkZWZhdWx0LlxuICovXG5leHBvcnQgZnVuY3Rpb24gc2V0UGxhdGZvcm1Kd2tzQ2FjaGUoY2FjaGU6IEp3a3NDYWNoZSB8IHVuZGVmaW5lZCk6IHZvaWQge1xuICBwbGF0Zm9ybUp3a3MgPSBjYWNoZTtcbn1cbiJdfQ==
|
|
@@ -30,12 +30,15 @@ export declare function closeLeaderLock(redis: LockRedis | null | undefined): Pr
|
|
|
30
30
|
* released on completion, but only if we still own it — a slower predecessor
|
|
31
31
|
* never deletes a successor's lock. `ttlMs` should comfortably exceed one run.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
33
|
+
* If Redis can't be reached the run is skipped (returns false) rather than
|
|
34
|
+
* rejecting — callers are timers, and a Redis blip must not crash the process.
|
|
35
|
+
*
|
|
36
|
+
* @returns true if we held the lock and ran `fn`; false if another holder did or
|
|
37
|
+
* the lock couldn't be taken.
|
|
34
38
|
*/
|
|
35
39
|
export declare function withLeaderLock(redis: LockRedis, key: string, ttlMs: number, fn: () => Promise<void>): Promise<boolean>;
|
|
36
40
|
/**
|
|
37
|
-
* A `LockRedis` backed by
|
|
38
|
-
* `REDIS_HOST`) — the leader-lock counterpart to `createEnvRedisAuditSpool` /
|
|
41
|
+
* A `LockRedis` backed by the shared env Redis client (see env-redis.ts) — the leader-lock counterpart to `createEnvRedisAuditSpool` /
|
|
39
42
|
* `createEnvRedisTokenRevocationStore`, for a service that has no BullMQ client to
|
|
40
43
|
* borrow. Returns `null` when Redis isn't configured, so a caller can degrade to
|
|
41
44
|
* running on every pod (the atomic guards it wraps must still make that safe).
|
|
@@ -38,11 +38,27 @@ const RELEASE_IF_OWNER = 'if redis.call("get", KEYS[1]) == ARGV[1] then return r
|
|
|
38
38
|
* released on completion, but only if we still own it — a slower predecessor
|
|
39
39
|
* never deletes a successor's lock. `ttlMs` should comfortably exceed one run.
|
|
40
40
|
*
|
|
41
|
-
*
|
|
41
|
+
* If Redis can't be reached the run is skipped (returns false) rather than
|
|
42
|
+
* rejecting — callers are timers, and a Redis blip must not crash the process.
|
|
43
|
+
*
|
|
44
|
+
* @returns true if we held the lock and ran `fn`; false if another holder did or
|
|
45
|
+
* the lock couldn't be taken.
|
|
42
46
|
*/
|
|
43
47
|
export async function withLeaderLock(redis, key, ttlMs, fn) {
|
|
44
48
|
const token = randomUUID();
|
|
45
|
-
|
|
49
|
+
let acquired;
|
|
50
|
+
try {
|
|
51
|
+
acquired = await redis.set(key, token, 'PX', ttlMs, 'NX');
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// Redis unreachable (including before the first connection completes): no
|
|
55
|
+
// pod can prove it's the leader, so nobody runs this window. Skipping is
|
|
56
|
+
// the safe side — running here could duplicate destructive work across pods.
|
|
57
|
+
lockLogger.warn('Leader lock unavailable; skipping this run', {
|
|
58
|
+
key, error: err instanceof Error ? err.message : String(err),
|
|
59
|
+
});
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
46
62
|
if (acquired !== 'OK')
|
|
47
63
|
return false;
|
|
48
64
|
try {
|
|
@@ -68,8 +84,7 @@ export async function withLeaderLock(redis, key, ttlMs, fn) {
|
|
|
68
84
|
return true;
|
|
69
85
|
}
|
|
70
86
|
/**
|
|
71
|
-
* A `LockRedis` backed by
|
|
72
|
-
* `REDIS_HOST`) — the leader-lock counterpart to `createEnvRedisAuditSpool` /
|
|
87
|
+
* A `LockRedis` backed by the shared env Redis client (see env-redis.ts) — the leader-lock counterpart to `createEnvRedisAuditSpool` /
|
|
73
88
|
* `createEnvRedisTokenRevocationStore`, for a service that has no BullMQ client to
|
|
74
89
|
* borrow. Returns `null` when Redis isn't configured, so a caller can degrade to
|
|
75
90
|
* running on every pod (the atomic guards it wraps must still make that safe).
|
|
@@ -82,4 +97,4 @@ export function createEnvRedisLock() {
|
|
|
82
97
|
lockLogger.info('Redis leader-lock client initialized');
|
|
83
98
|
return inst;
|
|
84
99
|
}
|
|
85
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
100
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGVhZGVyLWxvY2suanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvc2VydmljZXMvbGVhZGVyLWxvY2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQUV0QyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBQ3BDLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ3RELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUVsRCxNQUFNLFVBQVUsR0FBRyxZQUFZLENBQUMsYUFBYSxDQUFDLENBQUM7QUFvQi9DOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLGVBQWUsQ0FBQyxLQUFtQztJQUN2RSxJQUFJLENBQUMsS0FBSztRQUFFLE9BQU87SUFDbkIsSUFBSSxDQUFDO1FBQ0gsSUFBSSxPQUFPLEtBQUssQ0FBQyxJQUFJLEtBQUssVUFBVSxFQUFFLENBQUM7WUFBQyxNQUFNLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUFDLE9BQU87UUFBQyxDQUFDO0lBQ3ZFLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCw2QkFBNkI7SUFDL0IsQ0FBQztJQUNELElBQUksQ0FBQztRQUFDLEtBQUssQ0FBQyxVQUFVLEVBQUUsRUFBRSxDQUFDO0lBQUMsQ0FBQztJQUFDLE1BQU0sQ0FBQyxDQUFDLGlCQUFpQixDQUFDLENBQUM7QUFDM0QsQ0FBQztBQUVEO2lFQUNpRTtBQUNqRSxNQUFNLGdCQUFnQixHQUNwQixtR0FBbUcsQ0FBQztBQUV0Rzs7Ozs7Ozs7Ozs7Ozs7R0FjRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsY0FBYyxDQUNsQyxLQUFnQixFQUNoQixHQUFXLEVBQ1gsS0FBYSxFQUNiLEVBQXVCO0lBRXZCLE1BQU0sS0FBSyxHQUFHLFVBQVUsRUFBRSxDQUFDO0lBQzNCLElBQUksUUFBaUIsQ0FBQztJQUN0QixJQUFJLENBQUM7UUFDSCxRQUFRLEdBQUcsTUFBTSxLQUFLLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxJQUFJLENBQUMsQ0FBQztJQUM1RCxDQUFDO0lBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztRQUNiLDBFQUEwRTtRQUMxRSx5RUFBeUU7UUFDekUsNkVBQTZFO1FBQzdFLFVBQVUsQ0FBQyxJQUFJLENBQUMsNENBQTRDLEVBQUU7WUFDNUQsR0FBRyxFQUFFLEtBQUssRUFBRSxHQUFHLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDO1NBQzdELENBQUMsQ0FBQztRQUNILE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUNELElBQUksUUFBUSxLQUFLLElBQUk7UUFBRSxPQUFPLEtBQUssQ0FBQztJQUVwQyxJQUFJLENBQUM7UUFDSCxNQUFNLEVBQUUsRUFBRSxDQUFDO0lBQ2IsQ0FBQztZQUFTLENBQUM7UUFDVCwwRUFBMEU7UUFDMUUsd0VBQXdFO1FBQ3hFLDRFQUE0RTtRQUM1RSxxREFBcUQ7UUFDckQsSUFBSSxDQUFDO1lBQ0gsSUFBSSxPQUFPLEtBQUssQ0FBQyxJQUFJLEtBQUssVUFBVSxFQUFFLENBQUM7Z0JBQ3JDLE1BQU0sS0FBSyxDQUFDLElBQUksQ0FBQyxnQkFBZ0IsRUFBRSxDQUFDLEVBQUUsR0FBRyxFQUFFLEtBQUssQ0FBQyxDQUFDO1lBQ3BELENBQUM7aUJBQU0sSUFBSSxDQUFDLE1BQU0sS0FBSyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxLQUFLLEtBQUssRUFBRSxDQUFDO2dCQUM1QyxNQUFNLEtBQUssQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDdkIsQ0FBQztRQUNILENBQUM7UUFBQyxNQUFNLENBQUM7WUFDUCxvREFBb0Q7UUFDdEQsQ0FBQztJQUNILENBQUM7SUFDRCxPQUFPLElBQUksQ0FBQztBQUNkLENBQUM7QUFFRDs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxVQUFVLGtCQUFrQjtJQUNoQyxNQUFNLElBQUksR0FBRyxvQkFBb0IsQ0FBWSxhQUFhLENBQUMsQ0FBQztJQUM1RCxJQUFJLElBQUk7UUFBRSxVQUFVLENBQUMsSUFBSSxDQUFDLHNDQUFzQyxDQUFDLENBQUM7SUFDbEUsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IHJhbmRvbVVVSUQgfSBmcm9tICdjcnlwdG8nO1xuaW1wb3J0IHsgY3JlYXRlRW52UmVkaXNDbGllbnQgfSBmcm9tICcuL2Vudi1yZWRpcy5qcyc7XG5pbXBvcnQgeyBjcmVhdGVMb2dnZXIgfSBmcm9tICcuLi91dGlscy9sb2dnZXIuanMnO1xuXG5jb25zdCBsb2NrTG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdsZWFkZXItbG9jaycpO1xuXG4vKipcbiAqIE1pbmltYWwgUmVkaXMgY2xpZW50IHN1cmZhY2UgbmVlZGVkIGZvciBhIGxlYWRlciBsb2NrIChhIHN1YnNldCBvZiBpb3JlZGlzKS5cbiAqIGBzZXRgIGlzIHZhcmlhZGljIHNvIGNhbGxlcnMgY2FuIHBhc3MgYCdQWCcsIHR0bCwgJ05YJ2AuXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgTG9ja1JlZGlzIHtcbiAgc2V0KGtleTogc3RyaW5nLCB2YWx1ZTogc3RyaW5nLCAuLi5hcmdzOiB1bmtub3duW10pOiBQcm9taXNlPHVua25vd24+O1xuICBnZXQoa2V5OiBzdHJpbmcpOiBQcm9taXNlPHN0cmluZyB8IG51bGw+O1xuICBkZWwoLi4ua2V5czogc3RyaW5nW10pOiBQcm9taXNlPG51bWJlcj47XG4gIC8qKiBPcHRpb25hbCBMdWEgZXZhbCAoaW9yZWRpcyBoYXMgaXQpLiBXaGVuIHByZXNlbnQsIHJlbGVhc2UgdXNlcyBhbiBhdG9taWNcbiAgICogIGNvbXBhcmUtYW5kLWRlbGV0ZSBzbyBhIGxhcHNlZCBob2xkZXIgY2FuJ3QgZnJlZSBhIHN1Y2Nlc3NvcidzIGxvY2suICovXG4gIGV2YWw/KHNjcmlwdDogc3RyaW5nLCBudW1LZXlzOiBudW1iZXIsIC4uLmFyZ3M6IHVua25vd25bXSk6IFByb21pc2U8dW5rbm93bj47XG4gIC8qKiBHcmFjZWZ1bCBjbG9zZSAoaW9yZWRpcyBgcXVpdGApIOKAlCBjYWxsZWQgb24gc2h1dGRvd24gc28gdGhlIGNvbm5lY3Rpb25cbiAgICogIGRvZXNuJ3Qga2VlcCB0aGUgZXZlbnQgbG9vcCBhbGl2ZS4gKi9cbiAgcXVpdD8oKTogUHJvbWlzZTx1bmtub3duPjtcbiAgLyoqIEltbWVkaWF0ZSBjbG9zZSAoaW9yZWRpcyBgZGlzY29ubmVjdGApIOKAlCB0aGUgZmFsbGJhY2sgaWYgYHF1aXRgIGZhaWxzLiAqL1xuICBkaXNjb25uZWN0PygpOiB2b2lkO1xufVxuXG4vKipcbiAqIEJlc3QtZWZmb3J0IGNsb3NlIG9mIGEgbGVhZGVyLWxvY2sgUmVkaXMgY2xpZW50IG9uIHNodXRkb3duLiBQcmVmZXJzIGdyYWNlZnVsXG4gKiBgcXVpdGA7IGZhbGxzIGJhY2sgdG8gYGRpc2Nvbm5lY3RgLiBOby1vcCBmb3IgYSBudWxsIGNsaWVudCAvIG9uZSB3aXRob3V0XG4gKiBlaXRoZXIgbWV0aG9kLiBOZXZlciB0aHJvd3MuXG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBjbG9zZUxlYWRlckxvY2socmVkaXM6IExvY2tSZWRpcyB8IG51bGwgfCB1bmRlZmluZWQpOiBQcm9taXNlPHZvaWQ+IHtcbiAgaWYgKCFyZWRpcykgcmV0dXJuO1xuICB0cnkge1xuICAgIGlmICh0eXBlb2YgcmVkaXMucXVpdCA9PT0gJ2Z1bmN0aW9uJykgeyBhd2FpdCByZWRpcy5xdWl0KCk7IHJldHVybjsgfVxuICB9IGNhdGNoIHtcbiAgICAvLyBmYWxsIHRocm91Z2ggdG8gZGlzY29ubmVjdFxuICB9XG4gIHRyeSB7IHJlZGlzLmRpc2Nvbm5lY3Q/LigpOyB9IGNhdGNoIHsgLyogYmVzdC1lZmZvcnQgKi8gfVxufVxuXG4vKiogQXRvbWljIFwiZGVsZXRlIG9ubHkgaWYgSSBzdGlsbCBvd24gaXRcIiDigJQgY2xvc2VzIHRoZSBnZXQtdGhlbi1kZWwgcmFjZSB3aGVyZVxuICogIHRoZSBsb2NrIGV4cGlyZXMgYW5kIGlzIHJlLWFjcXVpcmVkIGJldHdlZW4gdGhlIHR3byBjYWxscy4gKi9cbmNvbnN0IFJFTEVBU0VfSUZfT1dORVIgPVxuICAnaWYgcmVkaXMuY2FsbChcImdldFwiLCBLRVlTWzFdKSA9PSBBUkdWWzFdIHRoZW4gcmV0dXJuIHJlZGlzLmNhbGwoXCJkZWxcIiwgS0VZU1sxXSkgZWxzZSByZXR1cm4gMCBlbmQnO1xuXG4vKipcbiAqIFJ1biBgZm5gIG9ubHkgaWYgdGhpcyBwcm9jZXNzIHdpbnMgYSBzaG9ydC1saXZlZCBkaXN0cmlidXRlZCBsb2NrIOKAlCB0aGVcbiAqIGFjcm9zcy1wb2RzIFwic2luZ2xlIHJ1bm5lclwiIGd1YXJkIGZvciBwZXJpb2RpYyBqb2JzIChkaWdlc3QgZmx1c2hlcywgZXRjLikuXG4gKlxuICogVXNlcyBgU0VUIGtleSB0b2tlbiBOWCBQWCB0dGxgLCBzbyBleGFjdGx5IG9uZSBob2xkZXIgcnVucyBwZXIgd2luZG93IGFuZCB0aGVcbiAqIGxvY2sgYXV0by1leHBpcmVzIGlmIHRoYXQgaG9sZGVyIGRpZXMgbWlkLXJ1biAobm8gc3R1Y2sgbG9jaykuIFRoZSBsb2NrIGlzXG4gKiByZWxlYXNlZCBvbiBjb21wbGV0aW9uLCBidXQgb25seSBpZiB3ZSBzdGlsbCBvd24gaXQg4oCUIGEgc2xvd2VyIHByZWRlY2Vzc29yXG4gKiBuZXZlciBkZWxldGVzIGEgc3VjY2Vzc29yJ3MgbG9jay4gYHR0bE1zYCBzaG91bGQgY29tZm9ydGFibHkgZXhjZWVkIG9uZSBydW4uXG4gKlxuICogSWYgUmVkaXMgY2FuJ3QgYmUgcmVhY2hlZCB0aGUgcnVuIGlzIHNraXBwZWQgKHJldHVybnMgZmFsc2UpIHJhdGhlciB0aGFuXG4gKiByZWplY3Rpbmcg4oCUIGNhbGxlcnMgYXJlIHRpbWVycywgYW5kIGEgUmVkaXMgYmxpcCBtdXN0IG5vdCBjcmFzaCB0aGUgcHJvY2Vzcy5cbiAqXG4gKiBAcmV0dXJucyB0cnVlIGlmIHdlIGhlbGQgdGhlIGxvY2sgYW5kIHJhbiBgZm5gOyBmYWxzZSBpZiBhbm90aGVyIGhvbGRlciBkaWQgb3JcbiAqICAgdGhlIGxvY2sgY291bGRuJ3QgYmUgdGFrZW4uXG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiB3aXRoTGVhZGVyTG9jayhcbiAgcmVkaXM6IExvY2tSZWRpcyxcbiAga2V5OiBzdHJpbmcsXG4gIHR0bE1zOiBudW1iZXIsXG4gIGZuOiAoKSA9PiBQcm9taXNlPHZvaWQ+LFxuKTogUHJvbWlzZTxib29sZWFuPiB7XG4gIGNvbnN0IHRva2VuID0gcmFuZG9tVVVJRCgpO1xuICBsZXQgYWNxdWlyZWQ6IHVua25vd247XG4gIHRyeSB7XG4gICAgYWNxdWlyZWQgPSBhd2FpdCByZWRpcy5zZXQoa2V5LCB0b2tlbiwgJ1BYJywgdHRsTXMsICdOWCcpO1xuICB9IGNhdGNoIChlcnIpIHtcbiAgICAvLyBSZWRpcyB1bnJlYWNoYWJsZSAoaW5jbHVkaW5nIGJlZm9yZSB0aGUgZmlyc3QgY29ubmVjdGlvbiBjb21wbGV0ZXMpOiBub1xuICAgIC8vIHBvZCBjYW4gcHJvdmUgaXQncyB0aGUgbGVhZGVyLCBzbyBub2JvZHkgcnVucyB0aGlzIHdpbmRvdy4gU2tpcHBpbmcgaXNcbiAgICAvLyB0aGUgc2FmZSBzaWRlIOKAlCBydW5uaW5nIGhlcmUgY291bGQgZHVwbGljYXRlIGRlc3RydWN0aXZlIHdvcmsgYWNyb3NzIHBvZHMuXG4gICAgbG9ja0xvZ2dlci53YXJuKCdMZWFkZXIgbG9jayB1bmF2YWlsYWJsZTsgc2tpcHBpbmcgdGhpcyBydW4nLCB7XG4gICAgICBrZXksIGVycm9yOiBlcnIgaW5zdGFuY2VvZiBFcnJvciA/IGVyci5tZXNzYWdlIDogU3RyaW5nKGVyciksXG4gICAgfSk7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG4gIGlmIChhY3F1aXJlZCAhPT0gJ09LJykgcmV0dXJuIGZhbHNlO1xuXG4gIHRyeSB7XG4gICAgYXdhaXQgZm4oKTtcbiAgfSBmaW5hbGx5IHtcbiAgICAvLyBSZWxlYXNlIG9ubHkgaWYgdGhlIGxvY2sgaXMgc3RpbGwgb3VycyDigJQgaWYgb3VyIHJ1biBvdmVycmFuIHRoZSBUVEwgYW5kXG4gICAgLy8gYW5vdGhlciBob2xkZXIgdG9vayBvdmVyLCByZWxlYXNpbmcgaGVyZSB3b3VsZCBmcmVlIHRoZWlyIGxvY2sgZWFybHkuXG4gICAgLy8gUHJlZmVyIGFuIGF0b21pYyBDQVMgKEx1YSk7IGZhbGwgYmFjayB0byBnZXQtdGhlbi1kZWwgZm9yIGNsaWVudHMgd2l0aG91dFxuICAgIC8vIGV2YWwgKHRoZSBzbWFsbCBub24tYXRvbWljIHdpbmRvdyBpcyBUVEwtYm91bmRlZCkuXG4gICAgdHJ5IHtcbiAgICAgIGlmICh0eXBlb2YgcmVkaXMuZXZhbCA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICBhd2FpdCByZWRpcy5ldmFsKFJFTEVBU0VfSUZfT1dORVIsIDEsIGtleSwgdG9rZW4pO1xuICAgICAgfSBlbHNlIGlmICgoYXdhaXQgcmVkaXMuZ2V0KGtleSkpID09PSB0b2tlbikge1xuICAgICAgICBhd2FpdCByZWRpcy5kZWwoa2V5KTtcbiAgICAgIH1cbiAgICB9IGNhdGNoIHtcbiAgICAgIC8vIEJlc3QtZWZmb3J0OiB0aGUgVFRMIGV4cGlyZXMgdGhlIGxvY2sgcmVnYXJkbGVzcy5cbiAgICB9XG4gIH1cbiAgcmV0dXJuIHRydWU7XG59XG5cbi8qKlxuICogQSBgTG9ja1JlZGlzYCBiYWNrZWQgYnkgdGhlIHNoYXJlZCBlbnYgUmVkaXMgY2xpZW50IChzZWUgZW52LXJlZGlzLnRzKSDigJQgdGhlIGxlYWRlci1sb2NrIGNvdW50ZXJwYXJ0IHRvIGBjcmVhdGVFbnZSZWRpc0F1ZGl0U3Bvb2xgIC9cbiAqIGBjcmVhdGVFbnZSZWRpc1Rva2VuUmV2b2NhdGlvblN0b3JlYCwgZm9yIGEgc2VydmljZSB0aGF0IGhhcyBubyBCdWxsTVEgY2xpZW50IHRvXG4gKiBib3Jyb3cuIFJldHVybnMgYG51bGxgIHdoZW4gUmVkaXMgaXNuJ3QgY29uZmlndXJlZCwgc28gYSBjYWxsZXIgY2FuIGRlZ3JhZGUgdG9cbiAqIHJ1bm5pbmcgb24gZXZlcnkgcG9kICh0aGUgYXRvbWljIGd1YXJkcyBpdCB3cmFwcyBtdXN0IHN0aWxsIG1ha2UgdGhhdCBzYWZlKS5cbiAqIFRoZSBjbGllbnQgaXMgYSByZWFsIGlvcmVkaXMgaW5zdGFuY2U7IGlvcmVkaXMgZXhwb3NlcyBgc2V0YC9gZ2V0YC9gZGVsYC9gZXZhbGAsXG4gKiBzYXRpc2Z5aW5nIGBMb2NrUmVkaXNgIGluY2x1ZGluZyB0aGUgYXRvbWljIENBUyByZWxlYXNlLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlRW52UmVkaXNMb2NrKCk6IExvY2tSZWRpcyB8IG51bGwge1xuICBjb25zdCBpbnN0ID0gY3JlYXRlRW52UmVkaXNDbGllbnQ8TG9ja1JlZGlzPignbGVhZGVyLWxvY2snKTtcbiAgaWYgKGluc3QpIGxvY2tMb2dnZXIuaW5mbygnUmVkaXMgbGVhZGVyLWxvY2sgY2xpZW50IGluaXRpYWxpemVkJyk7XG4gIHJldHVybiBpbnN0O1xufVxuIl19
|
package/lib/services/quota.d.ts
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
1
2
|
import type { QuotaType, QuotaCheckResult } from '../types/common.js';
|
|
2
3
|
import { type QuotaTier } from '../types/quota-tiers.js';
|
|
4
|
+
/**
|
|
5
|
+
* Service-principal `Authorization` header for a quota call made on behalf of
|
|
6
|
+
* `orgId` by the CURRENT service (`SERVICE_NAME`, the same identity the health
|
|
7
|
+
* router reports).
|
|
8
|
+
*
|
|
9
|
+
* The quota service's mutation endpoints (`/:orgId/increment`, `/decrement`)
|
|
10
|
+
* reject anything but a signed service principal or a system admin, so
|
|
11
|
+
* forwarding the end user's bearer token there is always a 403. Service tokens
|
|
12
|
+
* are also exempt from the quota service's per-IP rate limiter, so hot-path
|
|
13
|
+
* quota traffic from a busy pod can't exhaust that pod IP's bucket and silently
|
|
14
|
+
* turn enforcement off. Scoped to the target org with the lowest role (member).
|
|
15
|
+
*/
|
|
16
|
+
export declare function getQuotaServiceAuthHeader(orgId: string): string;
|
|
3
17
|
/**
|
|
4
18
|
* Result of a synchronous quota reservation the atomic check+increment
|
|
5
19
|
* variant. `exceeded: true` means the operation was rejected at the DB
|
|
6
20
|
* level; the caller should return 429 without running the gated action.
|
|
7
21
|
*/
|
|
8
22
|
export interface QuotaReserveResult {
|
|
23
|
+
/** True when the slot was NOT reserved — the gated action must not run. */
|
|
9
24
|
exceeded: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Set (true) by the quota CLIENT when the reservation was denied because the
|
|
27
|
+
* quota service could not CONFIRM it (unreachable / timeout / circuit open /
|
|
28
|
+
* errored) — not because the org is over its limit. Always paired with
|
|
29
|
+
* `exceeded: true`. Answer with {@link sendQuotaReserveDenied}, which maps it to
|
|
30
|
+
* 503 instead of a misleading 429 "quota exceeded".
|
|
31
|
+
*/
|
|
32
|
+
unavailable?: boolean;
|
|
10
33
|
quota: {
|
|
11
34
|
type: QuotaType;
|
|
12
35
|
limit: number;
|
|
@@ -29,9 +52,9 @@ export interface QuotaService {
|
|
|
29
52
|
* reserved. Use this for expensive resources where the post-hoc fire-
|
|
30
53
|
* and-forget pattern allows concurrent over-spend.
|
|
31
54
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
55
|
+
* Fails CLOSED (`exceeded: true`) whenever the slot is not confirmed —
|
|
56
|
+
* unreachable, timeout, open circuit, non-ok, or a non-quota 429 — unless
|
|
57
|
+
* `QUOTA_RESERVE_FAIL_OPEN=true` (see the policy note at the top of this file).
|
|
35
58
|
*/
|
|
36
59
|
reserve(orgId: string, quotaType: QuotaType, authHeader: string, amount?: number, requestId?: string): Promise<QuotaReserveResult>;
|
|
37
60
|
/**
|
|
@@ -88,18 +111,20 @@ export interface QuotaServiceConfig {
|
|
|
88
111
|
*/
|
|
89
112
|
export declare function createQuotaService(config?: QuotaServiceConfig): QuotaService;
|
|
90
113
|
/**
|
|
91
|
-
* Fire-and-forget quota increment with standardized error logging.
|
|
114
|
+
* Fire-and-forget quota METERING increment with standardized error logging.
|
|
92
115
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
116
|
+
* Authenticates as the calling SERVICE ({@link getQuotaServiceAuthHeader}), never
|
|
117
|
+
* with the end user's token: `/quotas/:orgId/increment` is service-principal /
|
|
118
|
+
* system-admin only, so a forwarded user JWT was always rejected (403) and the
|
|
119
|
+
* metered quota (e.g. `apiCalls`, and the `api_pack` bundle raising it) never
|
|
120
|
+
* moved.
|
|
95
121
|
*
|
|
96
122
|
* @param quotaService - Quota service client
|
|
97
|
-
* @param orgId - Organization ID
|
|
123
|
+
* @param orgId - Organization ID whose counter is incremented
|
|
98
124
|
* @param quotaType - Quota type to increment
|
|
99
|
-
* @param authHeader - Authorization header value
|
|
100
125
|
* @param logWarn - Logging function for warnings
|
|
101
126
|
*/
|
|
102
|
-
export declare function incrementQuota(quotaService: QuotaService, orgId: string, quotaType: QuotaType,
|
|
127
|
+
export declare function incrementQuota(quotaService: QuotaService, orgId: string, quotaType: QuotaType, logWarn: (message: string, data?: unknown) => void): void;
|
|
103
128
|
/**
|
|
104
129
|
* Atomic reserve helper for the "reserve + commit / rollback" pattern.
|
|
105
130
|
* Use for expensive resources (pipelines, plugins, AI calls) where the
|
|
@@ -111,7 +136,7 @@ export declare function incrementQuota(quotaService: QuotaService, orgId: string
|
|
|
111
136
|
* @example
|
|
112
137
|
* ```typescript
|
|
113
138
|
* const reservation = await reserveQuota(quotaService, orgId, 'pipelines', authHeader);
|
|
114
|
-
* if (reservation.exceeded) return
|
|
139
|
+
* if (reservation.exceeded) return sendQuotaReserveDenied(res, 'pipelines', reservation);
|
|
115
140
|
* try {
|
|
116
141
|
* await doExpensiveThing();
|
|
117
142
|
* } catch (err) {
|
|
@@ -122,6 +147,15 @@ export declare function incrementQuota(quotaService: QuotaService, orgId: string
|
|
|
122
147
|
* ```
|
|
123
148
|
*/
|
|
124
149
|
export declare function reserveQuota(quotaService: QuotaService, orgId: string, quotaType: QuotaType, authHeader: string, amount?: number, requestId?: string): Promise<QuotaReserveResult>;
|
|
150
|
+
/**
|
|
151
|
+
* Answer a DENIED reservation (`reservation.exceeded === true`).
|
|
152
|
+
*
|
|
153
|
+
* - `unavailable` (the quota service couldn't confirm the slot) → **503**
|
|
154
|
+
* `SERVICE_UNAVAILABLE` with a short `Retry-After`: the org is not over its
|
|
155
|
+
* limit, so a 429 "quota exceeded" would send users to upgrade for an outage.
|
|
156
|
+
* - otherwise → the standard 429 `QUOTA_EXCEEDED` with the X-Quota-* headers.
|
|
157
|
+
*/
|
|
158
|
+
export declare function sendQuotaReserveDenied(res: Response, quotaType: QuotaType, reservation: QuotaReserveResult): void;
|
|
125
159
|
/**
|
|
126
160
|
* Fire-and-forget rollback for a previously reserved quota slot.
|
|
127
161
|
* Logs on failure but never throws the action that needed the rollback
|