@pipeline-builder/api-server 4.1.22 → 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.
- package/lib/api/index.d.ts +1 -1
- package/lib/api/index.js +2 -2
- package/lib/api/metrics.d.ts +23 -0
- package/lib/api/metrics.js +73 -54
- package/lib/api/server.js +9 -9
- package/lib/http/sse-connection-manager.d.ts +14 -84
- package/lib/http/sse-connection-manager.js +31 -144
- package/lib/http/sse-ticket-broker.d.ts +127 -0
- package/lib/http/sse-ticket-broker.js +170 -0
- package/lib/testing/exports.json +1 -0
- package/package.json +4 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type SseTicketStore } from '@pipeline-builder/api-core';
|
|
2
2
|
import type { Response } from 'express';
|
|
3
3
|
import type { SSERelay } from './sse-relay.js';
|
|
4
|
+
import { type CreateTicketResult } from './sse-ticket-broker.js';
|
|
4
5
|
/**
|
|
5
6
|
* Event types for SSE logging
|
|
6
7
|
*/
|
|
@@ -83,8 +84,8 @@ export interface SSEManagerOptions {
|
|
|
83
84
|
*/
|
|
84
85
|
logStream?: boolean;
|
|
85
86
|
/**
|
|
86
|
-
* TTL for a stream-ownership binding (default
|
|
87
|
-
*
|
|
87
|
+
* TTL for a stream-ownership binding (default 1h). Long enough to outlive a
|
|
88
|
+
* build; the producer re-binds as needed.
|
|
88
89
|
*/
|
|
89
90
|
streamOwnerTtlMs?: number;
|
|
90
91
|
/**
|
|
@@ -110,14 +111,7 @@ export interface SSEManagerOptions {
|
|
|
110
111
|
* both validate the subject identically.
|
|
111
112
|
*/
|
|
112
113
|
export declare const SSE_REQUEST_ID_RE: RegExp;
|
|
113
|
-
|
|
114
|
-
export type CreateTicketResult = {
|
|
115
|
-
ok: true;
|
|
116
|
-
ticket: string;
|
|
117
|
-
} | {
|
|
118
|
-
ok: false;
|
|
119
|
-
reason: 'org-limit' | 'capacity' | 'forbidden';
|
|
120
|
-
};
|
|
114
|
+
export type { CreateTicketResult };
|
|
121
115
|
/**
|
|
122
116
|
* SSE Manager statistics
|
|
123
117
|
*/
|
|
@@ -150,17 +144,11 @@ export declare class SSEManager {
|
|
|
150
144
|
* map mirrors `clients[].orgId` counts at all times. Orgs reach zero are
|
|
151
145
|
* deleted to keep the map bounded. */
|
|
152
146
|
private orgClientCounts;
|
|
153
|
-
/**
|
|
154
|
-
private
|
|
155
|
-
/** True when this manager built its ticket store (so shutdown stops it). */
|
|
156
|
-
private ownsTicketStore;
|
|
147
|
+
/** Ticket minting / redemption + stream ownership (see ./sse-ticket-broker.ts). */
|
|
148
|
+
private readonly tickets;
|
|
157
149
|
private readonly maxClientsPerRequest;
|
|
158
150
|
private readonly maxTotalClients;
|
|
159
151
|
private readonly maxClientsPerOrg;
|
|
160
|
-
private readonly maxTotalTickets;
|
|
161
|
-
private readonly maxTicketsPerOrg;
|
|
162
|
-
private readonly ticketTtlMs;
|
|
163
|
-
private readonly streamOwnerTtlMs;
|
|
164
152
|
private readonly clientTimeoutMs;
|
|
165
153
|
private cleanupInterval;
|
|
166
154
|
/** Cross-pod fan-out bus (undefined ⇒ local-only delivery). */
|
|
@@ -178,8 +166,6 @@ export declare class SSEManager {
|
|
|
178
166
|
* factory returns null (Redis not configured → local-only delivery).
|
|
179
167
|
*/
|
|
180
168
|
enableRelay(): void;
|
|
181
|
-
/** The ticket store, building the in-memory default on first use. */
|
|
182
|
-
private get ticketStore();
|
|
183
169
|
/**
|
|
184
170
|
* Handle a frame received from another pod via the relay. We already delivered
|
|
185
171
|
* our OWN frames to local clients before publishing, so ignore our echo. A
|
|
@@ -199,76 +185,20 @@ export declare class SSEManager {
|
|
|
199
185
|
/** Current open-stream count for an org (0 if unseen). */
|
|
200
186
|
getOrgClientCount(orgId: string): number;
|
|
201
187
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* `$request_id`) renderings of the same id compare equal. Callers are
|
|
205
|
-
* expected to have already format-validated against {@link SSE_REQUEST_ID_RE}.
|
|
206
|
-
*/
|
|
207
|
-
private static normalizeRequestId;
|
|
208
|
-
/**
|
|
209
|
-
* Canonical form of an orgId for ownership equality checks: trimmed and
|
|
210
|
-
* lowercased. The stream PRODUCER (bindStreamOwner) and the ticket-minting
|
|
211
|
-
* CONSUMER (createTicket) run in different services, so a casing / whitespace
|
|
212
|
-
* drift between how each renders the same org would otherwise make the owner
|
|
213
|
-
* comparison in {@link createTicket} false-`forbidden` the real owner.
|
|
214
|
-
* Normalizing both sides — mirroring {@link normalizeRequestId} — closes that.
|
|
215
|
-
*/
|
|
216
|
-
private static normalizeOrgId;
|
|
217
|
-
/**
|
|
218
|
-
* Record the org that OWNS a stream subject. The stream PRODUCER calls this
|
|
219
|
-
* when it creates a build-log stream so that ticket minting can assert the
|
|
220
|
-
* caller's org owns the subject (see {@link createTicket}). Backed by the
|
|
221
|
-
* injected ticket store, so when a Redis store is wired the platform producer
|
|
222
|
-
* (a different service sharing the same Redis) can bind ownership that this
|
|
223
|
-
* service reads — closing the cross-tenant attach gap where any org could mint
|
|
224
|
-
* a ticket for a guessed requestId.
|
|
225
|
-
*
|
|
226
|
-
* @param requestId - The stream subject (format-validated by the caller).
|
|
227
|
-
* @param orgId - The owning org (normalized internally via normalizeOrgId).
|
|
188
|
+
* Record the org that OWNS a stream subject, so ticket minting can refuse a
|
|
189
|
+
* cross-tenant mint. Delegates to the ticket broker.
|
|
228
190
|
*/
|
|
229
191
|
bindStreamOwner(requestId: string, orgId: string): Promise<void>;
|
|
230
192
|
/**
|
|
231
|
-
* Mint a short-lived, single-use
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* ticket can then only be consumed to open that one subject's stream — see
|
|
236
|
-
* {@link consumeTicket} — which is what enforces per-subject authorization.
|
|
237
|
-
*
|
|
238
|
-
* ORG-OWNERSHIP: if a stream owner has been bound for this subject (via
|
|
239
|
-
* {@link bindStreamOwner}) and it is a DIFFERENT org, minting is refused
|
|
240
|
-
* (`reason: 'forbidden'`) — an org cannot mint a ticket for another org's
|
|
241
|
-
* stream even if it guesses the requestId. When no owner is bound (producer
|
|
242
|
-
* wiring not yet present), minting falls back to binding the ticket to the
|
|
243
|
-
* caller's own org, preserving current behavior.
|
|
244
|
-
*
|
|
245
|
-
* Bounded by the ticket store's live-ticket caps (total and per org). An
|
|
246
|
-
* ownership lookup the store can't answer is refused as `capacity`.
|
|
247
|
-
*
|
|
248
|
-
* @param orgId - Owning org (normalized internally via normalizeOrgId).
|
|
249
|
-
* @param requestId - The build-log stream subject this ticket authorizes.
|
|
250
|
-
* The caller must have format-validated it (see {@link SSE_REQUEST_ID_RE}).
|
|
251
|
-
* @returns `{ ok: true, ticket }` on success, or `{ ok: false, reason }`
|
|
252
|
-
* where reason is `'org-limit'`, `'capacity'`, or `'forbidden'`.
|
|
193
|
+
* Mint a short-lived, single-use ticket bound to `orgId` AND this stream
|
|
194
|
+
* subject — how a browser opens an EventSource without putting its JWT in a
|
|
195
|
+
* query string. Delegates to the ticket broker, which owns the ownership and
|
|
196
|
+
* cap rules.
|
|
253
197
|
*/
|
|
254
198
|
createTicket(orgId: string, requestId: string): Promise<CreateTicketResult>;
|
|
255
199
|
/**
|
|
256
|
-
* Validate and CONSUME a ticket for a specific stream subject
|
|
257
|
-
*
|
|
258
|
-
* of the same value always fails. Returns the bound org on success, or null
|
|
259
|
-
* when the ticket is unknown / already-used / expired / OR was minted for a
|
|
260
|
-
* DIFFERENT `requestId` than the one being opened.
|
|
261
|
-
*
|
|
262
|
-
* The subject check is the authorization fix: without it, a ticket minted for
|
|
263
|
-
* one stream could be presented to open ANY stream the holder's org could
|
|
264
|
-
* name, letting an authenticated org attach to another org's log stream by
|
|
265
|
-
* guessing its requestId. The generic 401 the caller returns does not
|
|
266
|
-
* distinguish "unknown ticket" from "wrong subject", so it leaks nothing
|
|
267
|
-
* about which requestIds exist.
|
|
268
|
-
*
|
|
269
|
-
* @param ticketId - The opaque ticket value from `?ticket=`.
|
|
270
|
-
* @param requestId - The stream subject from the URL path (`:requestId`),
|
|
271
|
-
* already format-validated by the caller.
|
|
200
|
+
* Validate and CONSUME a ticket for a specific stream subject (single-use,
|
|
201
|
+
* subject-bound). Delegates to the ticket broker.
|
|
272
202
|
*/
|
|
273
203
|
consumeTicket(ticketId: string, requestId: string): Promise<{
|
|
274
204
|
orgId: string;
|