@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,221 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { Config } from '@pipeline-builder/pipeline-core';
|
|
4
|
+
import { Registry, collectDefaultMetrics, Counter, Histogram, Gauge } from 'prom-client';
|
|
5
|
+
const SERVICE_NAME = Config.getAny('observability').serviceName;
|
|
6
|
+
/** Shared Prometheus registry */
|
|
7
|
+
const register = new Registry();
|
|
8
|
+
// Set default labels for all metrics
|
|
9
|
+
register.setDefaultLabels({ service: SERVICE_NAME });
|
|
10
|
+
// Collect default Node.js process metrics (CPU, memory, heap, event loop lag, GC)
|
|
11
|
+
collectDefaultMetrics({ register });
|
|
12
|
+
/** HTTP request duration histogram (seconds) */
|
|
13
|
+
const httpRequestDuration = new Histogram({
|
|
14
|
+
name: 'http_request_duration_seconds',
|
|
15
|
+
help: 'Duration of HTTP requests in seconds',
|
|
16
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
17
|
+
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
18
|
+
registers: [register],
|
|
19
|
+
});
|
|
20
|
+
/** HTTP request counter */
|
|
21
|
+
const httpRequestsTotal = new Counter({
|
|
22
|
+
name: 'http_requests_total',
|
|
23
|
+
help: 'Total number of HTTP requests',
|
|
24
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
25
|
+
registers: [register],
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Per-org HTTP request counter — SAMPLED to keep Prometheus cardinality
|
|
29
|
+
* under control. Cardinality of `org_id * method * route * status_code`
|
|
30
|
+
* grows multiplicatively; with N orgs * 50 routes * 5 status_codes that's
|
|
31
|
+
* 250×N series. We sample 1-in-N requests (configurable via
|
|
32
|
+
* `HTTP_METRICS_ORG_SAMPLE_RATE`, default 1.0 = every request).
|
|
33
|
+
*
|
|
34
|
+
* Operators answer "what's org X's p99?" via separate per-org histograms
|
|
35
|
+
* scraped at a lower rate, OR via Loki LogQL queries against the
|
|
36
|
+
* unsampled access-log stream. This counter is the fast-path "noisy
|
|
37
|
+
* neighbor detection" — operators alert when one org's request rate
|
|
38
|
+
* deviates from the fleet median.
|
|
39
|
+
*
|
|
40
|
+
* NOT a histogram: histogram label cardinality is even more sensitive
|
|
41
|
+
* (each bucket is a separate series). If per-org latency percentiles are
|
|
42
|
+
* needed later, add a separate histogram with a coarser bucket set and
|
|
43
|
+
* lower sample rate.
|
|
44
|
+
*/
|
|
45
|
+
const httpRequestsByOrgTotal = new Counter({
|
|
46
|
+
name: 'http_requests_by_org_total',
|
|
47
|
+
help: 'Sampled per-org HTTP request count (sample rate via HTTP_METRICS_ORG_SAMPLE_RATE)',
|
|
48
|
+
labelNames: ['method', 'route', 'status_code', 'org_id'],
|
|
49
|
+
registers: [register],
|
|
50
|
+
});
|
|
51
|
+
/** Sample rate for the per-org counter. 1.0 = every request, 0.0 = disabled.
|
|
52
|
+
* Parsed once at module load — operators tune by restarting the service. */
|
|
53
|
+
const ORG_SAMPLE_RATE = (() => {
|
|
54
|
+
const raw = process.env.HTTP_METRICS_ORG_SAMPLE_RATE;
|
|
55
|
+
if (raw === undefined || raw === '')
|
|
56
|
+
return 1.0;
|
|
57
|
+
const parsed = parseFloat(raw);
|
|
58
|
+
if (Number.isNaN(parsed) || parsed < 0 || parsed > 1)
|
|
59
|
+
return 1.0;
|
|
60
|
+
return parsed;
|
|
61
|
+
})();
|
|
62
|
+
/**
|
|
63
|
+
* Normalize an Express route path to prevent label cardinality explosion.
|
|
64
|
+
*
|
|
65
|
+
* Uses Express's matched route pattern (e.g. `/plugins/:id`) when available,
|
|
66
|
+
* otherwise falls back to the raw path with UUID/numeric segments replaced.
|
|
67
|
+
*/
|
|
68
|
+
function normalizeRoute(req) {
|
|
69
|
+
// Prefer Express matched route pattern
|
|
70
|
+
if (req.route?.path) {
|
|
71
|
+
return req.baseUrl + req.route.path;
|
|
72
|
+
}
|
|
73
|
+
// Fallback: replace UUIDs and numeric IDs with :id
|
|
74
|
+
return req.path
|
|
75
|
+
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id')
|
|
76
|
+
.replace(/\/\d+(?=\/|$)/g, '/:id');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Express middleware that records request duration and count.
|
|
80
|
+
*
|
|
81
|
+
* Must be registered before route handlers so `res.on('finish')` fires
|
|
82
|
+
* after the response is sent.
|
|
83
|
+
*/
|
|
84
|
+
export function metricsMiddleware() {
|
|
85
|
+
return (req, res, next) => {
|
|
86
|
+
// NOTE: no /metrics or /health skip needed here — both endpoints are
|
|
87
|
+
// registered in app-factory BEFORE this middleware (createHealthRouter and
|
|
88
|
+
// app.get('/metrics') both terminate the response), so neither ever reaches
|
|
89
|
+
// this handler.
|
|
90
|
+
const end = httpRequestDuration.startTimer();
|
|
91
|
+
res.on('finish', () => {
|
|
92
|
+
const route = normalizeRoute(req);
|
|
93
|
+
const labels = {
|
|
94
|
+
method: req.method,
|
|
95
|
+
route,
|
|
96
|
+
status_code: String(res.statusCode),
|
|
97
|
+
};
|
|
98
|
+
end(labels);
|
|
99
|
+
httpRequestsTotal.inc(labels);
|
|
100
|
+
// Per-org sampled counter. Skip when:
|
|
101
|
+
// - no org context on the request (anonymous / health endpoints)
|
|
102
|
+
// - sample rate is 0 (disabled)
|
|
103
|
+
// - dice roll fails
|
|
104
|
+
// Using Math.random for the sampler — for unbiased low-rate sampling
|
|
105
|
+
// a deterministic per-request hash would be more uniform, but at the
|
|
106
|
+
// default rate of 1.0 every request lands so this is fine.
|
|
107
|
+
const orgId = req.user?.organizationId;
|
|
108
|
+
if (orgId && ORG_SAMPLE_RATE > 0 && Math.random() < ORG_SAMPLE_RATE) {
|
|
109
|
+
httpRequestsByOrgTotal.inc({ ...labels, org_id: orgId });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
next();
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Express handler that returns Prometheus metrics in text exposition format.
|
|
117
|
+
*/
|
|
118
|
+
export function metricsHandler() {
|
|
119
|
+
return async (_req, res) => {
|
|
120
|
+
res.set('Content-Type', register.contentType);
|
|
121
|
+
res.end(await register.metrics());
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Business / domain metrics
|
|
126
|
+
//
|
|
127
|
+
// Services should emit counters for the things operators actually care about
|
|
128
|
+
// (pipelines generated, plugin builds completed, compliance violations,
|
|
129
|
+
// AI tokens consumed) — not just HTTP traffic. The helpers below register
|
|
130
|
+
// counters lazily on the shared registry so multiple services can call
|
|
131
|
+
// `incCounter('plugin_builds_total', { status: 'completed' })` without
|
|
132
|
+
// worrying about double-registration.
|
|
133
|
+
//
|
|
134
|
+
// Naming convention: `<domain>_<noun>_total` for counters, `_seconds` for
|
|
135
|
+
// histograms — matches Prometheus best-practice.
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
const businessCounters = new Map();
|
|
138
|
+
const businessHistograms = new Map();
|
|
139
|
+
const businessGauges = new Map();
|
|
140
|
+
/**
|
|
141
|
+
* Increment a business counter, creating it on first use.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* incCounter('pipelines_generated_total', { provider: 'anthropic' });
|
|
146
|
+
* incCounter('plugin_builds_total', { status: 'completed' });
|
|
147
|
+
* incCounter('quota_threshold_crossed_total', { type: 'aiCalls', tier: 'pro' });
|
|
148
|
+
* incCounter('compliance_violations_found_total', { severity: 'critical' });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export function incCounter(name, labels = {}, value = 1) {
|
|
152
|
+
let counter = businessCounters.get(name);
|
|
153
|
+
if (!counter) {
|
|
154
|
+
counter = new Counter({
|
|
155
|
+
name,
|
|
156
|
+
help: humanizeName(name),
|
|
157
|
+
labelNames: Object.keys(labels),
|
|
158
|
+
registers: [register],
|
|
159
|
+
});
|
|
160
|
+
businessCounters.set(name, counter);
|
|
161
|
+
}
|
|
162
|
+
counter.inc(labels, value);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Record an observation on a business histogram, creating it on first use.
|
|
166
|
+
* Use for durations, sizes, anything where percentiles matter.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* observe('ai_generation_duration_seconds', { provider: 'anthropic', model: 'sonnet' }, durationSec);
|
|
171
|
+
* observe('plugin_build_duration_seconds', { buildType: 'docker' }, durationSec);
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export function observe(name, labels, value) {
|
|
175
|
+
let hist = businessHistograms.get(name);
|
|
176
|
+
if (!hist) {
|
|
177
|
+
hist = new Histogram({
|
|
178
|
+
name,
|
|
179
|
+
help: humanizeName(name),
|
|
180
|
+
labelNames: Object.keys(labels),
|
|
181
|
+
// Default buckets target sub-second through several-minute observations.
|
|
182
|
+
// Override by registering the histogram explicitly before first call
|
|
183
|
+
// if you need a different bucket distribution.
|
|
184
|
+
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300],
|
|
185
|
+
registers: [register],
|
|
186
|
+
});
|
|
187
|
+
businessHistograms.set(name, hist);
|
|
188
|
+
}
|
|
189
|
+
hist.observe(labels, value);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Set the value of a business gauge, creating it on first use. Use for
|
|
193
|
+
* point-in-time observations sampled by a scraper — queue depth, active
|
|
194
|
+
* connection counts, in-flight requests.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```typescript
|
|
198
|
+
* setGauge('plugin_queue_jobs', { queue: 'plugin-build', state: 'waiting' }, 12);
|
|
199
|
+
* setGauge('plugin_queue_jobs', { queue: 'plugin-build', state: 'active' }, 3);
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export function setGauge(name, labels, value) {
|
|
203
|
+
let gauge = businessGauges.get(name);
|
|
204
|
+
if (!gauge) {
|
|
205
|
+
gauge = new Gauge({
|
|
206
|
+
name,
|
|
207
|
+
help: humanizeName(name),
|
|
208
|
+
labelNames: Object.keys(labels),
|
|
209
|
+
registers: [register],
|
|
210
|
+
});
|
|
211
|
+
businessGauges.set(name, gauge);
|
|
212
|
+
}
|
|
213
|
+
gauge.set(labels, value);
|
|
214
|
+
}
|
|
215
|
+
function humanizeName(metricName) {
|
|
216
|
+
// `pipelines_generated_total` → "Pipelines generated total"
|
|
217
|
+
return metricName
|
|
218
|
+
.replace(/_/g, ' ')
|
|
219
|
+
.replace(/^./, (c) => c.toUpperCase());
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0cmljcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hcGkvbWV0cmljcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUV6RCxPQUFPLEVBQUUsUUFBUSxFQUFFLHFCQUFxQixFQUFFLE9BQU8sRUFBRSxTQUFTLEVBQUUsS0FBSyxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBRXpGLE1BQU0sWUFBWSxHQUFJLE1BQU0sQ0FBQyxNQUFNLENBQUMsZUFBZSxDQUE2QixDQUFDLFdBQVcsQ0FBQztBQUU3RixpQ0FBaUM7QUFDakMsTUFBTSxRQUFRLEdBQUcsSUFBSSxRQUFRLEVBQUUsQ0FBQztBQUVoQyxxQ0FBcUM7QUFDckMsUUFBUSxDQUFDLGdCQUFnQixDQUFDLEVBQUUsT0FBTyxFQUFFLFlBQVksRUFBRSxDQUFDLENBQUM7QUFFckQsa0ZBQWtGO0FBQ2xGLHFCQUFxQixDQUFDLEVBQUUsUUFBUSxFQUFFLENBQUMsQ0FBQztBQUVwQyxnREFBZ0Q7QUFDaEQsTUFBTSxtQkFBbUIsR0FBRyxJQUFJLFNBQVMsQ0FBQztJQUN4QyxJQUFJLEVBQUUsK0JBQStCO0lBQ3JDLElBQUksRUFBRSxzQ0FBc0M7SUFDNUMsVUFBVSxFQUFFLENBQUMsUUFBUSxFQUFFLE9BQU8sRUFBRSxhQUFhLENBQVU7SUFDdkQsT0FBTyxFQUFFLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFFLEdBQUcsRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLENBQUMsRUFBRSxHQUFHLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQztJQUNsRSxTQUFTLEVBQUUsQ0FBQyxRQUFRLENBQUM7Q0FDdEIsQ0FBQyxDQUFDO0FBRUgsMkJBQTJCO0FBQzNCLE1BQU0saUJBQWlCLEdBQUcsSUFBSSxPQUFPLENBQUM7SUFDcEMsSUFB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SSxDQUFDLENBQUM7SUFDckMsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ1gsS0FBSyxHQUFHLElBQUksS0FBSyxDQUFDO1lBQ2hCLElBQUk7WUFDSixJQUFJLEVBQUUsWUFBWSxDQUFDLElBQUksQ0FBQztZQUN4QixVQUFVLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUM7WUFDL0IsU0FBUyxFQUFFLENBQUMsUUFBUSxDQUFDO1NBQ3RCLENBQUMsQ0FBQztRQUNILGNBQWMsQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQ2xDLENBQUM7SUFDRCxLQUFLLENBQUMsR0FBRyxDQUFDLE1BQU0sRUFBRSxLQUFLLENBQUMsQ0FBQztBQUMzQixDQUFDO0FBRUQsU0FBUyxZQUFZLENBQUMsVUFBa0I7SUFDdEMsNERBQTREO0lBQzVELE9BQU8sVUFBVTtTQUNkLE9BQU8sQ0FBQyxJQUFJLEVBQUUsR0FBRyxDQUFDO1NBQ2xCLE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO0FBQzNDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuaW1wb3J0IHsgQ29uZmlnIH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvcGlwZWxpbmUtY29yZSc7XG5pbXBvcnQgdHlwZSB7IFJlcXVlc3QsIFJlc3BvbnNlLCBOZXh0RnVuY3Rpb24gfSBmcm9tICdleHByZXNzJztcbmltcG9ydCB7IFJlZ2lzdHJ5LCBjb2xsZWN0RGVmYXVsdE1ldHJpY3MsIENvdW50ZXIsIEhpc3RvZ3JhbSwgR2F1Z2UgfSBmcm9tICdwcm9tLWNsaWVudCc7XG5cbmNvbnN0IFNFUlZJQ0VfTkFNRSA9IChDb25maWcuZ2V0QW55KCdvYnNlcnZhYmlsaXR5JykgYXMgeyBzZXJ2aWNlTmFtZTogc3RyaW5nIH0pLnNlcnZpY2VOYW1lO1xuXG4vKiogU2hhcmVkIFByb21ldGhldXMgcmVnaXN0cnkgKi9cbmNvbnN0IHJlZ2lzdGVyID0gbmV3IFJlZ2lzdHJ5KCk7XG5cbi8vIFNldCBkZWZhdWx0IGxhYmVscyBmb3IgYWxsIG1ldHJpY3NcbnJlZ2lzdGVyLnNldERlZmF1bHRMYWJlbHMoeyBzZXJ2aWNlOiBTRVJWSUNFX05BTUUgfSk7XG5cbi8vIENvbGxlY3QgZGVmYXVsdCBOb2RlLmpzIHByb2Nlc3MgbWV0cmljcyAoQ1BVLCBtZW1vcnksIGhlYXAsIGV2ZW50IGxvb3AgbGFnLCBHQylcbmNvbGxlY3REZWZhdWx0TWV0cmljcyh7IHJlZ2lzdGVyIH0pO1xuXG4vKiogSFRUUCByZXF1ZXN0IGR1cmF0aW9uIGhpc3RvZ3JhbSAoc2Vjb25kcykgKi9cbmNvbnN0IGh0dHBSZXF1ZXN0RHVyYXRpb24gPSBuZXcgSGlzdG9ncmFtKHtcbiAgbmFtZTogJ2h0dHBfcmVxdWVzdF9kdXJhdGlvbl9zZWNvbmRzJyxcbiAgaGVscDogJ0R1cmF0aW9uIG9mIEhUVFAgcmVxdWVzdHMgaW4gc2Vjb25kcycsXG4gIGxhYmVsTmFtZXM6IFsnbWV0aG9kJywgJ3JvdXRlJywgJ3N0YXR1c19jb2RlJ10gYXMgY29uc3QsXG4gIGJ1Y2tldHM6IFswLjAwNSwgMC4wMSwgMC4wMjUsIDAuMDUsIDAuMSwgMC4yNSwgMC41LCAxLCAyLjUsIDUsIDEwXSxcbiAgcmVnaXN0ZXJzOiBbcmVnaXN0ZXJdLFxufSk7XG5cbi8qKiBIVFRQIHJlcXVlc3QgY291bnRlciAqL1xuY29uc3QgaHR0cFJlcXVlc3RzVG90YWwgPSBuZXcgQ291bnRlcih7XG4gIG5hbWU6ICdodHRwX3JlcXVlc3RzX3RvdGFsJyxcbiAgaGVscDogJ1RvdGFsIG51bWJlciBvZiBIVFRQIHJlcXVlc3RzJyxcbiAgbGFiZWxOYW1lczogWydtZXRob2QnLCAncm91dGUnLCAnc3RhdHVzX2NvZGUnXSBhcyBjb25zdCxcbiAgcmVnaXN0ZXJzOiBbcmVnaXN0ZXJdLFxufSk7XG5cbi8qKlxuICogUGVyLW9yZyBIVFRQIHJlcXVlc3QgY291bnRlciDigJQgU0FNUExFRCB0byBrZWVwIFByb21ldGhldXMgY2FyZGluYWxpdHlcbiAqIHVuZGVyIGNvbnRyb2wuIENhcmRpbmFsaXR5IG9mIGBvcmdfaWQgKiBtZXRob2QgKiByb3V0ZSAqIHN0YXR1c19jb2RlYFxuICogZ3Jvd3MgbXVsdGlwbGljYXRpdmVseTsgd2l0aCBOIG9yZ3MgKiA1MCByb3V0ZXMgKiA1IHN0YXR1c19jb2RlcyB0aGF0J3NcbiAqIDI1MMOXTiBzZXJpZXMuIFdlIHNhbXBsZSAxLWluLU4gcmVxdWVzdHMgKGNvbmZpZ3VyYWJsZSB2aWFcbiAqIGBIVFRQX01FVFJJQ1NfT1JHX1NBTVBMRV9SQVRFYCwgZGVmYXVsdCAxLjAgPSBldmVyeSByZXF1ZXN0KS5cbiAqXG4gKiBPcGVyYXRvcnMgYW5zd2VyIFwid2hhdCdzIG9yZyBYJ3MgcDk5P1wiIHZpYSBzZXBhcmF0ZSBwZXItb3JnIGhpc3RvZ3JhbXNcbiAqIHNjcmFwZWQgYXQgYSBsb3dlciByYXRlLCBPUiB2aWEgTG9raSBMb2dRTCBxdWVyaWVzIGFnYWluc3QgdGhlXG4gKiB1bnNhbXBsZWQgYWNjZXNzLWxvZyBzdHJlYW0uIFRoaXMgY291bnRlciBpcyB0aGUgZmFzdC1wYXRoIFwibm9pc3lcbiAqIG5laWdoYm9yIGRldGVjdGlvblwiIOKAlCBvcGVyYXRvcnMgYWxlcnQgd2hlbiBvbmUgb3JnJ3MgcmVxdWVzdCByYXRlXG4gKiBkZXZpYXRlcyBmcm9tIHRoZSBmbGVldCBtZWRpYW4uXG4gKlxuICogTk9UIGEgaGlzdG9ncmFtOiBoaXN0b2dyYW0gbGFiZWwgY2FyZGluYWxpdHkgaXMgZXZlbiBtb3JlIHNlbnNpdGl2ZVxuICogKGVhY2ggYnVja2V0IGlzIGEgc2VwYXJhdGUgc2VyaWVzKS4gSWYgcGVyLW9yZyBsYXRlbmN5IHBlcmNlbnRpbGVzIGFyZVxuICogbmVlZGVkIGxhdGVyLCBhZGQgYSBzZXBhcmF0ZSBoaXN0b2dyYW0gd2l0aCBhIGNvYXJzZXIgYnVja2V0IHNldCBhbmRcbiAqIGxvd2VyIHNhbXBsZSByYXRlLlxuICovXG5jb25zdCBodHRwUmVxdWVzdHNCeU9yZ1RvdGFsID0gbmV3IENvdW50ZXIoe1xuICBuYW1lOiAnaHR0cF9yZXF1ZXN0c19ieV9vcmdfdG90YWwnLFxuICBoZWxwOiAnU2FtcGxlZCBwZXItb3JnIEhUVFAgcmVxdWVzdCBjb3VudCAoc2FtcGxlIHJhdGUgdmlhIEhUVFBfTUVUUklDU19PUkdfU0FNUExFX1JBVEUpJyxcbiAgbGFiZWxOYW1lczogWydtZXRob2QnLCAncm91dGUnLCAnc3RhdHVzX2NvZGUnLCAnb3JnX2lkJ10gYXMgY29uc3QsXG4gIHJlZ2lzdGVyczogW3JlZ2lzdGVyXSxcbn0pO1xuXG4vKiogU2FtcGxlIHJhdGUgZm9yIHRoZSBwZXItb3JnIGNvdW50ZXIuIDEuMCA9IGV2ZXJ5IHJlcXVlc3QsIDAuMCA9IGRpc2FibGVkLlxuICogIFBhcnNlZCBvbmNlIGF0IG1vZHVsZSBsb2FkIOKAlCBvcGVyYXRvcnMgdHVuZSBieSByZXN0YXJ0aW5nIHRoZSBzZXJ2aWNlLiAqL1xuY29uc3QgT1JHX1NBTVBMRV9SQVRFID0gKCgpID0+IHtcbiAgY29uc3QgcmF3ID0gcHJvY2Vzcy5lbnYuSFRUUF9NRVRSSUNTX09SR19TQU1QTEVfUkFURTtcbiAgaWYgKHJhdyA9PT0gdW5kZWZpbmVkIHx8IHJhdyA9PT0gJycpIHJldHVybiAxLjA7XG4gIGNvbnN0IHBhcnNlZCA9IHBhcnNlRmxvYXQocmF3KTtcbiAgaWYgKE51bWJlci5pc05hTihwYXJzZWQpIHx8IHBhcnNlZCA8IDAgfHwgcGFyc2VkID4gMSkgcmV0dXJuIDEuMDtcbiAgcmV0dXJuIHBhcnNlZDtcbn0pKCk7XG5cbi8qKlxuICogTm9ybWFsaXplIGFuIEV4cHJlc3Mgcm91dGUgcGF0aCB0byBwcmV2ZW50IGxhYmVsIGNhcmRpbmFsaXR5IGV4cGxvc2lvbi5cbiAqXG4gKiBVc2VzIEV4cHJlc3MncyBtYXRjaGVkIHJvdXRlIHBhdHRlcm4gKGUuZy4gYC9wbHVnaW5zLzppZGApIHdoZW4gYXZhaWxhYmxlLFxuICogb3RoZXJ3aXNlIGZhbGxzIGJhY2sgdG8gdGhlIHJhdyBwYXRoIHdpdGggVVVJRC9udW1lcmljIHNlZ21lbnRzIHJlcGxhY2VkLlxuICovXG5mdW5jdGlvbiBub3JtYWxpemVSb3V0ZShyZXE6IFJlcXVlc3QpOiBzdHJpbmcge1xuICAvLyBQcmVmZXIgRXhwcmVzcyBtYXRjaGVkIHJvdXRlIHBhdHRlcm5cbiAgaWYgKHJlcS5yb3V0ZT8ucGF0aCkge1xuICAgIHJldHVybiByZXEuYmFzZVVybCArIHJlcS5yb3V0ZS5wYXRoO1xuICB9XG5cbiAgLy8gRmFsbGJhY2s6IHJlcGxhY2UgVVVJRHMgYW5kIG51bWVyaWMgSURzIHdpdGggOmlkXG4gIHJldHVybiByZXEucGF0aFxuICAgIC5yZXBsYWNlKC9bMC05YS1mXXs4fS1bMC05YS1mXXs0fS1bMC05YS1mXXs0fS1bMC05YS1mXXs0fS1bMC05YS1mXXsxMn0vZ2ksICc6aWQnKVxuICAgIC5yZXBsYWNlKC9cXC9cXGQrKD89XFwvfCQpL2csICcvOmlkJyk7XG59XG5cbi8qKlxuICogRXhwcmVzcyBtaWRkbGV3YXJlIHRoYXQgcmVjb3JkcyByZXF1ZXN0IGR1cmF0aW9uIGFuZCBjb3VudC5cbiAqXG4gKiBNdXN0IGJlIHJlZ2lzdGVyZWQgYmVmb3JlIHJvdXRlIGhhbmRsZXJzIHNvIGByZXMub24oJ2ZpbmlzaCcpYCBmaXJlc1xuICogYWZ0ZXIgdGhlIHJlc3BvbnNlIGlzIHNlbnQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBtZXRyaWNzTWlkZGxld2FyZSgpIHtcbiAgcmV0dXJuIChyZXE6IFJlcXVlc3QsIHJlczogUmVzcG9uc2UsIG5leHQ6IE5leHRGdW5jdGlvbik6IHZvaWQgPT4ge1xuICAgIC8vIE5PVEU6IG5vIC9tZXRyaWNzIG9yIC9oZWFsdGggc2tpcCBuZWVkZWQgaGVyZSDigJQgYm90aCBlbmRwb2ludHMgYXJlXG4gICAgLy8gcmVnaXN0ZXJlZCBpbiBhcHAtZmFjdG9yeSBCRUZPUkUgdGhpcyBtaWRkbGV3YXJlIChjcmVhdGVIZWFsdGhSb3V0ZXIgYW5kXG4gICAgLy8gYXBwLmdldCgnL21ldHJpY3MnKSBib3RoIHRlcm1pbmF0ZSB0aGUgcmVzcG9uc2UpLCBzbyBuZWl0aGVyIGV2ZXIgcmVhY2hlc1xuICAgIC8vIHRoaXMgaGFuZGxlci5cbiAgICBjb25zdCBlbmQgPSBodHRwUmVxdWVzdER1cmF0aW9uLnN0YXJ0VGltZXIoKTtcblxuICAgIHJlcy5vbignZmluaXNoJywgKCkgPT4ge1xuICAgICAgY29uc3Qgcm91dGUgPSBub3JtYWxpemVSb3V0ZShyZXEpO1xuICAgICAgY29uc3QgbGFiZWxzID0ge1xuICAgICAgICBtZXRob2Q6IHJlcS5tZXRob2QsXG4gICAgICAgIHJvdXRlLFxuICAgICAgICBzdGF0dXNfY29kZTogU3RyaW5nKHJlcy5zdGF0dXNDb2RlKSxcbiAgICAgIH07XG5cbiAgICAgIGVuZChsYWJlbHMpO1xuICAgICAgaHR0cFJlcXVlc3RzVG90YWwuaW5jKGxhYmVscyk7XG5cbiAgICAgIC8vIFBlci1vcmcgc2FtcGxlZCBjb3VudGVyLiBTa2lwIHdoZW46XG4gICAgICAvLyAgIC0gbm8gb3JnIGNvbnRleHQgb24gdGhlIHJlcXVlc3QgKGFub255bW91cyAvIGhlYWx0aCBlbmRwb2ludHMpXG4gICAgICAvLyAgIC0gc2FtcGxlIHJhdGUgaXMgMCAoZGlzYWJsZWQpXG4gICAgICAvLyAgIC0gZGljZSByb2xsIGZhaWxzXG4gICAgICAvLyBVc2luZyBNYXRoLnJhbmRvbSBmb3IgdGhlIHNhbXBsZXIg4oCUIGZvciB1bmJpYXNlZCBsb3ctcmF0ZSBzYW1wbGluZ1xuICAgICAgLy8gYSBkZXRlcm1pbmlzdGljIHBlci1yZXF1ZXN0IGhhc2ggd291bGQgYmUgbW9yZSB1bmlmb3JtLCBidXQgYXQgdGhlXG4gICAgICAvLyBkZWZhdWx0IHJhdGUgb2YgMS4wIGV2ZXJ5IHJlcXVlc3QgbGFuZHMgc28gdGhpcyBpcyBmaW5lLlxuICAgICAgY29uc3Qgb3JnSWQgPSByZXEudXNlcj8ub3JnYW5pemF0aW9uSWQ7XG4gICAgICBpZiAob3JnSWQgJiYgT1JHX1NBTVBMRV9SQVRFID4gMCAmJiBNYXRoLnJhbmRvbSgpIDwgT1JHX1NBTVBMRV9SQVRFKSB7XG4gICAgICAgIGh0dHBSZXF1ZXN0c0J5T3JnVG90YWwuaW5jKHsgLi4ubGFiZWxzLCBvcmdfaWQ6IG9yZ0lkIH0pO1xuICAgICAgfVxuICAgIH0pO1xuXG4gICAgbmV4dCgpO1xuICB9O1xufVxuXG4vKipcbiAqIEV4cHJlc3MgaGFuZGxlciB0aGF0IHJldHVybnMgUHJvbWV0aGV1cyBtZXRyaWNzIGluIHRleHQgZXhwb3NpdGlvbiBmb3JtYXQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBtZXRyaWNzSGFuZGxlcigpIHtcbiAgcmV0dXJuIGFzeW5jIChfcmVxOiBSZXF1ZXN0LCByZXM6IFJlc3BvbnNlKTogUHJvbWlzZTx2b2lkPiA9PiB7XG4gICAgcmVzLnNldCgnQ29udGVudC1UeXBlJywgcmVnaXN0ZXIuY29udGVudFR5cGUpO1xuICAgIHJlcy5lbmQoYXdhaXQgcmVnaXN0ZXIubWV0cmljcygpKTtcbiAgfTtcbn1cblxuLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG4vLyBCdXNpbmVzcyAvIGRvbWFpbiBtZXRyaWNzXG4vL1xuLy8gU2VydmljZXMgc2hvdWxkIGVtaXQgY291bnRlcnMgZm9yIHRoZSB0aGluZ3Mgb3BlcmF0b3JzIGFjdHVhbGx5IGNhcmUgYWJvdXRcbi8vIChwaXBlbGluZXMgZ2VuZXJhdGVkLCBwbHVnaW4gYnVpbGRzIGNvbXBsZXRlZCwgY29tcGxpYW5jZSB2aW9sYXRpb25zLFxuLy8gQUkgdG9rZW5zIGNvbnN1bWVkKSDigJQgbm90IGp1c3QgSFRUUCB0cmFmZmljLiBUaGUgaGVscGVycyBiZWxvdyByZWdpc3RlclxuLy8gY291bnRlcnMgbGF6aWx5IG9uIHRoZSBzaGFyZWQgcmVnaXN0cnkgc28gbXVsdGlwbGUgc2VydmljZXMgY2FuIGNhbGxcbi8vIGBpbmNDb3VudGVyKCdwbHVnaW5fYnVpbGRzX3RvdGFsJywgeyBzdGF0dXM6ICdjb21wbGV0ZWQnIH0pYCB3aXRob3V0XG4vLyB3b3JyeWluZyBhYm91dCBkb3VibGUtcmVnaXN0cmF0aW9uLlxuLy9cbi8vIE5hbWluZyBjb252ZW50aW9uOiBgPGRvbWFpbj5fPG5vdW4+X3RvdGFsYCBmb3IgY291bnRlcnMsIGBfc2Vjb25kc2AgZm9yXG4vLyBoaXN0b2dyYW1zIOKAlCBtYXRjaGVzIFByb21ldGhldXMgYmVzdC1wcmFjdGljZS5cbi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuXG5jb25zdCBidXNpbmVzc0NvdW50ZXJzID0gbmV3IE1hcDxzdHJpbmcsIENvdW50ZXI8c3RyaW5nPj4oKTtcbmNvbnN0IGJ1c2luZXNzSGlzdG9ncmFtcyA9IG5ldyBNYXA8c3RyaW5nLCBIaXN0b2dyYW08c3RyaW5nPj4oKTtcbmNvbnN0IGJ1c2luZXNzR2F1Z2VzID0gbmV3IE1hcDxzdHJpbmcsIEdhdWdlPHN0cmluZz4+KCk7XG5cbi8qKlxuICogSW5jcmVtZW50IGEgYnVzaW5lc3MgY291bnRlciwgY3JlYXRpbmcgaXQgb24gZmlyc3QgdXNlLlxuICpcbiAqIEBleGFtcGxlXG4gKiBgYGB0eXBlc2NyaXB0XG4gKiBpbmNDb3VudGVyKCdwaXBlbGluZXNfZ2VuZXJhdGVkX3RvdGFsJywgeyBwcm92aWRlcjogJ2FudGhyb3BpYycgfSk7XG4gKiBpbmNDb3VudGVyKCdwbHVnaW5fYnVpbGRzX3RvdGFsJywgeyBzdGF0dXM6ICdjb21wbGV0ZWQnIH0pO1xuICogaW5jQ291bnRlcigncXVvdGFfdGhyZXNob2xkX2Nyb3NzZWRfdG90YWwnLCB7IHR5cGU6ICdhaUNhbGxzJywgdGllcjogJ3BybycgfSk7XG4gKiBpbmNDb3VudGVyKCdjb21wbGlhbmNlX3Zpb2xhdGlvbnNfZm91bmRfdG90YWwnLCB7IHNldmVyaXR5OiAnY3JpdGljYWwnIH0pO1xuICogYGBgXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBpbmNDb3VudGVyKG5hbWU6IHN0cmluZywgbGFiZWxzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0ge30sIHZhbHVlID0gMSk6IHZvaWQge1xuICBsZXQgY291bnRlciA9IGJ1c2luZXNzQ291bnRlcnMuZ2V0KG5hbWUpO1xuICBpZiAoIWNvdW50ZXIpIHtcbiAgICBjb3VudGVyID0gbmV3IENvdW50ZXIoe1xuICAgICAgbmFtZSxcbiAgICAgIGhlbHA6IGh1bWFuaXplTmFtZShuYW1lKSxcbiAgICAgIGxhYmVsTmFtZXM6IE9iamVjdC5rZXlzKGxhYmVscyksXG4gICAgICByZWdpc3RlcnM6IFtyZWdpc3Rlcl0sXG4gICAgfSk7XG4gICAgYnVzaW5lc3NDb3VudGVycy5zZXQobmFtZSwgY291bnRlcik7XG4gIH1cbiAgY291bnRlci5pbmMobGFiZWxzLCB2YWx1ZSk7XG59XG5cbi8qKlxuICogUmVjb3JkIGFuIG9ic2VydmF0aW9uIG9uIGEgYnVzaW5lc3MgaGlzdG9ncmFtLCBjcmVhdGluZyBpdCBvbiBmaXJzdCB1c2UuXG4gKiBVc2UgZm9yIGR1cmF0aW9ucywgc2l6ZXMsIGFueXRoaW5nIHdoZXJlIHBlcmNlbnRpbGVzIG1hdHRlci5cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogb2JzZXJ2ZSgnYWlfZ2VuZXJhdGlvbl9kdXJhdGlvbl9zZWNvbmRzJywgeyBwcm92aWRlcjogJ2FudGhyb3BpYycsIG1vZGVsOiAnc29ubmV0JyB9LCBkdXJhdGlvblNlYyk7XG4gKiBvYnNlcnZlKCdwbHVnaW5fYnVpbGRfZHVyYXRpb25fc2Vjb25kcycsIHsgYnVpbGRUeXBlOiAnZG9ja2VyJyB9LCBkdXJhdGlvblNlYyk7XG4gKiBgYGBcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG9ic2VydmUobmFtZTogc3RyaW5nLCBsYWJlbHM6IFJlY29yZDxzdHJpbmcsIHN0cmluZz4sIHZhbHVlOiBudW1iZXIpOiB2b2lkIHtcbiAgbGV0IGhpc3QgPSBidXNpbmVzc0hpc3RvZ3JhbXMuZ2V0KG5hbWUpO1xuICBpZiAoIWhpc3QpIHtcbiAgICBoaXN0ID0gbmV3IEhpc3RvZ3JhbSh7XG4gICAgICBuYW1lLFxuICAgICAgaGVscDogaHVtYW5pemVOYW1lKG5hbWUpLFxuICAgICAgbGFiZWxOYW1lczogT2JqZWN0LmtleXMobGFiZWxzKSxcbiAgICAgIC8vIERlZmF1bHQgYnVja2V0cyB0YXJnZXQgc3ViLXNlY29uZCB0aHJvdWdoIHNldmVyYWwtbWludXRlIG9ic2VydmF0aW9ucy5cbiAgICAgIC8vIE92ZXJyaWRlIGJ5IHJlZ2lzdGVyaW5nIHRoZSBoaXN0b2dyYW0gZXhwbGljaXRseSBiZWZvcmUgZmlyc3QgY2FsbFxuICAgICAgLy8gaWYgeW91IG5lZWQgYSBkaWZmZXJlbnQgYnVja2V0IGRpc3RyaWJ1dGlvbi5cbiAgICAgIGJ1Y2tldHM6IFswLjA1LCAwLjEsIDAuMjUsIDAuNSwgMSwgMi41LCA1LCAxMCwgMzAsIDYwLCAxMjAsIDMwMF0sXG4gICAgICByZWdpc3RlcnM6IFtyZWdpc3Rlcl0sXG4gICAgfSk7XG4gICAgYnVzaW5lc3NIaXN0b2dyYW1zLnNldChuYW1lLCBoaXN0KTtcbiAgfVxuICBoaXN0Lm9ic2VydmUobGFiZWxzLCB2YWx1ZSk7XG59XG5cbi8qKlxuICogU2V0IHRoZSB2YWx1ZSBvZiBhIGJ1c2luZXNzIGdhdWdlLCBjcmVhdGluZyBpdCBvbiBmaXJzdCB1c2UuIFVzZSBmb3JcbiAqIHBvaW50LWluLXRpbWUgb2JzZXJ2YXRpb25zIHNhbXBsZWQgYnkgYSBzY3JhcGVyIOKAlCBxdWV1ZSBkZXB0aCwgYWN0aXZlXG4gKiBjb25uZWN0aW9uIGNvdW50cywgaW4tZmxpZ2h0IHJlcXVlc3RzLlxuICpcbiAqIEBleGFtcGxlXG4gKiBgYGB0eXBlc2NyaXB0XG4gKiBzZXRHYXVnZSgncGx1Z2luX3F1ZXVlX2pvYnMnLCB7IHF1ZXVlOiAncGx1Z2luLWJ1aWxkJywgc3RhdGU6ICd3YWl0aW5nJyB9LCAxMik7XG4gKiBzZXRHYXVnZSgncGx1Z2luX3F1ZXVlX2pvYnMnLCB7IHF1ZXVlOiAncGx1Z2luLWJ1aWxkJywgc3RhdGU6ICdhY3RpdmUnIH0sIDMpO1xuICogYGBgXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzZXRHYXVnZShuYW1lOiBzdHJpbmcsIGxhYmVsczogUmVjb3JkPHN0cmluZywgc3RyaW5nPiwgdmFsdWU6IG51bWJlcik6IHZvaWQge1xuICBsZXQgZ2F1Z2UgPSBidXNpbmVzc0dhdWdlcy5nZXQobmFtZSk7XG4gIGlmICghZ2F1Z2UpIHtcbiAgICBnYXVnZSA9IG5ldyBHYXVnZSh7XG4gICAgICBuYW1lLFxuICAgICAgaGVscDogaHVtYW5pemVOYW1lKG5hbWUpLFxuICAgICAgbGFiZWxOYW1lczogT2JqZWN0LmtleXMobGFiZWxzKSxcbiAgICAgIHJlZ2lzdGVyczogW3JlZ2lzdGVyXSxcbiAgICB9KTtcbiAgICBidXNpbmVzc0dhdWdlcy5zZXQobmFtZSwgZ2F1Z2UpO1xuICB9XG4gIGdhdWdlLnNldChsYWJlbHMsIHZhbHVlKTtcbn1cblxuZnVuY3Rpb24gaHVtYW5pemVOYW1lKG1ldHJpY05hbWU6IHN0cmluZyk6IHN0cmluZyB7XG4gIC8vIGBwaXBlbGluZXNfZ2VuZXJhdGVkX3RvdGFsYCDihpIgXCJQaXBlbGluZXMgZ2VuZXJhdGVkIHRvdGFsXCJcbiAgcmV0dXJuIG1ldHJpY05hbWVcbiAgICAucmVwbGFjZSgvXy9nLCAnICcpXG4gICAgLnJlcGxhY2UoL14uLywgKGMpID0+IGMudG9VcHBlckNhc2UoKSk7XG59XG4iXX0=
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { QuotaType, QuotaService } from '@pipeline-builder/api-core';
|
|
2
|
+
import type { RequestHandler } from 'express';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a middleware chain for protected routes requiring authentication, org ID, and quota check.
|
|
5
|
+
*
|
|
6
|
+
* Applies middleware in order:
|
|
7
|
+
* 1. requireAuth - Validates JWT and extracts user identity
|
|
8
|
+
* 2. requireOrgId - Ensures request has x-org-id header
|
|
9
|
+
* 3. idempotencyMiddleware - Dedupes keyed mutation retries (no-op without an
|
|
10
|
+
* Idempotency-Key header or on non-mutation methods)
|
|
11
|
+
* 4. withTenantContext - Opens the RLS tenant scope (orgId + isSuperAdmin) for the request
|
|
12
|
+
* 5. checkQuota - Validates quota for the specified resource type
|
|
13
|
+
*
|
|
14
|
+
* Idempotency sits AFTER auth+orgId (so it can namespace the replay cache on the
|
|
15
|
+
* VERIFIED org — a pre-auth global mount saw no identity and was a permanent
|
|
16
|
+
* no-op) and BEFORE withTenantContext/checkQuota (so a replayed request short-
|
|
17
|
+
* circuits without opening an RLS scope or spending a quota check).
|
|
18
|
+
*
|
|
19
|
+
* @param quotaService - Quota service client
|
|
20
|
+
* @param quotaType - Which quota to check (e.g., 'apiCalls', 'pipelines', 'plugins')
|
|
21
|
+
* @returns Array of middleware handlers ready to spread into route definition
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* router.post('/',
|
|
26
|
+
* ...createProtectedRoute(quotaService, 'pipelines'),
|
|
27
|
+
* async (req, res) => {
|
|
28
|
+
* // Handler implementation
|
|
29
|
+
* }
|
|
30
|
+
* );
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function createProtectedRoute(quotaService: QuotaService, quotaType: QuotaType): RequestHandler[];
|
|
34
|
+
/**
|
|
35
|
+
* Creates a middleware chain for authenticated routes with org ID requirement but no quota check.
|
|
36
|
+
*
|
|
37
|
+
* Applies middleware in order:
|
|
38
|
+
* 1. requireAuth - Validates JWT and extracts user identity
|
|
39
|
+
* 2. requireOrgId - Ensures request has x-org-id header
|
|
40
|
+
* 3. withTenantContext - Opens the RLS tenant scope (orgId + isSuperAdmin) for the request
|
|
41
|
+
*
|
|
42
|
+
* Use this for read-only routes that don't consume quota.
|
|
43
|
+
*
|
|
44
|
+
* @returns Array of middleware handlers ready to spread into route definition
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* router.get('/',
|
|
49
|
+
* ...createAuthenticatedWithOrgRoute(),
|
|
50
|
+
* async (req, res) => {
|
|
51
|
+
* // Handler implementation
|
|
52
|
+
* }
|
|
53
|
+
* );
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function createAuthenticatedWithOrgRoute(): RequestHandler[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { checkQuota } from './check-quota.js';
|
|
4
|
+
import { idempotencyMiddleware } from './idempotency-middleware.js';
|
|
5
|
+
import { requireAuth } from './middleware.js';
|
|
6
|
+
import { requireOrgId } from './require-org-id.js';
|
|
7
|
+
import { withTenantContext } from './tenant-context.js';
|
|
8
|
+
/**
|
|
9
|
+
* Creates a middleware chain for protected routes requiring authentication, org ID, and quota check.
|
|
10
|
+
*
|
|
11
|
+
* Applies middleware in order:
|
|
12
|
+
* 1. requireAuth - Validates JWT and extracts user identity
|
|
13
|
+
* 2. requireOrgId - Ensures request has x-org-id header
|
|
14
|
+
* 3. idempotencyMiddleware - Dedupes keyed mutation retries (no-op without an
|
|
15
|
+
* Idempotency-Key header or on non-mutation methods)
|
|
16
|
+
* 4. withTenantContext - Opens the RLS tenant scope (orgId + isSuperAdmin) for the request
|
|
17
|
+
* 5. checkQuota - Validates quota for the specified resource type
|
|
18
|
+
*
|
|
19
|
+
* Idempotency sits AFTER auth+orgId (so it can namespace the replay cache on the
|
|
20
|
+
* VERIFIED org — a pre-auth global mount saw no identity and was a permanent
|
|
21
|
+
* no-op) and BEFORE withTenantContext/checkQuota (so a replayed request short-
|
|
22
|
+
* circuits without opening an RLS scope or spending a quota check).
|
|
23
|
+
*
|
|
24
|
+
* @param quotaService - Quota service client
|
|
25
|
+
* @param quotaType - Which quota to check (e.g., 'apiCalls', 'pipelines', 'plugins')
|
|
26
|
+
* @returns Array of middleware handlers ready to spread into route definition
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* router.post('/',
|
|
31
|
+
* ...createProtectedRoute(quotaService, 'pipelines'),
|
|
32
|
+
* async (req, res) => {
|
|
33
|
+
* // Handler implementation
|
|
34
|
+
* }
|
|
35
|
+
* );
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function createProtectedRoute(quotaService, quotaType) {
|
|
39
|
+
return [
|
|
40
|
+
requireAuth,
|
|
41
|
+
requireOrgId(),
|
|
42
|
+
idempotencyMiddleware(),
|
|
43
|
+
withTenantContext(),
|
|
44
|
+
checkQuota(quotaService, quotaType),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Creates a middleware chain for authenticated routes with org ID requirement but no quota check.
|
|
49
|
+
*
|
|
50
|
+
* Applies middleware in order:
|
|
51
|
+
* 1. requireAuth - Validates JWT and extracts user identity
|
|
52
|
+
* 2. requireOrgId - Ensures request has x-org-id header
|
|
53
|
+
* 3. withTenantContext - Opens the RLS tenant scope (orgId + isSuperAdmin) for the request
|
|
54
|
+
*
|
|
55
|
+
* Use this for read-only routes that don't consume quota.
|
|
56
|
+
*
|
|
57
|
+
* @returns Array of middleware handlers ready to spread into route definition
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* router.get('/',
|
|
62
|
+
* ...createAuthenticatedWithOrgRoute(),
|
|
63
|
+
* async (req, res) => {
|
|
64
|
+
* // Handler implementation
|
|
65
|
+
* }
|
|
66
|
+
* );
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function createAuthenticatedWithOrgRoute() {
|
|
70
|
+
return [
|
|
71
|
+
requireAuth,
|
|
72
|
+
requireOrgId(),
|
|
73
|
+
// Mounted post-orgId (identity populated) so keyed mutation retries on the
|
|
74
|
+
// non-quota routes — notably the create paths, which use this factory to
|
|
75
|
+
// avoid the checkQuota pre-flight racing their reserve — are deduplicated.
|
|
76
|
+
idempotencyMiddleware(),
|
|
77
|
+
withTenantContext(),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWlkZGxld2FyZS1mYWN0b3J5LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FwaS9taWRkbGV3YXJlLWZhY3RvcnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQUl0QyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDOUMsT0FBTyxFQUFFLHFCQUFxQixFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDcEUsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQzlDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUNuRCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUV4RDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0E2Qkc7QUFDSCxNQUFNLFVBQVUsb0JBQW9CLENBQ2xDLFlBQTBCLEVBQzFCLFNBQW9CO0lBRXBCLE9BQU87UUFDTCxXQUE2QjtRQUM3QixZQUFZLEVBQW9CO1FBQ2hDLHFCQUFxQixFQUFvQjtRQUN6QyxpQkFBaUIsRUFBb0I7UUFDckMsVUFBVSxDQUFDLFlBQVksRUFBRSxTQUFTLENBQW1CO0tBQ3RELENBQUM7QUFDSixDQUFDO0FBRUQ7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFCRztBQUNILE1BQU0sVUFBVSwrQkFBK0I7SUFDN0MsT0FBTztRQUNMLFdBQTZCO1FBQzdCLFlBQVksRUFBb0I7UUFDaEMsMkVBQTJFO1FBQzNFLHlFQUF5RTtRQUN6RSwyRUFBMkU7UUFDM0UscUJBQXFCLEVBQW9CO1FBQ3pDLGlCQUFpQixFQUFvQjtLQUN0QyxDQUFDO0FBQ0osQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG5pbXBvcnQgdHlwZSB7IFF1b3RhVHlwZSwgUXVvdGFTZXJ2aWNlIH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvYXBpLWNvcmUnO1xuaW1wb3J0IHR5cGUgeyBSZXF1ZXN0SGFuZGxlciB9IGZyb20gJ2V4cHJlc3MnO1xuaW1wb3J0IHsgY2hlY2tRdW90YSB9IGZyb20gJy4vY2hlY2stcXVvdGEuanMnO1xuaW1wb3J0IHsgaWRlbXBvdGVuY3lNaWRkbGV3YXJlIH0gZnJvbSAnLi9pZGVtcG90ZW5jeS1taWRkbGV3YXJlLmpzJztcbmltcG9ydCB7IHJlcXVpcmVBdXRoIH0gZnJvbSAnLi9taWRkbGV3YXJlLmpzJztcbmltcG9ydCB7IHJlcXVpcmVPcmdJZCB9IGZyb20gJy4vcmVxdWlyZS1vcmctaWQuanMnO1xuaW1wb3J0IHsgd2l0aFRlbmFudENvbnRleHQgfSBmcm9tICcuL3RlbmFudC1jb250ZXh0LmpzJztcblxuLyoqXG4gKiBDcmVhdGVzIGEgbWlkZGxld2FyZSBjaGFpbiBmb3IgcHJvdGVjdGVkIHJvdXRlcyByZXF1aXJpbmcgYXV0aGVudGljYXRpb24sIG9yZyBJRCwgYW5kIHF1b3RhIGNoZWNrLlxuICpcbiAqIEFwcGxpZXMgbWlkZGxld2FyZSBpbiBvcmRlcjpcbiAqIDEuIHJlcXVpcmVBdXRoIC0gVmFsaWRhdGVzIEpXVCBhbmQgZXh0cmFjdHMgdXNlciBpZGVudGl0eVxuICogMi4gcmVxdWlyZU9yZ0lkIC0gRW5zdXJlcyByZXF1ZXN0IGhhcyB4LW9yZy1pZCBoZWFkZXJcbiAqIDMuIGlkZW1wb3RlbmN5TWlkZGxld2FyZSAtIERlZHVwZXMga2V5ZWQgbXV0YXRpb24gcmV0cmllcyAobm8tb3Agd2l0aG91dCBhblxuICogICAgSWRlbXBvdGVuY3ktS2V5IGhlYWRlciBvciBvbiBub24tbXV0YXRpb24gbWV0aG9kcylcbiAqIDQuIHdpdGhUZW5hbnRDb250ZXh0IC0gT3BlbnMgdGhlIFJMUyB0ZW5hbnQgc2NvcGUgKG9yZ0lkICsgaXNTdXBlckFkbWluKSBmb3IgdGhlIHJlcXVlc3RcbiAqIDUuIGNoZWNrUXVvdGEgLSBWYWxpZGF0ZXMgcXVvdGEgZm9yIHRoZSBzcGVjaWZpZWQgcmVzb3VyY2UgdHlwZVxuICpcbiAqIElkZW1wb3RlbmN5IHNpdHMgQUZURVIgYXV0aCtvcmdJZCAoc28gaXQgY2FuIG5hbWVzcGFjZSB0aGUgcmVwbGF5IGNhY2hlIG9uIHRoZVxuICogVkVSSUZJRUQgb3JnIOKAlCBhIHByZS1hdXRoIGdsb2JhbCBtb3VudCBzYXcgbm8gaWRlbnRpdHkgYW5kIHdhcyBhIHBlcm1hbmVudFxuICogbm8tb3ApIGFuZCBCRUZPUkUgd2l0aFRlbmFudENvbnRleHQvY2hlY2tRdW90YSAoc28gYSByZXBsYXllZCByZXF1ZXN0IHNob3J0LVxuICogY2lyY3VpdHMgd2l0aG91dCBvcGVuaW5nIGFuIFJMUyBzY29wZSBvciBzcGVuZGluZyBhIHF1b3RhIGNoZWNrKS5cbiAqXG4gKiBAcGFyYW0gcXVvdGFTZXJ2aWNlIC0gUXVvdGEgc2VydmljZSBjbGllbnRcbiAqIEBwYXJhbSBxdW90YVR5cGUgLSBXaGljaCBxdW90YSB0byBjaGVjayAoZS5nLiwgJ2FwaUNhbGxzJywgJ3BpcGVsaW5lcycsICdwbHVnaW5zJylcbiAqIEByZXR1cm5zIEFycmF5IG9mIG1pZGRsZXdhcmUgaGFuZGxlcnMgcmVhZHkgdG8gc3ByZWFkIGludG8gcm91dGUgZGVmaW5pdGlvblxuICpcbiAqIEBleGFtcGxlXG4gKiBgYGB0eXBlc2NyaXB0XG4gKiByb3V0ZXIucG9zdCgnLycsXG4gKiAgIC4uLmNyZWF0ZVByb3RlY3RlZFJvdXRlKHF1b3RhU2VydmljZSwgJ3BpcGVsaW5lcycpLFxuICogICBhc3luYyAocmVxLCByZXMpID0+IHtcbiAqICAgICAvLyBIYW5kbGVyIGltcGxlbWVudGF0aW9uXG4gKiAgIH1cbiAqICk7XG4gKiBgYGBcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZVByb3RlY3RlZFJvdXRlKFxuICBxdW90YVNlcnZpY2U6IFF1b3RhU2VydmljZSxcbiAgcXVvdGFUeXBlOiBRdW90YVR5cGUsXG4pOiBSZXF1ZXN0SGFuZGxlcltdIHtcbiAgcmV0dXJuIFtcbiAgICByZXF1aXJlQXV0aCBhcyBSZXF1ZXN0SGFuZGxlcixcbiAgICByZXF1aXJlT3JnSWQoKSBhcyBSZXF1ZXN0SGFuZGxlcixcbiAgICBpZGVtcG90ZW5jeU1pZGRsZXdhcmUoKSBhcyBSZXF1ZXN0SGFuZGxlcixcbiAgICB3aXRoVGVuYW50Q29udGV4dCgpIGFzIFJlcXVlc3RIYW5kbGVyLFxuICAgIGNoZWNrUXVvdGEocXVvdGFTZXJ2aWNlLCBxdW90YVR5cGUpIGFzIFJlcXVlc3RIYW5kbGVyLFxuICBdO1xufVxuXG4vKipcbiAqIENyZWF0ZXMgYSBtaWRkbGV3YXJlIGNoYWluIGZvciBhdXRoZW50aWNhdGVkIHJvdXRlcyB3aXRoIG9yZyBJRCByZXF1aXJlbWVudCBidXQgbm8gcXVvdGEgY2hlY2suXG4gKlxuICogQXBwbGllcyBtaWRkbGV3YXJlIGluIG9yZGVyOlxuICogMS4gcmVxdWlyZUF1dGggLSBWYWxpZGF0ZXMgSldUIGFuZCBleHRyYWN0cyB1c2VyIGlkZW50aXR5XG4gKiAyLiByZXF1aXJlT3JnSWQgLSBFbnN1cmVzIHJlcXVlc3QgaGFzIHgtb3JnLWlkIGhlYWRlclxuICogMy4gd2l0aFRlbmFudENvbnRleHQgLSBPcGVucyB0aGUgUkxTIHRlbmFudCBzY29wZSAob3JnSWQgKyBpc1N1cGVyQWRtaW4pIGZvciB0aGUgcmVxdWVzdFxuICpcbiAqIFVzZSB0aGlzIGZvciByZWFkLW9ubHkgcm91dGVzIHRoYXQgZG9uJ3QgY29uc3VtZSBxdW90YS5cbiAqXG4gKiBAcmV0dXJucyBBcnJheSBvZiBtaWRkbGV3YXJlIGhhbmRsZXJzIHJlYWR5IHRvIHNwcmVhZCBpbnRvIHJvdXRlIGRlZmluaXRpb25cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogcm91dGVyLmdldCgnLycsXG4gKiAgIC4uLmNyZWF0ZUF1dGhlbnRpY2F0ZWRXaXRoT3JnUm91dGUoKSxcbiAqICAgYXN5bmMgKHJlcSwgcmVzKSA9PiB7XG4gKiAgICAgLy8gSGFuZGxlciBpbXBsZW1lbnRhdGlvblxuICogICB9XG4gKiApO1xuICogYGBgXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVBdXRoZW50aWNhdGVkV2l0aE9yZ1JvdXRlKCk6IFJlcXVlc3RIYW5kbGVyW10ge1xuICByZXR1cm4gW1xuICAgIHJlcXVpcmVBdXRoIGFzIFJlcXVlc3RIYW5kbGVyLFxuICAgIHJlcXVpcmVPcmdJZCgpIGFzIFJlcXVlc3RIYW5kbGVyLFxuICAgIC8vIE1vdW50ZWQgcG9zdC1vcmdJZCAoaWRlbnRpdHkgcG9wdWxhdGVkKSBzbyBrZXllZCBtdXRhdGlvbiByZXRyaWVzIG9uIHRoZVxuICAgIC8vIG5vbi1xdW90YSByb3V0ZXMg4oCUIG5vdGFibHkgdGhlIGNyZWF0ZSBwYXRocywgd2hpY2ggdXNlIHRoaXMgZmFjdG9yeSB0b1xuICAgIC8vIGF2b2lkIHRoZSBjaGVja1F1b3RhIHByZS1mbGlnaHQgcmFjaW5nIHRoZWlyIHJlc2VydmUg4oCUIGFyZSBkZWR1cGxpY2F0ZWQuXG4gICAgaWRlbXBvdGVuY3lNaWRkbGV3YXJlKCkgYXMgUmVxdWVzdEhhbmRsZXIsXG4gICAgd2l0aFRlbmFudENvbnRleHQoKSBhcyBSZXF1ZXN0SGFuZGxlcixcbiAgXTtcbn1cbiJdfQ==
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { requireAuth } from '@pipeline-builder/api-core';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Convenience re-export: api-server consumers usually compose middleware
|
|
4
|
+
// already imported from this package, so `requireAuth` is exposed here too.
|
|
5
|
+
// Other auth middleware (requireFeature, requireSystemAdmin, isSystemAdmin) is
|
|
6
|
+
// consumed directly from api-core where it's defined.
|
|
7
|
+
export { requireAuth } from '@pipeline-builder/api-core';
|
|
8
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWlkZGxld2FyZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hcGkvbWlkZGxld2FyZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLHlFQUF5RTtBQUN6RSw0RUFBNEU7QUFDNUUsK0VBQStFO0FBQy9FLHNEQUFzRDtBQUN0RCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sNEJBQTRCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuLy8gQ29udmVuaWVuY2UgcmUtZXhwb3J0OiBhcGktc2VydmVyIGNvbnN1bWVycyB1c3VhbGx5IGNvbXBvc2UgbWlkZGxld2FyZVxuLy8gYWxyZWFkeSBpbXBvcnRlZCBmcm9tIHRoaXMgcGFja2FnZSwgc28gYHJlcXVpcmVBdXRoYCBpcyBleHBvc2VkIGhlcmUgdG9vLlxuLy8gT3RoZXIgYXV0aCBtaWRkbGV3YXJlIChyZXF1aXJlRmVhdHVyZSwgcmVxdWlyZVN5c3RlbUFkbWluLCBpc1N5c3RlbUFkbWluKSBpc1xuLy8gY29uc3VtZWQgZGlyZWN0bHkgZnJvbSBhcGktY29yZSB3aGVyZSBpdCdzIGRlZmluZWQuXG5leHBvcnQgeyByZXF1aXJlQXV0aCB9IGZyb20gJ0BwaXBlbGluZS1idWlsZGVyL2FwaS1jb3JlJztcbiJdfQ==
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal shape of mongoose accepted by `connectMongo`. Passing the real
|
|
3
|
+
* mongoose object satisfies it; api-server takes mongoose as a parameter
|
|
4
|
+
* (dependency injection) so it doesn't have to declare mongoose as a
|
|
5
|
+
* dependency itself.
|
|
6
|
+
*/
|
|
7
|
+
interface MongooseLike {
|
|
8
|
+
set(option: string, value: unknown): unknown;
|
|
9
|
+
connection: {
|
|
10
|
+
on(event: string, listener: (err?: Error) => void): unknown;
|
|
11
|
+
};
|
|
12
|
+
connect(uri: string, options: {
|
|
13
|
+
maxPoolSize: number;
|
|
14
|
+
minPoolSize: number;
|
|
15
|
+
serverSelectionTimeoutMS: number;
|
|
16
|
+
}): Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
export declare function connectMongo(mongoose: MongooseLike, uri: string): Promise<void>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { createLogger } from '@pipeline-builder/api-core';
|
|
4
|
+
const logger = createLogger('mongo-connect');
|
|
5
|
+
/**
|
|
6
|
+
* Connect to MongoDB with sensible production-ready pool defaults and
|
|
7
|
+
* standard event-handler wiring (error / disconnected / reconnected).
|
|
8
|
+
*
|
|
9
|
+
* Pool sizing is read from env (`MONGO_MAX_POOL`, `MONGO_MIN_POOL`,
|
|
10
|
+
* `MONGO_SERVER_SELECTION_MS`). Without these the driver default of
|
|
11
|
+
* `maxPoolSize=100` lets a burst exhaust Mongo's connection ceiling
|
|
12
|
+
* across replicas. Defaults: 20 max / 2 min / 5s server selection.
|
|
13
|
+
*
|
|
14
|
+
* Pass the service's own mongoose instance — api-server doesn't bundle
|
|
15
|
+
* mongoose itself.
|
|
16
|
+
*/
|
|
17
|
+
let handlersWired = false;
|
|
18
|
+
export async function connectMongo(mongoose, uri) {
|
|
19
|
+
mongoose.set('strictQuery', true);
|
|
20
|
+
// Wire the connection event handlers exactly once. The startup supervisor
|
|
21
|
+
// may call connectMongo repeatedly (retrying until Mongo is reachable);
|
|
22
|
+
// re-registering on every attempt would leak listeners.
|
|
23
|
+
if (!handlersWired) {
|
|
24
|
+
handlersWired = true;
|
|
25
|
+
mongoose.connection.on('error', (err) => {
|
|
26
|
+
logger.error('MongoDB connection error', { error: err?.message });
|
|
27
|
+
});
|
|
28
|
+
mongoose.connection.on('disconnected', () => {
|
|
29
|
+
logger.warn('MongoDB disconnected');
|
|
30
|
+
});
|
|
31
|
+
mongoose.connection.on('reconnected', () => {
|
|
32
|
+
logger.info('MongoDB reconnected');
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const maxPoolSize = parseInt(process.env.MONGO_MAX_POOL || '20', 10);
|
|
36
|
+
const minPoolSize = parseInt(process.env.MONGO_MIN_POOL || '2', 10);
|
|
37
|
+
const serverSelectionTimeoutMS = parseInt(process.env.MONGO_SERVER_SELECTION_MS || '5000', 10);
|
|
38
|
+
await mongoose.connect(uri, { maxPoolSize, minPoolSize, serverSelectionTimeoutMS });
|
|
39
|
+
logger.info('MongoDB connection established', { maxPoolSize, minPoolSize, serverSelectionTimeoutMS });
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibW9uZ28tY29ubmVjdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hcGkvbW9uZ28tY29ubmVjdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUUxRCxNQUFNLE1BQU0sR0FBRyxZQUFZLENBQUMsZUFBZSxDQUFDLENBQUM7QUFvQjdDOzs7Ozs7Ozs7OztHQVdHO0FBQ0gsSUFBSSxhQUFhLEdBQUcsS0FBSyxDQUFDO0FBRTFCLE1BQU0sQ0FBQyxLQUFLLFVBQVUsWUFBWSxDQUFDLFFBQXNCLEVBQUUsR0FBVztJQUNwRSxRQUFRLENBQUMsR0FBRyxDQUFDLGFBQWEsRUFBRSxJQUFJLENBQUMsQ0FBQztJQUVsQywwRUFBMEU7SUFDMUUsd0VBQXdFO0lBQ3hFLHdEQUF3RDtJQUN4RCxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUM7UUFDbkIsYUFBYSxHQUFHLElBQUksQ0FBQztRQUVyQixRQUFRLENBQUMsVUFBVSxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxHQUFHLEVBQUUsRUFBRTtZQUN0QyxNQUFNLENBQUMsS0FBSyxDQUFDLDBCQUEwQixFQUFFLEVBQUUsS0FBSyxFQUFFLEdBQUcsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDO1FBQ3BFLENBQUMsQ0FBQyxDQUFDO1FBRUgsUUFBUSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUMsY0FBYyxFQUFFLEdBQUcsRUFBRTtZQUMxQyxNQUFNLENBQUMsSUFBSSxDQUFDLHNCQUFzQixDQUFDLENBQUM7UUFDdEMsQ0FBQyxDQUFDLENBQUM7UUFFSCxRQUFRLENBQUMsVUFBVSxDQUFDLEVBQUUsQ0FBQyxhQUFhLEVBQUUsR0FBRyxFQUFFO1lBQ3pDLE1BQU0sQ0FBQyxJQUFJLENBQUMscUJBQXFCLENBQUMsQ0FBQztRQUNyQyxDQUFDLENBQUMsQ0FBQztJQUNMLENBQUM7SUFFRCxNQUFNLFdBQVcsR0FBRyxRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLElBQUksSUFBSSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBQ3JFLE1BQU0sV0FBVyxHQUFHLFFBQVEsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsSUFBSSxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEUsTUFBTSx3QkFBd0IsR0FBRyxRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyx5QkFBeUIsSUFBSSxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFFL0YsTUFBTSxRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxFQUFFLFdBQVcsRUFBRSxXQUFXLEVBQUUsd0JBQXdCLEVBQUUsQ0FBQyxDQUFDO0lBQ3BGLE1BQU0sQ0FBQyxJQUFJLENBQUMsZ0NBQWdDLEVBQUUsRUFBRSxXQUFXLEVBQUUsV0FBVyxFQUFFLHdCQUF3QixFQUFFLENBQUMsQ0FBQztBQUN4RyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciB9IGZyb20gJ0BwaXBlbGluZS1idWlsZGVyL2FwaS1jb3JlJztcblxuY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdtb25nby1jb25uZWN0Jyk7XG5cbi8qKlxuICogTWluaW1hbCBzaGFwZSBvZiBtb25nb29zZSBhY2NlcHRlZCBieSBgY29ubmVjdE1vbmdvYC4gUGFzc2luZyB0aGUgcmVhbFxuICogbW9uZ29vc2Ugb2JqZWN0IHNhdGlzZmllcyBpdDsgYXBpLXNlcnZlciB0YWtlcyBtb25nb29zZSBhcyBhIHBhcmFtZXRlclxuICogKGRlcGVuZGVuY3kgaW5qZWN0aW9uKSBzbyBpdCBkb2Vzbid0IGhhdmUgdG8gZGVjbGFyZSBtb25nb29zZSBhcyBhXG4gKiBkZXBlbmRlbmN5IGl0c2VsZi5cbiAqL1xuaW50ZXJmYWNlIE1vbmdvb3NlTGlrZSB7XG4gIHNldChvcHRpb246IHN0cmluZywgdmFsdWU6IHVua25vd24pOiB1bmtub3duO1xuICBjb25uZWN0aW9uOiB7XG4gICAgb24oZXZlbnQ6IHN0cmluZywgbGlzdGVuZXI6IChlcnI/OiBFcnJvcikgPT4gdm9pZCk6IHVua25vd247XG4gIH07XG4gIGNvbm5lY3QodXJpOiBzdHJpbmcsIG9wdGlvbnM6IHtcbiAgICBtYXhQb29sU2l6ZTogbnVtYmVyO1xuICAgIG1pblBvb2xTaXplOiBudW1iZXI7XG4gICAgc2VydmVyU2VsZWN0aW9uVGltZW91dE1TOiBudW1iZXI7XG4gIH0pOiBQcm9taXNlPHVua25vd24+O1xufVxuXG4vKipcbiAqIENvbm5lY3QgdG8gTW9uZ29EQiB3aXRoIHNlbnNpYmxlIHByb2R1Y3Rpb24tcmVhZHkgcG9vbCBkZWZhdWx0cyBhbmRcbiAqIHN0YW5kYXJkIGV2ZW50LWhhbmRsZXIgd2lyaW5nIChlcnJvciAvIGRpc2Nvbm5lY3RlZCAvIHJlY29ubmVjdGVkKS5cbiAqXG4gKiBQb29sIHNpemluZyBpcyByZWFkIGZyb20gZW52IChgTU9OR09fTUFYX1BPT0xgLCBgTU9OR09fTUlOX1BPT0xgLFxuICogYE1PTkdPX1NFUlZFUl9TRUxFQ1RJT05fTVNgKS4gV2l0aG91dCB0aGVzZSB0aGUgZHJpdmVyIGRlZmF1bHQgb2ZcbiAqIGBtYXhQb29sU2l6ZT0xMDBgIGxldHMgYSBidXJzdCBleGhhdXN0IE1vbmdvJ3MgY29ubmVjdGlvbiBjZWlsaW5nXG4gKiBhY3Jvc3MgcmVwbGljYXMuIERlZmF1bHRzOiAyMCBtYXggLyAyIG1pbiAvIDVzIHNlcnZlciBzZWxlY3Rpb24uXG4gKlxuICogUGFzcyB0aGUgc2VydmljZSdzIG93biBtb25nb29zZSBpbnN0YW5jZSDigJQgYXBpLXNlcnZlciBkb2Vzbid0IGJ1bmRsZVxuICogbW9uZ29vc2UgaXRzZWxmLlxuICovXG5sZXQgaGFuZGxlcnNXaXJlZCA9IGZhbHNlO1xuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gY29ubmVjdE1vbmdvKG1vbmdvb3NlOiBNb25nb29zZUxpa2UsIHVyaTogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gIG1vbmdvb3NlLnNldCgnc3RyaWN0UXVlcnknLCB0cnVlKTtcblxuICAvLyBXaXJlIHRoZSBjb25uZWN0aW9uIGV2ZW50IGhhbmRsZXJzIGV4YWN0bHkgb25jZS4gVGhlIHN0YXJ0dXAgc3VwZXJ2aXNvclxuICAvLyBtYXkgY2FsbCBjb25uZWN0TW9uZ28gcmVwZWF0ZWRseSAocmV0cnlpbmcgdW50aWwgTW9uZ28gaXMgcmVhY2hhYmxlKTtcbiAgLy8gcmUtcmVnaXN0ZXJpbmcgb24gZXZlcnkgYXR0ZW1wdCB3b3VsZCBsZWFrIGxpc3RlbmVycy5cbiAgaWYgKCFoYW5kbGVyc1dpcmVkKSB7XG4gICAgaGFuZGxlcnNXaXJlZCA9IHRydWU7XG5cbiAgICBtb25nb29zZS5jb25uZWN0aW9uLm9uKCdlcnJvcicsIChlcnIpID0+IHtcbiAgICAgIGxvZ2dlci5lcnJvcignTW9uZ29EQiBjb25uZWN0aW9uIGVycm9yJywgeyBlcnJvcjogZXJyPy5tZXNzYWdlIH0pO1xuICAgIH0pO1xuXG4gICAgbW9uZ29vc2UuY29ubmVjdGlvbi5vbignZGlzY29ubmVjdGVkJywgKCkgPT4ge1xuICAgICAgbG9nZ2VyLndhcm4oJ01vbmdvREIgZGlzY29ubmVjdGVkJyk7XG4gICAgfSk7XG5cbiAgICBtb25nb29zZS5jb25uZWN0aW9uLm9uKCdyZWNvbm5lY3RlZCcsICgpID0+IHtcbiAgICAgIGxvZ2dlci5pbmZvKCdNb25nb0RCIHJlY29ubmVjdGVkJyk7XG4gICAgfSk7XG4gIH1cblxuICBjb25zdCBtYXhQb29sU2l6ZSA9IHBhcnNlSW50KHByb2Nlc3MuZW52Lk1PTkdPX01BWF9QT09MIHx8ICcyMCcsIDEwKTtcbiAgY29uc3QgbWluUG9vbFNpemUgPSBwYXJzZUludChwcm9jZXNzLmVudi5NT05HT19NSU5fUE9PTCB8fCAnMicsIDEwKTtcbiAgY29uc3Qgc2VydmVyU2VsZWN0aW9uVGltZW91dE1TID0gcGFyc2VJbnQocHJvY2Vzcy5lbnYuTU9OR09fU0VSVkVSX1NFTEVDVElPTl9NUyB8fCAnNTAwMCcsIDEwKTtcblxuICBhd2FpdCBtb25nb29zZS5jb25uZWN0KHVyaSwgeyBtYXhQb29sU2l6ZSwgbWluUG9vbFNpemUsIHNlcnZlclNlbGVjdGlvblRpbWVvdXRNUyB9KTtcbiAgbG9nZ2VyLmluZm8oJ01vbmdvREIgY29ubmVjdGlvbiBlc3RhYmxpc2hlZCcsIHsgbWF4UG9vbFNpemUsIG1pblBvb2xTaXplLCBzZXJ2ZXJTZWxlY3Rpb25UaW1lb3V0TVMgfSk7XG59XG4iXX0=
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type QuotaService, type QuotaType } from '@pipeline-builder/api-core';
|
|
2
|
+
import type { Request } from 'express';
|
|
3
|
+
import type { RequestContext } from './request-types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Increment a quota counter using values pulled from a route context.
|
|
6
|
+
*
|
|
7
|
+
* Wraps `incrementQuota(quotaService, orgId, type, authHeader, logWarn)` so
|
|
8
|
+
* route handlers don't have to re-derive `req.headers.authorization` and
|
|
9
|
+
* `ctx.log.bind(null, 'WARN')` at every call site.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* router.get('/', withRoute(async ({ req, res, ctx, orgId }) => {
|
|
14
|
+
* // ...
|
|
15
|
+
* incrementQuotaFromCtx(quotaService, { req, ctx, orgId }, 'apiCalls');
|
|
16
|
+
* }));
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function incrementQuotaFromCtx(quotaService: QuotaService, rc: {
|
|
20
|
+
req: Request;
|
|
21
|
+
ctx: RequestContext;
|
|
22
|
+
orgId: string;
|
|
23
|
+
}, type: QuotaType): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { incrementQuota } from '@pipeline-builder/api-core';
|
|
4
|
+
/**
|
|
5
|
+
* Increment a quota counter using values pulled from a route context.
|
|
6
|
+
*
|
|
7
|
+
* Wraps `incrementQuota(quotaService, orgId, type, authHeader, logWarn)` so
|
|
8
|
+
* route handlers don't have to re-derive `req.headers.authorization` and
|
|
9
|
+
* `ctx.log.bind(null, 'WARN')` at every call site.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* router.get('/', withRoute(async ({ req, res, ctx, orgId }) => {
|
|
14
|
+
* // ...
|
|
15
|
+
* incrementQuotaFromCtx(quotaService, { req, ctx, orgId }, 'apiCalls');
|
|
16
|
+
* }));
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function incrementQuotaFromCtx(quotaService, rc, type) {
|
|
20
|
+
incrementQuota(quotaService, rc.orgId, type, rc.req.headers.authorization || '', rc.ctx.log.bind(null, 'WARN'));
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicXVvdGEtaGVscGVycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hcGkvcXVvdGEtaGVscGVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLE9BQU8sRUFBRSxjQUFjLEVBQXFDLE1BQU0sNEJBQTRCLENBQUM7QUFJL0Y7Ozs7Ozs7Ozs7Ozs7O0dBY0c7QUFDSCxNQUFNLFVBQVUscUJBQXFCLENBQ25DLFlBQTBCLEVBQzFCLEVBQXdELEVBQ3hELElBQWU7SUFFZixjQUFjLENBQ1osWUFBWSxFQUNaLEVBQUUsQ0FBQyxLQUFLLEVBQ1IsSUFBSSxFQUNKLEVBQUUsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLGFBQWEsSUFBSSxFQUFFLEVBQ2xDLEVBQUUsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsTUFBTSxDQUFDLENBQzlCLENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IGluY3JlbWVudFF1b3RhLCB0eXBlIFF1b3RhU2VydmljZSwgdHlwZSBRdW90YVR5cGUgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgdHlwZSB7IFJlcXVlc3QgfSBmcm9tICdleHByZXNzJztcbmltcG9ydCB0eXBlIHsgUmVxdWVzdENvbnRleHQgfSBmcm9tICcuL3JlcXVlc3QtdHlwZXMuanMnO1xuXG4vKipcbiAqIEluY3JlbWVudCBhIHF1b3RhIGNvdW50ZXIgdXNpbmcgdmFsdWVzIHB1bGxlZCBmcm9tIGEgcm91dGUgY29udGV4dC5cbiAqXG4gKiBXcmFwcyBgaW5jcmVtZW50UXVvdGEocXVvdGFTZXJ2aWNlLCBvcmdJZCwgdHlwZSwgYXV0aEhlYWRlciwgbG9nV2FybilgIHNvXG4gKiByb3V0ZSBoYW5kbGVycyBkb24ndCBoYXZlIHRvIHJlLWRlcml2ZSBgcmVxLmhlYWRlcnMuYXV0aG9yaXphdGlvbmAgYW5kXG4gKiBgY3R4LmxvZy5iaW5kKG51bGwsICdXQVJOJylgIGF0IGV2ZXJ5IGNhbGwgc2l0ZS5cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogcm91dGVyLmdldCgnLycsIHdpdGhSb3V0ZShhc3luYyAoeyByZXEsIHJlcywgY3R4LCBvcmdJZCB9KSA9PiB7XG4gKiAgIC8vIC4uLlxuICogICBpbmNyZW1lbnRRdW90YUZyb21DdHgocXVvdGFTZXJ2aWNlLCB7IHJlcSwgY3R4LCBvcmdJZCB9LCAnYXBpQ2FsbHMnKTtcbiAqIH0pKTtcbiAqIGBgYFxuICovXG5leHBvcnQgZnVuY3Rpb24gaW5jcmVtZW50UXVvdGFGcm9tQ3R4KFxuICBxdW90YVNlcnZpY2U6IFF1b3RhU2VydmljZSxcbiAgcmM6IHsgcmVxOiBSZXF1ZXN0OyBjdHg6IFJlcXVlc3RDb250ZXh0OyBvcmdJZDogc3RyaW5nIH0sXG4gIHR5cGU6IFF1b3RhVHlwZSxcbik6IHZvaWQge1xuICBpbmNyZW1lbnRRdW90YShcbiAgICBxdW90YVNlcnZpY2UsXG4gICAgcmMub3JnSWQsXG4gICAgdHlwZSxcbiAgICByYy5yZXEuaGVhZGVycy5hdXRob3JpemF0aW9uIHx8ICcnLFxuICAgIHJjLmN0eC5sb2cuYmluZChudWxsLCAnV0FSTicpLFxuICApO1xufVxuIl19
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Config for a {@link rateLimitByOrg} limiter. */
|
|
2
|
+
export interface OrgRateLimitOptions {
|
|
3
|
+
/** Unique name — namespaces the Redis bucket + labels logs. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Max requests per window per org (or per client IP when unauthenticated). */
|
|
6
|
+
max: number;
|
|
7
|
+
/** Window length in ms. */
|
|
8
|
+
windowMs: number;
|
|
9
|
+
/** Optional 429 message override. */
|
|
10
|
+
message?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Per-ORGANIZATION rate limiter for a specific hot/expensive route, keyed on the
|
|
14
|
+
* VERIFIED auth org — the complement to the coarse global per-IP limiter in
|
|
15
|
+
* `createApp` (which a whole org behind one NAT shares, and a distributed
|
|
16
|
+
* attacker spreads across IP buckets to evade).
|
|
17
|
+
*
|
|
18
|
+
* MUST be mounted AFTER `requireAuth` so `req.user.organizationId` is populated;
|
|
19
|
+
* an unauthenticated caller falls back to a client-IP bucket (never a spoofable
|
|
20
|
+
* header) so it stays bounded. Verified internal service principals are exempt
|
|
21
|
+
* (they carry a signed service JWT). Uses the shared env Redis store
|
|
22
|
+
* (cross-replica, namespaced by `name`) when configured, else in-memory.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* router.post('/', ...createAuthenticatedWithOrgRoute(), requirePermission('messages:write'),
|
|
26
|
+
* rateLimitByOrg({ name: 'message-send', max: 60, windowMs: 60_000 }),
|
|
27
|
+
* withRoute(handler));
|
|
28
|
+
*/
|
|
29
|
+
export declare function rateLimitByOrg(opts: OrgRateLimitOptions): import("express-rate-limit").RateLimitRequestHandler;
|