@pipeline-builder/api-server 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +39 -0
  3. package/lib/api/app-factory.d.ts +71 -0
  4. package/lib/api/app-factory.js +212 -0
  5. package/lib/api/check-quota.d.ts +13 -0
  6. package/lib/api/check-quota.js +67 -0
  7. package/lib/api/context-middleware.d.ts +34 -0
  8. package/lib/api/context-middleware.js +45 -0
  9. package/lib/api/etag-middleware.d.ts +7 -0
  10. package/lib/api/etag-middleware.js +37 -0
  11. package/lib/api/get-context.d.ts +22 -0
  12. package/lib/api/get-context.js +31 -0
  13. package/lib/api/health-checks.d.ts +25 -0
  14. package/lib/api/health-checks.js +36 -0
  15. package/lib/api/idempotency-middleware.d.ts +9 -0
  16. package/lib/api/idempotency-middleware.js +64 -0
  17. package/lib/api/index.d.ts +15 -0
  18. package/lib/api/index.js +43 -0
  19. package/lib/api/metrics.d.ts +12 -0
  20. package/lib/api/metrics.js +83 -0
  21. package/lib/api/middleware-factory.d.ts +47 -0
  22. package/lib/api/middleware-factory.js +66 -0
  23. package/lib/api/middleware.d.ts +1 -0
  24. package/lib/api/middleware.js +14 -0
  25. package/lib/api/quota-helpers.d.ts +23 -0
  26. package/lib/api/quota-helpers.js +25 -0
  27. package/lib/api/request-types.d.ts +55 -0
  28. package/lib/api/request-types.js +62 -0
  29. package/lib/api/require-org-id.d.ts +14 -0
  30. package/lib/api/require-org-id.js +31 -0
  31. package/lib/api/route-wrapper.d.ts +50 -0
  32. package/lib/api/route-wrapper.js +62 -0
  33. package/lib/api/server.d.ts +79 -0
  34. package/lib/api/server.js +144 -0
  35. package/lib/api/tracing.d.ts +15 -0
  36. package/lib/api/tracing.js +53 -0
  37. package/lib/http/index.d.ts +2 -0
  38. package/lib/http/index.js +21 -0
  39. package/lib/http/sse-connection-manager.d.ts +145 -0
  40. package/lib/http/sse-connection-manager.js +329 -0
  41. package/lib/http/ws-manager.d.ts +37 -0
  42. package/lib/http/ws-manager.js +105 -0
  43. package/lib/index.d.ts +30 -0
  44. package/lib/index.js +51 -0
  45. package/package.json +143 -0
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ // Copyright 2026 Pipeline Builder Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.checkQuota = checkQuota;
6
+ const api_core_1 = require("@pipeline-builder/api-core");
7
+ const get_context_1 = require("./get-context");
8
+ const logger = (0, api_core_1.createLogger)('check-quota');
9
+ /** Human-readable labels for quota exceeded messages. */
10
+ const QUOTA_LABELS = {
11
+ apiCalls: 'API call',
12
+ pipelines: 'Pipeline',
13
+ plugins: 'Plugin',
14
+ };
15
+ /**
16
+ * Create middleware that checks a specific quota type before proceeding.
17
+ *
18
+ * On quota exceeded, returns a 429 response with quota details.
19
+ * On quota service failure, fails open (allows the request).
20
+ *
21
+ * @param quotaService - Quota service client
22
+ * @param quotaType - Which quota to check (e.g. 'apiCalls', 'pipelines', 'plugins')
23
+ * @returns Express middleware
24
+ */
25
+ function checkQuota(quotaService, quotaType) {
26
+ return async (req, res, next) => {
27
+ let orgId;
28
+ let authHeader = '';
29
+ try {
30
+ const ctx = (0, get_context_1.getContext)(req);
31
+ orgId = ctx.identity.orgId;
32
+ authHeader = req.headers.authorization || '';
33
+ }
34
+ catch {
35
+ // Context middleware not applied — fail open (log and continue)
36
+ logger.warn('Quota check skipped: request context not initialized');
37
+ return next();
38
+ }
39
+ if (!orgId) {
40
+ (0, api_core_1.sendError)(res, 400, 'Organization ID is required for quota check', api_core_1.ErrorCode.VALIDATION_ERROR);
41
+ return;
42
+ }
43
+ try {
44
+ const quotaStatus = await quotaService.check(orgId, quotaType, authHeader);
45
+ if (!quotaStatus.allowed) {
46
+ logger.warn(`${quotaType} quota exceeded`, {
47
+ orgId,
48
+ limit: quotaStatus.limit,
49
+ used: quotaStatus.used,
50
+ });
51
+ (0, api_core_1.sendError)(res, 429, `${QUOTA_LABELS[quotaType]} quota exceeded. Please contact your administrator to increase your quota.`, api_core_1.ErrorCode.QUOTA_EXCEEDED, { quota: { type: quotaType, limit: quotaStatus.limit, used: quotaStatus.used, remaining: quotaStatus.remaining } });
52
+ return;
53
+ }
54
+ next();
55
+ }
56
+ catch (error) {
57
+ // Fail open — allow the request if quota service is unavailable
58
+ logger.warn('QUOTA_FAIL_OPEN: Quota check exception, allowing request', {
59
+ orgId,
60
+ quotaType,
61
+ error: error instanceof Error ? error.message : String(error),
62
+ });
63
+ next();
64
+ }
65
+ };
66
+ }
67
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hlY2stcXVvdGEuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXBpL2NoZWNrLXF1b3RhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDOztBQTBCdEMsZ0NBc0RDO0FBOUVELHlEQUFnRjtBQUdoRiwrQ0FBMkM7QUFFM0MsTUFBTSxNQUFNLEdBQUcsSUFBQSx1QkFBWSxFQUFDLGFBQWEsQ0FBQyxDQUFDO0FBRTNDLHlEQUF5RDtBQUN6RCxNQUFNLFlBQVksR0FBOEI7SUFDOUMsUUFBUSxFQUFFLFVBQVU7SUFDcEIsU0FBUyxFQUFFLFVBQVU7SUFDckIsT0FBTyxFQUFFLFFBQVE7Q0FDbEIsQ0FBQztBQUVGOzs7Ozs7Ozs7R0FTRztBQUNILFNBQWdCLFVBQVUsQ0FDeEIsWUFBMEIsRUFDMUIsU0FBb0I7SUFFcEIsT0FBTyxLQUFLLEVBQUUsR0FBWSxFQUFFLEdBQWEsRUFBRSxJQUFrQixFQUFpQixFQUFFO1FBQzlFLElBQUksS0FBeUIsQ0FBQztRQUM5QixJQUFJLFVBQVUsR0FBRyxFQUFFLENBQUM7UUFFcEIsSUFBSSxDQUFDO1lBQ0gsTUFBTSxHQUFHLEdBQUcsSUFBQSx3QkFBVSxFQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQzVCLEtBQUssR0FBRyxHQUFHLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQztZQUMzQixVQUFVLEdBQUcsR0FBRyxDQUFDLE9BQU8sQ0FBQyxhQUFhLElBQUksRUFBRSxDQUFDO1FBQy9DLENBQUM7UUFBQyxNQUFNLENBQUM7WUFDUCxnRUFBZ0U7WUFDaEUsTUFBTSxDQUFDLElBQUksQ0FBQyxzREFBc0QsQ0FBQyxDQUFDO1lBQ3BFLE9BQU8sSUFBSSxFQUFFLENBQUM7UUFDaEIsQ0FBQztRQUVELElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztZQUNYLElBQUEsb0JBQVMsRUFBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLDZDQUE2QyxFQUFFLG9CQUFTLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztZQUMvRixPQUFPO1FBQ1QsQ0FBQztRQUVELElBQUksQ0FBQztZQUNILE1BQU0sV0FBVyxHQUFHLE1BQU0sWUFBWSxDQUFDLEtBQUssQ0FBQyxLQUFLLEVBQUUsU0FBUyxFQUFFLFVBQVUsQ0FBQyxDQUFDO1lBRTNFLElBQUksQ0FBQyxXQUFXLENBQUMsT0FBTyxFQUFFLENBQUM7Z0JBQ3pCLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxTQUFTLGlCQUFpQixFQUFFO29CQUN6QyxLQUFLO29CQUNMLEtBQUssRUFBRSxXQUFXLENBQUMsS0FBSztvQkFDeEIsSUFBSSxFQUFFLFdBQVcsQ0FBQyxJQUFJO2lCQUN2QixDQUFDLENBQUM7Z0JBRUgsSUFBQSxvQkFBUyxFQUNQLEdBQUcsRUFDSCxHQUFHLEVBQ0gsR0FBRyxZQUFZLENBQUMsU0FBUyxDQUFDLDRFQUE0RSxFQUN0RyxvQkFBUyxDQUFDLGNBQWMsRUFDeEIsRUFBRSxLQUFLLEVBQUUsRUFBRSxJQUFJLEVBQUUsU0FBUyxFQUFFLEtBQUssRUFBRSxXQUFXLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxXQUFXLENBQUMsSUFBSSxFQUFFLFNBQVMsRUFBRSxXQUFXLENBQUMsU0FBUyxFQUFFLEVBQUUsQ0FDbkgsQ0FBQztnQkFDRixPQUFPO1lBQ1QsQ0FBQztZQUVELElBQUksRUFBRSxDQUFDO1FBQ1QsQ0FBQztRQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7WUFDZixnRUFBZ0U7WUFDaEUsTUFBTSxDQUFDLElBQUksQ0FBQywwREFBMEQsRUFBRTtnQkFDdEUsS0FBSztnQkFDTCxTQUFTO2dCQUNULEtBQUssRUFBRSxLQUFLLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDO2FBQzlELENBQUMsQ0FBQztZQUNILElBQUksRUFBRSxDQUFDO1FBQ1QsQ0FBQztJQUNILENBQUMsQ0FBQztBQUNKLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuaW1wb3J0IHsgRXJyb3JDb2RlLCBjcmVhdGVMb2dnZXIsIHNlbmRFcnJvciB9IGZyb20gJ0BwaXBlbGluZS1idWlsZGVyL2FwaS1jb3JlJztcbmltcG9ydCB0eXBlIHsgUXVvdGFUeXBlLCBRdW90YVNlcnZpY2UgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgeyBSZXF1ZXN0LCBSZXNwb25zZSwgTmV4dEZ1bmN0aW9uIH0gZnJvbSAnZXhwcmVzcyc7XG5pbXBvcnQgeyBnZXRDb250ZXh0IH0gZnJvbSAnLi9nZXQtY29udGV4dCc7XG5cbmNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcignY2hlY2stcXVvdGEnKTtcblxuLyoqIEh1bWFuLXJlYWRhYmxlIGxhYmVscyBmb3IgcXVvdGEgZXhjZWVkZWQgbWVzc2FnZXMuICovXG5jb25zdCBRVU9UQV9MQUJFTFM6IFJlY29yZDxRdW90YVR5cGUsIHN0cmluZz4gPSB7XG4gIGFwaUNhbGxzOiAnQVBJIGNhbGwnLFxuICBwaXBlbGluZXM6ICdQaXBlbGluZScsXG4gIHBsdWdpbnM6ICdQbHVnaW4nLFxufTtcblxuLyoqXG4gKiBDcmVhdGUgbWlkZGxld2FyZSB0aGF0IGNoZWNrcyBhIHNwZWNpZmljIHF1b3RhIHR5cGUgYmVmb3JlIHByb2NlZWRpbmcuXG4gKlxuICogT24gcXVvdGEgZXhjZWVkZWQsIHJldHVybnMgYSA0MjkgcmVzcG9uc2Ugd2l0aCBxdW90YSBkZXRhaWxzLlxuICogT24gcXVvdGEgc2VydmljZSBmYWlsdXJlLCBmYWlscyBvcGVuIChhbGxvd3MgdGhlIHJlcXVlc3QpLlxuICpcbiAqIEBwYXJhbSBxdW90YVNlcnZpY2UgLSBRdW90YSBzZXJ2aWNlIGNsaWVudFxuICogQHBhcmFtIHF1b3RhVHlwZSAtIFdoaWNoIHF1b3RhIHRvIGNoZWNrIChlLmcuICdhcGlDYWxscycsICdwaXBlbGluZXMnLCAncGx1Z2lucycpXG4gKiBAcmV0dXJucyBFeHByZXNzIG1pZGRsZXdhcmVcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGNoZWNrUXVvdGEoXG4gIHF1b3RhU2VydmljZTogUXVvdGFTZXJ2aWNlLFxuICBxdW90YVR5cGU6IFF1b3RhVHlwZSxcbikge1xuICByZXR1cm4gYXN5bmMgKHJlcTogUmVxdWVzdCwgcmVzOiBSZXNwb25zZSwgbmV4dDogTmV4dEZ1bmN0aW9uKTogUHJvbWlzZTx2b2lkPiA9PiB7XG4gICAgbGV0IG9yZ0lkOiBzdHJpbmcgfCB1bmRlZmluZWQ7XG4gICAgbGV0IGF1dGhIZWFkZXIgPSAnJztcblxuICAgIHRyeSB7XG4gICAgICBjb25zdCBjdHggPSBnZXRDb250ZXh0KHJlcSk7XG4gICAgICBvcmdJZCA9IGN0eC5pZGVudGl0eS5vcmdJZDtcbiAgICAgIGF1dGhIZWFkZXIgPSByZXEuaGVhZGVycy5hdXRob3JpemF0aW9uIHx8ICcnO1xuICAgIH0gY2F0Y2gge1xuICAgICAgLy8gQ29udGV4dCBtaWRkbGV3YXJlIG5vdCBhcHBsaWVkIOKAlCBmYWlsIG9wZW4gKGxvZyBhbmQgY29udGludWUpXG4gICAgICBsb2dnZXIud2FybignUXVvdGEgY2hlY2sgc2tpcHBlZDogcmVxdWVzdCBjb250ZXh0IG5vdCBpbml0aWFsaXplZCcpO1xuICAgICAgcmV0dXJuIG5leHQoKTtcbiAgICB9XG5cbiAgICBpZiAoIW9yZ0lkKSB7XG4gICAgICBzZW5kRXJyb3IocmVzLCA0MDAsICdPcmdhbml6YXRpb24gSUQgaXMgcmVxdWlyZWQgZm9yIHF1b3RhIGNoZWNrJywgRXJyb3JDb2RlLlZBTElEQVRJT05fRVJST1IpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIHRyeSB7XG4gICAgICBjb25zdCBxdW90YVN0YXR1cyA9IGF3YWl0IHF1b3RhU2VydmljZS5jaGVjayhvcmdJZCwgcXVvdGFUeXBlLCBhdXRoSGVhZGVyKTtcblxuICAgICAgaWYgKCFxdW90YVN0YXR1cy5hbGxvd2VkKSB7XG4gICAgICAgIGxvZ2dlci53YXJuKGAke3F1b3RhVHlwZX0gcXVvdGEgZXhjZWVkZWRgLCB7XG4gICAgICAgICAgb3JnSWQsXG4gICAgICAgICAgbGltaXQ6IHF1b3RhU3RhdHVzLmxpbWl0LFxuICAgICAgICAgIHVzZWQ6IHF1b3RhU3RhdHVzLnVzZWQsXG4gICAgICAgIH0pO1xuXG4gICAgICAgIHNlbmRFcnJvcihcbiAgICAgICAgICByZXMsXG4gICAgICAgICAgNDI5LFxuICAgICAgICAgIGAke1FVT1RBX0xBQkVMU1txdW90YVR5cGVdfSBxdW90YSBleGNlZWRlZC4gUGxlYXNlIGNvbnRhY3QgeW91ciBhZG1pbmlzdHJhdG9yIHRvIGluY3JlYXNlIHlvdXIgcXVvdGEuYCxcbiAgICAgICAgICBFcnJvckNvZGUuUVVPVEFfRVhDRUVERUQsXG4gICAgICAgICAgeyBxdW90YTogeyB0eXBlOiBxdW90YVR5cGUsIGxpbWl0OiBxdW90YVN0YXR1cy5saW1pdCwgdXNlZDogcXVvdGFTdGF0dXMudXNlZCwgcmVtYWluaW5nOiBxdW90YVN0YXR1cy5yZW1haW5pbmcgfSB9LFxuICAgICAgICApO1xuICAgICAgICByZXR1cm47XG4gICAgICB9XG5cbiAgICAgIG5leHQoKTtcbiAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgLy8gRmFpbCBvcGVuIOKAlCBhbGxvdyB0aGUgcmVxdWVzdCBpZiBxdW90YSBzZXJ2aWNlIGlzIHVuYXZhaWxhYmxlXG4gICAgICBsb2dnZXIud2FybignUVVPVEFfRkFJTF9PUEVOOiBRdW90YSBjaGVjayBleGNlcHRpb24sIGFsbG93aW5nIHJlcXVlc3QnLCB7XG4gICAgICAgIG9yZ0lkLFxuICAgICAgICBxdW90YVR5cGUsXG4gICAgICAgIGVycm9yOiBlcnJvciBpbnN0YW5jZW9mIEVycm9yID8gZXJyb3IubWVzc2FnZSA6IFN0cmluZyhlcnJvciksXG4gICAgICB9KTtcbiAgICAgIG5leHQoKTtcbiAgICB9XG4gIH07XG59XG4iXX0=
@@ -0,0 +1,34 @@
1
+ import { RequestHandler } from 'express';
2
+ import type { SSEManager } from '../http/sse-connection-manager';
3
+ /**
4
+ * Creates middleware that attaches RequestContext to req.context
5
+ *
6
+ * The context includes:
7
+ * - requestId: Unique request identifier
8
+ * - identity: User identity from JWT (orgId, userId, role)
9
+ * - log: Logger function that sends to both Winston and SSE
10
+ *
11
+ * @param sseManager - SSE manager for real-time logging
12
+ * @returns Express middleware that attaches context to req.context
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const { app, sseManager } = createApp();
17
+ *
18
+ * // Apply context middleware globally
19
+ * app.use(attachRequestContext(sseManager));
20
+ *
21
+ * // Use context in route handlers
22
+ * app.get('/pipelines', requireAuth, async (req, res) => {
23
+ * const ctx = getContext(req);
24
+ * ctx.log('INFO', 'Fetching pipelines');
25
+ *
26
+ * if (!ctx.identity.orgId) {
27
+ * return sendError(res, 400, 'Organization ID required');
28
+ * }
29
+ *
30
+ * // ... rest of handler
31
+ * });
32
+ * ```
33
+ */
34
+ export declare function attachRequestContext(sseManager: SSEManager): RequestHandler;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ // Copyright 2026 Pipeline Builder Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.attachRequestContext = attachRequestContext;
6
+ const request_types_1 = require("./request-types");
7
+ /**
8
+ * Creates middleware that attaches RequestContext to req.context
9
+ *
10
+ * The context includes:
11
+ * - requestId: Unique request identifier
12
+ * - identity: User identity from JWT (orgId, userId, role)
13
+ * - log: Logger function that sends to both Winston and SSE
14
+ *
15
+ * @param sseManager - SSE manager for real-time logging
16
+ * @returns Express middleware that attaches context to req.context
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const { app, sseManager } = createApp();
21
+ *
22
+ * // Apply context middleware globally
23
+ * app.use(attachRequestContext(sseManager));
24
+ *
25
+ * // Use context in route handlers
26
+ * app.get('/pipelines', requireAuth, async (req, res) => {
27
+ * const ctx = getContext(req);
28
+ * ctx.log('INFO', 'Fetching pipelines');
29
+ *
30
+ * if (!ctx.identity.orgId) {
31
+ * return sendError(res, 400, 'Organization ID required');
32
+ * }
33
+ *
34
+ * // ... rest of handler
35
+ * });
36
+ * ```
37
+ */
38
+ function attachRequestContext(sseManager) {
39
+ return (req, _res, next) => {
40
+ // Create and attach context to request
41
+ req.context = (0, request_types_1.createRequestContext)(req, sseManager);
42
+ next();
43
+ };
44
+ }
45
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udGV4dC1taWRkbGV3YXJlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FwaS9jb250ZXh0LW1pZGRsZXdhcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7O0FBcUN0QyxvREFNQztBQXhDRCxtREFBdUQ7QUFHdkQ7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQThCRztBQUNILFNBQWdCLG9CQUFvQixDQUFDLFVBQXNCO0lBQ3pELE9BQU8sQ0FBQyxHQUFZLEVBQUUsSUFBYyxFQUFFLElBQWtCLEVBQVEsRUFBRTtRQUNoRSx1Q0FBdUM7UUFDdkMsR0FBRyxDQUFDLE9BQU8sR0FBRyxJQUFBLG9DQUFvQixFQUFDLEdBQUcsRUFBRSxVQUFVLENBQUMsQ0FBQztRQUNwRCxJQUFJLEVBQUUsQ0FBQztJQUNULENBQUMsQ0FBQztBQUNKLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuaW1wb3J0IHsgUmVxdWVzdCwgUmVzcG9uc2UsIE5leHRGdW5jdGlvbiwgUmVxdWVzdEhhbmRsZXIgfSBmcm9tICdleHByZXNzJztcbmltcG9ydCB7IGNyZWF0ZVJlcXVlc3RDb250ZXh0IH0gZnJvbSAnLi9yZXF1ZXN0LXR5cGVzJztcbmltcG9ydCB0eXBlIHsgU1NFTWFuYWdlciB9IGZyb20gJy4uL2h0dHAvc3NlLWNvbm5lY3Rpb24tbWFuYWdlcic7XG5cbi8qKlxuICogQ3JlYXRlcyBtaWRkbGV3YXJlIHRoYXQgYXR0YWNoZXMgUmVxdWVzdENvbnRleHQgdG8gcmVxLmNvbnRleHRcbiAqXG4gKiBUaGUgY29udGV4dCBpbmNsdWRlczpcbiAqIC0gcmVxdWVzdElkOiBVbmlxdWUgcmVxdWVzdCBpZGVudGlmaWVyXG4gKiAtIGlkZW50aXR5OiBVc2VyIGlkZW50aXR5IGZyb20gSldUIChvcmdJZCwgdXNlcklkLCByb2xlKVxuICogLSBsb2c6IExvZ2dlciBmdW5jdGlvbiB0aGF0IHNlbmRzIHRvIGJvdGggV2luc3RvbiBhbmQgU1NFXG4gKlxuICogQHBhcmFtIHNzZU1hbmFnZXIgLSBTU0UgbWFuYWdlciBmb3IgcmVhbC10aW1lIGxvZ2dpbmdcbiAqIEByZXR1cm5zIEV4cHJlc3MgbWlkZGxld2FyZSB0aGF0IGF0dGFjaGVzIGNvbnRleHQgdG8gcmVxLmNvbnRleHRcbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogY29uc3QgeyBhcHAsIHNzZU1hbmFnZXIgfSA9IGNyZWF0ZUFwcCgpO1xuICpcbiAqIC8vIEFwcGx5IGNvbnRleHQgbWlkZGxld2FyZSBnbG9iYWxseVxuICogYXBwLnVzZShhdHRhY2hSZXF1ZXN0Q29udGV4dChzc2VNYW5hZ2VyKSk7XG4gKlxuICogLy8gVXNlIGNvbnRleHQgaW4gcm91dGUgaGFuZGxlcnNcbiAqIGFwcC5nZXQoJy9waXBlbGluZXMnLCByZXF1aXJlQXV0aCwgYXN5bmMgKHJlcSwgcmVzKSA9PiB7XG4gKiAgIGNvbnN0IGN0eCA9IGdldENvbnRleHQocmVxKTtcbiAqICAgY3R4LmxvZygnSU5GTycsICdGZXRjaGluZyBwaXBlbGluZXMnKTtcbiAqXG4gKiAgIGlmICghY3R4LmlkZW50aXR5Lm9yZ0lkKSB7XG4gKiAgICAgcmV0dXJuIHNlbmRFcnJvcihyZXMsIDQwMCwgJ09yZ2FuaXphdGlvbiBJRCByZXF1aXJlZCcpO1xuICogICB9XG4gKlxuICogICAvLyAuLi4gcmVzdCBvZiBoYW5kbGVyXG4gKiB9KTtcbiAqIGBgYFxuICovXG5leHBvcnQgZnVuY3Rpb24gYXR0YWNoUmVxdWVzdENvbnRleHQoc3NlTWFuYWdlcjogU1NFTWFuYWdlcik6IFJlcXVlc3RIYW5kbGVyIHtcbiAgcmV0dXJuIChyZXE6IFJlcXVlc3QsIF9yZXM6IFJlc3BvbnNlLCBuZXh0OiBOZXh0RnVuY3Rpb24pOiB2b2lkID0+IHtcbiAgICAvLyBDcmVhdGUgYW5kIGF0dGFjaCBjb250ZXh0IHRvIHJlcXVlc3RcbiAgICByZXEuY29udGV4dCA9IGNyZWF0ZVJlcXVlc3RDb250ZXh0KHJlcSwgc3NlTWFuYWdlcik7XG4gICAgbmV4dCgpO1xuICB9O1xufVxuIl19
@@ -0,0 +1,7 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ /**
3
+ * Middleware that adds ETag support for conditional GET requests.
4
+ * Generates a weak ETag from the response body hash.
5
+ * Returns 304 Not Modified when the client's If-None-Match header matches.
6
+ */
7
+ export declare function etagMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ // Copyright 2026 Pipeline Builder Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.etagMiddleware = etagMiddleware;
6
+ const crypto_1 = require("crypto");
7
+ /**
8
+ * Middleware that adds ETag support for conditional GET requests.
9
+ * Generates a weak ETag from the response body hash.
10
+ * Returns 304 Not Modified when the client's If-None-Match header matches.
11
+ */
12
+ function etagMiddleware() {
13
+ return (req, res, next) => {
14
+ // Only apply to GET requests
15
+ if (req.method !== 'GET')
16
+ return next();
17
+ res.json = (body) => {
18
+ // Serialize once, hash, and send — avoids double JSON.stringify
19
+ const serialized = JSON.stringify(body);
20
+ const hash = (0, crypto_1.createHash)('md5').update(serialized).digest('hex').slice(0, 16);
21
+ const etag = `W/"${hash}"`;
22
+ res.setHeader('ETag', etag);
23
+ // Check If-None-Match header
24
+ const ifNoneMatch = req.headers['if-none-match'];
25
+ if (ifNoneMatch && ifNoneMatch === etag) {
26
+ res.status(304).end();
27
+ return res;
28
+ }
29
+ // Send pre-serialized body to avoid double JSON.stringify
30
+ res.setHeader('Content-Type', 'application/json');
31
+ res.end(serialized);
32
+ return res;
33
+ };
34
+ next();
35
+ };
36
+ }
37
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXRhZy1taWRkbGV3YXJlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FwaS9ldGFnLW1pZGRsZXdhcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7O0FBVXRDLHdDQTRCQztBQXBDRCxtQ0FBb0M7QUFHcEM7Ozs7R0FJRztBQUNILFNBQWdCLGNBQWM7SUFDNUIsT0FBTyxDQUFDLEdBQVksRUFBRSxHQUFhLEVBQUUsSUFBa0IsRUFBRSxFQUFFO1FBQ3pELDZCQUE2QjtRQUM3QixJQUFJLEdBQUcsQ0FBQyxNQUFNLEtBQUssS0FBSztZQUFFLE9BQU8sSUFBSSxFQUFFLENBQUM7UUFFeEMsR0FBRyxDQUFDLElBQUksR0FBRyxDQUFDLElBQWEsRUFBRSxFQUFFO1lBQzNCLGdFQUFnRTtZQUNoRSxNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ3hDLE1BQU0sSUFBSSxHQUFHLElBQUEsbUJBQVUsRUFBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDN0UsTUFBTSxJQUFJLEdBQUcsTUFBTSxJQUFJLEdBQUcsQ0FBQztZQUUzQixHQUFHLENBQUMsU0FBUyxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsQ0FBQztZQUU1Qiw2QkFBNkI7WUFDN0IsTUFBTSxXQUFXLEdBQUcsR0FBRyxDQUFDLE9BQU8sQ0FBQyxlQUFlLENBQUMsQ0FBQztZQUNqRCxJQUFJLFdBQVcsSUFBSSxXQUFXLEtBQUssSUFBSSxFQUFFLENBQUM7Z0JBQ3hDLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsR0FBRyxFQUFFLENBQUM7Z0JBQ3RCLE9BQU8sR0FBRyxDQUFDO1lBQ2IsQ0FBQztZQUVELDBEQUEwRDtZQUMxRCxHQUFHLENBQUMsU0FBUyxDQUFDLGNBQWMsRUFBRSxrQkFBa0IsQ0FBQyxDQUFDO1lBQ2xELEdBQUcsQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLENBQUM7WUFDcEIsT0FBTyxHQUFHLENBQUM7UUFDYixDQUFDLENBQUM7UUFFRixJQUFJLEVBQUUsQ0FBQztJQUNULENBQUMsQ0FBQztBQUNKLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuaW1wb3J0IHsgY3JlYXRlSGFzaCB9IGZyb20gJ2NyeXB0byc7XG5pbXBvcnQgeyBSZXF1ZXN0LCBSZXNwb25zZSwgTmV4dEZ1bmN0aW9uIH0gZnJvbSAnZXhwcmVzcyc7XG5cbi8qKlxuICogTWlkZGxld2FyZSB0aGF0IGFkZHMgRVRhZyBzdXBwb3J0IGZvciBjb25kaXRpb25hbCBHRVQgcmVxdWVzdHMuXG4gKiBHZW5lcmF0ZXMgYSB3ZWFrIEVUYWcgZnJvbSB0aGUgcmVzcG9uc2UgYm9keSBoYXNoLlxuICogUmV0dXJucyAzMDQgTm90IE1vZGlmaWVkIHdoZW4gdGhlIGNsaWVudCdzIElmLU5vbmUtTWF0Y2ggaGVhZGVyIG1hdGNoZXMuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBldGFnTWlkZGxld2FyZSgpIHtcbiAgcmV0dXJuIChyZXE6IFJlcXVlc3QsIHJlczogUmVzcG9uc2UsIG5leHQ6IE5leHRGdW5jdGlvbikgPT4ge1xuICAgIC8vIE9ubHkgYXBwbHkgdG8gR0VUIHJlcXVlc3RzXG4gICAgaWYgKHJlcS5tZXRob2QgIT09ICdHRVQnKSByZXR1cm4gbmV4dCgpO1xuXG4gICAgcmVzLmpzb24gPSAoYm9keTogdW5rbm93bikgPT4ge1xuICAgICAgLy8gU2VyaWFsaXplIG9uY2UsIGhhc2gsIGFuZCBzZW5kIOKAlCBhdm9pZHMgZG91YmxlIEpTT04uc3RyaW5naWZ5XG4gICAgICBjb25zdCBzZXJpYWxpemVkID0gSlNPTi5zdHJpbmdpZnkoYm9keSk7XG4gICAgICBjb25zdCBoYXNoID0gY3JlYXRlSGFzaCgnbWQ1JykudXBkYXRlKHNlcmlhbGl6ZWQpLmRpZ2VzdCgnaGV4Jykuc2xpY2UoMCwgMTYpO1xuICAgICAgY29uc3QgZXRhZyA9IGBXL1wiJHtoYXNofVwiYDtcblxuICAgICAgcmVzLnNldEhlYWRlcignRVRhZycsIGV0YWcpO1xuXG4gICAgICAvLyBDaGVjayBJZi1Ob25lLU1hdGNoIGhlYWRlclxuICAgICAgY29uc3QgaWZOb25lTWF0Y2ggPSByZXEuaGVhZGVyc1snaWYtbm9uZS1tYXRjaCddO1xuICAgICAgaWYgKGlmTm9uZU1hdGNoICYmIGlmTm9uZU1hdGNoID09PSBldGFnKSB7XG4gICAgICAgIHJlcy5zdGF0dXMoMzA0KS5lbmQoKTtcbiAgICAgICAgcmV0dXJuIHJlcztcbiAgICAgIH1cblxuICAgICAgLy8gU2VuZCBwcmUtc2VyaWFsaXplZCBib2R5IHRvIGF2b2lkIGRvdWJsZSBKU09OLnN0cmluZ2lmeVxuICAgICAgcmVzLnNldEhlYWRlcignQ29udGVudC1UeXBlJywgJ2FwcGxpY2F0aW9uL2pzb24nKTtcbiAgICAgIHJlcy5lbmQoc2VyaWFsaXplZCk7XG4gICAgICByZXR1cm4gcmVzO1xuICAgIH07XG5cbiAgICBuZXh0KCk7XG4gIH07XG59XG4iXX0=
@@ -0,0 +1,22 @@
1
+ import type { Request } from 'express';
2
+ import type { RequestContext } from './request-types';
3
+ /**
4
+ * Safely retrieve the RequestContext from an Express request.
5
+ *
6
+ * Throws a descriptive error if the context middleware has not been applied,
7
+ * instead of silently returning undefined or crashing with a cryptic error.
8
+ *
9
+ * @param req - Express request object
10
+ * @returns The attached RequestContext
11
+ * @throws Error if req.context is not initialized
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * app.get('/pipelines', requireAuth, async (req, res) => {
16
+ * const ctx = getContext(req);
17
+ * ctx.log('INFO', 'Fetching pipelines');
18
+ * // ...
19
+ * });
20
+ * ```
21
+ */
22
+ export declare function getContext(req: Request): RequestContext;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ // Copyright 2026 Pipeline Builder Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.getContext = getContext;
6
+ /**
7
+ * Safely retrieve the RequestContext from an Express request.
8
+ *
9
+ * Throws a descriptive error if the context middleware has not been applied,
10
+ * instead of silently returning undefined or crashing with a cryptic error.
11
+ *
12
+ * @param req - Express request object
13
+ * @returns The attached RequestContext
14
+ * @throws Error if req.context is not initialized
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * app.get('/pipelines', requireAuth, async (req, res) => {
19
+ * const ctx = getContext(req);
20
+ * ctx.log('INFO', 'Fetching pipelines');
21
+ * // ...
22
+ * });
23
+ * ```
24
+ */
25
+ function getContext(req) {
26
+ if (!req.context) {
27
+ throw new Error('Request context not initialized. Ensure attachRequestContext middleware is applied.');
28
+ }
29
+ return req.context;
30
+ }
31
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2V0LWNvbnRleHQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXBpL2dldC1jb250ZXh0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDOztBQXdCdEMsZ0NBT0M7QUExQkQ7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQWtCRztBQUNILFNBQWdCLFVBQVUsQ0FBQyxHQUFZO0lBQ3JDLElBQUksQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDakIsTUFBTSxJQUFJLEtBQUssQ0FDYixxRkFBcUYsQ0FDdEYsQ0FBQztJQUNKLENBQUM7SUFDRCxPQUFPLEdBQUcsQ0FBQyxPQUFPLENBQUM7QUFDckIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG5pbXBvcnQgdHlwZSB7IFJlcXVlc3QgfSBmcm9tICdleHByZXNzJztcbmltcG9ydCB0eXBlIHsgUmVxdWVzdENvbnRleHQgfSBmcm9tICcuL3JlcXVlc3QtdHlwZXMnO1xuXG4vKipcbiAqIFNhZmVseSByZXRyaWV2ZSB0aGUgUmVxdWVzdENvbnRleHQgZnJvbSBhbiBFeHByZXNzIHJlcXVlc3QuXG4gKlxuICogVGhyb3dzIGEgZGVzY3JpcHRpdmUgZXJyb3IgaWYgdGhlIGNvbnRleHQgbWlkZGxld2FyZSBoYXMgbm90IGJlZW4gYXBwbGllZCxcbiAqIGluc3RlYWQgb2Ygc2lsZW50bHkgcmV0dXJuaW5nIHVuZGVmaW5lZCBvciBjcmFzaGluZyB3aXRoIGEgY3J5cHRpYyBlcnJvci5cbiAqXG4gKiBAcGFyYW0gcmVxIC0gRXhwcmVzcyByZXF1ZXN0IG9iamVjdFxuICogQHJldHVybnMgVGhlIGF0dGFjaGVkIFJlcXVlc3RDb250ZXh0XG4gKiBAdGhyb3dzIEVycm9yIGlmIHJlcS5jb250ZXh0IGlzIG5vdCBpbml0aWFsaXplZFxuICpcbiAqIEBleGFtcGxlXG4gKiBgYGB0eXBlc2NyaXB0XG4gKiBhcHAuZ2V0KCcvcGlwZWxpbmVzJywgcmVxdWlyZUF1dGgsIGFzeW5jIChyZXEsIHJlcykgPT4ge1xuICogICBjb25zdCBjdHggPSBnZXRDb250ZXh0KHJlcSk7XG4gKiAgIGN0eC5sb2coJ0lORk8nLCAnRmV0Y2hpbmcgcGlwZWxpbmVzJyk7XG4gKiAgIC8vIC4uLlxuICogfSk7XG4gKiBgYGBcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGdldENvbnRleHQocmVxOiBSZXF1ZXN0KTogUmVxdWVzdENvbnRleHQge1xuICBpZiAoIXJlcS5jb250ZXh0KSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgJ1JlcXVlc3QgY29udGV4dCBub3QgaW5pdGlhbGl6ZWQuIEVuc3VyZSBhdHRhY2hSZXF1ZXN0Q29udGV4dCBtaWRkbGV3YXJlIGlzIGFwcGxpZWQuJyxcbiAgICApO1xuICB9XG4gIHJldHVybiByZXEuY29udGV4dDtcbn1cbiJdfQ==
@@ -0,0 +1,25 @@
1
+ type HealthStatus = 'connected' | 'disconnected' | 'unknown';
2
+ type HealthResult = Record<string, HealthStatus>;
3
+ /**
4
+ * Postgres health probe: returns `{ postgres: 'connected' }` when the pool's
5
+ * connection test succeeds, `{ postgres: 'unknown' }` otherwise. Use as the
6
+ * `checkDependencies` option of `createApp()` for any service backed by the
7
+ * shared pipeline-data Drizzle connection.
8
+ */
9
+ export declare function postgresHealthCheck(): Promise<HealthResult>;
10
+ /**
11
+ * Minimal shape expected of a mongoose connection for the health probe.
12
+ * Accepts `mongoose.connection` directly without requiring mongoose as a
13
+ * dependency of api-server.
14
+ */
15
+ interface MongooseConnectionLike {
16
+ readyState: number;
17
+ }
18
+ /**
19
+ * MongoDB health probe factory: pass `mongoose.connection` and get back a
20
+ * `checkDependencies` callback that reports `{ mongodb: 'connected' |
21
+ * 'disconnected' | 'unknown' }` based on mongoose's readyState. Use for
22
+ * services backed by mongoose.
23
+ */
24
+ export declare function mongoHealthCheck(connection: MongooseConnectionLike): () => Promise<HealthResult>;
25
+ export {};
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ // Copyright 2026 Pipeline Builder Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.postgresHealthCheck = postgresHealthCheck;
6
+ exports.mongoHealthCheck = mongoHealthCheck;
7
+ const pipeline_core_1 = require("@pipeline-builder/pipeline-core");
8
+ /**
9
+ * Postgres health probe: returns `{ postgres: 'connected' }` when the pool's
10
+ * connection test succeeds, `{ postgres: 'unknown' }` otherwise. Use as the
11
+ * `checkDependencies` option of `createApp()` for any service backed by the
12
+ * shared pipeline-data Drizzle connection.
13
+ */
14
+ async function postgresHealthCheck() {
15
+ try {
16
+ await (0, pipeline_core_1.getConnection)().testConnection();
17
+ return { postgres: 'connected' };
18
+ }
19
+ catch {
20
+ return { postgres: 'unknown' };
21
+ }
22
+ }
23
+ /**
24
+ * MongoDB health probe factory: pass `mongoose.connection` and get back a
25
+ * `checkDependencies` callback that reports `{ mongodb: 'connected' |
26
+ * 'disconnected' | 'unknown' }` based on mongoose's readyState. Use for
27
+ * services backed by mongoose.
28
+ */
29
+ function mongoHealthCheck(connection) {
30
+ return async () => ({
31
+ mongodb: connection.readyState === 1 ? 'connected'
32
+ : connection.readyState === 0 ? 'unknown' // starting up
33
+ : 'disconnected',
34
+ });
35
+ }
36
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGVhbHRoLWNoZWNrcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hcGkvaGVhbHRoLWNoZWNrcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUEsK0NBQStDO0FBQy9DLHNDQUFzQzs7QUFhdEMsa0RBT0M7QUFpQkQsNENBTUM7QUF6Q0QsbUVBQWdFO0FBS2hFOzs7OztHQUtHO0FBQ0ksS0FBSyxVQUFVLG1CQUFtQjtJQUN2QyxJQUFJLENBQUM7UUFDSCxNQUFNLElBQUEsNkJBQWEsR0FBRSxDQUFDLGNBQWMsRUFBRSxDQUFDO1FBQ3ZDLE9BQU8sRUFBRSxRQUFRLEVBQUUsV0FBVyxFQUFFLENBQUM7SUFDbkMsQ0FBQztJQUFDLE1BQU0sQ0FBQztRQUNQLE9BQU8sRUFBRSxRQUFRLEVBQUUsU0FBUyxFQUFFLENBQUM7SUFDakMsQ0FBQztBQUNILENBQUM7QUFXRDs7Ozs7R0FLRztBQUNILFNBQWdCLGdCQUFnQixDQUFDLFVBQWtDO0lBQ2pFLE9BQU8sS0FBSyxJQUFJLEVBQUUsQ0FBQyxDQUFDO1FBQ2xCLE9BQU8sRUFBRSxVQUFVLENBQUMsVUFBVSxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsV0FBVztZQUNoRCxDQUFDLENBQUMsVUFBVSxDQUFDLFVBQVUsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxjQUFjO2dCQUN0RCxDQUFDLENBQUMsY0FBYztLQUNyQixDQUFDLENBQUM7QUFDTCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IGdldENvbm5lY3Rpb24gfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9waXBlbGluZS1jb3JlJztcblxudHlwZSBIZWFsdGhTdGF0dXMgPSAnY29ubmVjdGVkJyB8ICdkaXNjb25uZWN0ZWQnIHwgJ3Vua25vd24nO1xudHlwZSBIZWFsdGhSZXN1bHQgPSBSZWNvcmQ8c3RyaW5nLCBIZWFsdGhTdGF0dXM+O1xuXG4vKipcbiAqIFBvc3RncmVzIGhlYWx0aCBwcm9iZTogcmV0dXJucyBgeyBwb3N0Z3JlczogJ2Nvbm5lY3RlZCcgfWAgd2hlbiB0aGUgcG9vbCdzXG4gKiBjb25uZWN0aW9uIHRlc3Qgc3VjY2VlZHMsIGB7IHBvc3RncmVzOiAndW5rbm93bicgfWAgb3RoZXJ3aXNlLiBVc2UgYXMgdGhlXG4gKiBgY2hlY2tEZXBlbmRlbmNpZXNgIG9wdGlvbiBvZiBgY3JlYXRlQXBwKClgIGZvciBhbnkgc2VydmljZSBiYWNrZWQgYnkgdGhlXG4gKiBzaGFyZWQgcGlwZWxpbmUtZGF0YSBEcml6emxlIGNvbm5lY3Rpb24uXG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBwb3N0Z3Jlc0hlYWx0aENoZWNrKCk6IFByb21pc2U8SGVhbHRoUmVzdWx0PiB7XG4gIHRyeSB7XG4gICAgYXdhaXQgZ2V0Q29ubmVjdGlvbigpLnRlc3RDb25uZWN0aW9uKCk7XG4gICAgcmV0dXJuIHsgcG9zdGdyZXM6ICdjb25uZWN0ZWQnIH07XG4gIH0gY2F0Y2gge1xuICAgIHJldHVybiB7IHBvc3RncmVzOiAndW5rbm93bicgfTtcbiAgfVxufVxuXG4vKipcbiAqIE1pbmltYWwgc2hhcGUgZXhwZWN0ZWQgb2YgYSBtb25nb29zZSBjb25uZWN0aW9uIGZvciB0aGUgaGVhbHRoIHByb2JlLlxuICogQWNjZXB0cyBgbW9uZ29vc2UuY29ubmVjdGlvbmAgZGlyZWN0bHkgd2l0aG91dCByZXF1aXJpbmcgbW9uZ29vc2UgYXMgYVxuICogZGVwZW5kZW5jeSBvZiBhcGktc2VydmVyLlxuICovXG5pbnRlcmZhY2UgTW9uZ29vc2VDb25uZWN0aW9uTGlrZSB7XG4gIHJlYWR5U3RhdGU6IG51bWJlcjtcbn1cblxuLyoqXG4gKiBNb25nb0RCIGhlYWx0aCBwcm9iZSBmYWN0b3J5OiBwYXNzIGBtb25nb29zZS5jb25uZWN0aW9uYCBhbmQgZ2V0IGJhY2sgYVxuICogYGNoZWNrRGVwZW5kZW5jaWVzYCBjYWxsYmFjayB0aGF0IHJlcG9ydHMgYHsgbW9uZ29kYjogJ2Nvbm5lY3RlZCcgfFxuICogJ2Rpc2Nvbm5lY3RlZCcgfCAndW5rbm93bicgfWAgYmFzZWQgb24gbW9uZ29vc2UncyByZWFkeVN0YXRlLiBVc2UgZm9yXG4gKiBzZXJ2aWNlcyBiYWNrZWQgYnkgbW9uZ29vc2UuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBtb25nb0hlYWx0aENoZWNrKGNvbm5lY3Rpb246IE1vbmdvb3NlQ29ubmVjdGlvbkxpa2UpOiAoKSA9PiBQcm9taXNlPEhlYWx0aFJlc3VsdD4ge1xuICByZXR1cm4gYXN5bmMgKCkgPT4gKHtcbiAgICBtb25nb2RiOiBjb25uZWN0aW9uLnJlYWR5U3RhdGUgPT09IDEgPyAnY29ubmVjdGVkJ1xuICAgICAgOiBjb25uZWN0aW9uLnJlYWR5U3RhdGUgPT09IDAgPyAndW5rbm93bicgLy8gc3RhcnRpbmcgdXBcbiAgICAgICAgOiAnZGlzY29ubmVjdGVkJyxcbiAgfSk7XG59XG4iXX0=
@@ -0,0 +1,9 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ /**
3
+ * Middleware that supports idempotency keys for POST/PUT/DELETE mutations.
4
+ *
5
+ * When a request includes the `Idempotency-Key` header:
6
+ * - First call: processes normally, caches the response
7
+ * - Subsequent calls with same key: returns cached response (prevents duplicate mutations)
8
+ */
9
+ export declare function idempotencyMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ // Copyright 2026 Pipeline Builder Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.idempotencyMiddleware = idempotencyMiddleware;
6
+ const api_core_1 = require("@pipeline-builder/api-core");
7
+ const pipeline_core_1 = require("@pipeline-builder/pipeline-core");
8
+ const logger = (0, api_core_1.createLogger)('Idempotency');
9
+ /** In-memory store for idempotency results (TTL-based). */
10
+ const store = new Map();
11
+ // Periodic cleanup of expired entries
12
+ const cleanupTimer = setInterval(() => {
13
+ const now = Date.now();
14
+ for (const [key, entry] of store) {
15
+ if (now > entry.expiresAt)
16
+ store.delete(key);
17
+ }
18
+ }, pipeline_core_1.CoreConstants.IDEMPOTENCY_CLEANUP_INTERVAL_MS);
19
+ cleanupTimer.unref();
20
+ /**
21
+ * Middleware that supports idempotency keys for POST/PUT/DELETE mutations.
22
+ *
23
+ * When a request includes the `Idempotency-Key` header:
24
+ * - First call: processes normally, caches the response
25
+ * - Subsequent calls with same key: returns cached response (prevents duplicate mutations)
26
+ */
27
+ function idempotencyMiddleware() {
28
+ return (req, res, next) => {
29
+ const key = req.headers['idempotency-key'];
30
+ if (!key)
31
+ return next();
32
+ // Only apply to mutation methods
33
+ if (!['POST', 'PUT', 'DELETE'].includes(req.method))
34
+ return next();
35
+ // Namespace by orgId to prevent cross-org cache collisions
36
+ const orgId = req.context?.identity?.orgId || req.user?.organizationId;
37
+ if (!orgId)
38
+ return next(); // skip idempotency for unauthenticated requests
39
+ const fullKey = `${orgId}:${key}`;
40
+ // Check for cached response
41
+ const cached = store.get(fullKey);
42
+ if (cached && Date.now() < cached.expiresAt) {
43
+ logger.debug('Idempotent request replayed', { key: fullKey });
44
+ res.setHeader('X-Idempotent-Replayed', 'true');
45
+ res.status(cached.statusCode).json(cached.body);
46
+ return;
47
+ }
48
+ // Intercept res.json to cache the response
49
+ const originalJson = res.json.bind(res);
50
+ res.json = (body) => {
51
+ res.setHeader('X-Idempotent-Replayed', 'false');
52
+ if (res.statusCode >= 200 && res.statusCode < 300 && store.size < pipeline_core_1.CoreConstants.IDEMPOTENCY_MAX_STORE_SIZE) {
53
+ store.set(fullKey, {
54
+ statusCode: res.statusCode,
55
+ body,
56
+ expiresAt: Date.now() + pipeline_core_1.CoreConstants.IDEMPOTENCY_TTL_MS,
57
+ });
58
+ }
59
+ return originalJson(body);
60
+ };
61
+ next();
62
+ };
63
+ }
64
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaWRlbXBvdGVuY3ktbWlkZGxld2FyZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hcGkvaWRlbXBvdGVuY3ktbWlkZGxld2FyZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUEsK0NBQStDO0FBQy9DLHNDQUFzQzs7QUEyQnRDLHNEQXNDQztBQS9ERCx5REFBMEQ7QUFDMUQsbUVBQWdFO0FBR2hFLE1BQU0sTUFBTSxHQUFHLElBQUEsdUJBQVksRUFBQyxhQUFhLENBQUMsQ0FBQztBQUUzQywyREFBMkQ7QUFDM0QsTUFBTSxLQUFLLEdBQUcsSUFBSSxHQUFHLEVBQW9FLENBQUM7QUFFMUYsc0NBQXNDO0FBQ3RDLE1BQU0sWUFBWSxHQUFHLFdBQVcsQ0FBQyxHQUFHLEVBQUU7SUFDcEMsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO0lBQ3ZCLEtBQUssTUFBTSxDQUFDLEdBQUcsRUFBRSxLQUFLLENBQUMsSUFBSSxLQUFLLEVBQUUsQ0FBQztRQUNqQyxJQUFJLEdBQUcsR0FBRyxLQUFLLENBQUMsU0FBUztZQUFFLEtBQUssQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDL0MsQ0FBQztBQUNILENBQUMsRUFBRSw2QkFBYSxDQUFDLCtCQUErQixDQUFDLENBQUM7QUFDbEQsWUFBWSxDQUFDLEtBQUssRUFBRSxDQUFDO0FBRXJCOzs7Ozs7R0FNRztBQUNILFNBQWdCLHFCQUFxQjtJQUNuQyxPQUFPLENBQUMsR0FBWSxFQUFFLEdBQWEsRUFBRSxJQUFrQixFQUFFLEVBQUU7UUFDekQsTUFBTSxHQUFHLEdBQUcsR0FBRyxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBdUIsQ0FBQztRQUNqRSxJQUFJLENBQUMsR0FBRztZQUFFLE9BQU8sSUFBSSxFQUFFLENBQUM7UUFFeEIsaUNBQWlDO1FBQ2pDLElBQUksQ0FBQyxDQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsUUFBUSxDQUFDLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUM7WUFBRSxPQUFPLElBQUksRUFBRSxDQUFDO1FBRW5FLDJEQUEyRDtRQUMzRCxNQUFNLEtBQUssR0FBRyxHQUFHLENBQUMsT0FBTyxFQUFFLFFBQVEsRUFBRSxLQUFLLElBQUksR0FBRyxDQUFDLElBQUksRUFBRSxjQUFjLENBQUM7UUFDdkUsSUFBSSxDQUFDLEtBQUs7WUFBRSxPQUFPLElBQUksRUFBRSxDQUFDLENBQUMsZ0RBQWdEO1FBQzNFLE1BQU0sT0FBTyxHQUFHLEdBQUcsS0FBSyxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBRWxDLDRCQUE0QjtRQUM1QixNQUFNLE1BQU0sR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ2xDLElBQUksTUFBTSxJQUFJLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxNQUFNLENBQUMsU0FBUyxFQUFFLENBQUM7WUFDNUMsTUFBTSxDQUFDLEtBQUssQ0FBQyw2QkFBNkIsRUFBRSxFQUFFLEdBQUcsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDO1lBQzlELEdBQUcsQ0FBQyxTQUFTLENBQUMsdUJBQXVCLEVBQUUsTUFBTSxDQUFDLENBQUM7WUFDL0MsR0FBRyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUNoRCxPQUFPO1FBQ1QsQ0FBQztRQUVELDJDQUEyQztRQUMzQyxNQUFNLFlBQVksR0FBRyxHQUFHLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUN4QyxHQUFHLENBQUMsSUFBSSxHQUFHLENBQUMsSUFBYSxFQUFFLEVBQUU7WUFDM0IsR0FBRyxDQUFDLFNBQVMsQ0FBQyx1QkFBdUIsRUFBRSxPQUFPLENBQUMsQ0FBQztZQUNoRCxJQUFJLEdBQUcsQ0FBQyxVQUFVLElBQUksR0FBRyxJQUFJLEdBQUcsQ0FBQyxVQUFVLEdBQUcsR0FBRyxJQUFJLEtBQUssQ0FBQyxJQUFJLEdBQUcsNkJBQWEsQ0FBQywwQkFBMEIsRUFBRSxDQUFDO2dCQUMzRyxLQUFLLENBQUMsR0FBRyxDQUFDLE9BQU8sRUFBRTtvQkFDakIsVUFBVSxFQUFFLEdBQUcsQ0FBQyxVQUFVO29CQUMxQixJQUFJO29CQUNKLFNBQVMsRUFBRSxJQUFJLENBQUMsR0FBRyxFQUFFLEdBQUcsNkJBQWEsQ0FBQyxrQkFBa0I7aUJBQ3pELENBQUMsQ0FBQztZQUNMLENBQUM7WUFDRCxPQUFPLFlBQVksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUM1QixDQUFDLENBQUM7UUFFRixJQUFJLEVBQUUsQ0FBQztJQUNULENBQUMsQ0FBQztBQUNKLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuaW1wb3J0IHsgY3JlYXRlTG9nZ2VyIH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvYXBpLWNvcmUnO1xuaW1wb3J0IHsgQ29yZUNvbnN0YW50cyB9IGZyb20gJ0BwaXBlbGluZS1idWlsZGVyL3BpcGVsaW5lLWNvcmUnO1xuaW1wb3J0IHsgUmVxdWVzdCwgUmVzcG9uc2UsIE5leHRGdW5jdGlvbiB9IGZyb20gJ2V4cHJlc3MnO1xuXG5jb25zdCBsb2dnZXIgPSBjcmVhdGVMb2dnZXIoJ0lkZW1wb3RlbmN5Jyk7XG5cbi8qKiBJbi1tZW1vcnkgc3RvcmUgZm9yIGlkZW1wb3RlbmN5IHJlc3VsdHMgKFRUTC1iYXNlZCkuICovXG5jb25zdCBzdG9yZSA9IG5ldyBNYXA8c3RyaW5nLCB7IHN0YXR1c0NvZGU6IG51bWJlcjsgYm9keTogdW5rbm93bjsgZXhwaXJlc0F0OiBudW1iZXIgfT4oKTtcblxuLy8gUGVyaW9kaWMgY2xlYW51cCBvZiBleHBpcmVkIGVudHJpZXNcbmNvbnN0IGNsZWFudXBUaW1lciA9IHNldEludGVydmFsKCgpID0+IHtcbiAgY29uc3Qgbm93ID0gRGF0ZS5ub3coKTtcbiAgZm9yIChjb25zdCBba2V5LCBlbnRyeV0gb2Ygc3RvcmUpIHtcbiAgICBpZiAobm93ID4gZW50cnkuZXhwaXJlc0F0KSBzdG9yZS5kZWxldGUoa2V5KTtcbiAgfVxufSwgQ29yZUNvbnN0YW50cy5JREVNUE9URU5DWV9DTEVBTlVQX0lOVEVSVkFMX01TKTtcbmNsZWFudXBUaW1lci51bnJlZigpO1xuXG4vKipcbiAqIE1pZGRsZXdhcmUgdGhhdCBzdXBwb3J0cyBpZGVtcG90ZW5jeSBrZXlzIGZvciBQT1NUL1BVVC9ERUxFVEUgbXV0YXRpb25zLlxuICpcbiAqIFdoZW4gYSByZXF1ZXN0IGluY2x1ZGVzIHRoZSBgSWRlbXBvdGVuY3ktS2V5YCBoZWFkZXI6XG4gKiAtIEZpcnN0IGNhbGw6IHByb2Nlc3NlcyBub3JtYWxseSwgY2FjaGVzIHRoZSByZXNwb25zZVxuICogLSBTdWJzZXF1ZW50IGNhbGxzIHdpdGggc2FtZSBrZXk6IHJldHVybnMgY2FjaGVkIHJlc3BvbnNlIChwcmV2ZW50cyBkdXBsaWNhdGUgbXV0YXRpb25zKVxuICovXG5leHBvcnQgZnVuY3Rpb24gaWRlbXBvdGVuY3lNaWRkbGV3YXJlKCkge1xuICByZXR1cm4gKHJlcTogUmVxdWVzdCwgcmVzOiBSZXNwb25zZSwgbmV4dDogTmV4dEZ1bmN0aW9uKSA9PiB7XG4gICAgY29uc3Qga2V5ID0gcmVxLmhlYWRlcnNbJ2lkZW1wb3RlbmN5LWtleSddIGFzIHN0cmluZyB8IHVuZGVmaW5lZDtcbiAgICBpZiAoIWtleSkgcmV0dXJuIG5leHQoKTtcblxuICAgIC8vIE9ubHkgYXBwbHkgdG8gbXV0YXRpb24gbWV0aG9kc1xuICAgIGlmICghWydQT1NUJywgJ1BVVCcsICdERUxFVEUnXS5pbmNsdWRlcyhyZXEubWV0aG9kKSkgcmV0dXJuIG5leHQoKTtcblxuICAgIC8vIE5hbWVzcGFjZSBieSBvcmdJZCB0byBwcmV2ZW50IGNyb3NzLW9yZyBjYWNoZSBjb2xsaXNpb25zXG4gICAgY29uc3Qgb3JnSWQgPSByZXEuY29udGV4dD8uaWRlbnRpdHk/Lm9yZ0lkIHx8IHJlcS51c2VyPy5vcmdhbml6YXRpb25JZDtcbiAgICBpZiAoIW9yZ0lkKSByZXR1cm4gbmV4dCgpOyAvLyBza2lwIGlkZW1wb3RlbmN5IGZvciB1bmF1dGhlbnRpY2F0ZWQgcmVxdWVzdHNcbiAgICBjb25zdCBmdWxsS2V5ID0gYCR7b3JnSWR9OiR7a2V5fWA7XG5cbiAgICAvLyBDaGVjayBmb3IgY2FjaGVkIHJlc3BvbnNlXG4gICAgY29uc3QgY2FjaGVkID0gc3RvcmUuZ2V0KGZ1bGxLZXkpO1xuICAgIGlmIChjYWNoZWQgJiYgRGF0ZS5ub3coKSA8IGNhY2hlZC5leHBpcmVzQXQpIHtcbiAgICAgIGxvZ2dlci5kZWJ1ZygnSWRlbXBvdGVudCByZXF1ZXN0IHJlcGxheWVkJywgeyBrZXk6IGZ1bGxLZXkgfSk7XG4gICAgICByZXMuc2V0SGVhZGVyKCdYLUlkZW1wb3RlbnQtUmVwbGF5ZWQnLCAndHJ1ZScpO1xuICAgICAgcmVzLnN0YXR1cyhjYWNoZWQuc3RhdHVzQ29kZSkuanNvbihjYWNoZWQuYm9keSk7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgLy8gSW50ZXJjZXB0IHJlcy5qc29uIHRvIGNhY2hlIHRoZSByZXNwb25zZVxuICAgIGNvbnN0IG9yaWdpbmFsSnNvbiA9IHJlcy5qc29uLmJpbmQocmVzKTtcbiAgICByZXMuanNvbiA9IChib2R5OiB1bmtub3duKSA9PiB7XG4gICAgICByZXMuc2V0SGVhZGVyKCdYLUlkZW1wb3RlbnQtUmVwbGF5ZWQnLCAnZmFsc2UnKTtcbiAgICAgIGlmIChyZXMuc3RhdHVzQ29kZSA+PSAyMDAgJiYgcmVzLnN0YXR1c0NvZGUgPCAzMDAgJiYgc3RvcmUuc2l6ZSA8IENvcmVDb25zdGFudHMuSURFTVBPVEVOQ1lfTUFYX1NUT1JFX1NJWkUpIHtcbiAgICAgICAgc3RvcmUuc2V0KGZ1bGxLZXksIHtcbiAgICAgICAgICBzdGF0dXNDb2RlOiByZXMuc3RhdHVzQ29kZSxcbiAgICAgICAgICBib2R5LFxuICAgICAgICAgIGV4cGlyZXNBdDogRGF0ZS5ub3coKSArIENvcmVDb25zdGFudHMuSURFTVBPVEVOQ1lfVFRMX01TLFxuICAgICAgICB9KTtcbiAgICAgIH1cbiAgICAgIHJldHVybiBvcmlnaW5hbEpzb24oYm9keSk7XG4gICAgfTtcblxuICAgIG5leHQoKTtcbiAgfTtcbn1cbiJdfQ==
@@ -0,0 +1,15 @@
1
+ export * from './middleware';
2
+ export * from './middleware-factory';
3
+ export * from './context-middleware';
4
+ export * from './check-quota';
5
+ export * from './require-org-id';
6
+ export * from './get-context';
7
+ export * from './app-factory';
8
+ export * from './health-checks';
9
+ export * from './quota-helpers';
10
+ export * from './idempotency-middleware';
11
+ export * from './tracing';
12
+ export * from './metrics';
13
+ export * from './server';
14
+ export * from './route-wrapper';
15
+ export * from './request-types';
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ // Copyright 2026 Pipeline Builder Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ // Middleware
20
+ __exportStar(require("./middleware"), exports);
21
+ __exportStar(require("./middleware-factory"), exports);
22
+ __exportStar(require("./context-middleware"), exports);
23
+ __exportStar(require("./check-quota"), exports);
24
+ __exportStar(require("./require-org-id"), exports);
25
+ __exportStar(require("./get-context"), exports);
26
+ // App factory
27
+ __exportStar(require("./app-factory"), exports);
28
+ // Health-check helpers
29
+ __exportStar(require("./health-checks"), exports);
30
+ // Quota helpers
31
+ __exportStar(require("./quota-helpers"), exports);
32
+ // Idempotency
33
+ __exportStar(require("./idempotency-middleware"), exports);
34
+ // Observability
35
+ __exportStar(require("./tracing"), exports);
36
+ __exportStar(require("./metrics"), exports);
37
+ // Server utilities
38
+ __exportStar(require("./server"), exports);
39
+ // Route wrapper
40
+ __exportStar(require("./route-wrapper"), exports);
41
+ // Request/Response types
42
+ __exportStar(require("./request-types"), exports);
43
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXBpL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDOzs7Ozs7Ozs7Ozs7Ozs7O0FBRXRDLGFBQWE7QUFDYiwrQ0FBNkI7QUFDN0IsdURBQXFDO0FBQ3JDLHVEQUFxQztBQUNyQyxnREFBOEI7QUFDOUIsbURBQWlDO0FBQ2pDLGdEQUE4QjtBQUU5QixjQUFjO0FBQ2QsZ0RBQThCO0FBRTlCLHVCQUF1QjtBQUN2QixrREFBZ0M7QUFFaEMsZ0JBQWdCO0FBQ2hCLGtEQUFnQztBQUVoQyxjQUFjO0FBQ2QsMkRBQXlDO0FBRXpDLGdCQUFnQjtBQUNoQiw0Q0FBMEI7QUFDMUIsNENBQTBCO0FBRTFCLG1CQUFtQjtBQUNuQiwyQ0FBeUI7QUFFekIsZ0JBQWdCO0FBQ2hCLGtEQUFnQztBQUVoQyx5QkFBeUI7QUFDekIsa0RBQWdDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbi8vIE1pZGRsZXdhcmVcbmV4cG9ydCAqIGZyb20gJy4vbWlkZGxld2FyZSc7XG5leHBvcnQgKiBmcm9tICcuL21pZGRsZXdhcmUtZmFjdG9yeSc7XG5leHBvcnQgKiBmcm9tICcuL2NvbnRleHQtbWlkZGxld2FyZSc7XG5leHBvcnQgKiBmcm9tICcuL2NoZWNrLXF1b3RhJztcbmV4cG9ydCAqIGZyb20gJy4vcmVxdWlyZS1vcmctaWQnO1xuZXhwb3J0ICogZnJvbSAnLi9nZXQtY29udGV4dCc7XG5cbi8vIEFwcCBmYWN0b3J5XG5leHBvcnQgKiBmcm9tICcuL2FwcC1mYWN0b3J5JztcblxuLy8gSGVhbHRoLWNoZWNrIGhlbHBlcnNcbmV4cG9ydCAqIGZyb20gJy4vaGVhbHRoLWNoZWNrcyc7XG5cbi8vIFF1b3RhIGhlbHBlcnNcbmV4cG9ydCAqIGZyb20gJy4vcXVvdGEtaGVscGVycyc7XG5cbi8vIElkZW1wb3RlbmN5XG5leHBvcnQgKiBmcm9tICcuL2lkZW1wb3RlbmN5LW1pZGRsZXdhcmUnO1xuXG4vLyBPYnNlcnZhYmlsaXR5XG5leHBvcnQgKiBmcm9tICcuL3RyYWNpbmcnO1xuZXhwb3J0ICogZnJvbSAnLi9tZXRyaWNzJztcblxuLy8gU2VydmVyIHV0aWxpdGllc1xuZXhwb3J0ICogZnJvbSAnLi9zZXJ2ZXInO1xuXG4vLyBSb3V0ZSB3cmFwcGVyXG5leHBvcnQgKiBmcm9tICcuL3JvdXRlLXdyYXBwZXInO1xuXG4vLyBSZXF1ZXN0L1Jlc3BvbnNlIHR5cGVzXG5leHBvcnQgKiBmcm9tICcuL3JlcXVlc3QtdHlwZXMnO1xuIl19
@@ -0,0 +1,12 @@
1
+ import { 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>;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ // Copyright 2026 Pipeline Builder Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.metricsMiddleware = metricsMiddleware;
6
+ exports.metricsHandler = metricsHandler;
7
+ const pipeline_core_1 = require("@pipeline-builder/pipeline-core");
8
+ const prom_client_1 = require("prom-client");
9
+ const SERVICE_NAME = pipeline_core_1.Config.getAny('observability').serviceName;
10
+ /** Shared Prometheus registry */
11
+ const register = new prom_client_1.Registry();
12
+ // Set default labels for all metrics
13
+ register.setDefaultLabels({ service: SERVICE_NAME });
14
+ // Collect default Node.js process metrics (CPU, memory, heap, event loop lag, GC)
15
+ (0, prom_client_1.collectDefaultMetrics)({ register });
16
+ /** HTTP request duration histogram (seconds) */
17
+ const httpRequestDuration = new prom_client_1.Histogram({
18
+ name: 'http_request_duration_seconds',
19
+ help: 'Duration of HTTP requests in seconds',
20
+ labelNames: ['method', 'route', 'status_code'],
21
+ buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
22
+ registers: [register],
23
+ });
24
+ /** HTTP request counter */
25
+ const httpRequestsTotal = new prom_client_1.Counter({
26
+ name: 'http_requests_total',
27
+ help: 'Total number of HTTP requests',
28
+ labelNames: ['method', 'route', 'status_code'],
29
+ registers: [register],
30
+ });
31
+ /**
32
+ * Normalize an Express route path to prevent label cardinality explosion.
33
+ *
34
+ * Uses Express's matched route pattern (e.g. `/plugins/:id`) when available,
35
+ * otherwise falls back to the raw path with UUID/numeric segments replaced.
36
+ */
37
+ function normalizeRoute(req) {
38
+ // Prefer Express matched route pattern
39
+ if (req.route?.path) {
40
+ return req.baseUrl + req.route.path;
41
+ }
42
+ // Fallback: replace UUIDs and numeric IDs with :id
43
+ return req.path
44
+ .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id')
45
+ .replace(/\/\d+(?=\/|$)/g, '/:id');
46
+ }
47
+ /**
48
+ * Express middleware that records request duration and count.
49
+ *
50
+ * Must be registered before route handlers so `res.on('finish')` fires
51
+ * after the response is sent.
52
+ */
53
+ function metricsMiddleware() {
54
+ return (req, res, next) => {
55
+ // Skip recording the /metrics and /health endpoints themselves
56
+ if (req.path === '/metrics' || req.path === '/health') {
57
+ next();
58
+ return;
59
+ }
60
+ const end = httpRequestDuration.startTimer();
61
+ res.on('finish', () => {
62
+ const route = normalizeRoute(req);
63
+ const labels = {
64
+ method: req.method,
65
+ route,
66
+ status_code: String(res.statusCode),
67
+ };
68
+ end(labels);
69
+ httpRequestsTotal.inc(labels);
70
+ });
71
+ next();
72
+ };
73
+ }
74
+ /**
75
+ * Express handler that returns Prometheus metrics in text exposition format.
76
+ */
77
+ function metricsHandler() {
78
+ return async (_req, res) => {
79
+ res.set('Content-Type', register.contentType);
80
+ res.end(await register.metrics());
81
+ };
82
+ }
83
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0cmljcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hcGkvbWV0cmljcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUEsK0NBQStDO0FBQy9DLHNDQUFzQzs7QUEwRHRDLDhDQXdCQztBQUtELHdDQUtDO0FBMUZELG1FQUF5RDtBQUV6RCw2Q0FBa0Y7QUFFbEYsTUFBTSxZQUFZLEdBQUksc0JBQU0sQ0FBQyxNQUFNLENBQUMsZUFBZSxDQUE2QixDQUFDLFdBQVcsQ0FBQztBQUU3RixpQ0FBaUM7QUFDakMsTUFBTSxRQUFRLEdBQUcsSUFBSSxzQkFBUSxFQUFFLENBQUM7QUFFaEMscUNBQXFDO0FBQ3JDLFFBQVEsQ0FBQyxnQkFBZ0IsQ0FBQyxFQUFFLE9BQU8sRUFBRSxZQUFZLEVBQUUsQ0FBQyxDQUFDO0FBRXJELGtGQUFrRjtBQUNsRixJQUFBLG1DQUFxQixFQUFDLEVBQUUsUUFBUSxFQUFFLENBQUMsQ0FBQztBQUVwQyxnREFBZ0Q7QUFDaEQsTUFBTSxtQkFBbUIsR0FBRyxJQUFJLHVCQUFTLENBQUM7SUFDeEMsSUFBSSxFQUFFLCtCQUErQjtJQUNyQyxJQUFJLEVBQUUsc0NBQXNDO0lBQzVDLFVBQVUsRUFBRSxDQUFDLFFBQVEsRUFBRSxPQUFPLEVBQUUsYUFBYSxDQUFVO0lBQ3ZELE9BQU8sRUFBRSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsSUFBSSxFQUFFLEdBQUcsRUFBRSxDQUFDLEVBQUUsR0FBRyxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUM7SUFDbEUsU0FBUyxFQUFFLENBQUMsUUFBUSxDQUFDO0NBQ3RCLENBQUMsQ0FBQztBQUVILDJCQUEyQjtBQUMzQixNQUFNLGlCQUFpQixHQUFHLElBQUkscUJBQU8sQ0FBQztJQUNwQyxJQUFJLEVBQUUscUJBQXFCO0lBQzNCLElBQUksRUFBRSwrQkFBK0I7SUFDckMsVUFBVSxFQUFFLENBQUMsUUFBUSxFQUFFLE9BQU8sRUFBRSxhQUFhLENBQVU7SUFDdkQsU0FBUyxFQUFFLENBQUMsUUFBUSxDQUFDO0NBQ3RCLENBQUMsQ0FBQztBQUVIOzs7OztHQUtHO0FBQ0gsU0FBUyxjQUFjLENBQUMsR0FBWTtJQUNsQyx1Q0FBdUM7SUFDdkMsSUFBSSxHQUFHLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDO1FBQ3BCLE9BQU8sR0FBRyxDQUFDLE9BQU8sR0FBRyxHQUFHLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQztJQUN0QyxDQUFDO0lBRUQsbURBQW1EO0lBQ25ELE9BQU8sR0FBRyxDQUFDLElBQUk7U0FDWixPQUFPLENBQUMsZ0VBQWdFLEVBQUUsS0FBSyxDQUFDO1NBQ2hGLE9BQU8sQ0FBQyxnQkFBZ0IsRUFBRSxNQUFNLENBQUMsQ0FBQztBQUN2QyxDQUFDO0FBRUQ7Ozs7O0dBS0c7QUFDSCxTQUFnQixpQkFBaUI7SUFDL0IsT0FBTyxDQUFDLEdBQVksRUFBRSxHQUFhLEVBQUUsSUFBa0IsRUFBUSxFQUFFO1FBQy9ELCtEQUErRDtRQUMvRCxJQUFJLEdBQUcsQ0FBQyxJQUFJLEtBQUssVUFBVSxJQUFJLEdBQUcsQ0FBQyxJQUFJLEtBQUssU0FBUyxFQUFFLENBQUM7WUFDdEQsSUFBSSxFQUFFLENBQUM7WUFDUCxPQUFPO1FBQ1QsQ0FBQztRQUVELE1BQU0sR0FBRyxHQUFHLG1CQUFtQixDQUFDLFVBQVUsRUFBRSxDQUFDO1FBRTdDLEdBQUcsQ0FBQyxFQUFFLENBQUMsUUFBUSxFQUFFLEdBQUcsRUFBRTtZQUNwQixNQUFNLEtBQUssR0FBRyxjQUFjLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDbEMsTUFBTSxNQUFNLEdBQUc7Z0JBQ2IsTUFBTSxFQUFFLEdBQUcsQ0FBQyxNQUFNO2dCQUNsQixLQUFLO2dCQUNMLFdBQVcsRUFBRSxNQUFNLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQzthQUNwQyxDQUFDO1lBRUYsR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBQ1osaUJBQWlCLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQ2hDLENBQUMsQ0FBQyxDQUFDO1FBRUgsSUFBSSxFQUFFLENBQUM7SUFDVCxDQUFDLENBQUM7QUFDSixDQUFDO0FBRUQ7O0dBRUc7QUFDSCxTQUFnQixjQUFjO0lBQzVCLE9BQU8sS0FBSyxFQUFFLElBQWEsRUFBRSxHQUFhLEVBQWlCLEVBQUU7UUFDM0QsR0FBRyxDQUFDLEdBQUcsQ0FBQyxjQUFjLEVBQUUsUUFBUSxDQUFDLFdBQVcsQ0FBQyxDQUFDO1FBQzlDLEdBQUcsQ0FBQyxHQUFHLENBQUMsTUFBTSxRQUFRLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQztJQUNwQyxDQUFDLENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IENvbmZpZyB9IGZyb20gJ0BwaXBlbGluZS1idWlsZGVyL3BpcGVsaW5lLWNvcmUnO1xuaW1wb3J0IHsgUmVxdWVzdCwgUmVzcG9uc2UsIE5leHRGdW5jdGlvbiB9IGZyb20gJ2V4cHJlc3MnO1xuaW1wb3J0IHsgUmVnaXN0cnksIGNvbGxlY3REZWZhdWx0TWV0cmljcywgQ291bnRlciwgSGlzdG9ncmFtIH0gZnJvbSAncHJvbS1jbGllbnQnO1xuXG5jb25zdCBTRVJWSUNFX05BTUUgPSAoQ29uZmlnLmdldEFueSgnb2JzZXJ2YWJpbGl0eScpIGFzIHsgc2VydmljZU5hbWU6IHN0cmluZyB9KS5zZXJ2aWNlTmFtZTtcblxuLyoqIFNoYXJlZCBQcm9tZXRoZXVzIHJlZ2lzdHJ5ICovXG5jb25zdCByZWdpc3RlciA9IG5ldyBSZWdpc3RyeSgpO1xuXG4vLyBTZXQgZGVmYXVsdCBsYWJlbHMgZm9yIGFsbCBtZXRyaWNzXG5yZWdpc3Rlci5zZXREZWZhdWx0TGFiZWxzKHsgc2VydmljZTogU0VSVklDRV9OQU1FIH0pO1xuXG4vLyBDb2xsZWN0IGRlZmF1bHQgTm9kZS5qcyBwcm9jZXNzIG1ldHJpY3MgKENQVSwgbWVtb3J5LCBoZWFwLCBldmVudCBsb29wIGxhZywgR0MpXG5jb2xsZWN0RGVmYXVsdE1ldHJpY3MoeyByZWdpc3RlciB9KTtcblxuLyoqIEhUVFAgcmVxdWVzdCBkdXJhdGlvbiBoaXN0b2dyYW0gKHNlY29uZHMpICovXG5jb25zdCBodHRwUmVxdWVzdER1cmF0aW9uID0gbmV3IEhpc3RvZ3JhbSh7XG4gIG5hbWU6ICdodHRwX3JlcXVlc3RfZHVyYXRpb25fc2Vjb25kcycsXG4gIGhlbHA6ICdEdXJhdGlvbiBvZiBIVFRQIHJlcXVlc3RzIGluIHNlY29uZHMnLFxuICBsYWJlbE5hbWVzOiBbJ21ldGhvZCcsICdyb3V0ZScsICdzdGF0dXNfY29kZSddIGFzIGNvbnN0LFxuICBidWNrZXRzOiBbMC4wMDUsIDAuMDEsIDAuMDI1LCAwLjA1LCAwLjEsIDAuMjUsIDAuNSwgMSwgMi41LCA1LCAxMF0sXG4gIHJlZ2lzdGVyczogW3JlZ2lzdGVyXSxcbn0pO1xuXG4vKiogSFRUUCByZXF1ZXN0IGNvdW50ZXIgKi9cbmNvbnN0IGh0dHBSZXF1ZXN0c1RvdGFsID0gbmV3IENvdW50ZXIoe1xuICBuYW1lOiAnaHR0cF9yZXF1ZXN0c190b3RhbCcsXG4gIGhlbHA6ICdUb3RhbCBudW1iZXIgb2YgSFRUUCByZXF1ZXN0cycsXG4gIGxhYmVsTmFtZXM6IFsnbWV0aG9kJywgJ3JvdXRlJywgJ3N0YXR1c19jb2RlJ10gYXMgY29uc3QsXG4gIHJlZ2lzdGVyczogW3JlZ2lzdGVyXSxcbn0pO1xuXG4vKipcbiAqIE5vcm1hbGl6ZSBhbiBFeHByZXNzIHJvdXRlIHBhdGggdG8gcHJldmVudCBsYWJlbCBjYXJkaW5hbGl0eSBleHBsb3Npb24uXG4gKlxuICogVXNlcyBFeHByZXNzJ3MgbWF0Y2hlZCByb3V0ZSBwYXR0ZXJuIChlLmcuIGAvcGx1Z2lucy86aWRgKSB3aGVuIGF2YWlsYWJsZSxcbiAqIG90aGVyd2lzZSBmYWxscyBiYWNrIHRvIHRoZSByYXcgcGF0aCB3aXRoIFVVSUQvbnVtZXJpYyBzZWdtZW50cyByZXBsYWNlZC5cbiAqL1xuZnVuY3Rpb24gbm9ybWFsaXplUm91dGUocmVxOiBSZXF1ZXN0KTogc3RyaW5nIHtcbiAgLy8gUHJlZmVyIEV4cHJlc3MgbWF0Y2hlZCByb3V0ZSBwYXR0ZXJuXG4gIGlmIChyZXEucm91dGU/LnBhdGgpIHtcbiAgICByZXR1cm4gcmVxLmJhc2VVcmwgKyByZXEucm91dGUucGF0aDtcbiAgfVxuXG4gIC8vIEZhbGxiYWNrOiByZXBsYWNlIFVVSURzIGFuZCBudW1lcmljIElEcyB3aXRoIDppZFxuICByZXR1cm4gcmVxLnBhdGhcbiAgICAucmVwbGFjZSgvWzAtOWEtZl17OH0tWzAtOWEtZl17NH0tWzAtOWEtZl17NH0tWzAtOWEtZl17NH0tWzAtOWEtZl17MTJ9L2dpLCAnOmlkJylcbiAgICAucmVwbGFjZSgvXFwvXFxkKyg/PVxcL3wkKS9nLCAnLzppZCcpO1xufVxuXG4vKipcbiAqIEV4cHJlc3MgbWlkZGxld2FyZSB0aGF0IHJlY29yZHMgcmVxdWVzdCBkdXJhdGlvbiBhbmQgY291bnQuXG4gKlxuICogTXVzdCBiZSByZWdpc3RlcmVkIGJlZm9yZSByb3V0ZSBoYW5kbGVycyBzbyBgcmVzLm9uKCdmaW5pc2gnKWAgZmlyZXNcbiAqIGFmdGVyIHRoZSByZXNwb25zZSBpcyBzZW50LlxuICovXG5leHBvcnQgZnVuY3Rpb24gbWV0cmljc01pZGRsZXdhcmUoKSB7XG4gIHJldHVybiAocmVxOiBSZXF1ZXN0LCByZXM6IFJlc3BvbnNlLCBuZXh0OiBOZXh0RnVuY3Rpb24pOiB2b2lkID0+IHtcbiAgICAvLyBTa2lwIHJlY29yZGluZyB0aGUgL21ldHJpY3MgYW5kIC9oZWFsdGggZW5kcG9pbnRzIHRoZW1zZWx2ZXNcbiAgICBpZiAocmVxLnBhdGggPT09ICcvbWV0cmljcycgfHwgcmVxLnBhdGggPT09ICcvaGVhbHRoJykge1xuICAgICAgbmV4dCgpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGNvbnN0IGVuZCA9IGh0dHBSZXF1ZXN0RHVyYXRpb24uc3RhcnRUaW1lcigpO1xuXG4gICAgcmVzLm9uKCdmaW5pc2gnLCAoKSA9PiB7XG4gICAgICBjb25zdCByb3V0ZSA9IG5vcm1hbGl6ZVJvdXRlKHJlcSk7XG4gICAgICBjb25zdCBsYWJlbHMgPSB7XG4gICAgICAgIG1ldGhvZDogcmVxLm1ldGhvZCxcbiAgICAgICAgcm91dGUsXG4gICAgICAgIHN0YXR1c19jb2RlOiBTdHJpbmcocmVzLnN0YXR1c0NvZGUpLFxuICAgICAgfTtcblxuICAgICAgZW5kKGxhYmVscyk7XG4gICAgICBodHRwUmVxdWVzdHNUb3RhbC5pbmMobGFiZWxzKTtcbiAgICB9KTtcblxuICAgIG5leHQoKTtcbiAgfTtcbn1cblxuLyoqXG4gKiBFeHByZXNzIGhhbmRsZXIgdGhhdCByZXR1cm5zIFByb21ldGhldXMgbWV0cmljcyBpbiB0ZXh0IGV4cG9zaXRpb24gZm9ybWF0LlxuICovXG5leHBvcnQgZnVuY3Rpb24gbWV0cmljc0hhbmRsZXIoKSB7XG4gIHJldHVybiBhc3luYyAoX3JlcTogUmVxdWVzdCwgcmVzOiBSZXNwb25zZSk6IFByb21pc2U8dm9pZD4gPT4ge1xuICAgIHJlcy5zZXQoJ0NvbnRlbnQtVHlwZScsIHJlZ2lzdGVyLmNvbnRlbnRUeXBlKTtcbiAgICByZXMuZW5kKGF3YWl0IHJlZ2lzdGVyLm1ldHJpY3MoKSk7XG4gIH07XG59XG4iXX0=