@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,63 @@
1
+ import type { SSEPayload } from './sse-connection-manager.js';
2
+ /**
3
+ * A relayed SSE frame. `origin` is the id of the SSEManager instance that
4
+ * published it, so the publishing pod can ignore its own echo (it already wrote
5
+ * the frame to its local clients before publishing). `kind` distinguishes a
6
+ * per-subject `send` (carries `requestId`) from a `broadcast` (no subject —
7
+ * every pod re-emits to ALL of its local clients).
8
+ */
9
+ export interface SSERelayMessage {
10
+ origin: string;
11
+ kind: 'send' | 'broadcast';
12
+ /** Present for `send`; the stream subject the payload targets. */
13
+ requestId?: string;
14
+ payload: SSEPayload;
15
+ }
16
+ /**
17
+ * Cross-pod fan-out bus for SSE frames.
18
+ *
19
+ * Under multiple replicas the producer of a log frame (e.g. a plugin build
20
+ * worker on pod A) and the consumer (a browser EventSource on pod B) are on
21
+ * DIFFERENT pods. The in-process client map only reaches the producer's own
22
+ * pod, so B's stream would silently miss frames. The relay closes that: every
23
+ * pod SUBSCRIBES once on startup and re-emits received frames to its LOCAL
24
+ * clients, while `send()`/`broadcast()` PUBLISH to the bus (in addition to
25
+ * writing locally). The origin tag prevents a pod from double-delivering its own
26
+ * frame when it receives its own publish back.
27
+ *
28
+ * All methods are best-effort / fail-safe: a bus outage must degrade to
29
+ * local-only delivery (today's single-replica behavior), never crash the stream.
30
+ */
31
+ export interface SSERelay {
32
+ /** Fire-and-forget publish of a frame to every subscribed pod. Never throws. */
33
+ publish(msg: SSERelayMessage): void;
34
+ /** Subscribe this pod's re-emit handler. Called once on manager construction. */
35
+ subscribe(handler: (msg: SSERelayMessage) => void): void;
36
+ /** Tear down the underlying connection(s). */
37
+ close(): Promise<void>;
38
+ }
39
+ /**
40
+ * Minimal ioredis pub/sub surface the Redis relay needs. `subscribe` requires a
41
+ * DEDICATED connection (a subscribed ioredis client can't run other commands),
42
+ * so the relay `duplicate()`s the publisher for the subscriber side.
43
+ */
44
+ export interface RedisPubSubClient {
45
+ publish(channel: string, message: string): Promise<number>;
46
+ subscribe(...channels: string[]): Promise<unknown>;
47
+ on(event: 'message', cb: (channel: string, message: string) => void): void;
48
+ duplicate(): RedisPubSubClient;
49
+ quit(): Promise<unknown>;
50
+ }
51
+ /**
52
+ * Redis-backed relay. Uses the given client for PUBLISH and a `duplicate()` for
53
+ * SUBSCRIBE (ioredis forbids mixing subscribe with normal commands on one
54
+ * connection). Publish is fire-and-forget with a swallowed rejection; a subscribe
55
+ * handler that throws is isolated so one bad frame can't kill the subscriber.
56
+ */
57
+ export declare function createRedisSSERelay(publisher: RedisPubSubClient): SSERelay;
58
+ /**
59
+ * Build a Redis-backed SSE relay from the shared env Redis (same wiring as the
60
+ * SSE ticket store / rate-limiter / audit-spool). Returns null when Redis isn't
61
+ * configured so the caller keeps local-only delivery. Never throws.
62
+ */
63
+ export declare function createEnvRedisSSERelay(): SSERelay | null;
@@ -0,0 +1,70 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { createLogger, createEnvRedisClient } from '@pipeline-builder/api-core';
4
+ const logger = createLogger('sse-relay');
5
+ /**
6
+ * Single channel carrying every relayed frame. `requestId` rides in the message
7
+ * body rather than the channel name so each pod SUBSCRIBES exactly once on
8
+ * startup — no per-subject subscribe/unsubscribe churn as clients come and go.
9
+ */
10
+ const RELAY_CHANNEL = 'sse:relay';
11
+ /**
12
+ * Redis-backed relay. Uses the given client for PUBLISH and a `duplicate()` for
13
+ * SUBSCRIBE (ioredis forbids mixing subscribe with normal commands on one
14
+ * connection). Publish is fire-and-forget with a swallowed rejection; a subscribe
15
+ * handler that throws is isolated so one bad frame can't kill the subscriber.
16
+ */
17
+ export function createRedisSSERelay(publisher) {
18
+ const subscriber = publisher.duplicate();
19
+ let closed = false;
20
+ return {
21
+ publish(msg) {
22
+ if (closed)
23
+ return;
24
+ // Fire-and-forget: never await, never surface a rejection to the caller.
25
+ void Promise.resolve(publisher.publish(RELAY_CHANNEL, JSON.stringify(msg))).catch((err) => {
26
+ logger.warn('SSE relay publish failed', { error: err instanceof Error ? err.message : String(err) });
27
+ });
28
+ },
29
+ subscribe(handler) {
30
+ subscriber.on('message', (channel, message) => {
31
+ if (channel !== RELAY_CHANNEL)
32
+ return;
33
+ let parsed;
34
+ try {
35
+ parsed = JSON.parse(message);
36
+ }
37
+ catch (err) {
38
+ logger.warn('SSE relay received unparseable frame', { error: err instanceof Error ? err.message : String(err) });
39
+ return;
40
+ }
41
+ try {
42
+ handler(parsed);
43
+ }
44
+ catch (err) {
45
+ logger.warn('SSE relay handler threw', { error: err instanceof Error ? err.message : String(err) });
46
+ }
47
+ });
48
+ void Promise.resolve(subscriber.subscribe(RELAY_CHANNEL)).catch((err) => {
49
+ logger.warn('SSE relay subscribe failed — falling back to local-only delivery', { error: err instanceof Error ? err.message : String(err) });
50
+ });
51
+ },
52
+ async close() {
53
+ closed = true;
54
+ await Promise.allSettled([publisher.quit(), subscriber.quit()]);
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * Build a Redis-backed SSE relay from the shared env Redis (same wiring as the
60
+ * SSE ticket store / rate-limiter / audit-spool). Returns null when Redis isn't
61
+ * configured so the caller keeps local-only delivery. Never throws.
62
+ */
63
+ export function createEnvRedisSSERelay() {
64
+ const client = createEnvRedisClient('sse-relay');
65
+ if (!client)
66
+ return null;
67
+ logger.info('Redis SSE relay initialized (cross-pod fan-out enabled)');
68
+ return createRedisSSERelay(client);
69
+ }
70
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3NlLXJlbGF5LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2h0dHAvc3NlLXJlbGF5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEMsT0FBTyxFQUFFLFlBQVksRUFBRSxvQkFBb0IsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBR2hGLE1BQU0sTUFBTSxHQUFHLFlBQVksQ0FBQyxXQUFXLENBQUMsQ0FBQztBQXNEekM7Ozs7R0FJRztBQUNILE1BQU0sYUFBYSxHQUFHLFdBQVcsQ0FBQztBQUVsQzs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxTQUE0QjtJQUM5RCxNQUFNLFVBQVUsR0FBRyxTQUFTLENBQUMsU0FBUyxFQUFFLENBQUM7SUFDekMsSUFBSSxNQUFNLEdBQUcsS0FBSyxDQUFDO0lBRW5CLE9BQU87UUFDTCxPQUFPLENBQUMsR0FBRztZQUNULElBQUksTUFBTTtnQkFBRSxPQUFPO1lBQ25CLHlFQUF5RTtZQUN6RSxLQUFLLE9BQU8sQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxhQUFhLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUU7Z0JBQ3hGLE1BQU0sQ0FBQyxJQUFJLENBQUMsMEJBQTBCLEVBQUUsRUFBRSxLQUFLLEVBQUUsR0FBRyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztZQUN2RyxDQUFDLENBQUMsQ0FBQztRQUNMLENBQUM7UUFDRCxTQUFTLENBQUMsT0FBTztZQUNmLFVBQVUsQ0FBQyxFQUFFLENBQUMsU0FBUyxFQUFFLENBQUMsT0FBTyxFQUFFLE9BQU8sRUFBRSxFQUFFO2dCQUM1QyxJQUFJLE9BQU8sS0FBSyxhQUFhO29CQUFFLE9BQU87Z0JBQ3RDLElBQUksTUFBdUIsQ0FBQztnQkFDNUIsSUFBSSxDQUFDO29CQUNILE1BQU0sR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBb0IsQ0FBQztnQkFDbEQsQ0FBQztnQkFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO29CQUNiLE1BQU0sQ0FBQyxJQUFJLENBQUMsc0NBQXNDLEVBQUUsRUFBRSxLQUFLLEVBQUUsR0FBRyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztvQkFDakgsT0FBTztnQkFDVCxDQUFDO2dCQUNELElBQUksQ0FBQztvQkFDSCxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUM7Z0JBQ2xCLENBQUM7Z0JBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztvQkFDYixNQUFNLENBQUMsSUFBSSxDQUFDLHlCQUF5QixFQUFFLEVBQUUsS0FBSyxFQUFFLEdBQUcsWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLENBQUM7Z0JBQ3RHLENBQUM7WUFDSCxDQUFDLENBQUMsQ0FBQztZQUNILEtBQUssT0FBTyxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsU0FBUyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUU7Z0JBQ3RFLE1BQU0sQ0FBQyxJQUFJLENBQUMsa0VBQWtFLEVBQUUsRUFBRSxLQUFLLEVBQUUsR0FBRyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztZQUMvSSxDQUFDLENBQUMsQ0FBQztRQUNMLENBQUM7UUFDRCxLQUFLLENBQUMsS0FBSztZQUNULE1BQU0sR0FBRyxJQUFJLENBQUM7WUFDZCxNQUFNLE9BQU8sQ0FBQyxVQUFVLENBQUMsQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLEVBQUUsVUFBVSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQztRQUNsRSxDQUFDO0tBQ0YsQ0FBQztBQUNKLENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsTUFBTSxVQUFVLHNCQUFzQjtJQUNwQyxNQUFNLE1BQU0sR0FBRyxvQkFBb0IsQ0FBb0IsV0FBVyxDQUFDLENBQUM7SUFDcEUsSUFBSSxDQUFDLE1BQU07UUFBRSxPQUFPLElBQUksQ0FBQztJQUN6QixNQUFNLENBQUMsSUFBSSxDQUFDLHlEQUF5RCxDQUFDLENBQUM7SUFDdkUsT0FBTyxtQkFBbUIsQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUNyQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciwgY3JlYXRlRW52UmVkaXNDbGllbnQgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgdHlwZSB7IFNTRVBheWxvYWQgfSBmcm9tICcuL3NzZS1jb25uZWN0aW9uLW1hbmFnZXIuanMnO1xuXG5jb25zdCBsb2dnZXIgPSBjcmVhdGVMb2dnZXIoJ3NzZS1yZWxheScpO1xuXG4vKipcbiAqIEEgcmVsYXllZCBTU0UgZnJhbWUuIGBvcmlnaW5gIGlzIHRoZSBpZCBvZiB0aGUgU1NFTWFuYWdlciBpbnN0YW5jZSB0aGF0XG4gKiBwdWJsaXNoZWQgaXQsIHNvIHRoZSBwdWJsaXNoaW5nIHBvZCBjYW4gaWdub3JlIGl0cyBvd24gZWNobyAoaXQgYWxyZWFkeSB3cm90ZVxuICogdGhlIGZyYW1lIHRvIGl0cyBsb2NhbCBjbGllbnRzIGJlZm9yZSBwdWJsaXNoaW5nKS4gYGtpbmRgIGRpc3Rpbmd1aXNoZXMgYVxuICogcGVyLXN1YmplY3QgYHNlbmRgIChjYXJyaWVzIGByZXF1ZXN0SWRgKSBmcm9tIGEgYGJyb2FkY2FzdGAgKG5vIHN1YmplY3Qg4oCUXG4gKiBldmVyeSBwb2QgcmUtZW1pdHMgdG8gQUxMIG9mIGl0cyBsb2NhbCBjbGllbnRzKS5cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBTU0VSZWxheU1lc3NhZ2Uge1xuICBvcmlnaW46IHN0cmluZztcbiAga2luZDogJ3NlbmQnIHwgJ2Jyb2FkY2FzdCc7XG4gIC8qKiBQcmVzZW50IGZvciBgc2VuZGA7IHRoZSBzdHJlYW0gc3ViamVjdCB0aGUgcGF5bG9hZCB0YXJnZXRzLiAqL1xuICByZXF1ZXN0SWQ/OiBzdHJpbmc7XG4gIHBheWxvYWQ6IFNTRVBheWxvYWQ7XG59XG5cbi8qKlxuICogQ3Jvc3MtcG9kIGZhbi1vdXQgYnVzIGZvciBTU0UgZnJhbWVzLlxuICpcbiAqIFVuZGVyIG11bHRpcGxlIHJlcGxpY2FzIHRoZSBwcm9kdWNlciBvZiBhIGxvZyBmcmFtZSAoZS5nLiBhIHBsdWdpbiBidWlsZFxuICogd29ya2VyIG9uIHBvZCBBKSBhbmQgdGhlIGNvbnN1bWVyIChhIGJyb3dzZXIgRXZlbnRTb3VyY2Ugb24gcG9kIEIpIGFyZSBvblxuICogRElGRkVSRU5UIHBvZHMuIFRoZSBpbi1wcm9jZXNzIGNsaWVudCBtYXAgb25seSByZWFjaGVzIHRoZSBwcm9kdWNlcidzIG93blxuICogcG9kLCBzbyBCJ3Mgc3RyZWFtIHdvdWxkIHNpbGVudGx5IG1pc3MgZnJhbWVzLiBUaGUgcmVsYXkgY2xvc2VzIHRoYXQ6IGV2ZXJ5XG4gKiBwb2QgU1VCU0NSSUJFUyBvbmNlIG9uIHN0YXJ0dXAgYW5kIHJlLWVtaXRzIHJlY2VpdmVkIGZyYW1lcyB0byBpdHMgTE9DQUxcbiAqIGNsaWVudHMsIHdoaWxlIGBzZW5kKClgL2Bicm9hZGNhc3QoKWAgUFVCTElTSCB0byB0aGUgYnVzIChpbiBhZGRpdGlvbiB0b1xuICogd3JpdGluZyBsb2NhbGx5KS4gVGhlIG9yaWdpbiB0YWcgcHJldmVudHMgYSBwb2QgZnJvbSBkb3VibGUtZGVsaXZlcmluZyBpdHMgb3duXG4gKiBmcmFtZSB3aGVuIGl0IHJlY2VpdmVzIGl0cyBvd24gcHVibGlzaCBiYWNrLlxuICpcbiAqIEFsbCBtZXRob2RzIGFyZSBiZXN0LWVmZm9ydCAvIGZhaWwtc2FmZTogYSBidXMgb3V0YWdlIG11c3QgZGVncmFkZSB0b1xuICogbG9jYWwtb25seSBkZWxpdmVyeSAodG9kYXkncyBzaW5nbGUtcmVwbGljYSBiZWhhdmlvciksIG5ldmVyIGNyYXNoIHRoZSBzdHJlYW0uXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgU1NFUmVsYXkge1xuICAvKiogRmlyZS1hbmQtZm9yZ2V0IHB1Ymxpc2ggb2YgYSBmcmFtZSB0byBldmVyeSBzdWJzY3JpYmVkIHBvZC4gTmV2ZXIgdGhyb3dzLiAqL1xuICBwdWJsaXNoKG1zZzogU1NFUmVsYXlNZXNzYWdlKTogdm9pZDtcbiAgLyoqIFN1YnNjcmliZSB0aGlzIHBvZCdzIHJlLWVtaXQgaGFuZGxlci4gQ2FsbGVkIG9uY2Ugb24gbWFuYWdlciBjb25zdHJ1Y3Rpb24uICovXG4gIHN1YnNjcmliZShoYW5kbGVyOiAobXNnOiBTU0VSZWxheU1lc3NhZ2UpID0+IHZvaWQpOiB2b2lkO1xuICAvKiogVGVhciBkb3duIHRoZSB1bmRlcmx5aW5nIGNvbm5lY3Rpb24ocykuICovXG4gIGNsb3NlKCk6IFByb21pc2U8dm9pZD47XG59XG5cbi8qKlxuICogTWluaW1hbCBpb3JlZGlzIHB1Yi9zdWIgc3VyZmFjZSB0aGUgUmVkaXMgcmVsYXkgbmVlZHMuIGBzdWJzY3JpYmVgIHJlcXVpcmVzIGFcbiAqIERFRElDQVRFRCBjb25uZWN0aW9uIChhIHN1YnNjcmliZWQgaW9yZWRpcyBjbGllbnQgY2FuJ3QgcnVuIG90aGVyIGNvbW1hbmRzKSxcbiAqIHNvIHRoZSByZWxheSBgZHVwbGljYXRlKClgcyB0aGUgcHVibGlzaGVyIGZvciB0aGUgc3Vic2NyaWJlciBzaWRlLlxuICovXG5leHBvcnQgaW50ZXJmYWNlIFJlZGlzUHViU3ViQ2xpZW50IHtcbiAgcHVibGlzaChjaGFubmVsOiBzdHJpbmcsIG1lc3NhZ2U6IHN0cmluZyk6IFByb21pc2U8bnVtYmVyPjtcbiAgc3Vic2NyaWJlKC4uLmNoYW5uZWxzOiBzdHJpbmdbXSk6IFByb21pc2U8dW5rbm93bj47XG4gIG9uKGV2ZW50OiAnbWVzc2FnZScsIGNiOiAoY2hhbm5lbDogc3RyaW5nLCBtZXNzYWdlOiBzdHJpbmcpID0+IHZvaWQpOiB2b2lkO1xuICBkdXBsaWNhdGUoKTogUmVkaXNQdWJTdWJDbGllbnQ7XG4gIHF1aXQoKTogUHJvbWlzZTx1bmtub3duPjtcbn1cblxuLyoqXG4gKiBTaW5nbGUgY2hhbm5lbCBjYXJyeWluZyBldmVyeSByZWxheWVkIGZyYW1lLiBgcmVxdWVzdElkYCByaWRlcyBpbiB0aGUgbWVzc2FnZVxuICogYm9keSByYXRoZXIgdGhhbiB0aGUgY2hhbm5lbCBuYW1lIHNvIGVhY2ggcG9kIFNVQlNDUklCRVMgZXhhY3RseSBvbmNlIG9uXG4gKiBzdGFydHVwIOKAlCBubyBwZXItc3ViamVjdCBzdWJzY3JpYmUvdW5zdWJzY3JpYmUgY2h1cm4gYXMgY2xpZW50cyBjb21lIGFuZCBnby5cbiAqL1xuY29uc3QgUkVMQVlfQ0hBTk5FTCA9ICdzc2U6cmVsYXknO1xuXG4vKipcbiAqIFJlZGlzLWJhY2tlZCByZWxheS4gVXNlcyB0aGUgZ2l2ZW4gY2xpZW50IGZvciBQVUJMSVNIIGFuZCBhIGBkdXBsaWNhdGUoKWAgZm9yXG4gKiBTVUJTQ1JJQkUgKGlvcmVkaXMgZm9yYmlkcyBtaXhpbmcgc3Vic2NyaWJlIHdpdGggbm9ybWFsIGNvbW1hbmRzIG9uIG9uZVxuICogY29ubmVjdGlvbikuIFB1Ymxpc2ggaXMgZmlyZS1hbmQtZm9yZ2V0IHdpdGggYSBzd2FsbG93ZWQgcmVqZWN0aW9uOyBhIHN1YnNjcmliZVxuICogaGFuZGxlciB0aGF0IHRocm93cyBpcyBpc29sYXRlZCBzbyBvbmUgYmFkIGZyYW1lIGNhbid0IGtpbGwgdGhlIHN1YnNjcmliZXIuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVSZWRpc1NTRVJlbGF5KHB1Ymxpc2hlcjogUmVkaXNQdWJTdWJDbGllbnQpOiBTU0VSZWxheSB7XG4gIGNvbnN0IHN1YnNjcmliZXIgPSBwdWJsaXNoZXIuZHVwbGljYXRlKCk7XG4gIGxldCBjbG9zZWQgPSBmYWxzZTtcblxuICByZXR1cm4ge1xuICAgIHB1Ymxpc2gobXNnKSB7XG4gICAgICBpZiAoY2xvc2VkKSByZXR1cm47XG4gICAgICAvLyBGaXJlLWFuZC1mb3JnZXQ6IG5ldmVyIGF3YWl0LCBuZXZlciBzdXJmYWNlIGEgcmVqZWN0aW9uIHRvIHRoZSBjYWxsZXIuXG4gICAgICB2b2lkIFByb21pc2UucmVzb2x2ZShwdWJsaXNoZXIucHVibGlzaChSRUxBWV9DSEFOTkVMLCBKU09OLnN0cmluZ2lmeShtc2cpKSkuY2F0Y2goKGVycikgPT4ge1xuICAgICAgICBsb2dnZXIud2FybignU1NFIHJlbGF5IHB1Ymxpc2ggZmFpbGVkJywgeyBlcnJvcjogZXJyIGluc3RhbmNlb2YgRXJyb3IgPyBlcnIubWVzc2FnZSA6IFN0cmluZyhlcnIpIH0pO1xuICAgICAgfSk7XG4gICAgfSxcbiAgICBzdWJzY3JpYmUoaGFuZGxlcikge1xuICAgICAgc3Vic2NyaWJlci5vbignbWVzc2FnZScsIChjaGFubmVsLCBtZXNzYWdlKSA9PiB7XG4gICAgICAgIGlmIChjaGFubmVsICE9PSBSRUxBWV9DSEFOTkVMKSByZXR1cm47XG4gICAgICAgIGxldCBwYXJzZWQ6IFNTRVJlbGF5TWVzc2FnZTtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICBwYXJzZWQgPSBKU09OLnBhcnNlKG1lc3NhZ2UpIGFzIFNTRVJlbGF5TWVzc2FnZTtcbiAgICAgICAgfSBjYXRjaCAoZXJyKSB7XG4gICAgICAgICAgbG9nZ2VyLndhcm4oJ1NTRSByZWxheSByZWNlaXZlZCB1bnBhcnNlYWJsZSBmcmFtZScsIHsgZXJyb3I6IGVyciBpbnN0YW5jZW9mIEVycm9yID8gZXJyLm1lc3NhZ2UgOiBTdHJpbmcoZXJyKSB9KTtcbiAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICBoYW5kbGVyKHBhcnNlZCk7XG4gICAgICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgICAgIGxvZ2dlci53YXJuKCdTU0UgcmVsYXkgaGFuZGxlciB0aHJldycsIHsgZXJyb3I6IGVyciBpbnN0YW5jZW9mIEVycm9yID8gZXJyLm1lc3NhZ2UgOiBTdHJpbmcoZXJyKSB9KTtcbiAgICAgICAgfVxuICAgICAgfSk7XG4gICAgICB2b2lkIFByb21pc2UucmVzb2x2ZShzdWJzY3JpYmVyLnN1YnNjcmliZShSRUxBWV9DSEFOTkVMKSkuY2F0Y2goKGVycikgPT4ge1xuICAgICAgICBsb2dnZXIud2FybignU1NFIHJlbGF5IHN1YnNjcmliZSBmYWlsZWQg4oCUIGZhbGxpbmcgYmFjayB0byBsb2NhbC1vbmx5IGRlbGl2ZXJ5JywgeyBlcnJvcjogZXJyIGluc3RhbmNlb2YgRXJyb3IgPyBlcnIubWVzc2FnZSA6IFN0cmluZyhlcnIpIH0pO1xuICAgICAgfSk7XG4gICAgfSxcbiAgICBhc3luYyBjbG9zZSgpIHtcbiAgICAgIGNsb3NlZCA9IHRydWU7XG4gICAgICBhd2FpdCBQcm9taXNlLmFsbFNldHRsZWQoW3B1Ymxpc2hlci5xdWl0KCksIHN1YnNjcmliZXIucXVpdCgpXSk7XG4gICAgfSxcbiAgfTtcbn1cblxuLyoqXG4gKiBCdWlsZCBhIFJlZGlzLWJhY2tlZCBTU0UgcmVsYXkgZnJvbSB0aGUgc2hhcmVkIGVudiBSZWRpcyAoc2FtZSB3aXJpbmcgYXMgdGhlXG4gKiBTU0UgdGlja2V0IHN0b3JlIC8gcmF0ZS1saW1pdGVyIC8gYXVkaXQtc3Bvb2wpLiBSZXR1cm5zIG51bGwgd2hlbiBSZWRpcyBpc24ndFxuICogY29uZmlndXJlZCBzbyB0aGUgY2FsbGVyIGtlZXBzIGxvY2FsLW9ubHkgZGVsaXZlcnkuIE5ldmVyIHRocm93cy5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZUVudlJlZGlzU1NFUmVsYXkoKTogU1NFUmVsYXkgfCBudWxsIHtcbiAgY29uc3QgY2xpZW50ID0gY3JlYXRlRW52UmVkaXNDbGllbnQ8UmVkaXNQdWJTdWJDbGllbnQ+KCdzc2UtcmVsYXknKTtcbiAgaWYgKCFjbGllbnQpIHJldHVybiBudWxsO1xuICBsb2dnZXIuaW5mbygnUmVkaXMgU1NFIHJlbGF5IGluaXRpYWxpemVkIChjcm9zcy1wb2QgZmFuLW91dCBlbmFibGVkKScpO1xuICByZXR1cm4gY3JlYXRlUmVkaXNTU0VSZWxheShjbGllbnQpO1xufVxuIl19
@@ -0,0 +1,31 @@
1
+ import { type SseTicketStore } from '@pipeline-builder/api-core';
2
+ import type { Express, RequestHandler } from 'express';
3
+ import type { SSEManager } from './sse-connection-manager.js';
4
+ export interface SseTicketChannelOptions {
5
+ /** POST route that exchanges the JWT for a single-use ticket (e.g. '/messages/notifications/ticket'). */
6
+ ticketPath: string;
7
+ /** GET route that redeems the ticket and opens the EventSource (e.g. '/messages/notifications'). */
8
+ streamPath: string;
9
+ /** Org-scoped, single-use ticket store (Redis-backed for multi-replica). */
10
+ ticketStore: SseTicketStore;
11
+ /** SSE manager — the org id is the stream subject (`sseManager.send(orgId, …)`). */
12
+ sseManager: SSEManager;
13
+ /** Noun used in capacity/connection error messages (e.g. 'notification', 'execution-stream'). */
14
+ label: string;
15
+ /** Extra middleware applied AFTER requireAuth on the mint route (e.g. requirePermission('reports:read')). */
16
+ ticketGuards?: RequestHandler[];
17
+ }
18
+ /**
19
+ * Register a per-ORG SSE channel: a POST that exchanges the caller's VERIFIED JWT
20
+ * org for a single-use, org-bound ticket, and a GET that redeems the ticket and
21
+ * attaches an EventSource keyed by that org. The org IS the stream subject, so a
22
+ * producer reaches subscribers with `sseManager.send(orgId, …)` (cross-pod via the
23
+ * relay). Shared by the message-notification and reporting execution-status
24
+ * channels so the subtle bits — the ticket→addClient→flushHeaders ordering (a 429
25
+ * MUST precede flushHeaders, which commits the 200) and the capacity semantics —
26
+ * live in exactly one place.
27
+ *
28
+ * Distinct from app-factory's `/logs` channel, which is SUBJECT-bound (per build,
29
+ * via SSEManager.createTicket) rather than org-bound.
30
+ */
31
+ export declare function registerSseTicketChannel(app: Express, opts: SseTicketChannelOptions): void;
@@ -0,0 +1,52 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { requireAuth, sendSuccess, sendError, ErrorCode, createLogger } from '@pipeline-builder/api-core';
4
+ const logger = createLogger('sse-ticket-channel');
5
+ /**
6
+ * Register a per-ORG SSE channel: a POST that exchanges the caller's VERIFIED JWT
7
+ * org for a single-use, org-bound ticket, and a GET that redeems the ticket and
8
+ * attaches an EventSource keyed by that org. The org IS the stream subject, so a
9
+ * producer reaches subscribers with `sseManager.send(orgId, …)` (cross-pod via the
10
+ * relay). Shared by the message-notification and reporting execution-status
11
+ * channels so the subtle bits — the ticket→addClient→flushHeaders ordering (a 429
12
+ * MUST precede flushHeaders, which commits the 200) and the capacity semantics —
13
+ * live in exactly one place.
14
+ *
15
+ * Distinct from app-factory's `/logs` channel, which is SUBJECT-bound (per build,
16
+ * via SSEManager.createTicket) rather than org-bound.
17
+ */
18
+ export function registerSseTicketChannel(app, opts) {
19
+ const { ticketPath, streamPath, ticketStore, sseManager, label, ticketGuards = [] } = opts;
20
+ app.post(ticketPath, requireAuth, ...ticketGuards, async (req, res) => {
21
+ const orgId = req.user?.organizationId?.toLowerCase();
22
+ if (!orgId)
23
+ return sendError(res, 400, 'Token missing organization', ErrorCode.VALIDATION_ERROR);
24
+ const result = await ticketStore.issue(orgId);
25
+ if (!result.ok) {
26
+ return result.reason === 'total'
27
+ ? sendError(res, 503, `The ${label} subsystem is at capacity`, ErrorCode.QUOTA_EXCEEDED)
28
+ : sendError(res, 429, `Too many ${label} tickets issued`, ErrorCode.QUOTA_EXCEEDED);
29
+ }
30
+ return sendSuccess(res, 200, { ticket: result.ticket });
31
+ });
32
+ app.get(streamPath, async (req, res) => {
33
+ const ticketId = req.query.ticket;
34
+ if (!ticketId)
35
+ return void sendError(res, 401, 'Missing ticket parameter', ErrorCode.UNAUTHORIZED);
36
+ const ticket = await ticketStore.consume(ticketId); // atomic single-use
37
+ if (!ticket)
38
+ return void sendError(res, 401, 'Invalid or expired ticket', ErrorCode.UNAUTHORIZED);
39
+ // Reserve the connection slot BEFORE flushing SSE headers: once flushHeaders
40
+ // runs the response is committed at 200 and a later 429 is silently dropped.
41
+ const added = sseManager.addClient(ticket.orgId, res);
42
+ if (!added)
43
+ return void sendError(res, 429, `Too many ${label} connections`, ErrorCode.QUOTA_EXCEEDED);
44
+ res.setHeader('Content-Type', 'text/event-stream');
45
+ res.setHeader('Cache-Control', 'no-cache');
46
+ res.setHeader('Connection', 'keep-alive');
47
+ res.setHeader('X-Accel-Buffering', 'no');
48
+ res.flushHeaders();
49
+ logger.info('SSE client connected', { channel: label, orgId: ticket.orgId });
50
+ });
51
+ }
52
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3NlLXRpY2tldC1jaGFubmVsLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2h0dHAvc3NlLXRpY2tldC1jaGFubmVsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEMsT0FBTyxFQUFFLFdBQVcsRUFBRSxXQUFXLEVBQUUsU0FBUyxFQUFFLFNBQVMsRUFBRSxZQUFZLEVBQXVCLE1BQU0sNEJBQTRCLENBQUM7QUFJL0gsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLG9CQUFvQixDQUFDLENBQUM7QUFpQmxEOzs7Ozs7Ozs7Ozs7R0FZRztBQUNILE1BQU0sVUFBVSx3QkFBd0IsQ0FBQyxHQUFZLEVBQUUsSUFBNkI7SUFDbEYsTUFBTSxFQUFFLFVBQVUsRUFBRSxVQUFVLEVBQUUsV0FBVyxFQUFFLFVBQVUsRUFBRSxLQUFLLEVBQUUsWUFBWSxHQUFHLEVBQUUsRUFBRSxHQUFHLElBQUksQ0FBQztJQUUzRixHQUFHLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxXQUFXLEVBQUUsR0FBRyxZQUFZLEVBQUUsS0FBSyxFQUFFLEdBQVksRUFBRSxHQUFhLEVBQUUsRUFBRTtRQUN2RixNQUFNLEtBQUssR0FBRyxHQUFHLENBQUMsSUFBSSxFQUFFLGNBQWMsRUFBRSxXQUFXLEVBQUUsQ0FBQztRQUN0RCxJQUFJLENBQUMsS0FBSztZQUFFLE9BQU8sU0FBUyxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsNEJBQTRCLEVBQUUsU0FBUyxDQUFDLGdCQUFnQixDQUFDLENBQUM7UUFDakcsTUFBTSxNQUFNLEdBQUcsTUFBTSxXQUFXLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQzlDLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRSxFQUFFLENBQUM7WUFDZixPQUFPLE1BQU0sQ0FBQyxNQUFNLEtBQUssT0FBTztnQkFDOUIsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLE9BQU8sS0FBSywyQkFBMkIsRUFBRSxTQUFTLENBQUMsY0FBYyxDQUFDO2dCQUN4RixDQUFDLENBQUMsU0FBUyxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsWUFBWSxLQUFLLGlCQUFpQixFQUFFLFNBQVMsQ0FBQyxjQUFjLENBQUMsQ0FBQztRQUN4RixDQUFDO1FBQ0QsT0FBTyxXQUFXLENBQUMsR0FBRyxFQUFFLEdBQUcsRUFBRSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQztJQUMxRCxDQUFDLENBQUMsQ0FBQztJQUVILEdBQUcsQ0FBQyxHQUFHLENBQUMsVUFBVSxFQUFFLEtBQUssRUFBRSxHQUFZLEVBQUUsR0FBYSxFQUFFLEVBQUU7UUFDeEQsTUFBTSxRQUFRLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxNQUE0QixDQUFDO1FBQ3hELElBQUksQ0FBQyxRQUFRO1lBQUUsT0FBTyxLQUFLLFNBQVMsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLDBCQUEwQixFQUFFLFNBQVMsQ0FBQyxZQUFZLENBQUMsQ0FBQztRQUNuRyxNQUFNLE1BQU0sR0FBRyxNQUFNLFdBQVcsQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxvQkFBb0I7UUFDeEUsSUFBSSxDQUFDLE1BQU07WUFBRSxPQUFPLEtBQUssU0FBUyxDQUFDLEdBQUcsRUFBRSxHQUFHLEVBQUUsMkJBQTJCLEVBQUUsU0FBUyxDQUFDLFlBQVksQ0FBQyxDQUFDO1FBQ2xHLDZFQUE2RTtRQUM3RSw2RUFBNkU7UUFDN0UsTUFBTSxLQUFLLEdBQUcsVUFBVSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQyxDQUFDO1FBQ3RELElBQUksQ0FBQyxLQUFLO1lBQUUsT0FBTyxLQUFLLFNBQVMsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLFlBQVksS0FBSyxjQUFjLEVBQUUsU0FBUyxDQUFDLGNBQWMsQ0FBQyxDQUFDO1FBQ3ZHLEdBQUcsQ0FBQyxTQUFTLENBQUMsY0FBYyxFQUFFLG1CQUFtQixDQUFDLENBQUM7UUFDbkQsR0FBRyxDQUFDLFNBQVMsQ0FBQyxlQUFlLEVBQUUsVUFBVSxDQUFDLENBQUM7UUFDM0MsR0FBRyxDQUFDLFNBQVMsQ0FBQyxZQUFZLEVBQUUsWUFBWSxDQUFDLENBQUM7UUFDMUMsR0FBRyxDQUFDLFNBQVMsQ0FBQyxtQkFBbUIsRUFBRSxJQUFJLENBQUMsQ0FBQztRQUN6QyxHQUFHLENBQUMsWUFBWSxFQUFFLENBQUM7UUFDbkIsTUFBTSxDQUFDLElBQUksQ0FBQyxzQkFBc0IsRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBQy9FLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIENvcHlyaWdodCAyMDI2IFBpcGVsaW5lIEJ1aWxkZXIgQ29udHJpYnV0b3JzXG4vLyBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMFxuXG5pbXBvcnQgeyByZXF1aXJlQXV0aCwgc2VuZFN1Y2Nlc3MsIHNlbmRFcnJvciwgRXJyb3JDb2RlLCBjcmVhdGVMb2dnZXIsIHR5cGUgU3NlVGlja2V0U3RvcmUgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgdHlwZSB7IEV4cHJlc3MsIFJlcXVlc3QsIFJlcXVlc3RIYW5kbGVyLCBSZXNwb25zZSB9IGZyb20gJ2V4cHJlc3MnO1xuaW1wb3J0IHR5cGUgeyBTU0VNYW5hZ2VyIH0gZnJvbSAnLi9zc2UtY29ubmVjdGlvbi1tYW5hZ2VyLmpzJztcblxuY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdzc2UtdGlja2V0LWNoYW5uZWwnKTtcblxuZXhwb3J0IGludGVyZmFjZSBTc2VUaWNrZXRDaGFubmVsT3B0aW9ucyB7XG4gIC8qKiBQT1NUIHJvdXRlIHRoYXQgZXhjaGFuZ2VzIHRoZSBKV1QgZm9yIGEgc2luZ2xlLXVzZSB0aWNrZXQgKGUuZy4gJy9tZXNzYWdlcy9ub3RpZmljYXRpb25zL3RpY2tldCcpLiAqL1xuICB0aWNrZXRQYXRoOiBzdHJpbmc7XG4gIC8qKiBHRVQgcm91dGUgdGhhdCByZWRlZW1zIHRoZSB0aWNrZXQgYW5kIG9wZW5zIHRoZSBFdmVudFNvdXJjZSAoZS5nLiAnL21lc3NhZ2VzL25vdGlmaWNhdGlvbnMnKS4gKi9cbiAgc3RyZWFtUGF0aDogc3RyaW5nO1xuICAvKiogT3JnLXNjb3BlZCwgc2luZ2xlLXVzZSB0aWNrZXQgc3RvcmUgKFJlZGlzLWJhY2tlZCBmb3IgbXVsdGktcmVwbGljYSkuICovXG4gIHRpY2tldFN0b3JlOiBTc2VUaWNrZXRTdG9yZTtcbiAgLyoqIFNTRSBtYW5hZ2VyIOKAlCB0aGUgb3JnIGlkIGlzIHRoZSBzdHJlYW0gc3ViamVjdCAoYHNzZU1hbmFnZXIuc2VuZChvcmdJZCwg4oCmKWApLiAqL1xuICBzc2VNYW5hZ2VyOiBTU0VNYW5hZ2VyO1xuICAvKiogTm91biB1c2VkIGluIGNhcGFjaXR5L2Nvbm5lY3Rpb24gZXJyb3IgbWVzc2FnZXMgKGUuZy4gJ25vdGlmaWNhdGlvbicsICdleGVjdXRpb24tc3RyZWFtJykuICovXG4gIGxhYmVsOiBzdHJpbmc7XG4gIC8qKiBFeHRyYSBtaWRkbGV3YXJlIGFwcGxpZWQgQUZURVIgcmVxdWlyZUF1dGggb24gdGhlIG1pbnQgcm91dGUgKGUuZy4gcmVxdWlyZVBlcm1pc3Npb24oJ3JlcG9ydHM6cmVhZCcpKS4gKi9cbiAgdGlja2V0R3VhcmRzPzogUmVxdWVzdEhhbmRsZXJbXTtcbn1cblxuLyoqXG4gKiBSZWdpc3RlciBhIHBlci1PUkcgU1NFIGNoYW5uZWw6IGEgUE9TVCB0aGF0IGV4Y2hhbmdlcyB0aGUgY2FsbGVyJ3MgVkVSSUZJRUQgSldUXG4gKiBvcmcgZm9yIGEgc2luZ2xlLXVzZSwgb3JnLWJvdW5kIHRpY2tldCwgYW5kIGEgR0VUIHRoYXQgcmVkZWVtcyB0aGUgdGlja2V0IGFuZFxuICogYXR0YWNoZXMgYW4gRXZlbnRTb3VyY2Uga2V5ZWQgYnkgdGhhdCBvcmcuIFRoZSBvcmcgSVMgdGhlIHN0cmVhbSBzdWJqZWN0LCBzbyBhXG4gKiBwcm9kdWNlciByZWFjaGVzIHN1YnNjcmliZXJzIHdpdGggYHNzZU1hbmFnZXIuc2VuZChvcmdJZCwg4oCmKWAgKGNyb3NzLXBvZCB2aWEgdGhlXG4gKiByZWxheSkuIFNoYXJlZCBieSB0aGUgbWVzc2FnZS1ub3RpZmljYXRpb24gYW5kIHJlcG9ydGluZyBleGVjdXRpb24tc3RhdHVzXG4gKiBjaGFubmVscyBzbyB0aGUgc3VidGxlIGJpdHMg4oCUIHRoZSB0aWNrZXTihpJhZGRDbGllbnTihpJmbHVzaEhlYWRlcnMgb3JkZXJpbmcgKGEgNDI5XG4gKiBNVVNUIHByZWNlZGUgZmx1c2hIZWFkZXJzLCB3aGljaCBjb21taXRzIHRoZSAyMDApIGFuZCB0aGUgY2FwYWNpdHkgc2VtYW50aWNzIOKAlFxuICogbGl2ZSBpbiBleGFjdGx5IG9uZSBwbGFjZS5cbiAqXG4gKiBEaXN0aW5jdCBmcm9tIGFwcC1mYWN0b3J5J3MgYC9sb2dzYCBjaGFubmVsLCB3aGljaCBpcyBTVUJKRUNULWJvdW5kIChwZXIgYnVpbGQsXG4gKiB2aWEgU1NFTWFuYWdlci5jcmVhdGVUaWNrZXQpIHJhdGhlciB0aGFuIG9yZy1ib3VuZC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlZ2lzdGVyU3NlVGlja2V0Q2hhbm5lbChhcHA6IEV4cHJlc3MsIG9wdHM6IFNzZVRpY2tldENoYW5uZWxPcHRpb25zKTogdm9pZCB7XG4gIGNvbnN0IHsgdGlja2V0UGF0aCwgc3RyZWFtUGF0aCwgdGlja2V0U3RvcmUsIHNzZU1hbmFnZXIsIGxhYmVsLCB0aWNrZXRHdWFyZHMgPSBbXSB9ID0gb3B0cztcblxuICBhcHAucG9zdCh0aWNrZXRQYXRoLCByZXF1aXJlQXV0aCwgLi4udGlja2V0R3VhcmRzLCBhc3luYyAocmVxOiBSZXF1ZXN0LCByZXM6IFJlc3BvbnNlKSA9PiB7XG4gICAgY29uc3Qgb3JnSWQgPSByZXEudXNlcj8ub3JnYW5pemF0aW9uSWQ/LnRvTG93ZXJDYXNlKCk7XG4gICAgaWYgKCFvcmdJZCkgcmV0dXJuIHNlbmRFcnJvcihyZXMsIDQwMCwgJ1Rva2VuIG1pc3Npbmcgb3JnYW5pemF0aW9uJywgRXJyb3JDb2RlLlZBTElEQVRJT05fRVJST1IpO1xuICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHRpY2tldFN0b3JlLmlzc3VlKG9yZ0lkKTtcbiAgICBpZiAoIXJlc3VsdC5vaykge1xuICAgICAgcmV0dXJuIHJlc3VsdC5yZWFzb24gPT09ICd0b3RhbCdcbiAgICAgICAgPyBzZW5kRXJyb3IocmVzLCA1MDMsIGBUaGUgJHtsYWJlbH0gc3Vic3lzdGVtIGlzIGF0IGNhcGFjaXR5YCwgRXJyb3JDb2RlLlFVT1RBX0VYQ0VFREVEKVxuICAgICAgICA6IHNlbmRFcnJvcihyZXMsIDQyOSwgYFRvbyBtYW55ICR7bGFiZWx9IHRpY2tldHMgaXNzdWVkYCwgRXJyb3JDb2RlLlFVT1RBX0VYQ0VFREVEKTtcbiAgICB9XG4gICAgcmV0dXJuIHNlbmRTdWNjZXNzKHJlcywgMjAwLCB7IHRpY2tldDogcmVzdWx0LnRpY2tldCB9KTtcbiAgfSk7XG5cbiAgYXBwLmdldChzdHJlYW1QYXRoLCBhc3luYyAocmVxOiBSZXF1ZXN0LCByZXM6IFJlc3BvbnNlKSA9PiB7XG4gICAgY29uc3QgdGlja2V0SWQgPSByZXEucXVlcnkudGlja2V0IGFzIHN0cmluZyB8IHVuZGVmaW5lZDtcbiAgICBpZiAoIXRpY2tldElkKSByZXR1cm4gdm9pZCBzZW5kRXJyb3IocmVzLCA0MDEsICdNaXNzaW5nIHRpY2tldCBwYXJhbWV0ZXInLCBFcnJvckNvZGUuVU5BVVRIT1JJWkVEKTtcbiAgICBjb25zdCB0aWNrZXQgPSBhd2FpdCB0aWNrZXRTdG9yZS5jb25zdW1lKHRpY2tldElkKTsgLy8gYXRvbWljIHNpbmdsZS11c2VcbiAgICBpZiAoIXRpY2tldCkgcmV0dXJuIHZvaWQgc2VuZEVycm9yKHJlcywgNDAxLCAnSW52YWxpZCBvciBleHBpcmVkIHRpY2tldCcsIEVycm9yQ29kZS5VTkFVVEhPUklaRUQpO1xuICAgIC8vIFJlc2VydmUgdGhlIGNvbm5lY3Rpb24gc2xvdCBCRUZPUkUgZmx1c2hpbmcgU1NFIGhlYWRlcnM6IG9uY2UgZmx1c2hIZWFkZXJzXG4gICAgLy8gcnVucyB0aGUgcmVzcG9uc2UgaXMgY29tbWl0dGVkIGF0IDIwMCBhbmQgYSBsYXRlciA0MjkgaXMgc2lsZW50bHkgZHJvcHBlZC5cbiAgICBjb25zdCBhZGRlZCA9IHNzZU1hbmFnZXIuYWRkQ2xpZW50KHRpY2tldC5vcmdJZCwgcmVzKTtcbiAgICBpZiAoIWFkZGVkKSByZXR1cm4gdm9pZCBzZW5kRXJyb3IocmVzLCA0MjksIGBUb28gbWFueSAke2xhYmVsfSBjb25uZWN0aW9uc2AsIEVycm9yQ29kZS5RVU9UQV9FWENFRURFRCk7XG4gICAgcmVzLnNldEhlYWRlcignQ29udGVudC1UeXBlJywgJ3RleHQvZXZlbnQtc3RyZWFtJyk7XG4gICAgcmVzLnNldEhlYWRlcignQ2FjaGUtQ29udHJvbCcsICduby1jYWNoZScpO1xuICAgIHJlcy5zZXRIZWFkZXIoJ0Nvbm5lY3Rpb24nLCAna2VlcC1hbGl2ZScpO1xuICAgIHJlcy5zZXRIZWFkZXIoJ1gtQWNjZWwtQnVmZmVyaW5nJywgJ25vJyk7XG4gICAgcmVzLmZsdXNoSGVhZGVycygpO1xuICAgIGxvZ2dlci5pbmZvKCdTU0UgY2xpZW50IGNvbm5lY3RlZCcsIHsgY2hhbm5lbDogbGFiZWwsIG9yZ0lkOiB0aWNrZXQub3JnSWQgfSk7XG4gIH0pO1xufVxuIl19
@@ -0,0 +1,75 @@
1
+ /** A minted ticket's bound identity: owning org + normalized stream subject. */
2
+ export interface SSEStreamTicketRecord {
3
+ orgId: string;
4
+ /** Normalized (dashes stripped, lowercased) stream subject id. */
5
+ requestId: string;
6
+ }
7
+ /**
8
+ * Pluggable backend for SSE log-stream tickets AND stream-ownership.
9
+ *
10
+ * NOTE: distinct from api-core's `SseTicketStore` (services/sse-ticket-store.ts),
11
+ * which is the message-service ticket store (issue + abuse counters). This one
12
+ * (api-server) additionally tracks stream ownership. The names deliberately differ
13
+ * (`SSEStreamTicketStore` here vs `SseTicketStore` there) so consumers don't pick
14
+ * the wrong one.
15
+ *
16
+ * Two concerns share one store because both must be consistent across pods:
17
+ * - **Tickets** — short-lived, single-use, bound to (org, subject). The default
18
+ * in-memory store is correct only single-replica; the Redis store makes the
19
+ * mint (one pod) / redeem (another pod) exchange work behind a load balancer.
20
+ * - **Stream ownership** — which org OWNS a given stream subject. The stream
21
+ * producer (platform, a different service) records this so ticket minting can
22
+ * assert the caller's org owns the subject, closing the cross-tenant attach
23
+ * gap where any org could mint a ticket for a guessed requestId.
24
+ *
25
+ * All methods are async (Redis is async) and best-effort on the failure side per
26
+ * their doc — a store outage must degrade gracefully, never crash the stream.
27
+ */
28
+ export interface SSEStreamTicketStore {
29
+ /** Persist a single-use ticket bound to `record`, expiring after `ttlMs`. */
30
+ put(ticketId: string, record: SSEStreamTicketRecord, ttlMs: number): Promise<void>;
31
+ /** Atomically fetch-and-delete a ticket (single-use). Null if absent/expired. */
32
+ consume(ticketId: string): Promise<SSEStreamTicketRecord | null>;
33
+ /** Count of live (unexpired) tickets held for an org (per-org cap). */
34
+ countForOrg(orgId: string): Promise<number>;
35
+ /** Count of all live tickets across the store (capacity cap). */
36
+ total(): Promise<number>;
37
+ /** Record the org that OWNS a stream subject (producer-side), TTL `ttlMs`. */
38
+ bindStreamOwner(requestId: string, orgId: string, ttlMs: number): Promise<void>;
39
+ /** The owning org of a stream subject, or null when none is bound. */
40
+ getStreamOwner(requestId: string): Promise<string | null>;
41
+ /** Optional hygiene sweep of expired entries (Redis TTL makes this a no-op). */
42
+ sweep?(): Promise<void>;
43
+ }
44
+ /**
45
+ * In-memory ticket store — the default. Correct for a single replica only;
46
+ * mirrors the behavior the SSEManager previously implemented inline.
47
+ */
48
+ export declare function createMemoryTicketStore(): SSEStreamTicketStore;
49
+ /**
50
+ * Minimal ioredis surface the Redis ticket store needs. `set` takes the variadic
51
+ * option tail (`EX <s>`); `zadd`/`zremrangebyscore`/`zcard` back the cap counters.
52
+ */
53
+ export interface RedisTicketClient {
54
+ get(key: string): Promise<string | null>;
55
+ set(key: string, value: string, ...args: (string | number)[]): Promise<string | null>;
56
+ del(...keys: string[]): Promise<number>;
57
+ zadd(key: string, score: number, member: string): Promise<number>;
58
+ zrem(key: string, ...members: string[]): Promise<number>;
59
+ zcard(key: string): Promise<number>;
60
+ zremrangebyscore(key: string, min: number | string, max: number | string): Promise<number>;
61
+ expire(key: string, seconds: number): Promise<number>;
62
+ }
63
+ /**
64
+ * Redis-backed ticket store — required for multi-pod correctness. Tickets are
65
+ * per-key with native TTL; cap counters are ZSETs scored by expiry (pruned on
66
+ * read). Stream ownership is a per-subject key with its own TTL. All reads are
67
+ * fail-safe (null / 0) so a Redis blip never wedges the stream.
68
+ */
69
+ export declare function createRedisTicketStore(redis: RedisTicketClient): SSEStreamTicketStore;
70
+ /**
71
+ * Build a Redis-backed ticket store from the shared env Redis (same wiring as
72
+ * the rate-limiter / audit-spool). Returns null when Redis isn't configured so
73
+ * the caller keeps the in-memory default. Never throws.
74
+ */
75
+ export declare function createEnvRedisTicketStore(): SSEStreamTicketStore | null;
@@ -0,0 +1,160 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { createLogger, createEnvRedisClient } from '@pipeline-builder/api-core';
4
+ const logger = createLogger('sse-ticket-store');
5
+ /**
6
+ * In-memory ticket store — the default. Correct for a single replica only;
7
+ * mirrors the behavior the SSEManager previously implemented inline.
8
+ */
9
+ export function createMemoryTicketStore() {
10
+ const tickets = new Map();
11
+ const owners = new Map();
12
+ return {
13
+ async put(ticketId, record, ttlMs) {
14
+ tickets.set(ticketId, { ...record, expiresAt: Date.now() + ttlMs });
15
+ },
16
+ async consume(ticketId) {
17
+ const t = tickets.get(ticketId);
18
+ tickets.delete(ticketId); // single-use — remove whether or not valid
19
+ if (!t || Date.now() > t.expiresAt)
20
+ return null;
21
+ return { orgId: t.orgId, requestId: t.requestId };
22
+ },
23
+ async countForOrg(orgId) {
24
+ const now = Date.now();
25
+ let n = 0;
26
+ for (const t of tickets.values())
27
+ if (t.orgId === orgId && t.expiresAt > now)
28
+ n++;
29
+ return n;
30
+ },
31
+ async total() {
32
+ const now = Date.now();
33
+ let n = 0;
34
+ for (const t of tickets.values())
35
+ if (t.expiresAt > now)
36
+ n++;
37
+ return n;
38
+ },
39
+ async bindStreamOwner(requestId, orgId, ttlMs) {
40
+ owners.set(requestId, { orgId, expiresAt: Date.now() + ttlMs });
41
+ },
42
+ async getStreamOwner(requestId) {
43
+ const o = owners.get(requestId);
44
+ if (!o)
45
+ return null;
46
+ if (Date.now() > o.expiresAt) {
47
+ owners.delete(requestId);
48
+ return null;
49
+ }
50
+ return o.orgId;
51
+ },
52
+ async sweep() {
53
+ const now = Date.now();
54
+ for (const [id, t] of tickets)
55
+ if (now > t.expiresAt)
56
+ tickets.delete(id);
57
+ for (const [id, o] of owners)
58
+ if (now > o.expiresAt)
59
+ owners.delete(id);
60
+ },
61
+ };
62
+ }
63
+ const TICKET_PREFIX = 'sse:tkt:';
64
+ const ORG_ZSET_PREFIX = 'sse:tkt:org:';
65
+ const ALL_ZSET = 'sse:tkt:all';
66
+ const OWNER_PREFIX = 'sse:owner:';
67
+ /**
68
+ * Redis-backed ticket store — required for multi-pod correctness. Tickets are
69
+ * per-key with native TTL; cap counters are ZSETs scored by expiry (pruned on
70
+ * read). Stream ownership is a per-subject key with its own TTL. All reads are
71
+ * fail-safe (null / 0) so a Redis blip never wedges the stream.
72
+ */
73
+ export function createRedisTicketStore(redis) {
74
+ const tkey = (id) => `${TICKET_PREFIX}${id}`;
75
+ const orgKey = (orgId) => `${ORG_ZSET_PREFIX}${orgId}`;
76
+ const ownerKey = (requestId) => `${OWNER_PREFIX}${requestId}`;
77
+ const ttlSec = (ttlMs) => Math.max(1, Math.ceil(ttlMs / 1000));
78
+ return {
79
+ async put(ticketId, record, ttlMs) {
80
+ const expiresAt = Date.now() + ttlMs;
81
+ const sec = ttlSec(ttlMs);
82
+ try {
83
+ await redis.set(tkey(ticketId), JSON.stringify(record), 'EX', sec);
84
+ // Track for the caps. Score = expiry so pruning is a range delete.
85
+ await redis.zadd(orgKey(record.orgId), expiresAt, ticketId);
86
+ await redis.zadd(ALL_ZSET, expiresAt, ticketId);
87
+ // Bound the per-org ZSET's own lifetime so an idle org's key can't leak.
88
+ await redis.expire(orgKey(record.orgId), sec);
89
+ }
90
+ catch (err) {
91
+ logger.warn('SSE ticket put failed', { error: err instanceof Error ? err.message : String(err) });
92
+ }
93
+ },
94
+ async consume(ticketId) {
95
+ try {
96
+ const raw = await redis.get(tkey(ticketId));
97
+ // Single-use: delete regardless. A concurrent double-redeem: at most one
98
+ // saw a non-null value before the delete; the ZREM cleans the counters.
99
+ await redis.del(tkey(ticketId));
100
+ if (!raw)
101
+ return null;
102
+ const rec = JSON.parse(raw);
103
+ await redis.zrem(orgKey(rec.orgId), ticketId);
104
+ await redis.zrem(ALL_ZSET, ticketId);
105
+ return rec;
106
+ }
107
+ catch (err) {
108
+ logger.warn('SSE ticket consume failed', { error: err instanceof Error ? err.message : String(err) });
109
+ return null;
110
+ }
111
+ },
112
+ async countForOrg(orgId) {
113
+ try {
114
+ await redis.zremrangebyscore(orgKey(orgId), 0, Date.now());
115
+ return await redis.zcard(orgKey(orgId));
116
+ }
117
+ catch {
118
+ return 0;
119
+ }
120
+ },
121
+ async total() {
122
+ try {
123
+ await redis.zremrangebyscore(ALL_ZSET, 0, Date.now());
124
+ return await redis.zcard(ALL_ZSET);
125
+ }
126
+ catch {
127
+ return 0;
128
+ }
129
+ },
130
+ async bindStreamOwner(requestId, orgId, ttlMs) {
131
+ try {
132
+ await redis.set(ownerKey(requestId), orgId, 'EX', ttlSec(ttlMs));
133
+ }
134
+ catch (err) {
135
+ logger.warn('SSE stream-owner bind failed', { error: err instanceof Error ? err.message : String(err) });
136
+ }
137
+ },
138
+ async getStreamOwner(requestId) {
139
+ try {
140
+ return await redis.get(ownerKey(requestId));
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ },
146
+ };
147
+ }
148
+ /**
149
+ * Build a Redis-backed ticket store from the shared env Redis (same wiring as
150
+ * the rate-limiter / audit-spool). Returns null when Redis isn't configured so
151
+ * the caller keeps the in-memory default. Never throws.
152
+ */
153
+ export function createEnvRedisTicketStore() {
154
+ const client = createEnvRedisClient('sse-ticket');
155
+ if (!client)
156
+ return null;
157
+ logger.info('Redis SSE ticket store initialized');
158
+ return createRedisTicketStore(client);
159
+ }
160
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3NlLXRpY2tldC1zdG9yZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9odHRwL3NzZS10aWNrZXQtc3RvcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQUV0QyxPQUFPLEVBQUUsWUFBWSxFQUFFLG9CQUFvQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFFaEYsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLGtCQUFrQixDQUFDLENBQUM7QUF3RGhEOzs7R0FHRztBQUNILE1BQU0sVUFBVSx1QkFBdUI7SUFDckMsTUFBTSxPQUFPLEdBQUcsSUFBSSxHQUFHLEVBQXdCLENBQUM7SUFDaEQsTUFBTSxNQUFNLEdBQUcsSUFBSSxHQUFHLEVBQXVCLENBQUM7SUFFOUMsT0FBTztRQUNMLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLE1BQU0sRUFBRSxLQUFLO1lBQy9CLE9BQU8sQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLEVBQUUsR0FBRyxNQUFNLEVBQUUsU0FBUyxFQUFFLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxLQUFLLEVBQUUsQ0FBQyxDQUFDO1FBQ3RFLENBQUM7UUFDRCxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVE7WUFDcEIsTUFBTSxDQUFDLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsQ0FBQztZQUNoQyxPQUFPLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsMkNBQTJDO1lBQ3JFLElBQUksQ0FBQyxDQUFDLElBQUksSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLENBQUMsQ0FBQyxTQUFTO2dCQUFFLE9BQU8sSUFBSSxDQUFDO1lBQ2hELE9BQU8sRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDLEtBQUssRUFBRSxTQUFTLEVBQUUsQ0FBQyxDQUFDLFNBQVMsRUFBRSxDQUFDO1FBQ3BELENBQUM7UUFDRCxLQUFLLENBQUMsV0FBVyxD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QUFPLElBQUksQ0FBQztJQUN6QixNQUFNLENBQUMsSUFBSSxDQUFDLG9DQUFvQyxDQUFDLENBQUM7SUFDbEQsT0FBTyxzQkFBc0IsQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUN4QyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciwgY3JlYXRlRW52UmVkaXNDbGllbnQgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5cbmNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcignc3NlLXRpY2tldC1zdG9yZScpO1xuXG4vKiogQSBtaW50ZWQgdGlja2V0J3MgYm91bmQgaWRlbnRpdHk6IG93bmluZyBvcmcgKyBub3JtYWxpemVkIHN0cmVhbSBzdWJqZWN0LiAqL1xuZXhwb3J0IGludGVyZmFjZSBTU0VTdHJlYW1UaWNrZXRSZWNvcmQge1xuICBvcmdJZDogc3RyaW5nO1xuICAvKiogTm9ybWFsaXplZCAoZGFzaGVzIHN0cmlwcGVkLCBsb3dlcmNhc2VkKSBzdHJlYW0gc3ViamVjdCBpZC4gKi9cbiAgcmVxdWVzdElkOiBzdHJpbmc7XG59XG5cbi8qKlxuICogUGx1Z2dhYmxlIGJhY2tlbmQgZm9yIFNTRSBsb2ctc3RyZWFtIHRpY2tldHMgQU5EIHN0cmVhbS1vd25lcnNoaXAuXG4gKlxuICogTk9URTogZGlzdGluY3QgZnJvbSBhcGktY29yZSdzIGBTc2VUaWNrZXRTdG9yZWAgKHNlcnZpY2VzL3NzZS10aWNrZXQtc3RvcmUudHMpLFxuICogd2hpY2ggaXMgdGhlIG1lc3NhZ2Utc2VydmljZSB0aWNrZXQgc3RvcmUgKGlzc3VlICsgYWJ1c2UgY291bnRlcnMpLiBUaGlzIG9uZVxuICogKGFwaS1zZXJ2ZXIpIGFkZGl0aW9uYWxseSB0cmFja3Mgc3RyZWFtIG93bmVyc2hpcC4gVGhlIG5hbWVzIGRlbGliZXJhdGVseSBkaWZmZXJcbiAqIChgU1NFU3RyZWFtVGlja2V0U3RvcmVgIGhlcmUgdnMgYFNzZVRpY2tldFN0b3JlYCB0aGVyZSkgc28gY29uc3VtZXJzIGRvbid0IHBpY2tcbiAqIHRoZSB3cm9uZyBvbmUuXG4gKlxuICogVHdvIGNvbmNlcm5zIHNoYXJlIG9uZSBzdG9yZSBiZWNhdXNlIGJvdGggbXVzdCBiZSBjb25zaXN0ZW50IGFjcm9zcyBwb2RzOlxuICogLSAqKlRpY2tldHMqKiDigJQgc2hvcnQtbGl2ZWQsIHNpbmdsZS11c2UsIGJvdW5kIHRvIChvcmcsIHN1YmplY3QpLiBUaGUgZGVmYXVsdFxuICogICBpbi1tZW1vcnkgc3RvcmUgaXMgY29ycmVjdCBvbmx5IHNpbmdsZS1yZXBsaWNhOyB0aGUgUmVkaXMgc3RvcmUgbWFrZXMgdGhlXG4gKiAgIG1pbnQgKG9uZSBwb2QpIC8gcmVkZWVtIChhbm90aGVyIHBvZCkgZXhjaGFuZ2Ugd29yayBiZWhpbmQgYSBsb2FkIGJhbGFuY2VyLlxuICogLSAqKlN0cmVhbSBvd25lcnNoaXAqKiDigJQgd2hpY2ggb3JnIE9XTlMgYSBnaXZlbiBzdHJlYW0gc3ViamVjdC4gVGhlIHN0cmVhbVxuICogICBwcm9kdWNlciAocGxhdGZvcm0sIGEgZGlmZmVyZW50IHNlcnZpY2UpIHJlY29yZHMgdGhpcyBzbyB0aWNrZXQgbWludGluZyBjYW5cbiAqICAgYXNzZXJ0IHRoZSBjYWxsZXIncyBvcmcgb3ducyB0aGUgc3ViamVjdCwgY2xvc2luZyB0aGUgY3Jvc3MtdGVuYW50IGF0dGFjaFxuICogICBnYXAgd2hlcmUgYW55IG9yZyBjb3VsZCBtaW50IGEgdGlja2V0IGZvciBhIGd1ZXNzZWQgcmVxdWVzdElkLlxuICpcbiAqIEFsbCBtZXRob2RzIGFyZSBhc3luYyAoUmVkaXMgaXMgYXN5bmMpIGFuZCBiZXN0LWVmZm9ydCBvbiB0aGUgZmFpbHVyZSBzaWRlIHBlclxuICogdGhlaXIgZG9jIOKAlCBhIHN0b3JlIG91dGFnZSBtdXN0IGRlZ3JhZGUgZ3JhY2VmdWxseSwgbmV2ZXIgY3Jhc2ggdGhlIHN0cmVhbS5cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBTU0VTdHJlYW1UaWNrZXRTdG9yZSB7XG4gIC8qKiBQZXJzaXN0IGEgc2luZ2xlLXVzZSB0aWNrZXQgYm91bmQgdG8gYHJlY29yZGAsIGV4cGlyaW5nIGFmdGVyIGB0dGxNc2AuICovXG4gIHB1dCh0aWNrZXRJZDogc3RyaW5nLCByZWNvcmQ6IFNTRVN0cmVhbVRpY2tldFJlY29yZCwgdHRsTXM6IG51bWJlcik6IFByb21pc2U8dm9pZD47XG4gIC8qKiBBdG9taWNhbGx5IGZldGNoLWFuZC1kZWxldGUgYSB0aWNrZXQgKHNpbmdsZS11c2UpLiBOdWxsIGlmIGFic2VudC9leHBpcmVkLiAqL1xuICBjb25zdW1lKHRpY2tldElkOiBzdHJpbmcpOiBQcm9taXNlPFNTRVN0cmVhbVRpY2tldFJlY29yZCB8IG51bGw+O1xuICAvKiogQ291bnQgb2YgbGl2ZSAodW5leHBpcmVkKSB0aWNrZXRzIGhlbGQgZm9yIGFuIG9yZyAocGVyLW9yZyBjYXApLiAqL1xuICBjb3VudEZvck9yZyhvcmdJZDogc3RyaW5nKTogUHJvbWlzZTxudW1iZXI+O1xuICAvKiogQ291bnQgb2YgYWxsIGxpdmUgdGlja2V0cyBhY3Jvc3MgdGhlIHN0b3JlIChjYXBhY2l0eSBjYXApLiAqL1xuICB0b3RhbCgpOiBQcm9taXNlPG51bWJlcj47XG4gIC8qKiBSZWNvcmQgdGhlIG9yZyB0aGF0IE9XTlMgYSBzdHJlYW0gc3ViamVjdCAocHJvZHVjZXItc2lkZSksIFRUTCBgdHRsTXNgLiAqL1xuICBiaW5kU3RyZWFtT3duZXIocmVxdWVzdElkOiBzdHJpbmcsIG9yZ0lkOiBzdHJpbmcsIHR0bE1zOiBudW1iZXIpOiBQcm9taXNlPHZvaWQ+O1xuICAvKiogVGhlIG93bmluZyBvcmcgb2YgYSBzdHJlYW0gc3ViamVjdCwgb3IgbnVsbCB3aGVuIG5vbmUgaXMgYm91bmQuICovXG4gIGdldFN0cmVhbU93bmVyKHJlcXVlc3RJZDogc3RyaW5nKTogUHJvbWlzZTxzdHJpbmcgfCBudWxsPjtcbiAgLyoqIE9wdGlvbmFsIGh5Z2llbmUgc3dlZXAgb2YgZXhwaXJlZCBlbnRyaWVzIChSZWRpcyBUVEwgbWFrZXMgdGhpcyBhIG5vLW9wKS4gKi9cbiAgc3dlZXA/KCk6IFByb21pc2U8dm9pZD47XG59XG5cbmludGVyZmFjZSBTdG9yZWRUaWNrZXQgZXh0ZW5kcyBTU0VTdHJlYW1UaWNrZXRSZWNvcmQge1xuICBleHBpcmVzQXQ6IG51bWJlcjtcbn1cblxuaW50ZXJmYWNlIFN0b3JlZE93bmVyIHtcbiAgb3JnSWQ6IHN0cmluZztcbiAgZXhwaXJlc0F0OiBudW1iZXI7XG59XG5cbi8qKlxuICogSW4tbWVtb3J5IHRpY2tldCBzdG9yZSDigJQgdGhlIGRlZmF1bHQuIENvcnJlY3QgZm9yIGEgc2luZ2xlIHJlcGxpY2Egb25seTtcbiAqIG1pcnJvcnMgdGhlIGJlaGF2aW9yIHRoZSBTU0VNYW5hZ2VyIHByZXZpb3VzbHkgaW1wbGVtZW50ZWQgaW5saW5lLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlTWVtb3J5VGlja2V0U3RvcmUoKTogU1NFU3RyZWFtVGlja2V0U3RvcmUge1xuICBjb25zdCB0aWNrZXRzID0gbmV3IE1hcDxzdHJpbmcsIFN0b3JlZFRpY2tldD4oKTtcbiAgY29uc3Qgb3duZXJzID0gbmV3IE1hcDxzdHJpbmcsIFN0b3JlZE93bmVyPigpO1xuXG4gIHJldHVybiB7XG4gICAgYXN5bmMgcHV0KHRpY2tldElkLCByZWNvcmQsIHR0bE1zKSB7XG4gICAgICB0aWNrZXRzLnNldCh0aWNrZXRJZCwgeyAuLi5yZWNvcmQsIGV4cGlyZXNBdDogRGF0ZS5ub3coKSArIHR0bE1zIH0pO1xuICAgIH0sXG4gICAgYXN5bmMgY29uc3VtZSh0aWNrZXRJZCkge1xuICAgICAgY29uc3QgdCA9IHRpY2tldHMuZ2V0KHRpY2tldElkKTtcbiAgICAgIHRpY2tldHMuZGVsZXRlKHRpY2tldElkKTsgLy8gc2luZ2xlLXVzZSDigJQgcmVtb3ZlIHdoZXRoZXIgb3Igbm90IHZhbGlkXG4gICAgICBpZiAoIXQgfHwgRGF0ZS5ub3coKSA+IHQuZXhwaXJlc0F0KSByZXR1cm4gbnVsbDtcbiAgICAgIHJldHVybiB7IG9yZ0lkOiB0Lm9yZ0lkLCByZXF1ZXN0SWQ6IHQucmVxdWVzdElkIH07XG4gICAgfSxcbiAgICBhc3luYyBjb3VudEZvck9yZyhvcmdJZCkge1xuICAgICAgY29uc3Qgbm93ID0gRGF0ZS5ub3coKTtcbiAgICAgIGxldCBuID0gMDtcbiAgICAgIGZvciAoY29uc3QgdCBvZiB0aWNrZXRzLnZhbHVlcygpKSBpZiAodC5vcmdJZCA9PT0gb3JnSWQgJiYgdC5leHBpcmVzQXQgPiBub3cpIG4rKztcbiAgICAgIHJldHVybiBuO1xuICAgIH0sXG4gICAgYXN5bmMgdG90YWwoKSB7XG4gICAgICBjb25zdCBub3cgPSBEYXRlLm5vdygpO1xuICAgICAgbGV0IG4gPSAwO1xuICAgICAgZm9yIChjb25zdCB0IG9mIHRpY2tldHMudmFsdWVzKCkpIGlmICh0LmV4cGlyZXNBdCA+IG5vdykgbisrO1xuICAgICAgcmV0dXJuIG47XG4gICAgfSxcbiAgICBhc3luYyBiaW5kU3RyZWFtT3duZXIocmVxdWVzdElkLCBvcmdJZCwgdHRsTXMpIHtcbiAgICAgIG93bmVycy5zZXQocmVxdWVzdElkLCB7IG9yZ0lkLCBleHBpcmVzQXQ6IERhdGUubm93KCkgKyB0dGxNcyB9KTtcbiAgICB9LFxuICAgIGFzeW5jIGdldFN0cmVhbU93bmVyKHJlcXVlc3RJZCkge1xuICAgICAgY29uc3QgbyA9IG93bmVycy5nZXQocmVxdWVzdElkKTtcbiAgICAgIGlmICghbykgcmV0dXJuIG51bGw7XG4gICAgICBpZiAoRGF0ZS5ub3coKSA+IG8uZXhwaXJlc0F0KSB7IG93bmVycy5kZWxldGUocmVxdWVzdElkKTsgcmV0dXJuIG51bGw7IH1cbiAgICAgIHJldHVybiBvLm9yZ0lkO1xuICAgIH0sXG4gICAgYXN5bmMgc3dlZXAoKSB7XG4gICAgICBjb25zdCBub3cgPSBEYXRlLm5vdygpO1xuICAgICAgZm9yIChjb25zdCBbaWQsIHRdIG9mIHRpY2tldHMpIGlmIChub3cgPiB0LmV4cGlyZXNBdCkgdGlja2V0cy5kZWxldGUoaWQpO1xuICAgICAgZm9yIChjb25zdCBbaWQsIG9dIG9mIG93bmVycykgaWYgKG5vdyA+IG8uZXhwaXJlc0F0KSBvd25lcnMuZGVsZXRlKGlkKTtcbiAgICB9LFxuICB9O1xufVxuXG4vKipcbiAqIE1pbmltYWwgaW9yZWRpcyBzdXJmYWNlIHRoZSBSZWRpcyB0aWNrZXQgc3RvcmUgbmVlZHMuIGBzZXRgIHRha2VzIHRoZSB2YXJpYWRpY1xuICogb3B0aW9uIHRhaWwgKGBFWCA8cz5gKTsgYHphZGRgL2B6cmVtcmFuZ2VieXNjb3JlYC9gemNhcmRgIGJhY2sgdGhlIGNhcCBjb3VudGVycy5cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBSZWRpc1RpY2tldENsaWVudCB7XG4gIGdldChrZXk6IHN0cmluZyk6IFByb21pc2U8c3RyaW5nIHwgbnVsbD47XG4gIHNldChrZXk6IHN0cmluZywgdmFsdWU6IHN0cmluZywgLi4uYXJnczogKHN0cmluZyB8IG51bWJlcilbXSk6IFByb21pc2U8c3RyaW5nIHwgbnVsbD47XG4gIGRlbCguLi5rZXlzOiBzdHJpbmdbXSk6IFByb21pc2U8bnVtYmVyPjtcbiAgemFkZChrZXk6IHN0cmluZywgc2NvcmU6IG51bWJlciwgbWVtYmVyOiBzdHJpbmcpOiBQcm9taXNlPG51bWJlcj47XG4gIHpyZW0oa2V5OiBzdHJpbmcsIC4uLm1lbWJlcnM6IHN0cmluZ1tdKTogUHJvbWlzZTxudW1iZXI+O1xuICB6Y2FyZChrZXk6IHN0cmluZyk6IFByb21pc2U8bnVtYmVyPjtcbiAgenJlbXJhbmdlYnlzY29yZShrZXk6IHN0cmluZywgbWluOiBudW1iZXIgfCBzdHJpbmcsIG1heDogbnVtYmVyIHwgc3RyaW5nKTogUHJvbWlzZTxudW1iZXI+O1xuICBleHBpcmUoa2V5OiBzdHJpbmcsIHNlY29uZHM6IG51bWJlcik6IFByb21pc2U8bnVtYmVyPjtcbn1cblxuY29uc3QgVElDS0VUX1BSRUZJWCA9ICdzc2U6dGt0Oic7XG5jb25zdCBPUkdfWlNFVF9QUkVGSVggPSAnc3NlOnRrdDpvcmc6JztcbmNvbnN0IEFMTF9aU0VUID0gJ3NzZTp0a3Q6YWxsJztcbmNvbnN0IE9XTkVSX1BSRUZJWCA9ICdzc2U6b3duZXI6JztcblxuLyoqXG4gKiBSZWRpcy1iYWNrZWQgdGlja2V0IHN0b3JlIOKAlCByZXF1aXJlZCBmb3IgbXVsdGktcG9kIGNvcnJlY3RuZXNzLiBUaWNrZXRzIGFyZVxuICogcGVyLWtleSB3aXRoIG5hdGl2ZSBUVEw7IGNhcCBjb3VudGVycyBhcmUgWlNFVHMgc2NvcmVkIGJ5IGV4cGlyeSAocHJ1bmVkIG9uXG4gKiByZWFkKS4gU3RyZWFtIG93bmVyc2hpcCBpcyBhIHBlci1zdWJqZWN0IGtleSB3aXRoIGl0cyBvd24gVFRMLiBBbGwgcmVhZHMgYXJlXG4gKiBmYWlsLXNhZmUgKG51bGwgLyAwKSBzbyBhIFJlZGlzIGJsaXAgbmV2ZXIgd2VkZ2VzIHRoZSBzdHJlYW0uXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVSZWRpc1RpY2tldFN0b3JlKHJlZGlzOiBSZWRpc1RpY2tldENsaWVudCk6IFNTRVN0cmVhbVRpY2tldFN0b3JlIHtcbiAgY29uc3QgdGtleSA9IChpZDogc3RyaW5nKTogc3RyaW5nID0+IGAke1RJQ0tFVF9QUkVGSVh9JHtpZH1gO1xuICBjb25zdCBvcmdLZXkgPSAob3JnSWQ6IHN0cmluZyk6IHN0cmluZyA9PiBgJHtPUkdfWlNFVF9QUkVGSVh9JHtvcmdJZH1gO1xuICBjb25zdCBvd25lcktleSA9IChyZXF1ZXN0SWQ6IHN0cmluZyk6IHN0cmluZyA9PiBgJHtPV05FUl9QUkVGSVh9JHtyZXF1ZXN0SWR9YDtcbiAgY29uc3QgdHRsU2VjID0gKHR0bE1zOiBudW1iZXIpOiBudW1iZXIgPT4gTWF0aC5tYXgoMSwgTWF0aC5jZWlsKHR0bE1zIC8gMTAwMCkpO1xuXG4gIHJldHVybiB7XG4gICAgYXN5bmMgcHV0KHRpY2tldElkLCByZWNvcmQsIHR0bE1zKSB7XG4gICAgICBjb25zdCBleHBpcmVzQXQgPSBEYXRlLm5vdygpICsgdHRsTXM7XG4gICAgICBjb25zdCBzZWMgPSB0dGxTZWModHRsTXMpO1xuICAgICAgdHJ5IHtcbiAgICAgICAgYXdhaXQgcmVkaXMuc2V0KHRrZXkodGlja2V0SWQpLCBKU09OLnN0cmluZ2lmeShyZWNvcmQpLCAnRVgnLCBzZWMpO1xuICAgICAgICAvLyBUcmFjayBmb3IgdGhlIGNhcHMuIFNjb3JlID0gZXhwaXJ5IHNvIHBydW5pbmcgaXMgYSByYW5nZSBkZWxldGUuXG4gICAgICAgIGF3YWl0IHJlZGlzLnphZGQob3JnS2V5KHJlY29yZC5vcmdJZCksIGV4cGlyZXNBdCwgdGlja2V0SWQpO1xuICAgICAgICBhd2FpdCByZWRpcy56YWRkKEFMTF9aU0VULCBleHBpcmVzQXQsIHRpY2tldElkKTtcbiAgICAgICAgLy8gQm91bmQgdGhlIHBlci1vcmcgWlNFVCdzIG93biBsaWZldGltZSBzbyBhbiBpZGxlIG9yZydzIGtleSBjYW4ndCBsZWFrLlxuICAgICAgICBhd2FpdCByZWRpcy5leHBpcmUob3JnS2V5KHJlY29yZC5vcmdJZCksIHNlYyk7XG4gICAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgbG9nZ2VyLndhcm4oJ1NTRSB0aWNrZXQgcHV0IGZhaWxlZCcsIHsgZXJyb3I6IGVyciBpbnN0YW5jZW9mIEVycm9yID8gZXJyLm1lc3NhZ2UgOiBTdHJpbmcoZXJyKSB9KTtcbiAgICAgIH1cbiAgICB9LFxuICAgIGFzeW5jIGNvbnN1bWUodGlja2V0SWQpIHtcbiAgICAgIHRyeSB7XG4gICAgICAgIGNvbnN0IHJhdyA9IGF3YWl0IHJlZGlzLmdldCh0a2V5KHRpY2tldElkKSk7XG4gICAgICAgIC8vIFNpbmdsZS11c2U6IGRlbGV0ZSByZWdhcmRsZXNzLiBBIGNvbmN1cnJlbnQgZG91YmxlLXJlZGVlbTogYXQgbW9zdCBvbmVcbiAgICAgICAgLy8gc2F3IGEgbm9uLW51bGwgdmFsdWUgYmVmb3JlIHRoZSBkZWxldGU7IHRoZSBaUkVNIGNsZWFucyB0aGUgY291bnRlcnMuXG4gICAgICAgIGF3YWl0IHJlZGlzLmRlbCh0a2V5KHRpY2tldElkKSk7XG4gICAgICAgIGlmICghcmF3KSByZXR1cm4gbnVsbDtcbiAgICAgICAgY29uc3QgcmVjID0gSlNPTi5wYXJzZShyYXcpIGFzIFNTRVN0cmVhbVRpY2tldFJlY29yZDtcbiAgICAgICAgYXdhaXQgcmVkaXMuenJlbShvcmdLZXkocmVjLm9yZ0lkKSwgdGlja2V0SWQpO1xuICAgICAgICBhd2FpdCByZWRpcy56cmVtKEFMTF9aU0VULCB0aWNrZXRJZCk7XG4gICAgICAgIHJldHVybiByZWM7XG4gICAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgbG9nZ2VyLndhcm4oJ1NTRSB0aWNrZXQgY29uc3VtZSBmYWlsZWQnLCB7IGVycm9yOiBlcnIgaW5zdGFuY2VvZiBFcnJvciA/IGVyci5tZXNzYWdlIDogU3RyaW5nKGVycikgfSk7XG4gICAgICAgIHJldHVybiBudWxsO1xuICAgICAgfVxuICAgIH0sXG4gICAgYXN5bmMgY291bnRGb3JPcmcob3JnSWQpIHtcbiAgICAgIHRyeSB7XG4gICAgICAgIGF3YWl0IHJlZGlzLnpyZW1yYW5nZWJ5c2NvcmUob3JnS2V5KG9yZ0lkKSwgMCwgRGF0ZS5ub3coKSk7XG4gICAgICAgIHJldHVybiBhd2FpdCByZWRpcy56Y2FyZChvcmdLZXkob3JnSWQpKTtcbiAgICAgIH0gY2F0Y2ggeyByZXR1cm4gMDsgfVxuICAgIH0sXG4gICAgYXN5bmMgdG90YWwoKSB7XG4gICAgICB0cnkge1xuICAgICAgICBhd2FpdCByZWRpcy56cmVtcmFuZ2VieXNjb3JlKEFMTF9aU0VULCAwLCBEYXRlLm5vdygpKTtcbiAgICAgICAgcmV0dXJuIGF3YWl0IHJlZGlzLnpjYXJkKEFMTF9aU0VUKTtcbiAgICAgIH0gY2F0Y2ggeyByZXR1cm4gMDsgfVxuICAgIH0sXG4gICAgYXN5bmMgYmluZFN0cmVhbU93bmVyKHJlcXVlc3RJZCwgb3JnSWQsIHR0bE1zKSB7XG4gICAgICB0cnkge1xuICAgICAgICBhd2FpdCByZWRpcy5zZXQob3duZXJLZXkocmVxdWVzdElkKSwgb3JnSWQsICdFWCcsIHR0bFNlYyh0dGxNcykpO1xuICAgICAgfSBjYXRjaCAoZXJyKSB7XG4gICAgICAgIGxvZ2dlci53YXJuKCdTU0Ugc3RyZWFtLW93bmVyIGJpbmQgZmFpbGVkJywgeyBlcnJvcjogZXJyIGluc3RhbmNlb2YgRXJyb3IgPyBlcnIubWVzc2FnZSA6IFN0cmluZyhlcnIpIH0pO1xuICAgICAgfVxuICAgIH0sXG4gICAgYXN5bmMgZ2V0U3RyZWFtT3duZXIocmVxdWVzdElkKSB7XG4gICAgICB0cnkge1xuICAgICAgICByZXR1cm4gYXdhaXQgcmVkaXMuZ2V0KG93bmVyS2V5KHJlcXVlc3RJZCkpO1xuICAgICAgfSBjYXRjaCB7IHJldHVybiBudWxsOyB9XG4gICAgfSxcbiAgfTtcbn1cblxuLyoqXG4gKiBCdWlsZCBhIFJlZGlzLWJhY2tlZCB0aWNrZXQgc3RvcmUgZnJvbSB0aGUgc2hhcmVkIGVudiBSZWRpcyAoc2FtZSB3aXJpbmcgYXNcbiAqIHRoZSByYXRlLWxpbWl0ZXIgLyBhdWRpdC1zcG9vbCkuIFJldHVybnMgbnVsbCB3aGVuIFJlZGlzIGlzbid0IGNvbmZpZ3VyZWQgc29cbiAqIHRoZSBjYWxsZXIga2VlcHMgdGhlIGluLW1lbW9yeSBkZWZhdWx0LiBOZXZlciB0aHJvd3MuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVFbnZSZWRpc1RpY2tldFN0b3JlKCk6IFNTRVN0cmVhbVRpY2tldFN0b3JlIHwgbnVsbCB7XG4gIGNvbnN0IGNsaWVudCA9IGNyZWF0ZUVudlJlZGlzQ2xpZW50PFJlZGlzVGlja2V0Q2xpZW50Pignc3NlLXRpY2tldCcpO1xuICBpZiAoIWNsaWVudCkgcmV0dXJuIG51bGw7XG4gIGxvZ2dlci5pbmZvKCdSZWRpcyBTU0UgdGlja2V0IHN0b3JlIGluaXRpYWxpemVkJyk7XG4gIHJldHVybiBjcmVhdGVSZWRpc1RpY2tldFN0b3JlKGNsaWVudCk7XG59XG4iXX0=
package/lib/index.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @module @pipeline-builder/api-server
3
+ *
4
+ * Express server infrastructure and request lifecycle utilities.
5
+ *
6
+ * **App Factory**
7
+ * - createApp — creates a configured Express app with CORS, Helmet, rate limiting
8
+ * - runServer, startServer — server lifecycle with graceful shutdown
9
+ *
10
+ * **Middleware**
11
+ * - attachRequestContext / createRequestContext — attaches identity + logging to each request
12
+ * - requireOrgId — validates organization ID is present on the request
13
+ * - withTenantContext — opens the RLS tenant scope for the request
14
+ * - checkQuota — quota enforcement middleware
15
+ * - idempotencyMiddleware — idempotent request handling
16
+ * - createProtectedRoute / createAuthenticatedWithOrgRoute — composable middleware chains
17
+ *
18
+ * **Route Helpers**
19
+ * - withRoute — wraps async route handlers with context extraction, orgId validation, and error handling
20
+ * - getContext — retrieves RequestContext from the Express request
21
+ * - RouteContext, RequestContext — route and request context types
22
+ *
23
+ * **SSE**
24
+ * - SSEManager — Server-Sent Events connection manager
25
+ *
26
+ * **Observability**
27
+ * - Tracing and metrics collection utilities
28
+ */
29
+ export * from './api/index.js';
30
+ export * from './http/index.js';
package/lib/index.js ADDED
@@ -0,0 +1,35 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * @module @pipeline-builder/api-server
5
+ *
6
+ * Express server infrastructure and request lifecycle utilities.
7
+ *
8
+ * **App Factory**
9
+ * - createApp — creates a configured Express app with CORS, Helmet, rate limiting
10
+ * - runServer, startServer — server lifecycle with graceful shutdown
11
+ *
12
+ * **Middleware**
13
+ * - attachRequestContext / createRequestContext — attaches identity + logging to each request
14
+ * - requireOrgId — validates organization ID is present on the request
15
+ * - withTenantContext — opens the RLS tenant scope for the request
16
+ * - checkQuota — quota enforcement middleware
17
+ * - idempotencyMiddleware — idempotent request handling
18
+ * - createProtectedRoute / createAuthenticatedWithOrgRoute — composable middleware chains
19
+ *
20
+ * **Route Helpers**
21
+ * - withRoute — wraps async route handlers with context extraction, orgId validation, and error handling
22
+ * - getContext — retrieves RequestContext from the Express request
23
+ * - RouteContext, RequestContext — route and request context types
24
+ *
25
+ * **SSE**
26
+ * - SSEManager — Server-Sent Events connection manager
27
+ *
28
+ * **Observability**
29
+ * - Tracing and metrics collection utilities
30
+ */
31
+ // API Infrastructure
32
+ export * from './api/index.js';
33
+ // HTTP Utilities
34
+ export * from './http/index.js';
35
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQUV0Qzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBMkJHO0FBRUgscUJBQXFCO0FBQ3JCLGNBQWMsZ0JBQWdCLENBQUM7QUFFL0IsaUJBQWlCO0FBQ2pCLGNBQWMsaUJBQWlCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuLyoqXG4gKiBAbW9kdWxlIEBwaXBlbGluZS1idWlsZGVyL2FwaS1zZXJ2ZXJcbiAqXG4gKiBFeHByZXNzIHNlcnZlciBpbmZyYXN0cnVjdHVyZSBhbmQgcmVxdWVzdCBsaWZlY3ljbGUgdXRpbGl0aWVzLlxuICpcbiAqICoqQXBwIEZhY3RvcnkqKlxuICogLSBjcmVhdGVBcHAg4oCUIGNyZWF0ZXMgYSBjb25maWd1cmVkIEV4cHJlc3MgYXBwIHdpdGggQ09SUywgSGVsbWV0LCByYXRlIGxpbWl0aW5nXG4gKiAtIHJ1blNlcnZlciwgc3RhcnRTZXJ2ZXIg4oCUIHNlcnZlciBsaWZlY3ljbGUgd2l0aCBncmFjZWZ1bCBzaHV0ZG93blxuICpcbiAqICoqTWlkZGxld2FyZSoqXG4gKiAtIGF0dGFjaFJlcXVlc3RDb250ZXh0IC8gY3JlYXRlUmVxdWVzdENvbnRleHQg4oCUIGF0dGFjaGVzIGlkZW50aXR5ICsgbG9nZ2luZyB0byBlYWNoIHJlcXVlc3RcbiAqIC0gcmVxdWlyZU9yZ0lkIOKAlCB2YWxpZGF0ZXMgb3JnYW5pemF0aW9uIElEIGlzIHByZXNlbnQgb24gdGhlIHJlcXVlc3RcbiAqIC0gd2l0aFRlbmFudENvbnRleHQg4oCUIG9wZW5zIHRoZSBSTFMgdGVuYW50IHNjb3BlIGZvciB0aGUgcmVxdWVzdFxuICogLSBjaGVja1F1b3RhIOKAlCBxdW90YSBlbmZvcmNlbWVudCBtaWRkbGV3YXJlXG4gKiAtIGlkZW1wb3RlbmN5TWlkZGxld2FyZSDigJQgaWRlbXBvdGVudCByZXF1ZXN0IGhhbmRsaW5nXG4gKiAtIGNyZWF0ZVByb3RlY3RlZFJvdXRlIC8gY3JlYXRlQXV0aGVudGljYXRlZFdpdGhPcmdSb3V0ZSDigJQgY29tcG9zYWJsZSBtaWRkbGV3YXJlIGNoYWluc1xuICpcbiAqICoqUm91dGUgSGVscGVycyoqXG4gKiAtIHdpdGhSb3V0ZSDigJQgd3JhcHMgYXN5bmMgcm91dGUgaGFuZGxlcnMgd2l0aCBjb250ZXh0IGV4dHJhY3Rpb24sIG9yZ0lkIHZhbGlkYXRpb24sIGFuZCBlcnJvciBoYW5kbGluZ1xuICogLSBnZXRDb250ZXh0IOKAlCByZXRyaWV2ZXMgUmVxdWVzdENvbnRleHQgZnJvbSB0aGUgRXhwcmVzcyByZXF1ZXN0XG4gKiAtIFJvdXRlQ29udGV4dCwgUmVxdWVzdENvbnRleHQg4oCUIHJvdXRlIGFuZCByZXF1ZXN0IGNvbnRleHQgdHlwZXNcbiAqXG4gKiAqKlNTRSoqXG4gKiAtIFNTRU1hbmFnZXIg4oCUIFNlcnZlci1TZW50IEV2ZW50cyBjb25uZWN0aW9uIG1hbmFnZXJcbiAqXG4gKiAqKk9ic2VydmFiaWxpdHkqKlxuICogLSBUcmFjaW5nIGFuZCBtZXRyaWNzIGNvbGxlY3Rpb24gdXRpbGl0aWVzXG4gKi9cblxuLy8gQVBJIEluZnJhc3RydWN0dXJlXG5leHBvcnQgKiBmcm9tICcuL2FwaS9pbmRleC5qcyc7XG5cbi8vIEhUVFAgVXRpbGl0aWVzXG5leHBvcnQgKiBmcm9tICcuL2h0dHAvaW5kZXguanMnOyJdfQ==
@@ -0,0 +1 @@
1
+ export {};