@pipeline-builder/api-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +79 -0
- package/lib/api/app-factory.d.ts +95 -0
- package/lib/api/app-factory.js +328 -0
- package/lib/api/check-quota.d.ts +13 -0
- package/lib/api/check-quota.js +74 -0
- package/lib/api/context-middleware.d.ts +34 -0
- package/lib/api/context-middleware.js +42 -0
- package/lib/api/etag-middleware.d.ts +7 -0
- package/lib/api/etag-middleware.js +44 -0
- package/lib/api/get-context.d.ts +22 -0
- package/lib/api/get-context.js +28 -0
- package/lib/api/health-checks.d.ts +62 -0
- package/lib/api/health-checks.js +87 -0
- package/lib/api/idempotency-middleware.d.ts +80 -0
- package/lib/api/idempotency-middleware.js +321 -0
- package/lib/api/index.d.ts +20 -0
- package/lib/api/index.js +34 -0
- package/lib/api/meter-quota.d.ts +32 -0
- package/lib/api/meter-quota.js +70 -0
- package/lib/api/metrics.d.ts +47 -0
- package/lib/api/metrics.js +221 -0
- package/lib/api/middleware-factory.d.ts +56 -0
- package/lib/api/middleware-factory.js +80 -0
- package/lib/api/middleware.d.ts +1 -0
- package/lib/api/middleware.js +8 -0
- package/lib/api/mongo-connect.d.ts +19 -0
- package/lib/api/mongo-connect.js +41 -0
- package/lib/api/quota-helpers.d.ts +23 -0
- package/lib/api/quota-helpers.js +22 -0
- package/lib/api/rate-limit-by-org.d.ts +29 -0
- package/lib/api/rate-limit-by-org.js +64 -0
- package/lib/api/readiness.d.ts +29 -0
- package/lib/api/readiness.js +65 -0
- package/lib/api/request-types.d.ts +55 -0
- package/lib/api/request-types.js +65 -0
- package/lib/api/require-org-id.d.ts +14 -0
- package/lib/api/require-org-id.js +28 -0
- package/lib/api/route-wrapper.d.ts +50 -0
- package/lib/api/route-wrapper.js +84 -0
- package/lib/api/server.d.ts +79 -0
- package/lib/api/server.js +234 -0
- package/lib/api/tenant-context.d.ts +28 -0
- package/lib/api/tenant-context.js +48 -0
- package/lib/api/tracing.d.ts +25 -0
- package/lib/api/tracing.js +69 -0
- package/lib/http/index.d.ts +4 -0
- package/lib/http/index.js +7 -0
- package/lib/http/sse-connection-manager.d.ts +357 -0
- package/lib/http/sse-connection-manager.js +665 -0
- package/lib/http/sse-relay.d.ts +63 -0
- package/lib/http/sse-relay.js +70 -0
- package/lib/http/sse-ticket-channel.d.ts +31 -0
- package/lib/http/sse-ticket-channel.js +52 -0
- package/lib/http/sse-ticket-store.d.ts +75 -0
- package/lib/http/sse-ticket-store.js +160 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +35 -0
- package/lib/otel-bootstrap.d.ts +1 -0
- package/lib/otel-bootstrap.js +75 -0
- package/package.json +183 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { sendError, ErrorCode, createLogger, verifyServicePrincipal, createEnvRedisClient, safeCreateRequire } from '@pipeline-builder/api-core';
|
|
4
|
+
import rateLimit, { ipKeyGenerator } from 'express-rate-limit';
|
|
5
|
+
const logger = createLogger('rate-limit-by-org');
|
|
6
|
+
/**
|
|
7
|
+
* Per-ORGANIZATION rate limiter for a specific hot/expensive route, keyed on the
|
|
8
|
+
* VERIFIED auth org — the complement to the coarse global per-IP limiter in
|
|
9
|
+
* `createApp` (which a whole org behind one NAT shares, and a distributed
|
|
10
|
+
* attacker spreads across IP buckets to evade).
|
|
11
|
+
*
|
|
12
|
+
* MUST be mounted AFTER `requireAuth` so `req.user.organizationId` is populated;
|
|
13
|
+
* an unauthenticated caller falls back to a client-IP bucket (never a spoofable
|
|
14
|
+
* header) so it stays bounded. Verified internal service principals are exempt
|
|
15
|
+
* (they carry a signed service JWT). Uses the shared env Redis store
|
|
16
|
+
* (cross-replica, namespaced by `name`) when configured, else in-memory.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* router.post('/', ...createAuthenticatedWithOrgRoute(), requirePermission('messages:write'),
|
|
20
|
+
* rateLimitByOrg({ name: 'message-send', max: 60, windowMs: 60_000 }),
|
|
21
|
+
* withRoute(handler));
|
|
22
|
+
*/
|
|
23
|
+
export function rateLimitByOrg(opts) {
|
|
24
|
+
const { name, max, windowMs, message } = opts;
|
|
25
|
+
const options = {
|
|
26
|
+
max,
|
|
27
|
+
windowMs,
|
|
28
|
+
standardHeaders: true,
|
|
29
|
+
legacyHeaders: false,
|
|
30
|
+
// Exempt cryptographically-verified internal service callers (signed JWT).
|
|
31
|
+
skip: (req) => verifyServicePrincipal(req),
|
|
32
|
+
// Bucket by VERIFIED org (set by requireAuth), NOT the spoofable `x-org-id`
|
|
33
|
+
// header. Fall back to a normalized client-IP bucket when unauthenticated so
|
|
34
|
+
// the limiter still bounds pre-auth traffic. Namespaced (`org:`/`ip:`) so an
|
|
35
|
+
// IP literal can never collide with an org id.
|
|
36
|
+
keyGenerator: (req) => {
|
|
37
|
+
const orgId = req.user?.organizationId;
|
|
38
|
+
return orgId ? `org:${orgId.toLowerCase()}` : `ip:${ipKeyGenerator(req.ip || 'anon', 64)}`;
|
|
39
|
+
},
|
|
40
|
+
handler: (_req, res) => {
|
|
41
|
+
sendError(res, 429, message ?? `Too many ${name} requests, please slow down.`, ErrorCode.RATE_LIMIT_EXCEEDED);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
// Shared Redis store for cross-replica buckets when Redis is configured;
|
|
45
|
+
// namespaced per limiter so distinct limiters don't share a bucket. Fail-open:
|
|
46
|
+
// no Redis / a load failure falls back to per-process in-memory limiting.
|
|
47
|
+
const redis = createEnvRedisClient(`rate-limit-${name}`);
|
|
48
|
+
if (redis) {
|
|
49
|
+
try {
|
|
50
|
+
const require = safeCreateRequire(import.meta.url);
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
|
+
const { RedisStore } = require('rate-limit-redis');
|
|
53
|
+
options.store = new RedisStore({
|
|
54
|
+
prefix: `rl:${name}:`,
|
|
55
|
+
sendCommand: (...args) => redis.call(...args),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
logger.warn('Redis store unavailable; per-org limiter falls back to in-memory', { name });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return rateLimit(options);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmF0ZS1saW1pdC1ieS1vcmcuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXBpL3JhdGUtbGltaXQtYnktb3JnLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEMsT0FBTyxFQUFFLFNBQVMsRUFBRSxTQUFTLEVBQUUsWUFBWSxFQUFFLHNCQUFzQixFQUFFLG9CQUFvQixFQUFFLGlCQUFpQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFFakosT0FBTyxTQUFTLEVBQUUsRUFBRSxjQUFjLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUUvRCxNQUFNLE1BQU0sR0FBRyxZQUFZLENBQUMsbUJBQW1CLENBQUMsQ0FBQztBQWNqRDs7Ozs7Ozs7Ozs7Ozs7OztHQWdCRztBQUNILE1BQU0sVUFBVSxjQUFjLENBQUMsSUFBeUI7SUFDdEQsTUFBTSxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsUUFBUSxFQUFFLE9BQU8sRUFBRSxHQUFHLElBQUksQ0FBQztJQUU5QyxNQUFNLE9BQU8sR0FBb0M7UUFDL0MsR0FBRztRQUNILFFBQVE7UUFDUixlQUFlLEVBQUUsSUFBSTtRQUNyQixhQUFhLEVBQUUsS0FBSztRQUNwQiwyRUFBMkU7UUFDM0UsSUFBSSxFQUFFLENBQUMsR0FBWSxFQUFFLEVBQUUsQ0FBQyxzQkFBc0IsQ0FBQyxHQUFHLENBQUM7UUFDbkQsNEVBQTRFO1FBQzVFLDZFQUE2RTtRQUM3RSw2RUFBNkU7UUFDN0UsK0NBQStDO1FBQy9DLFlBQVksRUFBRSxDQUFDLEdBQVksRUFBVSxFQUFFO1lBQ3JDLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxJQUFJLEVBQUUsY0FBYyxDQUFDO1lBQ3ZDLE9BQU8sS0FBSyxDQUFDLENBQUMsQ0FBQyxPQUFPLEtBQUssQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxNQUFNLGNBQWMsQ0FBQyxHQUFHLENBQUMsRUFBRSxJQUFJLE1BQU0sRUFBRSxFQUFFLENBQUMsRUFBRSxDQUFDO1FBQzdGLENBQUM7UUFDRCxPQUFPLEVBQUUsQ0FBQyxJQUFhLEVBQUUsR0FBYSxFQUFRLEVBQUU7WUFDOUMsU0FBUyxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsT0FBTyxJQUFJLFlBQVksSUFBSSw4QkFBOEIsRUFBRSxTQUFTLENBQUMsbUJBQW1CLENBQUMsQ0FBQztRQUNoSCxDQUFDO0tBQ0YsQ0FBQztJQUVGLHlFQUF5RTtJQUN6RSwrRUFBK0U7SUFDL0UsMEVBQTBFO0lBQzFFLE1BQU0sS0FBSyxHQUFHLG9CQUFvQixDQUFxRCxjQUFjLElBQUksRUFBRSxDQUFDLENBQUM7SUFDN0csSUFBSSxLQUFLLEVBQUUsQ0FBQztRQUNWLElBQUksQ0FBQztZQUNILE1BQU0sT0FBTyxHQUFHLGlCQUFpQixDQUFDLE9BQU8sSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ25ELGlFQUFpRTtZQUNqRSxNQUFNLEVBQUUsVUFBVSxFQUFFLEdBQUcsT0FBTyxDQUFDLGtCQUFrQixDQUFDLENBQUM7WUFDbkQsT0FBTyxDQUFDLEtBQUssR0FBRyxJQUFJLFVBQVUsQ0FBQztnQkFDN0IsTUFBTSxFQUFFLE1BQU0sSUFBSSxHQUFHO2dCQUNyQixXQUFXLEVBQUUsQ0FBQyxHQUFHLElBQWMsRUFBRSxFQUFFLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxHQUFHLElBQUksQ0FBQzthQUN4RCxDQUFDLENBQUM7UUFDTCxDQUFDO1FBQUMsTUFBTSxDQUFDO1lBQ1AsTUFBTSxDQUFDLElBQUksQ0FBQyxrRUFBa0UsRUFBRSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDNUYsQ0FBQztJQUNILENBQUM7SUFFRCxPQUFPLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUM1QixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IHNlbmRFcnJvciwgRXJyb3JDb2RlLCBjcmVhdGVMb2dnZXIsIHZlcmlmeVNlcnZpY2VQcmluY2lwYWwsIGNyZWF0ZUVudlJlZGlzQ2xpZW50LCBzYWZlQ3JlYXRlUmVxdWlyZSB9IGZyb20gJ0BwaXBlbGluZS1idWlsZGVyL2FwaS1jb3JlJztcbmltcG9ydCB0eXBlIHsgUmVxdWVzdCwgUmVzcG9uc2UgfSBmcm9tICdleHByZXNzJztcbmltcG9ydCByYXRlTGltaXQsIHsgaXBLZXlHZW5lcmF0b3IgfSBmcm9tICdleHByZXNzLXJhdGUtbGltaXQnO1xuXG5jb25zdCBsb2dnZXIgPSBjcmVhdGVMb2dnZXIoJ3JhdGUtbGltaXQtYnktb3JnJyk7XG5cbi8qKiBDb25maWcgZm9yIGEge0BsaW5rIHJhdGVMaW1pdEJ5T3JnfSBsaW1pdGVyLiAqL1xuZXhwb3J0IGludGVyZmFjZSBPcmdSYXRlTGltaXRPcHRpb25zIHtcbiAgLyoqIFVuaXF1ZSBuYW1lIOKAlCBuYW1lc3BhY2VzIHRoZSBSZWRpcyBidWNrZXQgKyBsYWJlbHMgbG9ncy4gKi9cbiAgbmFtZTogc3RyaW5nO1xuICAvKiogTWF4IHJlcXVlc3RzIHBlciB3aW5kb3cgcGVyIG9yZyAob3IgcGVyIGNsaWVudCBJUCB3aGVuIHVuYXV0aGVudGljYXRlZCkuICovXG4gIG1heDogbnVtYmVyO1xuICAvKiogV2luZG93IGxlbmd0aCBpbiBtcy4gKi9cbiAgd2luZG93TXM6IG51bWJlcjtcbiAgLyoqIE9wdGlvbmFsIDQyOSBtZXNzYWdlIG92ZXJyaWRlLiAqL1xuICBtZXNzYWdlPzogc3RyaW5nO1xufVxuXG4vKipcbiAqIFBlci1PUkdBTklaQVRJT04gcmF0ZSBsaW1pdGVyIGZvciBhIHNwZWNpZmljIGhvdC9leHBlbnNpdmUgcm91dGUsIGtleWVkIG9uIHRoZVxuICogVkVSSUZJRUQgYXV0aCBvcmcg4oCUIHRoZSBjb21wbGVtZW50IHRvIHRoZSBjb2Fyc2UgZ2xvYmFsIHBlci1JUCBsaW1pdGVyIGluXG4gKiBgY3JlYXRlQXBwYCAod2hpY2ggYSB3aG9sZSBvcmcgYmVoaW5kIG9uZSBOQVQgc2hhcmVzLCBhbmQgYSBkaXN0cmlidXRlZFxuICogYXR0YWNrZXIgc3ByZWFkcyBhY3Jvc3MgSVAgYnVja2V0cyB0byBldmFkZSkuXG4gKlxuICogTVVTVCBiZSBtb3VudGVkIEFGVEVSIGByZXF1aXJlQXV0aGAgc28gYHJlcS51c2VyLm9yZ2FuaXphdGlvbklkYCBpcyBwb3B1bGF0ZWQ7XG4gKiBhbiB1bmF1dGhlbnRpY2F0ZWQgY2FsbGVyIGZhbGxzIGJhY2sgdG8gYSBjbGllbnQtSVAgYnVja2V0IChuZXZlciBhIHNwb29mYWJsZVxuICogaGVhZGVyKSBzbyBpdCBzdGF5cyBib3VuZGVkLiBWZXJpZmllZCBpbnRlcm5hbCBzZXJ2aWNlIHByaW5jaXBhbHMgYXJlIGV4ZW1wdFxuICogKHRoZXkgY2FycnkgYSBzaWduZWQgc2VydmljZSBKV1QpLiBVc2VzIHRoZSBzaGFyZWQgZW52IFJlZGlzIHN0b3JlXG4gKiAoY3Jvc3MtcmVwbGljYSwgbmFtZXNwYWNlZCBieSBgbmFtZWApIHdoZW4gY29uZmlndXJlZCwgZWxzZSBpbi1tZW1vcnkuXG4gKlxuICogQGV4YW1wbGVcbiAqICAgcm91dGVyLnBvc3QoJy8nLCAuLi5jcmVhdGVBdXRoZW50aWNhdGVkV2l0aE9yZ1JvdXRlKCksIHJlcXVpcmVQZXJtaXNzaW9uKCdtZXNzYWdlczp3cml0ZScpLFxuICogICAgIHJhdGVMaW1pdEJ5T3JnKHsgbmFtZTogJ21lc3NhZ2Utc2VuZCcsIG1heDogNjAsIHdpbmRvd01zOiA2MF8wMDAgfSksXG4gKiAgICAgd2l0aFJvdXRlKGhhbmRsZXIpKTtcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJhdGVMaW1pdEJ5T3JnKG9wdHM6IE9yZ1JhdGVMaW1pdE9wdGlvbnMpIHtcbiAgY29uc3QgeyBuYW1lLCBtYXgsIHdpbmRvd01zLCBtZXNzYWdlIH0gPSBvcHRzO1xuXG4gIGNvbnN0IG9wdGlvbnM6IFBhcmFtZXRlcnM8dHlwZW9mIHJhdGVMaW1pdD5bMF0gPSB7XG4gICAgbWF4LFxuICAgIHdpbmRvd01zLFxuICAgIHN0YW5kYXJkSGVhZGVyczogdHJ1ZSxcbiAgICBsZWdhY3lIZWFkZXJzOiBmYWxzZSxcbiAgICAvLyBFeGVtcHQgY3J5cHRvZ3JhcGhpY2FsbHktdmVyaWZpZWQgaW50ZXJuYWwgc2VydmljZSBjYWxsZXJzIChzaWduZWQgSldUKS5cbiAgICBza2lwOiAocmVxOiBSZXF1ZXN0KSA9PiB2ZXJpZnlTZXJ2aWNlUHJpbmNpcGFsKHJlcSksXG4gICAgLy8gQnVja2V0IGJ5IFZFUklGSUVEIG9yZyAoc2V0IGJ5IHJlcXVpcmVBdXRoKSwgTk9UIHRoZSBzcG9vZmFibGUgYHgtb3JnLWlkYFxuICAgIC8vIGhlYWRlci4gRmFsbCBiYWNrIHRvIGEgbm9ybWFsaXplZCBjbGllbnQtSVAgYnVja2V0IHdoZW4gdW5hdXRoZW50aWNhdGVkIHNvXG4gICAgLy8gdGhlIGxpbWl0ZXIgc3RpbGwgYm91bmRzIHByZS1hdXRoIHRyYWZmaWMuIE5hbWVzcGFjZWQgKGBvcmc6YC9gaXA6YCkgc28gYW5cbiAgICAvLyBJUCBsaXRlcmFsIGNhbiBuZXZlciBjb2xsaWRlIHdpdGggYW4gb3JnIGlkLlxuICAgIGtleUdlbmVyYXRvcjogKHJlcTogUmVxdWVzdCk6IHN0cmluZyA9PiB7XG4gICAgICBjb25zdCBvcmdJZCA9IHJlcS51c2VyPy5vcmdhbml6YXRpb25JZDtcbiAgICAgIHJldHVybiBvcmdJZCA/IGBvcmc6JHtvcmdJZC50b0xvd2VyQ2FzZSgpfWAgOiBgaXA6JHtpcEtleUdlbmVyYXRvcihyZXEuaXAgfHwgJ2Fub24nLCA2NCl9YDtcbiAgICB9LFxuICAgIGhhbmRsZXI6IChfcmVxOiBSZXF1ZXN0LCByZXM6IFJlc3BvbnNlKTogdm9pZCA9PiB7XG4gICAgICBzZW5kRXJyb3IocmVzLCA0MjksIG1lc3NhZ2UgPz8gYFRvbyBtYW55ICR7bmFtZX0gcmVxdWVzdHMsIHBsZWFzZSBzbG93IGRvd24uYCwgRXJyb3JDb2RlLlJBVEVfTElNSVRfRVhDRUVERUQpO1xuICAgIH0sXG4gIH07XG5cbiAgLy8gU2hhcmVkIFJlZGlzIHN0b3JlIGZvciBjcm9zcy1yZXBsaWNhIGJ1Y2tldHMgd2hlbiBSZWRpcyBpcyBjb25maWd1cmVkO1xuICAvLyBuYW1lc3BhY2VkIHBlciBsaW1pdGVyIHNvIGRpc3RpbmN0IGxpbWl0ZXJzIGRvbid0IHNoYXJlIGEgYnVja2V0LiBGYWlsLW9wZW46XG4gIC8vIG5vIFJlZGlzIC8gYSBsb2FkIGZhaWx1cmUgZmFsbHMgYmFjayB0byBwZXItcHJvY2VzcyBpbi1tZW1vcnkgbGltaXRpbmcuXG4gIGNvbnN0IHJlZGlzID0gY3JlYXRlRW52UmVkaXNDbGllbnQ8eyBjYWxsOiAoLi4uYXJnczogdW5rbm93bltdKSA9PiBQcm9taXNlPHVua25vd24+IH0+KGByYXRlLWxpbWl0LSR7bmFtZX1gKTtcbiAgaWYgKHJlZGlzKSB7XG4gICAgdHJ5IHtcbiAgICAgIGNvbnN0IHJlcXVpcmUgPSBzYWZlQ3JlYXRlUmVxdWlyZShpbXBvcnQubWV0YS51cmwpO1xuICAgICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIEB0eXBlc2NyaXB0LWVzbGludC9uby1yZXF1aXJlLWltcG9ydHNcbiAgICAgIGNvbnN0IHsgUmVkaXNTdG9yZSB9ID0gcmVxdWlyZSgncmF0ZS1saW1pdC1yZWRpcycpO1xuICAgICAgb3B0aW9ucy5zdG9yZSA9IG5ldyBSZWRpc1N0b3JlKHtcbiAgICAgICAgcHJlZml4OiBgcmw6JHtuYW1lfTpgLFxuICAgICAgICBzZW5kQ29tbWFuZDogKC4uLmFyZ3M6IHN0cmluZ1tdKSA9PiByZWRpcy5jYWxsKC4uLmFyZ3MpLFxuICAgICAgfSk7XG4gICAgfSBjYXRjaCB7XG4gICAgICBsb2dnZXIud2FybignUmVkaXMgc3RvcmUgdW5hdmFpbGFibGU7IHBlci1vcmcgbGltaXRlciBmYWxscyBiYWNrIHRvIGluLW1lbW9yeScsIHsgbmFtZSB9KTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4gcmF0ZUxpbWl0KG9wdGlvbnMpO1xufVxuIl19
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RequestHandler } from 'express';
|
|
2
|
+
/** True once dependencies have connected (and remain connected). */
|
|
3
|
+
export declare function isReady(): boolean;
|
|
4
|
+
/** Set by the startup supervisor (and tests). */
|
|
5
|
+
export declare function setReady(ready: boolean): void;
|
|
6
|
+
/**
|
|
7
|
+
* Default infra endpoints that must stay reachable while NotReady, so
|
|
8
|
+
* orchestrators can probe liveness/readiness, scrape metrics, warm pools,
|
|
9
|
+
* serve docs, and relay SSE logs during startup or a dependency outage. This
|
|
10
|
+
* matches what `createApp` mounts. A service whose own routes COLLIDE with one
|
|
11
|
+
* of these prefixes (e.g. platform serves a tenant log API at `/logs`) must
|
|
12
|
+
* pass a narrower list to `readinessGuard()` so its business routes stay gated.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_READINESS_BYPASS: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Readiness guard — while the service is NotReady (dependencies not yet
|
|
17
|
+
* connected, or dropped), reject business requests with 503 + Retry-After
|
|
18
|
+
* instead of letting them hit a disconnected datastore and 500.
|
|
19
|
+
*
|
|
20
|
+
* This is the portable, app-level readiness mechanism that works on every
|
|
21
|
+
* deploy target — including Docker Compose (single replica, no orchestrator
|
|
22
|
+
* traffic-draining) and Fargate (ECS exposes only one per-service health
|
|
23
|
+
* signal, which is also a kill trigger). k8s readiness probes layer on top.
|
|
24
|
+
*
|
|
25
|
+
* @param bypassPaths Infra prefixes that always pass through even while
|
|
26
|
+
* NotReady. Defaults to {@link DEFAULT_READINESS_BYPASS}; pass a custom list
|
|
27
|
+
* when a business route would otherwise collide with a default prefix.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readinessGuard(bypassPaths?: readonly string[]): RequestHandler;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { sendError } from '@pipeline-builder/api-core';
|
|
4
|
+
/**
|
|
5
|
+
* Service readiness state.
|
|
6
|
+
*
|
|
7
|
+
* Flipped by startServer's dependency supervisor: set false the instant we
|
|
8
|
+
* start listening, true once the backing store(s) connect, and back to false
|
|
9
|
+
* if they later drop. Read by the readiness-guard middleware (and tests).
|
|
10
|
+
*
|
|
11
|
+
* Module-level singleton: each service runs as its own process with exactly
|
|
12
|
+
* one server, so a single flag is sufficient and avoids threading state
|
|
13
|
+
* through createApp + startServer.
|
|
14
|
+
*
|
|
15
|
+
* Defaults to `true` so an app built with createApp but NOT started via
|
|
16
|
+
* startServer — every route unit test, which drives the app directly with
|
|
17
|
+
* supertest — is never blocked by the guard. startServer flips it to false
|
|
18
|
+
* synchronously before it opens the port, so the production path is gated.
|
|
19
|
+
*/
|
|
20
|
+
let serviceReady = true;
|
|
21
|
+
/** True once dependencies have connected (and remain connected). */
|
|
22
|
+
export function isReady() {
|
|
23
|
+
return serviceReady;
|
|
24
|
+
}
|
|
25
|
+
/** Set by the startup supervisor (and tests). */
|
|
26
|
+
export function setReady(ready) {
|
|
27
|
+
serviceReady = ready;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Default infra endpoints that must stay reachable while NotReady, so
|
|
31
|
+
* orchestrators can probe liveness/readiness, scrape metrics, warm pools,
|
|
32
|
+
* serve docs, and relay SSE logs during startup or a dependency outage. This
|
|
33
|
+
* matches what `createApp` mounts. A service whose own routes COLLIDE with one
|
|
34
|
+
* of these prefixes (e.g. platform serves a tenant log API at `/logs`) must
|
|
35
|
+
* pass a narrower list to `readinessGuard()` so its business routes stay gated.
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_READINESS_BYPASS = ['/health', '/ready', '/metrics', '/warmup', '/docs', '/logs'];
|
|
38
|
+
function isBypassed(path, bypass) {
|
|
39
|
+
return bypass.some((p) => path === p || path.startsWith(`${p}/`));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Readiness guard — while the service is NotReady (dependencies not yet
|
|
43
|
+
* connected, or dropped), reject business requests with 503 + Retry-After
|
|
44
|
+
* instead of letting them hit a disconnected datastore and 500.
|
|
45
|
+
*
|
|
46
|
+
* This is the portable, app-level readiness mechanism that works on every
|
|
47
|
+
* deploy target — including Docker Compose (single replica, no orchestrator
|
|
48
|
+
* traffic-draining) and Fargate (ECS exposes only one per-service health
|
|
49
|
+
* signal, which is also a kill trigger). k8s readiness probes layer on top.
|
|
50
|
+
*
|
|
51
|
+
* @param bypassPaths Infra prefixes that always pass through even while
|
|
52
|
+
* NotReady. Defaults to {@link DEFAULT_READINESS_BYPASS}; pass a custom list
|
|
53
|
+
* when a business route would otherwise collide with a default prefix.
|
|
54
|
+
*/
|
|
55
|
+
export function readinessGuard(bypassPaths = DEFAULT_READINESS_BYPASS) {
|
|
56
|
+
return (req, res, next) => {
|
|
57
|
+
if (serviceReady || isBypassed(req.path, bypassPaths)) {
|
|
58
|
+
next();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
res.setHeader('Retry-After', '5');
|
|
62
|
+
sendError(res, 503, 'Service not ready — dependencies are still connecting');
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVhZGluZXNzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FwaS9yZWFkaW5lc3MudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQUV0QyxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFHdkQ7Ozs7Ozs7Ozs7Ozs7OztHQWVHO0FBQ0gsSUFBSSxZQUFZLEdBQUcsSUFBSSxDQUFDO0FBRXhCLG9FQUFvRTtBQUNwRSxNQUFNLFVBQVUsT0FBTztJQUNyQixPQUFPLFlBQVksQ0FBQztBQUN0QixDQUFDO0FBRUQsaURBQWlEO0FBQ2pELE1BQU0sVUFBVSxRQUFRLENBQUMsS0FBYztJQUNyQyxZQUFZLEdBQUcsS0FBSyxDQUFDO0FBQ3ZCLENBQUM7QUFFRDs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxDQUFDLE1BQU0sd0JBQXdCLEdBQUcsQ0FBQyxTQUFTLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsT0FBTyxFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBRXZHLFNBQVMsVUFBVSxDQUFDLElBQVksRUFBRSxNQUF5QjtJQUN6RCxPQUFPLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLElBQUksS0FBSyxDQUFDLElBQUksSUFBSSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztBQUNwRSxDQUFDO0FBRUQ7Ozs7Ozs7Ozs7Ozs7R0FhRztBQUNILE1BQU0sVUFBVSxjQUFjLENBQUMsV0FBVyxHQUFzQix3QkFBd0I7SUFDdEYsT0FBTyxDQUFDLEdBQVksRUFBRSxHQUFhLEVBQUUsSUFBSSxFQUFRLEVBQUU7UUFDakQsSUFBSSxZQUFZLElBQUksVUFBVSxDQUFDLEdBQUcsQ0FBQyxJQUFJLEVBQUUsV0FBVyxDQUFDLEVBQUUsQ0FBQztZQUN0RCxJQUFJLEVBQUUsQ0FBQztZQUNQLE9BQU87UUFDVCxDQUFDO1FBQ0QsR0FBRyxDQUFDLFNBQVMsQ0FBQyxhQUFhLEVBQUUsR0FBRyxDQUFDLENBQUM7UUFDbEMsU0FBUyxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsdURBQXVELENBQUMsQ0FBQztJQUMvRSxDQUFDLENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IHNlbmRFcnJvciB9IGZyb20gJ0BwaXBlbGluZS1idWlsZGVyL2FwaS1jb3JlJztcbmltcG9ydCB0eXBlIHsgUmVxdWVzdCwgUmVxdWVzdEhhbmRsZXIsIFJlc3BvbnNlIH0gZnJvbSAnZXhwcmVzcyc7XG5cbi8qKlxuICogU2VydmljZSByZWFkaW5lc3Mgc3RhdGUuXG4gKlxuICogRmxpcHBlZCBieSBzdGFydFNlcnZlcidzIGRlcGVuZGVuY3kgc3VwZXJ2aXNvcjogc2V0IGZhbHNlIHRoZSBpbnN0YW50IHdlXG4gKiBzdGFydCBsaXN0ZW5pbmcsIHRydWUgb25jZSB0aGUgYmFja2luZyBzdG9yZShzKSBjb25uZWN0LCBhbmQgYmFjayB0byBmYWxzZVxuICogaWYgdGhleSBsYXRlciBkcm9wLiBSZWFkIGJ5IHRoZSByZWFkaW5lc3MtZ3VhcmQgbWlkZGxld2FyZSAoYW5kIHRlc3RzKS5cbiAqXG4gKiBNb2R1bGUtbGV2ZWwgc2luZ2xldG9uOiBlYWNoIHNlcnZpY2UgcnVucyBhcyBpdHMgb3duIHByb2Nlc3Mgd2l0aCBleGFjdGx5XG4gKiBvbmUgc2VydmVyLCBzbyBhIHNpbmdsZSBmbGFnIGlzIHN1ZmZpY2llbnQgYW5kIGF2b2lkcyB0aHJlYWRpbmcgc3RhdGVcbiAqIHRocm91Z2ggY3JlYXRlQXBwICsgc3RhcnRTZXJ2ZXIuXG4gKlxuICogRGVmYXVsdHMgdG8gYHRydWVgIHNvIGFuIGFwcCBidWlsdCB3aXRoIGNyZWF0ZUFwcCBidXQgTk9UIHN0YXJ0ZWQgdmlhXG4gKiBzdGFydFNlcnZlciDigJQgZXZlcnkgcm91dGUgdW5pdCB0ZXN0LCB3aGljaCBkcml2ZXMgdGhlIGFwcCBkaXJlY3RseSB3aXRoXG4gKiBzdXBlcnRlc3Qg4oCUIGlzIG5ldmVyIGJsb2NrZWQgYnkgdGhlIGd1YXJkLiBzdGFydFNlcnZlciBmbGlwcyBpdCB0byBmYWxzZVxuICogc3luY2hyb25vdXNseSBiZWZvcmUgaXQgb3BlbnMgdGhlIHBvcnQsIHNvIHRoZSBwcm9kdWN0aW9uIHBhdGggaXMgZ2F0ZWQuXG4gKi9cbmxldCBzZXJ2aWNlUmVhZHkgPSB0cnVlO1xuXG4vKiogVHJ1ZSBvbmNlIGRlcGVuZGVuY2llcyBoYXZlIGNvbm5lY3RlZCAoYW5kIHJlbWFpbiBjb25uZWN0ZWQpLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGlzUmVhZHkoKTogYm9vbGVhbiB7XG4gIHJldHVybiBzZXJ2aWNlUmVhZHk7XG59XG5cbi8qKiBTZXQgYnkgdGhlIHN0YXJ0dXAgc3VwZXJ2aXNvciAoYW5kIHRlc3RzKS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzZXRSZWFkeShyZWFkeTogYm9vbGVhbik6IHZvaWQge1xuICBzZXJ2aWNlUmVhZHkgPSByZWFkeTtcbn1cblxuLyoqXG4gKiBEZWZhdWx0IGluZnJhIGVuZHBvaW50cyB0aGF0IG11c3Qgc3RheSByZWFjaGFibGUgd2hpbGUgTm90UmVhZHksIHNvXG4gKiBvcmNoZXN0cmF0b3JzIGNhbiBwcm9iZSBsaXZlbmVzcy9yZWFkaW5lc3MsIHNjcmFwZSBtZXRyaWNzLCB3YXJtIHBvb2xzLFxuICogc2VydmUgZG9jcywgYW5kIHJlbGF5IFNTRSBsb2dzIGR1cmluZyBzdGFydHVwIG9yIGEgZGVwZW5kZW5jeSBvdXRhZ2UuIFRoaXNcbiAqIG1hdGNoZXMgd2hhdCBgY3JlYXRlQXBwYCBtb3VudHMuIEEgc2VydmljZSB3aG9zZSBvd24gcm91dGVzIENPTExJREUgd2l0aCBvbmVcbiAqIG9mIHRoZXNlIHByZWZpeGVzIChlLmcuIHBsYXRmb3JtIHNlcnZlcyBhIHRlbmFudCBsb2cgQVBJIGF0IGAvbG9nc2ApIG11c3RcbiAqIHBhc3MgYSBuYXJyb3dlciBsaXN0IHRvIGByZWFkaW5lc3NHdWFyZCgpYCBzbyBpdHMgYnVzaW5lc3Mgcm91dGVzIHN0YXkgZ2F0ZWQuXG4gKi9cbmV4cG9ydCBjb25zdCBERUZBVUxUX1JFQURJTkVTU19CWVBBU1MgPSBbJy9oZWFsdGgnLCAnL3JlYWR5JywgJy9tZXRyaWNzJywgJy93YXJtdXAnLCAnL2RvY3MnLCAnL2xvZ3MnXTtcblxuZnVuY3Rpb24gaXNCeXBhc3NlZChwYXRoOiBzdHJpbmcsIGJ5cGFzczogcmVhZG9ubHkgc3RyaW5nW10pOiBib29sZWFuIHtcbiAgcmV0dXJuIGJ5cGFzcy5zb21lKChwKSA9PiBwYXRoID09PSBwIHx8IHBhdGguc3RhcnRzV2l0aChgJHtwfS9gKSk7XG59XG5cbi8qKlxuICogUmVhZGluZXNzIGd1YXJkIOKAlCB3aGlsZSB0aGUgc2VydmljZSBpcyBOb3RSZWFkeSAoZGVwZW5kZW5jaWVzIG5vdCB5ZXRcbiAqIGNvbm5lY3RlZCwgb3IgZHJvcHBlZCksIHJlamVjdCBidXNpbmVzcyByZXF1ZXN0cyB3aXRoIDUwMyArIFJldHJ5LUFmdGVyXG4gKiBpbnN0ZWFkIG9mIGxldHRpbmcgdGhlbSBoaXQgYSBkaXNjb25uZWN0ZWQgZGF0YXN0b3JlIGFuZCA1MDAuXG4gKlxuICogVGhpcyBpcyB0aGUgcG9ydGFibGUsIGFwcC1sZXZlbCByZWFkaW5lc3MgbWVjaGFuaXNtIHRoYXQgd29ya3Mgb24gZXZlcnlcbiAqIGRlcGxveSB0YXJnZXQg4oCUIGluY2x1ZGluZyBEb2NrZXIgQ29tcG9zZSAoc2luZ2xlIHJlcGxpY2EsIG5vIG9yY2hlc3RyYXRvclxuICogdHJhZmZpYy1kcmFpbmluZykgYW5kIEZhcmdhdGUgKEVDUyBleHBvc2VzIG9ubHkgb25lIHBlci1zZXJ2aWNlIGhlYWx0aFxuICogc2lnbmFsLCB3aGljaCBpcyBhbHNvIGEga2lsbCB0cmlnZ2VyKS4gazhzIHJlYWRpbmVzcyBwcm9iZXMgbGF5ZXIgb24gdG9wLlxuICpcbiAqIEBwYXJhbSBieXBhc3NQYXRocyBJbmZyYSBwcmVmaXhlcyB0aGF0IGFsd2F5cyBwYXNzIHRocm91Z2ggZXZlbiB3aGlsZVxuICogICBOb3RSZWFkeS4gRGVmYXVsdHMgdG8ge0BsaW5rIERFRkFVTFRfUkVBRElORVNTX0JZUEFTU307IHBhc3MgYSBjdXN0b20gbGlzdFxuICogICB3aGVuIGEgYnVzaW5lc3Mgcm91dGUgd291bGQgb3RoZXJ3aXNlIGNvbGxpZGUgd2l0aCBhIGRlZmF1bHQgcHJlZml4LlxuICovXG5leHBvcnQgZnVuY3Rpb24gcmVhZGluZXNzR3VhcmQoYnlwYXNzUGF0aHM6IHJlYWRvbmx5IHN0cmluZ1tdID0gREVGQVVMVF9SRUFESU5FU1NfQllQQVNTKTogUmVxdWVzdEhhbmRsZXIge1xuICByZXR1cm4gKHJlcTogUmVxdWVzdCwgcmVzOiBSZXNwb25zZSwgbmV4dCk6IHZvaWQgPT4ge1xuICAgIGlmIChzZXJ2aWNlUmVhZHkgfHwgaXNCeXBhc3NlZChyZXEucGF0aCwgYnlwYXNzUGF0aHMpKSB7XG4gICAgICBuZXh0KCk7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIHJlcy5zZXRIZWFkZXIoJ1JldHJ5LUFmdGVyJywgJzUnKTtcbiAgICBzZW5kRXJyb3IocmVzLCA1MDMsICdTZXJ2aWNlIG5vdCByZWFkeSDigJQgZGVwZW5kZW5jaWVzIGFyZSBzdGlsbCBjb25uZWN0aW5nJyk7XG4gIH07XG59XG4iXX0=
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type RequestIdentity } from '@pipeline-builder/api-core';
|
|
2
|
+
import type { Request } from 'express';
|
|
3
|
+
import { SSEManager, type SSEEventType } from '../http/sse-connection-manager.js';
|
|
4
|
+
declare global {
|
|
5
|
+
namespace Express {
|
|
6
|
+
interface Request {
|
|
7
|
+
/** Unique request ID (set by app-factory middleware) */
|
|
8
|
+
requestId?: string;
|
|
9
|
+
/** Request context with identity, logging, and SSE */
|
|
10
|
+
context?: RequestContext;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Request logger function type
|
|
16
|
+
*/
|
|
17
|
+
export type RequestLogger = (type: SSEEventType, message: string, data?: unknown) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Request context with identity and logging
|
|
20
|
+
*/
|
|
21
|
+
export interface RequestContext {
|
|
22
|
+
/** Unique request ID */
|
|
23
|
+
requestId: string;
|
|
24
|
+
/** Identity from headers */
|
|
25
|
+
identity: RequestIdentity;
|
|
26
|
+
/** Logging function that sends to console and SSE */
|
|
27
|
+
log: RequestLogger;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a request context with identity and logging
|
|
31
|
+
*
|
|
32
|
+
* Creates a logger that outputs to both console and SSE.
|
|
33
|
+
*
|
|
34
|
+
* @param req - Express request
|
|
35
|
+
* @param sseManager - SSE manager for real-time logs
|
|
36
|
+
* @returns Request context with identity and logger
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* app.post('/api/resource', requireAuth, async (req, res) => {
|
|
41
|
+
* const ctx = createRequestContext(req, sseManager);
|
|
42
|
+
*
|
|
43
|
+
* ctx.log('INFO', 'Processing request', { data: req.body });
|
|
44
|
+
*
|
|
45
|
+
* if (!ctx.identity.orgId) {
|
|
46
|
+
* ctx.log('ERROR', 'Missing organization ID');
|
|
47
|
+
* return sendBadRequest(res, 'x-org-id header required');
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* // Process request...
|
|
51
|
+
* ctx.log('COMPLETED', 'Request processed successfully');
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function createRequestContext(req: Request, sseManager: SSEManager): RequestContext;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { getIdentity, createLogger, redactSensitive } from '@pipeline-builder/api-core';
|
|
4
|
+
import { v7 as uuid } from 'uuid';
|
|
5
|
+
import { SSEManager } from '../http/sse-connection-manager.js';
|
|
6
|
+
const logger = createLogger('api-server');
|
|
7
|
+
/**
|
|
8
|
+
* Create a request context with identity and logging
|
|
9
|
+
*
|
|
10
|
+
* Creates a logger that outputs to both console and SSE.
|
|
11
|
+
*
|
|
12
|
+
* @param req - Express request
|
|
13
|
+
* @param sseManager - SSE manager for real-time logs
|
|
14
|
+
* @returns Request context with identity and logger
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* app.post('/api/resource', requireAuth, async (req, res) => {
|
|
19
|
+
* const ctx = createRequestContext(req, sseManager);
|
|
20
|
+
*
|
|
21
|
+
* ctx.log('INFO', 'Processing request', { data: req.body });
|
|
22
|
+
*
|
|
23
|
+
* if (!ctx.identity.orgId) {
|
|
24
|
+
* ctx.log('ERROR', 'Missing organization ID');
|
|
25
|
+
* return sendBadRequest(res, 'x-org-id header required');
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* // Process request...
|
|
29
|
+
* ctx.log('COMPLETED', 'Request processed successfully');
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function createRequestContext(req, sseManager) {
|
|
34
|
+
const identity = getIdentity(req);
|
|
35
|
+
// Prefer the already-parsed requestId from app-factory middleware,
|
|
36
|
+
// fall back to identity header, then generate a new one.
|
|
37
|
+
const requestId = req.requestId || identity.requestId || uuid();
|
|
38
|
+
// Create logger that outputs to Winston and SSE
|
|
39
|
+
const log = (type, message, data) => {
|
|
40
|
+
const meta = { requestId, orgId: identity.orgId, type, data };
|
|
41
|
+
switch (type) {
|
|
42
|
+
case 'ERROR':
|
|
43
|
+
logger.error(message, meta);
|
|
44
|
+
break;
|
|
45
|
+
case 'WARN':
|
|
46
|
+
logger.warn(message, meta);
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
logger.info(message, meta);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
// The Winston call above is redacted by the logger's `redactFormat`, but the
|
|
53
|
+
// SSE frame bypasses the logger entirely — apply the SAME key-pattern
|
|
54
|
+
// redaction to `data` before it's pushed to the client so a secret in the
|
|
55
|
+
// log payload (e.g. an accidental `token`/`password` field) isn't streamed
|
|
56
|
+
// out over the build-log SSE channel.
|
|
57
|
+
sseManager.send(requestId, type, message, redactSensitive(data));
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
requestId,
|
|
61
|
+
identity,
|
|
62
|
+
log,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVxdWVzdC10eXBlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hcGkvcmVxdWVzdC10eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLE9BQU8sRUFBRSxXQUFXLEVBQUUsWUFBWSxFQUFFLGVBQWUsRUFBMEMsTUFBTSw0QkFBNEIsQ0FBQztBQUVoSSxPQUFPLEVBQUUsRUFBRSxJQUFJLElBQUksRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUNsQyxPQUFPLEVBQUUsVUFBVSxFQUFxQixNQUFNLG1DQUFtQyxDQUFDO0FBY2xGLE1BQU0sTUFBTSxHQUFHLFlBQVksQ0FBQyxZQUFZLENBQUMsQ0FBQztBQW1CMUM7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0F5Qkc7QUFDSCxNQUFNLFVBQVUsb0JBQW9CLENBQ2xDLEdBQVksRUFDWixVQUFzQjtJQUV0QixNQUFNLFFBQVEsR0FBRyxXQUFXLENBQUMsR0FBa0IsQ0FBQyxDQUFDO0lBQ2pELG1FQUFtRTtJQUNuRSx5REFBeUQ7SUFDekQsTUFBTSxTQUFTLEdBQUcsR0FBRyxDQUFDLFNBQVMsSUFBSSxRQUFRLENBQUMsU0FBUyxJQUFJLElBQUksRUFBRSxDQUFDO0lBRWhFLGdEQUFnRDtJQUNoRCxNQUFNLEdBQUcsR0FBa0IsQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxFQUFFO1FBQ2pELE1BQU0sSUFBSSxHQUFHLEVBQUUsU0FBUyxFQUFFLEtBQUssRUFBRSxRQUFRLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsQ0FBQztRQUM5RCxRQUFRLElBQUksRUFBRSxDQUFDO1lBQ2IsS0FBSyxPQUFPO2dCQUNWLE1BQU0sQ0FBQyxLQUFLLENBQUMsT0FBTyxFQUFFLElBQUksQ0FBQyxDQUFDO2dCQUM1QixNQUFNO1lBQ1IsS0FBSyxNQUFNO2dCQUNULE1BQU0sQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLElBQUksQ0FBQyxDQUFDO2dCQUMzQixNQUFNO1lBQ1I7Z0JBQ0UsTUFBTSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsSUFBSSxDQUFDLENBQUM7Z0JBQzNCLE1BQU07UUFDVixDQUFDO1FBQ0QsNkVBQTZFO1FBQzdFLHNFQUFzRTtRQUN0RSwwRUFBMEU7UUFDMUUsMkVBQTJFO1FBQzNFLHNDQUFzQztRQUN0QyxVQUFVLENBQUMsSUFBSSxDQUFDLFNBQVMsRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLGVBQWUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQ25FLENBQUMsQ0FBQztJQUVGLE9BQU87UUFDTCxTQUFTO1FBQ1QsUUFBUTtRQUNSLEdBQUc7S0FDSixDQUFDO0FBQ0osQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG5pbXBvcnQgeyBnZXRJZGVudGl0eSwgY3JlYXRlTG9nZ2VyLCByZWRhY3RTZW5zaXRpdmUsIHR5cGUgUmVxdWVzdElkZW50aXR5LCB0eXBlIEh0dHBSZXF1ZXN0IH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvYXBpLWNvcmUnO1xuaW1wb3J0IHR5cGUgeyBSZXF1ZXN0IH0gZnJvbSAnZXhwcmVzcyc7XG5pbXBvcnQgeyB2NyBhcyB1dWlkIH0gZnJvbSAndXVpZCc7XG5pbXBvcnQgeyBTU0VNYW5hZ2VyLCB0eXBlIFNTRUV2ZW50VHlwZSB9IGZyb20gJy4uL2h0dHAvc3NlLWNvbm5lY3Rpb24tbWFuYWdlci5qcyc7XG5cbi8vIENvbnNvbGlkYXRlZCBFeHByZXNzIFJlcXVlc3QgYXVnbWVudGF0aW9uc1xuZGVjbGFyZSBnbG9iYWwge1xuICBuYW1lc3BhY2UgRXhwcmVzcyB7XG4gICAgaW50ZXJmYWNlIFJlcXVlc3Qge1xuICAgICAgLyoqIFVuaXF1ZSByZXF1ZXN0IElEIChzZXQgYnkgYXBwLWZhY3RvcnkgbWlkZGxld2FyZSkgKi9cbiAgICAgIHJlcXVlc3RJZD86IHN0cmluZztcbiAgICAgIC8qKiBSZXF1ZXN0IGNvbnRleHQgd2l0aCBpZGVudGl0eSwgbG9nZ2luZywgYW5kIFNTRSAqL1xuICAgICAgY29udGV4dD86IFJlcXVlc3RDb250ZXh0O1xuICAgIH1cbiAgfVxufVxuXG5jb25zdCBsb2dnZXIgPSBjcmVhdGVMb2dnZXIoJ2FwaS1zZXJ2ZXInKTtcblxuLyoqXG4gKiBSZXF1ZXN0IGxvZ2dlciBmdW5jdGlvbiB0eXBlXG4gKi9cbmV4cG9ydCB0eXBlIFJlcXVlc3RMb2dnZXIgPSAodHlwZTogU1NFRXZlbnRUeXBlLCBtZXNzYWdlOiBzdHJpbmcsIGRhdGE/OiB1bmtub3duKSA9PiB2b2lkO1xuXG4vKipcbiAqIFJlcXVlc3QgY29udGV4dCB3aXRoIGlkZW50aXR5IGFuZCBsb2dnaW5nXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUmVxdWVzdENvbnRleHQge1xuICAvKiogVW5pcXVlIHJlcXVlc3QgSUQgKi9cbiAgcmVxdWVzdElkOiBzdHJpbmc7XG4gIC8qKiBJZGVudGl0eSBmcm9tIGhlYWRlcnMgKi9cbiAgaWRlbnRpdHk6IFJlcXVlc3RJZGVudGl0eTtcbiAgLyoqIExvZ2dpbmcgZnVuY3Rpb24gdGhhdCBzZW5kcyB0byBjb25zb2xlIGFuZCBTU0UgKi9cbiAgbG9nOiBSZXF1ZXN0TG9nZ2VyO1xufVxuXG4vKipcbiAqIENyZWF0ZSBhIHJlcXVlc3QgY29udGV4dCB3aXRoIGlkZW50aXR5IGFuZCBsb2dnaW5nXG4gKlxuICogQ3JlYXRlcyBhIGxvZ2dlciB0aGF0IG91dHB1dHMgdG8gYm90aCBjb25zb2xlIGFuZCBTU0UuXG4gKlxuICogQHBhcmFtIHJlcSAtIEV4cHJlc3MgcmVxdWVzdFxuICogQHBhcmFtIHNzZU1hbmFnZXIgLSBTU0UgbWFuYWdlciBmb3IgcmVhbC10aW1lIGxvZ3NcbiAqIEByZXR1cm5zIFJlcXVlc3QgY29udGV4dCB3aXRoIGlkZW50aXR5IGFuZCBsb2dnZXJcbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogYXBwLnBvc3QoJy9hcGkvcmVzb3VyY2UnLCByZXF1aXJlQXV0aCwgYXN5bmMgKHJlcSwgcmVzKSA9PiB7XG4gKiAgIGNvbnN0IGN0eCA9IGNyZWF0ZVJlcXVlc3RDb250ZXh0KHJlcSwgc3NlTWFuYWdlcik7XG4gKlxuICogICBjdHgubG9nKCdJTkZPJywgJ1Byb2Nlc3NpbmcgcmVxdWVzdCcsIHsgZGF0YTogcmVxLmJvZHkgfSk7XG4gKlxuICogICBpZiAoIWN0eC5pZGVudGl0eS5vcmdJZCkge1xuICogICAgIGN0eC5sb2coJ0VSUk9SJywgJ01pc3Npbmcgb3JnYW5pemF0aW9uIElEJyk7XG4gKiAgICAgcmV0dXJuIHNlbmRCYWRSZXF1ZXN0KHJlcywgJ3gtb3JnLWlkIGhlYWRlciByZXF1aXJlZCcpO1xuICogICB9XG4gKlxuICogICAvLyBQcm9jZXNzIHJlcXVlc3QuLi5cbiAqICAgY3R4LmxvZygnQ09NUExFVEVEJywgJ1JlcXVlc3QgcHJvY2Vzc2VkIHN1Y2Nlc3NmdWxseScpO1xuICogfSk7XG4gKiBgYGBcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZVJlcXVlc3RDb250ZXh0KFxuICByZXE6IFJlcXVlc3QsXG4gIHNzZU1hbmFnZXI6IFNTRU1hbmFnZXIsXG4pOiBSZXF1ZXN0Q29udGV4dCB7XG4gIGNvbnN0IGlkZW50aXR5ID0gZ2V0SWRlbnRpdHkocmVxIGFzIEh0dHBSZXF1ZXN0KTtcbiAgLy8gUHJlZmVyIHRoZSBhbHJlYWR5LXBhcnNlZCByZXF1ZXN0SWQgZnJvbSBhcHAtZmFjdG9yeSBtaWRkbGV3YXJlLFxuICAvLyBmYWxsIGJhY2sgdG8gaWRlbnRpdHkgaGVhZGVyLCB0aGVuIGdlbmVyYXRlIGEgbmV3IG9uZS5cbiAgY29uc3QgcmVxdWVzdElkID0gcmVxLnJlcXVlc3RJZCB8fCBpZGVudGl0eS5yZXF1ZXN0SWQgfHwgdXVpZCgpO1xuXG4gIC8vIENyZWF0ZSBsb2dnZXIgdGhhdCBvdXRwdXRzIHRvIFdpbnN0b24gYW5kIFNTRVxuICBjb25zdCBsb2c6IFJlcXVlc3RMb2dnZXIgPSAodHlwZSwgbWVzc2FnZSwgZGF0YSkgPT4ge1xuICAgIGNvbnN0IG1ldGEgPSB7IHJlcXVlc3RJZCwgb3JnSWQ6IGlkZW50aXR5Lm9yZ0lkLCB0eXBlLCBkYXRhIH07XG4gICAgc3dpdGNoICh0eXBlKSB7XG4gICAgICBjYXNlICdFUlJPUic6XG4gICAgICAgIGxvZ2dlci5lcnJvcihtZXNzYWdlLCBtZXRhKTtcbiAgICAgICAgYnJlYWs7XG4gICAgICBjYXNlICdXQVJOJzpcbiAgICAgICAgbG9nZ2VyLndhcm4obWVzc2FnZSwgbWV0YSk7XG4gICAgICAgIGJyZWFrO1xuICAgICAgZGVmYXVsdDpcbiAgICAgICAgbG9nZ2VyLmluZm8obWVzc2FnZSwgbWV0YSk7XG4gICAgICAgIGJyZWFrO1xuICAgIH1cbiAgICAvLyBUaGUgV2luc3RvbiBjYWxsIGFib3ZlIGlzIHJlZGFjdGVkIGJ5IHRoZSBsb2dnZXIncyBgcmVkYWN0Rm9ybWF0YCwgYnV0IHRoZVxuICAgIC8vIFNTRSBmcmFtZSBieXBhc3NlcyB0aGUgbG9nZ2VyIGVudGlyZWx5IOKAlCBhcHBseSB0aGUgU0FNRSBrZXktcGF0dGVyblxuICAgIC8vIHJlZGFjdGlvbiB0byBgZGF0YWAgYmVmb3JlIGl0J3MgcHVzaGVkIHRvIHRoZSBjbGllbnQgc28gYSBzZWNyZXQgaW4gdGhlXG4gICAgLy8gbG9nIHBheWxvYWQgKGUuZy4gYW4gYWNjaWRlbnRhbCBgdG9rZW5gL2BwYXNzd29yZGAgZmllbGQpIGlzbid0IHN0cmVhbWVkXG4gICAgLy8gb3V0IG92ZXIgdGhlIGJ1aWxkLWxvZyBTU0UgY2hhbm5lbC5cbiAgICBzc2VNYW5hZ2VyLnNlbmQocmVxdWVzdElkLCB0eXBlLCBtZXNzYWdlLCByZWRhY3RTZW5zaXRpdmUoZGF0YSkpO1xuICB9O1xuXG4gIHJldHVybiB7XG4gICAgcmVxdWVzdElkLFxuICAgIGlkZW50aXR5LFxuICAgIGxvZyxcbiAgfTtcbn1cbiJdfQ==
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Create middleware that validates the request has an orgId in the identity headers.
|
|
4
|
+
*
|
|
5
|
+
* @returns Express middleware
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const { app, sseManager } = createApp();
|
|
10
|
+
*
|
|
11
|
+
* app.get('/pipelines', requireAuth, requireOrgId(), handler);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare function requireOrgId(): (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { ErrorCode, sendError } from '@pipeline-builder/api-core';
|
|
4
|
+
import { getContext } from './get-context.js';
|
|
5
|
+
/**
|
|
6
|
+
* Create middleware that validates the request has an orgId in the identity headers.
|
|
7
|
+
*
|
|
8
|
+
* @returns Express middleware
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const { app, sseManager } = createApp();
|
|
13
|
+
*
|
|
14
|
+
* app.get('/pipelines', requireAuth, requireOrgId(), handler);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function requireOrgId() {
|
|
18
|
+
return (req, res, next) => {
|
|
19
|
+
const ctx = getContext(req);
|
|
20
|
+
if (!ctx.identity.orgId) {
|
|
21
|
+
ctx.log('ERROR', 'Organization ID is missing from request headers');
|
|
22
|
+
sendError(res, 400, 'Organization ID is required. Please provide x-org-id header.', ErrorCode.VALIDATION_ERROR);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
next();
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVxdWlyZS1vcmctaWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvYXBpL3JlcXVpcmUtb3JnLWlkLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEMsT0FBTyxFQUFFLFNBQVMsRUFBRSxTQUFTLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUVsRSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFOUM7Ozs7Ozs7Ozs7O0dBV0c7QUFDSCxNQUFNLFVBQVUsWUFBWTtJQUMxQixPQUFPLENBQUMsR0FBWSxFQUFFLEdBQWEsRUFBRSxJQUFrQixFQUFRLEVBQUU7UUFDL0QsTUFBTSxHQUFHLEdBQUcsVUFBVSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBRTVCLElBQUksQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQ3hCLEdBQUcsQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLGlEQUFpRCxDQUFDLENBQUM7WUFDcEUsU0FBUyxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsOERBQThELEVBQUUsU0FBUyxDQUFDLGdCQUFnQixDQUFDLENBQUM7WUFDaEgsT0FBTztRQUNULENBQUM7UUFFRCxJQUFJLEVBQUUsQ0FBQztJQUNULENBQUMsQ0FBQztBQUNKLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuaW1wb3J0IHsgRXJyb3JDb2RlLCBzZW5kRXJyb3IgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgdHlwZSB7IFJlcXVlc3QsIFJlc3BvbnNlLCBOZXh0RnVuY3Rpb24gfSBmcm9tICdleHByZXNzJztcbmltcG9ydCB7IGdldENvbnRleHQgfSBmcm9tICcuL2dldC1jb250ZXh0LmpzJztcblxuLyoqXG4gKiBDcmVhdGUgbWlkZGxld2FyZSB0aGF0IHZhbGlkYXRlcyB0aGUgcmVxdWVzdCBoYXMgYW4gb3JnSWQgaW4gdGhlIGlkZW50aXR5IGhlYWRlcnMuXG4gKlxuICogQHJldHVybnMgRXhwcmVzcyBtaWRkbGV3YXJlXG4gKlxuICogQGV4YW1wbGVcbiAqIGBgYHR5cGVzY3JpcHRcbiAqIGNvbnN0IHsgYXBwLCBzc2VNYW5hZ2VyIH0gPSBjcmVhdGVBcHAoKTtcbiAqXG4gKiBhcHAuZ2V0KCcvcGlwZWxpbmVzJywgcmVxdWlyZUF1dGgsIHJlcXVpcmVPcmdJZCgpLCBoYW5kbGVyKTtcbiAqIGBgYFxuICovXG5leHBvcnQgZnVuY3Rpb24gcmVxdWlyZU9yZ0lkKCkge1xuICByZXR1cm4gKHJlcTogUmVxdWVzdCwgcmVzOiBSZXNwb25zZSwgbmV4dDogTmV4dEZ1bmN0aW9uKTogdm9pZCA9PiB7XG4gICAgY29uc3QgY3R4ID0gZ2V0Q29udGV4dChyZXEpO1xuXG4gICAgaWYgKCFjdHguaWRlbnRpdHkub3JnSWQpIHtcbiAgICAgIGN0eC5sb2coJ0VSUk9SJywgJ09yZ2FuaXphdGlvbiBJRCBpcyBtaXNzaW5nIGZyb20gcmVxdWVzdCBoZWFkZXJzJyk7XG4gICAgICBzZW5kRXJyb3IocmVzLCA0MDAsICdPcmdhbml6YXRpb24gSUQgaXMgcmVxdWlyZWQuIFBsZWFzZSBwcm92aWRlIHgtb3JnLWlkIGhlYWRlci4nLCBFcnJvckNvZGUuVkFMSURBVElPTl9FUlJPUik7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgbmV4dCgpO1xuICB9O1xufVxuIl19
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Request, Response, RequestHandler } from 'express';
|
|
2
|
+
import type { RequestContext } from './request-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Context object passed to every route handler wrapped with `withRoute()`.
|
|
5
|
+
*/
|
|
6
|
+
export interface RouteContext {
|
|
7
|
+
/** Original Express request */
|
|
8
|
+
req: Request;
|
|
9
|
+
/** Original Express response */
|
|
10
|
+
res: Response;
|
|
11
|
+
/** Request context with identity and logging */
|
|
12
|
+
ctx: RequestContext;
|
|
13
|
+
/** Lowercased organization ID (guaranteed non-empty when requireOrgId is true) */
|
|
14
|
+
orgId: string;
|
|
15
|
+
/** User ID from JWT (defaults to empty string) */
|
|
16
|
+
userId: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Options for `withRoute()`.
|
|
20
|
+
*/
|
|
21
|
+
export interface WithRouteOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Whether to require orgId on the request.
|
|
24
|
+
* When true (default), returns 400 if orgId is missing.
|
|
25
|
+
* Set to false for routes that don't need org context.
|
|
26
|
+
*/
|
|
27
|
+
requireOrgId?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Wrap an async route handler with standard boilerplate.
|
|
31
|
+
*
|
|
32
|
+
* Extracts context, orgId, userId from the request, validates orgId,
|
|
33
|
+
* and catches errors. Typed `AppError` subclasses are automatically
|
|
34
|
+
* mapped to the correct HTTP response.
|
|
35
|
+
*
|
|
36
|
+
* @param handler - Async function receiving a RouteContext
|
|
37
|
+
* @param options - Configuration options
|
|
38
|
+
* @returns Express RequestHandler
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* router.get('/:id', withRoute(async ({ req, res, ctx, orgId }) => {
|
|
43
|
+
* const id = getParam(req.params, 'id');
|
|
44
|
+
* const result = await pipelineService.findById(id, orgId);
|
|
45
|
+
* if (!result) throw new NotFoundError('Pipeline not found');
|
|
46
|
+
* return sendSuccess(res, 200, { pipeline: result });
|
|
47
|
+
* }));
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function withRoute(handler: (rc: RouteContext) => Promise<void>, options?: WithRouteOptions): RequestHandler;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { AppError, errorMessage, extractDbError, sendError, sendBadRequest, sendInternalError, createLogger, } from '@pipeline-builder/api-core';
|
|
4
|
+
import { getContext } from './get-context.js';
|
|
5
|
+
const logger = createLogger('route-wrapper');
|
|
6
|
+
/**
|
|
7
|
+
* A log-safe rendering of an unknown error. drizzle-orm's `DrizzleQueryError`
|
|
8
|
+
* embeds the failed SQL statement AND the bound parameter values (real data) in
|
|
9
|
+
* its `.message` (`Failed query: <sql>\nparams: <values>`). Never put that in a
|
|
10
|
+
* response or a log line — collapse it to the error name. Other errors are safe.
|
|
11
|
+
*/
|
|
12
|
+
function safeErrorForLog(error) {
|
|
13
|
+
if (error instanceof Error && error.message.startsWith('Failed query:')) {
|
|
14
|
+
return `${error.name || 'DrizzleQueryError'}: database query failed`;
|
|
15
|
+
}
|
|
16
|
+
return errorMessage(error);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wrap an async route handler with standard boilerplate.
|
|
20
|
+
*
|
|
21
|
+
* Extracts context, orgId, userId from the request, validates orgId,
|
|
22
|
+
* and catches errors. Typed `AppError` subclasses are automatically
|
|
23
|
+
* mapped to the correct HTTP response.
|
|
24
|
+
*
|
|
25
|
+
* @param handler - Async function receiving a RouteContext
|
|
26
|
+
* @param options - Configuration options
|
|
27
|
+
* @returns Express RequestHandler
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* router.get('/:id', withRoute(async ({ req, res, ctx, orgId }) => {
|
|
32
|
+
* const id = getParam(req.params, 'id');
|
|
33
|
+
* const result = await pipelineService.findById(id, orgId);
|
|
34
|
+
* if (!result) throw new NotFoundError('Pipeline not found');
|
|
35
|
+
* return sendSuccess(res, 200, { pipeline: result });
|
|
36
|
+
* }));
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function withRoute(handler, options = {}) {
|
|
40
|
+
const { requireOrgId = true } = options;
|
|
41
|
+
return async (req, res) => {
|
|
42
|
+
const ctx = getContext(req);
|
|
43
|
+
// `identity.orgId` is already normalized (trimmed + lowercased) once at
|
|
44
|
+
// resolution in api-core's `getIdentity`, so the previously-duplicated
|
|
45
|
+
// `.toLowerCase()` here is redundant — kept normalization at the single
|
|
46
|
+
// source of truth instead. This value now matches the RLS GUC exactly.
|
|
47
|
+
const orgId = ctx.identity.orgId || '';
|
|
48
|
+
const userId = ctx.identity.userId || '';
|
|
49
|
+
if (requireOrgId && !orgId) {
|
|
50
|
+
return sendBadRequest(res, 'Organization ID is required');
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await handler({ req, res, ctx, orgId, userId });
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// Don't send another response if one was already sent
|
|
57
|
+
if (res.headersSent) {
|
|
58
|
+
logger.error('Route handler error after response sent', {
|
|
59
|
+
requestId: ctx.requestId,
|
|
60
|
+
error: safeErrorForLog(error),
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (error instanceof AppError) {
|
|
65
|
+
const status = error.statusCode >= 400 && error.statusCode < 600
|
|
66
|
+
? error.statusCode
|
|
67
|
+
: 500;
|
|
68
|
+
return sendError(res, status, error.message, error.code);
|
|
69
|
+
}
|
|
70
|
+
// Unhandled error → 500. NEVER echo the raw message to the client: DB driver
|
|
71
|
+
// errors carry the SQL + bound params, and CrudService deliberately lets them
|
|
72
|
+
// propagate here. Send a generic message + requestId (to correlate with logs),
|
|
73
|
+
// and log only sanitized DB metadata (code/constraint/table — no SQL/params).
|
|
74
|
+
const db = extractDbError(error);
|
|
75
|
+
ctx.log('ERROR', 'Request failed', {
|
|
76
|
+
requestId: ctx.requestId,
|
|
77
|
+
error: safeErrorForLog(error),
|
|
78
|
+
...(Object.keys(db).length ? { db } : {}),
|
|
79
|
+
});
|
|
80
|
+
return sendInternalError(res, 'Internal server error', { requestId: ctx.requestId });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUtd3JhcHBlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hcGkvcm91dGUtd3JhcHBlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLE9BQU8sRUFDTCxRQUFRLEVBQ1IsWUFBWSxFQUNaLGNBQWMsRUFDZCxTQUFTLEVBQ1QsY0FBYyxFQUNkLGlCQUFpQixFQUNqQixZQUFZLEdBQ2IsTUFBTSw0QkFBNEIsQ0FBQztBQUVwQyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFHOUMsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLGVBQWUsQ0FBQyxDQUFDO0FBRTdDOzs7OztHQUtHO0FBQ0gsU0FBUyxlQUFlLENBQUMsS0FBYztJQUNyQyxJQUFJLEtBQUssWUFBWSxLQUFLLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsZUFBZSxDQUFDLEVBQUUsQ0FBQztRQUN4RSxPQUFPLEdBQUcsS0FBSyxDQUFDLElBQUksSUFBSSxtQkFBbUIseUJBQXlCLENBQUM7SUFDdkUsQ0FBQztJQUNELE9BQU8sWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQzdCLENBQUM7QUE4QkQ7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBb0JHO0FBQ0gsTUFBTSxVQUFVLFNBQVMsQ0FDdkIsT0FBNEMsRUFDNUMsT0FBTyxHQUFxQixFQUFFO0lBRTlCLE1BQU0sRUFBRSxZQUFZLEdBQUcsSUFBSSxFQUFFLEdBQUcsT0FBTyxDQUFDO0lBRXhDLE9BQU8sS0FBSyxFQUFFLEdBQVksRUFBRSxHQUFhLEVBQUUsRUFBRTtRQUMzQyxNQUFNLEdBQUcsR0FBRyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDNUIsd0VBQXdFO1FBQ3hFLHVFQUF1RTtRQUN2RSx3RUFBd0U7UUFDeEUsdUVBQXVFO1FBQ3ZFLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxRQUFRLENBQUMsS0FBSyxJQUFJLEVBQUUsQ0FBQztRQUN2QyxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsUUFBUSxDQUFDLE1BQU0sSUFBSSxFQUFFLENBQUM7UUFFekMsSUFBSSxZQUFZLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztZQUMzQixPQUFPLGNBQWMsQ0FBQyxHQUFHLEVBQUUsNkJBQTZCLENBQUMsQ0FBQztRQUM1RCxDQUFDO1FBRUQsSUFBSSxDQUFDO1lBQ0gsTUFBTSxPQUFPLENBQUMsRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLENBQUMsQ0FBQztRQUNsRCxDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLHNEQUFzRDtZQUN0RCxJQUFJLEdBQUcsQ0FBQyxXQUFXLEVBQUUsQ0FBQztnQkFDcEIsTUFBTSxDQUFDLEtBQUssQ0FBQyx5Q0FBeUMsRUFBRTtvQkFDdEQsU0FBUyxFQUFFLEdBQUcsQ0FBQyxTQUFTO29CQUN4QixLQUFLLEVBQUUsZUFBZSxDQUFDLEtBQUssQ0FBQztpQkFDOUIsQ0FBQyxDQUFDO2dCQUNILE9BQU87WUFDVCxDQUFDO1lBRUQsSUFBSSxLQUFLLFlBQVksUUFBUSxFQUFFLENBQUM7Z0JBQzlCLE1BQU0sTUFBTSxHQUFHLEtBQUssQ0FBQyxVQUFVLElBQUksR0FBRyxJQUFJLEtBQUssQ0FBQyxVQUFVLEdBQUcsR0FBRztvQkFDOUQsQ0FBQyxDQUFDLEtBQUssQ0FBQyxVQUFVO29CQUNsQixDQUFDLENBQUMsR0FBRyxDQUFDO2dCQUNSLE9BQU8sU0FBUyxDQUFDLEdBQUcsRUFBRSxNQUFNLEVBQUUsS0FBSyxDQUFDLE9BQU8sRUFBRSxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDM0QsQ0FBQztZQUVELDZFQUE2RTtZQUM3RSw4RUFBOEU7WUFDOUUsK0VBQStFO1lBQy9FLDhFQUE4RTtZQUM5RSxNQUFNLEVBQUUsR0FBRyxjQUFjLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDakMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUU7Z0JBQ2pDLFNBQVMsRUFBRSxHQUFHLENBQUMsU0FBUztnQkFDeEIsS0FBSyxFQUFFLGVBQWUsQ0FBQyxLQUFLLENBQUM7Z0JBQzdCLEdBQUcsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO2FBQzFDLENBQUMsQ0FBQztZQUNILE9BQU8saUJBQWlCLENBQUMsR0FBRyxFQUFFLHVCQUF1QixFQUFFLEVBQUUsU0FBUyxFQUFFLEdBQUcsQ0FBQyxTQUFTLEVBQUUsQ0FBQyxDQUFDO1FBQ3ZGLENBQUM7SUFDSCxDQUFDLENBQUM7QUFDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7XG4gIEFwcEVycm9yLFxuICBlcnJvck1lc3NhZ2UsXG4gIGV4dHJhY3REYkVycm9yLFxuICBzZW5kRXJyb3IsXG4gIHNlbmRCYWRSZXF1ZXN0LFxuICBzZW5kSW50ZXJuYWxFcnJvcixcbiAgY3JlYXRlTG9nZ2VyLFxufSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgdHlwZSB7IFJlcXVlc3QsIFJlc3BvbnNlLCBSZXF1ZXN0SGFuZGxlciB9IGZyb20gJ2V4cHJlc3MnO1xuaW1wb3J0IHsgZ2V0Q29udGV4dCB9IGZyb20gJy4vZ2V0LWNvbnRleHQuanMnO1xuaW1wb3J0IHR5cGUgeyBSZXF1ZXN0Q29udGV4dCB9IGZyb20gJy4vcmVxdWVzdC10eXBlcy5qcyc7XG5cbmNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcigncm91dGUtd3JhcHBlcicpO1xuXG4vKipcbiAqIEEgbG9nLXNhZmUgcmVuZGVyaW5nIG9mIGFuIHVua25vd24gZXJyb3IuIGRyaXp6bGUtb3JtJ3MgYERyaXp6bGVRdWVyeUVycm9yYFxuICogZW1iZWRzIHRoZSBmYWlsZWQgU1FMIHN0YXRlbWVudCBBTkQgdGhlIGJvdW5kIHBhcmFtZXRlciB2YWx1ZXMgKHJlYWwgZGF0YSkgaW5cbiAqIGl0cyBgLm1lc3NhZ2VgIChgRmFpbGVkIHF1ZXJ5OiA8c3FsPlxcbnBhcmFtczogPHZhbHVlcz5gKS4gTmV2ZXIgcHV0IHRoYXQgaW4gYVxuICogcmVzcG9uc2Ugb3IgYSBsb2cgbGluZSDigJQgY29sbGFwc2UgaXQgdG8gdGhlIGVycm9yIG5hbWUuIE90aGVyIGVycm9ycyBhcmUgc2FmZS5cbiAqL1xuZnVuY3Rpb24gc2FmZUVycm9yRm9yTG9nKGVycm9yOiB1bmtub3duKTogc3RyaW5nIHtcbiAgaWYgKGVycm9yIGluc3RhbmNlb2YgRXJyb3IgJiYgZXJyb3IubWVzc2FnZS5zdGFydHNXaXRoKCdGYWlsZWQgcXVlcnk6JykpIHtcbiAgICByZXR1cm4gYCR7ZXJyb3IubmFtZSB8fCAnRHJpenpsZVF1ZXJ5RXJyb3InfTogZGF0YWJhc2UgcXVlcnkgZmFpbGVkYDtcbiAgfVxuICByZXR1cm4gZXJyb3JNZXNzYWdlKGVycm9yKTtcbn1cblxuLyoqXG4gKiBDb250ZXh0IG9iamVjdCBwYXNzZWQgdG8gZXZlcnkgcm91dGUgaGFuZGxlciB3cmFwcGVkIHdpdGggYHdpdGhSb3V0ZSgpYC5cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBSb3V0ZUNvbnRleHQge1xuICAvKiogT3JpZ2luYWwgRXhwcmVzcyByZXF1ZXN0ICovXG4gIHJlcTogUmVxdWVzdDtcbiAgLyoqIE9yaWdpbmFsIEV4cHJlc3MgcmVzcG9uc2UgKi9cbiAgcmVzOiBSZXNwb25zZTtcbiAgLyoqIFJlcXVlc3QgY29udGV4dCB3aXRoIGlkZW50aXR5IGFuZCBsb2dnaW5nICovXG4gIGN0eDogUmVxdWVzdENvbnRleHQ7XG4gIC8qKiBMb3dlcmNhc2VkIG9yZ2FuaXphdGlvbiBJRCAoZ3VhcmFudGVlZCBub24tZW1wdHkgd2hlbiByZXF1aXJlT3JnSWQgaXMgdHJ1ZSkgKi9cbiAgb3JnSWQ6IHN0cmluZztcbiAgLyoqIFVzZXIgSUQgZnJvbSBKV1QgKGRlZmF1bHRzIHRvIGVtcHR5IHN0cmluZykgKi9cbiAgdXNlcklkOiBzdHJpbmc7XG59XG5cbi8qKlxuICogT3B0aW9ucyBmb3IgYHdpdGhSb3V0ZSgpYC5cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBXaXRoUm91dGVPcHRpb25zIHtcbiAgLyoqXG4gICAqIFdoZXRoZXIgdG8gcmVxdWlyZSBvcmdJZCBvbiB0aGUgcmVxdWVzdC5cbiAgICogV2hlbiB0cnVlIChkZWZhdWx0KSwgcmV0dXJucyA0MDAgaWYgb3JnSWQgaXMgbWlzc2luZy5cbiAgICogU2V0IHRvIGZhbHNlIGZvciByb3V0ZXMgdGhhdCBkb24ndCBuZWVkIG9yZyBjb250ZXh0LlxuICAgKi9cbiAgcmVxdWlyZU9yZ0lkPzogYm9vbGVhbjtcbn1cblxuLyoqXG4gKiBXcmFwIGFuIGFzeW5jIHJvdXRlIGhhbmRsZXIgd2l0aCBzdGFuZGFyZCBib2lsZXJwbGF0ZS5cbiAqXG4gKiBFeHRyYWN0cyBjb250ZXh0LCBvcmdJZCwgdXNlcklkIGZyb20gdGhlIHJlcXVlc3QsIHZhbGlkYXRlcyBvcmdJZCxcbiAqIGFuZCBjYXRjaGVzIGVycm9ycy4gVHlwZWQgYEFwcEVycm9yYCBzdWJjbGFzc2VzIGFyZSBhdXRvbWF0aWNhbGx5XG4gKiBtYXBwZWQgdG8gdGhlIGNvcnJlY3QgSFRUUCByZXNwb25zZS5cbiAqXG4gKiBAcGFyYW0gaGFuZGxlciAtIEFzeW5jIGZ1bmN0aW9uIHJlY2VpdmluZyBhIFJvdXRlQ29udGV4dFxuICogQHBhcmFtIG9wdGlvbnMgLSBDb25maWd1cmF0aW9uIG9wdGlvbnNcbiAqIEByZXR1cm5zIEV4cHJlc3MgUmVxdWVzdEhhbmRsZXJcbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogcm91dGVyLmdldCgnLzppZCcsIHdpdGhSb3V0ZShhc3luYyAoeyByZXEsIHJlcywgY3R4LCBvcmdJZCB9KSA9PiB7XG4gKiAgIGNvbnN0IGlkID0gZ2V0UGFyYW0ocmVxLnBhcmFtcywgJ2lkJyk7XG4gKiAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHBpcGVsaW5lU2VydmljZS5maW5kQnlJZChpZCwgb3JnSWQpO1xuICogICBpZiAoIXJlc3VsdCkgdGhyb3cgbmV3IE5vdEZvdW5kRXJyb3IoJ1BpcGVsaW5lIG5vdCBmb3VuZCcpO1xuICogICByZXR1cm4gc2VuZFN1Y2Nlc3MocmVzLCAyMDAsIHsgcGlwZWxpbmU6IHJlc3VsdCB9KTtcbiAqIH0pKTtcbiAqIGBgYFxuICovXG5leHBvcnQgZnVuY3Rpb24gd2l0aFJvdXRlKFxuICBoYW5kbGVyOiAocmM6IFJvdXRlQ29udGV4dCkgPT4gUHJvbWlzZTx2b2lkPixcbiAgb3B0aW9uczogV2l0aFJvdXRlT3B0aW9ucyA9IHt9LFxuKTogUmVxdWVzdEhhbmRsZXIge1xuICBjb25zdCB7IHJlcXVpcmVPcmdJZCA9IHRydWUgfSA9IG9wdGlvbnM7XG5cbiAgcmV0dXJuIGFzeW5jIChyZXE6IFJlcXVlc3QsIHJlczogUmVzcG9uc2UpID0+IHtcbiAgICBjb25zdCBjdHggPSBnZXRDb250ZXh0KHJlcSk7XG4gICAgLy8gYGlkZW50aXR5Lm9yZ0lkYCBpcyBhbHJlYWR5IG5vcm1hbGl6ZWQgKHRyaW1tZWQgKyBsb3dlcmNhc2VkKSBvbmNlIGF0XG4gICAgLy8gcmVzb2x1dGlvbiBpbiBhcGktY29yZSdzIGBnZXRJZGVudGl0eWAsIHNvIHRoZSBwcmV2aW91c2x5LWR1cGxpY2F0ZWRcbiAgICAvLyBgLnRvTG93ZXJDYXNlKClgIGhlcmUgaXMgcmVkdW5kYW50IOKAlCBrZXB0IG5vcm1hbGl6YXRpb24gYXQgdGhlIHNpbmdsZVxuICAgIC8vIHNvdXJjZSBvZiB0cnV0aCBpbnN0ZWFkLiBUaGlzIHZhbHVlIG5vdyBtYXRjaGVzIHRoZSBSTFMgR1VDIGV4YWN0bHkuXG4gICAgY29uc3Qgb3JnSWQgPSBjdHguaWRlbnRpdHkub3JnSWQgfHwgJyc7XG4gICAgY29uc3QgdXNlcklkID0gY3R4LmlkZW50aXR5LnVzZXJJZCB8fCAnJztcblxuICAgIGlmIChyZXF1aXJlT3JnSWQgJiYgIW9yZ0lkKSB7XG4gICAgICByZXR1cm4gc2VuZEJhZFJlcXVlc3QocmVzLCAnT3JnYW5pemF0aW9uIElEIGlzIHJlcXVpcmVkJyk7XG4gICAgfVxuXG4gICAgdHJ5IHtcbiAgICAgIGF3YWl0IGhhbmRsZXIoeyByZXEsIHJlcywgY3R4LCBvcmdJZCwgdXNlcklkIH0pO1xuICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICAvLyBEb24ndCBzZW5kIGFub3RoZXIgcmVzcG9uc2UgaWYgb25lIHdhcyBhbHJlYWR5IHNlbnRcbiAgICAgIGlmIChyZXMuaGVhZGVyc1NlbnQpIHtcbiAgICAgICAgbG9nZ2VyLmVycm9yKCdSb3V0ZSBoYW5kbGVyIGVycm9yIGFmdGVyIHJlc3BvbnNlIHNlbnQnLCB7XG4gICAgICAgICAgcmVxdWVzdElkOiBjdHgucmVxdWVzdElkLFxuICAgICAgICAgIGVycm9yOiBzYWZlRXJyb3JGb3JMb2coZXJyb3IpLFxuICAgICAgICB9KTtcbiAgICAgICAgcmV0dXJuO1xuICAgICAgfVxuXG4gICAgICBpZiAoZXJyb3IgaW5zdGFuY2VvZiBBcHBFcnJvcikge1xuICAgICAgICBjb25zdCBzdGF0dXMgPSBlcnJvci5zdGF0dXNDb2RlID49IDQwMCAmJiBlcnJvci5zdGF0dXNDb2RlIDwgNjAwXG4gICAgICAgICAgPyBlcnJvci5zdGF0dXNDb2RlXG4gICAgICAgICAgOiA1MDA7XG4gICAgICAgIHJldHVybiBzZW5kRXJyb3IocmVzLCBzdGF0dXMsIGVycm9yLm1lc3NhZ2UsIGVycm9yLmNvZGUpO1xuICAgICAgfVxuXG4gICAgICAvLyBVbmhhbmRsZWQgZXJyb3Ig4oaSIDUwMC4gTkVWRVIgZWNobyB0aGUgcmF3IG1lc3NhZ2UgdG8gdGhlIGNsaWVudDogREIgZHJpdmVyXG4gICAgICAvLyBlcnJvcnMgY2FycnkgdGhlIFNRTCArIGJvdW5kIHBhcmFtcywgYW5kIENydWRTZXJ2aWNlIGRlbGliZXJhdGVseSBsZXRzIHRoZW1cbiAgICAgIC8vIHByb3BhZ2F0ZSBoZXJlLiBTZW5kIGEgZ2VuZXJpYyBtZXNzYWdlICsgcmVxdWVzdElkICh0byBjb3JyZWxhdGUgd2l0aCBsb2dzKSxcbiAgICAgIC8vIGFuZCBsb2cgb25seSBzYW5pdGl6ZWQgREIgbWV0YWRhdGEgKGNvZGUvY29uc3RyYWludC90YWJsZSDigJQgbm8gU1FML3BhcmFtcykuXG4gICAgICBjb25zdCBkYiA9IGV4dHJhY3REYkVycm9yKGVycm9yKTtcbiAgICAgIGN0eC5sb2coJ0VSUk9SJywgJ1JlcXVlc3QgZmFpbGVkJywge1xuICAgICAgICByZXF1ZXN0SWQ6IGN0eC5yZXF1ZXN0SWQsXG4gICAgICAgIGVycm9yOiBzYWZlRXJyb3JGb3JMb2coZXJyb3IpLFxuICAgICAgICAuLi4oT2JqZWN0LmtleXMoZGIpLmxlbmd0aCA/IHsgZGIgfSA6IHt9KSxcbiAgICAgIH0pO1xuICAgICAgcmV0dXJuIHNlbmRJbnRlcm5hbEVycm9yKHJlcywgJ0ludGVybmFsIHNlcnZlciBlcnJvcicsIHsgcmVxdWVzdElkOiBjdHgucmVxdWVzdElkIH0pO1xuICAgIH1cbiAgfTtcbn1cbiJdfQ==
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Server } from 'http';
|
|
2
|
+
import type { Express } from 'express';
|
|
3
|
+
import { SSEManager } from '../http/sse-connection-manager.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options for starting a server
|
|
6
|
+
*/
|
|
7
|
+
export interface StartServerOptions {
|
|
8
|
+
/** Service name for logging */
|
|
9
|
+
name?: string;
|
|
10
|
+
/** Port to listen on (default: from config) */
|
|
11
|
+
port?: number;
|
|
12
|
+
/** SSE manager for graceful shutdown */
|
|
13
|
+
sseManager?: SSEManager;
|
|
14
|
+
/** Shutdown timeout in milliseconds (default: 15000) */
|
|
15
|
+
shutdownTimeoutMs?: number;
|
|
16
|
+
/** Callback after server starts */
|
|
17
|
+
onStart?: (port: number) => void;
|
|
18
|
+
/** Callback before shutdown */
|
|
19
|
+
onShutdown?: () => Promise<void>;
|
|
20
|
+
/** Runs before database check (e.g., initialize external connections) */
|
|
21
|
+
onBeforeStart?: () => Promise<void>;
|
|
22
|
+
/** Custom database health check, or false to skip. Default: PostgreSQL testConnection */
|
|
23
|
+
testDatabase?: (() => Promise<boolean>) | false;
|
|
24
|
+
/** Custom database close, or false to skip. Default: PostgreSQL closeConnection */
|
|
25
|
+
closeDatabase?: (() => Promise<void>) | false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Result of starting a server
|
|
29
|
+
*/
|
|
30
|
+
export interface StartServerResult {
|
|
31
|
+
/** HTTP server instance */
|
|
32
|
+
server: Server;
|
|
33
|
+
/** Port the server is listening on */
|
|
34
|
+
port: number;
|
|
35
|
+
/** Function to manually trigger shutdown */
|
|
36
|
+
shutdown: () => Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Start an Express server with graceful shutdown handling
|
|
40
|
+
*
|
|
41
|
+
* Features:
|
|
42
|
+
* - Automatic database connection testing
|
|
43
|
+
* - Graceful shutdown on SIGINT/SIGTERM
|
|
44
|
+
* - SSE connection cleanup
|
|
45
|
+
* - Configurable shutdown timeout
|
|
46
|
+
*
|
|
47
|
+
* @param app - Express application
|
|
48
|
+
* @param options - Server options
|
|
49
|
+
* @returns Server instance and control functions
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const { app, sseManager } = createApp();
|
|
54
|
+
*
|
|
55
|
+
* app.post('/api/resource', requireAuth, handler);
|
|
56
|
+
*
|
|
57
|
+
* await startServer(app, {
|
|
58
|
+
* name: 'My Microservice',
|
|
59
|
+
* sseManager,
|
|
60
|
+
* onStart: (port) => console.log(`Listening on ${port}`),
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function startServer(app: Express, options?: StartServerOptions): Promise<StartServerResult>;
|
|
65
|
+
/**
|
|
66
|
+
* Simple server start wrapper with error handling
|
|
67
|
+
*
|
|
68
|
+
* @param app - Express application
|
|
69
|
+
* @param options - Server options
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const { app, sseManager } = createApp();
|
|
74
|
+
* app.post('/api/resource', handler);
|
|
75
|
+
*
|
|
76
|
+
* void runServer(app, { name: 'My Service', sseManager });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function runServer(app: Express, options?: StartServerOptions): void;
|