@pipeline-builder/api-server 4.1.21 → 4.1.23

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.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Minting and redeeming the short-lived SSE tickets that authorize a log
3
+ * stream, plus the stream-ownership binding those mints are checked against.
4
+ *
5
+ * Separate from {@link SSEManager} because it shares nothing with the
6
+ * connection registry: its state lives entirely in the injected
7
+ * {@link SseTicketStore} (Redis in a multi-pod deploy), and every rule in here
8
+ * is an AUTHORIZATION rule — who may open which subject's stream — which is
9
+ * easier to audit as one unit than interleaved with connection bookkeeping.
10
+ */
11
+ import { type SseTicketStore } from '@pipeline-builder/api-core';
12
+ /** Result of {@link SseTicketBroker.createTicket}. */
13
+ export type CreateTicketResult = {
14
+ ok: true;
15
+ ticket: string;
16
+ } | {
17
+ ok: false;
18
+ reason: 'org-limit' | 'capacity' | 'forbidden';
19
+ };
20
+ export interface SseTicketBrokerOptions {
21
+ /** Ticket + stream-ownership backend. Defaults to an in-memory store (single
22
+ * replica only), built on first use. Inject an api-core
23
+ * `createEnvSseTicketStore(...)` so mint/redeem and stream ownership work
24
+ * across pods. */
25
+ store?: SseTicketStore;
26
+ /** Cap on live tickets across the process, for the DEFAULT in-memory store.
27
+ * Ignored when `store` is injected (the store carries its own caps). */
28
+ maxTotal?: number;
29
+ /** Per-org cap on live tickets, for the DEFAULT in-memory store. Ignored when
30
+ * `store` is injected. */
31
+ maxPerOrg?: number;
32
+ /** Ticket TTL in ms for the default store. */
33
+ ttlMs?: number;
34
+ /** TTL for a stream-ownership binding (default 1h). Long enough to outlive a
35
+ * build; the producer re-binds as needed. */
36
+ ownerTtlMs?: number;
37
+ }
38
+ /**
39
+ * Canonical form of a stream subject id for map/store keys: the id is allowed
40
+ * in two renderings (a dashed api-server uuid and nginx's undashed
41
+ * `$request_id`), and normalizing makes them compare equal. Callers are
42
+ * expected to have already format-validated against `SSE_REQUEST_ID_RE`.
43
+ */
44
+ export declare function normalizeRequestId(requestId: string): string;
45
+ /**
46
+ * Canonical form of an orgId for ownership equality checks: trimmed and
47
+ * lowercased. The stream PRODUCER (bindStreamOwner) and the ticket-minting
48
+ * CONSUMER (createTicket) run in different services, so a casing / whitespace
49
+ * drift between how each renders the same org would otherwise make the owner
50
+ * comparison in {@link SseTicketBroker.createTicket} false-`forbidden` the real
51
+ * owner. Normalizing both sides — mirroring {@link normalizeRequestId} — closes that.
52
+ */
53
+ export declare function normalizeOrgId(orgId: string): string;
54
+ export declare class SseTicketBroker {
55
+ private storeInstance?;
56
+ /** True when this broker built its store (so shutdown stops it). */
57
+ private ownsStore;
58
+ private readonly maxTotal;
59
+ private readonly maxPerOrg;
60
+ private readonly ttlMs;
61
+ private readonly ownerTtlMs;
62
+ constructor(options?: SseTicketBrokerOptions);
63
+ /** The ticket store, building the in-memory default on first use. */
64
+ private get store();
65
+ /**
66
+ * Record the org that OWNS a stream subject. The stream PRODUCER calls this
67
+ * when it creates a build-log stream so that ticket minting can assert the
68
+ * caller's org owns the subject (see {@link createTicket}). Backed by the
69
+ * injected ticket store, so when a Redis store is wired the platform producer
70
+ * (a different service sharing the same Redis) can bind ownership that this
71
+ * service reads — closing the cross-tenant attach gap where any org could mint
72
+ * a ticket for a guessed requestId.
73
+ *
74
+ * @param requestId - The stream subject (format-validated by the caller).
75
+ * @param orgId - The owning org (normalized internally via normalizeOrgId).
76
+ */
77
+ bindStreamOwner(requestId: string, orgId: string): Promise<void>;
78
+ /**
79
+ * Mint a short-lived, single-use SSE ticket bound to `orgId` AND a specific
80
+ * stream subject (`requestId`). Clients POST to obtain one (JWT-authenticated)
81
+ * for the exact stream they intend to open, then open the EventSource with
82
+ * `?ticket=<t>` so the JWT never lands in a query string / access log. The
83
+ * ticket can then only be consumed to open that one subject's stream — see
84
+ * {@link consumeTicket} — which is what enforces per-subject authorization.
85
+ *
86
+ * ORG-OWNERSHIP: if a stream owner has been bound for this subject (via
87
+ * {@link bindStreamOwner}) and it is a DIFFERENT org, minting is refused
88
+ * (`reason: 'forbidden'`) — an org cannot mint a ticket for another org's
89
+ * stream even if it guesses the requestId. When no owner is bound (producer
90
+ * wiring not yet present), minting falls back to binding the ticket to the
91
+ * caller's own org, preserving current behavior.
92
+ *
93
+ * Bounded by the ticket store's live-ticket caps (total and per org). An
94
+ * ownership lookup the store can't answer is refused as `capacity`.
95
+ *
96
+ * @param orgId - Owning org (normalized internally via normalizeOrgId).
97
+ * @param requestId - The build-log stream subject this ticket authorizes.
98
+ * The caller must have format-validated it (see `SSE_REQUEST_ID_RE`).
99
+ * @returns `{ ok: true, ticket }` on success, or `{ ok: false, reason }`
100
+ * where reason is `'org-limit'`, `'capacity'`, or `'forbidden'`.
101
+ */
102
+ createTicket(orgId: string, requestId: string): Promise<CreateTicketResult>;
103
+ /**
104
+ * Validate and CONSUME a ticket for a specific stream subject. Single-use:
105
+ * the ticket is deleted whether or not it turns out to be valid, so a replay
106
+ * of the same value always fails. Returns the bound org on success, or null
107
+ * when the ticket is unknown / already-used / expired / OR was minted for a
108
+ * DIFFERENT `requestId` than the one being opened.
109
+ *
110
+ * The subject check is the authorization fix: without it, a ticket minted for
111
+ * one stream could be presented to open ANY stream the holder's org could
112
+ * name, letting an authenticated org attach to another org's log stream by
113
+ * guessing its requestId. The generic 401 the caller returns does not
114
+ * distinguish "unknown ticket" from "wrong subject", so it leaks nothing
115
+ * about which requestIds exist.
116
+ *
117
+ * @param ticketId - The opaque ticket value from `?ticket=`.
118
+ * @param requestId - The stream subject from the URL path (`:requestId`),
119
+ * already format-validated by the caller.
120
+ */
121
+ consumeTicket(ticketId: string, requestId: string): Promise<{
122
+ orgId: string;
123
+ } | null>;
124
+ /** Stop the store, but only the one this broker built — an injected store is
125
+ * the caller's to close. */
126
+ shutdown(): void;
127
+ }
@@ -0,0 +1,170 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Minting and redeeming the short-lived SSE tickets that authorize a log
5
+ * stream, plus the stream-ownership binding those mints are checked against.
6
+ *
7
+ * Separate from {@link SSEManager} because it shares nothing with the
8
+ * connection registry: its state lives entirely in the injected
9
+ * {@link SseTicketStore} (Redis in a multi-pod deploy), and every rule in here
10
+ * is an AUTHORIZATION rule — who may open which subject's stream — which is
11
+ * easier to audit as one unit than interleaved with connection bookkeeping.
12
+ */
13
+ import { createLogger, createMemorySseTicketStore, envSseTicketCaps, SSE_TICKET_TTL_MS, errorMessage } from '@pipeline-builder/api-core';
14
+ import { incCounter } from '../api/metrics.js';
15
+ const logger = createLogger('sse-tickets');
16
+ /**
17
+ * Canonical form of a stream subject id for map/store keys: the id is allowed
18
+ * in two renderings (a dashed api-server uuid and nginx's undashed
19
+ * `$request_id`), and normalizing makes them compare equal. Callers are
20
+ * expected to have already format-validated against `SSE_REQUEST_ID_RE`.
21
+ */
22
+ export function normalizeRequestId(requestId) {
23
+ return requestId.replace(/-/g, '').toLowerCase();
24
+ }
25
+ /**
26
+ * Canonical form of an orgId for ownership equality checks: trimmed and
27
+ * lowercased. The stream PRODUCER (bindStreamOwner) and the ticket-minting
28
+ * CONSUMER (createTicket) run in different services, so a casing / whitespace
29
+ * drift between how each renders the same org would otherwise make the owner
30
+ * comparison in {@link SseTicketBroker.createTicket} false-`forbidden` the real
31
+ * owner. Normalizing both sides — mirroring {@link normalizeRequestId} — closes that.
32
+ */
33
+ export function normalizeOrgId(orgId) {
34
+ return orgId.trim().toLowerCase();
35
+ }
36
+ export class SseTicketBroker {
37
+ storeInstance;
38
+ /** True when this broker built its store (so shutdown stops it). */
39
+ ownsStore = false;
40
+ maxTotal;
41
+ maxPerOrg;
42
+ ttlMs;
43
+ ownerTtlMs;
44
+ constructor(options = {}) {
45
+ const caps = envSseTicketCaps();
46
+ this.maxTotal = options.maxTotal ?? caps.maxTotal;
47
+ this.maxPerOrg = options.maxPerOrg ?? caps.maxPerOrg;
48
+ this.ttlMs = options.ttlMs ?? SSE_TICKET_TTL_MS;
49
+ this.ownerTtlMs = options.ownerTtlMs ?? 3_600_000; // 1 hour
50
+ this.storeInstance = options.store;
51
+ }
52
+ /** The ticket store, building the in-memory default on first use. */
53
+ get store() {
54
+ if (!this.storeInstance) {
55
+ this.storeInstance = createMemorySseTicketStore({
56
+ ttlMs: this.ttlMs,
57
+ maxTotal: this.maxTotal,
58
+ maxPerOrg: this.maxPerOrg,
59
+ });
60
+ this.ownsStore = true;
61
+ }
62
+ return this.storeInstance;
63
+ }
64
+ /**
65
+ * Record the org that OWNS a stream subject. The stream PRODUCER calls this
66
+ * when it creates a build-log stream so that ticket minting can assert the
67
+ * caller's org owns the subject (see {@link createTicket}). Backed by the
68
+ * injected ticket store, so when a Redis store is wired the platform producer
69
+ * (a different service sharing the same Redis) can bind ownership that this
70
+ * service reads — closing the cross-tenant attach gap where any org could mint
71
+ * a ticket for a guessed requestId.
72
+ *
73
+ * @param requestId - The stream subject (format-validated by the caller).
74
+ * @param orgId - The owning org (normalized internally via normalizeOrgId).
75
+ */
76
+ async bindStreamOwner(requestId, orgId) {
77
+ await this.store.bindOwner(normalizeRequestId(requestId), normalizeOrgId(orgId), this.ownerTtlMs);
78
+ }
79
+ /**
80
+ * Mint a short-lived, single-use SSE ticket bound to `orgId` AND a specific
81
+ * stream subject (`requestId`). Clients POST to obtain one (JWT-authenticated)
82
+ * for the exact stream they intend to open, then open the EventSource with
83
+ * `?ticket=<t>` so the JWT never lands in a query string / access log. The
84
+ * ticket can then only be consumed to open that one subject's stream — see
85
+ * {@link consumeTicket} — which is what enforces per-subject authorization.
86
+ *
87
+ * ORG-OWNERSHIP: if a stream owner has been bound for this subject (via
88
+ * {@link bindStreamOwner}) and it is a DIFFERENT org, minting is refused
89
+ * (`reason: 'forbidden'`) — an org cannot mint a ticket for another org's
90
+ * stream even if it guesses the requestId. When no owner is bound (producer
91
+ * wiring not yet present), minting falls back to binding the ticket to the
92
+ * caller's own org, preserving current behavior.
93
+ *
94
+ * Bounded by the ticket store's live-ticket caps (total and per org). An
95
+ * ownership lookup the store can't answer is refused as `capacity`.
96
+ *
97
+ * @param orgId - Owning org (normalized internally via normalizeOrgId).
98
+ * @param requestId - The build-log stream subject this ticket authorizes.
99
+ * The caller must have format-validated it (see `SSE_REQUEST_ID_RE`).
100
+ * @returns `{ ok: true, ticket }` on success, or `{ ok: false, reason }`
101
+ * where reason is `'org-limit'`, `'capacity'`, or `'forbidden'`.
102
+ */
103
+ async createTicket(orgId, requestId) {
104
+ // Ownership gate first — a cross-tenant mint attempt should never even
105
+ // consume cap budget. Only enforced when an owner is actually bound. Both
106
+ // sides are org-normalized so a producer/consumer casing drift can't
107
+ // false-`forbidden` the real owner (mirrors requestId normalization).
108
+ const normalized = normalizeRequestId(requestId);
109
+ const normalizedOrg = normalizeOrgId(orgId);
110
+ let owner;
111
+ try {
112
+ owner = await this.store.getOwner(normalized);
113
+ }
114
+ catch (err) {
115
+ // Can't tell who owns the subject — refuse rather than mint unchecked.
116
+ logger.warn('SSE ticket refused: stream ownership lookup failed', { error: errorMessage(err) });
117
+ incCounter('sse_ticket_rejected_total', { reason: 'capacity' });
118
+ return { ok: false, reason: 'capacity' };
119
+ }
120
+ if (owner && owner !== normalizedOrg) {
121
+ logger.warn(`SSE ticket refused: org ${normalizedOrg} does not own stream subject`);
122
+ incCounter('sse_ticket_rejected_total', { reason: 'forbidden' });
123
+ return { ok: false, reason: 'forbidden' };
124
+ }
125
+ const issued = await this.store.issue(normalizedOrg, normalized);
126
+ if (!issued.ok) {
127
+ const reason = issued.reason === 'org' ? 'org-limit' : 'capacity';
128
+ logger.warn(`SSE ticket refused (${reason}) for ${normalizedOrg}`);
129
+ incCounter('sse_ticket_rejected_total', { reason });
130
+ return { ok: false, reason };
131
+ }
132
+ return { ok: true, ticket: issued.ticket };
133
+ }
134
+ /**
135
+ * Validate and CONSUME a ticket for a specific stream subject. Single-use:
136
+ * the ticket is deleted whether or not it turns out to be valid, so a replay
137
+ * of the same value always fails. Returns the bound org on success, or null
138
+ * when the ticket is unknown / already-used / expired / OR was minted for a
139
+ * DIFFERENT `requestId` than the one being opened.
140
+ *
141
+ * The subject check is the authorization fix: without it, a ticket minted for
142
+ * one stream could be presented to open ANY stream the holder's org could
143
+ * name, letting an authenticated org attach to another org's log stream by
144
+ * guessing its requestId. The generic 401 the caller returns does not
145
+ * distinguish "unknown ticket" from "wrong subject", so it leaks nothing
146
+ * about which requestIds exist.
147
+ *
148
+ * @param ticketId - The opaque ticket value from `?ticket=`.
149
+ * @param requestId - The stream subject from the URL path (`:requestId`),
150
+ * already format-validated by the caller.
151
+ */
152
+ async consumeTicket(ticketId, requestId) {
153
+ const ticket = await this.store.consume(ticketId); // single-use
154
+ if (!ticket)
155
+ return null;
156
+ // Subject binding — reject a ticket presented for a subject it was not
157
+ // minted for (including an unbound, org-channel ticket). Both sides are
158
+ // normalized so dashed/undashed forms match.
159
+ if (ticket.subject !== normalizeRequestId(requestId))
160
+ return null;
161
+ return { orgId: ticket.orgId };
162
+ }
163
+ /** Stop the store, but only the one this broker built — an injected store is
164
+ * the caller's to close. */
165
+ shutdown() {
166
+ if (this.ownsStore)
167
+ this.storeInstance?.stop();
168
+ }
169
+ }
170
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3NlLXRpY2tldC1icm9rZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaHR0cC9zc2UtdGlja2V0LWJyb2tlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDOzs7Ozs7Ozs7R0FTRztBQUVILE9BQU8sRUFBRSxZQUFZLEVBQUUsMEJBQTBCLEVBQUUsZ0JBQWdCLEVBQUUsaUJBQWlCLEVBQXVCLFlBQVksRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQzlKLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUUvQyxNQUFNLE1BQU0sR0FBRyxZQUFZLENBQUMsYUFBYSxDQUFDLENBQUM7QUEwQjNDOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLGtCQUFrQixDQUFDLFNBQWlCO0lBQ2xELE9BQU8sU0FBUyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUM7QUFDbkQsQ0FBQztBQUVEOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLFVBQVUsY0FBYyxDQUFDLEtBQWE7SUFDMUMsT0FBTyxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFLENBQUM7QUFDcEMsQ0FBQztBQUVELE1BQU0sT0FBTyxlQUFlO0lBQ2xCLGFBQWEsQ0FBa0I7SUFDdkMsb0VBQW9FO0lBQzVELFNBQVMsR0FBRyxLQUFLLENBQUM7SUFDVCxRQUFRLENBQVM7SUFDakIsU0FBUyxDQUFTO0lBQ2xCLEtBQUssQ0FBUztJQUNkLFVBQVUsQ0FBUztJQUVwQyxZQUFZLE9BQU8sR0FBMkIsRUFBRTtRQUM5QyxNQUFNLElBQUksR0FBRyxnQkFBZ0IsRUFBRSxDQUFDO1FBQ2hDLElBQUksQ0FBQyxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDO1FBQ2xELElBQUksQ0FBQyxTQUFTLEdBQUcsT0FBTyxDQUFDLFNB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QyxDQUFDO1lBQ25FLFVBQVUsQ0FBQywyQkFBMkIsRUFBRSxFQUFFLE1BQU0sRUFBRSxDQUFDLENBQUM7WUFDcEQsT0FBTyxFQUFFLEVBQUUsRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLENBQUM7UUFDL0IsQ0FBQztRQUNELE9BQU8sRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUMsTUFBTSxFQUFFLENBQUM7SUFDN0MsQ0FBQztJQUVEOzs7Ozs7Ozs7Ozs7Ozs7OztPQWlCRztJQUNILEtBQUssQ0FBQyxhQUFhLENBQUMsUUFBZ0IsRUFBRSxTQUFpQjtRQUNyRCxNQUFNLE1BQU0sR0FBRyxNQUFNLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsYUFBYTtRQUNoRSxJQUFJLENBQUMsTUFBTTtZQUFFLE9BQU8sSUFBSSxDQUFDO1FBQ3pCLHVFQUF1RTtRQUN2RSx3RUFBd0U7UUFDeEUsNkNBQTZDO1FBQzdDLElBQUksTUFBTSxDQUFDLE9BQU8sS0FBSyxrQkFBa0IsQ0FBQyxTQUFTLENBQUM7WUFBRSxPQUFPLElBQUksQ0FBQztRQUNsRSxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQztJQUNqQyxDQUFDO0lBRUQ7aUNBQzZCO0lBQzdCLFFBQVE7UUFDTixJQUFJLElBQUksQ0FBQyxTQUFTO1lBQUUsSUFBSSxDQUFDLGFBQWEsRUFBRSxJQUFJLEVBQUUsQ0FBQztJQUNqRCxDQUFDO0NBQ0YiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuLyoqXG4gKiBNaW50aW5nIGFuZCByZWRlZW1pbmcgdGhlIHNob3J0LWxpdmVkIFNTRSB0aWNrZXRzIHRoYXQgYXV0aG9yaXplIGEgbG9nXG4gKiBzdHJlYW0sIHBsdXMgdGhlIHN0cmVhbS1vd25lcnNoaXAgYmluZGluZyB0aG9zZSBtaW50cyBhcmUgY2hlY2tlZCBhZ2FpbnN0LlxuICpcbiAqIFNlcGFyYXRlIGZyb20ge0BsaW5rIFNTRU1hbmFnZXJ9IGJlY2F1c2UgaXQgc2hhcmVzIG5vdGhpbmcgd2l0aCB0aGVcbiAqIGNvbm5lY3Rpb24gcmVnaXN0cnk6IGl0cyBzdGF0ZSBsaXZlcyBlbnRpcmVseSBpbiB0aGUgaW5qZWN0ZWRcbiAqIHtAbGluayBTc2VUaWNrZXRTdG9yZX0gKFJlZGlzIGluIGEgbXVsdGktcG9kIGRlcGxveSksIGFuZCBldmVyeSBydWxlIGluIGhlcmVcbiAqIGlzIGFuIEFVVEhPUklaQVRJT04gcnVsZSDigJQgd2hvIG1heSBvcGVuIHdoaWNoIHN1YmplY3QncyBzdHJlYW0g4oCUIHdoaWNoIGlzXG4gKiBlYXNpZXIgdG8gYXVkaXQgYXMgb25lIHVuaXQgdGhhbiBpbnRlcmxlYXZlZCB3aXRoIGNvbm5lY3Rpb24gYm9va2tlZXBpbmcuXG4gKi9cblxuaW1wb3J0IHsgY3JlYXRlTG9nZ2VyLCBjcmVhdGVNZW1vcnlTc2VUaWNrZXRTdG9yZSwgZW52U3NlVGlja2V0Q2FwcywgU1NFX1RJQ0tFVF9UVExfTVMsIHR5cGUgU3NlVGlja2V0U3RvcmUsIGVycm9yTWVzc2FnZSB9IGZyb20gJ0BwaXBlbGluZS1idWlsZGVyL2FwaS1jb3JlJztcbmltcG9ydCB7IGluY0NvdW50ZXIgfSBmcm9tICcuLi9hcGkvbWV0cmljcy5qcyc7XG5cbmNvbnN0IGxvZ2dlciA9IGNyZWF0ZUxvZ2dlcignc3NlLXRpY2tldHMnKTtcblxuLyoqIFJlc3VsdCBvZiB7QGxpbmsgU3NlVGlja2V0QnJva2VyLmNyZWF0ZVRpY2tldH0uICovXG5leHBvcnQgdHlwZSBDcmVhdGVUaWNrZXRSZXN1bHQgPVxuICB8IHsgb2s6IHRydWU7IHRpY2tldDogc3RyaW5nIH1cbiAgfCB7IG9rOiBmYWxzZTsgcmVhc29uOiAnb3JnLWxpbWl0JyB8ICdjYXBhY2l0eScgfCAnZm9yYmlkZGVuJyB9O1xuXG5leHBvcnQgaW50ZXJmYWNlIFNzZVRpY2tldEJyb2tlck9wdGlvbnMge1xuICAvKiogVGlja2V0ICsgc3RyZWFtLW93bmVyc2hpcCBiYWNrZW5kLiBEZWZhdWx0cyB0byBhbiBpbi1tZW1vcnkgc3RvcmUgKHNpbmdsZVxuICAgKiAgcmVwbGljYSBvbmx5KSwgYnVpbHQgb24gZmlyc3QgdXNlLiBJbmplY3QgYW4gYXBpLWNvcmVcbiAgICogIGBjcmVhdGVFbnZTc2VUaWNrZXRTdG9yZSguLi4pYCBzbyBtaW50L3JlZGVlbSBhbmQgc3RyZWFtIG93bmVyc2hpcCB3b3JrXG4gICAqICBhY3Jvc3MgcG9kcy4gKi9cbiAgc3RvcmU/OiBTc2VUaWNrZXRTdG9yZTtcbiAgLyoqIENhcCBvbiBsaXZlIHRpY2tldHMgYWNyb3NzIHRoZSBwcm9jZXNzLCBmb3IgdGhlIERFRkFVTFQgaW4tbWVtb3J5IHN0b3JlLlxuICAgKiAgSWdub3JlZCB3aGVuIGBzdG9yZWAgaXMgaW5qZWN0ZWQgKHRoZSBzdG9yZSBjYXJyaWVzIGl0cyBvd24gY2FwcykuICovXG4gIG1heFRvdGFsPzogbnVtYmVyO1xuICAvKiogUGVyLW9yZyBjYXAgb24gbGl2ZSB0aWNrZXRzLCBmb3IgdGhlIERFRkFVTFQgaW4tbWVtb3J5IHN0b3JlLiBJZ25vcmVkIHdoZW5cbiAgICogIGBzdG9yZWAgaXMgaW5qZWN0ZWQuICovXG4gIG1heFBlck9yZz86IG51bWJlcjtcbiAgLyoqIFRpY2tldCBUVEwgaW4gbXMgZm9yIHRoZSBkZWZhdWx0IHN0b3JlLiAqL1xuICB0dGxNcz86IG51bWJlcjtcbiAgLyoqIFRUTCBmb3IgYSBzdHJlYW0tb3duZXJzaGlwIGJpbmRpbmcgKGRlZmF1bHQgMWgpLiBMb25nIGVub3VnaCB0byBvdXRsaXZlIGFcbiAgICogIGJ1aWxkOyB0aGUgcHJvZHVjZXIgcmUtYmluZHMgYXMgbmVlZGVkLiAqL1xuICBvd25lclR0bE1zPzogbnVtYmVyO1xufVxuXG4vKipcbiAqIENhbm9uaWNhbCBmb3JtIG9mIGEgc3RyZWFtIHN1YmplY3QgaWQgZm9yIG1hcC9zdG9yZSBrZXlzOiB0aGUgaWQgaXMgYWxsb3dlZFxuICogaW4gdHdvIHJlbmRlcmluZ3MgKGEgZGFzaGVkIGFwaS1zZXJ2ZXIgdXVpZCBhbmQgbmdpbngncyB1bmRhc2hlZFxuICogYCRyZXF1ZXN0X2lkYCksIGFuZCBub3JtYWxpemluZyBtYWtlcyB0aGVtIGNvbXBhcmUgZXF1YWwuIENhbGxlcnMgYXJlXG4gKiBleHBlY3RlZCB0byBoYXZlIGFscmVhZHkgZm9ybWF0LXZhbGlkYXRlZCBhZ2FpbnN0IGBTU0VfUkVRVUVTVF9JRF9SRWAuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBub3JtYWxpemVSZXF1ZXN0SWQocmVxdWVzdElkOiBzdHJpbmcpOiBzdHJpbmcge1xuICByZXR1cm4gcmVxdWVzdElkLnJlcGxhY2UoLy0vZywgJycpLnRvTG93ZXJDYXNlKCk7XG59XG5cbi8qKlxuICogQ2Fub25pY2FsIGZvcm0gb2YgYW4gb3JnSWQgZm9yIG93bmVyc2hpcCBlcXVhbGl0eSBjaGVja3M6IHRyaW1tZWQgYW5kXG4gKiBsb3dlcmNhc2VkLiBUaGUgc3RyZWFtIFBST0RVQ0VSIChiaW5kU3RyZWFtT3duZXIpIGFuZCB0aGUgdGlja2V0LW1pbnRpbmdcbiAqIENPTlNVTUVSIChjcmVhdGVUaWNrZXQpIHJ1biBpbiBkaWZmZXJlbnQgc2VydmljZXMsIHNvIGEgY2FzaW5nIC8gd2hpdGVzcGFjZVxuICogZHJpZnQgYmV0d2VlbiBob3cgZWFjaCByZW5kZXJzIHRoZSBzYW1lIG9yZyB3b3VsZCBvdGhlcndpc2UgbWFrZSB0aGUgb3duZXJcbiAqIGNvbXBhcmlzb24gaW4ge0BsaW5rIFNzZVRpY2tldEJyb2tlci5jcmVhdGVUaWNrZXR9IGZhbHNlLWBmb3JiaWRkZW5gIHRoZSByZWFsXG4gKiBvd25lci4gTm9ybWFsaXppbmcgYm90aCBzaWRlcyDigJQgbWlycm9yaW5nIHtAbGluayBub3JtYWxpemVSZXF1ZXN0SWR9IOKAlCBjbG9zZXMgdGhhdC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG5vcm1hbGl6ZU9yZ0lkKG9yZ0lkOiBzdHJpbmcpOiBzdHJpbmcge1xuICByZXR1cm4gb3JnSWQudHJpbSgpLnRvTG93ZXJDYXNlKCk7XG59XG5cbmV4cG9ydCBjbGFzcyBTc2VUaWNrZXRCcm9rZXIge1xuICBwcml2YXRlIHN0b3JlSW5zdGFuY2U/OiBTc2VUaWNrZXRTdG9yZTtcbiAgLyoqIFRydWUgd2hlbiB0aGlzIGJyb2tlciBidWlsdCBpdHMgc3RvcmUgKHNvIHNodXRkb3duIHN0b3BzIGl0KS4gKi9cbiAgcHJpdmF0ZSBvd25zU3RvcmUgPSBmYWxzZTtcbiAgcHJpdmF0ZSByZWFkb25seSBtYXhUb3RhbDogbnVtYmVyO1xuICBwcml2YXRlIHJlYWRvbmx5IG1heFBlck9yZzogbnVtYmVyO1xuICBwcml2YXRlIHJlYWRvbmx5IHR0bE1zOiBudW1iZXI7XG4gIHByaXZhdGUgcmVhZG9ubHkgb3duZXJUdGxNczogbnVtYmVyO1xuXG4gIGNvbnN0cnVjdG9yKG9wdGlvbnM6IFNzZVRpY2tldEJyb2tlck9wdGlvbnMgPSB7fSkge1xuICAgIGNvbnN0IGNhcHMgPSBlbnZTc2VUaWNrZXRDYXBzKCk7XG4gICAgdGhpcy5tYXhUb3RhbCA9IG9wdGlvbnMubWF4VG90YWwgPz8gY2Fwcy5tYXhUb3RhbDtcbiAgICB0aGlzLm1heFBlck9yZyA9IG9wdGlvbnMubWF4UGVyT3JnID8/IGNhcHMubWF4UGVyT3JnO1xuICAgIHRoaXMudHRsTXMgPSBvcHRpb25zLnR0bE1zID8/IFNTRV9USUNLRVRfVFRMX01TO1xuICAgIHRoaXMub3duZXJUdGxNcyA9IG9wdGlvbnMub3duZXJUdGxNcyA/PyAzXzYwMF8wMDA7IC8vIDEgaG91clxuICAgIHRoaXMuc3RvcmVJbnN0YW5jZSA9IG9wdGlvbnMuc3RvcmU7XG4gIH1cblxuICAvKiogVGhlIHRpY2tldCBzdG9yZSwgYnVpbGRpbmcgdGhlIGluLW1lbW9yeSBkZWZhdWx0IG9uIGZpcnN0IHVzZS4gKi9cbiAgcHJpdmF0ZSBnZXQgc3RvcmUoKTogU3NlVGlja2V0U3RvcmUge1xuICAgIGlmICghdGhpcy5zdG9yZUluc3RhbmNlKSB7XG4gICAgICB0aGlzLnN0b3JlSW5zdGFuY2UgPSBjcmVhdGVNZW1vcnlTc2VUaWNrZXRTdG9yZSh7XG4gICAgICAgIHR0bE1zOiB0aGlzLnR0bE1zLFxuICAgICAgICBtYXhUb3RhbDogdGhpcy5tYXhUb3RhbCxcbiAgICAgICAgbWF4UGVyT3JnOiB0aGlzLm1heFBlck9yZyxcbiAgICAgIH0pO1xuICAgICAgdGhpcy5vd25zU3RvcmUgPSB0cnVlO1xuICAgIH1cbiAgICByZXR1cm4gdGhpcy5zdG9yZUluc3RhbmNlO1xuICB9XG5cbiAgLyoqXG4gICAqIFJlY29yZCB0aGUgb3JnIHRoYXQgT1dOUyBhIHN0cmVhbSBzdWJqZWN0LiBUaGUgc3RyZWFtIFBST0RVQ0VSIGNhbGxzIHRoaXNcbiAgICogd2hlbiBpdCBjcmVhdGVzIGEgYnVpbGQtbG9nIHN0cmVhbSBzbyB0aGF0IHRpY2tldCBtaW50aW5nIGNhbiBhc3NlcnQgdGhlXG4gICAqIGNhbGxlcidzIG9yZyBvd25zIHRoZSBzdWJqZWN0IChzZWUge0BsaW5rIGNyZWF0ZVRpY2tldH0pLiBCYWNrZWQgYnkgdGhlXG4gICAqIGluamVjdGVkIHRpY2tldCBzdG9yZSwgc28gd2hlbiBhIFJlZGlzIHN0b3JlIGlzIHdpcmVkIHRoZSBwbGF0Zm9ybSBwcm9kdWNlclxuICAgKiAoYSBkaWZmZXJlbnQgc2VydmljZSBzaGFyaW5nIHRoZSBzYW1lIFJlZGlzKSBjYW4gYmluZCBvd25lcnNoaXAgdGhhdCB0aGlzXG4gICAqIHNlcnZpY2UgcmVhZHMg4oCUIGNsb3NpbmcgdGhlIGNyb3NzLXRlbmFudCBhdHRhY2ggZ2FwIHdoZXJlIGFueSBvcmcgY291bGQgbWludFxuICAgKiBhIHRpY2tldCBmb3IgYSBndWVzc2VkIHJlcXVlc3RJZC5cbiAgICpcbiAgICogQHBhcmFtIHJlcXVlc3RJZCAtIFRoZSBzdHJlYW0gc3ViamVjdCAoZm9ybWF0LXZhbGlkYXRlZCBieSB0aGUgY2FsbGVyKS5cbiAgICogQHBhcmFtIG9yZ0lkIC0gVGhlIG93bmluZyBvcmcgKG5vcm1hbGl6ZWQgaW50ZXJuYWxseSB2aWEgbm9ybWFsaXplT3JnSWQpLlxuICAgKi9cbiAgYXN5bmMgYmluZFN0cmVhbU93bmVyKHJlcXVlc3RJZDogc3RyaW5nLCBvcmdJZDogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgYXdhaXQgdGhpcy5zdG9yZS5iaW5kT3duZXIobm9ybWFsaXplUmVxdWVzdElkKHJlcXVlc3RJZCksIG5vcm1hbGl6ZU9yZ0lkKG9yZ0lkKSwgdGhpcy5vd25lclR0bE1zKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBNaW50IGEgc2hvcnQtbGl2ZWQsIHNpbmdsZS11c2UgU1NFIHRpY2tldCBib3VuZCB0byBgb3JnSWRgIEFORCBhIHNwZWNpZmljXG4gICAqIHN0cmVhbSBzdWJqZWN0IChgcmVxdWVzdElkYCkuIENsaWVudHMgUE9TVCB0byBvYnRhaW4gb25lIChKV1QtYXV0aGVudGljYXRlZClcbiAgICogZm9yIHRoZSBleGFjdCBzdHJlYW0gdGhleSBpbnRlbmQgdG8gb3BlbiwgdGhlbiBvcGVuIHRoZSBFdmVudFNvdXJjZSB3aXRoXG4gICAqIGA/dGlja2V0PTx0PmAgc28gdGhlIEpXVCBuZXZlciBsYW5kcyBpbiBhIHF1ZXJ5IHN0cmluZyAvIGFjY2VzcyBsb2cuIFRoZVxuICAgKiB0aWNrZXQgY2FuIHRoZW4gb25seSBiZSBjb25zdW1lZCB0byBvcGVuIHRoYXQgb25lIHN1YmplY3QncyBzdHJlYW0g4oCUIHNlZVxuICAgKiB7QGxpbmsgY29uc3VtZVRpY2tldH0g4oCUIHdoaWNoIGlzIHdoYXQgZW5mb3JjZXMgcGVyLXN1YmplY3QgYXV0aG9yaXphdGlvbi5cbiAgICpcbiAgICogT1JHLU9XTkVSU0hJUDogaWYgYSBzdHJlYW0gb3duZXIgaGFzIGJlZW4gYm91bmQgZm9yIHRoaXMgc3ViamVjdCAodmlhXG4gICAqIHtAbGluayBiaW5kU3RyZWFtT3duZXJ9KSBhbmQgaXQgaXMgYSBESUZGRVJFTlQgb3JnLCBtaW50aW5nIGlzIHJlZnVzZWRcbiAgICogKGByZWFzb246ICdmb3JiaWRkZW4nYCkg4oCUIGFuIG9yZyBjYW5ub3QgbWludCBhIHRpY2tldCBmb3IgYW5vdGhlciBvcmcnc1xuICAgKiBzdHJlYW0gZXZlbiBpZiBpdCBndWVzc2VzIHRoZSByZXF1ZXN0SWQuIFdoZW4gbm8gb3duZXIgaXMgYm91bmQgKHByb2R1Y2VyXG4gICAqIHdpcmluZyBub3QgeWV0IHByZXNlbnQpLCBtaW50aW5nIGZhbGxzIGJhY2sgdG8gYmluZGluZyB0aGUgdGlja2V0IHRvIHRoZVxuICAgKiBjYWxsZXIncyBvd24gb3JnLCBwcmVzZXJ2aW5nIGN1cnJlbnQgYmVoYXZpb3IuXG4gICAqXG4gICAqIEJvdW5kZWQgYnkgdGhlIHRpY2tldCBzdG9yZSdzIGxpdmUtdGlja2V0IGNhcHMgKHRvdGFsIGFuZCBwZXIgb3JnKS4gQW5cbiAgICogb3duZXJzaGlwIGxvb2t1cCB0aGUgc3RvcmUgY2FuJ3QgYW5zd2VyIGlzIHJlZnVzZWQgYXMgYGNhcGFjaXR5YC5cbiAgICpcbiAgICogQHBhcmFtIG9yZ0lkIC0gT3duaW5nIG9yZyAobm9ybWFsaXplZCBpbnRlcm5hbGx5IHZpYSBub3JtYWxpemVPcmdJZCkuXG4gICAqIEBwYXJhbSByZXF1ZXN0SWQgLSBUaGUgYnVpbGQtbG9nIHN0cmVhbSBzdWJqZWN0IHRoaXMgdGlja2V0IGF1dGhvcml6ZXMuXG4gICAqICAgVGhlIGNhbGxlciBtdXN0IGhhdmUgZm9ybWF0LXZhbGlkYXRlZCBpdCAoc2VlIGBTU0VfUkVRVUVTVF9JRF9SRWApLlxuICAgKiBAcmV0dXJucyBgeyBvazogdHJ1ZSwgdGlja2V0IH1gIG9uIHN1Y2Nlc3MsIG9yIGB7IG9rOiBmYWxzZSwgcmVhc29uIH1gXG4gICAqICAgd2hlcmUgcmVhc29uIGlzIGAnb3JnLWxpbWl0J2AsIGAnY2FwYWNpdHknYCwgb3IgYCdmb3JiaWRkZW4nYC5cbiAgICovXG4gIGFzeW5jIGNyZWF0ZVRpY2tldChvcmdJZDogc3RyaW5nLCByZXF1ZXN0SWQ6IHN0cmluZyk6IFByb21pc2U8Q3JlYXRlVGlja2V0UmVzdWx0PiB7XG4gICAgLy8gT3duZXJzaGlwIGdhdGUgZmlyc3Qg4oCUIGEgY3Jvc3MtdGVuYW50IG1pbnQgYXR0ZW1wdCBzaG91bGQgbmV2ZXIgZXZlblxuICAgIC8vIGNvbnN1bWUgY2FwIGJ1ZGdldC4gT25seSBlbmZvcmNlZCB3aGVuIGFuIG93bmVyIGlzIGFjdHVhbGx5IGJvdW5kLiBCb3RoXG4gICAgLy8gc2lkZXMgYXJlIG9yZy1ub3JtYWxpemVkIHNvIGEgcHJvZHVjZXIvY29uc3VtZXIgY2FzaW5nIGRyaWZ0IGNhbid0XG4gICAgLy8gZmFsc2UtYGZvcmJpZGRlbmAgdGhlIHJlYWwgb3duZXIgKG1pcnJvcnMgcmVxdWVzdElkIG5vcm1hbGl6YXRpb24pLlxuICAgIGNvbnN0IG5vcm1hbGl6ZWQgPSBub3JtYWxpemVSZXF1ZXN0SWQocmVxdWVzdElkKTtcbiAgICBjb25zdCBub3JtYWxpemVkT3JnID0gbm9ybWFsaXplT3JnSWQob3JnSWQpO1xuICAgIGxldCBvd25lcjogc3RyaW5nIHwgbnVsbDtcbiAgICB0cnkge1xuICAgICAgb3duZXIgPSBhd2FpdCB0aGlzLnN0b3JlLmdldE93bmVyKG5vcm1hbGl6ZWQpO1xuICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgLy8gQ2FuJ3QgdGVsbCB3aG8gb3ducyB0aGUgc3ViamVjdCDigJQgcmVmdXNlIHJhdGhlciB0aGFuIG1pbnQgdW5jaGVja2VkLlxuICAgICAgbG9nZ2VyLndhcm4oJ1NTRSB0aWNrZXQgcmVmdXNlZDogc3RyZWFtIG93bmVyc2hpcCBsb29rdXAgZmFpbGVkJywgeyBlcnJvcjogZXJyb3JNZXNzYWdlKGVycikgfSk7XG4gICAgICBpbmNDb3VudGVyKCdzc2VfdGlja2V0X3JlamVjdGVkX3RvdGFsJywgeyByZWFzb246ICdjYXBhY2l0eScgfSk7XG4gICAgICByZXR1cm4geyBvazogZmFsc2UsIHJlYXNvbjogJ2NhcGFjaXR5JyB9O1xuICAgIH1cbiAgICBpZiAob3duZXIgJiYgb3duZXIgIT09IG5vcm1hbGl6ZWRPcmcpIHtcbiAgICAgIGxvZ2dlci53YXJuKGBTU0UgdGlja2V0IHJlZnVzZWQ6IG9yZyAke25vcm1hbGl6ZWRPcmd9IGRvZXMgbm90IG93biBzdHJlYW0gc3ViamVjdGApO1xuICAgICAgaW5jQ291bnRlcignc3NlX3RpY2tldF9yZWplY3RlZF90b3RhbCcsIHsgcmVhc29uOiAnZm9yYmlkZGVuJyB9KTtcbiAgICAgIHJldHVybiB7IG9rOiBmYWxzZSwgcmVhc29uOiAnZm9yYmlkZGVuJyB9O1xuICAgIH1cblxuICAgIGNvbnN0IGlzc3VlZCA9IGF3YWl0IHRoaXMuc3RvcmUuaXNzdWUobm9ybWFsaXplZE9yZywgbm9ybWFsaXplZCk7XG4gICAgaWYgKCFpc3N1ZWQub2spIHtcbiAgICAgIGNvbnN0IHJlYXNvbiA9IGlzc3VlZC5yZWFzb24gPT09ICdvcmcnID8gJ29yZy1saW1pdCcgOiAnY2FwYWNpdHknO1xuICAgICAgbG9nZ2VyLndhcm4oYFNTRSB0aWNrZXQgcmVmdXNlZCAoJHtyZWFzb259KSBmb3IgJHtub3JtYWxpemVkT3JnfWApO1xuICAgICAgaW5jQ291bnRlcignc3NlX3RpY2tldF9yZWplY3RlZF90b3RhbCcsIHsgcmVhc29uIH0pO1xuICAgICAgcmV0dXJuIHsgb2s6IGZhbHNlLCByZWFzb24gfTtcbiAgICB9XG4gICAgcmV0dXJuIHsgb2s6IHRydWUsIHRpY2tldDogaXNzdWVkLnRpY2tldCB9O1xuICB9XG5cbiAgLyoqXG4gICAqIFZhbGlkYXRlIGFuZCBDT05TVU1FIGEgdGlja2V0IGZvciBhIHNwZWNpZmljIHN0cmVhbSBzdWJqZWN0LiBTaW5nbGUtdXNlOlxuICAgKiB0aGUgdGlja2V0IGlzIGRlbGV0ZWQgd2hldGhlciBvciBub3QgaXQgdHVybnMgb3V0IHRvIGJlIHZhbGlkLCBzbyBhIHJlcGxheVxuICAgKiBvZiB0aGUgc2FtZSB2YWx1ZSBhbHdheXMgZmFpbHMuIFJldHVybnMgdGhlIGJvdW5kIG9yZyBvbiBzdWNjZXNzLCBvciBudWxsXG4gICAqIHdoZW4gdGhlIHRpY2tldCBpcyB1bmtub3duIC8gYWxyZWFkeS11c2VkIC8gZXhwaXJlZCAvIE9SIHdhcyBtaW50ZWQgZm9yIGFcbiAgICogRElGRkVSRU5UIGByZXF1ZXN0SWRgIHRoYW4gdGhlIG9uZSBiZWluZyBvcGVuZWQuXG4gICAqXG4gICAqIFRoZSBzdWJqZWN0IGNoZWNrIGlzIHRoZSBhdXRob3JpemF0aW9uIGZpeDogd2l0aG91dCBpdCwgYSB0aWNrZXQgbWludGVkIGZvclxuICAgKiBvbmUgc3RyZWFtIGNvdWxkIGJlIHByZXNlbnRlZCB0byBvcGVuIEFOWSBzdHJlYW0gdGhlIGhvbGRlcidzIG9yZyBjb3VsZFxuICAgKiBuYW1lLCBsZXR0aW5nIGFuIGF1dGhlbnRpY2F0ZWQgb3JnIGF0dGFjaCB0byBhbm90aGVyIG9yZydzIGxvZyBzdHJlYW0gYnlcbiAgICogZ3Vlc3NpbmcgaXRzIHJlcXVlc3RJZC4gVGhlIGdlbmVyaWMgNDAxIHRoZSBjYWxsZXIgcmV0dXJucyBkb2VzIG5vdFxuICAgKiBkaXN0aW5ndWlzaCBcInVua25vd24gdGlja2V0XCIgZnJvbSBcIndyb25nIHN1YmplY3RcIiwgc28gaXQgbGVha3Mgbm90aGluZ1xuICAgKiBhYm91dCB3aGljaCByZXF1ZXN0SWRzIGV4aXN0LlxuICAgKlxuICAgKiBAcGFyYW0gdGlja2V0SWQgLSBUaGUgb3BhcXVlIHRpY2tldCB2YWx1ZSBmcm9tIGA/dGlja2V0PWAuXG4gICAqIEBwYXJhbSByZXF1ZXN0SWQgLSBUaGUgc3RyZWFtIHN1YmplY3QgZnJvbSB0aGUgVVJMIHBhdGggKGA6cmVxdWVzdElkYCksXG4gICAqICAgYWxyZWFkeSBmb3JtYXQtdmFsaWRhdGVkIGJ5IHRoZSBjYWxsZXIuXG4gICAqL1xuICBhc3luYyBjb25zdW1lVGlja2V0KHRpY2tldElkOiBzdHJpbmcsIHJlcXVlc3RJZDogc3RyaW5nKTogUHJvbWlzZTx7IG9yZ0lkOiBzdHJpbmcgfSB8IG51bGw+IHtcbiAgICBjb25zdCB0aWNrZXQgPSBhd2FpdCB0aGlzLnN0b3JlLmNvbnN1bWUodGlja2V0SWQpOyAvLyBzaW5nbGUtdXNlXG4gICAgaWYgKCF0aWNrZXQpIHJldHVybiBudWxsO1xuICAgIC8vIFN1YmplY3QgYmluZGluZyDigJQgcmVqZWN0IGEgdGlja2V0IHByZXNlbnRlZCBmb3IgYSBzdWJqZWN0IGl0IHdhcyBub3RcbiAgICAvLyBtaW50ZWQgZm9yIChpbmNsdWRpbmcgYW4gdW5ib3VuZCwgb3JnLWNoYW5uZWwgdGlja2V0KS4gQm90aCBzaWRlcyBhcmVcbiAgICAvLyBub3JtYWxpemVkIHNvIGRhc2hlZC91bmRhc2hlZCBmb3JtcyBtYXRjaC5cbiAgICBpZiAodGlja2V0LnN1YmplY3QgIT09IG5vcm1hbGl6ZVJlcXVlc3RJZChyZXF1ZXN0SWQpKSByZXR1cm4gbnVsbDtcbiAgICByZXR1cm4geyBvcmdJZDogdGlja2V0Lm9yZ0lkIH07XG4gIH1cblxuICAvKiogU3RvcCB0aGUgc3RvcmUsIGJ1dCBvbmx5IHRoZSBvbmUgdGhpcyBicm9rZXIgYnVpbHQg4oCUIGFuIGluamVjdGVkIHN0b3JlIGlzXG4gICAqICB0aGUgY2FsbGVyJ3MgdG8gY2xvc2UuICovXG4gIHNodXRkb3duKCk6IHZvaWQge1xuICAgIGlmICh0aGlzLm93bnNTdG9yZSkgdGhpcy5zdG9yZUluc3RhbmNlPy5zdG9wKCk7XG4gIH1cbn1cbiJdfQ==
@@ -8,6 +8,7 @@
8
8
  "connectMongo": "function",
