@camunda/ccma-shared-types 5.0.0 → 7.0.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.
Files changed (100) hide show
  1. package/dist/_tests/approvals/dns-subdomain.spec.d.ts +1 -0
  2. package/dist/_tests/approvals/expected-value.spec.d.ts +1 -0
  3. package/dist/_tests/charts/chart-query.spec.d.ts +1 -0
  4. package/dist/_tests/constants/customer-hardware-sizing.const.spec.d.ts +1 -0
  5. package/dist/_tests/dashboards/definition.spec.d.ts +1 -0
  6. package/dist/_tests/dtos/ClusterPlanWhiteList.spec.d.ts +1 -0
  7. package/dist/_tests/utils/elasticsearchPlanPath.utils.spec.d.ts +1 -0
  8. package/dist/_tests/utils/upgradeGateReason.utils.spec.d.ts +1 -0
  9. package/dist/approvals/dns-subdomain.d.ts +12 -0
  10. package/dist/approvals/dns-subdomain.js +16 -0
  11. package/dist/approvals/dns-subdomain.js.map +7 -0
  12. package/dist/approvals/expected-value.d.ts +33 -0
  13. package/dist/approvals/expected-value.js +26 -0
  14. package/dist/approvals/expected-value.js.map +7 -0
  15. package/dist/approvals/policy.js +1 -1
  16. package/dist/approvals/policy.js.map +2 -2
  17. package/dist/approvals/use-cases.const.d.ts +96 -0
  18. package/dist/approvals/use-cases.const.js +59 -2
  19. package/dist/approvals/use-cases.const.js.map +2 -2
  20. package/dist/charts/chart-metrics.d.ts +35 -0
  21. package/dist/charts/chart-metrics.js +188 -0
  22. package/dist/charts/chart-metrics.js.map +7 -0
  23. package/dist/charts/chart-query.d.ts +55 -0
  24. package/dist/charts/chart-query.js +376 -0
  25. package/dist/charts/chart-query.js.map +7 -0
  26. package/dist/charts/chart-spec.d.ts +103 -0
  27. package/dist/charts/chart-spec.js +79 -0
  28. package/dist/charts/chart-spec.js.map +7 -0
  29. package/dist/charts/index.d.ts +3 -0
  30. package/dist/charts/index.js +59 -0
  31. package/dist/charts/index.js.map +7 -0
  32. package/dist/constants/customer-hardware-sizing.const.d.ts +124 -0
  33. package/dist/constants/customer-hardware-sizing.const.js +266 -0
  34. package/dist/constants/customer-hardware-sizing.const.js.map +7 -0
  35. package/dist/constants/version-gates.const.d.ts +12 -0
  36. package/dist/constants/version-gates.const.js +2 -0
  37. package/dist/constants/version-gates.const.js.map +2 -2
  38. package/dist/dashboards/definition.d.ts +86 -0
  39. package/dist/dashboards/definition.js +137 -0
  40. package/dist/dashboards/definition.js.map +7 -0
  41. package/dist/dtos/Annotation.dto.d.ts +1 -1
  42. package/dist/dtos/Annotation.dto.js +10 -2
  43. package/dist/dtos/Annotation.dto.js.map +2 -2
  44. package/dist/dtos/ApprovalJob.dto.d.ts +33 -3
  45. package/dist/dtos/ApprovalJob.dto.js.map +2 -2
  46. package/dist/dtos/ClusterPlanTypeChange.dto.d.ts +23 -0
  47. package/dist/dtos/ClusterPlanTypeChange.dto.js +1 -0
  48. package/dist/dtos/ClusterPlanTypeChange.dto.js.map +7 -0
  49. package/dist/dtos/CommonCluster.dto.d.ts +5 -0
  50. package/dist/dtos/ControlV2.dto.d.ts +17 -0
  51. package/dist/dtos/Dashboard.dto.d.ts +336 -0
  52. package/dist/dtos/Dashboard.dto.js +70 -0
  53. package/dist/dtos/Dashboard.dto.js.map +7 -0
  54. package/dist/dtos/EarlyAccessFeature.dto.d.ts +1 -0
  55. package/dist/dtos/FeatureFlags.dto.d.ts +96 -54
  56. package/dist/dtos/Hdp.dto.d.ts +5 -88
  57. package/dist/dtos/Organization.dto.d.ts +21 -0
  58. package/dist/dtos/Organization.dto.js.map +1 -1
  59. package/dist/dtos/SalesPlan.dto.d.ts +18 -0
  60. package/dist/dtos/SalesPlan.dto.js +18 -0
  61. package/dist/dtos/SalesPlan.dto.js.map +2 -2
  62. package/dist/dtos/UpgradeReadiness.dto.d.ts +32 -0
  63. package/dist/dtos/UpgradeReadiness.dto.js +1 -0
  64. package/dist/dtos/UpgradeReadiness.dto.js.map +7 -0
  65. package/dist/enums/SystemAuditEntity.enum.d.ts +5 -2
  66. package/dist/enums/SystemAuditEntity.enum.js +3 -0
  67. package/dist/enums/SystemAuditEntity.enum.js.map +2 -2
  68. package/dist/index.d.ts +18 -5
  69. package/dist/index.js +192 -1
  70. package/dist/index.js.map +2 -2
  71. package/dist/permissions/accounts.permissions.d.ts +12 -0
  72. package/dist/permissions/accounts.permissions.js +11 -0
  73. package/dist/permissions/accounts.permissions.js.map +2 -2
  74. package/dist/permissions/combo.js +5 -0
  75. package/dist/permissions/combo.js.map +2 -2
  76. package/dist/search/compiler.d.ts +20 -0
  77. package/dist/search/compiler.js +48 -11
  78. package/dist/search/compiler.js.map +2 -2
  79. package/dist/search/index.d.ts +1 -1
  80. package/dist/search/index.js.map +2 -2
  81. package/dist/search/organization-search-fields.d.ts +5 -2
  82. package/dist/search/organization-search-fields.js +9 -1
  83. package/dist/search/organization-search-fields.js.map +2 -2
  84. package/dist/search/parser.d.ts +3 -0
  85. package/dist/search/parser.js +3 -1
  86. package/dist/search/parser.js.map +2 -2
  87. package/dist/types/ClusterBulkOperation.type.d.ts +14 -0
  88. package/dist/types/ClusterPlanFields.type.js +74 -74
  89. package/dist/types/ClusterPlanFields.type.js.map +2 -2
  90. package/dist/utils/elasticsearchPlanPath.utils.d.ts +20 -0
  91. package/dist/utils/elasticsearchPlanPath.utils.js +32 -0
  92. package/dist/utils/elasticsearchPlanPath.utils.js.map +7 -0
  93. package/dist/utils/qa-user.utils.d.ts +14 -0
  94. package/dist/utils/qa-user.utils.js +14 -0
  95. package/dist/utils/qa-user.utils.js.map +7 -0
  96. package/dist/utils/qa-user.utils.spec.d.ts +1 -0
  97. package/dist/utils/upgradeGateReason.utils.d.ts +8 -0
  98. package/dist/utils/upgradeGateReason.utils.js +23 -0
  99. package/dist/utils/upgradeGateReason.utils.js.map +7 -0
  100. package/package.json +1 -1
