@pipeline-builder/api-core 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 +98 -0
- package/lib/constants/ai-providers.d.ts +52 -0
- package/lib/constants/ai-providers.js +129 -0
- package/lib/constants/http-status.d.ts +20 -0
- package/lib/constants/http-status.js +26 -0
- package/lib/constants/index.d.ts +3 -0
- package/lib/constants/index.js +6 -0
- package/lib/constants/time.d.ts +10 -0
- package/lib/constants/time.js +13 -0
- package/lib/errors/app-errors.d.ts +30 -0
- package/lib/errors/app-errors.js +53 -0
- package/lib/errors/index.d.ts +1 -0
- package/lib/errors/index.js +4 -0
- package/lib/helpers/access-helpers.d.ts +63 -0
- package/lib/helpers/access-helpers.js +82 -0
- package/lib/helpers/crud-helpers.d.ts +9 -0
- package/lib/helpers/crud-helpers.js +23 -0
- package/lib/helpers/index.d.ts +6 -0
- package/lib/helpers/index.js +9 -0
- package/lib/helpers/org-hierarchy-http.d.ts +74 -0
- package/lib/helpers/org-hierarchy-http.js +144 -0
- package/lib/helpers/org-hierarchy.d.ts +57 -0
- package/lib/helpers/org-hierarchy.js +110 -0
- package/lib/helpers/restore-helpers.d.ts +49 -0
- package/lib/helpers/restore-helpers.js +78 -0
- package/lib/helpers/sse-helpers.d.ts +20 -0
- package/lib/helpers/sse-helpers.js +45 -0
- package/lib/index.d.ts +58 -0
- package/lib/index.js +71 -0
- package/lib/middleware/auth.d.ts +208 -0
- package/lib/middleware/auth.js +588 -0
- package/lib/middleware/index.d.ts +3 -0
- package/lib/middleware/index.js +6 -0
- package/lib/middleware/mongo-sanitize.d.ts +32 -0
- package/lib/middleware/mongo-sanitize.js +100 -0
- package/lib/middleware/step-up.d.ts +45 -0
- package/lib/middleware/step-up.js +138 -0
- package/lib/openapi/extend-zod.d.ts +1 -0
- package/lib/openapi/extend-zod.js +6 -0
- package/lib/openapi/index.d.ts +2 -0
- package/lib/openapi/index.js +5 -0
- package/lib/openapi/registry.d.ts +17 -0
- package/lib/openapi/registry.js +38 -0
- package/lib/openapi/routes/ask-routes.d.ts +7 -0
- package/lib/openapi/routes/ask-routes.js +62 -0
- package/lib/openapi/routes/billing-routes.d.ts +1 -0
- package/lib/openapi/routes/billing-routes.js +66 -0
- package/lib/openapi/routes/image-registry-routes.d.ts +7 -0
- package/lib/openapi/routes/image-registry-routes.js +98 -0
- package/lib/openapi/routes/index.d.ts +5 -0
- package/lib/openapi/routes/index.js +28 -0
- package/lib/openapi/routes/message-routes.d.ts +1 -0
- package/lib/openapi/routes/message-routes.js +105 -0
- package/lib/openapi/routes/pipeline-routes.d.ts +1 -0
- package/lib/openapi/routes/pipeline-routes.js +105 -0
- package/lib/openapi/routes/pipeline-template-routes.d.ts +1 -0
- package/lib/openapi/routes/pipeline-template-routes.js +61 -0
- package/lib/openapi/routes/plugin-routes.d.ts +1 -0
- package/lib/openapi/routes/plugin-routes.js +96 -0
- package/lib/openapi/routes/quota-routes.d.ts +1 -0
- package/lib/openapi/routes/quota-routes.js +62 -0
- package/lib/openapi/schema-registry.d.ts +25 -0
- package/lib/openapi/schema-registry.js +91 -0
- package/lib/routes/health.d.ts +41 -0
- package/lib/routes/health.js +82 -0
- package/lib/routes/index.d.ts +1 -0
- package/lib/routes/index.js +4 -0
- package/lib/services/audit-spool.d.ts +80 -0
- package/lib/services/audit-spool.js +172 -0
- package/lib/services/cache-service.d.ts +124 -0
- package/lib/services/cache-service.js +276 -0
- package/lib/services/circuit-breaker.d.ts +45 -0
- package/lib/services/circuit-breaker.js +138 -0
- package/lib/services/compliance-client.d.ts +46 -0
- package/lib/services/compliance-client.js +128 -0
- package/lib/services/compliance-event-subscriber.d.ts +16 -0
- package/lib/services/compliance-event-subscriber.js +72 -0
- package/lib/services/durable-event-bus.d.ts +96 -0
- package/lib/services/durable-event-bus.js +204 -0
- package/lib/services/entity-events.d.ts +48 -0
- package/lib/services/entity-events.js +79 -0
- package/lib/services/env-redis.d.ts +44 -0
- package/lib/services/env-redis.js +94 -0
- package/lib/services/http-client.d.ts +143 -0
- package/lib/services/http-client.js +335 -0
- package/lib/services/index.d.ts +18 -0
- package/lib/services/index.js +20 -0
- package/lib/services/leader-lock.d.ts +45 -0
- package/lib/services/leader-lock.js +85 -0
- package/lib/services/quota.d.ts +130 -0
- package/lib/services/quota.js +264 -0
- package/lib/services/remote-audit-client.d.ts +160 -0
- package/lib/services/remote-audit-client.js +390 -0
- package/lib/services/retry-strategy.d.ts +74 -0
- package/lib/services/retry-strategy.js +118 -0
- package/lib/services/scheduler.d.ts +41 -0
- package/lib/services/scheduler.js +86 -0
- package/lib/services/service-boot.d.ts +20 -0
- package/lib/services/service-boot.js +28 -0
- package/lib/services/sse-ticket-store.d.ts +51 -0
- package/lib/services/sse-ticket-store.js +130 -0
- package/lib/services/token-revocation.d.ts +52 -0
- package/lib/services/token-revocation.js +120 -0
- package/lib/testing/tier-mock.d.ts +44 -0
- package/lib/testing/tier-mock.js +65 -0
- package/lib/types/audit-events.d.ts +69 -0
- package/lib/types/audit-events.js +4 -0
- package/lib/types/catalog-metadata.d.ts +36 -0
- package/lib/types/catalog-metadata.js +21 -0
- package/lib/types/common.d.ts +255 -0
- package/lib/types/common.js +41 -0
- package/lib/types/error-codes.d.ts +54 -0
- package/lib/types/error-codes.js +107 -0
- package/lib/types/feature-flags.d.ts +36 -0
- package/lib/types/feature-flags.js +128 -0
- package/lib/types/http.d.ts +27 -0
- package/lib/types/http.js +4 -0
- package/lib/types/index.d.ts +11 -0
- package/lib/types/index.js +14 -0
- package/lib/types/permissions.d.ts +65 -0
- package/lib/types/permissions.js +131 -0
- package/lib/types/pipeline-template.d.ts +30 -0
- package/lib/types/pipeline-template.js +13 -0
- package/lib/types/pipeline.d.ts +59 -0
- package/lib/types/pipeline.js +31 -0
- package/lib/types/quota-tiers.d.ts +79 -0
- package/lib/types/quota-tiers.js +221 -0
- package/lib/types/visibility.d.ts +20 -0
- package/lib/types/visibility.js +22 -0
- package/lib/utils/alias-resolver.d.ts +37 -0
- package/lib/utils/alias-resolver.js +68 -0
- package/lib/utils/audit.d.ts +15 -0
- package/lib/utils/audit.js +39 -0
- package/lib/utils/aws-scrub.d.ts +11 -0
- package/lib/utils/aws-scrub.js +67 -0
- package/lib/utils/compliance-attributes.d.ts +6 -0
- package/lib/utils/compliance-attributes.js +69 -0
- package/lib/utils/concurrency.d.ts +9 -0
- package/lib/utils/concurrency.js +29 -0
- package/lib/utils/crash-handlers.d.ts +18 -0
- package/lib/utils/crash-handlers.js +36 -0
- package/lib/utils/env.d.ts +28 -0
- package/lib/utils/env.js +54 -0
- package/lib/utils/headers.d.ts +18 -0
- package/lib/utils/headers.js +21 -0
- package/lib/utils/identity.d.ts +40 -0
- package/lib/utils/identity.js +51 -0
- package/lib/utils/index.d.ts +17 -0
- package/lib/utils/index.js +20 -0
- package/lib/utils/logger.d.ts +38 -0
- package/lib/utils/logger.js +174 -0
- package/lib/utils/metric-emitter.d.ts +36 -0
- package/lib/utils/metric-emitter.js +32 -0
- package/lib/utils/object.d.ts +13 -0
- package/lib/utils/object.js +18 -0
- package/lib/utils/params.d.ts +150 -0
- package/lib/utils/params.js +206 -0
- package/lib/utils/response.d.ts +117 -0
- package/lib/utils/response.js +223 -0
- package/lib/utils/safe-require.d.ts +13 -0
- package/lib/utils/safe-require.js +19 -0
- package/lib/utils/secret-encryption.d.ts +176 -0
- package/lib/utils/secret-encryption.js +374 -0
- package/lib/utils/ssrf.d.ts +30 -0
- package/lib/utils/ssrf.js +106 -0
- package/lib/validation/ai-schemas.d.ts +56 -0
- package/lib/validation/ai-schemas.js +78 -0
- package/lib/validation/common-schemas.d.ts +133 -0
- package/lib/validation/common-schemas.js +94 -0
- package/lib/validation/index.d.ts +7 -0
- package/lib/validation/index.js +10 -0
- package/lib/validation/message-schemas.d.ts +160 -0
- package/lib/validation/message-schemas.js +156 -0
- package/lib/validation/middleware.d.ts +72 -0
- package/lib/validation/middleware.js +65 -0
- package/lib/validation/pipeline-schemas.d.ts +181 -0
- package/lib/validation/pipeline-schemas.js +101 -0
- package/lib/validation/pipeline-template-schemas.d.ts +155 -0
- package/lib/validation/pipeline-template-schemas.js +66 -0
- package/lib/validation/plugin-schemas.d.ts +171 -0
- package/lib/validation/plugin-schemas.js +81 -0
- package/openapi.yaml +292 -0
- package/package.json +167 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import winston from 'winston';
|
|
4
|
+
import { safeCreateRequire } from './safe-require.js';
|
|
5
|
+
const { combine, timestamp, printf, colorize, errors, json } = winston.format;
|
|
6
|
+
// ESM has no global `require`; safeCreateRequire enables the synchronous,
|
|
7
|
+
// OPTIONAL lazy-load of @opentelemetry/api in getCurrentTraceId() below — a
|
|
8
|
+
// winston format callback must stay synchronous, so a dynamic import() won't
|
|
9
|
+
// work there. (CJS-bundle safe — see safe-require.ts.)
|
|
10
|
+
const require = safeCreateRequire(import.meta.url);
|
|
11
|
+
/**
|
|
12
|
+
* Custom log format for human-readable console output.
|
|
13
|
+
*/
|
|
14
|
+
const consoleFormat = printf(({ level, message, timestamp, service, ...meta }) => {
|
|
15
|
+
const serviceName = service ? `[${service}]` : '';
|
|
16
|
+
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
|
|
17
|
+
return `${timestamp} ${level} ${serviceName} ${message}${metaStr}`;
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Lazy lookup of OpenTelemetry trace ID. Returns undefined when tracing
|
|
21
|
+
* isn't initialized or @opentelemetry/api isn't installed — never throws.
|
|
22
|
+
* Cheap: a single property read after the first successful lookup.
|
|
23
|
+
*/
|
|
24
|
+
let _otelApi;
|
|
25
|
+
function getCurrentTraceId() {
|
|
26
|
+
if (_otelApi === undefined) {
|
|
27
|
+
try {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
|
+
_otelApi = require('@opentelemetry/api');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
_otelApi = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!_otelApi)
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
return _otelApi.trace?.getActiveSpan()?.spanContext().traceId;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Winston format that stamps `trace_id` on every entry when an OTel span is active. */
|
|
45
|
+
const traceIdFormat = winston.format((info) => {
|
|
46
|
+
const traceId = getCurrentTraceId();
|
|
47
|
+
if (traceId)
|
|
48
|
+
info.trace_id = traceId;
|
|
49
|
+
return info;
|
|
50
|
+
})();
|
|
51
|
+
/**
|
|
52
|
+
* Keys that should never appear in logs. Match is case-insensitive and
|
|
53
|
+
* anchored substring — `Authorization`, `auth_header`, `MY_PASSWORD` all hit.
|
|
54
|
+
* Add a new term here only if you've seen real-world leakage; over-redaction
|
|
55
|
+
* makes incident debugging harder.
|
|
56
|
+
*/
|
|
57
|
+
const SENSITIVE_KEY_PATTERN = /password|secret|bearer|api[_-]?key|cookie|token|^auth(orization|_header)?$|stripe[_-]?(key|secret)|mongo(db)?[_-]?uri/i;
|
|
58
|
+
const REDACTED = '[REDACTED]';
|
|
59
|
+
function redactDeep(value, depth = 0) {
|
|
60
|
+
// Cap depth so a malicious / pathological circular object can't lock the logger.
|
|
61
|
+
// Degrade to a placeholder rather than the raw subtree: returning `value` here
|
|
62
|
+
// would log a secret-keyed field nested deeper than the cap in cleartext.
|
|
63
|
+
if (depth > 6)
|
|
64
|
+
return '[TRUNCATED]';
|
|
65
|
+
if (value == null || typeof value !== 'object')
|
|
66
|
+
return value;
|
|
67
|
+
if (Array.isArray(value))
|
|
68
|
+
return value.map((v) => redactDeep(v, depth + 1));
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const [k, v] of Object.entries(value)) {
|
|
71
|
+
if (SENSITIVE_KEY_PATTERN.test(k)) {
|
|
72
|
+
out[k] = REDACTED;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
out[k] = redactDeep(v, depth + 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Redact sensitive values from an ARBITRARY payload using the same key-pattern
|
|
82
|
+
* rules the Winston `redactFormat` applies to log metadata. Use this for
|
|
83
|
+
* payloads that reach an output channel OUTSIDE the logger — e.g. an SSE frame
|
|
84
|
+
* pushed straight to a client — so a `password`/`token`/`secret` field is
|
|
85
|
+
* masked there too. A top-level sensitive key is masked; nested objects/arrays
|
|
86
|
+
* are walked (depth-capped) exactly as in the logger. Returns a redacted COPY;
|
|
87
|
+
* primitives pass through unchanged.
|
|
88
|
+
*/
|
|
89
|
+
export function redactSensitive(value) {
|
|
90
|
+
if (value == null || typeof value !== 'object')
|
|
91
|
+
return value;
|
|
92
|
+
return redactDeep(value);
|
|
93
|
+
}
|
|
94
|
+
/** Winston format that masks values for sensitive-looking keys (PII / secrets).
|
|
95
|
+
* Mutates `info` in place so winston's internal Symbol-keyed properties
|
|
96
|
+
* (`Symbol.for('level')`, `Symbol.for('message')`, `Symbol.for('splat')`) survive
|
|
97
|
+
* — rebuilding a fresh object via `Object.entries`/spread silently strips them,
|
|
98
|
+
* which makes the Console transport drop every entry without warning. */
|
|
99
|
+
const PRESERVED_KEYS = new Set(['level', 'message', 'timestamp', 'service', 'trace_id']);
|
|
100
|
+
const redactFormat = winston.format((info) => {
|
|
101
|
+
for (const key of Object.keys(info)) {
|
|
102
|
+
if (PRESERVED_KEYS.has(key))
|
|
103
|
+
continue;
|
|
104
|
+
if (SENSITIVE_KEY_PATTERN.test(key)) {
|
|
105
|
+
info[key] = REDACTED;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
info[key] = redactDeep(info[key]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return info;
|
|
112
|
+
})();
|
|
113
|
+
/**
|
|
114
|
+
* Create a logger instance for a service.
|
|
115
|
+
*
|
|
116
|
+
* When LOG_FORMAT=json (default), outputs structured JSON for Loki ingestion:
|
|
117
|
+
* {"level":"info","message":"Server started","service":"pipeline","timestamp":"..."}
|
|
118
|
+
*
|
|
119
|
+
* When LOG_FORMAT=text, outputs colorized human-readable format:
|
|
120
|
+
* 2026-02-13T10:30:00.000Z info [pipeline] Server started
|
|
121
|
+
*
|
|
122
|
+
* @param serviceName - Name of the service for log identification
|
|
123
|
+
* @returns Configured Winston logger instance
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* import { createLogger } from '@pipeline-builder/api-core';
|
|
128
|
+
*
|
|
129
|
+
* const logger = createLogger('get-plugin');
|
|
130
|
+
* logger.info('Server started', { port: 3000 });
|
|
131
|
+
* logger.error('Database error', { error: err.message });
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export function createLogger(serviceName) {
|
|
135
|
+
const logLevel = process.env.LOG_LEVEL || 'info';
|
|
136
|
+
const logFormat = process.env.LOG_FORMAT || 'json';
|
|
137
|
+
if (logFormat !== 'json' && logFormat !== 'text') {
|
|
138
|
+
// eslint-disable-next-line no-console -- startup warning before logger is available
|
|
139
|
+
console.warn(`Invalid LOG_FORMAT="${logFormat}", expected "json" or "text". Defaulting to "json".`);
|
|
140
|
+
}
|
|
141
|
+
const useJson = logFormat !== 'text';
|
|
142
|
+
// Order matters: redact BEFORE serialization so masked keys never reach
|
|
143
|
+
// the output. trace_id is stamped first so it survives redaction.
|
|
144
|
+
const baseFormats = [
|
|
145
|
+
errors({ stack: true }),
|
|
146
|
+
timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }),
|
|
147
|
+
traceIdFormat,
|
|
148
|
+
redactFormat,
|
|
149
|
+
];
|
|
150
|
+
if (useJson) {
|
|
151
|
+
return winston.createLogger({
|
|
152
|
+
level: logLevel,
|
|
153
|
+
defaultMeta: { service: serviceName },
|
|
154
|
+
format: combine(...baseFormats, json()),
|
|
155
|
+
transports: [new winston.transports.Console()],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return winston.createLogger({
|
|
159
|
+
level: logLevel,
|
|
160
|
+
defaultMeta: { service: serviceName },
|
|
161
|
+
format: combine(...baseFormats),
|
|
162
|
+
transports: [
|
|
163
|
+
new winston.transports.Console({
|
|
164
|
+
format: combine(colorize(), consoleFormat),
|
|
165
|
+
}),
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Default logger instance (service name from SERVICE_NAME env var or 'api').
|
|
171
|
+
*/
|
|
172
|
+
export const logger = createLogger(process.env.SERVICE_NAME || 'api');
|
|
173
|
+
export default logger;
|
|
174
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9nZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL2xvZ2dlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLE9BQU8sT0FBTyxNQUFNLFNBQVMsQ0FBQztBQUM5QixPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUV0RCxNQUFNLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDO0FBRTlFLDBFQUEwRTtBQUMxRSw0RUFBNEU7QUFDNUUsNkVBQTZFO0FBQzdFLHVEQUF1RDtBQUN2RCxNQUFNLE9BQU8sR0FBRyxpQkFBaUIsQ0FBQyxPQUFPLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQztBQUVuRDs7R0FFRztBQUNILE1BQU0sYUFBYSxHQUFHLE1BQU0sQ0FBQyxDQUFDLEVBQUUsS0FBSyxFQUFFLE9BQU8sRUFBRSxTQUFTLEVBQUUsT0FBTyxFQUFFLEdBQUcsSUFBSSxFQUFFLEVBQUUsRUFBRTtJQUMvRSxNQUFNLFdBQVcsR0FBRyxPQUFPLENBQUMsQ0FBQyxDQUFDLElBQUksT0FBTyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUNsRCxNQUFNLE9BQU8sR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUMzRSxPQUFPLEdBQUcsU0FBUyxJQUFJLEtBQUssSUFBSSxXQUFXLElBQUksT0FBTyxHQUFHLE9BQU8sRUFBRSxDQUFDO0FBQ3JFLENBQUMsQ0FBQyxDQUFDO0FBRUg7Ozs7R0FJRztBQUNILElBQUksUUFBZ0gsQ0FBQztBQUNySCxT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QUFXLEVBQUUsRUFBRSxPQUFPLEVBQUUsV0FBVyxFQUFFO1lBQ3JDLE1BQU0sRUFBRSxPQUFPLENBQUMsR0FBRyxXQUFXLEVBQUUsSUFBSSxFQUFFLENBQUM7WUFDdkMsVUFBVSxFQUFFLENBQUMsSUFBSSxPQUFPLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDO1NBQy9DLENBQUMsQ0FBQztJQUNMLENBQUM7SUFFRCxPQUFPLE9BQU8sQ0FBQyxZQUFZLENBQUM7UUFDMUIsS0FBSyxFQUFFLFFBQVE7UUFDZixXQUFXLEVBQUUsRUFBRSxPQUFPLEVBQUUsV0FBVyxFQUFFO1FBQ3JDLE1BQU0sRUFBRSxPQUFPLENBQUMsR0FBRyxXQUFXLENBQUM7UUFDL0IsVUFBVSxFQUFFO1lBQ1YsSUFBSSxPQUFPLENBQUMsVUFBVSxDQUFDLE9BQU8sQ0FBQztnQkFDN0IsTUFBTSxFQUFFLE9BQU8sQ0FBQyxRQUFRLEVBQUUsRUFBRSxhQUFhLENBQUM7YUFDM0MsQ0FBQztTQUNIO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQztBQUVEOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sTUFBTSxHQUFHLFlBQVksQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLFlBQVksSUFBSSxLQUFLLENBQUMsQ0FBQztBQUV0RSxlQUFlLE1BQU0sQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG5pbXBvcnQgd2luc3RvbiBmcm9tICd3aW5zdG9uJztcbmltcG9ydCB7IHNhZmVDcmVhdGVSZXF1aXJlIH0gZnJvbSAnLi9zYWZlLXJlcXVpcmUuanMnO1xuXG5jb25zdCB7IGNvbWJpbmUsIHRpbWVzdGFtcCwgcHJpbnRmLCBjb2xvcml6ZSwgZXJyb3JzLCBqc29uIH0gPSB3aW5zdG9uLmZvcm1hdDtcblxuLy8gRVNNIGhhcyBubyBnbG9iYWwgYHJlcXVpcmVgOyBzYWZlQ3JlYXRlUmVxdWlyZSBlbmFibGVzIHRoZSBzeW5jaHJvbm91cyxcbi8vIE9QVElPTkFMIGxhenktbG9hZCBvZiBAb3BlbnRlbGVtZXRyeS9hcGkgaW4gZ2V0Q3VycmVudFRyYWNlSWQoKSBiZWxvdyDigJQgYVxuLy8gd2luc3RvbiBmb3JtYXQgY2FsbGJhY2sgbXVzdCBzdGF5IHN5bmNocm9ub3VzLCBzbyBhIGR5bmFtaWMgaW1wb3J0KCkgd29uJ3Rcbi8vIHdvcmsgdGhlcmUuIChDSlMtYnVuZGxlIHNhZmUg4oCUIHNlZSBzYWZlLXJlcXVpcmUudHMuKVxuY29uc3QgcmVxdWlyZSA9IHNhZmVDcmVhdGVSZXF1aXJlKGltcG9ydC5tZXRhLnVybCk7XG5cbi8qKlxuICogQ3VzdG9tIGxvZyBmb3JtYXQgZm9yIGh1bWFuLXJlYWRhYmxlIGNvbnNvbGUgb3V0cHV0LlxuICovXG5jb25zdCBjb25zb2xlRm9ybWF0ID0gcHJpbnRmKCh7IGxldmVsLCBtZXNzYWdlLCB0aW1lc3RhbXAsIHNlcnZpY2UsIC4uLm1ldGEgfSkgPT4ge1xuICBjb25zdCBzZXJ2aWNlTmFtZSA9IHNlcnZpY2UgPyBgWyR7c2VydmljZX1dYCA6ICcnO1xuICBjb25zdCBtZXRhU3RyID0gT2JqZWN0LmtleXMobWV0YSkubGVuZ3RoID8gYCAke0pTT04uc3RyaW5naWZ5KG1ldGEpfWAgOiAnJztcbiAgcmV0dXJuIGAke3RpbWVzdGFtcH0gJHtsZXZlbH0gJHtzZXJ2aWNlTmFtZX0gJHttZXNzYWdlfSR7bWV0YVN0cn1gO1xufSk7XG5cbi8qKlxuICogTGF6eSBsb29rdXAgb2YgT3BlblRlbGVtZXRyeSB0cmFjZSBJRC4gUmV0dXJucyB1bmRlZmluZWQgd2hlbiB0cmFjaW5nXG4gKiBpc24ndCBpbml0aWFsaXplZCBvciBAb3BlbnRlbGVtZXRyeS9hcGkgaXNuJ3QgaW5zdGFsbGVkIOKAlCBuZXZlciB0aHJvd3MuXG4gKiBDaGVhcDogYSBzaW5nbGUgcHJvcGVydHkgcmVhZCBhZnRlciB0aGUgZmlyc3Qgc3VjY2Vzc2Z1bCBsb29rdXAuXG4gKi9cbmxldCBfb3RlbEFwaTogeyB0cmFjZT86IHsgZ2V0QWN0aXZlU3BhbigpOiB7IHNwYW5Db250ZXh0KCk6IHsgdHJhY2VJZDogc3RyaW5nIH0gfSB8IHVuZGVmaW5lZCB9IH0gfCBudWxsIHwgdW5kZWZpbmVkO1xuZnVuY3Rpb24gZ2V0Q3VycmVudFRyYWNlSWQoKTogc3RyaW5nIHwgdW5kZWZpbmVkIHtcbiAgaWYgKF9vdGVsQXBpID09PSB1bmRlZmluZWQpIHtcbiAgICB0cnkge1xuICAgICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIEB0eXBlc2NyaXB0LWVzbGludC9uby1yZXF1aXJlLWltcG9ydHNcbiAgICAgIF9vdGVsQXBpID0gcmVxdWlyZSgnQG9wZW50ZWxlbWV0cnkvYXBpJyk7XG4gICAgfSBjYXRjaCB7XG4gICAgICBfb3RlbEFwaSA9IG51bGw7XG4gICAgfVxuICB9XG4gIGlmICghX290ZWxBcGkpIHJldHVybiB1bmRlZmluZWQ7XG4gIHRyeSB7XG4gICAgcmV0dXJuIF9vdGVsQXBpLnRyYWNlPy5nZXRBY3RpdmVTcGFuKCk/LnNwYW5Db250ZXh0KCkudHJhY2VJZDtcbiAgfSBjYXRjaCB7XG4gICAgcmV0dXJuIHVuZGVmaW5lZDtcbiAgfVxufVxuXG4vKiogV2luc3RvbiBmb3JtYXQgdGhhdCBzdGFtcHMgYHRyYWNlX2lkYCBvbiBldmVyeSBlbnRyeSB3aGVuIGFuIE9UZWwgc3BhbiBpcyBhY3RpdmUuICovXG5jb25zdCB0cmFjZUlkRm9ybWF0ID0gd2luc3Rvbi5mb3JtYXQoKGluZm8pID0+IHtcbiAgY29uc3QgdHJhY2VJZCA9IGdldEN1cnJlbnRUcmFjZUlkKCk7XG4gIGlmICh0cmFjZUlkKSBpbmZvLnRyYWNlX2lkID0gdHJhY2VJZDtcbiAgcmV0dXJuIGluZm87XG59KSgpO1xuXG4vKipcbiAqIEtleXMgdGhhdCBzaG91bGQgbmV2ZXIgYXBwZWFyIGluIGxvZ3MuIE1hdGNoIGlzIGNhc2UtaW5zZW5zaXRpdmUgYW5kXG4gKiBhbmNob3JlZCBzdWJzdHJpbmcg4oCUIGBBdXRob3JpemF0aW9uYCwgYGF1dGhfaGVhZGVyYCwgYE1ZX1BBU1NXT1JEYCBhbGwgaGl0LlxuICogQWRkIGEgbmV3IHRlcm0gaGVyZSBvbmx5IGlmIHlvdSd2ZSBzZWVuIHJlYWwtd29ybGQgbGVha2FnZTsgb3Zlci1yZWRhY3Rpb25cbiAqIG1ha2VzIGluY2lkZW50IGRlYnVnZ2luZyBoYXJkZXIuXG4gKi9cbmNvbnN0IFNFTlNJVElWRV9LRVlfUEFUVEVSTiA9IC9wYXNzd29yZHxzZWNyZXR8YmVhcmVyfGFwaVtfLV0/a2V5fGNvb2tpZXx0b2tlbnxeYXV0aChvcml6YXRpb258X2hlYWRlcik/JHxzdHJpcGVbXy1dPyhrZXl8c2VjcmV0KXxtb25nbyhkYik/W18tXT91cmkvaTtcbmNvbnN0IFJFREFDVEVEID0gJ1tSRURBQ1RFRF0nO1xuXG5mdW5jdGlvbiByZWRhY3REZWVwKHZhbHVlOiB1bmtub3duLCBkZXB0aCA9IDApOiB1bmtub3duIHtcbiAgLy8gQ2FwIGRlcHRoIHNvIGEgbWFsaWNpb3VzIC8gcGF0aG9sb2dpY2FsIGNpcmN1bGFyIG9iamVjdCBjYW4ndCBsb2NrIHRoZSBsb2dnZXIuXG4gIC8vIERlZ3JhZGUgdG8gYSBwbGFjZWhvbGRlciByYXRoZXIgdGhhbiB0aGUgcmF3IHN1YnRyZWU6IHJldHVybmluZyBgdmFsdWVgIGhlcmVcbiAgLy8gd291bGQgbG9nIGEgc2VjcmV0LWtleWVkIGZpZWxkIG5lc3RlZCBkZWVwZXIgdGhhbiB0aGUgY2FwIGluIGNsZWFydGV4dC5cbiAgaWYgKGRlcHRoID4gNikgcmV0dXJuICdbVFJVTkNBVEVEXSc7XG4gIGlmICh2YWx1ZSA9PSBudWxsIHx8IHR5cGVvZiB2YWx1ZSAhPT0gJ29iamVjdCcpIHJldHVybiB2YWx1ZTtcbiAgaWYgKEFycmF5LmlzQXJyYXkodmFsdWUpKSByZXR1cm4gdmFsdWUubWFwKCh2KSA9PiByZWRhY3REZWVwKHYsIGRlcHRoICsgMSkpO1xuICBjb25zdCBvdXQ6IFJlY29yZDxzdHJpbmcsIHVua25vd24+ID0ge307XG4gIGZvciAoY29uc3QgW2ssIHZdIG9mIE9iamVjdC5lbnRyaWVzKHZhbHVlIGFzIFJlY29yZDxzdHJpbmcsIHVua25vd24+KSkge1xuICAgIGlmIChTRU5TSVRJVkVfS0VZX1BBVFRFUk4udGVzdChrKSkge1xuICAgICAgb3V0W2tdID0gUkVEQUNURUQ7XG4gICAgfSBlbHNlIHtcbiAgICAgIG91dFtrXSA9IHJlZGFjdERlZXAodiwgZGVwdGggKyAxKTtcbiAgICB9XG4gIH1cbiAgcmV0dXJuIG91dDtcbn1cblxuLyoqXG4gKiBSZWRhY3Qgc2Vuc2l0aXZlIHZhbHVlcyBmcm9tIGFuIEFSQklUUkFSWSBwYXlsb2FkIHVzaW5nIHRoZSBzYW1lIGtleS1wYXR0ZXJuXG4gKiBydWxlcyB0aGUgV2luc3RvbiBgcmVkYWN0Rm9ybWF0YCBhcHBsaWVzIHRvIGxvZyBtZXRhZGF0YS4gVXNlIHRoaXMgZm9yXG4gKiBwYXlsb2FkcyB0aGF0IHJlYWNoIGFuIG91dHB1dCBjaGFubmVsIE9VVFNJREUgdGhlIGxvZ2dlciDigJQgZS5nLiBhbiBTU0UgZnJhbWVcbiAqIHB1c2hlZCBzdHJhaWdodCB0byBhIGNsaWVudCDigJQgc28gYSBgcGFzc3dvcmRgL2B0b2tlbmAvYHNlY3JldGAgZmllbGQgaXNcbiAqIG1hc2tlZCB0aGVyZSB0b28uIEEgdG9wLWxldmVsIHNlbnNpdGl2ZSBrZXkgaXMgbWFza2VkOyBuZXN0ZWQgb2JqZWN0cy9hcnJheXNcbiAqIGFyZSB3YWxrZWQgKGRlcHRoLWNhcHBlZCkgZXhhY3RseSBhcyBpbiB0aGUgbG9nZ2VyLiBSZXR1cm5zIGEgcmVkYWN0ZWQgQ09QWTtcbiAqIHByaW1pdGl2ZXMgcGFzcyB0aHJvdWdoIHVuY2hhbmdlZC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlZGFjdFNlbnNpdGl2ZSh2YWx1ZTogdW5rbm93bik6IHVua25vd24ge1xuICBpZiAodmFsdWUgPT0gbnVsbCB8fCB0eXBlb2YgdmFsdWUgIT09ICdvYmplY3QnKSByZXR1cm4gdmFsdWU7XG4gIHJldHVybiByZWRhY3REZWVwKHZhbHVlKTtcbn1cblxuLyoqIFdpbnN0b24gZm9ybWF0IHRoYXQgbWFza3MgdmFsdWVzIGZvciBzZW5zaXRpdmUtbG9va2luZyBrZXlzIChQSUkgLyBzZWNyZXRzKS5cbiAqIE11dGF0ZXMgYGluZm9gIGluIHBsYWNlIHNvIHdpbnN0b24ncyBpbnRlcm5hbCBTeW1ib2wta2V5ZWQgcHJvcGVydGllc1xuICogKGBTeW1ib2wuZm9yKCdsZXZlbCcpYCwgYFN5bWJvbC5mb3IoJ21lc3NhZ2UnKWAsIGBTeW1ib2wuZm9yKCdzcGxhdCcpYCkgc3Vydml2ZVxuICog4oCUIHJlYnVpbGRpbmcgYSBmcmVzaCBvYmplY3QgdmlhIGBPYmplY3QuZW50cmllc2Avc3ByZWFkIHNpbGVudGx5IHN0cmlwcyB0aGVtLFxuICogd2hpY2ggbWFrZXMgdGhlIENvbnNvbGUgdHJhbnNwb3J0IGRyb3AgZXZlcnkgZW50cnkgd2l0aG91dCB3YXJuaW5nLiAqL1xuY29uc3QgUFJFU0VSVkVEX0tFWVMgPSBuZXcgU2V0KFsnbGV2ZWwnLCAnbWVzc2FnZScsICd0aW1lc3RhbXAnLCAnc2VydmljZScsICd0cmFjZV9pZCddKTtcbmNvbnN0IHJlZGFjdEZvcm1hdCA9IHdpbnN0b24uZm9ybWF0KChpbmZvKSA9PiB7XG4gIGZvciAoY29uc3Qga2V5IG9mIE9iamVjdC5rZXlzKGluZm8pKSB7XG4gICAgaWYgKFBSRVNFUlZFRF9LRVlTLmhhcyhrZXkpKSBjb250aW51ZTtcbiAgICBpZiAoU0VOU0lUSVZFX0tFWV9QQVRURVJOLnRlc3Qoa2V5KSkge1xuICAgICAgaW5mb1trZXldID0gUkVEQUNURUQ7XG4gICAgfSBlbHNlIHtcbiAgICAgIGluZm9ba2V5XSA9IHJlZGFjdERlZXAoaW5mb1trZXldKTtcbiAgICB9XG4gIH1cbiAgcmV0dXJuIGluZm87XG59KSgpO1xuXG4vKipcbiAqIENyZWF0ZSBhIGxvZ2dlciBpbnN0YW5jZSBmb3IgYSBzZXJ2aWNlLlxuICpcbiAqIFdoZW4gTE9HX0ZPUk1BVD1qc29uIChkZWZhdWx0KSwgb3V0cHV0cyBzdHJ1Y3R1cmVkIEpTT04gZm9yIExva2kgaW5nZXN0aW9uOlxuICogICB7XCJsZXZlbFwiOlwiaW5mb1wiLFwibWVzc2FnZVwiOlwiU2VydmVyIHN0YXJ0ZWRcIixcInNlcnZpY2VcIjpcInBpcGVsaW5lXCIsXCJ0aW1lc3RhbXBcIjpcIi4uLlwifVxuICpcbiAqIFdoZW4gTE9HX0ZPUk1BVD10ZXh0LCBvdXRwdXRzIGNvbG9yaXplZCBodW1hbi1yZWFkYWJsZSBmb3JtYXQ6XG4gKiAgIDIwMjYtMDItMTNUMTA6MzA6MDAuMDAwWiBpbmZvIFtwaXBlbGluZV0gU2VydmVyIHN0YXJ0ZWRcbiAqXG4gKiBAcGFyYW0gc2VydmljZU5hbWUgLSBOYW1lIG9mIHRoZSBzZXJ2aWNlIGZvciBsb2cgaWRlbnRpZmljYXRpb25cbiAqIEByZXR1cm5zIENvbmZpZ3VyZWQgV2luc3RvbiBsb2dnZXIgaW5zdGFuY2VcbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogaW1wb3J0IHsgY3JlYXRlTG9nZ2VyIH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvYXBpLWNvcmUnO1xuICpcbiAqIGNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcignZ2V0LXBsdWdpbicpO1xuICogbG9nZ2VyLmluZm8oJ1NlcnZlciBzdGFydGVkJywgeyBwb3J0OiAzMDAwIH0pO1xuICogbG9nZ2VyLmVycm9yKCdEYXRhYmFzZSBlcnJvcicsIHsgZXJyb3I6IGVyci5tZXNzYWdlIH0pO1xuICogYGBgXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVMb2dnZXIoc2VydmljZU5hbWU6IHN0cmluZyk6IHdpbnN0b24uTG9nZ2VyIHtcbiAgY29uc3QgbG9nTGV2ZWwgPSBwcm9jZXNzLmVudi5MT0dfTEVWRUwgfHwgJ2luZm8nO1xuICBjb25zdCBsb2dGb3JtYXQgPSBwcm9jZXNzLmVudi5MT0dfRk9STUFUIHx8ICdqc29uJztcbiAgaWYgKGxvZ0Zvcm1hdCAhPT0gJ2pzb24nICYmIGxvZ0Zvcm1hdCAhPT0gJ3RleHQnKSB7XG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLWNvbnNvbGUgLS0gc3RhcnR1cCB3YXJuaW5nIGJlZm9yZSBsb2dnZXIgaXMgYXZhaWxhYmxlXG4gICAgY29uc29sZS53YXJuKGBJbnZhbGlkIExPR19GT1JNQVQ9XCIke2xvZ0Zvcm1hdH1cIiwgZXhwZWN0ZWQgXCJqc29uXCIgb3IgXCJ0ZXh0XCIuIERlZmF1bHRpbmcgdG8gXCJqc29uXCIuYCk7XG4gIH1cbiAgY29uc3QgdXNlSnNvbiA9IGxvZ0Zvcm1hdCAhPT0gJ3RleHQnO1xuXG4gIC8vIE9yZGVyIG1hdHRlcnM6IHJlZGFjdCBCRUZPUkUgc2VyaWFsaXphdGlvbiBzbyBtYXNrZWQga2V5cyBuZXZlciByZWFjaFxuICAvLyB0aGUgb3V0cHV0LiB0cmFjZV9pZCBpcyBzdGFtcGVkIGZpcnN0IHNvIGl0IHN1cnZpdmVzIHJlZGFjdGlvbi5cbiAgY29uc3QgYmFzZUZvcm1hdHMgPSBbXG4gICAgZXJyb3JzKHsgc3RhY2s6IHRydWUgfSksXG4gICAgdGltZXN0YW1wKHsgZm9ybWF0OiAnWVlZWS1NTS1ERFRISDptbTpzcy5TU1NaJyB9KSxcbiAgICB0cmFjZUlkRm9ybWF0LFxuICAgIHJlZGFjdEZvcm1hdCxcbiAgXTtcblxuICBpZiAodXNlSnNvbikge1xuICAgIHJldHVybiB3aW5zdG9uLmNyZWF0ZUxvZ2dlcih7XG4gICAgICBsZXZlbDogbG9nTGV2ZWwsXG4gICAgICBkZWZhdWx0TWV0YTogeyBzZXJ2aWNlOiBzZXJ2aWNlTmFtZSB9LFxuICAgICAgZm9ybWF0OiBjb21iaW5lKC4uLmJhc2VGb3JtYXRzLCBqc29uKCkpLFxuICAgICAgdHJhbnNwb3J0czogW25ldyB3aW5zdG9uLnRyYW5zcG9ydHMuQ29uc29sZSgpXSxcbiAgICB9KTtcbiAgfVxuXG4gIHJldHVybiB3aW5zdG9uLmNyZWF0ZUxvZ2dlcih7XG4gICAgbGV2ZWw6IGxvZ0xldmVsLFxuICAgIGRlZmF1bHRNZXRhOiB7IHNlcnZpY2U6IHNlcnZpY2VOYW1lIH0sXG4gICAgZm9ybWF0OiBjb21iaW5lKC4uLmJhc2VGb3JtYXRzKSxcbiAgICB0cmFuc3BvcnRzOiBbXG4gICAgICBuZXcgd2luc3Rvbi50cmFuc3BvcnRzLkNvbnNvbGUoe1xuICAgICAgICBmb3JtYXQ6IGNvbWJpbmUoY29sb3JpemUoKSwgY29uc29sZUZvcm1hdCksXG4gICAgICB9KSxcbiAgICBdLFxuICB9KTtcbn1cblxuLyoqXG4gKiBEZWZhdWx0IGxvZ2dlciBpbnN0YW5jZSAoc2VydmljZSBuYW1lIGZyb20gU0VSVklDRV9OQU1FIGVudiB2YXIgb3IgJ2FwaScpLlxuICovXG5leHBvcnQgY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKHByb2Nlc3MuZW52LlNFUlZJQ0VfTkFNRSB8fCAnYXBpJyk7XG5cbmV4cG9ydCBkZWZhdWx0IGxvZ2dlcjtcbiJdfQ==
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable counter-emit shim.
|
|
3
|
+
*
|
|
4
|
+
* Background: several api-core primitives (quota client fail-open, future
|
|
5
|
+
* security-event hooks) want to record Prometheus counters when something
|
|
6
|
+
* notable happens. The actual `prom-client` Counter registration lives in
|
|
7
|
+
* api-server, and api-core must not depend on api-server (api-core is
|
|
8
|
+
* upstream — Express infrastructure imports it, not the other way around).
|
|
9
|
+
*
|
|
10
|
+
* This module is the seam. api-core code calls `emitCounter(name, labels)`;
|
|
11
|
+
* by default it's a no-op (tests, CLIs, environments without Prometheus).
|
|
12
|
+
* api-server's app-factory calls `setCounterEmitter(incCounter)` at startup
|
|
13
|
+
* so production processes start incrementing the real counter on the
|
|
14
|
+
* shared registry.
|
|
15
|
+
*
|
|
16
|
+
* Why a callback instead of an interface registered via DI? The call sites
|
|
17
|
+
* (quota fail-open, etc.) sit in deep helpers that never touch the Express
|
|
18
|
+
* context, so requiring a `metrics` parameter on every call would mean
|
|
19
|
+
* threading it through dozens of unrelated functions. A process-singleton
|
|
20
|
+
* callback is the simplest non-invasive option; the trade-off is one
|
|
21
|
+
* shared global, which is acceptable for a sink that's strictly write-only.
|
|
22
|
+
*/
|
|
23
|
+
/** Callable signature mirroring api-server's `incCounter`. */
|
|
24
|
+
export type CounterEmitter = (name: string, labels?: Record<string, string>, value?: number) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Register the real counter implementation. Idempotent — the last call wins;
|
|
27
|
+
* typically called once at service startup from api-server.
|
|
28
|
+
*/
|
|
29
|
+
export declare function setCounterEmitter(fn: CounterEmitter): void;
|
|
30
|
+
/** Reset to the no-op emitter. For tests that want to clear any wiring. */
|
|
31
|
+
export declare function resetCounterEmitter(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Record a counter event. Safe to call from any api-core module — if no
|
|
34
|
+
* emitter is wired, this is a no-op.
|
|
35
|
+
*/
|
|
36
|
+
export declare function emitCounter(name: string, labels?: Record<string, string>, value?: number): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
let emitter = () => {
|
|
4
|
+
// Default no-op. api-server wires this to a real Prometheus counter at
|
|
5
|
+
// startup; until then, calls from api-core helpers are silently dropped
|
|
6
|
+
// (which is what we want in tests and CLIs that don't run a Prom server).
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Register the real counter implementation. Idempotent — the last call wins;
|
|
10
|
+
* typically called once at service startup from api-server.
|
|
11
|
+
*/
|
|
12
|
+
export function setCounterEmitter(fn) {
|
|
13
|
+
emitter = fn;
|
|
14
|
+
}
|
|
15
|
+
/** Reset to the no-op emitter. For tests that want to clear any wiring. */
|
|
16
|
+
export function resetCounterEmitter() {
|
|
17
|
+
emitter = () => undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Record a counter event. Safe to call from any api-core module — if no
|
|
21
|
+
* emitter is wired, this is a no-op.
|
|
22
|
+
*/
|
|
23
|
+
export function emitCounter(name, labels = {}, value = 1) {
|
|
24
|
+
try {
|
|
25
|
+
emitter(name, labels, value);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Never let metric emission break the calling path. A misbehaving
|
|
29
|
+
// emitter shouldn't be able to take down quota checks (or anything else).
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0cmljLWVtaXR0ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdXRpbHMvbWV0cmljLWVtaXR0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQTRCdEMsSUFBSSxPQUFPLEdBQW1CLEdBQUcsRUFBRTtJQUNqQyx1RUFBdUU7SUFDdkUsd0VBQXdFO0lBQ3hFLDBFQUEwRTtBQUM1RSxDQUFDLENBQUM7QUFFRjs7O0dBR0c7QUFDSCxNQUFNLFVBQVUsaUJBQWlCLENBQUMsRUFBa0I7SUFDbEQsT0FBTyxHQUFHLEVBQUUsQ0FBQztBQUNmLENBQUM7QUFFRCwyRUFBMkU7QUFDM0UsTUFBTSxVQUFVLG1CQUFtQjtJQUNqQyxPQUFPLEdBQUcsR0FBRyxFQUFFLENBQUMsU0FBUyxDQUFDO0FBQzVCLENBQUM7QUFFRDs7O0dBR0c7QUFDSCxNQUFNLFVBQVUsV0FBVyxDQUFDLElBQVksRUFBRSxNQUFNLEdBQTJCLEVBQUUsRUFBRSxLQUFLLEdBQUcsQ0FBQztJQUN0RixJQUFJLENBQUM7UUFDSCxPQUFPLENBQUMsSUFBSSxFQUFFLE1BQU0sRUFBRSxLQUFLLENBQUMsQ0FBQztJQUMvQixDQUFDO0lBQUMsTUFBTSxDQUFDO1FBQ1Asa0VBQWtFO1FBQ2xFLDBFQUEwRTtJQUM1RSxDQUFDO0FBQ0gsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG4vKipcbiAqIFBsdWdnYWJsZSBjb3VudGVyLWVtaXQgc2hpbS5cbiAqXG4gKiBCYWNrZ3JvdW5kOiBzZXZlcmFsIGFwaS1jb3JlIHByaW1pdGl2ZXMgKHF1b3RhIGNsaWVudCBmYWlsLW9wZW4sIGZ1dHVyZVxuICogc2VjdXJpdHktZXZlbnQgaG9va3MpIHdhbnQgdG8gcmVjb3JkIFByb21ldGhldXMgY291bnRlcnMgd2hlbiBzb21ldGhpbmdcbiAqIG5vdGFibGUgaGFwcGVucy4gVGhlIGFjdHVhbCBgcHJvbS1jbGllbnRgIENvdW50ZXIgcmVnaXN0cmF0aW9uIGxpdmVzIGluXG4gKiBhcGktc2VydmVyLCBhbmQgYXBpLWNvcmUgbXVzdCBub3QgZGVwZW5kIG9uIGFwaS1zZXJ2ZXIgKGFwaS1jb3JlIGlzXG4gKiB1cHN0cmVhbSDigJQgRXhwcmVzcyBpbmZyYXN0cnVjdHVyZSBpbXBvcnRzIGl0LCBub3QgdGhlIG90aGVyIHdheSBhcm91bmQpLlxuICpcbiAqIFRoaXMgbW9kdWxlIGlzIHRoZSBzZWFtLiBhcGktY29yZSBjb2RlIGNhbGxzIGBlbWl0Q291bnRlcihuYW1lLCBsYWJlbHMpYDtcbiAqIGJ5IGRlZmF1bHQgaXQncyBhIG5vLW9wICh0ZXN0cywgQ0xJcywgZW52aXJvbm1lbnRzIHdpdGhvdXQgUHJvbWV0aGV1cykuXG4gKiBhcGktc2VydmVyJ3MgYXBwLWZhY3RvcnkgY2FsbHMgYHNldENvdW50ZXJFbWl0dGVyKGluY0NvdW50ZXIpYCBhdCBzdGFydHVwXG4gKiBzbyBwcm9kdWN0aW9uIHByb2Nlc3NlcyBzdGFydCBpbmNyZW1lbnRpbmcgdGhlIHJlYWwgY291bnRlciBvbiB0aGVcbiAqIHNoYXJlZCByZWdpc3RyeS5cbiAqXG4gKiBXaHkgYSBjYWxsYmFjayBpbnN0ZWFkIG9mIGFuIGludGVyZmFjZSByZWdpc3RlcmVkIHZpYSBEST8gVGhlIGNhbGwgc2l0ZXNcbiAqIChxdW90YSBmYWlsLW9wZW4sIGV0Yy4pIHNpdCBpbiBkZWVwIGhlbHBlcnMgdGhhdCBuZXZlciB0b3VjaCB0aGUgRXhwcmVzc1xuICogY29udGV4dCwgc28gcmVxdWlyaW5nIGEgYG1ldHJpY3NgIHBhcmFtZXRlciBvbiBldmVyeSBjYWxsIHdvdWxkIG1lYW5cbiAqIHRocmVhZGluZyBpdCB0aHJvdWdoIGRvemVucyBvZiB1bnJlbGF0ZWQgZnVuY3Rpb25zLiBBIHByb2Nlc3Mtc2luZ2xldG9uXG4gKiBjYWxsYmFjayBpcyB0aGUgc2ltcGxlc3Qgbm9uLWludmFzaXZlIG9wdGlvbjsgdGhlIHRyYWRlLW9mZiBpcyBvbmVcbiAqIHNoYXJlZCBnbG9iYWwsIHdoaWNoIGlzIGFjY2VwdGFibGUgZm9yIGEgc2luayB0aGF0J3Mgc3RyaWN0bHkgd3JpdGUtb25seS5cbiAqL1xuXG4vKiogQ2FsbGFibGUgc2lnbmF0dXJlIG1pcnJvcmluZyBhcGktc2VydmVyJ3MgYGluY0NvdW50ZXJgLiAqL1xuZXhwb3J0IHR5cGUgQ291bnRlckVtaXR0ZXIgPSAobmFtZTogc3RyaW5nLCBsYWJlbHM/OiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+LCB2YWx1ZT86IG51bWJlcikgPT4gdm9pZDtcblxubGV0IGVtaXR0ZXI6IENvdW50ZXJFbWl0dGVyID0gKCkgPT4ge1xuICAvLyBEZWZhdWx0IG5vLW9wLiBhcGktc2VydmVyIHdpcmVzIHRoaXMgdG8gYSByZWFsIFByb21ldGhldXMgY291bnRlciBhdFxuICAvLyBzdGFydHVwOyB1bnRpbCB0aGVuLCBjYWxscyBmcm9tIGFwaS1jb3JlIGhlbHBlcnMgYXJlIHNpbGVudGx5IGRyb3BwZWRcbiAgLy8gKHdoaWNoIGlzIHdoYXQgd2Ugd2FudCBpbiB0ZXN0cyBhbmQgQ0xJcyB0aGF0IGRvbid0IHJ1biBhIFByb20gc2VydmVyKS5cbn07XG5cbi8qKlxuICogUmVnaXN0ZXIgdGhlIHJlYWwgY291bnRlciBpbXBsZW1lbnRhdGlvbi4gSWRlbXBvdGVudCDigJQgdGhlIGxhc3QgY2FsbCB3aW5zO1xuICogdHlwaWNhbGx5IGNhbGxlZCBvbmNlIGF0IHNlcnZpY2Ugc3RhcnR1cCBmcm9tIGFwaS1zZXJ2ZXIuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzZXRDb3VudGVyRW1pdHRlcihmbjogQ291bnRlckVtaXR0ZXIpOiB2b2lkIHtcbiAgZW1pdHRlciA9IGZuO1xufVxuXG4vKiogUmVzZXQgdG8gdGhlIG5vLW9wIGVtaXR0ZXIuIEZvciB0ZXN0cyB0aGF0IHdhbnQgdG8gY2xlYXIgYW55IHdpcmluZy4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZXNldENvdW50ZXJFbWl0dGVyKCk6IHZvaWQge1xuICBlbWl0dGVyID0gKCkgPT4gdW5kZWZpbmVkO1xufVxuXG4vKipcbiAqIFJlY29yZCBhIGNvdW50ZXIgZXZlbnQuIFNhZmUgdG8gY2FsbCBmcm9tIGFueSBhcGktY29yZSBtb2R1bGUg4oCUIGlmIG5vXG4gKiBlbWl0dGVyIGlzIHdpcmVkLCB0aGlzIGlzIGEgbm8tb3AuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBlbWl0Q291bnRlcihuYW1lOiBzdHJpbmcsIGxhYmVsczogUmVjb3JkPHN0cmluZywgc3RyaW5nPiA9IHt9LCB2YWx1ZSA9IDEpOiB2b2lkIHtcbiAgdHJ5IHtcbiAgICBlbWl0dGVyKG5hbWUsIGxhYmVscywgdmFsdWUpO1xuICB9IGNhdGNoIHtcbiAgICAvLyBOZXZlciBsZXQgbWV0cmljIGVtaXNzaW9uIGJyZWFrIHRoZSBjYWxsaW5nIHBhdGguIEEgbWlzYmVoYXZpbmdcbiAgICAvLyBlbWl0dGVyIHNob3VsZG4ndCBiZSBhYmxlIHRvIHRha2UgZG93biBxdW90YSBjaGVja3MgKG9yIGFueXRoaW5nIGVsc2UpLlxuICB9XG59XG4iXX0=
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter an object to only include entries where the value is not `undefined`.
|
|
3
|
+
* Keeps `null`, `false`, `0`, and empty string — only removes `undefined`.
|
|
4
|
+
*
|
|
5
|
+
* Useful for building partial update payloads from validated request bodies.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const body = { name: 'foo', description: undefined, isActive: false };
|
|
10
|
+
* pickDefined(body); // { name: 'foo', isActive: false }
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function pickDefined<T extends Record<string, unknown>>(obj: T): Partial<T>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Filter an object to only include entries where the value is not `undefined`.
|
|
5
|
+
* Keeps `null`, `false`, `0`, and empty string — only removes `undefined`.
|
|
6
|
+
*
|
|
7
|
+
* Useful for building partial update payloads from validated request bodies.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const body = { name: 'foo', description: undefined, isActive: false };
|
|
12
|
+
* pickDefined(body); // { name: 'foo', isActive: false }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function pickDefined(obj) {
|
|
16
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2JqZWN0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL29iamVjdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDOzs7Ozs7Ozs7OztHQVdHO0FBQ0gsTUFBTSxVQUFVLFdBQVcsQ0FBb0MsR0FBTTtJQUNuRSxPQUFPLE1BQU0sQ0FBQyxXQUFXLENBQ3ZCLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLEtBQUssU0FBUyxDQUFDLENBQ3pDLENBQUM7QUFDbEIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG4vKipcbiAqIEZpbHRlciBhbiBvYmplY3QgdG8gb25seSBpbmNsdWRlIGVudHJpZXMgd2hlcmUgdGhlIHZhbHVlIGlzIG5vdCBgdW5kZWZpbmVkYC5cbiAqIEtlZXBzIGBudWxsYCwgYGZhbHNlYCwgYDBgLCBhbmQgZW1wdHkgc3RyaW5nIOKAlCBvbmx5IHJlbW92ZXMgYHVuZGVmaW5lZGAuXG4gKlxuICogVXNlZnVsIGZvciBidWlsZGluZyBwYXJ0aWFsIHVwZGF0ZSBwYXlsb2FkcyBmcm9tIHZhbGlkYXRlZCByZXF1ZXN0IGJvZGllcy5cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogY29uc3QgYm9keSA9IHsgbmFtZTogJ2ZvbycsIGRlc2NyaXB0aW9uOiB1bmRlZmluZWQsIGlzQWN0aXZlOiBmYWxzZSB9O1xuICogcGlja0RlZmluZWQoYm9keSk7IC8vIHsgbmFtZTogJ2ZvbycsIGlzQWN0aXZlOiBmYWxzZSB9XG4gKiBgYGBcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHBpY2tEZWZpbmVkPFQgZXh0ZW5kcyBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPj4ob2JqOiBUKTogUGFydGlhbDxUPiB7XG4gIHJldHVybiBPYmplY3QuZnJvbUVudHJpZXMoXG4gICAgT2JqZWN0LmVudHJpZXMob2JqKS5maWx0ZXIoKFssIHZdKSA9PiB2ICE9PSB1bmRlZmluZWQpLFxuICApIGFzIFBhcnRpYWw8VD47XG59XG4iXX0=
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Shape convention for the inline parse-guard helpers below.
|
|
4
|
+
*
|
|
5
|
+
* These return a lightweight `{ value } | { error }` (narrowed with
|
|
6
|
+
* `'error' in result`) — deliberately DISTINCT from the schema-validation
|
|
7
|
+
* `ValidationResult<T>` (`{ ok, value } | { ok, error }`) in
|
|
8
|
+
* `validation/middleware.ts`. Zod schema parsing uses the `ok`-discriminated
|
|
9
|
+
* shape; these ad-hoc request-parameter guards use `'error' in`. The split is
|
|
10
|
+
* intentional and documented (see the note on `ValidationResult`): converging
|
|
11
|
+
* them would churn every `'error' in result` call site across the downstream
|
|
12
|
+
* services with no behavioural change, so it's left as a documented convention.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Express 5 parameter type.
|
|
16
|
+
*/
|
|
17
|
+
type ParamValue = string | string[] | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Extract a single string parameter from Express 5 route params.
|
|
20
|
+
*
|
|
21
|
+
* @param params - Request params object
|
|
22
|
+
* @param key - Parameter key
|
|
23
|
+
* @returns The parameter value as a string, or undefined if not present
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // Route: GET /plugins/:id
|
|
28
|
+
* const id = getParam(req.params, 'id');
|
|
29
|
+
* if (!id) {
|
|
30
|
+
* return sendError(res, 400, 'Missing id parameter');
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function getParam(params: Record<string, ParamValue>, key: string): string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Extract the organization ID from request.
|
|
37
|
+
* Checks params, headers, and user object.
|
|
38
|
+
*
|
|
39
|
+
* @param req - Express request object
|
|
40
|
+
* @returns Organization ID or undefined
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const orgId = getOrgId(req);
|
|
45
|
+
* if (!orgId) {
|
|
46
|
+
* return sendError(res, 400, 'Organization ID required');
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function getOrgId(req: Request): string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Parse a query parameter as an integer.
|
|
53
|
+
*
|
|
54
|
+
* @param value - Query parameter value
|
|
55
|
+
* @param defaultValue - Default value if parsing fails
|
|
56
|
+
* @returns Parsed integer
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const limit = parseQueryInt(req.query.limit, 10);
|
|
61
|
+
* const offset = parseQueryInt(req.query.offset, 0);
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseQueryInt(value: unknown, defaultValue: number): number;
|
|
65
|
+
/**
|
|
66
|
+
* Parse a query parameter as a string.
|
|
67
|
+
*
|
|
68
|
+
* @param value - Query parameter value
|
|
69
|
+
* @returns String value or undefined
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const search = parseQueryString(req.query.search);
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseQueryString(value: unknown): string | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Parse a string to a positive integer, returning a fallback if invalid.
|
|
79
|
+
* Useful for parsing environment variables with numeric defaults.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parsePositiveInt(value: string | undefined, fallback: number): number;
|
|
82
|
+
/**
|
|
83
|
+
* Parse a query int, then clamp to `[1, max]`. Returns `defaultValue` (after
|
|
84
|
+
* the same clamp) when the input is missing/invalid. Use for `limit`-style
|
|
85
|
+
* query parameters where an unbounded value would cost real money.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* const limit = parseQueryIntClamped(req.query.limit, 50, 1000);
|
|
89
|
+
*/
|
|
90
|
+
export declare function parseQueryIntClamped(value: unknown, defaultValue: number, max: number): number;
|
|
91
|
+
/**
|
|
92
|
+
* Bulk-array request guard. Validates that `value` is an array, non-empty,
|
|
93
|
+
* and within the configured cap. Returns the typed array or an error object
|
|
94
|
+
* the caller can forward to `sendError`/`sendBadRequest`.
|
|
95
|
+
*
|
|
96
|
+
* Replaces a hand-rolled `if (!Array.isArray(...)) { ... }` + length check
|
|
97
|
+
* block that was duplicated across bulk endpoints in pipeline, plugin,
|
|
98
|
+
* compliance, etc.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* const result = validateBulkArray(req.body.ids, 'ids');
|
|
102
|
+
* if ('error' in result) return sendBadRequest(res, result.error, ErrorCode.VALIDATION_ERROR);
|
|
103
|
+
* const ids = result.value;
|
|
104
|
+
*/
|
|
105
|
+
export declare function validateBulkArray<T = unknown>(value: unknown, fieldName: string, maxItems?: number): {
|
|
106
|
+
value: T[];
|
|
107
|
+
} | {
|
|
108
|
+
error: string;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Supported intervals for time-bucketed report queries. Used as the
|
|
112
|
+
* `DATE_TRUNC` first argument in Postgres; restricted to a fixed enum so
|
|
113
|
+
* the value can never be injected into raw SQL.
|
|
114
|
+
*/
|
|
115
|
+
export declare const REPORT_INTERVALS: readonly ['day', 'week', 'month'];
|
|
116
|
+
export type ReportInterval = (typeof REPORT_INTERVALS)[number];
|
|
117
|
+
/**
|
|
118
|
+
* Parse + validate a `?interval=` report bucket (defaults to 'week'). Returns the
|
|
119
|
+
* validated interval or `{ error }`. Shared by the timeseries report routes so the
|
|
120
|
+
* default + allow-list + error message live in one place.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const interval = parseReportInterval(req.query);
|
|
124
|
+
* if (typeof interval === 'object') return sendBadRequest(res, interval.error, ErrorCode.VALIDATION_ERROR);
|
|
125
|
+
*/
|
|
126
|
+
export declare function parseReportInterval(query: Record<string, unknown>): ReportInterval | {
|
|
127
|
+
error: string;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Parse a `?from=&to=` date range with defaults of "30 days ago → now".
|
|
131
|
+
* Rejects non-string inputs (Express's parser can produce string[] for
|
|
132
|
+
* repeated keys), invalid ISO timestamps, and inverted ranges.
|
|
133
|
+
*
|
|
134
|
+
* Returns either `{ from, to }` ISO strings or `{ error }`.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* const range = parseDateRange(req.query);
|
|
138
|
+
* if ('error' in range) return sendBadRequest(res, range.error, ErrorCode.VALIDATION_ERROR);
|
|
139
|
+
* const rows = await svc.list(range.from, range.to);
|
|
140
|
+
*/
|
|
141
|
+
export declare function parseDateRange(query: Record<string, unknown>, options?: {
|
|
142
|
+
maxRangeMs?: number;
|
|
143
|
+
defaultDaysBack?: number;
|
|
144
|
+
}): {
|
|
145
|
+
from: string;
|
|
146
|
+
to: string;
|
|
147
|
+
} | {
|
|
148
|
+
error: string;
|
|
149
|
+
};
|
|
150
|
+
export {};
|