9
9
  "createApp": "function",
10
10
  "createAuthenticatedWithOrgRoute": "function",
11
+ "createMetricHelpers": "function",
11
12
  "createProtectedRoute": "function",
12
13
  "createSharedRateLimitStore": "function",
13
14
  "currentTraceId": "function",
package/package.json CHANGED
@@ -50,9 +50,9 @@
50
50
  "swagger-ui-express": "5.0.1",
51
51
  "unified": "11.0.5",
52
52
  "uuid": "14.0.2",
53
- "@pipeline-builder/api-core": "4.1.19",
54
- "@pipeline-builder/pipeline-data": "4.1.21",
55
- "@pipeline-builder/pipeline-core": "4.1.21"
53
+ "@pipeline-builder/api-core": "4.1.21",
54
+ "@pipeline-builder/pipeline-data": "4.1.23",
55
+ "@pipeline-builder/pipeline-core": "4.1.23"
56
56
  },
57
57
  "keywords": [
58
58
  "ci-cd",
@@ -108,7 +108,7 @@
108
108
  "access": "public",
109
109
  "registry": "https://registry.npmjs.org/"
110
110
  },
111
- "version": "4.1.21",
111
+ "version": "4.1.23",
112
112
  "bugs": {
113
113
  "url": "https://github.com/mwashburn160/pipeline-builder/issues"
114
114
  },