@@ -0,0 +1,137 @@
1
+ import {
2
+ DASHBOARD_DEFINITION_BYTES_MAX,
3
+ DASHBOARD_DEFINITION_VERSION,
4
+ DASHBOARD_GRID_COLUMNS
5
+ } from "../dtos/Dashboard.dto.js";
6
+ const DASHBOARD_TILE_DEFAULT_SIZE = {
7
+ "fleet-query": { w: 12, h: 8 },
8
+ annotations: { w: 6, h: 6 },
9
+ "cluster-detail": { w: 4, h: 6 },
10
+ "org-detail": { w: 4, h: 6 },
11
+ heading: { w: 12, h: 1 },
12
+ note: { w: 4, h: 3 },
13
+ link: { w: 3, h: 2 },
14
+ chart: { w: 6, h: 6 }
15
+ };
16
+ const CLUSTER_CARD_SIZE = {
17
+ identity: { w: 4, h: 5 },
18
+ "component-status": { w: 4, h: 7 },
19
+ datacenter: { w: 4, h: 5 },
20
+ generation: { w: 4, h: 6 },
21
+ plan: { w: 4, h: 7 },
22
+ "backup-encryption": { w: 4, h: 5 },
23
+ "settings-flags": { w: 4, h: 13 },
24
+ "external-idp": { w: 4, h: 11 },
25
+ "data-filters": { w: 4, h: 6 },
26
+ "auto-pause": { w: 4, h: 6 },
27
+ "change-history": { w: 4, h: 4 },
28
+ "disk-usage": { w: 6, h: 7 }
29
+ };
30
+ const ORG_CARD_SIZE = {
31
+ summary: { w: 12, h: 3 },
32
+ details: { w: 8, h: 8 },
33
+ "sales-plan": { w: 4, h: 5 },
34
+ "queued-operations": { w: 6, h: 5 },
35
+ clusters: { w: 4, h: 7 },
36
+ "managed-llm": { w: 4, h: 6 },
37
+ "recent-activity": { w: 4, h: 6 }
38
+ };
39
+ const dashboardTileDefaultSize = (tile) => {
40
+ switch (tile.kind) {
41
+ case "cluster-detail":
42
+ return CLUSTER_CARD_SIZE[tile.query.card] ?? DASHBOARD_TILE_DEFAULT_SIZE[tile.kind];
43
+ case "org-detail":
44
+ return ORG_CARD_SIZE[tile.query.card] ?? DASHBOARD_TILE_DEFAULT_SIZE[tile.kind];
45
+ default:
46
+ return DASHBOARD_TILE_DEFAULT_SIZE[tile.kind];
47
+ }
48
+ };
49
+ const PATH_PROBE_ORIGIN = "https://dashboard-link.invalid";
50
+ const isSafeDashboardLink = (url) => {
51
+ if (url.length === 0 || /[\u0000-\u0020\u007f]/.test(url)) return false;
52
+ try {
53
+ if (url.startsWith("/")) {
54
+ return !url.startsWith("//") && !url.startsWith("/\\") && new URL(url, PATH_PROBE_ORIGIN).origin === PATH_PROBE_ORIGIN;
55
+ }
56
+ if (!/^https:\/\//i.test(url)) return false;
57
+ const parsed = new URL(url);
58
+ return parsed.protocol === "https:" && parsed.hostname.length > 0;
59
+ } catch {
60
+ return false;
61
+ }
62
+ };
63
+ const dashboardDefinitionBytes = (definition) => new TextEncoder().encode(JSON.stringify(definition)).length;
64
+ const dashboardDefinitionFits = (definition) => dashboardDefinitionBytes(definition) <= DASHBOARD_DEFINITION_BYTES_MAX;
65
+ const emptyDashboardDefinition = () => ({
66
+ schemaVersion: DASHBOARD_DEFINITION_VERSION,
67
+ tiles: []
68
+ });
69
+ const upgradeDashboardDefinition = (stored) => {
70
+ switch (stored.schemaVersion) {
71
+ // Versions 2 and 3 only added kinds of tiles, so every tile of an older
72
+ // version is a tile of version 3.
73
+ case 1:
74
+ case 2:
75
+ return { ...stored, schemaVersion: 3 };
76
+ case 3:
77
+ return stored;
78
+ default:
79
+ return void 0;
80
+ }
81
+ };
82
+ const isTileV1 = (tile) => tile.kind === "fleet-query" || tile.kind === "annotations";
83
+ const isTileV2 = (tile) => tile.kind !== "chart";
84
+ const toStoredDashboardDefinition = (definition) => {
85
+ const tiles = definition.tiles;
86
+ if (tiles.every(isTileV1)) {
87
+ return { schemaVersion: 1, tiles };
88
+ }
89
+ if (tiles.every(isTileV2)) {
90
+ return { schemaVersion: 2, tiles };
91
+ }
92
+ return definition;
93
+ };
94
+ const placeNewTile = (tiles, size) => {
95
+ const w = Math.min(size.w, DASHBOARD_GRID_COLUMNS);
96
+ const bottom = tiles.reduce(
97
+ (lowest, tile) => Math.max(lowest, tile.layout.y + tile.layout.h),
98
+ 0
99
+ );
100
+ if (tiles.length > 0) {
101
+ const rowTop = Math.max(...tiles.map((tile) => tile.layout.y));
102
+ const right = Math.max(
103
+ ...tiles.filter((tile) => tile.layout.y === rowTop).map((tile) => tile.layout.x + tile.layout.w)
104
+ );
105
+ const candidate = { x: right, y: rowTop, w, h: size.h };
106
+ if (right + w <= DASHBOARD_GRID_COLUMNS && tiles.every((tile) => !tilesOverlap(tile.layout, candidate))) {
107
+ return candidate;
108
+ }
109
+ }
110
+ return { x: 0, y: bottom, w, h: size.h };
111
+ };
112
+ const placeNewTiles = (tiles, added) => {
113
+ const placed = [];
114
+ for (const tile of added) {
115
+ const layout = placeNewTile(
116
+ [...tiles, ...placed],
117
+ dashboardTileDefaultSize(tile)
118
+ );
119
+ placed.push({ ...tile, layout });
120
+ }
121
+ return placed;
122
+ };
123
+ const tilesOverlap = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
124
+ export {
125
+ DASHBOARD_TILE_DEFAULT_SIZE,
126
+ dashboardDefinitionBytes,
127
+ dashboardDefinitionFits,
128
+ dashboardTileDefaultSize,
129
+ emptyDashboardDefinition,
130
+ isSafeDashboardLink,
131
+ placeNewTile,
132
+ placeNewTiles,
133
+ tilesOverlap,
134
+ toStoredDashboardDefinition,
135
+ upgradeDashboardDefinition
136
+ };
137
+ //# sourceMappingURL=definition.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/dashboards/definition.ts"],
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport type {\n DashboardClusterCard,\n DashboardDefinition,\n DashboardDefinitionV1,\n DashboardDefinitionV2,\n DashboardNewTile,\n DashboardOrgCard,\n DashboardTile,\n DashboardTileKind,\n DashboardTileLayout,\n DashboardTileV1,\n DashboardTileV2,\n StoredDashboardDefinition,\n} from '../dtos/Dashboard.dto.js'\nimport {\n DASHBOARD_DEFINITION_BYTES_MAX,\n DASHBOARD_DEFINITION_VERSION,\n DASHBOARD_GRID_COLUMNS,\n} from '../dtos/Dashboard.dto.js'\n\n/**\n * The rules of a dashboard definition, as pure functions.\n *\n * control-backend places a tile that a client adds with `placeNewTile`.\n * The grid of control-frontend lays tiles out with its own rules, and reads\n * only `tilesOverlap` from here, so both sides agree on what an overlap is.\n */\n\n/** DASHBOARD_TILE_DEFAULT_SIZE is the size of a new tile of each kind. */\nexport const DASHBOARD_TILE_DEFAULT_SIZE: Record<\n DashboardTileKind,\n { w: number; h: number }\n> = {\n 'fleet-query': { w: 12, h: 8 },\n annotations: { w: 6, h: 6 },\n 'cluster-detail': { w: 4, h: 6 },\n 'org-detail': { w: 4, h: 6 },\n heading: { w: 12, h: 1 },\n note: { w: 4, h: 3 },\n link: { w: 3, h: 2 },\n chart: { w: 6, h: 6 },\n}\n\n/**\n * The size of a new card of a cluster. A card with more rows starts taller,\n * so that a reader does not have to resize every card that they add.\n */\nconst CLUSTER_CARD_SIZE: Record<\n DashboardClusterCard,\n { w: number; h: number }\n> = {\n identity: { w: 4, h: 5 },\n 'component-status': { w: 4, h: 7 },\n datacenter: { w: 4, h: 5 },\n generation: { w: 4, h: 6 },\n plan: { w: 4, h: 7 },\n 'backup-encryption': { w: 4, h: 5 },\n 'settings-flags': { w: 4, h: 13 },\n 'external-idp': { w: 4, h: 11 },\n 'data-filters': { w: 4, h: 6 },\n 'auto-pause': { w: 4, h: 6 },\n 'change-history': { w: 4, h: 4 },\n 'disk-usage': { w: 6, h: 7 },\n}\n\n/** The size of a new card of an organization. */\nconst ORG_CARD_SIZE: Record<DashboardOrgCard, { w: number; h: number }> = {\n summary: { w: 12, h: 3 },\n details: { w: 8, h: 8 },\n 'sales-plan': { w: 4, h: 5 },\n 'queued-operations': { w: 6, h: 5 },\n clusters: { w: 4, h: 7 },\n 'managed-llm': { w: 4, h: 6 },\n 'recent-activity': { w: 4, h: 6 },\n}\n\n/** dashboardTileDefaultSize is the size of a new tile. */\nexport const dashboardTileDefaultSize = (\n tile: DashboardNewTile,\n): { w: number; h: number } => {\n switch (tile.kind) {\n case 'cluster-detail':\n return (\n CLUSTER_CARD_SIZE[tile.query.card] ??\n DASHBOARD_TILE_DEFAULT_SIZE[tile.kind]\n )\n case 'org-detail':\n return (\n ORG_CARD_SIZE[tile.query.card] ?? DASHBOARD_TILE_DEFAULT_SIZE[tile.kind]\n )\n default:\n return DASHBOARD_TILE_DEFAULT_SIZE[tile.kind]\n }\n}\n\n/** An origin that no real address has. A path resolves against it. */\nconst PATH_PROBE_ORIGIN = 'https://dashboard-link.invalid'\n\n/**\n * isSafeDashboardLink reports whether a link tile can hold `url`.\n *\n * A path of Control (`/clusters`) or an `https://` address. A `javascript:`\n * address would run code in the session of each reader, and `//host` leaves\n * Control while it looks like a path, so both are refused.\n *\n * A browser deletes a tab or a line break from an address before it reads\n * it, so `/<tab>/host` is `//host`. Thus a link holds no control character\n * and no space, and a path must resolve to the origin that it started from.\n */\nexport const isSafeDashboardLink = (url: string): boolean => {\n // biome-ignore lint/suspicious/noControlCharactersInRegex: the control characters are what this refuses\n if (url.length === 0 || /[\\u0000-\\u0020\\u007f]/.test(url)) return false\n try {\n if (url.startsWith('/')) {\n return (\n !url.startsWith('//') &&\n !url.startsWith('/\\\\') &&\n new URL(url, PATH_PROBE_ORIGIN).origin === PATH_PROBE_ORIGIN\n )\n }\n // `URL` repairs `https:host` and `https:/host` into a full address, so\n // the raw text must hold the `//` itself.\n if (!/^https:\\/\\//i.test(url)) return false\n const parsed = new URL(url)\n return parsed.protocol === 'https:' && parsed.hostname.length > 0\n } catch {\n return false\n }\n}\n\n/**\n * dashboardDefinitionBytes is the size of a definition as the backend\n * measures it: UTF-8 bytes of its JSON text.\n */\nexport const dashboardDefinitionBytes = (\n definition: DashboardDefinition,\n): number => new TextEncoder().encode(JSON.stringify(definition)).length\n\n/** dashboardDefinitionFits reports whether the backend stores `definition`. */\nexport const dashboardDefinitionFits = (\n definition: DashboardDefinition,\n): boolean =>\n dashboardDefinitionBytes(definition) <= DASHBOARD_DEFINITION_BYTES_MAX\n\n/** emptyDashboardDefinition is the definition of a new dashboard. */\nexport const emptyDashboardDefinition = (): DashboardDefinition => ({\n schemaVersion: DASHBOARD_DEFINITION_VERSION,\n tiles: [],\n})\n\n/**\n * upgradeDashboardDefinition brings a stored definition to the current\n * version.\n *\n * It answers `undefined` for a version that this code does not know. That is\n * a row that NEWER code wrote, for example during a rollout. Do not guess at\n * it, and do not write over it.\n */\nexport const upgradeDashboardDefinition = (stored: {\n schemaVersion?: unknown\n}): DashboardDefinition | undefined => {\n switch (stored.schemaVersion) {\n // Versions 2 and 3 only added kinds of tiles, so every tile of an older\n // version is a tile of version 3.\n case 1:\n case 2:\n return { ...(stored as DashboardDefinition), schemaVersion: 3 }\n case 3:\n return stored as DashboardDefinition\n default:\n return undefined\n }\n}\n\nconst isTileV1 = (tile: DashboardTile): tile is DashboardTileV1 =>\n tile.kind === 'fleet-query' || tile.kind === 'annotations'\n\nconst isTileV2 = (tile: DashboardTile): tile is DashboardTileV2 =>\n tile.kind !== 'chart'\n\n/**\n * toStoredDashboardDefinition is the shape that a write stores: the lowest\n * version that holds the tiles.\n *\n * A pod with older code then still reads the row, during a rollout and after\n * a rollback. Only a dashboard that uses a new kind needs the new version.\n */\nexport const toStoredDashboardDefinition = (\n definition: DashboardDefinition,\n): StoredDashboardDefinition => {\n const tiles = definition.tiles\n if (tiles.every(isTileV1)) {\n return { schemaVersion: 1, tiles } satisfies DashboardDefinitionV1\n }\n if (tiles.every(isTileV2)) {\n return { schemaVersion: 2, tiles } satisfies DashboardDefinitionV2\n }\n return definition\n}\n\n/**\n * placeNewTile gives a tile of `size` the place after the last tile, in\n * reading order.\n *\n * The last row is the row of the tile that starts lowest. The new tile goes\n * to the right of the tiles in that row, if it fits there and covers no cell\n * of another tile. If not, it goes under every tile, at the left edge.\n *\n * A new tile never goes into a gap between two tiles: a reader who added it\n * from another page must find it at the end. Small tiles that a reader adds\n * one after the other fill a row, and do not make a tall column.\n */\nexport const placeNewTile = (\n tiles: readonly Pick<DashboardTile, 'layout'>[],\n size: { w: number; h: number },\n): DashboardTileLayout => {\n const w = Math.min(size.w, DASHBOARD_GRID_COLUMNS)\n const bottom = tiles.reduce(\n (lowest, tile) => Math.max(lowest, tile.layout.y + tile.layout.h),\n 0,\n )\n if (tiles.length > 0) {\n const rowTop = Math.max(...tiles.map((tile) => tile.layout.y))\n const right = Math.max(\n ...tiles\n .filter((tile) => tile.layout.y === rowTop)\n .map((tile) => tile.layout.x + tile.layout.w),\n )\n const candidate = { x: right, y: rowTop, w, h: size.h }\n if (\n right + w <= DASHBOARD_GRID_COLUMNS &&\n tiles.every((tile) => !tilesOverlap(tile.layout, candidate))\n ) {\n return candidate\n }\n }\n return { x: 0, y: bottom, w, h: size.h }\n}\n\n/**\n * placeNewTiles places tiles one after the other, each with\n * `placeNewTile`, and answers the tiles with their layout.\n */\nexport const placeNewTiles = (\n tiles: readonly Pick<DashboardTile, 'layout'>[],\n added: readonly DashboardNewTile[],\n): DashboardTile[] => {\n const placed: DashboardTile[] = []\n for (const tile of added) {\n const layout = placeNewTile(\n [...tiles, ...placed],\n dashboardTileDefaultSize(tile),\n )\n placed.push({ ...tile, layout } as DashboardTile)\n }\n return placed\n}\n\n/**\n * tilesOverlap reports whether two tiles cover one cell of the grid.\n *\n * The backend refuses a definition with an overlap. The grid of the\n * frontend never writes one, so an overlap is a client that is wrong.\n */\nexport const tilesOverlap = (\n a: DashboardTileLayout,\n b: DashboardTileLayout,\n): boolean =>\n a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h\n"],
5
+ "mappings": "AAoBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWA,MAAM,8BAGT;AAAA,EACF,eAAe,EAAE,GAAG,IAAI,GAAG,EAAE;AAAA,EAC7B,aAAa,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EAC1B,kBAAkB,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EAC/B,cAAc,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EAC3B,SAAS,EAAE,GAAG,IAAI,GAAG,EAAE;AAAA,EACvB,MAAM,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACnB,MAAM,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACnB,OAAO,EAAE,GAAG,GAAG,GAAG,EAAE;AACtB;AAMA,MAAM,oBAGF;AAAA,EACF,UAAU,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACvB,oBAAoB,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACjC,YAAY,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACzB,YAAY,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACzB,MAAM,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACnB,qBAAqB,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EAClC,kBAAkB,EAAE,GAAG,GAAG,GAAG,GAAG;AAAA,EAChC,gBAAgB,EAAE,GAAG,GAAG,GAAG,GAAG;AAAA,EAC9B,gBAAgB,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EAC7B,cAAc,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EAC3B,kBAAkB,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EAC/B,cAAc,EAAE,GAAG,GAAG,GAAG,EAAE;AAC7B;AAGA,MAAM,gBAAoE;AAAA,EACxE,SAAS,EAAE,GAAG,IAAI,GAAG,EAAE;AAAA,EACvB,SAAS,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACtB,cAAc,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EAC3B,qBAAqB,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EAClC,UAAU,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACvB,eAAe,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EAC5B,mBAAmB,EAAE,GAAG,GAAG,GAAG,EAAE;AAClC;AAGO,MAAM,2BAA2B,CACtC,SAC6B;AAC7B,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,aACE,kBAAkB,KAAK,MAAM,IAAI,KACjC,4BAA4B,KAAK,IAAI;AAAA,IAEzC,KAAK;AACH,aACE,cAAc,KAAK,MAAM,IAAI,KAAK,4BAA4B,KAAK,IAAI;AAAA,IAE3E;AACE,aAAO,4BAA4B,KAAK,IAAI;AAAA,EAChD;AACF;AAGA,MAAM,oBAAoB;AAanB,MAAM,sBAAsB,CAAC,QAAyB;AAE3D,MAAI,IAAI,WAAW,KAAK,wBAAwB,KAAK,GAAG,EAAG,QAAO;AAClE,MAAI;AACF,QAAI,IAAI,WAAW,GAAG,GAAG;AACvB,aACE,CAAC,IAAI,WAAW,IAAI,KACpB,CAAC,IAAI,WAAW,KAAK,KACrB,IAAI,IAAI,KAAK,iBAAiB,EAAE,WAAW;AAAA,IAE/C;AAGA,QAAI,CAAC,eAAe,KAAK,GAAG,EAAG,QAAO;AACtC,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,WAAO,OAAO,aAAa,YAAY,OAAO,SAAS,SAAS;AAAA,EAClE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,MAAM,2BAA2B,CACtC,eACW,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,UAAU,CAAC,EAAE;AAG3D,MAAM,0BAA0B,CACrC,eAEA,yBAAyB,UAAU,KAAK;AAGnC,MAAM,2BAA2B,OAA4B;AAAA,EAClE,eAAe;AAAA,EACf,OAAO,CAAC;AACV;AAUO,MAAM,6BAA6B,CAAC,WAEJ;AACrC,UAAQ,OAAO,eAAe;AAAA;AAAA;AAAA,IAG5B,KAAK;AAAA,IACL,KAAK;AACH,aAAO,EAAE,GAAI,QAAgC,eAAe,EAAE;AAAA,IAChE,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEA,MAAM,WAAW,CAAC,SAChB,KAAK,SAAS,iBAAiB,KAAK,SAAS;AAE/C,MAAM,WAAW,CAAC,SAChB,KAAK,SAAS;AAST,MAAM,8BAA8B,CACzC,eAC8B;AAC9B,QAAM,QAAQ,WAAW;AACzB,MAAI,MAAM,MAAM,QAAQ,GAAG;AACzB,WAAO,EAAE,eAAe,GAAG,MAAM;AAAA,EACnC;AACA,MAAI,MAAM,MAAM,QAAQ,GAAG;AACzB,WAAO,EAAE,eAAe,GAAG,MAAM;AAAA,EACnC;AACA,SAAO;AACT;AAcO,MAAM,eAAe,CAC1B,OACA,SACwB;AACxB,QAAM,IAAI,KAAK,IAAI,KAAK,GAAG,sBAAsB;AACjD,QAAM,SAAS,MAAM;AAAA,IACnB,CAAC,QAAQ,SAAS,KAAK,IAAI,QAAQ,KAAK,OAAO,IAAI,KAAK,OAAO,CAAC;AAAA,IAChE;AAAA,EACF;AACA,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,SAAS,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,CAAC;AAC7D,UAAM,QAAQ,KAAK;AAAA,MACjB,GAAG,MACA,OAAO,CAAC,SAAS,KAAK,OAAO,MAAM,MAAM,EACzC,IAAI,CAAC,SAAS,KAAK,OAAO,IAAI,KAAK,OAAO,CAAC;AAAA,IAChD;AACA,UAAM,YAAY,EAAE,GAAG,OAAO,GAAG,QAAQ,GAAG,GAAG,KAAK,EAAE;AACtD,QACE,QAAQ,KAAK,0BACb,MAAM,MAAM,CAAC,SAAS,CAAC,aAAa,KAAK,QAAQ,SAAS,CAAC,GAC3D;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO,EAAE,GAAG,GAAG,GAAG,QAAQ,GAAG,GAAG,KAAK,EAAE;AACzC;AAMO,MAAM,gBAAgB,CAC3B,OACA,UACoB;AACpB,QAAM,SAA0B,CAAC;AACjC,aAAW,QAAQ,OAAO;AACxB,UAAM,SAAS;AAAA,MACb,CAAC,GAAG,OAAO,GAAG,MAAM;AAAA,MACpB,yBAAyB,IAAI;AAAA,IAC/B;AACA,WAAO,KAAK,EAAE,GAAG,MAAM,OAAO,CAAkB;AAAA,EAClD;AACA,SAAO;AACT;AAQO,MAAM,eAAe,CAC1B,GACA,MAEA,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE;",
6
+ "names": []
7
+ }
@@ -35,7 +35,7 @@
35
35
  * about to read or correct it. Add a kind that names a PERSON only with those
36
36
  * three answered.
37
37
  */
38
- export declare const ANNOTATION_ENTITY_KINDS: readonly ["cluster", "organization", "cluster-plan", "cluster-plan-type", "generation", "channel", "sales-plan", "k8s-context"];
38
+ export declare const ANNOTATION_ENTITY_KINDS: readonly ["cluster", "organization", "cluster-plan", "cluster-plan-type", "generation", "channel", "sales-plan", "k8s-context", "cluster-modification", "cluster-bulk-operation", "approval", "dashboard"];
39
39
  export type AnnotationEntityKind = (typeof ANNOTATION_ENTITY_KINDS)[number];
40
40
  /**
41
41
  * What a reader sees for each kind, in the singular.
@@ -6,7 +6,11 @@ const ANNOTATION_ENTITY_KINDS = [
6
6
  "generation",
7
7
  "channel",
8
8
  "sales-plan",
9
- "k8s-context"
9
+ "k8s-context",
10
+ "cluster-modification",
11
+ "cluster-bulk-operation",
12
+ "approval",
13
+ "dashboard"
10
14
  ];
11
15
  const ANNOTATION_ENTITY_KIND_LABELS = {
12
16
  cluster: "Cluster",
@@ -16,7 +20,11 @@ const ANNOTATION_ENTITY_KIND_LABELS = {
16
20
  generation: "Generation",
17
21
  channel: "Channel",
18
22
  "sales-plan": "Sales plan",
19
- "k8s-context": "Kubernetes context"
23
+ "k8s-context": "Kubernetes context",
24
+ "cluster-modification": "Cluster modification",
25
+ "cluster-bulk-operation": "Cluster bulk operation",
26
+ approval: "Approval request",
27
+ dashboard: "Dashboard"
20
28
  };
21
29
  const ANNOTATION_KINDS = ["note", "tag"];
22
30
  const ANNOTATION_LOOKUP_MAX_ENTITIES = 3e3;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/dtos/Annotation.dto.ts"],
4
- "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\n/**\n * The wire types of the annotations of the fleet: the notes and the tags that a\n * Camundi attaches to an entity.\n *\n * An annotation adds CONTEXT that the fleet itself does not hold. A cluster row\n * says how much memory it has; it does not say \"this is the playground of the\n * demo team\". A Camundi writes that here, and the next Camundi reads it.\n *\n * This file holds no logic and no dependency, because control-backend and\n * control-frontend both read it. The rules that read these types are in\n * `../annotations/policy.js`, and they are pure for the same reason.\n *\n * The feature is INTERNAL. No annotation reaches a customer, and no route of\n * another service reads one.\n */\n\n/**\n * Every kind of fleet entity that can carry an annotation.\n *\n * A `const` tuple, not an enum, so it is both the union type and the list that\n * validates a request at run time.\n *\n * The kind is a STRING on the row and not a foreign key, because the entity is\n * in a different database: a cluster belongs to console-backend and an\n * organization to accounts-backend. That is also what keeps an annotation after\n * the entity goes away. See `AnnotationDto.entityLabel`.\n *\n * Add a kind here and in `ANNOTATION_ENTITY_KIND_LABELS` below. The two lists\n * are one `Record`, so a kind with no label does not compile.\n *\n * Every kind here is a THING of the fleet, and none of them is a person. That\n * is a rule and not an accident. A `user` kind was in this list and came out\n * again: an annotation outlives its entity on purpose, which is right for a\n * cluster that somebody deleted and wrong for a colleague who left, and this\n * feature has no retention rule, no reaper, and no way for the person written\n * about to read or correct it. Add a kind that names a PERSON only with those\n * three answered.\n */\nexport const ANNOTATION_ENTITY_KINDS = [\n 'cluster',\n 'organization',\n 'cluster-plan',\n 'cluster-plan-type',\n 'generation',\n 'channel',\n 'sales-plan',\n 'k8s-context',\n] as const\n\nexport type AnnotationEntityKind = (typeof ANNOTATION_ENTITY_KINDS)[number]\n\n/**\n * What a reader sees for each kind, in the singular.\n *\n * The label is here and not in the frontend, because the search page shows\n * results of every kind together and the backend writes the same word into a\n * log line. Two spellings of one kind would read as two kinds.\n */\nexport const ANNOTATION_ENTITY_KIND_LABELS: Record<\n AnnotationEntityKind,\n string\n> = {\n cluster: 'Cluster',\n organization: 'Organization',\n 'cluster-plan': 'Cluster plan',\n 'cluster-plan-type': 'Cluster plan type',\n generation: 'Generation',\n channel: 'Channel',\n 'sales-plan': 'Sales plan',\n 'k8s-context': 'Kubernetes context',\n}\n\n/**\n * What an annotation IS: a written note, or a tag out of the dictionary.\n *\n * One row type carries both, because both answer the same question \u2014 what does\n * a Camundi need to know about this entity? \u2014 and both are read, listed and\n * removed the same way. The difference is what the row points at: a note holds\n * `body`, and a tag holds `tag`.\n */\nexport const ANNOTATION_KINDS = ['note', 'tag'] as const\n\nexport type AnnotationKind = (typeof ANNOTATION_KINDS)[number]\n\n/** Who wrote an annotation. The same shape as `ApprovalActor`. */\nexport interface AnnotationActor {\n userId: string\n email: string\n}\n\n/**\n * One tag of the dictionary.\n *\n * A tag is its own row, and not a string on the annotation, for one reason:\n * a Camundi must be able to ask \"what else carries `partner`?\". A string on\n * each row answers that only with a scan, and it drifts: `Partner`, `partner `\n * and `partner` would be three tags that read as one.\n */\nexport interface AnnotationTagDto {\n id: string\n /** What a reader sees. The first spelling that anybody wrote wins. */\n name: string\n /** `name` through `normalizeTagName`. It is the identity of the tag. */\n normalizedName: string\n createdBy: AnnotationActor\n createdAt: string\n /**\n * How many LIVE entities carry this tag.\n *\n * It is counted at read time and never stored, so a removed assignment\n * lowers it with no second write. A list of the dictionary needs it to sort\n * by use, which is how a Camundi finds the tag that the team already uses\n * instead of inventing a second spelling of it.\n */\n usageCount: number\n}\n\n/**\n * One note, or one tag, on one entity.\n *\n * `entityKind` and `entityId` together name the entity. Neither is checked\n * against the fleet: the entity lives in another service, and an annotation\n * must survive its entity. See `entityLabel`.\n */\nexport interface AnnotationDto {\n id: string\n kind: AnnotationKind\n entityKind: AnnotationEntityKind\n entityId: string\n /**\n * What the entity was CALLED when somebody wrote the annotation.\n *\n * This is the only reason a search result stays readable after the entity is\n * deleted: the id of a cluster that no longer exists says nothing, and the\n * fleet can no longer answer what its name was. The label is a copy on\n * purpose, so it can be stale \u2014 it says what the entity was called then, not\n * what it is called now.\n */\n entityLabel?: string\n /** The text of a note. Absent on a tag. */\n body?: string\n /** The tag of a tag annotation. Absent on a note. */\n tag?: AnnotationTagDto\n createdBy: AnnotationActor\n createdAt: string\n updatedAt: string\n /** True when `body` was changed after it was written. */\n edited: boolean\n /**\n * How many times this row was written. A new one carries 1.\n *\n * An edit sends the version that it read, and the route refuses the write\n * when the row moved on since. Without it the tab that somebody left open at\n * breakfast silently replaces the edit they made at lunch, and neither write\n * tells them that one of the two is gone.\n */\n version: number\n}\n\n/** The annotations of one entity, in one answer. */\nexport interface EntityAnnotationsDto {\n entityKind: AnnotationEntityKind\n entityId: string\n notes: AnnotationDto[]\n tags: AnnotationDto[]\n /**\n * True when the entity carries more than `ANNOTATION_ENTITY_MAX` rows, so\n * the two lists above are the newest of them and not all of them.\n *\n * A panel that showed no sign of this would be a page that quietly stops\n * being the whole story, which is worse for documentation than a page that\n * says where the rest is.\n */\n hasMore: boolean\n}\n\n/**\n * What one entity carries, in the answer of a lookup over many entities.\n *\n * A table of 3000 clusters cannot read 3000 panels. The lookup answers the\n * two questions that a row of such a table needs: which tags, and whether a\n * colleague wrote something that a person must read before they act.\n */\nexport interface AnnotationLookupEntryDto {\n entityId: string\n /**\n * The live tag annotations of the entity, oldest first, and at most\n * `ANNOTATION_LOOKUP_TAGS_MAX` of them.\n */\n tags: AnnotationDto[]\n /** How many live tags the entity carries. More than `tags` holds is cut. */\n tagCount: number\n /** How many live notes the entity carries. */\n noteCount: number\n /** The newest live note, so a row can show it with no second read. */\n latestNote?: AnnotationDto\n}\n\n/**\n * The answer of a lookup. It holds ONLY the entities that carry something,\n * so an entity with no entry carries nothing.\n */\nexport interface AnnotationLookupDto {\n entityKind: AnnotationEntityKind\n entries: AnnotationLookupEntryDto[]\n}\n\n/**\n * The most entities that one lookup names.\n *\n * The same number as the largest cluster bulk operation, which is the table\n * that asks the largest question.\n */\nexport const ANNOTATION_LOOKUP_MAX_ENTITIES = 3000\n\n/**\n * The most tags that a lookup answers for ONE entity.\n *\n * An entity can carry any number of tags. Without this cap, one lookup of\n * 3000 entities has no bound on its rows. A row of a table shows a few chips,\n * and `tagCount` tells it how many it did not get.\n */\nexport const ANNOTATION_LOOKUP_TAGS_MAX = 20\n\n/**\n * The most annotations that the read of ONE entity answers with.\n *\n * The read has no page of its own: a panel is a thread that a person scrolls,\n * not a table they walk. But it cannot be unbounded either \u2014 an entity that\n * collects notes for a year would make every load of that page serialize\n * thousands of rows of up to `ANNOTATION_BODY_MAX` characters each. So the\n * answer is the NEWEST rows up to this number, with `hasMore` to say that\n * older ones exist. The search page reads the rest.\n */\nexport const ANNOTATION_ENTITY_MAX = 200\n\n/**\n * The longest note that a route accepts.\n *\n * A note is context for a colleague and not a document. The ceiling keeps one\n * paste of a log file out of every read of the entity page, and a person who\n * needs more has a handbook page to link to.\n */\nexport const ANNOTATION_BODY_MAX = 4000\n\n/** The longest tag name. A tag is a label, so it is short by design. */\nexport const ANNOTATION_TAG_NAME_MAX = 60\n\n/**\n * The longest free text that the search accepts.\n *\n * The VIEW needs it as much as the route: a field with no ceiling lets a\n * person paste more than the route takes, and the answer is then a 400 that\n * reads like a broken search. The field holds them here instead, and a deep\n * link that carries more is cut to this length rather than breaking the page.\n */\nexport const ANNOTATION_SEARCH_MAX_LENGTH = 200\n\n/** Annotations for each page when the caller asks for no number. */\nexport const ANNOTATION_PAGE_SIZE_DEFAULT = 100\n\n/**\n * The most annotations that one page can hold.\n *\n * The view needs this number too: a page that asked for more rows than the\n * service allows would get a 400 instead of rows.\n */\nexport const ANNOTATION_PAGE_SIZE_MAX = 500\n\n/**\n * The identity of a tag, from what a person typed.\n *\n * The case and the outer spaces go, and every run of inner whitespace becomes\n * one hyphen. `Partner`, ` partner ` and `Partner ` are then one tag, and\n * `key account` is `key-account`.\n *\n * Both sides run this: the backend writes the result into `normalizedName` and\n * holds it unique, and the frontend runs it to tell a person, before they\n * press the button, that the tag they are typing already exists.\n */\nexport function normalizeTagName(name: string): string {\n return name.trim().toLowerCase().replace(/\\s+/g, '-')\n}\n"],
5
- "mappings": "AA4CO,MAAM,0BAA0B;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAWO,MAAM,gCAGT;AAAA,EACF,SAAS;AAAA,EACT,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,qBAAqB;AAAA,EACrB,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,cAAc;AAAA,EACd,eAAe;AACjB;AAUO,MAAM,mBAAmB,CAAC,QAAQ,KAAK;AAqIvC,MAAM,iCAAiC;AASvC,MAAM,6BAA6B;AAYnC,MAAM,wBAAwB;AAS9B,MAAM,sBAAsB;AAG5B,MAAM,0BAA0B;AAUhC,MAAM,+BAA+B;AAGrC,MAAM,+BAA+B;AAQrC,MAAM,2BAA2B;AAajC,SAAS,iBAAiB,MAAsB;AACrD,SAAO,KAAK,KAAK,EAAE,YAAY,EAAE,QAAQ,QAAQ,GAAG;AACtD;",
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\n/**\n * The wire types of the annotations of the fleet: the notes and the tags that a\n * Camundi attaches to an entity.\n *\n * An annotation adds CONTEXT that the fleet itself does not hold. A cluster row\n * says how much memory it has; it does not say \"this is the playground of the\n * demo team\". A Camundi writes that here, and the next Camundi reads it.\n *\n * This file holds no logic and no dependency, because control-backend and\n * control-frontend both read it. The rules that read these types are in\n * `../annotations/policy.js`, and they are pure for the same reason.\n *\n * The feature is INTERNAL. No annotation reaches a customer, and no route of\n * another service reads one.\n */\n\n/**\n * Every kind of fleet entity that can carry an annotation.\n *\n * A `const` tuple, not an enum, so it is both the union type and the list that\n * validates a request at run time.\n *\n * The kind is a STRING on the row and not a foreign key, because the entity is\n * in a different database: a cluster belongs to console-backend and an\n * organization to accounts-backend. That is also what keeps an annotation after\n * the entity goes away. See `AnnotationDto.entityLabel`.\n *\n * Add a kind here and in `ANNOTATION_ENTITY_KIND_LABELS` below. The two lists\n * are one `Record`, so a kind with no label does not compile.\n *\n * Every kind here is a THING of the fleet, and none of them is a person. That\n * is a rule and not an accident. A `user` kind was in this list and came out\n * again: an annotation outlives its entity on purpose, which is right for a\n * cluster that somebody deleted and wrong for a colleague who left, and this\n * feature has no retention rule, no reaper, and no way for the person written\n * about to read or correct it. Add a kind that names a PERSON only with those\n * three answered.\n */\nexport const ANNOTATION_ENTITY_KINDS = [\n 'cluster',\n 'organization',\n 'cluster-plan',\n 'cluster-plan-type',\n 'generation',\n 'channel',\n 'sales-plan',\n 'k8s-context',\n 'cluster-modification',\n 'cluster-bulk-operation',\n 'approval',\n 'dashboard',\n] as const\n\nexport type AnnotationEntityKind = (typeof ANNOTATION_ENTITY_KINDS)[number]\n\n/**\n * What a reader sees for each kind, in the singular.\n *\n * The label is here and not in the frontend, because the search page shows\n * results of every kind together and the backend writes the same word into a\n * log line. Two spellings of one kind would read as two kinds.\n */\nexport const ANNOTATION_ENTITY_KIND_LABELS: Record<\n AnnotationEntityKind,\n string\n> = {\n cluster: 'Cluster',\n organization: 'Organization',\n 'cluster-plan': 'Cluster plan',\n 'cluster-plan-type': 'Cluster plan type',\n generation: 'Generation',\n channel: 'Channel',\n 'sales-plan': 'Sales plan',\n 'k8s-context': 'Kubernetes context',\n 'cluster-modification': 'Cluster modification',\n 'cluster-bulk-operation': 'Cluster bulk operation',\n approval: 'Approval request',\n dashboard: 'Dashboard',\n}\n\n/**\n * What an annotation IS: a written note, or a tag out of the dictionary.\n *\n * One row type carries both, because both answer the same question \u2014 what does\n * a Camundi need to know about this entity? \u2014 and both are read, listed and\n * removed the same way. The difference is what the row points at: a note holds\n * `body`, and a tag holds `tag`.\n */\nexport const ANNOTATION_KINDS = ['note', 'tag'] as const\n\nexport type AnnotationKind = (typeof ANNOTATION_KINDS)[number]\n\n/** Who wrote an annotation. The same shape as `ApprovalActor`. */\nexport interface AnnotationActor {\n userId: string\n email: string\n}\n\n/**\n * One tag of the dictionary.\n *\n * A tag is its own row, and not a string on the annotation, for one reason:\n * a Camundi must be able to ask \"what else carries `partner`?\". A string on\n * each row answers that only with a scan, and it drifts: `Partner`, `partner `\n * and `partner` would be three tags that read as one.\n */\nexport interface AnnotationTagDto {\n id: string\n /** What a reader sees. The first spelling that anybody wrote wins. */\n name: string\n /** `name` through `normalizeTagName`. It is the identity of the tag. */\n normalizedName: string\n createdBy: AnnotationActor\n createdAt: string\n /**\n * How many LIVE entities carry this tag.\n *\n * It is counted at read time and never stored, so a removed assignment\n * lowers it with no second write. A list of the dictionary needs it to sort\n * by use, which is how a Camundi finds the tag that the team already uses\n * instead of inventing a second spelling of it.\n */\n usageCount: number\n}\n\n/**\n * One note, or one tag, on one entity.\n *\n * `entityKind` and `entityId` together name the entity. Neither is checked\n * against the fleet: the entity lives in another service, and an annotation\n * must survive its entity. See `entityLabel`.\n */\nexport interface AnnotationDto {\n id: string\n kind: AnnotationKind\n entityKind: AnnotationEntityKind\n entityId: string\n /**\n * What the entity was CALLED when somebody wrote the annotation.\n *\n * This is the only reason a search result stays readable after the entity is\n * deleted: the id of a cluster that no longer exists says nothing, and the\n * fleet can no longer answer what its name was. The label is a copy on\n * purpose, so it can be stale \u2014 it says what the entity was called then, not\n * what it is called now.\n */\n entityLabel?: string\n /** The text of a note. Absent on a tag. */\n body?: string\n /** The tag of a tag annotation. Absent on a note. */\n tag?: AnnotationTagDto\n createdBy: AnnotationActor\n createdAt: string\n updatedAt: string\n /** True when `body` was changed after it was written. */\n edited: boolean\n /**\n * How many times this row was written. A new one carries 1.\n *\n * An edit sends the version that it read, and the route refuses the write\n * when the row moved on since. Without it the tab that somebody left open at\n * breakfast silently replaces the edit they made at lunch, and neither write\n * tells them that one of the two is gone.\n */\n version: number\n}\n\n/** The annotations of one entity, in one answer. */\nexport interface EntityAnnotationsDto {\n entityKind: AnnotationEntityKind\n entityId: string\n notes: AnnotationDto[]\n tags: AnnotationDto[]\n /**\n * True when the entity carries more than `ANNOTATION_ENTITY_MAX` rows, so\n * the two lists above are the newest of them and not all of them.\n *\n * A panel that showed no sign of this would be a page that quietly stops\n * being the whole story, which is worse for documentation than a page that\n * says where the rest is.\n */\n hasMore: boolean\n}\n\n/**\n * What one entity carries, in the answer of a lookup over many entities.\n *\n * A table of 3000 clusters cannot read 3000 panels. The lookup answers the\n * two questions that a row of such a table needs: which tags, and whether a\n * colleague wrote something that a person must read before they act.\n */\nexport interface AnnotationLookupEntryDto {\n entityId: string\n /**\n * The live tag annotations of the entity, oldest first, and at most\n * `ANNOTATION_LOOKUP_TAGS_MAX` of them.\n */\n tags: AnnotationDto[]\n /** How many live tags the entity carries. More than `tags` holds is cut. */\n tagCount: number\n /** How many live notes the entity carries. */\n noteCount: number\n /** The newest live note, so a row can show it with no second read. */\n latestNote?: AnnotationDto\n}\n\n/**\n * The answer of a lookup. It holds ONLY the entities that carry something,\n * so an entity with no entry carries nothing.\n */\nexport interface AnnotationLookupDto {\n entityKind: AnnotationEntityKind\n entries: AnnotationLookupEntryDto[]\n}\n\n/**\n * The most entities that one lookup names.\n *\n * The same number as the largest cluster bulk operation, which is the table\n * that asks the largest question.\n */\nexport const ANNOTATION_LOOKUP_MAX_ENTITIES = 3000\n\n/**\n * The most tags that a lookup answers for ONE entity.\n *\n * An entity can carry any number of tags. Without this cap, one lookup of\n * 3000 entities has no bound on its rows. A row of a table shows a few chips,\n * and `tagCount` tells it how many it did not get.\n */\nexport const ANNOTATION_LOOKUP_TAGS_MAX = 20\n\n/**\n * The most annotations that the read of ONE entity answers with.\n *\n * The read has no page of its own: a panel is a thread that a person scrolls,\n * not a table they walk. But it cannot be unbounded either \u2014 an entity that\n * collects notes for a year would make every load of that page serialize\n * thousands of rows of up to `ANNOTATION_BODY_MAX` characters each. So the\n * answer is the NEWEST rows up to this number, with `hasMore` to say that\n * older ones exist. The search page reads the rest.\n */\nexport const ANNOTATION_ENTITY_MAX = 200\n\n/**\n * The longest note that a route accepts.\n *\n * A note is context for a colleague and not a document. The ceiling keeps one\n * paste of a log file out of every read of the entity page, and a person who\n * needs more has a handbook page to link to.\n */\nexport const ANNOTATION_BODY_MAX = 4000\n\n/** The longest tag name. A tag is a label, so it is short by design. */\nexport const ANNOTATION_TAG_NAME_MAX = 60\n\n/**\n * The longest free text that the search accepts.\n *\n * The VIEW needs it as much as the route: a field with no ceiling lets a\n * person paste more than the route takes, and the answer is then a 400 that\n * reads like a broken search. The field holds them here instead, and a deep\n * link that carries more is cut to this length rather than breaking the page.\n */\nexport const ANNOTATION_SEARCH_MAX_LENGTH = 200\n\n/** Annotations for each page when the caller asks for no number. */\nexport const ANNOTATION_PAGE_SIZE_DEFAULT = 100\n\n/**\n * The most annotations that one page can hold.\n *\n * The view needs this number too: a page that asked for more rows than the\n * service allows would get a 400 instead of rows.\n */\nexport const ANNOTATION_PAGE_SIZE_MAX = 500\n\n/**\n * The identity of a tag, from what a person typed.\n *\n * The case and the outer spaces go, and every run of inner whitespace becomes\n * one hyphen. `Partner`, ` partner ` and `Partner ` are then one tag, and\n * `key account` is `key-account`.\n *\n * Both sides run this: the backend writes the result into `normalizedName` and\n * holds it unique, and the frontend runs it to tell a person, before they\n * press the button, that the tag they are typing already exists.\n */\nexport function normalizeTagName(name: string): string {\n return name.trim().toLowerCase().replace(/\\s+/g, '-')\n}\n"],
5
+ "mappings": "AA4CO,MAAM,0BAA0B;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAWO,MAAM,gCAGT;AAAA,EACF,SAAS;AAAA,EACT,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,qBAAqB;AAAA,EACrB,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,cAAc;AAAA,EACd,eAAe;AAAA,EACf,wBAAwB;AAAA,EACxB,0BAA0B;AAAA,EAC1B,UAAU;AAAA,EACV,WAAW;AACb;AAUO,MAAM,mBAAmB,CAAC,QAAQ,KAAK;AAqIvC,MAAM,iCAAiC;AASvC,MAAM,6BAA6B;AAYnC,MAAM,wBAAwB;AAS9B,MAAM,sBAAsB;AAG5B,MAAM,0BAA0B;AAUhC,MAAM,+BAA+B;AAGrC,MAAM,+BAA+B;AAQrC,MAAM,2BAA2B;AAajC,SAAS,iBAAiB,MAAsB;AACrD,SAAO,KAAK,KAAK,EAAE,YAAY,EAAE,QAAQ,QAAQ,GAAG;AACtD;",
6
6
  "names": []
7
7
  }
