@dudousxd/nestjs-catalog 0.5.0 → 0.6.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.
@@ -16,6 +16,7 @@ exports.createCatalogController = createCatalogController;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const catalog_query_cache_1 = require("./catalog.query-cache");
18
18
  const catalog_registry_base_1 = require("./catalog.registry.base");
19
+ const catalog_route_auth_1 = require("./catalog.route-auth");
19
20
  const catalog_service_1 = require("./catalog.service");
20
21
  const catalog_workspace_1 = require("./catalog.workspace");
21
22
  /**
@@ -23,6 +24,52 @@ const catalog_workspace_1 = require("./catalog.workspace");
23
24
  * guards both come from `forRoot`. A library that hardcodes either one forces
24
25
  * every host app to accept its idea of auth, which for an endpoint that
25
26
  * enumerates every table in the database is not a reasonable default.
27
+ *
28
+ * ---------------------------------------------------------------------------
29
+ * Every route declares what it needs, and four of the choices are not obvious.
30
+ *
31
+ * Declaring is not enforcing — see `catalog.route-auth.ts` — but an *absent*
32
+ * declaration is a declaration too: it tells a host's guard that authenticated
33
+ * is enough. So a route left bare is not "undecided", it is "open", and for a
34
+ * long time this controller said that about arbitrary SQL and about every
35
+ * curation edit.
36
+ *
37
+ * **Arbitrary SQL is `catalog:admin`, not `catalog:read`.** `catalog:read` is
38
+ * "read object metadata and rows" — rows *of a catalogued type*, through a
39
+ * route that names one. `POST query` is not bounded by the model at all: it is
40
+ * whatever the store's read connection can select, and in the shipped MikroORM
41
+ * store that connection is the catalog's own schema. `SELECT * FROM
42
+ * catalog_principal` returns every principal's scopes, grants and `keyHash` —
43
+ * the SHA-256 of its static key — which is not something a reporting principal
44
+ * should be able to fetch, and is not reachable from any other route here. That
45
+ * is squarely the population `catalog:admin` ("manage principals and grants")
46
+ * describes.
47
+ *
48
+ * **So the two saved-query write routes are `catalog:admin` too.** They accept
49
+ * a `sql` field, and `POST saved-queries` + `POST saved-queries/:id/run` is
50
+ * `POST query` in two requests. Gating one and not the others would make the
51
+ * strict declaration decoration.
52
+ *
53
+ * **Running a saved query is only `catalog:read`.** The capability being held
54
+ * back is *choosing what SQL runs*, not *seeing a result*: a saved query is an
55
+ * artefact somebody with the authoring scope vetted, and running one is reading
56
+ * a report they wrote. Gating execution instead would be worse in both
57
+ * directions — it would stop an analyst opening a dashboard, and it would let an
58
+ * unprivileged caller plant a statement and wait for a privileged one to run it.
59
+ *
60
+ * **`POST reset` is `catalog:curate`, not `catalog:admin`.** It discards exactly
61
+ * what the two `PATCH`es write, catalog-wide, and a curator can already blank
62
+ * every label one request at a time. Requiring admin would deny nothing and
63
+ * would push a routine console action into the scope that manages principals.
64
+ *
65
+ * One consequence is worth stating rather than leaving to be discovered:
66
+ * `shared` rides on the dashboard write routes, so `catalog:curate` carries the
67
+ * power to hand a board to an outside application. That is not an oversight —
68
+ * it is one field on an authoring route, and splitting it would mean a route
69
+ * whose only job is to flip a boolean. What makes it accountable is that the
70
+ * act is audited, in both directions and including by deletion; see the
71
+ * sharing block in `catalog.service.ts`.
72
+ * ---------------------------------------------------------------------------
26
73
  */
27
74
  function createCatalogController(path, guards, decorators = []) {
28
75
  let CatalogController = class CatalogController {
@@ -88,11 +135,24 @@ function createCatalogController(path, guards, decorators = []) {
88
135
  snapshot,
89
136
  });
90
137
  }
