@pipeline-builder/api-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +79 -0
- package/lib/api/app-factory.d.ts +95 -0
- package/lib/api/app-factory.js +328 -0
- package/lib/api/check-quota.d.ts +13 -0
- package/lib/api/check-quota.js +74 -0
- package/lib/api/context-middleware.d.ts +34 -0
- package/lib/api/context-middleware.js +42 -0
- package/lib/api/etag-middleware.d.ts +7 -0
- package/lib/api/etag-middleware.js +44 -0
- package/lib/api/get-context.d.ts +22 -0
- package/lib/api/get-context.js +28 -0
- package/lib/api/health-checks.d.ts +62 -0
- package/lib/api/health-checks.js +87 -0
- package/lib/api/idempotency-middleware.d.ts +80 -0
- package/lib/api/idempotency-middleware.js +321 -0
- package/lib/api/index.d.ts +20 -0
- package/lib/api/index.js +34 -0
- package/lib/api/meter-quota.d.ts +32 -0
- package/lib/api/meter-quota.js +70 -0
- package/lib/api/metrics.d.ts +47 -0
- package/lib/api/metrics.js +221 -0
- package/lib/api/middleware-factory.d.ts +56 -0
- package/lib/api/middleware-factory.js +80 -0
- package/lib/api/middleware.d.ts +1 -0
- package/lib/api/middleware.js +8 -0
- package/lib/api/mongo-connect.d.ts +19 -0
- package/lib/api/mongo-connect.js +41 -0
- package/lib/api/quota-helpers.d.ts +23 -0
- package/lib/api/quota-helpers.js +22 -0
- package/lib/api/rate-limit-by-org.d.ts +29 -0
- package/lib/api/rate-limit-by-org.js +64 -0
- package/lib/api/readiness.d.ts +29 -0
- package/lib/api/readiness.js +65 -0
- package/lib/api/request-types.d.ts +55 -0
- package/lib/api/request-types.js +65 -0
- package/lib/api/require-org-id.d.ts +14 -0
- package/lib/api/require-org-id.js +28 -0
- package/lib/api/route-wrapper.d.ts +50 -0
- package/lib/api/route-wrapper.js +84 -0
- package/lib/api/server.d.ts +79 -0
- package/lib/api/server.js +234 -0
- package/lib/api/tenant-context.d.ts +28 -0
- package/lib/api/tenant-context.js +48 -0
- package/lib/api/tracing.d.ts +25 -0
- package/lib/api/tracing.js +69 -0
- package/lib/http/index.d.ts +4 -0
- package/lib/http/index.js +7 -0
- package/lib/http/sse-connection-manager.d.ts +357 -0
- package/lib/http/sse-connection-manager.js +665 -0
- package/lib/http/sse-relay.d.ts +63 -0
- package/lib/http/sse-relay.js +70 -0
- package/lib/http/sse-ticket-channel.d.ts +31 -0
- package/lib/http/sse-ticket-channel.js +52 -0
- package/lib/http/sse-ticket-store.d.ts +75 -0
- package/lib/http/sse-ticket-store.js +160 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +35 -0
- package/lib/otel-bootstrap.d.ts +1 -0
- package/lib/otel-bootstrap.js +75 -0
- package/package.json +183 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { createLogger, sendError, ErrorCode, createEnvRedisClient } from '@pipeline-builder/api-core';
|
|
5
|
+
import { CoreConstants } from '@pipeline-builder/pipeline-core';
|
|
6
|
+
const logger = createLogger('idempotency');
|
|
7
|
+
/**
|
|
8
|
+
* Canonical (key-sorted) JSON serialization so the SAME logical payload hashes
|
|
9
|
+
* identically regardless of property insertion order — a plain `JSON.stringify`
|
|
10
|
+
* would let `{a,b}` and `{b,a}` produce different fingerprints for one request.
|
|
11
|
+
*/
|
|
12
|
+
function stableStringify(value) {
|
|
13
|
+
if (value === null || typeof value !== 'object')
|
|
14
|
+
return JSON.stringify(value) ?? 'null';
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return '[' + value.map(stableStringify).join(',') + ']';
|
|
17
|
+
const obj = value;
|
|
18
|
+
return '{' + Object.keys(obj).sort().map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k])).join(',') + '}';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Short, stable fingerprint of a request body. Folds the payload into the
|
|
22
|
+
* idempotency key so replaying the SAME key with a DIFFERENT body is treated as
|
|
23
|
+
* a distinct operation (fresh execution) rather than replaying the old response.
|
|
24
|
+
* Empty/absent bodies collapse to '-' so body-less mutations stay unaffected.
|
|
25
|
+
*/
|
|
26
|
+
function bodyFingerprint(body) {
|
|
27
|
+
if (body === undefined || body === null)
|
|
28
|
+
return '-';
|
|
29
|
+
if (typeof body === 'object' && Object.keys(body).length === 0)
|
|
30
|
+
return '-';
|
|
31
|
+
try {
|
|
32
|
+
return createHash('sha256').update(stableStringify(body)).digest('hex').slice(0, 16);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Non-serializable body (e.g. a stream/circular ref): skip fingerprinting
|
|
36
|
+
// rather than fail the request — key stays scoped by method+route+org.
|
|
37
|
+
return '-';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Default in-memory store. Single-replica only. Exported for testing. */
|
|
41
|
+
export function createMemoryStore() {
|
|
42
|
+
const map = new Map();
|
|
43
|
+
const cleanupTimer = setInterval(() => {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
for (const [k, v] of map) {
|
|
46
|
+
if (now > v.expiresAt)
|
|
47
|
+
map.delete(k);
|
|
48
|
+
}
|
|
49
|
+
}, CoreConstants.IDEMPOTENCY_CLEANUP_INTERVAL_MS);
|
|
50
|
+
cleanupTimer.unref();
|
|
51
|
+
/** Return the live (non-expired) entry, evicting it lazily if expired. */
|
|
52
|
+
const live = (key) => {
|
|
53
|
+
const entry = map.get(key);
|
|
54
|
+
if (!entry)
|
|
55
|
+
return null;
|
|
56
|
+
if (Date.now() > entry.expiresAt) {
|
|
57
|
+
map.delete(key);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return entry;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* At capacity, evict the OLDEST entry (Map preserves insertion order) rather
|
|
64
|
+
* than silently dropping the NEW key — otherwise, once full, the store stops
|
|
65
|
+
* deduplicating fresh requests until the TTL sweep runs. Stays bounded; favors
|
|
66
|
+
* recent keys (most likely to be retried).
|
|
67
|
+
*/
|
|
68
|
+
const evictIfFull = (key) => {
|
|
69
|
+
if (!map.has(key) && map.size >= CoreConstants.IDEMPOTENCY_MAX_STORE_SIZE) {
|
|
70
|
+
const oldest = map.keys().next().value;
|
|
71
|
+
if (oldest !== undefined)
|
|
72
|
+
map.delete(oldest);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
async get(key) {
|
|
77
|
+
return live(key);
|
|
78
|
+
},
|
|
79
|
+
async set(key, entry) {
|
|
80
|
+
evictIfFull(key);
|
|
81
|
+
map.set(key, entry);
|
|
82
|
+
},
|
|
83
|
+
async reserve(key, entry) {
|
|
84
|
+
// Atomic in a single-threaded runtime: no await between the liveness check
|
|
85
|
+
// and the set, so two concurrent callers can't both observe "absent".
|
|
86
|
+
if (live(key))
|
|
87
|
+
return false;
|
|
88
|
+
evictIfFull(key);
|
|
89
|
+
map.set(key, entry);
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
92
|
+
async delete(key) {
|
|
93
|
+
map.delete(key);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const REDIS_KEY_PREFIX = 'idem:';
|
|
98
|
+
/**
|
|
99
|
+
* Redis-backed {@link IdempotencyStore} for correct deduplication across
|
|
100
|
+
* horizontally-scaled replicas. `reserve` maps to an atomic `SET … NX` so two
|
|
101
|
+
* pods behind a load-balancer can't both win a first-time request sharing an
|
|
102
|
+
* Idempotency-Key; a retry landing on ANY pod sees the same committed response.
|
|
103
|
+
* Redis TTL owns expiry (the `CachedEntry.expiresAt` field is carried only for
|
|
104
|
+
* API-shape parity with the memory store).
|
|
105
|
+
*/
|
|
106
|
+
export function createRedisIdempotencyStore(redis) {
|
|
107
|
+
const k = (key) => `${REDIS_KEY_PREFIX}${key}`;
|
|
108
|
+
return {
|
|
109
|
+
async get(key) {
|
|
110
|
+
const raw = await redis.get(k(key));
|
|
111
|
+
if (!raw)
|
|
112
|
+
return null;
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(raw);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Corrupt entry → treat as absent so the request proceeds fresh.
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
async set(key, entry, ttlSeconds) {
|
|
122
|
+
await redis.set(k(key), JSON.stringify(entry), 'EX', Math.max(1, ttlSeconds));
|
|
123
|
+
},
|
|
124
|
+
async reserve(key, entry, ttlSeconds) {
|
|
125
|
+
// Atomic claim: SET key val EX ttl NX → 'OK' only if the key was absent.
|
|
126
|
+
const res = await redis.set(k(key), JSON.stringify(entry), 'EX', Math.max(1, ttlSeconds), 'NX');
|
|
127
|
+
return res === 'OK';
|
|
128
|
+
},
|
|
129
|
+
async delete(key) {
|
|
130
|
+
await redis.del(k(key));
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build a Redis-backed idempotency store from the shared env Redis (the same
|
|
136
|
+
* `REDIS_URL` / `REDIS_HOST` wiring the rate-limiter and audit-spool use).
|
|
137
|
+
* Returns `null` when Redis isn't configured/available so the caller keeps the
|
|
138
|
+
* in-memory default (single-replica correctness). Never throws.
|
|
139
|
+
*/
|
|
140
|
+
export function createEnvRedisIdempotencyStore() {
|
|
141
|
+
const client = createEnvRedisClient('idempotency');
|
|
142
|
+
if (!client)
|
|
143
|
+
return null;
|
|
144
|
+
logger.info('Redis idempotency store initialized');
|
|
145
|
+
return createRedisIdempotencyStore(client);
|
|
146
|
+
}
|
|
147
|
+
const memoryStore = createMemoryStore();
|
|
148
|
+
/**
|
|
149
|
+
* Process-wide default store used by `idempotencyMiddleware()` when no explicit
|
|
150
|
+
* `store` is passed. Defaults to the in-memory store; `createApp` promotes it to
|
|
151
|
+
* the shared env-Redis store (multi-replica dedup) at startup via
|
|
152
|
+
* {@link setIdempotencyStore}. The route factories keep calling
|
|
153
|
+
* `idempotencyMiddleware()` with no args and transparently pick this up.
|
|
154
|
+
*/
|
|
155
|
+
let defaultStore = memoryStore;
|
|
156
|
+
/** Inject the process-wide idempotency store (e.g. a Redis-backed one). */
|
|
157
|
+
export function setIdempotencyStore(store) {
|
|
158
|
+
defaultStore = store;
|
|
159
|
+
}
|
|
160
|
+
/** The current process-wide default store (in-memory unless overridden). */
|
|
161
|
+
export function getIdempotencyStore() {
|
|
162
|
+
return defaultStore;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Middleware that supports idempotency keys for POST/PUT/DELETE mutations.
|
|
166
|
+
*
|
|
167
|
+
* When a request includes the `Idempotency-Key` header:
|
|
168
|
+
* - First call: processes normally, caches the response
|
|
169
|
+
* - Subsequent calls with same key: returns cached response (prevents duplicate mutations)
|
|
170
|
+
*
|
|
171
|
+
* Defaults to an in-memory store (single-replica). Pass a custom
|
|
172
|
+
* cross-replica `{ store }` implementing `IdempotencyStore` to dedupe across
|
|
173
|
+
* replicas.
|
|
174
|
+
*/
|
|
175
|
+
export function idempotencyMiddleware(options = {}) {
|
|
176
|
+
// Resolve the store PER REQUEST (not at factory time) so a store injected by
|
|
177
|
+
// createApp AFTER the route chains are built still takes effect. An explicit
|
|
178
|
+
// `options.store` always wins.
|
|
179
|
+
const explicitStore = options.store;
|
|
180
|
+
const ttlMs = CoreConstants.IDEMPOTENCY_TTL_MS;
|
|
181
|
+
const ttlSec = Math.floor(ttlMs / 1000);
|
|
182
|
+
return (req, res, next) => {
|
|
183
|
+
const store = explicitStore ?? defaultStore;
|
|
184
|
+
const key = req.headers['idempotency-key'];
|
|
185
|
+
if (!key)
|
|
186
|
+
return next();
|
|
187
|
+
// Only apply to mutation methods
|
|
188
|
+
if (!['POST', 'PUT', 'DELETE'].includes(req.method))
|
|
189
|
+
return next();
|
|
190
|
+
// Namespace by orgId to prevent cross-org cache collisions. Prefer the
|
|
191
|
+
// VERIFIED auth org (`req.user`, populated by requireAuth after signature
|
|
192
|
+
// verification) over `req.context.identity`, which some services populate
|
|
193
|
+
// pre-auth from an UNVERIFIED token peek — namespacing a mutation's replay
|
|
194
|
+
// cache on an unverified, caller-influenced org id would be unsafe.
|
|
195
|
+
const orgId = req.user?.organizationId || req.context?.identity?.orgId;
|
|
196
|
+
if (!orgId)
|
|
197
|
+
return next(); // skip idempotency for unauthenticated requests
|
|
198
|
+
// Namespace the stored key by (method, route, org, body) so an Idempotency-Key
|
|
199
|
+
// is scoped to ONE specific endpoint+payload. Reusing a single key across two
|
|
200
|
+
// routes (e.g. POST /pipelines then POST /plugins) previously replayed the
|
|
201
|
+
// first endpoint's 2xx for the second — the store is a module-level singleton
|
|
202
|
+
// shared by every route. Folding in method + route path + a body fingerprint
|
|
203
|
+
// only makes keys MORE specific, so there is no cross-contamination risk.
|
|
204
|
+
const routePath = (req.baseUrl ?? '') + (req.route?.path ?? req.path ?? '');
|
|
205
|
+
const fullKey = `${req.method}:${routePath}:${orgId}:${bodyFingerprint(req.body)}:${key}`;
|
|
206
|
+
/** Reject a duplicate whose original is still in-flight. */
|
|
207
|
+
const sendInProgress = () => {
|
|
208
|
+
res.setHeader('Retry-After', '1');
|
|
209
|
+
sendError(res, 409, 'A request with this Idempotency-Key is already being processed', ErrorCode.CONFLICT);
|
|
210
|
+
};
|
|
211
|
+
store.get(fullKey).then(async (cached) => {
|
|
212
|
+
if (cached && !cached.pending) {
|
|
213
|
+
// Completed response on record → replay it.
|
|
214
|
+
logger.debug('Idempotent request replayed', { key: fullKey });
|
|
215
|
+
res.setHeader('X-Idempotent-Replayed', 'true');
|
|
216
|
+
res.status(cached.statusCode).json(cached.body);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (cached && cached.pending) {
|
|
220
|
+
// The original is still running — don't run the mutation a second time.
|
|
221
|
+
logger.debug('Idempotent request rejected (in progress)', { key: fullKey });
|
|
222
|
+
return sendInProgress();
|
|
223
|
+
}
|
|
224
|
+
// Atomically reserve the key. Losing this race means a concurrent request
|
|
225
|
+
// with the same key got there first and is in-flight → 409.
|
|
226
|
+
const won = await store.reserve(fullKey, { statusCode: 0, body: null, pending: true, expiresAt: Date.now() + ttlMs }, ttlSec);
|
|
227
|
+
if (!won) {
|
|
228
|
+
logger.debug('Idempotent request rejected (lost reserve race)', { key: fullKey });
|
|
229
|
+
return sendInProgress();
|
|
230
|
+
}
|
|
231
|
+
// We own the reservation. It MUST be resolved exactly once — either the
|
|
232
|
+
// real 2xx response is cached for replay, or the reservation is released so
|
|
233
|
+
// a retry can proceed. `settled` guards against a double-resolve.
|
|
234
|
+
let settled = false;
|
|
235
|
+
let capturedBody;
|
|
236
|
+
let bodyCaptured = false;
|
|
237
|
+
/** Remember the response body for replay (first capture wins). */
|
|
238
|
+
const captureBody = (body) => {
|
|
239
|
+
if (!bodyCaptured) {
|
|
240
|
+
capturedBody = body;
|
|
241
|
+
bodyCaptured = true;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const release = () => {
|
|
245
|
+
if (settled)
|
|
246
|
+
return;
|
|
247
|
+
settled = true;
|
|
248
|
+
store.delete(fullKey).catch((err) => {
|
|
249
|
+
logger.warn('Idempotency reservation release failed', { key: fullKey, error: err instanceof Error ? err.message : String(err) });
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
const cacheSuccess = () => {
|
|
253
|
+
if (settled)
|
|
254
|
+
return;
|
|
255
|
+
settled = true;
|
|
256
|
+
store.set(fullKey, {
|
|
257
|
+
statusCode: res.statusCode,
|
|
258
|
+
body: bodyCaptured ? capturedBody : null,
|
|
259
|
+
expiresAt: Date.now() + ttlMs,
|
|
260
|
+
}, ttlSec).catch((err) => {
|
|
261
|
+
logger.warn('Idempotency store.set failed', { key: fullKey, error: err instanceof Error ? err.message : String(err) });
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
/** Resolve the reservation from the ACTUAL final status: cache 2xx, else release. */
|
|
265
|
+
const settleFromStatus = () => {
|
|
266
|
+
if (settled)
|
|
267
|
+
return;
|
|
268
|
+
if (res.statusCode >= 200 && res.statusCode < 300)
|
|
269
|
+
cacheSuccess();
|
|
270
|
+
else
|
|
271
|
+
release();
|
|
272
|
+
};
|
|
273
|
+
// Fast path: res.json is the common completion. Capture the body and settle
|
|
274
|
+
// immediately off the status set by res.status(...).json(...).
|
|
275
|
+
const originalJson = res.json.bind(res);
|
|
276
|
+
res.json = (body) => {
|
|
277
|
+
res.setHeader('X-Idempotent-Replayed', 'false');
|
|
278
|
+
captureBody(body);
|
|
279
|
+
settleFromStatus();
|
|
280
|
+
return originalJson(body);
|
|
281
|
+
};
|
|
282
|
+
// Body-capture backstops for completions that bypass res.json
|
|
283
|
+
// (res.send / res.end / res.sendFile → res.end). These only RECORD the body;
|
|
284
|
+
// the actual settle happens on `finish` from the real status code. First
|
|
285
|
+
// capture wins, so res.json's body is never clobbered by its internal
|
|
286
|
+
// res.send/res.end calls.
|
|
287
|
+
const patched = res;
|
|
288
|
+
if (typeof patched.send === 'function') {
|
|
289
|
+
const originalSend = patched.send.bind(res);
|
|
290
|
+
patched.send = (body) => {
|
|
291
|
+
captureBody(body);
|
|
292
|
+
return originalSend(body);
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (typeof patched.end === 'function') {
|
|
296
|
+
const originalEnd = patched.end.bind(res);
|
|
297
|
+
patched.end = (...args) => {
|
|
298
|
+
const chunk = args[0];
|
|
299
|
+
if (chunk !== undefined && typeof chunk !== 'function')
|
|
300
|
+
captureBody(chunk);
|
|
301
|
+
return originalEnd(...args);
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
// Backstop for ANY completion path — including a 2xx that finished via
|
|
305
|
+
// res.send/res.end/res.sendFile/204 and never hit the patched res.json.
|
|
306
|
+
// Deciding on the real final status here means a successful op is cached
|
|
307
|
+
// (or a failed one released) instead of stranding the reservation and
|
|
308
|
+
// 409-ing every retry for the full TTL. Guarded — test doubles may not be
|
|
309
|
+
// EventEmitters; res.json's fast path already settled the common case.
|
|
310
|
+
if (typeof patched.on === 'function') {
|
|
311
|
+
patched.on('finish', settleFromStatus);
|
|
312
|
+
}
|
|
313
|
+
next();
|
|
314
|
+
}).catch((err) => {
|
|
315
|
+
// If the store backend is down, fail open — process the request normally.
|
|
316
|
+
logger.warn('Idempotency store.get failed; proceeding without dedup', { key: fullKey, error: err instanceof Error ? err.message : String(err) });
|
|
317
|
+
next();
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaWRlbXBvdGVuY3ktbWlkZGxld2FyZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hcGkvaWRlbXBvdGVuY3ktbWlkZGxld2FyZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxRQUFRLENBQUM7QUFDcEMsT0FBTyxFQUFFLFlBQVksRUFBRSxTQUFTLEVBQUUsU0FBUyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFDdEcsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBR2hFLE1BQU0sTUFBTSxHQUFHLFlBQVksQ0FBQyxhQUFhLENBQUMsQ0FBQztBQUUzQzs7OztHQUlHO0FBQ0gsU0FBUyxlQUFlLENBQUMsS0FBYztJQUNyQyxJQUFJLEtBQUssS0FBSyxJQUFJLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtRQUFFLE9BQU8sSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsSUFBSSxNQUFNLENBQUM7SUFDeEYsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQztRQUFFLE9BQU8sR0FBRyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsZUFBZSxDQUFDLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLEdBQUcsQ0FBQztJQUNsRixNQUFNLEdBQUcsR0FBRyxLQUFnQyxDQUFDO0lBQzdDLE9BQU8sR0FBRyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxHQUFHLEdBQUcsR0FBRyxlQUFlLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcs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LENBQUM7b0JBQzNFLE9BQU8sV0FBVyxDQUFDLEdBQUcsSUFBSSxDQUFDLENBQUM7Z0JBQzlCLENBQUMsQ0FBQztZQUNKLENBQUM7WUFFRCx1RUFBdUU7WUFDdkUsd0VBQXdFO1lBQ3hFLHlFQUF5RTtZQUN6RSxzRUFBc0U7WUFDdEUsMEVBQTBFO1lBQzFFLHVFQUF1RTtZQUN2RSxJQUFJLE9BQU8sT0FBTyxDQUFDLEVBQUUsS0FBSyxVQUFVLEVBQUUsQ0FBQztnQkFDckMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxRQUFRLEVBQUUsZ0JBQWdCLENBQUMsQ0FBQztZQUN6QyxDQUFDO1lBRUQsSUFBSSxFQUFFLENBQUM7UUFDVCxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxHQUFHLEVBQUUsRUFBRTtZQUNmLDBFQUEwRTtZQUMxRSxNQUFNLENBQUMsSUFBSSxDQUFDLHdEQUF3RCxFQUFFLEVBQUUsR0FBRyxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsR0FBRyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztZQUNqSixJQUFJLEVBQUUsQ0FBQztRQUNULENBQUMsQ0FBQyxDQUFDO0lBQ0wsQ0FBQyxDQUFDO0FBQ0osQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG5pbXBvcnQgeyBjcmVhdGVIYXNoIH0gZnJvbSAnY3J5cHRvJztcbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciwgc2VuZEVycm9yLCBFcnJvckNvZGUsIGNyZWF0ZUVudlJlZGlzQ2xpZW50IH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvYXBpLWNvcmUnO1xuaW1wb3J0IHsgQ29yZUNvbnN0YW50cyB9IGZyb20gJ0BwaXBlbGluZS1idWlsZGVyL3BpcGVsaW5lLWNvcmUnO1xuaW1wb3J0IHR5cGUgeyBSZXF1ZXN0LCBSZXNwb25zZSwgTmV4dEZ1bmN0aW9uIH0gZnJvbSAnZXhwcmVzcyc7XG5cbmNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcignaWRlbXBvdGVuY3knKTtcblxuLyoqXG4gKiBDYW5vbmljYWwgKGtleS1zb3J0ZWQpIEpTT04gc2VyaWFsaXphdGlvbiBzbyB0aGUgU0FNRSBsb2dpY2FsIHBheWxvYWQgaGFzaGVzXG4gKiBpZGVudGljYWxseSByZWdhcmRsZXNzIG9mIHByb3BlcnR5IGluc2VydGlvbiBvcmRlciDigJQgYSBwbGFpbiBgSlNPTi5zdHJpbmdpZnlgXG4gKiB3b3VsZCBsZXQgYHthLGJ9YCBhbmQgYHtiLGF9YCBwcm9kdWNlIGRpZmZlcmVudCBmaW5nZXJwcmludHMgZm9yIG9uZSByZXF1ZXN0LlxuICovXG5mdW5jdGlvbiBzdGFibGVTdHJpbmdpZnkodmFsdWU6IHVua25vd24pOiBzdHJpbmcge1xuICBpZiAodmFsdWUgPT09IG51bGwgfHwgdHlwZW9mIHZhbHVlICE9PSAnb2JqZWN0JykgcmV0dXJuIEpTT04uc3RyaW5naWZ5KHZhbHVlKSA/PyAnbnVsbCc7XG4gIGlmIChBcnJheS5pc0FycmF5KHZhbHVlKSkgcmV0dXJuICdbJyArIHZhbHVlLm1hcChzdGFibGVTdHJpbmdpZnkpLmpvaW4oJywnKSArICddJztcbiAgY29uc3Qgb2JqID0gdmFsdWUgYXMgUmVjb3JkPHN0cmluZywgdW5rbm93bj47XG4gIHJldHVybiAneycgKyBPYmplY3Qua2V5cyhvYmopLnNvcnQoKS5tYXAoKGspID0+IEpTT04uc3RyaW5naWZ5KGspICsgJzonICsgc3RhYmxlU3RyaW5naWZ5KG9ialtrXSkpLmpvaW4oJywnKSArICd9Jztcbn1cblxuLyoqXG4gKiBTaG9ydCwgc3RhYmxlIGZpbmdlcnByaW50IG9mIGEgcmVxdWVzdCBib2R5LiBGb2xkcyB0aGUgcGF5bG9hZCBpbnRvIHRoZVxuICogaWRlbXBvdGVuY3kga2V5IHNvIHJlcGxheWluZyB0aGUgU0FNRSBrZXkgd2l0aCBhIERJRkZFUkVOVCBib2R5IGlzIHRyZWF0ZWQgYXNcbiAqIGEgZGlzdGluY3Qgb3BlcmF0aW9uIChmcmVzaCBleGVjdXRpb24pIHJhdGhlciB0aGFuIHJlcGxheWluZyB0aGUgb2xkIHJlc3BvbnNlLlxuICogRW1wdHkvYWJzZW50IGJvZGllcyBjb2xsYXBzZSB0byAnLScgc28gYm9keS1sZXNzIG11dGF0aW9ucyBzdGF5IHVuYWZmZWN0ZWQuXG4gKi9cbmZ1bmN0aW9uIGJvZHlGaW5nZXJwcmludChib2R5OiB1bmtub3duKTogc3RyaW5nIHtcbiAgaWYgKGJvZHkgPT09IHVuZGVmaW5lZCB8fCBib2R5ID09PSBudWxsKSByZXR1cm4gJy0nO1xuICBpZiAodHlwZW9mIGJvZHkgPT09ICdvYmplY3QnICYmIE9iamVjdC5rZXlzKGJvZHkgYXMgb2JqZWN0KS5sZW5ndGggPT09IDApIHJldHVybiAnLSc7XG4gIHRyeSB7XG4gICAgcmV0dXJuIGNyZWF0ZUhhc2goJ3NoYTI1NicpLnVwZGF0ZShzdGFibGVTdHJpbmdpZnkoYm9keSkpLmRpZ2VzdCgnaGV4Jykuc2xpY2UoMCwgMTYpO1xuICB9IGNhdGNoIHtcbiAgICAvLyBOb24tc2VyaWFsaXphYmxlIGJvZHkgKGUuZy4gYSBzdHJlYW0vY2lyY3VsYXIgcmVmKTogc2tpcCBmaW5nZXJwcmludGluZ1xuICAgIC8vIHJhdGhlciB0aGFuIGZhaWwgdGhlIHJlcXVlc3Qg4oCUIGtleSBzdGF5cyBzY29wZWQgYnkgbWV0aG9kK3JvdXRlK29yZy5cbiAgICByZXR1cm4gJy0nO1xuICB9XG59XG5cbmludGVyZmFjZSBDYWNoZWRFbnRyeSB7XG4gIHN0YXR1c0NvZGU6IG51bWJlcjtcbiAgYm9keTogdW5rbm93bjtcbiAgZXhwaXJlc0F0OiBudW1iZXI7XG4gIC8qKiBSZXNlcnZhdGlvbiBwbGFjZWhvbGRlcjogdGhlIHJlcXVlc3QgaXMgaW4tZmxpZ2h0LCBubyByZWFsIHJlc3BvbnNlIHlldC5cbiAgICogIEEgZHVwbGljYXRlIHRoYXQgc2VlcyBhIHBlbmRpbmcgZW50cnkgaXMgcmVqZWN0ZWQgd2l0aCA0MDksIG5vdCByZXBsYXllZC4gKi9cbiAgcGVuZGluZz86IGJvb2xlYW47XG59XG5cbi8qKlxuICogUGx1Z2dhYmxlIGJhY2tlbmQgZm9yIGlkZW1wb3RlbmN5LWtleSByZXBsYXkgY2FjaGUuIFRoZSBkZWZhdWx0IGluLW1lbW9yeVxuICogaW1wbGVtZW50YXRpb24gd29ya3MgZmluZSBzaW5nbGUtcmVwbGljYTsgYSBSZWRpcy1iYWNrZWQgaW1wbGVtZW50YXRpb25cbiAqIGlzIHJlcXVpcmVkIGZvciBjb3JyZWN0IGRlZHVwbGljYXRpb24gYWNyb3NzIGhvcml6b250YWxseS1zY2FsZWQgcmVwbGljYXNcbiAqIChvdGhlcndpc2UgdHdvIHJlcGxpY2FzIGJlaGluZCBhIGxvYWQtYmFsYW5jZXIgY2FjaGUgaW5kZXBlbmRlbnRseSBhbmQgYVxuICogcmV0cnkgaGl0dGluZyBhIGRpZmZlcmVudCBwb2Qgd29uJ3QgcmVwbGF5KS5cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBJZGVtcG90ZW5jeVN0b3JlIHtcbiAgZ2V0KGtleTogc3RyaW5nKTogUHJvbWlzZTxDYWNoZWRFbnRyeSB8IG51bGw+O1xuICBzZXQoa2V5OiBzdHJpbmcsIGVudHJ5OiBDYWNoZWRFbnRyeSwgdHRsU2Vjb25kczogbnVtYmVyKTogUHJvbWlzZTx2b2lkPjtcbiAgLyoqXG4gICAqIEF0b21pY2FsbHkgY2xhaW0gYGtleWAgSUZGIGl0IGlzIGN1cnJlbnRseSBhYnNlbnQuIFJldHVybnMgYHRydWVgIHdoZW4gVEhJU1xuICAgKiBjYWxsZXIgc2V0IHRoZSBlbnRyeSAod29uIHRoZSByYWNlKSwgYGZhbHNlYCB3aGVuIGFuIGVudHJ5IGFscmVhZHkgZXhpc3RzLlxuICAgKiBUaGlzIGlzIHdoYXQgc2VyaWFsaXplcyBjb25jdXJyZW50IGZpcnN0LXRpbWUgcmVxdWVzdHMgdGhhdCBzaGFyZSBhblxuICAgKiBJZGVtcG90ZW5jeS1LZXkg4oCUIGEgcGxhaW4gZ2V0LXRoZW4tc2V0IGxlYXZlcyBhIHdpbmRvdyB3aGVyZSB0d28gcmVxdWVzdHNcbiAgICogYm90aCBzZWUgXCJub3QgY2FjaGVkXCIgYW5kIGJvdGggZXhlY3V0ZSB0aGUgbXV0YXRpb24uXG4gICAqL1xuICByZXNlcnZlKGtleTogc3RyaW5nLCBlbnRyeTogQ2FjaGVkRW50cnksIHR0bFNlY29uZHM6IG51bWJlcik6IFByb21pc2U8Ym9vbGVhbj47XG4gIC8qKiBSZW1vdmUgYSBrZXkg4oCUIHJlbGVhc2VzIGEgcmVzZXJ2YXRpb24gYWZ0ZXIgYSBub24tY2FjaGVhYmxlIChub24tMnh4KSByZXNwb25zZS4gKi9cbiAgZGVsZXRlKGtleTogc3RyaW5nKTogUHJvbWlzZTx2b2lkPjtcbn1cblxuLyoqIERlZmF1bHQgaW4tbWVtb3J5IHN0b3JlLiBTaW5nbGUtcmVwbGljYSBvbmx5LiBFeHBvcnRlZCBmb3IgdGVzdGluZy4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVNZW1vcnlTdG9yZSgpOiBJZGVtcG90ZW5jeVN0b3JlIHtcbiAgY29uc3QgbWFwID0gbmV3IE1hcDxzdHJpbmcsIENhY2hlZEVudHJ5PigpO1xuICBjb25zdCBjbGVhbnVwVGltZXIgPSBzZXRJbnRlcnZhbCgoKSA9PiB7XG4gICAgY29uc3Qgbm93ID0gRGF0ZS5ub3coKTtcbiAgICBmb3IgKGNvbnN0IFtrLCB2XSBvZiBtYXApIHtcbiAgICAgIGlmIChub3cgPiB2LmV4cGlyZXNBdCkgbWFwLmRlbGV0ZShrKTtcbiAgICB9XG4gIH0sIENvcmVDb25zdGFudHMuSURFTVBPVEVOQ1lfQ0xFQU5VUF9JTlRFUlZBTF9NUyk7XG4gIGNsZWFudXBUaW1lci51bnJlZigpO1xuXG4gIC8qKiBSZXR1cm4gdGhlIGxpdmUgKG5vbi1leHBpcmVkKSBlbnRyeSwgZXZpY3RpbmcgaXQgbGF6aWx5IGlmIGV4cGlyZWQuICovXG4gIGNvbnN0IGxpdmUgPSAoa2V5OiBzdHJpbmcpOiBDYWNoZWRFbnRyeSB8IG51bGwgPT4ge1xuICAgIGNvbnN0IGVudHJ5ID0gbWFwLmdldChrZXkpO1xuICAgIGlmICghZW50cnkpIHJldHVybiBudWxsO1xuICAgIGlmIChEYXRlLm5vdygpID4gZW50cnkuZXhwaXJlc0F0KSB7XG4gICAgICBtYXAuZGVsZXRlKGtleSk7XG4gICAgICByZXR1cm4gbnVsbDtcbiAgICB9XG4gICAgcmV0dXJuIGVudHJ5O1xuICB9O1xuICAvKipcbiAgICogQXQgY2FwYWNpdHksIGV2aWN0IHRoZSBPTERFU1QgZW50cnkgKE1hcCBwcmVzZXJ2ZXMgaW5zZXJ0aW9uIG9yZGVyKSByYXRoZXJcbiAgICogdGhhbiBzaWxlbnRseSBkcm9wcGluZyB0aGUgTkVXIGtleSDigJQgb3RoZXJ3aXNlLCBvbmNlIGZ1bGwsIHRoZSBzdG9yZSBzdG9wc1xuICAgKiBkZWR1cGxpY2F0aW5nIGZyZXNoIHJlcXVlc3RzIHVudGlsIHRoZSBUVEwgc3dlZXAgcnVucy4gU3RheXMgYm91bmRlZDsgZmF2b3JzXG4gICAqIHJlY2VudCBrZXlzIChtb3N0IGxpa2VseSB0byBiZSByZXRyaWVkKS5cbiAgICovXG4gIGNvbnN0IGV2aWN0SWZGdWxsID0gKGtleTogc3RyaW5nKTogdm9pZCA9PiB7XG4gICAgaWYgKCFtYXAuaGFzKGtleSkgJiYgbWFwLnNpemUgPj0gQ29yZUNvbnN0YW50cy5JREVNUE9URU5DWV9NQVhfU1RPUkVfU0laRSkge1xuICAgICAgY29uc3Qgb2xkZXN0ID0gbWFwLmtleXMoKS5uZXh0KCkudmFsdWU7XG4gICAgICBpZiAob2xkZXN0ICE9PSB1bmRlZmluZWQpIG1hcC5kZWxldGUob2xkZXN0KTtcbiAgICB9XG4gIH07XG4gIHJldHVybiB7XG4gICAgYXN5bmMgZ2V0KGtleSkge1xuICAgICAgcmV0dXJuIGxpdmUoa2V5KTtcbiAgICB9LFxuICAgIGFzeW5jIHNldChrZXksIGVudHJ5KSB7XG4gICAgICBldmljdElmRnVsbChrZXkpO1xuICAgICAgbWFwLnNldChrZXksIGVudHJ5KTtcbiAgICB9LFxuICAgIGFzeW5jIHJlc2VydmUoa2V5LCBlbnRyeSkge1xuICAgICAgLy8gQXRvbWljIGluIGEgc2luZ2xlLXRocmVhZGVkIHJ1bnRpbWU6IG5vIGF3YWl0IGJldHdlZW4gdGhlIGxpdmVuZXNzIGNoZWNrXG4gICAgICAvLyBhbmQgdGhlIHNldCwgc28gdHdvIGNvbmN1cnJlbnQgY2FsbGVycyBjYW4ndCBib3RoIG9ic2VydmUgXCJhYnNlbnRcIi5cbiAgICAgIGlmIChsaXZlKGtleSkpIHJldHVybiBmYWxzZTtcbiAgICAgIGV2aWN0SWZGdWxsKGtleSk7XG4gICAgICBtYXAuc2V0KGtleSwgZW50cnkpO1xuICAgICAgcmV0dXJuIHRydWU7XG4gICAgfSxcbiAgICBhc3luYyBkZWxldGUoa2V5KSB7XG4gICAgICBtYXAuZGVsZXRlKGtleSk7XG4gICAgfSxcbiAgfTtcbn1cblxuLyoqXG4gKiBNaW5pbWFsIFJlZGlzIHN1cmZhY2UgdGhlIGlkZW1wb3RlbmN5IHN0b3JlIG5lZWRzIChhbiBpb3JlZGlzIHN1YnNldCkuXG4gKiBgc2V0YCBzdXBwb3J0cyB0aGUgdmFyaWFkaWMgb3B0aW9uIHRhaWw6IGBTRVQga2V5IHZhbCBFWCA8dHRsPiBbTlhdYCwgd2hpY2hcbiAqIHJldHVybnMgYCdPSydgIG9uIHN1Y2Nlc3MgYW5kIGBudWxsYCB3aGVuIGFuIGBOWGAgc2V0IHdhcyByZWZ1c2VkLlxuICovXG5leHBvcnQgaW50ZXJmYWNlIFJlZGlzSWRlbXBvdGVuY3lDbGllbnQge1xuICBnZXQoa2V5OiBzdHJpbmcpOiBQcm9taXNlPHN0cmluZyB8IG51bGw+O1xuICBzZXQoa2V5OiBzdHJpbmcsIHZhbHVlOiBzdHJpbmcsIC4uLmFyZ3M6IChzdHJpbmcgfCBudW1iZXIpW10pOiBQcm9taXNlPHN0cmluZyB8IG51bGw+O1xuICBkZWwoa2V5OiBzdHJpbmcpOiBQcm9taXNlPG51bWJlcj47XG59XG5cbmNvbnN0IFJFRElTX0tFWV9QUkVGSVggPSAnaWRlbTonO1xuXG4vKipcbiAqIFJlZGlzLWJhY2tlZCB7QGxpbmsgSWRlbXBvdGVuY3lTdG9yZX0gZm9yIGNvcnJlY3QgZGVkdXBsaWNhdGlvbiBhY3Jvc3NcbiAqIGhvcml6b250YWxseS1zY2FsZWQgcmVwbGljYXMuIGByZXNlcnZlYCBtYXBzIHRvIGFuIGF0b21pYyBgU0VUIOKApiBOWGAgc28gdHdvXG4gKiBwb2RzIGJlaGluZCBhIGxvYWQtYmFsYW5jZXIgY2FuJ3QgYm90aCB3aW4gYSBmaXJzdC10aW1lIHJlcXVlc3Qgc2hhcmluZyBhblxuICogSWRlbXBvdGVuY3ktS2V5OyBhIHJldHJ5IGxhbmRpbmcgb24gQU5ZIHBvZCBzZWVzIHRoZSBzYW1lIGNvbW1pdHRlZCByZXNwb25zZS5cbiAqIFJlZGlzIFRUTCBvd25zIGV4cGlyeSAodGhlIGBDYWNoZWRFbnRyeS5leHBpcmVzQXRgIGZpZWxkIGlzIGNhcnJpZWQgb25seSBmb3JcbiAqIEFQSS1zaGFwZSBwYXJpdHkgd2l0aCB0aGUgbWVtb3J5IHN0b3JlKS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZVJlZGlzSWRlbXBvdGVuY3lTdG9yZShyZWRpczogUmVkaXNJZGVtcG90ZW5jeUNsaWVudCk6IElkZW1wb3RlbmN5U3RvcmUge1xuICBjb25zdCBrID0gKGtleTogc3RyaW5nKTogc3RyaW5nID0+IGAke1JFRElTX0tFWV9QUkVGSVh9JHtrZXl9YDtcbiAgcmV0dXJuIHtcbiAgICBhc3luYyBnZXQoa2V5KSB7XG4gICAgICBjb25zdCByYXcgPSBhd2FpdCByZWRpcy5nZXQoayhrZXkpKTtcbiAgICAgIGlmICghcmF3KSByZXR1cm4gbnVsbDtcbiAgICAgIHRyeSB7XG4gICAgICAgIHJldHVybiBKU09OLnBhcnNlKHJhdykgYXMgQ2FjaGVkRW50cnk7XG4gICAgICB9IGNhdGNoIHtcbiAgICAgICAgLy8gQ29ycnVwdCBlbnRyeSDihpIgdHJlYXQgYXMgYWJzZW50IHNvIHRoZSByZXF1ZXN0IHByb2NlZWRzIGZyZXNoLlxuICAgICAgICByZXR1cm4gbnVsbDtcbiAgICAgIH1cbiAgICB9LFxuICAgIGFzeW5jIHNldChrZXksIGVudHJ5LCB0dGxTZWNvbmRzKSB7XG4gICAgICBhd2FpdCByZWRpcy5zZXQoayhrZXkpLCBKU09OLnN0cmluZ2lmeShlbnRyeSksICdFWCcsIE1hdGgubWF4KDEsIHR0bFNlY29uZHMpKTtcbiAgICB9LFxuICAgIGFzeW5jIHJlc2VydmUoa2V5LCBlbnRyeSwgdHRsU2Vjb25kcykge1xuICAgICAgLy8gQXRvbWljIGNsYWltOiBTRVQga2V5IHZhbCBFWCB0dGwgTlgg4oaSICdPSycgb25seSBpZiB0aGUga2V5IHdhcyBhYnNlbnQuXG4gICAgICBjb25zdCByZXMgPSBhd2FpdCByZWRpcy5zZXQoayhrZXkpLCBKU09OLnN0cmluZ2lmeShlbnRyeSksICdFWCcsIE1hdGgubWF4KDEsIHR0bFNlY29uZHMpLCAnTlgnKTtcbiAgICAgIHJldHVybiByZXMgPT09ICdPSyc7XG4gICAgfSxcbiAgICBhc3luYyBkZWxldGUoa2V5KSB7XG4gICAgICBhd2FpdCByZWRpcy5kZWwoayhrZXkpKTtcbiAgICB9LFxuICB9O1xufVxuXG4vKipcbiAqIEJ1aWxkIGEgUmVkaXMtYmFja2VkIGlkZW1wb3RlbmN5IHN0b3JlIGZyb20gdGhlIHNoYXJlZCBlbnYgUmVkaXMgKHRoZSBzYW1lXG4gKiBgUkVESVNfVVJMYCAvIGBSRURJU19IT1NUYCB3aXJpbmcgdGhlIHJhdGUtbGltaXRlciBhbmQgYXVkaXQtc3Bvb2wgdXNlKS5cbiAqIFJldHVybnMgYG51bGxgIHdoZW4gUmVkaXMgaXNuJ3QgY29uZmlndXJlZC9hdmFpbGFibGUgc28gdGhlIGNhbGxlciBrZWVwcyB0aGVcbiAqIGluLW1lbW9yeSBkZWZhdWx0IChzaW5nbGUtcmVwbGljYSBjb3JyZWN0bmVzcykuIE5ldmVyIHRocm93cy5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZUVudlJlZGlzSWRlbXBvdGVuY3lTdG9yZSgpOiBJZGVtcG90ZW5jeVN0b3JlIHwgbnVsbCB7XG4gIGNvbnN0IGNsaWVudCA9IGNyZWF0ZUVudlJlZGlzQ2xpZW50PFJlZGlzSWRlbXBvdGVuY3lDbGllbnQ+KCdpZGVtcG90ZW5jeScpO1xuICBpZiAoIWNsaWVudCkgcmV0dXJuIG51bGw7XG4gIGxvZ2dlci5pbmZvKCdSZWRpcyBpZGVtcG90ZW5jeSBzdG9yZSBpbml0aWFsaXplZCcpO1xuICByZXR1cm4gY3JlYXRlUmVkaXNJZGVtcG90ZW5jeVN0b3JlKGNsaWVudCk7XG59XG5cbmNvbnN0IG1lbW9yeVN0b3JlID0gY3JlYXRlTWVtb3J5U3RvcmUoKTtcblxuLyoqXG4gKiBQcm9jZXNzLXdpZGUgZGVmYXVsdCBzdG9yZSB1c2VkIGJ5IGBpZGVtcG90ZW5jeU1pZGRsZXdhcmUoKWAgd2hlbiBubyBleHBsaWNpdFxuICogYHN0b3JlYCBpcyBwYXNzZWQuIERlZmF1bHRzIHRvIHRoZSBpbi1tZW1vcnkgc3RvcmU7IGBjcmVhdGVBcHBgIHByb21vdGVzIGl0IHRvXG4gKiB0aGUgc2hhcmVkIGVudi1SZWRpcyBzdG9yZSAobXVsdGktcmVwbGljYSBkZWR1cCkgYXQgc3RhcnR1cCB2aWFcbiAqIHtAbGluayBzZXRJZGVtcG90ZW5jeVN0b3JlfS4gVGhlIHJvdXRlIGZhY3RvcmllcyBrZWVwIGNhbGxpbmdcbiAqIGBpZGVtcG90ZW5jeU1pZGRsZXdhcmUoKWAgd2l0aCBubyBhcmdzIGFuZCB0cmFuc3BhcmVudGx5IHBpY2sgdGhpcyB1cC5cbiAqL1xubGV0IGRlZmF1bHRTdG9yZTogSWRlbXBvdGVuY3lTdG9yZSA9IG1lbW9yeVN0b3JlO1xuXG4vKiogSW5qZWN0IHRoZSBwcm9jZXNzLXdpZGUgaWRlbXBvdGVuY3kgc3RvcmUgKGUuZy4gYSBSZWRpcy1iYWNrZWQgb25lKS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzZXRJZGVtcG90ZW5jeVN0b3JlKHN0b3JlOiBJZGVtcG90ZW5jeVN0b3JlKTogdm9pZCB7XG4gIGRlZmF1bHRTdG9yZSA9IHN0b3JlO1xufVxuXG4vKiogVGhlIGN1cnJlbnQgcHJvY2Vzcy13aWRlIGRlZmF1bHQgc3RvcmUgKGluLW1lbW9yeSB1bmxlc3Mgb3ZlcnJpZGRlbikuICovXG5leHBvcnQgZnVuY3Rpb24gZ2V0SWRlbXBvdGVuY3lTdG9yZSgpOiBJZGVtcG90ZW5jeVN0b3JlIHtcbiAgcmV0dXJuIGRlZmF1bHRTdG9yZTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBJZGVtcG90ZW5jeU1pZGRsZXdhcmVPcHRpb25zIHtcbiAgLyoqIEN1c3RvbSBzdG9yZSBiYWNrZW5kLiBEZWZhdWx0cyB0byB0aGUgcHJvY2Vzcy13aWRlIHN0b3JlIHNldCB2aWFcbiAgICogIHtAbGluayBzZXRJZGVtcG90ZW5jeVN0b3JlfSAoaW4tbWVtb3J5IHVudGlsIGBjcmVhdGVBcHBgIHdpcmVzIFJlZGlzKS4gKi9cbiAgc3RvcmU/OiBJZGVtcG90ZW5jeVN0b3JlO1xufVxuXG4vKipcbiAqIE1pZGRsZXdhcmUgdGhhdCBzdXBwb3J0cyBpZGVtcG90ZW5jeSBrZXlzIGZvciBQT1NUL1BVVC9ERUxFVEUgbXV0YXRpb25zLlxuICpcbiAqIFdoZW4gYSByZXF1ZXN0IGluY2x1ZGVzIHRoZSBgSWRlbXBvdGVuY3ktS2V5YCBoZWFkZXI6XG4gKiAtIEZpcnN0IGNhbGw6IHByb2Nlc3NlcyBub3JtYWxseSwgY2FjaGVzIHRoZSByZXNwb25zZVxuICogLSBTdWJzZXF1ZW50IGNhbGxzIHdpdGggc2FtZSBrZXk6IHJldHVybnMgY2FjaGVkIHJlc3BvbnNlIChwcmV2ZW50cyBkdXBsaWNhdGUgbXV0YXRpb25zKVxuICpcbiAqIERlZmF1bHRzIHRvIGFuIGluLW1lbW9yeSBzdG9yZSAoc2luZ2xlLXJlcGxpY2EpLiBQYXNzIGEgY3VzdG9tXG4gKiBjcm9zcy1yZXBsaWNhIGB7IHN0b3JlIH1gIGltcGxlbWVudGluZyBgSWRlbXBvdGVuY3lTdG9yZWAgdG8gZGVkdXBlIGFjcm9zc1xuICogcmVwbGljYXMuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBpZGVtcG90ZW5jeU1pZGRsZXdhcmUob3B0aW9uczogSWRlbXBvdGVuY3lNaWRkbGV3YXJlT3B0aW9ucyA9IHt9KSB7XG4gIC8vIFJlc29sdmUgdGhlIHN0b3JlIFBFUiBSRVFVRVNUIChub3QgYXQgZmFjdG9yeSB0aW1lKSBzbyBhIHN0b3JlIGluamVjdGVkIGJ5XG4gIC8vIGNyZWF0ZUFwcCBBRlRFUiB0aGUgcm91dGUgY2hhaW5zIGFyZSBidWlsdCBzdGlsbCB0YWtlcyBlZmZlY3QuIEFuIGV4cGxpY2l0XG4gIC8vIGBvcHRpb25zLnN0b3JlYCBhbHdheXMgd2lucy5cbiAgY29uc3QgZXhwbGljaXRTdG9yZSA9IG9wdGlvbnMuc3RvcmU7XG4gIGNvbnN0IHR0bE1zID0gQ29yZUNvbnN0YW50cy5JREVNUE9URU5DWV9UVExfTVM7XG4gIGNvbnN0IHR0bFNlYyA9IE1hdGguZmxvb3IodHRsTXMgLyAxMDAwKTtcblxuICByZXR1cm4gKHJlcTogUmVxdWVzdCwgcmVzOiBSZXNwb25zZSwgbmV4dDogTmV4dEZ1bmN0aW9uKSA9PiB7XG4gICAgY29uc3Qgc3RvcmUgPSBleHBsaWNpdFN0b3JlID8/IGRlZmF1bHRTdG9yZTtcbiAgICBjb25zdCBrZXkgPSByZXEuaGVhZGVyc1snaWRlbXBvdGVuY3kta2V5J10gYXMgc3RyaW5nIHwgdW5kZWZpbmVkO1xuICAgIGlmICgha2V5KSByZXR1cm4gbmV4dCgpO1xuXG4gICAgLy8gT25seSBhcHBseSB0byBtdXRhdGlvbiBtZXRob2RzXG4gICAgaWYgKCFbJ1BPU1QnLCAnUFVUJywgJ0RFTEVURSddLmluY2x1ZGVzKHJlcS5tZXRob2QpKSByZXR1cm4gbmV4dCgpO1xuXG4gICAgLy8gTmFtZXNwYWNlIGJ5IG9yZ0lkIHRvIHByZXZlbnQgY3Jvc3Mtb3JnIGNhY2hlIGNvbGxpc2lvbnMuIFByZWZlciB0aGVcbiAgICAvLyBWRVJJRklFRCBhdXRoIG9yZyAoYHJlcS51c2VyYCwgcG9wdWxhdGVkIGJ5IHJlcXVpcmVBdXRoIGFmdGVyIHNpZ25hdHVyZVxuICAgIC8vIHZlcmlmaWNhdGlvbikgb3ZlciBgcmVxLmNvbnRleHQuaWRlbnRpdHlgLCB3aGljaCBzb21lIHNlcnZpY2VzIHBvcHVsYXRlXG4gICAgLy8gcHJlLWF1dGggZnJvbSBhbiBVTlZFUklGSUVEIHRva2VuIHBlZWsg4oCUIG5hbWVzcGFjaW5nIGEgbXV0YXRpb24ncyByZXBsYXlcbiAgICAvLyBjYWNoZSBvbiBhbiB1bnZlcmlmaWVkLCBjYWxsZXItaW5mbHVlbmNlZCBvcmcgaWQgd291bGQgYmUgdW5zYWZlLlxuICAgIGNvbnN0IG9yZ0lkID0gcmVxLnVzZXI/Lm9yZ2FuaXphdGlvbklkIHx8IHJlcS5jb250ZXh0Py5pZGVudGl0eT8ub3JnSWQ7XG4gICAgaWYgKCFvcmdJZCkgcmV0dXJuIG5leHQoKTsgLy8gc2tpcCBpZGVtcG90ZW5jeSBmb3IgdW5hdXRoZW50aWNhdGVkIHJlcXVlc3RzXG5cbiAgICAvLyBOYW1lc3BhY2UgdGhlIHN0b3JlZCBrZXkgYnkgKG1ldGhvZCwgcm91dGUsIG9yZywgYm9keSkgc28gYW4gSWRlbXBvdGVuY3ktS2V5XG4gICAgLy8gaXMgc2NvcGVkIHRvIE9ORSBzcGVjaWZpYyBlbmRwb2ludCtwYXlsb2FkLiBSZXVzaW5nIGEgc2luZ2xlIGtleSBhY3Jvc3MgdHdvXG4gICAgLy8gcm91dGVzIChlLmcuIFBPU1QgL3BpcGVsaW5lcyB0aGVuIFBPU1QgL3BsdWdpbnMpIHByZXZpb3VzbHkgcmVwbGF5ZWQgdGhlXG4gICAgLy8gZmlyc3QgZW5kcG9pbnQncyAyeHggZm9yIHRoZSBzZWNvbmQg4oCUIHRoZSBzdG9yZSBpcyBhIG1vZHVsZS1sZXZlbCBzaW5nbGV0b25cbiAgICAvLyBzaGFyZWQgYnkgZXZlcnkgcm91dGUuIEZvbGRpbmcgaW4gbWV0aG9kICsgcm91dGUgcGF0aCArIGEgYm9keSBmaW5nZXJwcmludFxuICAgIC8vIG9ubHkgbWFrZXMga2V5cyBNT1JFIHNwZWNpZmljLCBzbyB0aGVyZSBpcyBubyBjcm9zcy1jb250YW1pbmF0aW9uIHJpc2suXG4gICAgY29uc3Qgcm91dGVQYXRoID0gKHJlcS5iYXNlVXJsID8/ICcnKSArIChyZXEucm91dGU/LnBhdGggPz8gcmVxLnBhdGggPz8gJycpO1xuICAgIGNvbnN0IGZ1bGxLZXkgPSBgJHtyZXEubWV0aG9kfToke3JvdXRlUGF0aH06JHtvcmdJZH06JHtib2R5RmluZ2VycHJpbnQocmVxLmJvZHkpfToke2tleX1gO1xuXG4gICAgLyoqIFJlamVjdCBhIGR1cGxpY2F0ZSB3aG9zZSBvcmlnaW5hbCBpcyBzdGlsbCBpbi1mbGlnaHQuICovXG4gICAgY29uc3Qgc2VuZEluUHJvZ3Jlc3MgPSAoKTogdm9pZCA9PiB7XG4gICAgICByZXMuc2V0SGVhZGVyKCdSZXRyeS1BZnRlcicsICcxJyk7XG4gICAgICBzZW5kRXJyb3IocmVzLCA0MDksICdBIHJlcXVlc3Qgd2l0aCB0aGlzIElkZW1wb3RlbmN5LUtleSBpcyBhbHJlYWR5IGJlaW5nIHByb2Nlc3NlZCcsIEVycm9yQ29kZS5DT05GTElDVCk7XG4gICAgfTtcblxuICAgIHN0b3JlLmdldChmdWxsS2V5KS50aGVuKGFzeW5jIChjYWNoZWQpID0+IHtcbiAgICAgIGlmIChjYWNoZWQgJiYgIWNhY2hlZC5wZW5kaW5nKSB7XG4gICAgICAgIC8vIENvbXBsZXRlZCByZXNwb25zZSBvbiByZWNvcmQg4oaSIHJlcGxheSBpdC5cbiAgICAgICAgbG9nZ2VyLmRlYnVnKCdJZGVtcG90ZW50IHJlcXVlc3QgcmVwbGF5ZWQnLCB7IGtleTogZnVsbEtleSB9KTtcbiAgICAgICAgcmVzLnNldEhlYWRlcignWC1JZGVtcG90ZW50LVJlcGxheWVkJywgJ3RydWUnKTtcbiAgICAgICAgcmVzLnN0YXR1cyhjYWNoZWQuc3RhdHVzQ29kZSkuanNvbihjYWNoZWQuYm9keSk7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICAgIGlmIChjYWNoZWQgJiYgY2FjaGVkLnBlbmRpbmcpIHtcbiAgICAgICAgLy8gVGhlIG9yaWdpbmFsIGlzIHN0aWxsIHJ1bm5pbmcg4oCUIGRvbid0IHJ1biB0aGUgbXV0YXRpb24gYSBzZWNvbmQgdGltZS5cbiAgICAgICAgbG9nZ2VyLmRlYnVnKCdJZGVtcG90ZW50IHJlcXVlc3QgcmVqZWN0ZWQgKGluIHByb2dyZXNzKScsIHsga2V5OiBmdWxsS2V5IH0pO1xuICAgICAgICByZXR1cm4gc2VuZEluUHJvZ3Jlc3MoKTtcbiAgICAgIH1cblxuICAgICAgLy8gQXRvbWljYWxseSByZXNlcnZlIHRoZSBrZXkuIExvc2luZyB0aGlzIHJhY2UgbWVhbnMgYSBjb25jdXJyZW50IHJlcXVlc3RcbiAgICAgIC8vIHdpdGggdGhlIHNhbWUga2V5IGdvdCB0aGVyZSBmaXJzdCBhbmQgaXMgaW4tZmxpZ2h0IOKGkiA0MDkuXG4gICAgICBjb25zdCB3b24gPSBhd2FpdCBzdG9yZS5yZXNlcnZlKFxuICAgICAgICBmdWxsS2V5LFxuICAgICAgICB7IHN0YXR1c0NvZGU6IDAsIGJvZHk6IG51bGwsIHBlbmRpbmc6IHRydWUsIGV4cGlyZXNBdDogRGF0ZS5ub3coKSArIHR0bE1zIH0sXG4gICAgICAgIHR0bFNlYyxcbiAgICAgICk7XG4gICAgICBpZiAoIXdvbikge1xuICAgICAgICBsb2dnZXIuZGVidWcoJ0lkZW1wb3RlbnQgcmVxdWVzdCByZWplY3RlZCAobG9zdCByZXNlcnZlIHJhY2UpJywgeyBrZXk6IGZ1bGxLZXkgfSk7XG4gICAgICAgIHJldHVybiBzZW5kSW5Qcm9ncmVzcygpO1xuICAgICAgfVxuXG4gICAgICAvLyBXZSBvd24gdGhlIHJlc2VydmF0aW9uLiBJdCBNVVNUIGJlIHJlc29sdmVkIGV4YWN0bHkgb25jZSDigJQgZWl0aGVyIHRoZVxuICAgICAgLy8gcmVhbCAyeHggcmVzcG9uc2UgaXMgY2FjaGVkIGZvciByZXBsYXksIG9yIHRoZSByZXNlcnZhdGlvbiBpcyByZWxlYXNlZCBzb1xuICAgICAgLy8gYSByZXRyeSBjYW4gcHJvY2VlZC4gYHNldHRsZWRgIGd1YXJkcyBhZ2FpbnN0IGEgZG91YmxlLXJlc29sdmUuXG4gICAgICBsZXQgc2V0dGxlZCA9IGZhbHNlO1xuICAgICAgbGV0IGNhcHR1cmVkQm9keTogdW5rbm93bjtcbiAgICAgIGxldCBib2R5Q2FwdHVyZWQgPSBmYWxzZTtcblxuICAgICAgLyoqIFJlbWVtYmVyIHRoZSByZXNwb25zZSBib2R5IGZvciByZXBsYXkgKGZpcnN0IGNhcHR1cmUgd2lucykuICovXG4gICAgICBjb25zdCBjYXB0dXJlQm9keSA9IChib2R5OiB1bmtub3duKTogdm9pZCA9PiB7XG4gICAgICAgIGlmICghYm9keUNhcHR1cmVkKSB7XG4gICAgICAgICAgY2FwdHVyZWRCb2R5ID0gYm9keTtcbiAgICAgICAgICBib2R5Q2FwdHVyZWQgPSB0cnVlO1xuICAgICAgICB9XG4gICAgICB9O1xuXG4gICAgICBjb25zdCByZWxlYXNlID0gKCk6IHZvaWQgPT4ge1xuICAgICAgICBpZiAoc2V0dGxlZCkgcmV0dXJuO1xuICAgICAgICBzZXR0bGVkID0gdHJ1ZTtcbiAgICAgICAgc3RvcmUuZGVsZXRlKGZ1bGxLZXkpLmNhdGNoKChlcnIpID0+IHtcbiAgICAgICAgICBsb2dnZXIud2FybignSWRlbXBvdGVuY3kgcmVzZXJ2YXRpb24gcmVsZWFzZSBmYWlsZWQnLCB7IGtleTogZnVsbEtleSwgZXJyb3I6IGVyciBpbnN0YW5jZW9mIEVycm9yID8gZXJyLm1lc3NhZ2UgOiBTdHJpbmcoZXJyKSB9KTtcbiAgICAgICAgfSk7XG4gICAgICB9O1xuXG4gICAgICBjb25zdCBjYWNoZVN1Y2Nlc3MgPSAoKTogdm9pZCA9PiB7XG4gICAgICAgIGlmIChzZXR0bGVkKSByZXR1cm47XG4gICAgICAgIHNldHRsZWQgPSB0cnVlO1xuICAgICAgICBzdG9yZS5zZXQoZnVsbEtleSwge1xuICAgICAgICAgIHN0YXR1c0NvZGU6IHJlcy5zdGF0dXNDb2RlLFxuICAgICAgICAgIGJvZHk6IGJvZHlDYXB0dXJlZCA/IGNhcHR1cmVkQm9keSA6IG51bGwsXG4gICAgICAgICAgZXhwaXJlc0F0OiBEYXRlLm5vdygpICsgdHRsTXMsXG4gICAgICAgIH0sIHR0bFNlYykuY2F0Y2goKGVycikgPT4ge1xuICAgICAgICAgIGxvZ2dlci53YXJuKCdJZGVtcG90ZW5jeSBzdG9yZS5zZXQgZmFpbGVkJywgeyBrZXk6IGZ1bGxLZXksIGVycm9yOiBlcnIgaW5zdGFuY2VvZiBFcnJvciA/IGVyci5tZXNzYWdlIDogU3RyaW5nKGVycikgfSk7XG4gICAgICAgIH0pO1xuICAgICAgfTtcblxuICAgICAgLyoqIFJlc29sdmUgdGhlIHJlc2VydmF0aW9uIGZyb20gdGhlIEFDVFVBTCBmaW5hbCBzdGF0dXM6IGNhY2hlIDJ4eCwgZWxzZSByZWxlYXNlLiAqL1xuICAgICAgY29uc3Qgc2V0dGxlRnJvbVN0YXR1cyA9ICgpOiB2b2lkID0+IHtcbiAgICAgICAgaWYgKHNldHRsZWQpIHJldHVybjtcbiAgICAgICAgaWYgKHJlcy5zdGF0dXNDb2RlID49IDIwMCAmJiByZXMuc3RhdHVzQ29kZSA8IDMwMCkgY2FjaGVTdWNjZXNzKCk7XG4gICAgICAgIGVsc2UgcmVsZWFzZSgpO1xuICAgICAgfTtcblxuICAgICAgLy8gRmFzdCBwYXRoOiByZXMuanNvbiBpcyB0aGUgY29tbW9uIGNvbXBsZXRpb24uIENhcHR1cmUgdGhlIGJvZHkgYW5kIHNldHRsZVxuICAgICAgLy8gaW1tZWRpYXRlbHkgb2ZmIHRoZSBzdGF0dXMgc2V0IGJ5IHJlcy5zdGF0dXMoLi4uKS5qc29uKC4uLikuXG4gICAgICBjb25zdCBvcmlnaW5hbEpzb24gPSByZXMuanNvbi5iaW5kKHJlcyk7XG4gICAgICByZXMuanNvbiA9IChib2R5OiB1bmtub3duKSA9PiB7XG4gICAgICAgIHJlcy5zZXRIZWFkZXIoJ1gtSWRlbXBvdGVudC1SZXBsYXllZCcsICdmYWxzZScpO1xuICAgICAgICBjYXB0dXJlQm9keShib2R5KTtcbiAgICAgICAgc2V0dGxlRnJvbVN0YXR1cygpO1xuICAgICAgICByZXR1cm4gb3JpZ2luYWxKc29uKGJvZHkpO1xuICAgICAgfTtcblxuICAgICAgLy8gQm9keS1jYXB0dXJlIGJhY2tzdG9wcyBmb3IgY29tcGxldGlvbnMgdGhhdCBieXBhc3MgcmVzLmpzb25cbiAgICAgIC8vIChyZXMuc2VuZCAvIHJlcy5lbmQgLyByZXMuc2VuZEZpbGUg4oaSIHJlcy5lbmQpLiBUaGVzZSBvbmx5IFJFQ09SRCB0aGUgYm9keTtcbiAgICAgIC8vIHRoZSBhY3R1YWwgc2V0dGxlIGhhcHBlbnMgb24gYGZpbmlzaGAgZnJvbSB0aGUgcmVhbCBzdGF0dXMgY29kZS4gRmlyc3RcbiAgICAgIC8vIGNhcHR1cmUgd2lucywgc28gcmVzLmpzb24ncyBib2R5IGlzIG5ldmVyIGNsb2JiZXJlZCBieSBpdHMgaW50ZXJuYWxcbiAgICAgIC8vIHJlcy5zZW5kL3Jlcy5lbmQgY2FsbHMuXG4gICAgICBjb25zdCBwYXRjaGVkID0gcmVzIGFzIHVua25vd24gYXMge1xuICAgICAgICBzZW5kPzogKGJvZHk/OiB1bmtub3duKSA9PiB1bmtub3duO1xuICAgICAgICBlbmQ/OiAoLi4uYXJnczogdW5rbm93bltdKSA9PiB1bmtub3duO1xuICAgICAgICBvbj86IChldmVudDogc3RyaW5nLCBjYjogKCkgPT4gdm9pZCkgPT4gdW5rbm93bjtcbiAgICAgIH07XG4gICAgICBpZiAodHlwZW9mIHBhdGNoZWQuc2VuZCA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICBjb25zdCBvcmlnaW5hbFNlbmQgPSBwYXRjaGVkLnNlbmQuYmluZChyZXMpO1xuICAgICAgICBwYXRjaGVkLnNlbmQgPSAoYm9keT86IHVua25vd24pID0+IHtcbiAgICAgICAgICBjYXB0dXJlQm9keShib2R5KTtcbiAgICAgICAgICByZXR1cm4gb3JpZ2luYWxTZW5kKGJvZHkpO1xuICAgICAgICB9O1xuICAgICAgfVxuICAgICAgaWYgKHR5cGVvZiBwYXRjaGVkLmVuZCA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICBjb25zdCBvcmlnaW5hbEVuZCA9IHBhdGNoZWQuZW5kLmJpbmQocmVzKTtcbiAgICAgICAgcGF0Y2hlZC5lbmQgPSAoLi4uYXJnczogdW5rbm93bltdKSA9PiB7XG4gICAgICAgICAgY29uc3QgY2h1bmsgPSBhcmdzWzBdO1xuICAgICAgICAgIGlmIChjaHVuayAhPT0gdW5kZWZpbmVkICYmIHR5cGVvZiBjaHVuayAhPT0gJ2Z1bmN0aW9uJykgY2FwdHVyZUJvZHkoY2h1bmspO1xuICAgICAgICAgIHJldHVybiBvcmlnaW5hbEVuZCguLi5hcmdzKTtcbiAgICAgICAgfTtcbiAgICAgIH1cblxuICAgICAgLy8gQmFja3N0b3AgZm9yIEFOWSBjb21wbGV0aW9uIHBhdGgg4oCUIGluY2x1ZGluZyBhIDJ4eCB0aGF0IGZpbmlzaGVkIHZpYVxuICAgICAgLy8gcmVzLnNlbmQvcmVzLmVuZC9yZXMuc2VuZEZpbGUvMjA0IGFuZCBuZXZlciBoaXQgdGhlIHBhdGNoZWQgcmVzLmpzb24uXG4gICAgICAvLyBEZWNpZGluZyBvbiB0aGUgcmVhbCBmaW5hbCBzdGF0dXMgaGVyZSBtZWFucyBhIHN1Y2Nlc3NmdWwgb3AgaXMgY2FjaGVkXG4gICAgICAvLyAob3IgYSBmYWlsZWQgb25lIHJlbGVhc2VkKSBpbnN0ZWFkIG9mIHN0cmFuZGluZyB0aGUgcmVzZXJ2YXRpb24gYW5kXG4gICAgICAvLyA0MDktaW5nIGV2ZXJ5IHJldHJ5IGZvciB0aGUgZnVsbCBUVEwuIEd1YXJkZWQg4oCUIHRlc3QgZG91YmxlcyBtYXkgbm90IGJlXG4gICAgICAvLyBFdmVudEVtaXR0ZXJzOyByZXMuanNvbidzIGZhc3QgcGF0aCBhbHJlYWR5IHNldHRsZWQgdGhlIGNvbW1vbiBjYXNlLlxuICAgICAgaWYgKHR5cGVvZiBwYXRjaGVkLm9uID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICAgIHBhdGNoZWQub24oJ2ZpbmlzaCcsIHNldHRsZUZyb21TdGF0dXMpO1xuICAgICAgfVxuXG4gICAgICBuZXh0KCk7XG4gICAgfSkuY2F0Y2goKGVycikgPT4ge1xuICAgICAgLy8gSWYgdGhlIHN0b3JlIGJhY2tlbmQgaXMgZG93biwgZmFpbCBvcGVuIOKAlCBwcm9jZXNzIHRoZSByZXF1ZXN0IG5vcm1hbGx5LlxuICAgICAgbG9nZ2VyLndhcm4oJ0lkZW1wb3RlbmN5IHN0b3JlLmdldCBmYWlsZWQ7IHByb2NlZWRpbmcgd2l0aG91dCBkZWR1cCcsIHsga2V5OiBmdWxsS2V5LCBlcnJvcjogZXJyIGluc3RhbmNlb2YgRXJyb3IgPyBlcnIubWVzc2FnZSA6IFN0cmluZyhlcnIpIH0pO1xuICAgICAgbmV4dCgpO1xuICAgIH0pO1xuICB9O1xufVxuIl19
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export * from './middleware.js';
|
|
2
|
+
export * from './middleware-factory.js';
|
|
3
|
+
export * from './context-middleware.js';
|
|
4
|
+
export * from './check-quota.js';
|
|
5
|
+
export * from './meter-quota.js';
|
|
6
|
+
export * from './rate-limit-by-org.js';
|
|
7
|
+
export * from './require-org-id.js';
|
|
8
|
+
export * from './tenant-context.js';
|
|
9
|
+
export * from './get-context.js';
|
|
10
|
+
export * from './app-factory.js';
|
|
11
|
+
export * from './health-checks.js';
|
|
12
|
+
export * from './mongo-connect.js';
|
|
13
|
+
export * from './quota-helpers.js';
|
|
14
|
+
export * from './idempotency-middleware.js';
|
|
15
|
+
export * from './tracing.js';
|
|
16
|
+
export * from './metrics.js';
|
|
17
|
+
export * from './server.js';
|
|
18
|
+
export * from './readiness.js';
|
|
19
|
+
export * from './route-wrapper.js';
|
|
20
|
+
export * from './request-types.js';
|
package/lib/api/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Middleware
|
|
4
|
+
export * from './middleware.js';
|
|
5
|
+
export * from './middleware-factory.js';
|
|
6
|
+
export * from './context-middleware.js';
|
|
7
|
+
export * from './check-quota.js';
|
|
8
|
+
export * from './meter-quota.js';
|
|
9
|
+
export * from './rate-limit-by-org.js';
|
|
10
|
+
export * from './require-org-id.js';
|
|
11
|
+
export * from './tenant-context.js';
|
|
12
|
+
export * from './get-context.js';
|
|
13
|
+
// App factory
|
|
14
|
+
export * from './app-factory.js';
|
|
15
|
+
// Health-check helpers
|
|
16
|
+
export * from './health-checks.js';
|
|
17
|
+
// MongoDB connection helper (dependency-injected mongoose)
|
|
18
|
+
export * from './mongo-connect.js';
|
|
19
|
+
// Quota helpers
|
|
20
|
+
export * from './quota-helpers.js';
|
|
21
|
+
// Idempotency
|
|
22
|
+
export * from './idempotency-middleware.js';
|
|
23
|
+
// Observability
|
|
24
|
+
export * from './tracing.js';
|
|
25
|
+
export * from './metrics.js';
|
|
26
|
+
// Server utilities
|
|
27
|
+
export * from './server.js';
|
|
28
|
+
// Readiness state + guard middleware
|
|
29
|
+
export * from './readiness.js';
|
|
30
|
+
// Route wrapper
|
|
31
|
+
export * from './route-wrapper.js';
|
|
32
|
+
// Request/Response types
|
|
33
|
+
export * from './request-types.js';
|
|
34
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXBpL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEMsYUFBYTtBQUNiLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyx5QkFBeUIsQ0FBQztBQUN4QyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGtCQUFrQixDQUFDO0FBRWpDLGNBQWM7QUFDZCxjQUFjLGtCQUFrQixDQUFDO0FBRWpDLHVCQUF1QjtBQUN2QixjQUFjLG9CQUFvQixDQUFDO0FBRW5DLDJEQUEyRDtBQUMzRCxjQUFjLG9CQUFvQixDQUFDO0FBRW5DLGdCQUFnQjtBQUNoQixjQUFjLG9CQUFvQixDQUFDO0FBRW5DLGNBQWM7QUFDZCxjQUFjLDZCQUE2QixDQUFDO0FBRTVDLGdCQUFnQjtBQUNoQixjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLGNBQWMsQ0FBQztBQUU3QixtQkFBbUI7QUFDbkIsY0FBYyxhQUFhLENBQUM7QUFFNUIscUNBQXFDO0FBQ3JDLGNBQWMsZ0JBQWdCLENBQUM7QUFFL0IsZ0JBQWdCO0FBQ2hCLGNBQWMsb0JBQW9CLENBQUM7QUFFbkMseUJBQXlCO0FBQ3pCLGNBQWMsb0JBQW9CLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuLy8gTWlkZGxld2FyZVxuZXhwb3J0ICogZnJvbSAnLi9taWRkbGV3YXJlLmpzJztcbmV4cG9ydCAqIGZyb20gJy4vbWlkZGxld2FyZS1mYWN0b3J5LmpzJztcbmV4cG9ydCAqIGZyb20gJy4vY29udGV4dC1taWRkbGV3YXJlLmpzJztcbmV4cG9ydCAqIGZyb20gJy4vY2hlY2stcXVvdGEuanMnO1xuZXhwb3J0ICogZnJvbSAnLi9tZXRlci1xdW90YS5qcyc7XG5leHBvcnQgKiBmcm9tICcuL3JhdGUtbGltaXQtYnktb3JnLmpzJztcbmV4cG9ydCAqIGZyb20gJy4vcmVxdWlyZS1vcmctaWQuanMnO1xuZXhwb3J0ICogZnJvbSAnLi90ZW5hbnQtY29udGV4dC5qcyc7XG5leHBvcnQgKiBmcm9tICcuL2dldC1jb250ZXh0LmpzJztcblxuLy8gQXBwIGZhY3RvcnlcbmV4cG9ydCAqIGZyb20gJy4vYXBwLWZhY3RvcnkuanMnO1xuXG4vLyBIZWFsdGgtY2hlY2sgaGVscGVyc1xuZXhwb3J0ICogZnJvbSAnLi9oZWFsdGgtY2hlY2tzLmpzJztcblxuLy8gTW9uZ29EQiBjb25uZWN0aW9uIGhlbHBlciAoZGVwZW5kZW5jeS1pbmplY3RlZCBtb25nb29zZSlcbmV4cG9ydCAqIGZyb20gJy4vbW9uZ28tY29ubmVjdC5qcyc7XG5cbi8vIFF1b3RhIGhlbHBlcnNcbmV4cG9ydCAqIGZyb20gJy4vcXVvdGEtaGVscGVycy5qcyc7XG5cbi8vIElkZW1wb3RlbmN5XG5leHBvcnQgKiBmcm9tICcuL2lkZW1wb3RlbmN5LW1pZGRsZXdhcmUuanMnO1xuXG4vLyBPYnNlcnZhYmlsaXR5XG5leHBvcnQgKiBmcm9tICcuL3RyYWNpbmcuanMnO1xuZXhwb3J0ICogZnJvbSAnLi9tZXRyaWNzLmpzJztcblxuLy8gU2VydmVyIHV0aWxpdGllc1xuZXhwb3J0ICogZnJvbSAnLi9zZXJ2ZXIuanMnO1xuXG4vLyBSZWFkaW5lc3Mgc3RhdGUgKyBndWFyZCBtaWRkbGV3YXJlXG5leHBvcnQgKiBmcm9tICcuL3JlYWRpbmVzcy5qcyc7XG5cbi8vIFJvdXRlIHdyYXBwZXJcbmV4cG9ydCAqIGZyb20gJy4vcm91dGUtd3JhcHBlci5qcyc7XG5cbi8vIFJlcXVlc3QvUmVzcG9uc2UgdHlwZXNcbmV4cG9ydCAqIGZyb20gJy4vcmVxdWVzdC10eXBlcy5qcyc7XG4iXX0=
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { QuotaType, QuotaService } from '@pipeline-builder/api-core';
|
|
2
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
3
|
+
/**
|
|
4
|
+
* Middleware that METERS (increments) a quota counter once per SUCCESSFUL
|
|
5
|
+
* request, on response `finish`.
|
|
6
|
+
*
|
|
7
|
+
* The complement to {@link checkQuota}: `checkQuota` only GATES (reads current
|
|
8
|
+
* usage and 429s when over) — it never increments, so a service that mounts it
|
|
9
|
+
* but never calls `incrementQuota` checks against a counter that its own traffic
|
|
10
|
+
* never moves (the metering gap this fixes for the compliance service). Mount
|
|
11
|
+
* this ALONGSIDE the auth/org chain to close the loop with one line instead of
|
|
12
|
+
* threading `quotaService` + a manual `incrementQuotaFromCtx` through every
|
|
13
|
+
* route factory and handler.
|
|
14
|
+
*
|
|
15
|
+
* Fire-and-forget + fail-safe by construction:
|
|
16
|
+
* - runs in a `finish` listener, so it never blocks or fails the response;
|
|
17
|
+
* - `incrementQuota` swallows its own errors (logs a warning);
|
|
18
|
+
* - only 2xx responses are metered (a rejected/errored request costs nothing);
|
|
19
|
+
* - a request with no VERIFIED org (`req.user.organizationId` — set by
|
|
20
|
+
* requireAuth, never the spoofable header identity) is skipped
|
|
21
|
+
* (unauthenticated / health / early-terminated);
|
|
22
|
+
* - SERVICE-PRINCIPAL callers are skipped: internal service-to-service traffic
|
|
23
|
+
* (e.g. a peer calling compliance `validate`) must not burn a tenant's quota —
|
|
24
|
+
* the originating service already meters the user action that triggered it.
|
|
25
|
+
*
|
|
26
|
+
* Do NOT combine this with a handler that already calls `incrementQuotaFromCtx`
|
|
27
|
+
* for the same quota type on the same route — that double-counts.
|
|
28
|
+
*
|
|
29
|
+
* @param quotaService - Quota service client
|
|
30
|
+
* @param quotaType - Which quota to meter (e.g. 'apiCalls')
|
|
31
|
+
*/
|
|
32
|
+
export declare function meterQuotaOnSuccess(quotaService: QuotaService, quotaType: QuotaType): (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { incrementQuota, isServicePrincipal, createLogger } from '@pipeline-builder/api-core';
|
|
4
|
+
import { getContext } from './get-context.js';
|
|
5
|
+
const logger = createLogger('meter-quota');
|
|
6
|
+
/**
|
|
7
|
+
* Middleware that METERS (increments) a quota counter once per SUCCESSFUL
|
|
8
|
+
* request, on response `finish`.
|
|
9
|
+
*
|
|
10
|
+
* The complement to {@link checkQuota}: `checkQuota` only GATES (reads current
|
|
11
|
+
* usage and 429s when over) — it never increments, so a service that mounts it
|
|
12
|
+
* but never calls `incrementQuota` checks against a counter that its own traffic
|
|
13
|
+
* never moves (the metering gap this fixes for the compliance service). Mount
|
|
14
|
+
* this ALONGSIDE the auth/org chain to close the loop with one line instead of
|
|
15
|
+
* threading `quotaService` + a manual `incrementQuotaFromCtx` through every
|
|
16
|
+
* route factory and handler.
|
|
17
|
+
*
|
|
18
|
+
* Fire-and-forget + fail-safe by construction:
|
|
19
|
+
* - runs in a `finish` listener, so it never blocks or fails the response;
|
|
20
|
+
* - `incrementQuota` swallows its own errors (logs a warning);
|
|
21
|
+
* - only 2xx responses are metered (a rejected/errored request costs nothing);
|
|
22
|
+
* - a request with no VERIFIED org (`req.user.organizationId` — set by
|
|
23
|
+
* requireAuth, never the spoofable header identity) is skipped
|
|
24
|
+
* (unauthenticated / health / early-terminated);
|
|
25
|
+
* - SERVICE-PRINCIPAL callers are skipped: internal service-to-service traffic
|
|
26
|
+
* (e.g. a peer calling compliance `validate`) must not burn a tenant's quota —
|
|
27
|
+
* the originating service already meters the user action that triggered it.
|
|
28
|
+
*
|
|
29
|
+
* Do NOT combine this with a handler that already calls `incrementQuotaFromCtx`
|
|
30
|
+
* for the same quota type on the same route — that double-counts.
|
|
31
|
+
*
|
|
32
|
+
* @param quotaService - Quota service client
|
|
33
|
+
* @param quotaType - Which quota to meter (e.g. 'apiCalls')
|
|
34
|
+
*/
|
|
35
|
+
export function meterQuotaOnSuccess(quotaService, quotaType) {
|
|
36
|
+
return (req, res, next) => {
|
|
37
|
+
res.on('finish', () => {
|
|
38
|
+
try {
|
|
39
|
+
// Only successful responses are billable.
|
|
40
|
+
if (res.statusCode < 200 || res.statusCode >= 300)
|
|
41
|
+
return;
|
|
42
|
+
// Resolve the org id from the SAME source `checkQuota` reads —
|
|
43
|
+
// `getContext().identity.orgId`, which is normalized (trimmed + lowercased).
|
|
44
|
+
// Metering off the raw `req.user.organizationId` while the gate checks the
|
|
45
|
+
// normalized id would land increments on a different key than the check
|
|
46
|
+
// reads (a silent bypass if an org id ever carries mixed case). Fall back to
|
|
47
|
+
// the raw auth org only when context middleware isn't mounted.
|
|
48
|
+
let orgId;
|
|
49
|
+
try {
|
|
50
|
+
orgId = getContext(req).identity.orgId;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
orgId = req.user?.organizationId;
|
|
54
|
+
}
|
|
55
|
+
if (!orgId)
|
|
56
|
+
return;
|
|
57
|
+
// Internal service-to-service calls must not consume a tenant's quota.
|
|
58
|
+
if (isServicePrincipal(req))
|
|
59
|
+
return;
|
|
60
|
+
incrementQuota(quotaService, orgId, quotaType, req.headers.authorization || '', (message, data) => logger.warn(message, data));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Metering must never affect the response; a missing user/context just
|
|
64
|
+
// means an unauthenticated or early-terminated request — skip silently.
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
next();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0ZXItcXVvdGEuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXBpL21ldGVyLXF1b3RhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEMsT0FBTyxFQUFFLGNBQWMsRUFBRSxrQkFBa0IsRUFBRSxZQUFZLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUc5RixPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFOUMsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLGFBQWEsQ0FBQyxDQUFDO0FBRTNDOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBNEJHO0FBQ0gsTUFBTSxVQUFVLG1CQUFtQixDQUFDLFlBQTBCLEVBQUUsU0FBb0I7SUFDbEYsT0FBTyxDQUFDLEdBQVksRUFBRSxHQUFhLEVBQUUsSUFBa0IsRUFBUSxFQUFFO1FBQy9ELEdBQUcsQ0FBQyxFQUFFLENBQUMsUUFBUSxFQUFFLEdBQUcsRUFBRTtZQUNwQixJQUFJLENBQUM7Z0JBQ0gsMENBQTBDO2dCQUMxQyxJQUFJLEdBQUcsQ0FBQyxVQUFVLEdBQUcsR0FBRyxJQUFJLEdBQUcsQ0FBQyxVQUFVLElBQUksR0FBRztvQkFBRSxPQUFPO2dCQUMxRCwrREFBK0Q7Z0JBQy9ELDZFQUE2RTtnQkFDN0UsMkVBQTJFO2dCQUMzRSx3RUFBd0U7Z0JBQ3hFLDZFQUE2RTtnQkFDN0UsK0RBQStEO2dCQUMvRCxJQUFJLEtBQXlCLENBQUM7Z0JBQzlCLElBQUksQ0FBQztvQkFBQyxLQUFLLEdBQUcsVUFBVSxDQUFDLEdBQUcsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUM7Z0JBQUMsQ0FBQztnQkFBQyxNQUFNLENBQUM7b0JBQUMsS0FBSyxHQUFHLEdBQUcsQ0FBQyxJQUFJLEVBQUUsY0FBYyxDQUFDO2dCQUFDLENBQUM7Z0JBQzNGLElBQUksQ0FBQyxLQUFLO29CQUFFLE9BQU87Z0JBQ25CLHVFQUF1RTtnQkFDdkUsSUFBSSxrQkFBa0IsQ0FBQyxHQUFHLENBQUM7b0JBQUUsT0FBTztnQkFDcEMsY0FBYyxDQUNaLFlBQVksRUFDWixLQUFLLEVBQ0wsU0FBUyxFQUNULEdBQUcsQ0FBQyxPQUFPLENBQUMsYUFBYSxJQUFJLEVBQUUsRUFDL0IsQ0FBQyxPQUFPLEVBQUUsSUFBSSxFQUFFLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxJQUErQixDQUFDLENBQ3pFLENBQUM7WUFDSixDQUFDO1lBQUMsTUFBTSxDQUFDO2dCQUNQLHVFQUF1RTtnQkFDdkUsd0VBQXdFO1lBQzFFLENBQUM7UUFDSCxDQUFDLENBQUMsQ0FBQztRQUNILElBQUksRUFBRSxDQUFDO0lBQ1QsQ0FBQyxDQUFDO0FBQ0osQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG5pbXBvcnQgeyBpbmNyZW1lbnRRdW90YSwgaXNTZXJ2aWNlUHJpbmNpcGFsLCBjcmVhdGVMb2dnZXIgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgdHlwZSB7IFF1b3RhVHlwZSwgUXVvdGFTZXJ2aWNlIH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvYXBpLWNvcmUnO1xuaW1wb3J0IHR5cGUgeyBSZXF1ZXN0LCBSZXNwb25zZSwgTmV4dEZ1bmN0aW9uIH0gZnJvbSAnZXhwcmVzcyc7XG5pbXBvcnQgeyBnZXRDb250ZXh0IH0gZnJvbSAnLi9nZXQtY29udGV4dC5qcyc7XG5cbmNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcignbWV0ZXItcXVvdGEnKTtcblxuLyoqXG4gKiBNaWRkbGV3YXJlIHRoYXQgTUVURVJTIChpbmNyZW1lbnRzKSBhIHF1b3RhIGNvdW50ZXIgb25jZSBwZXIgU1VDQ0VTU0ZVTFxuICogcmVxdWVzdCwgb24gcmVzcG9uc2UgYGZpbmlzaGAuXG4gKlxuICogVGhlIGNvbXBsZW1lbnQgdG8ge0BsaW5rIGNoZWNrUXVvdGF9OiBgY2hlY2tRdW90YWAgb25seSBHQVRFUyAocmVhZHMgY3VycmVudFxuICogdXNhZ2UgYW5kIDQyOXMgd2hlbiBvdmVyKSDigJQgaXQgbmV2ZXIgaW5jcmVtZW50cywgc28gYSBzZXJ2aWNlIHRoYXQgbW91bnRzIGl0XG4gKiBidXQgbmV2ZXIgY2FsbHMgYGluY3JlbWVudFF1b3RhYCBjaGVja3MgYWdhaW5zdCBhIGNvdW50ZXIgdGhhdCBpdHMgb3duIHRyYWZmaWNcbiAqIG5ldmVyIG1vdmVzICh0aGUgbWV0ZXJpbmcgZ2FwIHRoaXMgZml4ZXMgZm9yIHRoZSBjb21wbGlhbmNlIHNlcnZpY2UpLiBNb3VudFxuICogdGhpcyBBTE9OR1NJREUgdGhlIGF1dGgvb3JnIGNoYWluIHRvIGNsb3NlIHRoZSBsb29wIHdpdGggb25lIGxpbmUgaW5zdGVhZCBvZlxuICogdGhyZWFkaW5nIGBxdW90YVNlcnZpY2VgICsgYSBtYW51YWwgYGluY3JlbWVudFF1b3RhRnJvbUN0eGAgdGhyb3VnaCBldmVyeVxuICogcm91dGUgZmFjdG9yeSBhbmQgaGFuZGxlci5cbiAqXG4gKiBGaXJlLWFuZC1mb3JnZXQgKyBmYWlsLXNhZmUgYnkgY29uc3RydWN0aW9uOlxuICogLSBydW5zIGluIGEgYGZpbmlzaGAgbGlzdGVuZXIsIHNvIGl0IG5ldmVyIGJsb2NrcyBvciBmYWlscyB0aGUgcmVzcG9uc2U7XG4gKiAtIGBpbmNyZW1lbnRRdW90YWAgc3dhbGxvd3MgaXRzIG93biBlcnJvcnMgKGxvZ3MgYSB3YXJuaW5nKTtcbiAqIC0gb25seSAyeHggcmVzcG9uc2VzIGFyZSBtZXRlcmVkIChhIHJlamVjdGVkL2Vycm9yZWQgcmVxdWVzdCBjb3N0cyBub3RoaW5nKTtcbiAqIC0gYSByZXF1ZXN0IHdpdGggbm8gVkVSSUZJRUQgb3JnIChgcmVxLnVzZXIub3JnYW5pemF0aW9uSWRgIOKAlCBzZXQgYnlcbiAqICAgcmVxdWlyZUF1dGgsIG5ldmVyIHRoZSBzcG9vZmFibGUgaGVhZGVyIGlkZW50aXR5KSBpcyBza2lwcGVkXG4gKiAgICh1bmF1dGhlbnRpY2F0ZWQgLyBoZWFsdGggLyBlYXJseS10ZXJtaW5hdGVkKTtcbiAqIC0gU0VSVklDRS1QUklOQ0lQQUwgY2FsbGVycyBhcmUgc2tpcHBlZDogaW50ZXJuYWwgc2VydmljZS10by1zZXJ2aWNlIHRyYWZmaWNcbiAqICAgKGUuZy4gYSBwZWVyIGNhbGxpbmcgY29tcGxpYW5jZSBgdmFsaWRhdGVgKSBtdXN0IG5vdCBidXJuIGEgdGVuYW50J3MgcXVvdGEg4oCUXG4gKiAgIHRoZSBvcmlnaW5hdGluZyBzZXJ2aWNlIGFscmVhZHkgbWV0ZXJzIHRoZSB1c2VyIGFjdGlvbiB0aGF0IHRyaWdnZXJlZCBpdC5cbiAqXG4gKiBEbyBOT1QgY29tYmluZSB0aGlzIHdpdGggYSBoYW5kbGVyIHRoYXQgYWxyZWFkeSBjYWxscyBgaW5jcmVtZW50UXVvdGFGcm9tQ3R4YFxuICogZm9yIHRoZSBzYW1lIHF1b3RhIHR5cGUgb24gdGhlIHNhbWUgcm91dGUg4oCUIHRoYXQgZG91YmxlLWNvdW50cy5cbiAqXG4gKiBAcGFyYW0gcXVvdGFTZXJ2aWNlIC0gUXVvdGEgc2VydmljZSBjbGllbnRcbiAqIEBwYXJhbSBxdW90YVR5cGUgLSBXaGljaCBxdW90YSB0byBtZXRlciAoZS5nLiAnYXBpQ2FsbHMnKVxuICovXG5leHBvcnQgZnVuY3Rpb24gbWV0ZXJRdW90YU9uU3VjY2VzcyhxdW90YVNlcnZpY2U6IFF1b3RhU2VydmljZSwgcXVvdGFUeXBlOiBRdW90YVR5cGUpIHtcbiAgcmV0dXJuIChyZXE6IFJlcXVlc3QsIHJlczogUmVzcG9uc2UsIG5leHQ6IE5leHRGdW5jdGlvbik6IHZvaWQgPT4ge1xuICAgIHJlcy5vbignZmluaXNoJywgKCkgPT4ge1xuICAgICAgdHJ5IHtcbiAgICAgICAgLy8gT25seSBzdWNjZXNzZnVsIHJlc3BvbnNlcyBhcmUgYmlsbGFibGUuXG4gICAgICAgIGlmIChyZXMuc3RhdHVzQ29kZSA8IDIwMCB8fCByZXMuc3RhdHVzQ29kZSA+PSAzMDApIHJldHVybjtcbiAgICAgICAgLy8gUmVzb2x2ZSB0aGUgb3JnIGlkIGZyb20gdGhlIFNBTUUgc291cmNlIGBjaGVja1F1b3RhYCByZWFkcyDigJRcbiAgICAgICAgLy8gYGdldENvbnRleHQoKS5pZGVudGl0eS5vcmdJZGAsIHdoaWNoIGlzIG5vcm1hbGl6ZWQgKHRyaW1tZWQgKyBsb3dlcmNhc2VkKS5cbiAgICAgICAgLy8gTWV0ZXJpbmcgb2ZmIHRoZSByYXcgYHJlcS51c2VyLm9yZ2FuaXphdGlvbklkYCB3aGlsZSB0aGUgZ2F0ZSBjaGVja3MgdGhlXG4gICAgICAgIC8vIG5vcm1hbGl6ZWQgaWQgd291bGQgbGFuZCBpbmNyZW1lbnRzIG9uIGEgZGlmZmVyZW50IGtleSB0aGFuIHRoZSBjaGVja1xuICAgICAgICAvLyByZWFkcyAoYSBzaWxlbnQgYnlwYXNzIGlmIGFuIG9yZyBpZCBldmVyIGNhcnJpZXMgbWl4ZWQgY2FzZSkuIEZhbGwgYmFjayB0b1xuICAgICAgICAvLyB0aGUgcmF3IGF1dGggb3JnIG9ubHkgd2hlbiBjb250ZXh0IG1pZGRsZXdhcmUgaXNuJ3QgbW91bnRlZC5cbiAgICAgICAgbGV0IG9yZ0lkOiBzdHJpbmcgfCB1bmRlZmluZWQ7XG4gICAgICAgIHRyeSB7IG9yZ0lkID0gZ2V0Q29udGV4dChyZXEpLmlkZW50aXR5Lm9yZ0lkOyB9IGNhdGNoIHsgb3JnSWQgPSByZXEudXNlcj8ub3JnYW5pemF0aW9uSWQ7IH1cbiAgICAgICAgaWYgKCFvcmdJZCkgcmV0dXJuO1xuICAgICAgICAvLyBJbnRlcm5hbCBzZXJ2aWNlLXRvLXNlcnZpY2UgY2FsbHMgbXVzdCBub3QgY29uc3VtZSBhIHRlbmFudCdzIHF1b3RhLlxuICAgICAgICBpZiAoaXNTZXJ2aWNlUHJpbmNpcGFsKHJlcSkpIHJldHVybjtcbiAgICAgICAgaW5jcmVtZW50UXVvdGEoXG4gICAgICAgICAgcXVvdGFTZXJ2aWNlLFxuICAgICAgICAgIG9yZ0lkLFxuICAgICAgICAgIHF1b3RhVHlwZSxcbiAgICAgICAgICByZXEuaGVhZGVycy5hdXRob3JpemF0aW9uIHx8ICcnLFxuICAgICAgICAgIChtZXNzYWdlLCBkYXRhKSA9PiBsb2dnZXIud2FybihtZXNzYWdlLCBkYXRhIGFzIFJlY29yZDxzdHJpbmcsIHVua25vd24+KSxcbiAgICAgICAgKTtcbiAgICAgIH0gY2F0Y2gge1xuICAgICAgICAvLyBNZXRlcmluZyBtdXN0IG5ldmVyIGFmZmVjdCB0aGUgcmVzcG9uc2U7IGEgbWlzc2luZyB1c2VyL2NvbnRleHQganVzdFxuICAgICAgICAvLyBtZWFucyBhbiB1bmF1dGhlbnRpY2F0ZWQgb3IgZWFybHktdGVybWluYXRlZCByZXF1ZXN0IOKAlCBza2lwIHNpbGVudGx5LlxuICAgICAgfVxuICAgIH0pO1xuICAgIG5leHQoKTtcbiAgfTtcbn1cbiJdfQ==
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Express middleware that records request duration and count.
|
|
4
|
+
*
|
|
5
|
+
* Must be registered before route handlers so `res.on('finish')` fires
|
|
6
|
+
* after the response is sent.
|
|
7
|
+
*/
|
|
8
|
+
export declare function metricsMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Express handler that returns Prometheus metrics in text exposition format.
|
|
11
|
+
*/
|
|
12
|
+
export declare function metricsHandler(): (_req: Request, res: Response) => Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Increment a business counter, creating it on first use.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* incCounter('pipelines_generated_total', { provider: 'anthropic' });
|
|
19
|
+
* incCounter('plugin_builds_total', { status: 'completed' });
|
|
20
|
+
* incCounter('quota_threshold_crossed_total', { type: 'aiCalls', tier: 'pro' });
|
|
21
|
+
* incCounter('compliance_violations_found_total', { severity: 'critical' });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function incCounter(name: string, labels?: Record<string, string>, value?: number): void;
|
|
25
|
+
/**
|
|
26
|
+
* Record an observation on a business histogram, creating it on first use.
|
|
27
|
+
* Use for durations, sizes, anything where percentiles matter.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* observe('ai_generation_duration_seconds', { provider: 'anthropic', model: 'sonnet' }, durationSec);
|
|
32
|
+
* observe('plugin_build_duration_seconds', { buildType: 'docker' }, durationSec);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function observe(name: string, labels: Record<string, string>, value: number): void;
|
|
36
|
+
/**
|
|
37
|
+
* Set the value of a business gauge, creating it on first use. Use for
|
|
38
|
+
* point-in-time observations sampled by a scraper — queue depth, active
|
|
39
|
+
* connection counts, in-flight requests.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* setGauge('plugin_queue_jobs', { queue: 'plugin-build', state: 'waiting' }, 12);
|
|
44
|
+
* setGauge('plugin_queue_jobs', { queue: 'plugin-build', state: 'active' }, 3);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function setGauge(name: string, labels: Record<string, string>, value: number): void;
|