@@ -191,9 +191,11 @@ export interface ApprovalAck extends ApprovalActor {
191
191
  * holds, because every other record of the fleet holds an Auth0 `sub`.
192
192
  *
193
193
  * CAUTION: The envelope is a CLAIM of the caller, and not proof. A route that
194
- * acts on it must ask Auth0 again whether `requestedBy` holds the permission
195
- * of the action. `api/admin/approvals` in accounts-backend does that. See the
196
- * note on `M2M_CONTROL_APPROVALS`.
194
+ * acts on it must ask Auth0 again whether `requestedBy` may do the action.
195
+ * That means the permission of the action. For a use case with
196
+ * `openToEveryone`, it means that the person is a Camundi, because no
197
+ * permission runs that action. `api/admin/approvals` in accounts-backend does
198
+ * that. See the note on `M2M_CONTROL_APPROVALS`.
197
199
  */
198
200
  export interface ApprovalM2mActor {
199
201
  /** The id of the approval request, to join the records of both services. */
@@ -454,6 +456,10 @@ export interface ApprovalUseCaseDto {
454
456
  * It is also the permission to SEE a request of this use case, because a
455
457
  * person who cannot act on one has no reason to read it. The person who
456
458
  * opened a request always sees that request.
459
+ *
460
+ * The one exception to "or run" is a use case with `openToEveryone`. It has
461
+ * no approval, so this permission only decides who sees the requests of
462
+ * other people. Any signed-in person can open and run such a request.
457
463
  */
458
464
  requiredPermission: string;
459
465
  /**
@@ -484,4 +490,28 @@ export interface ApprovalUseCaseDto {
484
490
  * reads the user id alone and needs no address.
485
491
  */
486
492
  creatorEmailRequired?: boolean;
493
+ /**
494
+ * A use case with no approval that any signed-in person can open AND run.
495
+ *
496
+ * A request with a floor of zero runs when it is opened, so `canCreate`
497
+ * asks the person to hold `requiredPermission`. This flag lifts that one
498
+ * rule, for an action whose run needs no permission of the person. Set it
499
+ * only when two things hold:
500
+ *
501
+ * - Something else bounds how often a person can ask.
502
+ * - The service that runs the action authorizes the person again at its
503
+ * source, so that nobody can change what is not theirs. The action can
504
+ * touch an organization only when that check lets the person change it.
505
+ *
506
+ * `create-personal-organisation` and the two use cases of customer hardware
507
+ * sizing set it. The two use cases of customer hardware sizing do the second
508
+ * check in two ways:
509
+ *
510
+ * - Creating a plan, and unlinking a plan from one organization, read the
511
+ * role of the person in that organization. Only an OWNER passes.
512
+ * - Deleting a plan reads no role. Console checks that the person created
513
+ * the plan. Accounts reads the organizations that the person is a member
514
+ * of, in any role, and removes the plan from their lists.
515
+ */
516
+ openToEveryone?: boolean;
487
517
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/dtos/ApprovalJob.dto.ts"],
4
- "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\n/**\n * The wire types of the generic approval mechanism.\n *\n * This file holds no logic and no dependency, because three consumers read it:\n * control-backend, control-frontend and cloudbot. The rules that read these\n * types are in `../approvals/policy.js`, and they are pure for the same reason.\n */\n\n/**\n * Every state that an approval request can hold.\n *\n * A `const` tuple, not an enum, so it is both the union type and the list that\n * validates a status filter at run time.\n *\n * The path of a request: `pending` until it reaches the threshold. Then\n * `executing` (automatic mode) or `approved` (explicit mode, which waits for a\n * person to press the button). `executed` or `failed` after the action ran. A\n * request that never reaches the threshold ends `rejected`, `withdrawn` or\n * `expired`.\n */\nexport const APPROVAL_JOB_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n 'executed',\n 'failed',\n 'rejected',\n 'withdrawn',\n 'expired',\n] as const\n\nexport type ApprovalJobStatus = (typeof APPROVAL_JOB_STATUSES)[number]\n\n/**\n * The states in which the action of a request is still COMING.\n *\n * `pending` waits for approvals, `approved` waits for a person to press the\n * button, and `executing` is running now. Each of them can still change the\n * world, so two requests for one target must not be in these states together.\n * `dedupeKey` reads this set for that reason.\n *\n * `failed` is NOT here, although a person can run a failed request again. A\n * failed request runs only when a person presses the button, and that person\n * sees the row. Blocking a new request on a failed one would also trap the\n * key: `canWithdraw` accepts `pending` alone, so nobody could close it.\n *\n * `executed`, `rejected`, `withdrawn` and `expired` are the end of a request.\n */\nexport const APPROVAL_LIVE_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n] as const satisfies readonly ApprovalJobStatus[]\n\n/**\n * How many requests one page of the list holds when the caller asks for no\n * number.\n *\n * The list is a work queue that a Camundi reads, so the page is large: a\n * person wants the whole queue and not ten rows at a time. What the number is\n * for is the ceiling below.\n */\nexport const APPROVAL_PAGE_SIZE_DEFAULT = 500\n\n/**\n * The largest page that the service builds, and the largest that a caller may\n * ask for. The API answers 400 above it.\n *\n * The list had no limit at all. One authenticated caller could then make the\n * service read the whole table into memory, serialize it, and do it again, so\n * the cost of a request grew with the age of the service and with nothing that\n * the caller had to pay for. A ceiling turns that into a fixed cost.\n *\n * It is HERE and not only in the service, because the view must know it: a\n * page that asked for more would get a 400 instead of rows.\n */\nexport const APPROVAL_PAGE_SIZE_MAX = 2000\n\n/**\n * The columns that the queue can be ordered by.\n *\n * A NAME and not an SQL expression: the repository maps each one, so a caller\n * can never name a column that this list does not hold. `status` orders by the\n * EFFECTIVE status, because a `pending` row whose time ran out reads as\n * `expired` everywhere else.\n */\nexport const APPROVAL_SORT_FIELDS = [\n 'createdAt',\n 'expiresAt',\n 'status',\n 'type',\n 'createdBy',\n] as const\n\nexport type ApprovalSortField = (typeof APPROVAL_SORT_FIELDS)[number]\n\nexport type ApprovalSortOrder = 'asc' | 'desc'\n\n/** The default order of the queue: the newest request first. */\nexport const APPROVAL_SORT_DEFAULT: {\n sort: ApprovalSortField\n order: ApprovalSortOrder\n} = { sort: 'createdAt', order: 'desc' }\n\n/**\n * The longest reason that a request, a rejection or a withdrawal accepts.\n *\n * ONE number, because three routes of control-backend hold it and three fields\n * of control-frontend must stop there. A field that let a person write more\n * than the schema accepts turns their sentence into a 400 after they wrote it.\n */\nexport const APPROVAL_REASON_MAX_LENGTH = 2000\n\n/**\n * The largest number of approvals that a caller can ask for.\n *\n * A request that asks for 400 approvals can never reach them, so it is a\n * request that only fills the table and expires. The ceiling turns that into a\n * 400 at the moment a person types it. It is HERE because the form of the\n * bulk operation must stop at the same number as the route.\n */\nexport const APPROVAL_ACKS_MAX = 10\n\n/**\n * The longest search text that the list accepts.\n *\n * A search runs `ILIKE` over the payload of every visible row, so the cost of\n * one call follows the length of the text. Nothing a person types to find a\n * request is near this.\n */\nexport const APPROVAL_SEARCH_MAX_LENGTH = 200\n\n/**\n * The payload field that names the organization a request acts on.\n *\n * It is a CONVENTION, and this constant is what makes the convention readable\n * from both sides. A use case whose action hits one organization puts the id of\n * that organization in this field, and `GET /api/approvals?orgId=\u2026` then finds\n * the request with no knowledge of the use case. The filter reads this one\n * field of the payload, so it matches the whole id and nothing else. The free\n * text search reads the payload as TEXT, which also matches an id that only\n * appears inside another field, and it cannot answer \"the requests of this\n * organization\".\n *\n * The organization page of control-frontend does NOT read this field. It asks\n * the entity-link filter (`entityType=org`), which reads a row that\n * `relatedEntities` of a use case writes \u2014 see `ApprovalEntityRef`. The summary\n * is separate again: the `org` field of a `summarize` result is what a READER\n * follows.\n */\nexport const APPROVAL_PAYLOAD_ORG_FIELD = 'orgId'\n\n/**\n * How many requests each status holds, for the filters of the queue.\n *\n * Every number answers the SAME question as the list beside it: the counts are\n * built with the visibility rule, the action filter and the search text of the\n * caller, and with the lazy expiry. A count that ignored one of those would\n * send a person to a filter that then shows nothing.\n */\nexport interface ApprovalStatusCountsDto {\n /** Every request that matches, whatever its status. */\n all: number\n /** One number for each status. A status with no request is 0, never absent. */\n byStatus: Record<ApprovalJobStatus, number>\n}\n\n/**\n * What happens when a request reaches its threshold.\n *\n * `auto` runs the action immediately. `explicit` holds the request at\n * `approved` and waits for a person to run it.\n */\nexport type ApprovalExecutionMode = 'auto' | 'explicit'\n\n/**\n * WHERE the action runs. It is a separate question from who starts it.\n *\n * - `inline` runs the action inside the call that started it, and the answer\n * already carries the outcome. Right for one call to another service.\n * - `background` answers as soon as the request is claimed, and the action\n * runs on. It reports into `ApprovalJobDto.progress`, and the row is the\n * only place that holds the progress, so a reload and a second person read\n * the same numbers. A pod that stops loses the run, and the reconciler of\n * control-backend recovers the row.\n *\n * An action over thousands of targets MUST be `background`. `inline` would\n * hold one HTTP request for the whole run, and the ingress, the load balancer\n * and the browser each have their own deadline before it finishes.\n */\nexport type ApprovalActionMode = 'inline' | 'background'\n\n/**\n * A person, as an approval request records them.\n *\n * Both keys are kept. The machine then never has to choose one identity key,\n * which is what split the earlier acknowledgement flows: some held an email and\n * some held a user id, and the two could not be compared.\n */\nexport interface ApprovalActor {\n userId: string\n email: string\n}\n\nexport interface ApprovalAck extends ApprovalActor {\n /** An ISO timestamp. It is in a json column, so it is a string, not a Date. */\n date: string\n}\n\n/**\n * Who asked for an action, and who agreed to it.\n *\n * Every route of `api/admin/approvals` takes this envelope, and control-backend\n * builds it from the request that it runs. The envelope exists because the\n * token cannot carry it: the call is machine to machine, so `sub` holds the id\n * of the Auth0 application and not a person. A service that reads the token\n * alone therefore writes \"control-backend\" into its audit record, which hides\n * the two Camundis that the whole mechanism exists to record.\n *\n * `approvedBy` is a list, because a use case can need more than one approval,\n * and the record must name all of them. It can also be EMPTY: how many\n * approvals an action needs is a number of the use case, and `move-cluster-org`\n * holds zero on purpose. The person in `requestedBy` is never in the list: the\n * four-eyes rule is that the person who asks is not the person who agrees, and\n * control-backend holds that rule.\n *\n * Each person carries both keys of `ApprovalActor`. The email is what a route\n * uses to ask Auth0 about the person, and the user id is what an audit record\n * holds, because every other record of the fleet holds an Auth0 `sub`.\n *\n * CAUTION: The envelope is a CLAIM of the caller, and not proof. A route that\n * acts on it must ask Auth0 again whether `requestedBy` holds the permission\n * of the action. `api/admin/approvals` in accounts-backend does that. See the\n * note on `M2M_CONTROL_APPROVALS`.\n */\nexport interface ApprovalM2mActor {\n /** The id of the approval request, to join the records of both services. */\n requestId: string\n /** The Camundi who opened the request. */\n requestedBy: ApprovalActor\n /** The Camundis who approved it. Empty when the use case needs none. */\n approvedBy: ApprovalActor[]\n}\n\n/**\n * What one value of a summary field is.\n *\n * `text` is what a person reads. `id` is what a link points at, and it is\n * absent when the kind names no entity. The two are separate because the text\n * is a COPY that the request keeps: an organization that gets a new name after\n * the request was opened must still read as the organization that the person\n * chose.\n */\nexport interface ApprovalSummaryValue {\n text: string\n id?: string\n}\n\n/**\n * How a reader must render one field of a summary.\n *\n * `org`, `cluster` and `user` name an entity, so a reader can link to it and\n * can resolve a fresh name. The others are plain: `date` is an ISO timestamp,\n * and `code` is a value that must keep its characters, such as a json fragment.\n */\nexport const APPROVAL_SUMMARY_FIELD_KINDS = [\n 'text',\n 'number',\n 'boolean',\n 'date',\n 'org',\n 'cluster',\n 'user',\n 'code',\n] as const\n\nexport type ApprovalSummaryFieldKind =\n (typeof APPROVAL_SUMMARY_FIELD_KINDS)[number]\n\n/**\n * One labelled field of the payload of a request.\n *\n * This is what makes ONE view able to show EVERY use case. The payload of a\n * request is an object of anything, so a generic view can only print it as\n * json. The use case turns it into labelled fields here, and the view renders\n * a field by its kind: it links an `org`, it formats a `date`, and it lists\n * 300 clusters as 300 links.\n *\n * `values` is always an array. A field that holds one value holds an array of\n * one, so a reader needs no branch for \"one or many\".\n */\nexport interface ApprovalSummaryField {\n label: string\n kind: ApprovalSummaryFieldKind\n values: ApprovalSummaryValue[]\n /**\n * How many values the field holds, when `values` holds only the first of\n * them. The list route cuts every field to `APPROVAL_LIST_SUMMARY_VALUES_MAX`\n * values. The detail route sends every value and no `total`.\n */\n total?: number\n}\n\n/**\n * How many values of one summary field a row of the list carries.\n *\n * A bulk operation names up to 3000 clusters in one field. A page of 100 such\n * rows would carry 300,000 values that no row shows. The view shows at most\n * 12 values of a field, so the list stops there, and `total` holds the count.\n */\nexport const APPROVAL_LIST_SUMMARY_VALUES_MAX = 12\n\n/**\n * The kinds of entity that an approval request can concern.\n *\n * These are the entities that a request is findable by: the org and cluster\n * detail pages ask \"which requests touch me\", and the answer reads a row for\n * each entity. It is a subset of `APPROVAL_SUMMARY_FIELD_KINDS` \u2014 the kinds that\n * name an entity \u2014 because only those carry an id that a page can match.\n */\nexport const APPROVAL_ENTITY_TYPES = ['cluster', 'org', 'user'] as const\n\nexport type ApprovalEntityType = (typeof APPROVAL_ENTITY_TYPES)[number]\n\n/**\n * One entity that an approval request concerns.\n *\n * A use case reports these from its payload, and the service writes one row for\n * each into `approval_job_entity`. That row is what makes a request findable by\n * an entity it touches, without a text search over the payload: a cluster-move\n * request names its cluster, its target organization AND its source\n * organization, so it shows on all three detail pages. The `summary` still\n * carries the words a reader sees; this carries only the ids a page matches.\n */\nexport interface ApprovalEntityRef {\n type: ApprovalEntityType\n id: string\n}\n\n/**\n * How long a run may go quiet before a reader calls it stalled.\n *\n * The pod that runs an action writes its progress and renews its lease on one\n * timer, so a healthy run touches the row well inside this window. A row that\n * is `executing` and older than this held a pod that stopped, and the\n * reconciler of control-backend is on its way to it.\n *\n * It lives here, and not in the config of the service, because a READER needs\n * it: the browser must decide what to show without a second call, and a number\n * that only the backend knew would make the two disagree. `ACTION_LEASE_MS` in\n * `config/index.ts` is held below this on purpose.\n */\nexport const APPROVAL_RUN_STALE_MS = 120_000\n\n/**\n * What an action reports while it runs.\n *\n * This is what makes a long action watchable. Every field is in the DATABASE\n * and not in the answer to the call that started the run, so three readers see\n * the same thing: the person who pressed the button, the same person after a\n * reload, and a second person who opens the request.\n *\n * `total` can be 0 while an action counts its targets, and it stays 0 for an\n * action that cannot count them. A reader must then show that it runs, and no\n * percentage.\n */\nexport interface ApprovalProgress {\n /** Targets in total. 0 means the action did not say. */\n total: number\n /** Targets finished, whatever the outcome of each one. */\n done: number\n /** Targets that finished with an error. Part of `done`. */\n failed: number\n /** One short line for a person, such as the step that runs now. */\n note?: string\n /** An ISO timestamp: when the run started. */\n startedAt: string\n /** An ISO timestamp: when the run last wrote anything. */\n updatedAt: string\n}\n\n/** The share of the work that is finished, or `undefined` with no total. */\nexport function progressFraction(\n progress: ApprovalProgress | undefined,\n): number | undefined {\n if (!progress || progress.total <= 0) {\n return undefined\n }\n return Math.min(1, progress.done / progress.total)\n}\n\n/**\n * Reports whether a running request went quiet.\n *\n * True means that the pod which held the run stopped, or lost the row. It does\n * NOT mean that the action failed: the reconciler either runs it again, or it\n * writes `failed` so that a person can. See `APPROVAL_RUN_STALE_MS`.\n */\nexport function isRunStalled(job: ApprovalJobDto, now: Date): boolean {\n if (job.status !== 'executing') {\n return false\n }\n const lastSign = job.progress?.updatedAt ?? job.executingSince\n if (!lastSign) {\n return false\n }\n return now.getTime() - new Date(lastSign).getTime() > APPROVAL_RUN_STALE_MS\n}\n\n/** One approval request, as the API returns it. */\nexport interface ApprovalJobDto {\n id: string\n type: string\n dedupeKey?: string\n payload: Record<string, unknown>\n /**\n * The payload as labelled fields. See `ApprovalSummaryField`.\n *\n * The use case builds it when the request is opened AND every time the\n * request is read, so a field that a later version of the use case adds\n * appears on a request that is already open.\n */\n summary: ApprovalSummaryField[]\n /** Why the person opened the request. Some use cases demand one. */\n reason?: string\n createdBy: ApprovalActor\n acks: ApprovalAck[]\n /**\n * How many approvals THIS request needs.\n *\n * It is on the request and not only on the use case, because the person who\n * opens one can ask for more than the floor of the use case. A bulk\n * operation over 3000 clusters is the same action as one over 3, and it can\n * still deserve a third pair of eyes. `ApprovalUseCaseDto.minAcks` is the\n * floor that a caller cannot go below.\n */\n requiredAcks: number\n /**\n * What a person must hold to approve, reject or run THIS request.\n *\n * It is on the request and not only on the use case, because a payload can\n * raise the guard: a generation update with `yoloForceUpdate` needs the yolo\n * permission on top of the base one, and the same use case without the flag\n * does not. The set is computed ONE time, when the request is opened, so it\n * is also the record of what the request was guarded by.\n *\n * A person needs EVERY permission in the set, not one of them.\n */\n requiredPermissions: string[]\n executionMode: ApprovalExecutionMode\n actionMode: ApprovalActionMode\n status: ApprovalJobStatus\n closedBy?: ApprovalActor\n closedReason?: string\n /** An ISO timestamp. */\n expiresAt: string\n /**\n * An ISO timestamp: when the current run took the request.\n *\n * It is set every time a run starts, so a run that the reconciler starts\n * again moves it. `attempts` says how many runs there were.\n */\n executingSince?: string\n /** How many times a run started. A recovery raises it. */\n attempts: number\n /**\n * What the action reports while it runs. See `ApprovalProgress`.\n *\n * It stays on the row after the run, so the record of a finished run says\n * how many targets it touched and how many refused.\n */\n progress?: ApprovalProgress\n executedAt?: string\n executionError?: string\n /**\n * What the action reported when it ran.\n *\n * The shape belongs to the use case. An action over many targets puts the\n * result of each target here, which is the difference between \"it failed\"\n * and \"it did 2998 of 3000, and these two refused\".\n */\n executionResult?: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/**\n * What a use case is, as the API reports it.\n *\n * The frontend needs `requiredPermission` to run the same rule as the backend.\n * PR #9301 kept this registry a second time, by hand, in the frontend. A\n * registry that the server owns means a new use case needs no frontend deploy.\n */\nexport interface ApprovalUseCaseDto {\n type: string\n /** A short name for a person to read. */\n label: string\n /** One sentence: what the action does, and what it cannot undo. */\n description: string\n /** The approvals a new request asks for when the caller names no number. */\n defaultAcks: number\n /**\n * The floor. A caller can ask for more approvals, never for fewer.\n *\n * It is a separate number from `defaultAcks` because the two answer\n * different questions: the default is what the form offers, and the floor is\n * what the service refuses to go below.\n */\n minAcks: number\n ttlDays: number\n executionMode: ApprovalExecutionMode\n /** See `ApprovalActionMode`. An action over many targets is `background`. */\n actionMode: ApprovalActionMode\n /**\n * True when a run that was interrupted can start again by itself.\n *\n * It is a promise about the ACTION, and the reconciler reads it. Set it only\n * when the action can run a second time over the same payload and do no harm\n * twice: it must read what the first run already finished, out of\n * `ApprovalJobDto.executionResult`, and skip it.\n *\n * False, the default, is the safe answer. An interrupted run then lands in\n * `failed`, which a person can run again from the detail page, and a person\n * decides whether running it twice is safe.\n */\n resumable: boolean\n /**\n * The permission that every request of this use case needs to approve,\n * reject or run. A payload can add more. See\n * `ApprovalJobDto.requiredPermissions`, which is the effective set.\n *\n * It is also the permission to SEE a request of this use case, because a\n * person who cannot act on one has no reason to read it. The person who\n * opened a request always sees that request.\n */\n requiredPermission: string\n /**\n * The permission to open a request. Absent means any signed-in Camundi.\n *\n * Set it when the pool that can ASK must be as small as the pool that can\n * approve. Leave it absent when the approvals alone are the guard, and the\n * proposal is not.\n */\n createPermission?: string\n /** True when a request cannot be opened without a reason. */\n reasonRequired: boolean\n /**\n * True when the ACTION needs the email address of the person who asked.\n *\n * A token can carry no address, and the row then stores\n * `UNKNOWN_ACTOR_EMAIL`. An action that sends that placeholder to another\n * service asks it to find a user that does not exist, so the request can\n * only fail \u2014 and it fails at the END, after other people spent their\n * approvals on it. A use case that carries the creator into its call sets\n * this, and `canCreate` then refuses such a caller at the start.\n *\n * `extend-trial` and `send-to-expired-trial` are two: both send the creator\n * to accounts in an `ApprovalM2mActor`, and accounts reads the permissions\n * of that person again before it acts.\n *\n * `move-cluster-org` does NOT set this. Its envelope reaches console, which\n * reads the user id alone and needs no address.\n */\n creatorEmailRequired?: boolean\n}\n"],
5
- "mappings": "AA0BO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAmBO,MAAM,yBAAyB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF;AAUO,MAAM,6BAA6B;AAcnC,MAAM,yBAAyB;AAU/B,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,MAAM,wBAGT,EAAE,MAAM,aAAa,OAAO,OAAO;AAShC,MAAM,6BAA6B;AAUnC,MAAM,oBAAoB;AAS1B,MAAM,6BAA6B;AAoBnC,MAAM,6BAA6B;AAmHnC,MAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAoCO,MAAM,mCAAmC;AAUzC,MAAM,wBAAwB,CAAC,WAAW,OAAO,MAAM;AAgCvD,MAAM,wBAAwB;AA8B9B,SAAS,iBACd,UACoB;AACpB,MAAI,CAAC,YAAY,SAAS,SAAS,GAAG;AACpC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,GAAG,SAAS,OAAO,SAAS,KAAK;AACnD;AASO,SAAS,aAAa,KAAqB,KAAoB;AACpE,MAAI,IAAI,WAAW,aAAa;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,WAAW,IAAI,UAAU,aAAa,IAAI;AAChD,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AACA,SAAO,IAAI,QAAQ,IAAI,IAAI,KAAK,QAAQ,EAAE,QAAQ,IAAI;AACxD;",
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\n/**\n * The wire types of the generic approval mechanism.\n *\n * This file holds no logic and no dependency, because three consumers read it:\n * control-backend, control-frontend and cloudbot. The rules that read these\n * types are in `../approvals/policy.js`, and they are pure for the same reason.\n */\n\n/**\n * Every state that an approval request can hold.\n *\n * A `const` tuple, not an enum, so it is both the union type and the list that\n * validates a status filter at run time.\n *\n * The path of a request: `pending` until it reaches the threshold. Then\n * `executing` (automatic mode) or `approved` (explicit mode, which waits for a\n * person to press the button). `executed` or `failed` after the action ran. A\n * request that never reaches the threshold ends `rejected`, `withdrawn` or\n * `expired`.\n */\nexport const APPROVAL_JOB_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n 'executed',\n 'failed',\n 'rejected',\n 'withdrawn',\n 'expired',\n] as const\n\nexport type ApprovalJobStatus = (typeof APPROVAL_JOB_STATUSES)[number]\n\n/**\n * The states in which the action of a request is still COMING.\n *\n * `pending` waits for approvals, `approved` waits for a person to press the\n * button, and `executing` is running now. Each of them can still change the\n * world, so two requests for one target must not be in these states together.\n * `dedupeKey` reads this set for that reason.\n *\n * `failed` is NOT here, although a person can run a failed request again. A\n * failed request runs only when a person presses the button, and that person\n * sees the row. Blocking a new request on a failed one would also trap the\n * key: `canWithdraw` accepts `pending` alone, so nobody could close it.\n *\n * `executed`, `rejected`, `withdrawn` and `expired` are the end of a request.\n */\nexport const APPROVAL_LIVE_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n] as const satisfies readonly ApprovalJobStatus[]\n\n/**\n * How many requests one page of the list holds when the caller asks for no\n * number.\n *\n * The list is a work queue that a Camundi reads, so the page is large: a\n * person wants the whole queue and not ten rows at a time. What the number is\n * for is the ceiling below.\n */\nexport const APPROVAL_PAGE_SIZE_DEFAULT = 500\n\n/**\n * The largest page that the service builds, and the largest that a caller may\n * ask for. The API answers 400 above it.\n *\n * The list had no limit at all. One authenticated caller could then make the\n * service read the whole table into memory, serialize it, and do it again, so\n * the cost of a request grew with the age of the service and with nothing that\n * the caller had to pay for. A ceiling turns that into a fixed cost.\n *\n * It is HERE and not only in the service, because the view must know it: a\n * page that asked for more would get a 400 instead of rows.\n */\nexport const APPROVAL_PAGE_SIZE_MAX = 2000\n\n/**\n * The columns that the queue can be ordered by.\n *\n * A NAME and not an SQL expression: the repository maps each one, so a caller\n * can never name a column that this list does not hold. `status` orders by the\n * EFFECTIVE status, because a `pending` row whose time ran out reads as\n * `expired` everywhere else.\n */\nexport const APPROVAL_SORT_FIELDS = [\n 'createdAt',\n 'expiresAt',\n 'status',\n 'type',\n 'createdBy',\n] as const\n\nexport type ApprovalSortField = (typeof APPROVAL_SORT_FIELDS)[number]\n\nexport type ApprovalSortOrder = 'asc' | 'desc'\n\n/** The default order of the queue: the newest request first. */\nexport const APPROVAL_SORT_DEFAULT: {\n sort: ApprovalSortField\n order: ApprovalSortOrder\n} = { sort: 'createdAt', order: 'desc' }\n\n/**\n * The longest reason that a request, a rejection or a withdrawal accepts.\n *\n * ONE number, because three routes of control-backend hold it and three fields\n * of control-frontend must stop there. A field that let a person write more\n * than the schema accepts turns their sentence into a 400 after they wrote it.\n */\nexport const APPROVAL_REASON_MAX_LENGTH = 2000\n\n/**\n * The largest number of approvals that a caller can ask for.\n *\n * A request that asks for 400 approvals can never reach them, so it is a\n * request that only fills the table and expires. The ceiling turns that into a\n * 400 at the moment a person types it. It is HERE because the form of the\n * bulk operation must stop at the same number as the route.\n */\nexport const APPROVAL_ACKS_MAX = 10\n\n/**\n * The longest search text that the list accepts.\n *\n * A search runs `ILIKE` over the payload of every visible row, so the cost of\n * one call follows the length of the text. Nothing a person types to find a\n * request is near this.\n */\nexport const APPROVAL_SEARCH_MAX_LENGTH = 200\n\n/**\n * The payload field that names the organization a request acts on.\n *\n * It is a CONVENTION, and this constant is what makes the convention readable\n * from both sides. A use case whose action hits one organization puts the id of\n * that organization in this field, and `GET /api/approvals?orgId=\u2026` then finds\n * the request with no knowledge of the use case. The filter reads this one\n * field of the payload, so it matches the whole id and nothing else. The free\n * text search reads the payload as TEXT, which also matches an id that only\n * appears inside another field, and it cannot answer \"the requests of this\n * organization\".\n *\n * The organization page of control-frontend does NOT read this field. It asks\n * the entity-link filter (`entityType=org`), which reads a row that\n * `relatedEntities` of a use case writes \u2014 see `ApprovalEntityRef`. The summary\n * is separate again: the `org` field of a `summarize` result is what a READER\n * follows.\n */\nexport const APPROVAL_PAYLOAD_ORG_FIELD = 'orgId'\n\n/**\n * How many requests each status holds, for the filters of the queue.\n *\n * Every number answers the SAME question as the list beside it: the counts are\n * built with the visibility rule, the action filter and the search text of the\n * caller, and with the lazy expiry. A count that ignored one of those would\n * send a person to a filter that then shows nothing.\n */\nexport interface ApprovalStatusCountsDto {\n /** Every request that matches, whatever its status. */\n all: number\n /** One number for each status. A status with no request is 0, never absent. */\n byStatus: Record<ApprovalJobStatus, number>\n}\n\n/**\n * What happens when a request reaches its threshold.\n *\n * `auto` runs the action immediately. `explicit` holds the request at\n * `approved` and waits for a person to run it.\n */\nexport type ApprovalExecutionMode = 'auto' | 'explicit'\n\n/**\n * WHERE the action runs. It is a separate question from who starts it.\n *\n * - `inline` runs the action inside the call that started it, and the answer\n * already carries the outcome. Right for one call to another service.\n * - `background` answers as soon as the request is claimed, and the action\n * runs on. It reports into `ApprovalJobDto.progress`, and the row is the\n * only place that holds the progress, so a reload and a second person read\n * the same numbers. A pod that stops loses the run, and the reconciler of\n * control-backend recovers the row.\n *\n * An action over thousands of targets MUST be `background`. `inline` would\n * hold one HTTP request for the whole run, and the ingress, the load balancer\n * and the browser each have their own deadline before it finishes.\n */\nexport type ApprovalActionMode = 'inline' | 'background'\n\n/**\n * A person, as an approval request records them.\n *\n * Both keys are kept. The machine then never has to choose one identity key,\n * which is what split the earlier acknowledgement flows: some held an email and\n * some held a user id, and the two could not be compared.\n */\nexport interface ApprovalActor {\n userId: string\n email: string\n}\n\nexport interface ApprovalAck extends ApprovalActor {\n /** An ISO timestamp. It is in a json column, so it is a string, not a Date. */\n date: string\n}\n\n/**\n * Who asked for an action, and who agreed to it.\n *\n * Every route of `api/admin/approvals` takes this envelope, and control-backend\n * builds it from the request that it runs. The envelope exists because the\n * token cannot carry it: the call is machine to machine, so `sub` holds the id\n * of the Auth0 application and not a person. A service that reads the token\n * alone therefore writes \"control-backend\" into its audit record, which hides\n * the two Camundis that the whole mechanism exists to record.\n *\n * `approvedBy` is a list, because a use case can need more than one approval,\n * and the record must name all of them. It can also be EMPTY: how many\n * approvals an action needs is a number of the use case, and `move-cluster-org`\n * holds zero on purpose. The person in `requestedBy` is never in the list: the\n * four-eyes rule is that the person who asks is not the person who agrees, and\n * control-backend holds that rule.\n *\n * Each person carries both keys of `ApprovalActor`. The email is what a route\n * uses to ask Auth0 about the person, and the user id is what an audit record\n * holds, because every other record of the fleet holds an Auth0 `sub`.\n *\n * CAUTION: The envelope is a CLAIM of the caller, and not proof. A route that\n * acts on it must ask Auth0 again whether `requestedBy` may do the action.\n * That means the permission of the action. For a use case with\n * `openToEveryone`, it means that the person is a Camundi, because no\n * permission runs that action. `api/admin/approvals` in accounts-backend does\n * that. See the note on `M2M_CONTROL_APPROVALS`.\n */\nexport interface ApprovalM2mActor {\n /** The id of the approval request, to join the records of both services. */\n requestId: string\n /** The Camundi who opened the request. */\n requestedBy: ApprovalActor\n /** The Camundis who approved it. Empty when the use case needs none. */\n approvedBy: ApprovalActor[]\n}\n\n/**\n * What one value of a summary field is.\n *\n * `text` is what a person reads. `id` is what a link points at, and it is\n * absent when the kind names no entity. The two are separate because the text\n * is a COPY that the request keeps: an organization that gets a new name after\n * the request was opened must still read as the organization that the person\n * chose.\n */\nexport interface ApprovalSummaryValue {\n text: string\n id?: string\n}\n\n/**\n * How a reader must render one field of a summary.\n *\n * `org`, `cluster` and `user` name an entity, so a reader can link to it and\n * can resolve a fresh name. The others are plain: `date` is an ISO timestamp,\n * and `code` is a value that must keep its characters, such as a json fragment.\n */\nexport const APPROVAL_SUMMARY_FIELD_KINDS = [\n 'text',\n 'number',\n 'boolean',\n 'date',\n 'org',\n 'cluster',\n 'user',\n 'code',\n] as const\n\nexport type ApprovalSummaryFieldKind =\n (typeof APPROVAL_SUMMARY_FIELD_KINDS)[number]\n\n/**\n * One labelled field of the payload of a request.\n *\n * This is what makes ONE view able to show EVERY use case. The payload of a\n * request is an object of anything, so a generic view can only print it as\n * json. The use case turns it into labelled fields here, and the view renders\n * a field by its kind: it links an `org`, it formats a `date`, and it lists\n * 300 clusters as 300 links.\n *\n * `values` is always an array. A field that holds one value holds an array of\n * one, so a reader needs no branch for \"one or many\".\n */\nexport interface ApprovalSummaryField {\n label: string\n kind: ApprovalSummaryFieldKind\n values: ApprovalSummaryValue[]\n /**\n * How many values the field holds, when `values` holds only the first of\n * them. The list route cuts every field to `APPROVAL_LIST_SUMMARY_VALUES_MAX`\n * values. The detail route sends every value and no `total`.\n */\n total?: number\n}\n\n/**\n * How many values of one summary field a row of the list carries.\n *\n * A bulk operation names up to 3000 clusters in one field. A page of 100 such\n * rows would carry 300,000 values that no row shows. The view shows at most\n * 12 values of a field, so the list stops there, and `total` holds the count.\n */\nexport const APPROVAL_LIST_SUMMARY_VALUES_MAX = 12\n\n/**\n * The kinds of entity that an approval request can concern.\n *\n * These are the entities that a request is findable by: the org and cluster\n * detail pages ask \"which requests touch me\", and the answer reads a row for\n * each entity. It is a subset of `APPROVAL_SUMMARY_FIELD_KINDS` \u2014 the kinds that\n * name an entity \u2014 because only those carry an id that a page can match.\n */\nexport const APPROVAL_ENTITY_TYPES = ['cluster', 'org', 'user'] as const\n\nexport type ApprovalEntityType = (typeof APPROVAL_ENTITY_TYPES)[number]\n\n/**\n * One entity that an approval request concerns.\n *\n * A use case reports these from its payload, and the service writes one row for\n * each into `approval_job_entity`. That row is what makes a request findable by\n * an entity it touches, without a text search over the payload: a cluster-move\n * request names its cluster, its target organization AND its source\n * organization, so it shows on all three detail pages. The `summary` still\n * carries the words a reader sees; this carries only the ids a page matches.\n */\nexport interface ApprovalEntityRef {\n type: ApprovalEntityType\n id: string\n}\n\n/**\n * How long a run may go quiet before a reader calls it stalled.\n *\n * The pod that runs an action writes its progress and renews its lease on one\n * timer, so a healthy run touches the row well inside this window. A row that\n * is `executing` and older than this held a pod that stopped, and the\n * reconciler of control-backend is on its way to it.\n *\n * It lives here, and not in the config of the service, because a READER needs\n * it: the browser must decide what to show without a second call, and a number\n * that only the backend knew would make the two disagree. `ACTION_LEASE_MS` in\n * `config/index.ts` is held below this on purpose.\n */\nexport const APPROVAL_RUN_STALE_MS = 120_000\n\n/**\n * What an action reports while it runs.\n *\n * This is what makes a long action watchable. Every field is in the DATABASE\n * and not in the answer to the call that started the run, so three readers see\n * the same thing: the person who pressed the button, the same person after a\n * reload, and a second person who opens the request.\n *\n * `total` can be 0 while an action counts its targets, and it stays 0 for an\n * action that cannot count them. A reader must then show that it runs, and no\n * percentage.\n */\nexport interface ApprovalProgress {\n /** Targets in total. 0 means the action did not say. */\n total: number\n /** Targets finished, whatever the outcome of each one. */\n done: number\n /** Targets that finished with an error. Part of `done`. */\n failed: number\n /** One short line for a person, such as the step that runs now. */\n note?: string\n /** An ISO timestamp: when the run started. */\n startedAt: string\n /** An ISO timestamp: when the run last wrote anything. */\n updatedAt: string\n}\n\n/** The share of the work that is finished, or `undefined` with no total. */\nexport function progressFraction(\n progress: ApprovalProgress | undefined,\n): number | undefined {\n if (!progress || progress.total <= 0) {\n return undefined\n }\n return Math.min(1, progress.done / progress.total)\n}\n\n/**\n * Reports whether a running request went quiet.\n *\n * True means that the pod which held the run stopped, or lost the row. It does\n * NOT mean that the action failed: the reconciler either runs it again, or it\n * writes `failed` so that a person can. See `APPROVAL_RUN_STALE_MS`.\n */\nexport function isRunStalled(job: ApprovalJobDto, now: Date): boolean {\n if (job.status !== 'executing') {\n return false\n }\n const lastSign = job.progress?.updatedAt ?? job.executingSince\n if (!lastSign) {\n return false\n }\n return now.getTime() - new Date(lastSign).getTime() > APPROVAL_RUN_STALE_MS\n}\n\n/** One approval request, as the API returns it. */\nexport interface ApprovalJobDto {\n id: string\n type: string\n dedupeKey?: string\n payload: Record<string, unknown>\n /**\n * The payload as labelled fields. See `ApprovalSummaryField`.\n *\n * The use case builds it when the request is opened AND every time the\n * request is read, so a field that a later version of the use case adds\n * appears on a request that is already open.\n */\n summary: ApprovalSummaryField[]\n /** Why the person opened the request. Some use cases demand one. */\n reason?: string\n createdBy: ApprovalActor\n acks: ApprovalAck[]\n /**\n * How many approvals THIS request needs.\n *\n * It is on the request and not only on the use case, because the person who\n * opens one can ask for more than the floor of the use case. A bulk\n * operation over 3000 clusters is the same action as one over 3, and it can\n * still deserve a third pair of eyes. `ApprovalUseCaseDto.minAcks` is the\n * floor that a caller cannot go below.\n */\n requiredAcks: number\n /**\n * What a person must hold to approve, reject or run THIS request.\n *\n * It is on the request and not only on the use case, because a payload can\n * raise the guard: a generation update with `yoloForceUpdate` needs the yolo\n * permission on top of the base one, and the same use case without the flag\n * does not. The set is computed ONE time, when the request is opened, so it\n * is also the record of what the request was guarded by.\n *\n * A person needs EVERY permission in the set, not one of them.\n */\n requiredPermissions: string[]\n executionMode: ApprovalExecutionMode\n actionMode: ApprovalActionMode\n status: ApprovalJobStatus\n closedBy?: ApprovalActor\n closedReason?: string\n /** An ISO timestamp. */\n expiresAt: string\n /**\n * An ISO timestamp: when the current run took the request.\n *\n * It is set every time a run starts, so a run that the reconciler starts\n * again moves it. `attempts` says how many runs there were.\n */\n executingSince?: string\n /** How many times a run started. A recovery raises it. */\n attempts: number\n /**\n * What the action reports while it runs. See `ApprovalProgress`.\n *\n * It stays on the row after the run, so the record of a finished run says\n * how many targets it touched and how many refused.\n */\n progress?: ApprovalProgress\n executedAt?: string\n executionError?: string\n /**\n * What the action reported when it ran.\n *\n * The shape belongs to the use case. An action over many targets puts the\n * result of each target here, which is the difference between \"it failed\"\n * and \"it did 2998 of 3000, and these two refused\".\n */\n executionResult?: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/**\n * What a use case is, as the API reports it.\n *\n * The frontend needs `requiredPermission` to run the same rule as the backend.\n * PR #9301 kept this registry a second time, by hand, in the frontend. A\n * registry that the server owns means a new use case needs no frontend deploy.\n */\nexport interface ApprovalUseCaseDto {\n type: string\n /** A short name for a person to read. */\n label: string\n /** One sentence: what the action does, and what it cannot undo. */\n description: string\n /** The approvals a new request asks for when the caller names no number. */\n defaultAcks: number\n /**\n * The floor. A caller can ask for more approvals, never for fewer.\n *\n * It is a separate number from `defaultAcks` because the two answer\n * different questions: the default is what the form offers, and the floor is\n * what the service refuses to go below.\n */\n minAcks: number\n ttlDays: number\n executionMode: ApprovalExecutionMode\n /** See `ApprovalActionMode`. An action over many targets is `background`. */\n actionMode: ApprovalActionMode\n /**\n * True when a run that was interrupted can start again by itself.\n *\n * It is a promise about the ACTION, and the reconciler reads it. Set it only\n * when the action can run a second time over the same payload and do no harm\n * twice: it must read what the first run already finished, out of\n * `ApprovalJobDto.executionResult`, and skip it.\n *\n * False, the default, is the safe answer. An interrupted run then lands in\n * `failed`, which a person can run again from the detail page, and a person\n * decides whether running it twice is safe.\n */\n resumable: boolean\n /**\n * The permission that every request of this use case needs to approve,\n * reject or run. A payload can add more. See\n * `ApprovalJobDto.requiredPermissions`, which is the effective set.\n *\n * It is also the permission to SEE a request of this use case, because a\n * person who cannot act on one has no reason to read it. The person who\n * opened a request always sees that request.\n *\n * The one exception to \"or run\" is a use case with `openToEveryone`. It has\n * no approval, so this permission only decides who sees the requests of\n * other people. Any signed-in person can open and run such a request.\n */\n requiredPermission: string\n /**\n * The permission to open a request. Absent means any signed-in Camundi.\n *\n * Set it when the pool that can ASK must be as small as the pool that can\n * approve. Leave it absent when the approvals alone are the guard, and the\n * proposal is not.\n */\n createPermission?: string\n /** True when a request cannot be opened without a reason. */\n reasonRequired: boolean\n /**\n * True when the ACTION needs the email address of the person who asked.\n *\n * A token can carry no address, and the row then stores\n * `UNKNOWN_ACTOR_EMAIL`. An action that sends that placeholder to another\n * service asks it to find a user that does not exist, so the request can\n * only fail \u2014 and it fails at the END, after other people spent their\n * approvals on it. A use case that carries the creator into its call sets\n * this, and `canCreate` then refuses such a caller at the start.\n *\n * `extend-trial` and `send-to-expired-trial` are two: both send the creator\n * to accounts in an `ApprovalM2mActor`, and accounts reads the permissions\n * of that person again before it acts.\n *\n * `move-cluster-org` does NOT set this. Its envelope reaches console, which\n * reads the user id alone and needs no address.\n */\n creatorEmailRequired?: boolean\n /**\n * A use case with no approval that any signed-in person can open AND run.\n *\n * A request with a floor of zero runs when it is opened, so `canCreate`\n * asks the person to hold `requiredPermission`. This flag lifts that one\n * rule, for an action whose run needs no permission of the person. Set it\n * only when two things hold:\n *\n * - Something else bounds how often a person can ask.\n * - The service that runs the action authorizes the person again at its\n * source, so that nobody can change what is not theirs. The action can\n * touch an organization only when that check lets the person change it.\n *\n * `create-personal-organisation` and the two use cases of customer hardware\n * sizing set it. The two use cases of customer hardware sizing do the second\n * check in two ways:\n *\n * - Creating a plan, and unlinking a plan from one organization, read the\n * role of the person in that organization. Only an OWNER passes.\n * - Deleting a plan reads no role. Console checks that the person created\n * the plan. Accounts reads the organizations that the person is a member\n * of, in any role, and removes the plan from their lists.\n */\n openToEveryone?: boolean\n}\n"],
5
+ "mappings": "AA0BO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAmBO,MAAM,yBAAyB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF;AAUO,MAAM,6BAA6B;AAcnC,MAAM,yBAAyB;AAU/B,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,MAAM,wBAGT,EAAE,MAAM,aAAa,OAAO,OAAO;AAShC,MAAM,6BAA6B;AAUnC,MAAM,oBAAoB;AAS1B,MAAM,6BAA6B;AAoBnC,MAAM,6BAA6B;AAqHnC,MAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAoCO,MAAM,mCAAmC;AAUzC,MAAM,wBAAwB,CAAC,WAAW,OAAO,MAAM;AAgCvD,MAAM,wBAAwB;AA8B9B,SAAS,iBACd,UACoB;AACpB,MAAI,CAAC,YAAY,SAAS,SAAS,GAAG;AACpC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,GAAG,SAAS,OAAO,SAAS,KAAK;AACnD;AASO,SAAS,aAAa,KAAqB,KAAoB;AACpE,MAAI,IAAI,WAAW,aAAa;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,WAAW,IAAI,UAAU,aAAa,IAAI;AAChD,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AACA,SAAO,IAAI,QAAQ,IAAI,IAAI,KAAK,QAAQ,EAAE,QAAQ,IAAI;AACxD;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * What console-backend answers when an approved request changed the plan type
3
+ * of one cluster (`PATCH api/admin/approvals/clusters/:clusterId/plan-type`).
4
+ *
5
+ * control-backend keeps it as the result of the request, so the person who
6
+ * asked can read whether the cluster plan moved too.
7
+ */
8
+ export interface ChangeClusterPlanTypeResult {
9
+ clusterId: string;
10
+ orgId: string;
11
+ fromPlanTypeUuid: string;
12
+ toPlanTypeUuid: string;
13
+ /**
14
+ * True when the cluster plan moved to the active plan of the new plan type.
15
+ * False when the cluster already ran that plan, or when the cluster stayed
16
+ * on its plan type for a change of the packages and its plan is not
17
+ * migratable to the active plan of that type. The approval route refuses a
18
+ * NEW plan type whose active plan the cluster cannot run, so that case is
19
+ * a failed request and never a result.
20
+ */
21
+ clusterPlanUpdated: boolean;
22
+ numberOfAllocatedHwPackages: number;
23
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=ClusterPlanTypeChange.dto.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -58,6 +58,11 @@ export interface ExternalIdpConfig {
58
58
  scopes?: string[];
59
59
  usernameClaim?: string;
60
60
  clientIdClaim?: string;
61
+ /** True resolves a token that carries both `usernameClaim` and `clientIdClaim` as a
62
+ * user. Absent or false keeps the engine default, which resolves it as a client.
63
+ * Entra puts `azp` in user tokens too, so with `clientIdClaim: 'azp'` an Entra user
64
+ * signs in as a client unless this is true (camunda/camunda#39049). */
65
+ preferUsernameClaim?: boolean;
61
66
  /** Token audiences (`aud` values) the cluster accepts from this provider. A list,
62
67
  * and named for the CRD field (`oidcProviders[].audiences`,
63
68
  * camunda-operator#5961) — an `audience` string would be pruned on write.
@@ -164,6 +164,14 @@ export interface ClusterListItemV2Dto {
164
164
  channel?: string;
165
165
  /** ISO-8601; part of the cursor key. */
166
166
  created: string;
167
+ /**
168
+ * User id of the creator, e.g. `samlp|Okta|jane.doe@camunda.com`. Absent
169
+ * when a background flow created the cluster, for an old cluster that has
170
+ * no record of its creator, or on an older console-backend. Also absent
171
+ * for a caller without `canReadAllClusterInfo`: the org clusters route
172
+ * serves members of the org too (see `canReadClusterCreator`).
173
+ */
174
+ createdBy?: string;
167
175
  /**
168
176
  * ISO-8601 instant of the last suspend. Absent when the cluster is awake:
169
177
  * `Cluster.wakeUp()` clears the column, thus this is the time since the last
@@ -299,6 +307,8 @@ export interface ListClustersV2Params extends CursorParams {
299
307
  orgIds?: string[];
300
308
  /** Restrict to these cluster uuids (prefilter-by-id). */
301
309
  ids?: string[];
310
+ /** Restrict to clusters that these user ids created (`cluster.createdBy`). */
311
+ createdBy?: string[];
302
312
  /** Auto-update on/off. */
303
313
  autoUpdate?: boolean;
304
314
  /** Internal (Camunda-owned) clusters only / excluded. */
@@ -415,6 +425,13 @@ export interface ClusterFacetsV2Dto {
415
425
  };
416
426
  /** Clusters with an active (non-archived) modification. */
417
427
  activeModifications: number;
428
+ /**
429
+ * Keyed by the user id of the creator. Only Camundi ids (see
430
+ * `isCamundaUserId`): the "Created by" filter lists Camundis, and the id of
431
+ * a customer does not belong in a fleet-wide facet. Absent on an older
432
+ * console-backend.
433
+ */
434
+ createdBy?: Record<string, number>;
418
435
  }
419
436
  /** Cluster/owner resolution for a Zeebe client id (cross-backend org search). */
420
437
  export interface ClusterByClientV2Dto {