91
- /** What an ad-hoc query may select from, and whether it can run at all. */
138
+ /**
139
+ * What an ad-hoc query may select from, and whether it can run at all.
140
+ *
141
+ * `catalog:read` and not the scope the query route itself needs: this
142
+ * describes the catalogued types' physical relations and their columns,
143
+ * which is the same model `GET types/:name` already hands a reader. It is a
144
+ * schema panel, not an execution surface.
145
+ */
92
146
  queryRelations() {
93
147
  return this.service.queryRelations();
94
148
  }
95
- /** Run one read-only statement. */
149
+ /**
150
+ * Run one read-only statement.
151
+ *
152
+ * `catalog:admin`. Read-only is not the same as bounded: this reaches
153
+ * whatever the store's connection reaches, which includes the catalog's own
154
+ * governance tables. See the block above the controller.
155
+ */
96
156
  runQuery(body) {
97
157
  return this.service.runQuery(body ?? { sql: '' });
98
158
  }
@@ -106,19 +166,43 @@ function createCatalogController(path, guards, decorators = []) {
106
166
  savedQueries() {
107
167
  return this.service.listSavedQueries();
108
168
  }
109
- saveSavedQuery(body) {
110
- return this.service.saveQuery(body, body?.createdBy ?? 'console');
169
+ /** Takes a `sql` field, so it is the SQL scope and not a workspace one. */
170
+ saveSavedQuery(body, request) {
171
+ return this.service.saveQuery(body, actorOf(request, body?.createdBy));
111
172
  }
112
173
  savedQuery(id) {
113
174
  return this.service.getSavedQuery(id);
114
175
  }
115
- patchSavedQuery(id, body) {
116
- return this.service.updateSavedQuery(id, body);
176
+ /**
177
+ * The body names no actor, deliberately — unlike the create route above,
178
+ * which has always let one be declared because `createdBy` is a stored
179
+ * field. Here the only consumer of the name is the audit entry a `shared`
180
+ * toggle produces, and a caller that can put any string into the audit trail
181
+ * is worse than one the trail records as the console.
182
+ *
183
+ * `catalog:admin` because the body may carry `sql`. A patch that can rewrite
184
+ * the statement is the authoring capability whatever else it is used for.
185
+ */
186
+ patchSavedQuery(id, body, request) {
187
+ return this.service.updateSavedQuery(id, body, actorOf(request));
117
188
  }
118
- removeSavedQuery(id) {
119
- return this.service.deleteSavedQuery(id).then((deleted) => ({ deleted }));
189
+ /**
190
+ * `catalog:curate` and not the SQL scope: deleting takes a statement away
191
+ * rather than choosing one. It is the same workspace-authoring act as
192
+ * deleting a board.
193
+ *
194
+ * The principal is passed for the same reason the `PATCH` above takes one —
195
+ * deleting a shared query revokes outside access, and that is recorded.
196
+ */
197
+ removeSavedQuery(id, request) {
198
+ return this.service.deleteSavedQuery(id, actorOf(request)).then((deleted) => ({ deleted }));
120
199
  }
121
- /** Run a saved query, honouring the cache TTL it was saved with. */
200
+ /**
201
+ * Run a saved query, honouring the cache TTL it was saved with.
202
+ *
203
+ * `catalog:read`, unlike writing one. What is held back upstream is
204
+ * choosing what SQL runs; this runs a statement somebody already vetted.
205
+ */
122
206
  runSavedQuery(id, body) {
123
207
  return this.service.runSavedQuery(id, body?.maxRows);
124
208
  }
@@ -138,23 +222,43 @@ function createCatalogController(path, guards, decorators = []) {
138
222
  dashboards() {
139
223
  return this.service.listDashboards();
140
224
  }
141
- createDashboard(body) {
142
- return this.service.saveDashboard(body, body?.createdBy ?? 'console');
225
+ /**
226
+ * `shared` is declared rather than merely passed through.
227
+ *
228
+ * It is the entire access boundary of the embed API — a board is
229
+ * embeddable because a person said so — and a field a body type does not
230
+ * name is a field a host's whitelisting `ValidationPipe` deletes. The
231
+ * failure is silent in the worst direction: the toggle appears to save and
232
+ * the dashboard is never actually shareable.
233
+ *
234
+ * `catalog:curate`: a board carries no SQL of its own, only cards pointing
235
+ * at queries somebody else vetted. `shared` rides along, which is why the
236
+ * flag is audited rather than merely stored.
237
+ */
238
+ createDashboard(body, request) {
239
+ return this.service.saveDashboard(body, actorOf(request, body?.createdBy));
143
240
  }
144
241
  dashboard(id) {
145
242
  return this.service.getDashboard(id);
146
243
  }
147
- patchDashboard(id, body) {
148
- return this.service.updateDashboard(id, body);
244
+ patchDashboard(id, body, request) {
245
+ return this.service.updateDashboard(id, body, actorOf(request));
149
246
  }
150
- removeDashboard(id) {
151
- return this.service.deleteDashboard(id).then((deleted) => ({ deleted }));
247
+ /** Deleting a shared board revokes outside access, so the actor is passed. */
248
+ removeDashboard(id, request) {
249
+ return this.service.deleteDashboard(id, actorOf(request)).then((deleted) => ({ deleted }));
152
250
  }
153
251
  /**
154
252
  * What this caller may embed.
155
253
  *
156
254
  * A discovery endpoint so a consuming frontend can list what it is allowed
157
255
  * to render rather than being told the ids out of band.
256
+ *
257
+ * `catalog:embed` here as well as on the two fetches, and deliberately the
258
+ * SAME scope rather than a softer one. A caller that cannot fetch anything
259
+ * has no use for the list, and a discovery endpoint open to callers the
260
+ * fetches refuse is an inventory of this catalog's shared dashboards handed
261
+ * to whoever asks.
158
262
  */
159
263
  embeddable() {
160
264
  return this.service.listEmbeddable();
@@ -163,11 +267,26 @@ function createCatalogController(path, guards, decorators = []) {
163
267
  embedDashboard(id) {
164
268
  return this.service.embedDashboard(id);
165
269
  }
166
- /** One chart, for a consumer that wants to place it itself. */
270
+ /**
271
+ * One chart, for a consumer that wants to place it itself.
272
+ *
273
+ * `catalog:embed` and not `catalog:read`: the whole reason the scope exists
274
+ * separately is that an application rendering one chart in its own UI needs
275
+ * nothing else, and a route that accepted `catalog:read` instead would make
276
+ * the narrow grant unusable — every embed consumer would be handed the
277
+ * whole catalog to draw one bar chart.
278
+ */
167
279
  embedChart(id) {
168
280
  return this.service.embedChart(id);
169
281
  }
170
- /** The audit trail: what happened, to what, by whom. */
282
+ /**
283
+ * The audit trail: what happened, to what, by whom.
284
+ *
285
+ * `catalog:read`, because attribution here is part of the data rather than a
286
+ * log — "who loaded these rows" is a question a reader asks months later,
287
+ * which is the whole reason the principal is recorded onto every snapshot.
288
+ * The trail carries ids and payloads, never a credential.
289
+ */
171
290
  events(event, typeName, principalId, since, limit) {
172
291
  return this.service.listEvents({
173
292
  event,
@@ -216,18 +335,21 @@ function createCatalogController(path, guards, decorators = []) {
216
335
  };
217
336
  __decorate([
218
337
  (0, common_1.Get)(),
338
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
219
339
  __metadata("design:type", Function),
220
340
  __metadata("design:paramtypes", []),
221
341
  __metadata("design:returntype", void 0)
222
342
  ], CatalogController.prototype, "snapshot", null);
223
343
  __decorate([
224
344
  (0, common_1.Get)('graph'),
345
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
225
346
  __metadata("design:type", Function),
226
347
  __metadata("design:paramtypes", []),
227
348
  __metadata("design:returntype", void 0)
228
349
  ], CatalogController.prototype, "graph", null);
229
350
  __decorate([
230
351
  (0, common_1.Get)('types/:name'),
352
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
231
353
  __param(0, (0, common_1.Param)('name')),
232
354
  __metadata("design:type", Function),
233
355
  __metadata("design:paramtypes", [String]),
@@ -235,6 +357,7 @@ function createCatalogController(path, guards, decorators = []) {
235
357
  ], CatalogController.prototype, "type", null);
236
358
  __decorate([
237
359
  (0, common_1.Patch)('types/:name'),
360
+ (0, catalog_route_auth_1.RequireScopes)('catalog:curate'),
238
361
  __param(0, (0, common_1.Param)('name')),
239
362
  __param(1, (0, common_1.Body)()),
240
363
  __metadata("design:type", Function),
@@ -243,6 +366,7 @@ function createCatalogController(path, guards, decorators = []) {
243
366
  ], CatalogController.prototype, "patchType", null);
244
367
  __decorate([
245
368
  (0, common_1.Patch)('types/:name/properties/:property'),
369
+ (0, catalog_route_auth_1.RequireScopes)('catalog:curate'),
246
370
  __param(0, (0, common_1.Param)('name')),
247
371
  __param(1, (0, common_1.Param)('property')),
248
372
  __param(2, (0, common_1.Body)()),
@@ -252,12 +376,14 @@ function createCatalogController(path, guards, decorators = []) {
252
376
  ], CatalogController.prototype, "patchProperty", null);
253
377
  __decorate([
254
378
  (0, common_1.Post)('reset'),
379
+ (0, catalog_route_auth_1.RequireScopes)('catalog:curate'),
255
380
  __metadata("design:type", Function),
256
381
  __metadata("design:paramtypes", []),
257
382
  __metadata("design:returntype", Promise)
258
383
  ], CatalogController.prototype, "reset", null);
259
384
  __decorate([
260
385
  (0, common_1.Get)('objects/:name'),
386
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
261
387
  __param(0, (0, common_1.Param)('name')),
262
388
  __param(1, (0, common_1.Query)('page')),
263
389
  __param(2, (0, common_1.Query)('size')),
@@ -271,12 +397,14 @@ function createCatalogController(path, guards, decorators = []) {
271
397
  ], CatalogController.prototype, "objects", null);
272
398
  __decorate([
273
399
  (0, common_1.Get)('query/relations'),
400
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
274
401
  __metadata("design:type", Function),
275
402
  __metadata("design:paramtypes", []),
276
403
  __metadata("design:returntype", void 0)
277
404
  ], CatalogController.prototype, "queryRelations", null);
278
405
  __decorate([
279
406
  (0, common_1.Post)('query'),
407
+ (0, catalog_route_auth_1.RequireScopes)('catalog:admin'),
280
408
  __param(0, (0, common_1.Body)()),
281
409
  __metadata("design:type", Function),
282
410
  __metadata("design:paramtypes", [Object]),
@@ -284,25 +412,30 @@ function createCatalogController(path, guards, decorators = []) {
284
412
  ], CatalogController.prototype, "runQuery", null);
285
413
  __decorate([
286
414
  (0, common_1.Get)('workspace/capabilities'),
415
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
287
416
  __metadata("design:type", Function),
288
417
  __metadata("design:paramtypes", []),
289
418
  __metadata("design:returntype", void 0)
290
419
  ], CatalogController.prototype, "workspaceCapabilities", null);
291
420
  __decorate([
292
421
  (0, common_1.Get)('saved-queries'),
422
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
293
423
  __metadata("design:type", Function),
294
424
  __metadata("design:paramtypes", []),
295
425
  __metadata("design:returntype", void 0)
296
426
  ], CatalogController.prototype, "savedQueries", null);
297
427
  __decorate([
298
428
  (0, common_1.Post)('saved-queries'),
429
+ (0, catalog_route_auth_1.RequireScopes)('catalog:admin'),
299
430
  __param(0, (0, common_1.Body)()),
431
+ __param(1, (0, common_1.Req)()),
300
432
  __metadata("design:type", Function),
301
- __metadata("design:paramtypes", [Object]),
433
+ __metadata("design:paramtypes", [Object, Object]),
302
434
  __metadata("design:returntype", void 0)
303
435
  ], CatalogController.prototype, "saveSavedQuery", null);
304
436
  __decorate([
305
437
  (0, common_1.Get)('saved-queries/:id'),
438
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
306
439
  __param(0, (0, common_1.Param)('id')),
307
440
  __metadata("design:type", Function),
308
441
  __metadata("design:paramtypes", [String]),
@@ -310,21 +443,26 @@ function createCatalogController(path, guards, decorators = []) {
310
443
  ], CatalogController.prototype, "savedQuery", null);
311
444
  __decorate([
312
445
  (0, common_1.Patch)('saved-queries/:id'),
446
+ (0, catalog_route_auth_1.RequireScopes)('catalog:admin'),
313
447
  __param(0, (0, common_1.Param)('id')),
314
448
  __param(1, (0, common_1.Body)()),
449
+ __param(2, (0, common_1.Req)()),
315
450
  __metadata("design:type", Function),
316
- __metadata("design:paramtypes", [String, Object]),
451
+ __metadata("design:paramtypes", [String, Object, Object]),
317
452
  __metadata("design:returntype", void 0)
318
453
  ], CatalogController.prototype, "patchSavedQuery", null);
319
454
  __decorate([
320
455
  (0, common_1.Delete)('saved-queries/:id'),
456
+ (0, catalog_route_auth_1.RequireScopes)('catalog:curate'),
321
457
  __param(0, (0, common_1.Param)('id')),
458
+ __param(1, (0, common_1.Req)()),
322
459
  __metadata("design:type", Function),
323
- __metadata("design:paramtypes", [String]),
460
+ __metadata("design:paramtypes", [String, Object]),
324
461
  __metadata("design:returntype", void 0)
325
462
  ], CatalogController.prototype, "removeSavedQuery", null);
326
463
  __decorate([
327
464
  (0, common_1.Post)('saved-queries/:id/run'),
465
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
328
466
  __param(0, (0, common_1.Param)('id')),
329
467
  __param(1, (0, common_1.Body)()),
330
468
  __metadata("design:type", Function),
@@ -333,6 +471,7 @@ function createCatalogController(path, guards, decorators = []) {
333
471
  ], CatalogController.prototype, "runSavedQuery", null);
334
472
  __decorate([
335
473
  (0, common_1.Get)('saved-queries/:id/export.csv'),
474
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
336
475
  __param(0, (0, common_1.Param)('id')),
337
476
  __param(1, (0, common_1.Res)({ passthrough: true })),
338
477
  __metadata("design:type", Function),
@@ -341,19 +480,23 @@ function createCatalogController(path, guards, decorators = []) {
341
480
  ], CatalogController.prototype, "exportSavedQuery", null);
342
481
  __decorate([
343
482
  (0, common_1.Get)('dashboards'),
483
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
344
484
  __metadata("design:type", Function),
345
485
  __metadata("design:paramtypes", []),
346
486
  __metadata("design:returntype", void 0)
347
487
  ], CatalogController.prototype, "dashboards", null);
348
488
  __decorate([
349
489
  (0, common_1.Post)('dashboards'),
490
+ (0, catalog_route_auth_1.RequireScopes)('catalog:curate'),
350
491
  __param(0, (0, common_1.Body)()),
492
+ __param(1, (0, common_1.Req)()),
351
493
  __metadata("design:type", Function),
352
- __metadata("design:paramtypes", [Object]),
494
+ __metadata("design:paramtypes", [Object, Object]),
353
495
  __metadata("design:returntype", void 0)
354
496
  ], CatalogController.prototype, "createDashboard", null);
355
497
  __decorate([
356
498
  (0, common_1.Get)('dashboards/:id'),
499
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
357
500
  __param(0, (0, common_1.Param)('id')),
358
501
  __metadata("design:type", Function),
359
502
  __metadata("design:paramtypes", [String]),
@@ -361,27 +504,33 @@ function createCatalogController(path, guards, decorators = []) {
361
504
  ], CatalogController.prototype, "dashboard", null);
362
505
  __decorate([
363
506
  (0, common_1.Patch)('dashboards/:id'),
507
+ (0, catalog_route_auth_1.RequireScopes)('catalog:curate'),
364
508
  __param(0, (0, common_1.Param)('id')),
365
509
  __param(1, (0, common_1.Body)()),
510
+ __param(2, (0, common_1.Req)()),
366
511
  __metadata("design:type", Function),
367
- __metadata("design:paramtypes", [String, Object]),
512
+ __metadata("design:paramtypes", [String, Object, Object]),
368
513
  __metadata("design:returntype", void 0)
369
514
  ], CatalogController.prototype, "patchDashboard", null);
370
515
  __decorate([
371
516
  (0, common_1.Delete)('dashboards/:id'),
517
+ (0, catalog_route_auth_1.RequireScopes)('catalog:curate'),
372
518
  __param(0, (0, common_1.Param)('id')),
519
+ __param(1, (0, common_1.Req)()),
373
520
  __metadata("design:type", Function),
374
- __metadata("design:paramtypes", [String]),
521
+ __metadata("design:paramtypes", [String, Object]),
375
522
  __metadata("design:returntype", void 0)
376
523
  ], CatalogController.prototype, "removeDashboard", null);
377
524
  __decorate([
378
525
  (0, common_1.Get)('embed'),
526
+ (0, catalog_route_auth_1.RequireScopes)('catalog:embed'),
379
527
  __metadata("design:type", Function),
380
528
  __metadata("design:paramtypes", []),
381
529
  __metadata("design:returntype", void 0)
382
530
  ], CatalogController.prototype, "embeddable", null);
383
531
  __decorate([
384
532
  (0, common_1.Get)('embed/dashboards/:id'),
533
+ (0, catalog_route_auth_1.RequireScopes)('catalog:embed'),
385
534
  __param(0, (0, common_1.Param)('id')),
386
535
  __metadata("design:type", Function),
387
536
  __metadata("design:paramtypes", [String]),
@@ -389,6 +538,7 @@ function createCatalogController(path, guards, decorators = []) {
389
538
  ], CatalogController.prototype, "embedDashboard", null);
390
539
  __decorate([
391
540
  (0, common_1.Get)('embed/charts/:id'),
541
+ (0, catalog_route_auth_1.RequireScopes)('catalog:embed'),
392
542
  __param(0, (0, common_1.Param)('id')),
393
543
  __metadata("design:type", Function),
394
544
  __metadata("design:paramtypes", [String]),
@@ -396,6 +546,7 @@ function createCatalogController(path, guards, decorators = []) {
396
546
  ], CatalogController.prototype, "embedChart", null);
397
547
  __decorate([
398
548
  (0, common_1.Get)('events'),
549
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
399
550
  __param(0, (0, common_1.Query)('event')),
400
551
  __param(1, (0, common_1.Query)('type')),
401
552
  __param(2, (0, common_1.Query)('principal')),
@@ -407,6 +558,7 @@ function createCatalogController(path, guards, decorators = []) {
407
558
  ], CatalogController.prototype, "events", null);
408
559
  __decorate([
409
560
  (0, common_1.Get)('events/traces'),
561
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
410
562
  __param(0, (0, common_1.Query)('type')),
411
563
  __param(1, (0, common_1.Query)('principal')),
412
564
  __param(2, (0, common_1.Query)('event')),
@@ -420,6 +572,7 @@ function createCatalogController(path, guards, decorators = []) {
420
572
  ], CatalogController.prototype, "traceList", null);
421
573
  __decorate([
422
574
  (0, common_1.Get)('events/traces/:id'),
575
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
423
576
  __param(0, (0, common_1.Param)('id')),
424
577
  __metadata("design:type", Function),
425
578
  __metadata("design:paramtypes", [String]),
@@ -427,6 +580,7 @@ function createCatalogController(path, guards, decorators = []) {
427
580
  ], CatalogController.prototype, "trace", null);
428
581
  __decorate([
429
582
  (0, common_1.Get)('objects/:name/snapshots'),
583
+ (0, catalog_route_auth_1.RequireScopes)('catalog:read'),
430
584
  __param(0, (0, common_1.Param)('name')),
431
585
  __metadata("design:type", Function),
432
586
  __metadata("design:paramtypes", [String]),
@@ -447,6 +601,28 @@ function createCatalogController(path, guards, decorators = []) {
447
601
  }
448
602
  return CatalogController;
449
603
  }
604
+ /**
605
+ * Who to record a workspace change against.
606
+ *
607
+ * The host's resolved principal wins over anything the body claimed, and that
608
+ * order is the whole point: a `createdBy` in a request body is a name the caller
609
+ * chose for itself, and letting it beat the principal a guard authenticated
610
+ * would make the audit trail's actor column a free-text field. It is still
611
+ * honoured when there is no principal, because this library does not resolve
612
+ * one — see the enforcement note in `catalog.principal.ts` — and a host that has
613
+ * not wired a guard yet is better served by "console" than by nothing.
614
+ *
615
+ * `principal.id` rather than `applicationId`, matching every other event on this
616
+ * channel: for a delegated caller that string carries the person inside it, and
617
+ * dropping to the application half would attribute a person's decision to the
618
+ * console they used.
619
+ */
620
+ function actorOf(request, claimed) {
621
+ const resolved = request?.principal?.id?.trim();
622
+ if (resolved)
623
+ return resolved;
624
+ return claimed?.trim() || 'console';
625
+ }
450
626
  /**
451
627
  * `?outcome=failed`, `?outcome=failed,incomplete`, `?outcome=a&outcome=b`.
452
628
  *
@@ -94,13 +94,24 @@ export interface CatalogEnvironment {
94
94
  */
95
95
  rank: number;
96
96
  /**
97
- * Whether this environment refuses changes that did not arrive as a reviewed
98
- * promotion.
97
+ * A declaration that this environment is one where changes are supposed to
98
+ * arrive as a reviewed promotion. True for production.
99
99
  *
100
- * True for production. It does not lock the environment an operator can
101
- * still fix something by hand it means the API demands the explicit
102
- * confirmation described on {@link CatalogPromotionApproval} rather than
103
- * accepting a plan somebody generated in another tab ten minutes ago.
100
+ * **Advisory. Nothing in this library refuses anything because of it.** No
101
+ * guard, no store and no plan reads this field; the only code that does is the
102
+ * console, which paints the environment switcher amber so that "am I about to
103
+ * do this to production" is answerable at a glance. That is worth having and
104
+ * it is worth not overstating — a flag documented as an enforcement point,
105
+ * enforced nowhere, is worse than no flag, because it invites a host to
106
+ * believe the refusal already exists.
107
+ *
108
+ * Kept rather than removed because the enforcement it describes cannot live
109
+ * here. The library has no apply endpoint and no opinion about who may approve
110
+ * what — see {@link CatalogPromotionPlan.fingerprint} for the same division —
111
+ * so the refusal belongs to whatever host exposes promotion over HTTP. This
112
+ * field is how that host learns which environments to demand
113
+ * {@link CatalogPromotionApproval} for, without every deployment restating its
114
+ * own list of environment names in a guard.
104
115
  */
105
116
  protected: boolean;
106
117
  }
@@ -173,17 +184,40 @@ export declare function catalogDatabaseNameFor(base: string, environmentId: Cata
173
184
  * It stops being enough the moment anyone asks a governance question across
174
185
  * environments — "everything this person did this week" has to be answerable
175
186
  * without the reader having to remember which of three lists they are looking
176
- * at — so the environment is stamped onto every event as it leaves its store.
187
+ * at.
177
188
  *
178
189
  * Stamped on read rather than stored in a column on purpose: a stored column
179
190
  * can be wrong, because nothing in the database stops a row in the production
180
191
  * table saying `environment: "dev"`. A value derived from which connection the
181
192
  * row was read through cannot be.
193
+ *
194
+ * Which is also why the stamping does not happen here, and cannot. This package
195
+ * knows what an environment *is*; it has no connection and no scope, so it can
196
+ * only supply the shape and the function. The stamp is applied by the one reader
197
+ * that resolved an environment in order to do the read at all —
198
+ * `RoutingWorkspaceStore.listEvents` in the MikroORM store package, which widens
199
+ * its return type to this intersection. A read that went straight to a store,
200
+ * with no environment resolved anywhere above it, is *not* stamped, and that is
201
+ * the honest answer rather than a gap: nothing in that call knew which world it
202
+ * was reading, so anything written into the field would be a guess.
182
203
  */
183
204
  export interface EnvironmentStampedAuditEvent {
184
205
  environment: CatalogEnvironmentId;
185
206
  }
186
- /** Stamps a batch of events with the environment they were read from. */
207
+ /**
208
+ * Stamps a batch of events with the environment they were read from.
209
+ *
210
+ * A separate function taking the id as an argument rather than a method
211
+ * somewhere, so the caller has to have the environment in hand to call it. There
212
+ * is no default and nothing to look up — the id comes from the same resolution
213
+ * that chose the connection, which is what makes the stamp a fact about the read
214
+ * rather than a label somebody attached afterwards.
215
+ *
216
+ * Generic over the event rather than typed to `CatalogAuditEvent`, which lives
217
+ * in `catalog.workspace.ts` and would make this file depend on the workspace
218
+ * vocabulary to say something true of any row. Nothing here needs to know more
219
+ * about an event than that it is an object.
220
+ */
187
221
  export declare function stampEnvironment<T>(environment: CatalogEnvironmentId, events: readonly T[]): Array<T & EnvironmentStampedAuditEvent>;
188
222
  /** What a promotion is allowed to carry. */
189
223
  export declare const PROMOTABLE_KINDS: readonly ["objectType", "transform", "workflow", "connector"];
@@ -375,9 +409,22 @@ export interface CatalogPromotionPlan {
375
409
  * The point of a preview is that somebody read it. Without a fingerprint the
376
410
  * apply call is a fresh promotion that happens to have been preceded by a
377
411
  * preview, and anything that changed in between — a colleague editing the
378
- * transform, a connector deleted — goes in unreviewed. The apply endpoint
379
- * demands this value back, recomputes the plan, and refuses if the two
380
- * differ. That is what turns "we show a diff" into "you approved this diff".
412
+ * transform, a connector deleted — goes in unreviewed.
413
+ *
414
+ * **The check is the host's, and this library does not perform it.** There is
415
+ * no apply endpoint here: `applyPromotion` is exported for a host to call, and
416
+ * it does not recompute the plan or compare anything to this value. The
417
+ * expectation, stated the same way where the apply lives, is that the caller
418
+ * re-runs {@link planPromotion} immediately before applying and confirms the
419
+ * fingerprint still matches what the reviewer approved. It lives with the
420
+ * caller because it is a policy decision about who may approve what, and
421
+ * `applyPromotion` is the mechanism rather than the policy.
422
+ *
423
+ * So this field is what makes that check *possible* — a stable name for one
424
+ * reviewed set of changes, computed from the effect and not the clock — and
425
+ * not, on its own, the check. A host that carries the value from preview to
426
+ * apply and never compares it has a promotion nobody approved, and nothing in
427
+ * this package will say so.
381
428
  */
382
429
  fingerprint: string;
383
430
  }
@@ -394,7 +441,13 @@ export interface CatalogPromotionPlan {
394
441
  * which environment the row belongs to.
395
442
  */
396
443
  export declare const PROMOTION_AUDIT_EVENT = "promotion.applied";
397
- /** What an apply call must present to prove which plan was approved. */
444
+ /**
445
+ * What a host's apply call asks for, to prove which plan was approved.
446
+ *
447
+ * A shape rather than a check — see {@link CatalogPromotionPlan.fingerprint} for
448
+ * why the comparison lives with the caller. Written down here so that every host
449
+ * demands the same two things and an audit row means the same thing in each.
450
+ */
398
451
  export interface CatalogPromotionApproval {
399
452
  fingerprint: string;
400
453
  /** Free text the operator typed. Recorded in the audit trail, never parsed. */
@@ -216,7 +216,20 @@ function durableKeyspaceFor(environmentId, prefix = 'catalog') {
216
216
  function catalogDatabaseNameFor(base, environmentId) {
217
217
  return `${base}_${assertEnvironmentId(environmentId)}`;
218
218
  }
219
- /** Stamps a batch of events with the environment they were read from. */
219
+ /**
220
+ * Stamps a batch of events with the environment they were read from.
221
+ *
222
+ * A separate function taking the id as an argument rather than a method
223
+ * somewhere, so the caller has to have the environment in hand to call it. There
224
+ * is no default and nothing to look up — the id comes from the same resolution
225
+ * that chose the connection, which is what makes the stamp a fact about the read
226
+ * rather than a label somebody attached afterwards.
227
+ *
228
+ * Generic over the event rather than typed to `CatalogAuditEvent`, which lives
229
+ * in `catalog.workspace.ts` and would make this file depend on the workspace
230
+ * vocabulary to say something true of any row. Nothing here needs to know more
231
+ * about an event than that it is an object.
232
+ */
220
233
  function stampEnvironment(environment, events) {
221
234
  return events.map((event) => ({ ...event, environment }));
222
235
  }