@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.
Files changed (61) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +79 -0
  3. package/lib/api/app-factory.d.ts +95 -0
  4. package/lib/api/app-factory.js +328 -0
  5. package/lib/api/check-quota.d.ts +13 -0
  6. package/lib/api/check-quota.js +74 -0
  7. package/lib/api/context-middleware.d.ts +34 -0
  8. package/lib/api/context-middleware.js +42 -0
  9. package/lib/api/etag-middleware.d.ts +7 -0
  10. package/lib/api/etag-middleware.js +44 -0
  11. package/lib/api/get-context.d.ts +22 -0
  12. package/lib/api/get-context.js +28 -0
  13. package/lib/api/health-checks.d.ts +62 -0
  14. package/lib/api/health-checks.js +87 -0
  15. package/lib/api/idempotency-middleware.d.ts +80 -0
  16. package/lib/api/idempotency-middleware.js +321 -0
  17. package/lib/api/index.d.ts +20 -0
  18. package/lib/api/index.js +34 -0
  19. package/lib/api/meter-quota.d.ts +32 -0
  20. package/lib/api/meter-quota.js +70 -0
  21. package/lib/api/metrics.d.ts +47 -0
  22. package/lib/api/metrics.js +221 -0
  23. package/lib/api/middleware-factory.d.ts +56 -0
  24. package/lib/api/middleware-factory.js +80 -0
  25. package/lib/api/middleware.d.ts +1 -0
  26. package/lib/api/middleware.js +8 -0
  27. package/lib/api/mongo-connect.d.ts +19 -0
  28. package/lib/api/mongo-connect.js +41 -0
  29. package/lib/api/quota-helpers.d.ts +23 -0
  30. package/lib/api/quota-helpers.js +22 -0
  31. package/lib/api/rate-limit-by-org.d.ts +29 -0
  32. package/lib/api/rate-limit-by-org.js +64 -0
  33. package/lib/api/readiness.d.ts +29 -0
  34. package/lib/api/readiness.js +65 -0
  35. package/lib/api/request-types.d.ts +55 -0
  36. package/lib/api/request-types.js +65 -0
  37. package/lib/api/require-org-id.d.ts +14 -0
  38. package/lib/api/require-org-id.js +28 -0
  39. package/lib/api/route-wrapper.d.ts +50 -0
  40. package/lib/api/route-wrapper.js +84 -0
  41. package/lib/api/server.d.ts +79 -0
  42. package/lib/api/server.js +234 -0
  43. package/lib/api/tenant-context.d.ts +28 -0
  44. package/lib/api/tenant-context.js +48 -0
  45. package/lib/api/tracing.d.ts +25 -0
  46. package/lib/api/tracing.js +69 -0
  47. package/lib/http/index.d.ts +4 -0
  48. package/lib/http/index.js +7 -0
  49. package/lib/http/sse-connection-manager.d.ts +357 -0
  50. package/lib/http/sse-connection-manager.js +665 -0
  51. package/lib/http/sse-relay.d.ts +63 -0
  52. package/lib/http/sse-relay.js +70 -0
  53. package/lib/http/sse-ticket-channel.d.ts +31 -0
  54. package/lib/http/sse-ticket-channel.js +52 -0
  55. package/lib/http/sse-ticket-store.d.ts +75 -0
  56. package/lib/http/sse-ticket-store.js +160 -0
  57. package/lib/index.d.ts +30 -0
  58. package/lib/index.js +35 -0
  59. package/lib/otel-bootstrap.d.ts +1 -0
  60. package/lib/otel-bootstrap.js +75 -0
  61. 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;