@cat-factory/integrations 0.166.21 → 0.167.0
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/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/providers/OpenRouterCatalogService.d.ts.map +1 -1
- package/dist/modules/providers/OpenRouterCatalogService.js +2 -30
- package/dist/modules/providers/OpenRouterCatalogService.js.map +1 -1
- package/dist/modules/providers/openRouterModels.d.ts +12 -0
- package/dist/modules/providers/openRouterModels.d.ts.map +1 -0
- package/dist/modules/providers/openRouterModels.js +107 -0
- package/dist/modules/providers/openRouterModels.js.map +1 -0
- package/dist/modules/serviceCatalog/BackstageCatalogClient.d.ts +75 -0
- package/dist/modules/serviceCatalog/BackstageCatalogClient.d.ts.map +1 -0
- package/dist/modules/serviceCatalog/BackstageCatalogClient.js +410 -0
- package/dist/modules/serviceCatalog/BackstageCatalogClient.js.map +1 -0
- package/dist/modules/serviceCatalog/ServiceCatalogConnectionService.d.ts +85 -0
- package/dist/modules/serviceCatalog/ServiceCatalogConnectionService.d.ts.map +1 -0
- package/dist/modules/serviceCatalog/ServiceCatalogConnectionService.js +177 -0
- package/dist/modules/serviceCatalog/ServiceCatalogConnectionService.js.map +1 -0
- package/dist/modules/serviceCatalog/backstage-catalog.logic.d.ts +173 -0
- package/dist/modules/serviceCatalog/backstage-catalog.logic.d.ts.map +1 -0
- package/dist/modules/serviceCatalog/backstage-catalog.logic.js +362 -0
- package/dist/modules/serviceCatalog/backstage-catalog.logic.js.map +1 -0
- package/dist/modules/serviceCatalog/serviceCatalogAuth.d.ts +42 -0
- package/dist/modules/serviceCatalog/serviceCatalogAuth.d.ts.map +1 -0
- package/dist/modules/serviceCatalog/serviceCatalogAuth.js +201 -0
- package/dist/modules/serviceCatalog/serviceCatalogAuth.js.map +1 -0
- package/dist/modules/shared/base64.d.ts +19 -0
- package/dist/modules/shared/base64.d.ts.map +1 -0
- package/dist/modules/shared/base64.js +75 -0
- package/dist/modules/shared/base64.js.map +1 -0
- package/dist/modules/tracker/TicketTrackerService.js +1 -1
- package/dist/modules/tracker/TicketTrackerService.js.map +1 -1
- package/package.json +5 -5
- package/dist/modules/tracker/base64.d.ts +0 -2
- package/dist/modules/tracker/base64.d.ts.map +0 -1
- package/dist/modules/tracker/base64.js +0 -18
- package/dist/modules/tracker/base64.js.map +0 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { UnavailableError, connectionFailureResult, noopLogger, redactSecrets, } from '@cat-factory/kernel';
|
|
2
|
+
import { assertSafePublicUrl } from '../shared/url-guard.js';
|
|
3
|
+
import { readCappedText, safeFetch } from '../shared/safe-fetch.js';
|
|
4
|
+
import { API_FETCH_FIELDS, CATALOG_LIST_FIELDS, providedApiRefs, serviceIdForEntity, toServiceCatalogApi, toServiceCatalogEntry, } from './backstage-catalog.logic.js';
|
|
5
|
+
import { clientCredentialsBody, mintLegacyBackstageToken, readAccessToken, requiresResolvedBearer, serviceCatalogAuthHeaders, } from './serviceCatalogAuth.js';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// The WIRE half of the Backstage adapter: paged reads of the Software Catalog, and the token
|
|
8
|
+
// exchange the proxied deployments need. Hand-rolled on `fetch` (no vendor SDK) so it bundles
|
|
9
|
+
// into a Worker isolate, exactly as the MCP OAuth client is.
|
|
10
|
+
//
|
|
11
|
+
// Two requests per import, never one per service. The listing runs against
|
|
12
|
+
// `/entities/by-query` with a `fields` projection that leaves out the API definitions, and the
|
|
13
|
+
// definitions come back from ONE batched `/entities/by-refs` per chunk of references. A
|
|
14
|
+
// per-service definition fetch would be a textbook N+1 against someone else's server.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/** How many entities one listing page asks for. The vendor's own default ceiling is higher. */
|
|
17
|
+
const PAGE_SIZE = 100;
|
|
18
|
+
/** How many pages one import will walk, whatever the cap says. A stop, not a policy. */
|
|
19
|
+
const MAX_PAGES = 50;
|
|
20
|
+
/** How many entity references ride one batched by-refs request. */
|
|
21
|
+
const REF_CHUNK = 50;
|
|
22
|
+
/**
|
|
23
|
+
* How many API entities one import will resolve in total.
|
|
24
|
+
*
|
|
25
|
+
* Independent of the service cap because the two counts are independent: a 200-service estate
|
|
26
|
+
* where each component provides four interfaces is 800 entities, and the definitions are the
|
|
27
|
+
* large half of the payload. Whatever this drops stays out of the resolved map, which is what
|
|
28
|
+
* makes `fetchCatalog` count it as a skipped interface: a service never appears to publish fewer
|
|
29
|
+
* interfaces than it does without the number saying so.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_API_REFS = 600;
|
|
32
|
+
/**
|
|
33
|
+
* How many bytes one response may carry before it is refused.
|
|
34
|
+
*
|
|
35
|
+
* A ceiling on what this platform will buffer, not a claim about what a portal may send: a batched
|
|
36
|
+
* by-refs request carrying {@link REF_CHUNK} large definitions can exceed it legitimately, which is
|
|
37
|
+
* why passing it produces a refusal naming the two settings that shrink the response rather than a
|
|
38
|
+
* quietly truncated document (see `makeOversizedError`).
|
|
39
|
+
*/
|
|
40
|
+
const MAX_RESPONSE_BYTES = 8_000_000;
|
|
41
|
+
/** Reads a Backstage Software Catalog through the platform's neutral service-catalog port. */
|
|
42
|
+
export class BackstageCatalogClient {
|
|
43
|
+
options;
|
|
44
|
+
provider = 'backstage';
|
|
45
|
+
log;
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.options = options;
|
|
48
|
+
this.log = options.logger ?? noopLogger;
|
|
49
|
+
}
|
|
50
|
+
async fetchCatalog(fetchOptions) {
|
|
51
|
+
// An empty filter is REFUSED rather than sent as "no filter", which the vendor reads as the
|
|
52
|
+
// whole catalog. The write boundary substitutes a default for an empty one, so the only way to
|
|
53
|
+
// arrive here with none is a connection row whose filter could not be read, and importing an
|
|
54
|
+
// organisation's entire estate because its narrowing was lost is the one outcome worse than
|
|
55
|
+
// failing. (`probe` deliberately asks unfiltered: it wants "can I read ANY entity".)
|
|
56
|
+
if (fetchOptions.entityFilter.length === 0) {
|
|
57
|
+
throw new UnavailableError('This service-catalog connection has no entity filter, so there is nothing to import. Re-save the connection to restore its filter.', 'service_catalog_filter_missing');
|
|
58
|
+
}
|
|
59
|
+
const bearer = await this.resolveBearer();
|
|
60
|
+
const listed = await this.listEntities(fetchOptions, bearer);
|
|
61
|
+
const byRef = fetchOptions.includeApis
|
|
62
|
+
? await this.resolveApis(listed.entities, bearer)
|
|
63
|
+
: new Map();
|
|
64
|
+
const entries = [];
|
|
65
|
+
const claimed = new Set();
|
|
66
|
+
// Every DISTINCT interface an imported service declares and this pass could not store, counted
|
|
67
|
+
// once however many components declare it. A per-declaration tally double-counts twice over: an
|
|
68
|
+
// interface provided by three components would add three, and a reference dropped by the
|
|
69
|
+
// resolver's own cap would be counted there AND here. The number is the only evidence an
|
|
70
|
+
// operator has of how much of the estate is missing, so it has to be the count of things.
|
|
71
|
+
const unresolvedApis = new Set();
|
|
72
|
+
let skippedEntries = listed.skipped;
|
|
73
|
+
for (const entity of listed.entities) {
|
|
74
|
+
const id = serviceIdForEntity(entity);
|
|
75
|
+
// Two entities slugging onto one id is a real collision (a namespaced name whose prefix
|
|
76
|
+
// collapses, a title-cased duplicate), and the first one wins rather than the last: an
|
|
77
|
+
// import has to be STABLE across passes, and "last wins" would flip the survivor whenever
|
|
78
|
+
// the portal paged them in a different order.
|
|
79
|
+
if (!id || claimed.has(id)) {
|
|
80
|
+
skippedEntries += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const refs = fetchOptions.includeApis ? providedApiRefs(entity) : [];
|
|
84
|
+
const resolved = [];
|
|
85
|
+
const missing = [];
|
|
86
|
+
for (const ref of refs) {
|
|
87
|
+
const api = byRef.get(ref);
|
|
88
|
+
if (api)
|
|
89
|
+
resolved.push(api);
|
|
90
|
+
else
|
|
91
|
+
missing.push(ref);
|
|
92
|
+
}
|
|
93
|
+
const entry = toServiceCatalogEntry(entity, resolved);
|
|
94
|
+
if (!entry) {
|
|
95
|
+
skippedEntries += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Recorded only once the entity is ADMITTED: an entity that became no service at all is
|
|
99
|
+
// already reported as a skipped service, and counting its interfaces again would describe
|
|
100
|
+
// one loss twice.
|
|
101
|
+
for (const ref of missing)
|
|
102
|
+
unresolvedApis.add(ref);
|
|
103
|
+
claimed.add(id);
|
|
104
|
+
entries.push(entry);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
entries,
|
|
108
|
+
coverage: listed.coverage,
|
|
109
|
+
skippedEntries,
|
|
110
|
+
skippedApis: unresolvedApis.size,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async probe() {
|
|
114
|
+
try {
|
|
115
|
+
const bearer = await this.resolveBearer();
|
|
116
|
+
const page = await this.getJson(this.listUrl({ entityFilter: [] }, 1, null), bearer);
|
|
117
|
+
// An EMPTY answer is reported as a connected-but-empty success rather than a failure: the
|
|
118
|
+
// request was authorized and answered, and "this instance holds no entities the credential
|
|
119
|
+
// may read" is a different problem from "the platform cannot reach it" with a different fix.
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
message: readItems(page).length > 0
|
|
123
|
+
? 'Connected: the catalog answered and returned at least one entity.'
|
|
124
|
+
: 'Connected, but the catalog returned no entities. Check that this instance holds entities and that the credential is allowed to read them.',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return connectionFailureResult(error, {
|
|
129
|
+
subject: 'the Backstage catalog API',
|
|
130
|
+
target: this.base(),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The bearer value every request in this pass carries, or null for the modes that need none.
|
|
136
|
+
*
|
|
137
|
+
* Resolved ONCE per pass, which is the whole reason it is a separate step: a legacy token has a
|
|
138
|
+
* short expiry and an OAuth2 exchange is a round trip, so minting or fetching per page would
|
|
139
|
+
* multiply both the latency and the load on someone else's identity provider by the page count.
|
|
140
|
+
*/
|
|
141
|
+
async resolveBearer() {
|
|
142
|
+
const auth = this.options.auth;
|
|
143
|
+
if (!requiresResolvedBearer(auth.mode))
|
|
144
|
+
return null;
|
|
145
|
+
if (auth.mode === 'legacy-shared-secret') {
|
|
146
|
+
return mintLegacyBackstageToken(auth.sharedSecret, Math.floor(this.now() / 1000));
|
|
147
|
+
}
|
|
148
|
+
if (auth.mode !== 'oauth2-client-credentials')
|
|
149
|
+
return null;
|
|
150
|
+
assertSafePublicUrl(auth.tokenUrl, {
|
|
151
|
+
subject: 'Backstage',
|
|
152
|
+
label: 'OAuth token URL',
|
|
153
|
+
...(this.options.urlPolicy ? { policy: this.options.urlPolicy } : {}),
|
|
154
|
+
});
|
|
155
|
+
const response = await this.fetchSafely(auth.tokenUrl, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
159
|
+
accept: 'application/json',
|
|
160
|
+
},
|
|
161
|
+
body: clientCredentialsBody(auth),
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw upstream(`The identity provider refused the service-catalog client credentials (HTTP ${response.status}): ${snippet(await readSnippetBody(response))}`, 'service_catalog_unauthorized');
|
|
165
|
+
}
|
|
166
|
+
const token = readAccessToken(parseJson(await readWholeBody(response)));
|
|
167
|
+
if (!token) {
|
|
168
|
+
throw upstream('The identity provider answered the service-catalog token request without an `access_token`.', 'service_catalog_unauthorized');
|
|
169
|
+
}
|
|
170
|
+
return token;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Walk the listing until the cap, the last page, or the page ceiling.
|
|
174
|
+
*
|
|
175
|
+
* Coverage is decided HERE rather than by comparing counts afterwards, because only this loop
|
|
176
|
+
* knows WHY it stopped: a cursor still outstanding when the cap was reached is a truncation, and
|
|
177
|
+
* the same entity count with no cursor left is a complete read of a small estate.
|
|
178
|
+
*/
|
|
179
|
+
async listEntities(fetchOptions, bearer) {
|
|
180
|
+
const entities = [];
|
|
181
|
+
let skipped = 0;
|
|
182
|
+
let cursor = null;
|
|
183
|
+
let truncated = false;
|
|
184
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
185
|
+
const remaining = fetchOptions.maxServices - entities.length;
|
|
186
|
+
if (remaining <= 0) {
|
|
187
|
+
truncated = true;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
const body = await this.getJson(this.listUrl(fetchOptions, Math.min(PAGE_SIZE, remaining), cursor), bearer);
|
|
191
|
+
for (const item of readItems(body)) {
|
|
192
|
+
if (item && typeof item === 'object')
|
|
193
|
+
entities.push(item);
|
|
194
|
+
else
|
|
195
|
+
skipped += 1;
|
|
196
|
+
}
|
|
197
|
+
cursor = readNextCursor(body);
|
|
198
|
+
if (!cursor)
|
|
199
|
+
break;
|
|
200
|
+
if (page === MAX_PAGES - 1)
|
|
201
|
+
truncated = true;
|
|
202
|
+
}
|
|
203
|
+
if (cursor)
|
|
204
|
+
truncated = true;
|
|
205
|
+
const coverage = entities.length === 0 ? 'empty' : truncated ? 'truncated' : 'complete';
|
|
206
|
+
return { entities, coverage, skipped };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Every API entity the listed components declare, in batched requests, indexed by reference.
|
|
210
|
+
*
|
|
211
|
+
* Indexed rather than returned in order, because one interface is routinely provided by several
|
|
212
|
+
* components and the portal answers a by-refs request positionally: a map is what lets the
|
|
213
|
+
* caller attach the same definition to each provider without asking for it twice.
|
|
214
|
+
*
|
|
215
|
+
* Nothing is COUNTED here, deliberately. A reference dropped by {@link MAX_API_REFS} is simply
|
|
216
|
+
* absent from the map, which is the same state as one the portal would not serve, and the caller
|
|
217
|
+
* counts both once by looking at what it could not attach.
|
|
218
|
+
*/
|
|
219
|
+
async resolveApis(entities, bearer) {
|
|
220
|
+
const refs = [];
|
|
221
|
+
for (const entity of entities) {
|
|
222
|
+
for (const ref of providedApiRefs(entity)) {
|
|
223
|
+
if (refs.includes(ref))
|
|
224
|
+
continue;
|
|
225
|
+
if (refs.length >= MAX_API_REFS)
|
|
226
|
+
break;
|
|
227
|
+
refs.push(ref);
|
|
228
|
+
}
|
|
229
|
+
if (refs.length >= MAX_API_REFS)
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
const byRef = new Map();
|
|
233
|
+
for (let offset = 0; offset < refs.length; offset += REF_CHUNK) {
|
|
234
|
+
const chunk = refs.slice(offset, offset + REF_CHUNK);
|
|
235
|
+
const body = await this.postJson(`${this.base()}/api/catalog/entities/by-refs`, { entityRefs: chunk, fields: [...API_FETCH_FIELDS] }, bearer);
|
|
236
|
+
// Positional against the refs we asked for: the vendor answers null for an entity that does
|
|
237
|
+
// not exist (or that this credential may not read), and reading the answer by index is what
|
|
238
|
+
// keeps a null attributed to the reference that produced it.
|
|
239
|
+
for (const [index, item] of readItems(body).entries()) {
|
|
240
|
+
const ref = chunk[index];
|
|
241
|
+
if (!ref)
|
|
242
|
+
continue;
|
|
243
|
+
const api = item && typeof item === 'object' ? toServiceCatalogApi(item) : null;
|
|
244
|
+
if (api)
|
|
245
|
+
byRef.set(ref, api);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return byRef;
|
|
249
|
+
}
|
|
250
|
+
listUrl(fetchOptions, limit, cursor) {
|
|
251
|
+
const query = new URLSearchParams();
|
|
252
|
+
// Comma-separated terms are the vendor's AND, and one `filter` parameter is what keeps this an
|
|
253
|
+
// intersection: repeating the parameter would make the terms a UNION and silently widen an
|
|
254
|
+
// operator's narrowing into the whole catalog.
|
|
255
|
+
if (fetchOptions.entityFilter.length > 0) {
|
|
256
|
+
query.set('filter', fetchOptions.entityFilter.join(','));
|
|
257
|
+
}
|
|
258
|
+
query.set('limit', String(limit));
|
|
259
|
+
query.set('fields', CATALOG_LIST_FIELDS.join(','));
|
|
260
|
+
if (cursor)
|
|
261
|
+
query.set('cursor', cursor);
|
|
262
|
+
return `${this.base()}/api/catalog/entities/by-query?${query.toString()}`;
|
|
263
|
+
}
|
|
264
|
+
base() {
|
|
265
|
+
return this.options.baseUrl.replace(/\/+$/, '');
|
|
266
|
+
}
|
|
267
|
+
now() {
|
|
268
|
+
return this.options.clock?.now() ?? Date.now();
|
|
269
|
+
}
|
|
270
|
+
async getJson(url, bearer) {
|
|
271
|
+
const response = await this.fetchSafely(url, {
|
|
272
|
+
method: 'GET',
|
|
273
|
+
headers: { accept: 'application/json', ...this.authHeaders(bearer) },
|
|
274
|
+
});
|
|
275
|
+
return this.readJson(response, url);
|
|
276
|
+
}
|
|
277
|
+
async postJson(url, body, bearer) {
|
|
278
|
+
const response = await this.fetchSafely(url, {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: {
|
|
281
|
+
accept: 'application/json',
|
|
282
|
+
'content-type': 'application/json',
|
|
283
|
+
...this.authHeaders(bearer),
|
|
284
|
+
},
|
|
285
|
+
body: JSON.stringify(body),
|
|
286
|
+
});
|
|
287
|
+
return this.readJson(response, url);
|
|
288
|
+
}
|
|
289
|
+
authHeaders(bearer) {
|
|
290
|
+
return serviceCatalogAuthHeaders(this.options.auth, bearer);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* `fetch` with the SSRF guard re-run on every redirect hop.
|
|
294
|
+
*
|
|
295
|
+
* The per-hop check is what a per-connection host needs and a host-pinned helper cannot give:
|
|
296
|
+
* the base URL is operator-supplied, so a permitted first host could 302 the credential-bearing
|
|
297
|
+
* request at a link-local metadata address. `safeFetch` also drops the body and the
|
|
298
|
+
* `Authorization` header on a cross-origin hop, so a portal cannot bounce the token to a
|
|
299
|
+
* different public host either.
|
|
300
|
+
*/
|
|
301
|
+
fetchSafely(url, init) {
|
|
302
|
+
const assertSafe = (candidate) => assertSafePublicUrl(candidate, {
|
|
303
|
+
subject: 'Backstage',
|
|
304
|
+
label: 'catalog URL',
|
|
305
|
+
...(this.options.urlPolicy ? { policy: this.options.urlPolicy } : {}),
|
|
306
|
+
});
|
|
307
|
+
return safeFetch(url, init, assertSafe, makeCatalogError, undefined, this.options.fetchImpl);
|
|
308
|
+
}
|
|
309
|
+
async readJson(response, url) {
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
const reason = response.status === 401 || response.status === 403
|
|
312
|
+
? 'service_catalog_unauthorized'
|
|
313
|
+
: 'service_catalog_unreachable';
|
|
314
|
+
// The PATHNAME only: a catalog URL carries the filter and the cursor, and a cursor is an
|
|
315
|
+
// opaque vendor token that has no business in a log line or an operator-facing message.
|
|
316
|
+
this.log.warn('serviceCatalog.backstage.requestFailed', {
|
|
317
|
+
status: response.status,
|
|
318
|
+
path: pathOf(url),
|
|
319
|
+
});
|
|
320
|
+
throw upstream(`The Backstage catalog answered HTTP ${response.status}: ${snippet(await readSnippetBody(response))}`, reason);
|
|
321
|
+
}
|
|
322
|
+
const parsed = parseJson(await readWholeBody(response));
|
|
323
|
+
if (parsed === null) {
|
|
324
|
+
throw upstream('The Backstage catalog answered with a body that is not JSON.', 'service_catalog_unreachable');
|
|
325
|
+
}
|
|
326
|
+
return parsed;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* The body of a SUCCESSFUL response, or a refusal naming OUR limit.
|
|
331
|
+
*
|
|
332
|
+
* The overflow THROWS here, where the error path below truncates. That asymmetry is the whole
|
|
333
|
+
* point: a truncated error snippet is still a usable excerpt, while a truncated success body is a
|
|
334
|
+
* JSON document cut mid-token, which the parser then rejects and the caller reports as "the portal
|
|
335
|
+
* answered with a body that is not JSON", blaming someone else's server for a ceiling on this
|
|
336
|
+
* side of the wire. A batched by-refs request carrying fifty definitions can genuinely exceed it,
|
|
337
|
+
* so the refusal names the two knobs that shrink the response.
|
|
338
|
+
*/
|
|
339
|
+
function readWholeBody(response) {
|
|
340
|
+
return readCappedText(response, MAX_RESPONSE_BYTES, makeOversizedError, true);
|
|
341
|
+
}
|
|
342
|
+
/** The body of a FAILED response, truncated: it is quoted as an excerpt, never parsed. */
|
|
343
|
+
function readSnippetBody(response) {
|
|
344
|
+
return readCappedText(response, MAX_RESPONSE_BYTES, makeCatalogError, false);
|
|
345
|
+
}
|
|
346
|
+
/** The `items` array a catalog response carries, or empty when it carries none. */
|
|
347
|
+
function readItems(body) {
|
|
348
|
+
const items = body.items;
|
|
349
|
+
return Array.isArray(items) ? items : [];
|
|
350
|
+
}
|
|
351
|
+
/** The cursor the vendor's paged listing hands back, or null on the last page. */
|
|
352
|
+
function readNextCursor(body) {
|
|
353
|
+
const pageInfo = body.pageInfo;
|
|
354
|
+
const cursor = pageInfo?.nextCursor;
|
|
355
|
+
return typeof cursor === 'string' && cursor ? cursor : null;
|
|
356
|
+
}
|
|
357
|
+
function parseJson(text) {
|
|
358
|
+
try {
|
|
359
|
+
return JSON.parse(text);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// silent-catch-ok: the caller turns a null into a stated "not JSON" refusal that names the
|
|
363
|
+
// portal, which is strictly more useful than the parser's own column number.
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* A bounded, SCRUBBED excerpt of what the portal said.
|
|
369
|
+
*
|
|
370
|
+
* Scrubbed because an error body is the one response that routinely echoes the request: a proxy's
|
|
371
|
+
* 401 page can include the `Authorization` header it rejected, and this string reaches an operator
|
|
372
|
+
* message and a log field.
|
|
373
|
+
*/
|
|
374
|
+
function snippet(text) {
|
|
375
|
+
const cleaned = redactSecrets(text.replace(/\s+/g, ' ').trim()) ?? '';
|
|
376
|
+
if (!cleaned)
|
|
377
|
+
return '(empty response)';
|
|
378
|
+
return cleaned.length > 300 ? `${cleaned.slice(0, 300)}...` : cleaned;
|
|
379
|
+
}
|
|
380
|
+
function pathOf(url) {
|
|
381
|
+
try {
|
|
382
|
+
return new URL(url).pathname;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// silent-catch-ok: this is a log field, and an unparseable URL was already refused by the
|
|
386
|
+
// guard above; a placeholder keeps the line emitting rather than failing the request.
|
|
387
|
+
return '(unparseable)';
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* The refusal a portal failure raises: a 503 carrying the machine-readable reason, so the SPA maps
|
|
392
|
+
* it to translated copy instead of rendering the vendor's prose as the whole explanation.
|
|
393
|
+
*/
|
|
394
|
+
function upstream(message, reason) {
|
|
395
|
+
return new UnavailableError(message, reason);
|
|
396
|
+
}
|
|
397
|
+
/** The error shape `safeFetch` / `readCappedText` build for a blocked hop or an oversized body. */
|
|
398
|
+
function makeCatalogError(status, message) {
|
|
399
|
+
return new UnavailableError(`Backstage catalog request failed (${status}): ${message}`, 'service_catalog_unreachable');
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The refusal an oversized SUCCESS body raises. Its own reason, because it is the one portal
|
|
403
|
+
* failure with a remedy on this side: the operator lowers the service cap or turns off interface
|
|
404
|
+
* import, and the next pass fits. Reported as "unreachable" it would read as someone else's
|
|
405
|
+
* outage and send them to wait for it to end.
|
|
406
|
+
*/
|
|
407
|
+
function makeOversizedError(_status, _message) {
|
|
408
|
+
return new UnavailableError(`The Backstage catalog answered with more than ${MAX_RESPONSE_BYTES} bytes in one response, which this platform will not buffer. Lower the service cap, or turn off interface import, so the portal answers in smaller pieces.`, 'service_catalog_response_too_large');
|
|
409
|
+
}
|
|
410
|
+
//# sourceMappingURL=BackstageCatalogClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BackstageCatalogClient.js","sourceRoot":"","sources":["../../../src/modules/serviceCatalog/BackstageCatalogClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ConnectServiceCatalogInput, ConnectionTestResult, ServiceCatalogConnection } from '@cat-factory/contracts';
|
|
2
|
+
import type { Clock, Logger, SecretCipher, SecretDelegate, ServiceCatalogClient, ServiceCatalogConnectionRepository, UrlSafetyPolicy } from '@cat-factory/kernel';
|
|
3
|
+
/**
|
|
4
|
+
* The HKDF domain the service-catalog credential is sealed under.
|
|
5
|
+
*
|
|
6
|
+
* Its own domain rather than the documents or environments one, so a key rotation or a compromise
|
|
7
|
+
* scoped to one integration cannot open another's rows. Declared HERE, once, and imported by both
|
|
8
|
+
* runtime facades and by `SEALED_SECRET_SOURCES`: the value has to be the same in all three or a
|
|
9
|
+
* mothership-delegated unseal authenticates against a different derived key, and that drift is
|
|
10
|
+
* both silent and unrecoverable, which is not a property any copied literal should carry.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SERVICE_CATALOG_CIPHER_INFO = "cat-factory:service-catalog";
|
|
13
|
+
export interface ServiceCatalogConnectionServiceDependencies {
|
|
14
|
+
serviceCatalogConnectionRepository: ServiceCatalogConnectionRepository;
|
|
15
|
+
/** The deployment's {@link SERVICE_CATALOG_CIPHER_INFO} cipher. */
|
|
16
|
+
secretCipher: SecretCipher;
|
|
17
|
+
/** Present ONLY on a mothership-mode node, where the row was sealed under the org's key. */
|
|
18
|
+
secretDelegate?: SecretDelegate;
|
|
19
|
+
clock: Clock;
|
|
20
|
+
logger?: Logger;
|
|
21
|
+
/**
|
|
22
|
+
* The deployment's URL policy for this integration. A self-hosted portal on `.internal` or a
|
|
23
|
+
* private address is the NORMAL case, so an operator widens this rather than the integration
|
|
24
|
+
* assuming a public host; the strict default refuses one, which is the right default for a URL
|
|
25
|
+
* a workspace admin can type.
|
|
26
|
+
*/
|
|
27
|
+
urlPolicy?: UrlSafetyPolicy;
|
|
28
|
+
/** Injected for tests. */
|
|
29
|
+
fetchImpl?: typeof fetch;
|
|
30
|
+
}
|
|
31
|
+
export declare class ServiceCatalogConnectionService {
|
|
32
|
+
private readonly deps;
|
|
33
|
+
private readonly log;
|
|
34
|
+
private readonly orgSecrets;
|
|
35
|
+
constructor(deps: ServiceCatalogConnectionServiceDependencies);
|
|
36
|
+
/**
|
|
37
|
+
* The connection as the management surface sees it, or null when there is none.
|
|
38
|
+
*
|
|
39
|
+
* A tombstoned row answers NULL, exactly as `resolveClient` and the importer treat one. The
|
|
40
|
+
* repository's `get` deliberately still returns it, because a re-connect reads the row to keep
|
|
41
|
+
* the prior verdict and the original `connectedAt`; the disposal of a tombstone is the caller's,
|
|
42
|
+
* and every caller but that one wants "there is no connection". Returning it here would render a
|
|
43
|
+
* disconnected workspace as connected, with an "Import now" button that 404s.
|
|
44
|
+
*/
|
|
45
|
+
get(workspaceId: string): Promise<ServiceCatalogConnection | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Connect or replace the workspace's connection.
|
|
48
|
+
*
|
|
49
|
+
* The base URL is validated BEFORE anything is sealed, so a typo or a blocked host is refused
|
|
50
|
+
* while the operator is still looking at the form rather than becoming a stored connection that
|
|
51
|
+
* fails on every import.
|
|
52
|
+
*/
|
|
53
|
+
connect(workspaceId: string, input: ConnectServiceCatalogInput): Promise<ServiceCatalogConnection>;
|
|
54
|
+
/** Forget the credential. Tombstoning what it imported is the importer's half, called beside. */
|
|
55
|
+
disconnect(workspaceId: string): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Probe with the SUBMITTED credentials rather than the stored ones.
|
|
58
|
+
*
|
|
59
|
+
* Which is the point of the endpoint: an operator testing a connection is testing what they just
|
|
60
|
+
* typed, and probing the stored row would answer about the credential they are replacing.
|
|
61
|
+
*/
|
|
62
|
+
probe(input: ConnectServiceCatalogInput): Promise<ConnectionTestResult>;
|
|
63
|
+
/**
|
|
64
|
+
* The client for a workspace's stored connection, or null when it has none.
|
|
65
|
+
*
|
|
66
|
+
* Wired as the `ResolveServiceCatalogClient` port the importer depends on. It REJECTS when a row
|
|
67
|
+
* exists whose bag will not open, because that is a deployment fault with its own remedy
|
|
68
|
+
* (re-enter the credential) and is not the same fact as a workspace that never connected a
|
|
69
|
+
* portal, which the importer treats as nothing to do.
|
|
70
|
+
*/
|
|
71
|
+
resolveClient: (workspaceId: string) => Promise<ServiceCatalogClient | null>;
|
|
72
|
+
private buildClient;
|
|
73
|
+
private assertBaseUrl;
|
|
74
|
+
/**
|
|
75
|
+
* Seal the credential half of the submitted auth.
|
|
76
|
+
*
|
|
77
|
+
* `none` seals NOTHING and stores the empty string, rather than an envelope around an empty
|
|
78
|
+
* object. An unauthenticated portal has no secret, so a deployment whose `ENCRYPTION_KEY`
|
|
79
|
+
* drifted must keep working against it: sealing a placeholder would make the one mode with no
|
|
80
|
+
* credential the one that fails on a key problem.
|
|
81
|
+
*/
|
|
82
|
+
private sealAuth;
|
|
83
|
+
private openAuth;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=ServiceCatalogConnectionService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ServiceCatalogConnectionService.d.ts","sourceRoot":"","sources":["../../../src/modules/serviceCatalog/ServiceCatalogConnectionService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,oBAAoB,EAEpB,wBAAwB,EACzB,MAAM,wBAAwB,CAAA;AAK/B,OAAO,KAAK,EACV,KAAK,EACL,MAAM,EACN,YAAY,EACZ,cAAc,EACd,oBAAoB,EAEpB,kCAAkC,EAClC,eAAe,EAChB,MAAM,qBAAqB,CAAA;AAgB5B;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,gCAAgC,CAAA;AAExE,MAAM,WAAW,2CAA2C;IAC1D,kCAAkC,EAAE,kCAAkC,CAAA;IACtE,mEAAmE;IACnE,YAAY,EAAE,YAAY,CAAA;IAC1B,4FAA4F;IAC5F,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,0BAA0B;IAC1B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAED,qBAAa,+BAA+B;IAK9B,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0C;IAErE,YAA6B,IAAI,EAAE,2CAA2C,EAM7E;IAED;;;;;;;;OAQG;IACG,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAGvE;IAED;;;;;;OAMG;IACG,OAAO,CACX,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,wBAAwB,CAAC,CA8BnC;IAED,iGAAiG;IAC3F,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMnD;IAED;;;;;OAKG;IACG,KAAK,CAAC,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAG5E;IAED;;;;;;;OAOG;IACH,aAAa,gBAAuB,MAAM,KAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAKhF;IAED,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,aAAa;IAUrB;;;;;;;OAOG;YACW,QAAQ;YAQR,QAAQ;CAQvB"}
|