@dudousxd/nestjs-catalog 0.5.0 → 0.7.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
  *
@@ -31,6 +31,23 @@ export declare function CatalogType(options?: CatalogTypeOptions): ClassDecorato
31
31
  * Enriches one property. Everything it sets is tier 0 — the overlay can
32
32
  * override any of it at runtime without a migration, which is precisely why
33
33
  * these live in metadata rather than in the column definition.
34
+ *
35
+ * **This is also how a relation is enriched, and there is deliberately no
36
+ * `@CatalogRelation`.** The metadata is keyed by property name, and a
37
+ * `@ManyToOne` is a property; the registry looks the options up before it
38
+ * decides whether the field is a scalar or a link, so
39
+ * `@CatalogProperty({ displayName: 'Home base' })` on `Mvr.base` labels the link
40
+ * exactly as it labels a column. A second decorator would be a synonym for this
41
+ * one.
42
+ *
43
+ * A decorator that declared a relation *outright* — target, kind, join column —
44
+ * was considered and rejected twice over. Everything an ORM models is already
45
+ * derived, and a hand-written line that could restate it is a line that can
46
+ * disagree with the schema, which is the one thing this model does not allow.
47
+ * And the links an ORM genuinely cannot see are, in practice, the ones that
48
+ * cross applications: neither side's ORM holds both ends, so no decorator in
49
+ * either codebase can assert them. That is a curation act in the console, and it
50
+ * needs a route that does not exist yet.
34
51
  */
35
52
  export declare function CatalogProperty(options?: CatalogPropertyOptions): PropertyDecorator;
36
53
  export declare function readTypeOptions(target: unknown): CatalogTypeOptions;
@@ -27,6 +27,23 @@ function CatalogType(options = {}) {
27
27
  * Enriches one property. Everything it sets is tier 0 — the overlay can
28
28
  * override any of it at runtime without a migration, which is precisely why
29
29
  * these live in metadata rather than in the column definition.
30
+ *
31
+ * **This is also how a relation is enriched, and there is deliberately no
32
+ * `@CatalogRelation`.** The metadata is keyed by property name, and a
33
+ * `@ManyToOne` is a property; the registry looks the options up before it
34
+ * decides whether the field is a scalar or a link, so
35
+ * `@CatalogProperty({ displayName: 'Home base' })` on `Mvr.base` labels the link
36
+ * exactly as it labels a column. A second decorator would be a synonym for this
37
+ * one.
38
+ *
39
+ * A decorator that declared a relation *outright* — target, kind, join column —
40
+ * was considered and rejected twice over. Everything an ORM models is already
41
+ * derived, and a hand-written line that could restate it is a line that can
42
+ * disagree with the schema, which is the one thing this model does not allow.
43
+ * And the links an ORM genuinely cannot see are, in practice, the ones that
44
+ * cross applications: neither side's ORM holds both ends, so no decorator in
45
+ * either codebase can assert them. That is a curation act in the console, and it
46
+ * needs a route that does not exist yet.
30
47
  */
31
48
  function CatalogProperty(options = {}) {
32
49
  return (target, propertyKey) => {