@pipeline-builder/api-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +79 -0
  3. package/lib/api/app-factory.d.ts +95 -0
  4. package/lib/api/app-factory.js +328 -0
  5. package/lib/api/check-quota.d.ts +13 -0
  6. package/lib/api/check-quota.js +74 -0
  7. package/lib/api/context-middleware.d.ts +34 -0
  8. package/lib/api/context-middleware.js +42 -0
  9. package/lib/api/etag-middleware.d.ts +7 -0
  10. package/lib/api/etag-middleware.js +44 -0
  11. package/lib/api/get-context.d.ts +22 -0
  12. package/lib/api/get-context.js +28 -0
  13. package/lib/api/health-checks.d.ts +62 -0
  14. package/lib/api/health-checks.js +87 -0
  15. package/lib/api/idempotency-middleware.d.ts +80 -0
  16. package/lib/api/idempotency-middleware.js +321 -0
  17. package/lib/api/index.d.ts +20 -0
  18. package/lib/api/index.js +34 -0
  19. package/lib/api/meter-quota.d.ts +32 -0
  20. package/lib/api/meter-quota.js +70 -0
  21. package/lib/api/metrics.d.ts +47 -0
  22. package/lib/api/metrics.js +221 -0
  23. package/lib/api/middleware-factory.d.ts +56 -0
  24. package/lib/api/middleware-factory.js +80 -0
  25. package/lib/api/middleware.d.ts +1 -0
  26. package/lib/api/middleware.js +8 -0
  27. package/lib/api/mongo-connect.d.ts +19 -0
  28. package/lib/api/mongo-connect.js +41 -0
  29. package/lib/api/quota-helpers.d.ts +23 -0
  30. package/lib/api/quota-helpers.js +22 -0
  31. package/lib/api/rate-limit-by-org.d.ts +29 -0
  32. package/lib/api/rate-limit-by-org.js +64 -0
  33. package/lib/api/readiness.d.ts +29 -0
  34. package/lib/api/readiness.js +65 -0
  35. package/lib/api/request-types.d.ts +55 -0
  36. package/lib/api/request-types.js +65 -0
  37. package/lib/api/require-org-id.d.ts +14 -0
  38. package/lib/api/require-org-id.js +28 -0
  39. package/lib/api/route-wrapper.d.ts +50 -0
  40. package/lib/api/route-wrapper.js +84 -0
  41. package/lib/api/server.d.ts +79 -0
  42. package/lib/api/server.js +234 -0
  43. package/lib/api/tenant-context.d.ts +28 -0
  44. package/lib/api/tenant-context.js +48 -0
  45. package/lib/api/tracing.d.ts +25 -0
  46. package/lib/api/tracing.js +69 -0
  47. package/lib/http/index.d.ts +4 -0
  48. package/lib/http/index.js +7 -0
  49. package/lib/http/sse-connection-manager.d.ts +357 -0
  50. package/lib/http/sse-connection-manager.js +665 -0
  51. package/lib/http/sse-relay.d.ts +63 -0
  52. package/lib/http/sse-relay.js +70 -0
  53. package/lib/http/sse-ticket-channel.d.ts +31 -0
  54. package/lib/http/sse-ticket-channel.js +52 -0
  55. package/lib/http/sse-ticket-store.d.ts +75 -0
  56. package/lib/http/sse-ticket-store.js +160 -0
  57. package/lib/index.d.ts +30 -0
  58. package/lib/index.js +35 -0
  59. package/lib/otel-bootstrap.d.ts +1 -0
  60. package/lib/otel-bootstrap.js +75 -0
  61. package/package.json +183 -0
@@ -0,0 +1,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;