@camunda/ccma-shared-types 1.2.0 → 3.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 (59) hide show
  1. package/dist/_tests/permissions/session-permissions.spec.d.ts +1 -0
  2. package/dist/approvals/policy.d.ts +94 -5
  3. package/dist/approvals/policy.js +64 -14
  4. package/dist/approvals/policy.js.map +2 -2
  5. package/dist/approvals/use-cases.const.d.ts +41 -0
  6. package/dist/approvals/use-cases.const.js +12 -0
  7. package/dist/approvals/use-cases.const.js.map +7 -0
  8. package/dist/config/camundi-permission-rules.js +7 -1
  9. package/dist/config/camundi-permission-rules.js.map +2 -2
  10. package/dist/constants/version-gates.const.d.ts +9 -2
  11. package/dist/constants/version-gates.const.js +1 -1
  12. package/dist/constants/version-gates.const.js.map +2 -2
  13. package/dist/dtos/AlertSubscription.dto.d.ts +6 -0
  14. package/dist/dtos/AlertSubscription.dto.js.map +2 -2
  15. package/dist/dtos/ApprovalJob.dto.d.ts +258 -4
  16. package/dist/dtos/ApprovalJob.dto.js +42 -1
  17. package/dist/dtos/ApprovalJob.dto.js.map +2 -2
  18. package/dist/dtos/ClusterCreationDefaults.dto.d.ts +96 -0
  19. package/dist/dtos/ClusterCreationDefaults.dto.js +93 -0
  20. package/dist/dtos/ClusterCreationDefaults.dto.js.map +7 -0
  21. package/dist/dtos/ClusterOwnerMove.dto.d.ts +81 -0
  22. package/dist/dtos/ClusterOwnerMove.dto.js +1 -0
  23. package/dist/dtos/ClusterOwnerMove.dto.js.map +7 -0
  24. package/dist/dtos/CommonCluster.dto.d.ts +1 -0
  25. package/dist/dtos/ControlV2.dto.d.ts +25 -0
  26. package/dist/dtos/FeatureFlags.dto.d.ts +3 -3
  27. package/dist/dtos/Generations.dto.d.ts +17 -0
  28. package/dist/dtos/Generations.dto.js +7 -0
  29. package/dist/dtos/Generations.dto.js.map +2 -2
  30. package/dist/dtos/Generations.dto.spec.d.ts +1 -0
  31. package/dist/dtos/Organization.dto.d.ts +9 -0
  32. package/dist/dtos/Organization.dto.js.map +2 -2
  33. package/dist/dtos/ReleaseTrainSanity.dto.d.ts +6 -0
  34. package/dist/enums/AuditEntity.enum.d.ts +2 -0
  35. package/dist/enums/AuditEntity.enum.js +2 -0
  36. package/dist/enums/AuditEntity.enum.js.map +2 -2
  37. package/dist/enums/SystemAuditEntity.enum.d.ts +1 -0
  38. package/dist/enums/SystemAuditEntity.enum.js +1 -0
  39. package/dist/enums/SystemAuditEntity.enum.js.map +2 -2
  40. package/dist/index.d.ts +12 -7
  41. package/dist/index.js +53 -3
  42. package/dist/index.js.map +2 -2
  43. package/dist/permissions/combo.d.ts +9 -0
  44. package/dist/permissions/combo.js +57 -1
  45. package/dist/permissions/combo.js.map +2 -2
  46. package/dist/permissions/combo.optionalTelemetry.spec.d.ts +1 -0
  47. package/dist/permissions/console.permissions.d.ts +2 -0
  48. package/dist/permissions/console.permissions.js +13 -1
  49. package/dist/permissions/console.permissions.js.map +2 -2
  50. package/dist/types/ClusterBulkOperation.type.d.ts +10 -2
  51. package/dist/types/ClusterPlanTypeAvailabilityClass.type.d.ts +11 -0
  52. package/dist/types/ClusterPlanTypeAvailabilityClass.type.js +4 -0
  53. package/dist/types/ClusterPlanTypeAvailabilityClass.type.js.map +2 -2
  54. package/dist/types/Roles.d.ts +1 -1
  55. package/dist/types/Roles.js.map +2 -2
  56. package/dist/utils/versionCheck.utils.d.ts +48 -0
  57. package/dist/utils/versionCheck.utils.js +50 -4
  58. package/dist/utils/versionCheck.utils.js.map +2 -2
  59. package/package.json +1 -1
@@ -50,6 +50,14 @@ export interface ApprovalActorView {
50
50
  * are one mailbox and must not pass as two people.
51
51
  */
52
52
  export declare function isSameActorEmail(one?: string, other?: string): boolean;
53
+ /**
54
+ * Reports whether this actor carries an address that names a person.
55
+ *
56
+ * A machine-to-machine token carries none: accounts-backend builds the
57
+ * permissions of such a token from its `permissions` claim alone, and there is
58
+ * no person behind it. See `creatorEmailRequired` of the use case.
59
+ */
60
+ export declare function hasActorEmail(actor: ApprovalActorView): boolean;
53
61
  /**
54
62
  * Why an action is not allowed.
55
63
  *
@@ -57,7 +65,7 @@ export declare function isSameActorEmail(one?: string, other?: string): boolean;
57
65
  * frontend can branch on it. The message is for a person, and it is shown as
58
66
  * written.
59
67
  */
60
- export type ApprovalDenialCode = 'forbidden' | 'not-pending' | 'self-approval' | 'already-acknowledged' | 'expired' | 'not-creator' | 'not-executable' | 'no-reason';
68
+ export type ApprovalDenialCode = 'forbidden' | 'not-pending' | 'self-approval' | 'already-acknowledged' | 'expired' | 'not-creator' | 'not-executable' | 'no-reason' | 'too-few-acks' | 'no-creator-email';
61
69
  export type ApprovalDecision = {
62
70
  allowed: true;
63
71
  } | {
@@ -76,7 +84,75 @@ export declare function isExpired(job: ApprovalJobDto, now: Date): boolean;
76
84
  export declare function effectiveStatus(job: ApprovalJobDto, now: Date): ApprovalJobDto['status'];
77
85
  /** Reports how many more acknowledgements the request needs. */
78
86
  export declare function remainingAcks(job: ApprovalJobDto): number;
79
- export declare function canCreate(actor: ApprovalActorView, useCase: ApprovalUseCaseDto): ApprovalDecision;
87
+ /**
88
+ * Reports whether the actor holds EVERY permission of the set.
89
+ *
90
+ * Every, and not one of them. A payload that raises the guard adds a
91
+ * permission, so "one of them" would let the base permission alone carry a
92
+ * request that the extra permission exists to stop.
93
+ *
94
+ * An EMPTY set is held by nobody. That is the opposite of what `Array.every`
95
+ * answers, and it is deliberate: this function guards a privileged action, so
96
+ * the empty case must fail closed. A row with no permissions is not a row that
97
+ * everybody may run — it is a row that lost its guard, through a migration
98
+ * that did not backfill it or a write that went around the service. Every use
99
+ * case builds a set of at least one, so no real request meets this branch.
100
+ */
101
+ export declare function hasEveryPermission(actor: ApprovalActorView, permissions: string[]): boolean;
102
+ /**
103
+ * What a person must hold to act on a request.
104
+ *
105
+ * A request carries its own set, because a payload can raise the guard. A use
106
+ * case that never does has a set of one, and it is the base permission.
107
+ */
108
+ export declare function requiredPermissionsOf(job: ApprovalJobDto): string[];
109
+ /**
110
+ * Who can READ a request.
111
+ *
112
+ * Two people: one who can act on it, and the one who opened it. A Camundi who
113
+ * holds neither has no reason to read which customer somebody proposed to
114
+ * expire, so the list and the detail both hide it.
115
+ *
116
+ * The rule reads the BASE permission of the use case and not the effective set
117
+ * of the request. A payload that raises the guard raises who can APPROVE, and
118
+ * not who can look: a generation update with the yolo flag is visible to
119
+ * everybody who can approve a plain one, and only the yolo permission runs it.
120
+ * The list filters by use case in SQL, which is what makes one page of rows
121
+ * the same page that a person is allowed to read.
122
+ */
123
+ export declare function canView(job: ApprovalJobDto, actor: ApprovalActorView, useCase: ApprovalUseCaseDto): ApprovalDecision;
124
+ /**
125
+ * Who can OPEN a request, and with what.
126
+ *
127
+ * `createPermission` is what keeps the pool that asks as small as the pool that
128
+ * approves. A use case that leaves it absent says the opposite on purpose: the
129
+ * approvals are the guard, and anybody signed in can propose the action.
130
+ *
131
+ * The two checks after it are about the request and not about the person. A
132
+ * count below the floor of the use case, and a missing reason, are both a 400
133
+ * and not a 403, so the words say which one the caller must repair.
134
+ */
135
+ export declare function canCreate(actor: ApprovalActorView, useCase: ApprovalUseCaseDto, request?: {
136
+ reason?: string;
137
+ requiredAcks?: number;
138
+ /**
139
+ * The effective permissions of the payload, from `permissionsFor`.
140
+ *
141
+ * Absent means "the base permission alone", which is what most use cases
142
+ * build. Only the permissions ABOVE the base one gate opening a request:
143
+ * see the note in the body.
144
+ */
145
+ requiredPermissions?: string[];
146
+ }): ApprovalDecision;
147
+ /**
148
+ * The approvals that a new request asks for.
149
+ *
150
+ * The caller names a number or names none. A number below the floor is refused
151
+ * by `canCreate`, and this function holds the answer inside the range anyway:
152
+ * the two run in the same call, and a floor that only one of them knows is a
153
+ * floor that a later caller can step over.
154
+ */
155
+ export declare function resolveRequiredAcks(useCase: ApprovalUseCaseDto, requested?: number): number;
80
156
  /**
81
157
  * The four-eyes rule.
82
158
  *
@@ -88,7 +164,7 @@ export declare function canCreate(actor: ApprovalActorView, useCase: ApprovalUse
88
164
  * token, and the email. See `ApprovalActorView.email` for why one of them is
89
165
  * not enough.
90
166
  */
91
- export declare function canAcknowledge(job: ApprovalJobDto, actor: ApprovalActorView, useCase: ApprovalUseCaseDto, now: Date): ApprovalDecision;
167
+ export declare function canAcknowledge(job: ApprovalJobDto, actor: ApprovalActorView, now: Date): ApprovalDecision;
92
168
  /**
93
169
  * A rejection needs the same permission as an acknowledgement, and a reason.
94
170
  *
@@ -96,7 +172,7 @@ export declare function canAcknowledge(job: ApprovalJobDto, actor: ApprovalActor
96
172
  * and stopping it would only push the person to the withdraw button, where the
97
173
  * reason is lost.
98
174
  */
99
- export declare function canReject(job: ApprovalJobDto, actor: ApprovalActorView, useCase: ApprovalUseCaseDto, now: Date, reason: string): ApprovalDecision;
175
+ export declare function canReject(job: ApprovalJobDto, actor: ApprovalActorView, now: Date, reason: string): ApprovalDecision;
100
176
  /** Only the person who opened a request can take it back. */
101
177
  export declare function canWithdraw(job: ApprovalJobDto, actor: ApprovalActorView, now: Date): ApprovalDecision;
102
178
  /**
@@ -105,5 +181,18 @@ export declare function canWithdraw(job: ApprovalJobDto, actor: ApprovalActorVie
105
181
  * This is the button of the explicit mode. `failed` is executable again,
106
182
  * because a failure is often a problem of the system that is now repaired, and
107
183
  * asking for the approvals a second time helps nobody.
184
+ *
185
+ * ANY holder of the permission may press it, and not only the person who
186
+ * opened the request. The decision was made when the request reached
187
+ * `approved`: `canAcknowledge` refuses the creator, so the approvals on the
188
+ * row came from other people, and the button only carries out what they
189
+ * already agreed to. Holding the button to the creator would let one Camundi
190
+ * on holiday freeze an approved action, and a use case that must not run
191
+ * without its author has `executionMode: 'auto'` instead.
192
+ *
193
+ * Who PRESSED it and who ASKED for it are two different facts, and both are
194
+ * kept: the audit trail of control-backend records the actor of the run, and
195
+ * an action that names a person to another service names the creator. See
196
+ * `onBehalfOf` of `extend-trial`.
108
197
  */
109
- export declare function canExecute(job: ApprovalJobDto, actor: ApprovalActorView, useCase: ApprovalUseCaseDto): ApprovalDecision;
198
+ export declare function canExecute(job: ApprovalJobDto, actor: ApprovalActorView): ApprovalDecision;
@@ -1,11 +1,14 @@
1
1
  import { UNKNOWN_ACTOR_EMAIL } from "../constants/misc.const.js";
2
2
  function isSameActorEmail(one, other) {
3
- const normalize = (value) => {
4
- const trimmed = value?.trim().toLowerCase() ?? "";
5
- return trimmed === UNKNOWN_ACTOR_EMAIL ? "" : trimmed;
6
- };
7
- const left = normalize(one);
8
- return left.length > 0 && left === normalize(other);
3
+ const left = comparableActorEmail(one);
4
+ return left.length > 0 && left === comparableActorEmail(other);
5
+ }
6
+ function comparableActorEmail(value) {
7
+ const trimmed = value?.trim().toLowerCase() ?? "";
8
+ return trimmed === UNKNOWN_ACTOR_EMAIL ? "" : trimmed;
9
+ }
10
+ function hasActorEmail(actor) {
11
+ return comparableActorEmail(actor.email).length > 0;
9
12
  }
10
13
  const ALLOWED = { allowed: true };
11
14
  function deny(code, message) {
@@ -20,14 +23,56 @@ function effectiveStatus(job, now) {
20
23
  function remainingAcks(job) {
21
24
  return Math.max(0, job.requiredAcks - job.acks.length);
22
25
  }
23
- function canCreate(actor, useCase) {
26
+ function hasEveryPermission(actor, permissions) {
27
+ if (permissions.length === 0) {
28
+ return false;
29
+ }
30
+ return permissions.every((permission) => actor.has(permission));
31
+ }
32
+ function requiredPermissionsOf(job) {
33
+ return job.requiredPermissions;
34
+ }
35
+ function canView(job, actor, useCase) {
36
+ if (actor.has(useCase.requiredPermission) || job.createdBy.userId === actor.userId || isSameActorEmail(job.createdBy.email, actor.email)) {
37
+ return ALLOWED;
38
+ }
39
+ return deny("forbidden", "You cannot see this request.");
40
+ }
41
+ function canCreate(actor, useCase, request = {}) {
24
42
  if (useCase.createPermission && !actor.has(useCase.createPermission)) {
25
43
  return deny("forbidden", "You cannot open this request.");
26
44
  }
45
+ const raised = (request.requiredPermissions ?? []).filter(
46
+ (permission) => permission !== useCase.requiredPermission
47
+ );
48
+ if (raised.length > 0 && !hasEveryPermission(actor, raised)) {
49
+ return deny("forbidden", "You cannot open this request.");
50
+ }
51
+ if (useCase.creatorEmailRequired && !hasActorEmail(actor)) {
52
+ return deny(
53
+ "no-creator-email",
54
+ "This action needs the email address of the person who asks for it, and your session carries none. Sign in as a person to open this request."
55
+ );
56
+ }
57
+ if (useCase.reasonRequired && (request.reason ?? "").trim().length === 0) {
58
+ return deny("no-reason", "Give a reason to open this request.");
59
+ }
60
+ if (request.requiredAcks !== void 0 && request.requiredAcks < useCase.minAcks) {
61
+ return deny(
62
+ "too-few-acks",
63
+ `This action needs at least ${useCase.minAcks} approvals.`
64
+ );
65
+ }
27
66
  return ALLOWED;
28
67
  }
29
- function canAcknowledge(job, actor, useCase, now) {
30
- if (!actor.has(useCase.requiredPermission)) {
68
+ function resolveRequiredAcks(useCase, requested) {
69
+ if (requested === void 0) {
70
+ return Math.max(useCase.defaultAcks, useCase.minAcks);
71
+ }
72
+ return Math.max(Math.trunc(requested), useCase.minAcks);
73
+ }
74
+ function canAcknowledge(job, actor, now) {
75
+ if (!hasEveryPermission(actor, job.requiredPermissions)) {
31
76
  return deny("forbidden", "You cannot approve this request.");
32
77
  }
33
78
  if (isExpired(job, now)) {
@@ -55,8 +100,8 @@ function canAcknowledge(job, actor, useCase, now) {
55
100
  }
56
101
  return ALLOWED;
57
102
  }
58
- function canReject(job, actor, useCase, now, reason) {
59
- if (!actor.has(useCase.requiredPermission)) {
103
+ function canReject(job, actor, now, reason) {
104
+ if (!hasEveryPermission(actor, job.requiredPermissions)) {
60
105
  return deny("forbidden", "You cannot reject this request.");
61
106
  }
62
107
  if (reason.trim().length === 0) {
@@ -97,8 +142,8 @@ function canWithdraw(job, actor, now) {
97
142
  }
98
143
  return ALLOWED;
99
144
  }
100
- function canExecute(job, actor, useCase) {
101
- if (!actor.has(useCase.requiredPermission)) {
145
+ function canExecute(job, actor) {
146
+ if (!hasEveryPermission(actor, job.requiredPermissions)) {
102
147
  return deny("forbidden", "You cannot run this request.");
103
148
  }
104
149
  if (job.status !== "approved" && job.status !== "failed") {
@@ -114,10 +159,15 @@ export {
114
159
  canCreate,
115
160
  canExecute,
116
161
  canReject,
162
+ canView,
117
163
  canWithdraw,
118
164
  effectiveStatus,
165
+ hasActorEmail,
166
+ hasEveryPermission,
119
167
  isExpired,
120
168
  isSameActorEmail,
121
- remainingAcks
169
+ remainingAcks,
170
+ requiredPermissionsOf,
171
+ resolveRequiredAcks
122
172
  };
123
173
  //# sourceMappingURL=policy.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/approvals/policy.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 { UNKNOWN_ACTOR_EMAIL } from '../constants/misc.const.js'\nimport type {\n ApprovalJobDto,\n ApprovalUseCaseDto,\n} from '../dtos/ApprovalJob.dto.js'\n\n/**\n * The rules of the approval mechanism, as pure functions.\n *\n * Three consumers run this file: control-backend decides whether to reject a\n * request, control-frontend decides whether to disable a button, and cloudbot\n * decides whether to answer. One copy means the button and the endpoint can\n * never disagree.\n *\n * Two choices in the signatures carry the design:\n *\n * - The actor is an interface with a `has` method. The backend holds an array\n * of Auth0 claims, and `useSession()` in the frontend gives a flat record of\n * booleans. Neither side has to convert to the shape of the other.\n * - `now` is a parameter. The expiry is lazy, so a stored `pending` request can\n * be expired already. A parameter makes a difference between the two clocks\n * an input that a test can set, and not a silent disagreement.\n *\n * No function here does I/O or returns a promise. The browser call site is what\n * enforces that, and it also stops a later use case from turning a list of 50\n * rows into 200 network calls.\n */\n\n/** Who is asking. See the note above on why this is an interface. */\nexport interface ApprovalActorView {\n userId: string\n /**\n * The email of the actor. It is the SECOND identity of the four-eyes rule.\n *\n * `userId` alone is not enough. One person can hold more than one identity in\n * Auth0 \u2014 a connection through SSO and a connection through a database, or an\n * account that was made again \u2014 and each one carries a different `sub`. The\n * same human then opens a request with one identity and approves it with the\n * other, which is the rule defeated.\n *\n * It is optional because a token can carry no email. See\n * `isSameActorEmail` for what that means for the comparison.\n */\n email?: string\n has(permission: string): boolean\n}\n\n/**\n * Reports whether two email addresses name one person.\n *\n * Two values that are absent, empty, or `UNKNOWN_ACTOR_EMAIL` are NOT one\n * person. A token with no email would otherwise make every actor equal to\n * every other actor, and the rule would refuse everybody.\n *\n * The comparison removes the spaces and the case. Auth0 gives the address as\n * the identity provider wrote it, so `A.B@camunda.com` and `a.b@camunda.com`\n * are one mailbox and must not pass as two people.\n */\nexport function isSameActorEmail(one?: string, other?: string): boolean {\n const normalize = (value?: string): string => {\n const trimmed = value?.trim().toLowerCase() ?? ''\n return trimmed === UNKNOWN_ACTOR_EMAIL ? '' : trimmed\n }\n const left = normalize(one)\n return left.length > 0 && left === normalize(other)\n}\n\n/**\n * Why an action is not allowed.\n *\n * The code is for a program: control-backend maps it to a status, and the\n * frontend can branch on it. The message is for a person, and it is shown as\n * written.\n */\nexport type ApprovalDenialCode =\n | 'forbidden'\n | 'not-pending'\n | 'self-approval'\n | 'already-acknowledged'\n | 'expired'\n | 'not-creator'\n | 'not-executable'\n | 'no-reason'\n\nexport type ApprovalDecision =\n | { allowed: true }\n | { allowed: false; code: ApprovalDenialCode; message: string }\n\nconst ALLOWED: ApprovalDecision = { allowed: true }\n\nfunction deny(code: ApprovalDenialCode, message: string): ApprovalDecision {\n return { allowed: false, code, message }\n}\n\n/**\n * Reports whether the time to answer a request has passed.\n *\n * Only a `pending` request expires. A request that already reached an end state\n * keeps that state, because \"expired\" would lose the answer that was given.\n */\nexport function isExpired(job: ApprovalJobDto, now: Date): boolean {\n return (\n job.status === 'pending' &&\n new Date(job.expiresAt).getTime() <= now.getTime()\n )\n}\n\n/** Reports the status to show, which is `expired` once the time has passed. */\nexport function effectiveStatus(\n job: ApprovalJobDto,\n now: Date,\n): ApprovalJobDto['status'] {\n return isExpired(job, now) ? 'expired' : job.status\n}\n\n/** Reports how many more acknowledgements the request needs. */\nexport function remainingAcks(job: ApprovalJobDto): number {\n return Math.max(0, job.requiredAcks - job.acks.length)\n}\n\nexport function canCreate(\n actor: ApprovalActorView,\n useCase: ApprovalUseCaseDto,\n): ApprovalDecision {\n if (useCase.createPermission && !actor.has(useCase.createPermission)) {\n return deny('forbidden', 'You cannot open this request.')\n }\n return ALLOWED\n}\n\n/**\n * The four-eyes rule.\n *\n * The bans are in the order a person meets them, and each one has its own\n * message: \"you already approved this\" and \"you cannot approve your own\n * request\" are different mistakes, and one message for both helps nobody.\n *\n * Each of the two bans on a person reads BOTH identities: the `sub` of the\n * token, and the email. See `ApprovalActorView.email` for why one of them is\n * not enough.\n */\nexport function canAcknowledge(\n job: ApprovalJobDto,\n actor: ApprovalActorView,\n useCase: ApprovalUseCaseDto,\n now: Date,\n): ApprovalDecision {\n if (!actor.has(useCase.requiredPermission)) {\n return deny('forbidden', 'You cannot approve this request.')\n }\n if (isExpired(job, now)) {\n return deny(\n 'expired',\n 'This request expired and can no longer be approved.',\n )\n }\n if (job.status !== 'pending') {\n return deny(\n 'not-pending',\n `This request is ${job.status} and can no longer be approved.`,\n )\n }\n if (\n job.createdBy.userId === actor.userId ||\n isSameActorEmail(job.createdBy.email, actor.email)\n ) {\n return deny(\n 'self-approval',\n 'You opened this request, so you cannot approve it.',\n )\n }\n if (\n job.acks.some(\n (ack) =>\n ack.userId === actor.userId || isSameActorEmail(ack.email, actor.email),\n )\n ) {\n return deny('already-acknowledged', 'You already approved this request.')\n }\n return ALLOWED\n}\n\n/**\n * A rejection needs the same permission as an acknowledgement, and a reason.\n *\n * The creator MAY reject their own request. That is a withdrawal with a reason,\n * and stopping it would only push the person to the withdraw button, where the\n * reason is lost.\n */\nexport function canReject(\n job: ApprovalJobDto,\n actor: ApprovalActorView,\n useCase: ApprovalUseCaseDto,\n now: Date,\n reason: string,\n): ApprovalDecision {\n if (!actor.has(useCase.requiredPermission)) {\n return deny('forbidden', 'You cannot reject this request.')\n }\n if (reason.trim().length === 0) {\n return deny('no-reason', 'Give a reason to reject this request.')\n }\n if (isExpired(job, now)) {\n return deny(\n 'expired',\n 'This request expired and can no longer be rejected.',\n )\n }\n if (job.status !== 'pending') {\n return deny(\n 'not-pending',\n `This request is ${job.status} and can no longer be rejected.`,\n )\n }\n return ALLOWED\n}\n\n/** Only the person who opened a request can take it back. */\nexport function canWithdraw(\n job: ApprovalJobDto,\n actor: ApprovalActorView,\n now: Date,\n): ApprovalDecision {\n if (job.createdBy.userId !== actor.userId) {\n return deny(\n 'not-creator',\n 'Only the person who opened a request can withdraw it.',\n )\n }\n if (isExpired(job, now)) {\n return deny(\n 'expired',\n 'This request expired and can no longer be withdrawn.',\n )\n }\n if (job.status !== 'pending') {\n return deny(\n 'not-pending',\n `This request is ${job.status} and can no longer be withdrawn.`,\n )\n }\n return ALLOWED\n}\n\n/**\n * Runs a request that has its approvals.\n *\n * This is the button of the explicit mode. `failed` is executable again,\n * because a failure is often a problem of the system that is now repaired, and\n * asking for the approvals a second time helps nobody.\n */\nexport function canExecute(\n job: ApprovalJobDto,\n actor: ApprovalActorView,\n useCase: ApprovalUseCaseDto,\n): ApprovalDecision {\n if (!actor.has(useCase.requiredPermission)) {\n return deny('forbidden', 'You cannot run this request.')\n }\n if (job.status !== 'approved' && job.status !== 'failed') {\n return deny(\n 'not-executable',\n `This request is ${job.status} and cannot be run.`,\n )\n }\n return ALLOWED\n}\n"],
5
- "mappings": "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",
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 { UNKNOWN_ACTOR_EMAIL } from '../constants/misc.const.js'\nimport type {\n ApprovalJobDto,\n ApprovalUseCaseDto,\n} from '../dtos/ApprovalJob.dto.js'\n\n/**\n * The rules of the approval mechanism, as pure functions.\n *\n * Three consumers run this file: control-backend decides whether to reject a\n * request, control-frontend decides whether to disable a button, and cloudbot\n * decides whether to answer. One copy means the button and the endpoint can\n * never disagree.\n *\n * Two choices in the signatures carry the design:\n *\n * - The actor is an interface with a `has` method. The backend holds an array\n * of Auth0 claims, and `useSession()` in the frontend gives a flat record of\n * booleans. Neither side has to convert to the shape of the other.\n * - `now` is a parameter. The expiry is lazy, so a stored `pending` request can\n * be expired already. A parameter makes a difference between the two clocks\n * an input that a test can set, and not a silent disagreement.\n *\n * No function here does I/O or returns a promise. The browser call site is what\n * enforces that, and it also stops a later use case from turning a list of 50\n * rows into 200 network calls.\n */\n\n/** Who is asking. See the note above on why this is an interface. */\nexport interface ApprovalActorView {\n userId: string\n /**\n * The email of the actor. It is the SECOND identity of the four-eyes rule.\n *\n * `userId` alone is not enough. One person can hold more than one identity in\n * Auth0 \u2014 a connection through SSO and a connection through a database, or an\n * account that was made again \u2014 and each one carries a different `sub`. The\n * same human then opens a request with one identity and approves it with the\n * other, which is the rule defeated.\n *\n * It is optional because a token can carry no email. See\n * `isSameActorEmail` for what that means for the comparison.\n */\n email?: string\n has(permission: string): boolean\n}\n\n/**\n * Reports whether two email addresses name one person.\n *\n * Two values that are absent, empty, or `UNKNOWN_ACTOR_EMAIL` are NOT one\n * person. A token with no email would otherwise make every actor equal to\n * every other actor, and the rule would refuse everybody.\n *\n * The comparison removes the spaces and the case. Auth0 gives the address as\n * the identity provider wrote it, so `A.B@camunda.com` and `a.b@camunda.com`\n * are one mailbox and must not pass as two people.\n */\nexport function isSameActorEmail(one?: string, other?: string): boolean {\n const left = comparableActorEmail(one)\n return left.length > 0 && left === comparableActorEmail(other)\n}\n\n/**\n * The address in the one form that a comparison may use.\n *\n * An absent address, an empty one and `UNKNOWN_ACTOR_EMAIL` all become an\n * empty string, because none of the three names a person. The spaces and the\n * case go as well: Auth0 gives the address as the identity provider wrote it.\n */\nfunction comparableActorEmail(value?: string): string {\n const trimmed = value?.trim().toLowerCase() ?? ''\n return trimmed === UNKNOWN_ACTOR_EMAIL ? '' : trimmed\n}\n\n/**\n * Reports whether this actor carries an address that names a person.\n *\n * A machine-to-machine token carries none: accounts-backend builds the\n * permissions of such a token from its `permissions` claim alone, and there is\n * no person behind it. See `creatorEmailRequired` of the use case.\n */\nexport function hasActorEmail(actor: ApprovalActorView): boolean {\n return comparableActorEmail(actor.email).length > 0\n}\n\n/**\n * Why an action is not allowed.\n *\n * The code is for a program: control-backend maps it to a status, and the\n * frontend can branch on it. The message is for a person, and it is shown as\n * written.\n */\nexport type ApprovalDenialCode =\n | 'forbidden'\n | 'not-pending'\n | 'self-approval'\n | 'already-acknowledged'\n | 'expired'\n | 'not-creator'\n | 'not-executable'\n | 'no-reason'\n | 'too-few-acks'\n | 'no-creator-email'\n\nexport type ApprovalDecision =\n | { allowed: true }\n | { allowed: false; code: ApprovalDenialCode; message: string }\n\nconst ALLOWED: ApprovalDecision = { allowed: true }\n\nfunction deny(code: ApprovalDenialCode, message: string): ApprovalDecision {\n return { allowed: false, code, message }\n}\n\n/**\n * Reports whether the time to answer a request has passed.\n *\n * Only a `pending` request expires. A request that already reached an end state\n * keeps that state, because \"expired\" would lose the answer that was given.\n */\nexport function isExpired(job: ApprovalJobDto, now: Date): boolean {\n return (\n job.status === 'pending' &&\n new Date(job.expiresAt).getTime() <= now.getTime()\n )\n}\n\n/** Reports the status to show, which is `expired` once the time has passed. */\nexport function effectiveStatus(\n job: ApprovalJobDto,\n now: Date,\n): ApprovalJobDto['status'] {\n return isExpired(job, now) ? 'expired' : job.status\n}\n\n/** Reports how many more acknowledgements the request needs. */\nexport function remainingAcks(job: ApprovalJobDto): number {\n return Math.max(0, job.requiredAcks - job.acks.length)\n}\n\n/**\n * Reports whether the actor holds EVERY permission of the set.\n *\n * Every, and not one of them. A payload that raises the guard adds a\n * permission, so \"one of them\" would let the base permission alone carry a\n * request that the extra permission exists to stop.\n *\n * An EMPTY set is held by nobody. That is the opposite of what `Array.every`\n * answers, and it is deliberate: this function guards a privileged action, so\n * the empty case must fail closed. A row with no permissions is not a row that\n * everybody may run \u2014 it is a row that lost its guard, through a migration\n * that did not backfill it or a write that went around the service. Every use\n * case builds a set of at least one, so no real request meets this branch.\n */\nexport function hasEveryPermission(\n actor: ApprovalActorView,\n permissions: string[],\n): boolean {\n if (permissions.length === 0) {\n return false\n }\n return permissions.every((permission) => actor.has(permission))\n}\n\n/**\n * What a person must hold to act on a request.\n *\n * A request carries its own set, because a payload can raise the guard. A use\n * case that never does has a set of one, and it is the base permission.\n */\nexport function requiredPermissionsOf(job: ApprovalJobDto): string[] {\n return job.requiredPermissions\n}\n\n/**\n * Who can READ a request.\n *\n * Two people: one who can act on it, and the one who opened it. A Camundi who\n * holds neither has no reason to read which customer somebody proposed to\n * expire, so the list and the detail both hide it.\n *\n * The rule reads the BASE permission of the use case and not the effective set\n * of the request. A payload that raises the guard raises who can APPROVE, and\n * not who can look: a generation update with the yolo flag is visible to\n * everybody who can approve a plain one, and only the yolo permission runs it.\n * The list filters by use case in SQL, which is what makes one page of rows\n * the same page that a person is allowed to read.\n */\nexport function canView(\n job: ApprovalJobDto,\n actor: ApprovalActorView,\n useCase: ApprovalUseCaseDto,\n): ApprovalDecision {\n if (\n actor.has(useCase.requiredPermission) ||\n job.createdBy.userId === actor.userId ||\n isSameActorEmail(job.createdBy.email, actor.email)\n ) {\n return ALLOWED\n }\n return deny('forbidden', 'You cannot see this request.')\n}\n\n/**\n * Who can OPEN a request, and with what.\n *\n * `createPermission` is what keeps the pool that asks as small as the pool that\n * approves. A use case that leaves it absent says the opposite on purpose: the\n * approvals are the guard, and anybody signed in can propose the action.\n *\n * The two checks after it are about the request and not about the person. A\n * count below the floor of the use case, and a missing reason, are both a 400\n * and not a 403, so the words say which one the caller must repair.\n */\nexport function canCreate(\n actor: ApprovalActorView,\n useCase: ApprovalUseCaseDto,\n request: {\n reason?: string\n requiredAcks?: number\n /**\n * The effective permissions of the payload, from `permissionsFor`.\n *\n * Absent means \"the base permission alone\", which is what most use cases\n * build. Only the permissions ABOVE the base one gate opening a request:\n * see the note in the body.\n */\n requiredPermissions?: string[]\n } = {},\n): ApprovalDecision {\n if (useCase.createPermission && !actor.has(useCase.createPermission)) {\n return deny('forbidden', 'You cannot open this request.')\n }\n // What the PAYLOAD raised, on top of the base permission of the use case.\n // A bulk generation update with the yolo flag needs the yolo permission, and\n // without this check a person who holds the base permission alone could open\n // one. They could not approve it and could not run it, so it is not an\n // escalation \u2014 it is a request that its own author is not allowed to make,\n // waiting in the queue of the people who are.\n //\n // The BASE permission is deliberately not checked here. `createPermission`\n // above is the only gate on the person, and a use case that leaves it absent\n // means that anybody signed in can propose the action. Checking the base\n // permission too would take that choice away from every use case.\n const raised = (request.requiredPermissions ?? []).filter(\n (permission) => permission !== useCase.requiredPermission,\n )\n if (raised.length > 0 && !hasEveryPermission(actor, raised)) {\n return deny('forbidden', 'You cannot open this request.')\n }\n // The action of this use case carries the address of the person who asked,\n // so a caller with no address must be refused HERE. The alternative is a\n // request that looks ordinary, collects its approvals over days, and then\n // fails because the other service cannot find a user called \"unknown\".\n if (useCase.creatorEmailRequired && !hasActorEmail(actor)) {\n return deny(\n 'no-creator-email',\n 'This action needs the email address of the person who asks for it, ' +\n 'and your session carries none. Sign in as a person to open this ' +\n 'request.',\n )\n }\n if (useCase.reasonRequired && (request.reason ?? '').trim().length === 0) {\n return deny('no-reason', 'Give a reason to open this request.')\n }\n if (\n request.requiredAcks !== undefined &&\n request.requiredAcks < useCase.minAcks\n ) {\n return deny(\n 'too-few-acks',\n `This action needs at least ${useCase.minAcks} approvals.`,\n )\n }\n return ALLOWED\n}\n\n/**\n * The approvals that a new request asks for.\n *\n * The caller names a number or names none. A number below the floor is refused\n * by `canCreate`, and this function holds the answer inside the range anyway:\n * the two run in the same call, and a floor that only one of them knows is a\n * floor that a later caller can step over.\n */\nexport function resolveRequiredAcks(\n useCase: ApprovalUseCaseDto,\n requested?: number,\n): number {\n if (requested === undefined) {\n return Math.max(useCase.defaultAcks, useCase.minAcks)\n }\n return Math.max(Math.trunc(requested), useCase.minAcks)\n}\n\n/**\n * The four-eyes rule.\n *\n * The bans are in the order a person meets them, and each one has its own\n * message: \"you already approved this\" and \"you cannot approve your own\n * request\" are different mistakes, and one message for both helps nobody.\n *\n * Each of the two bans on a person reads BOTH identities: the `sub` of the\n * token, and the email. See `ApprovalActorView.email` for why one of them is\n * not enough.\n */\nexport function canAcknowledge(\n job: ApprovalJobDto,\n actor: ApprovalActorView,\n now: Date,\n): ApprovalDecision {\n if (!hasEveryPermission(actor, job.requiredPermissions)) {\n return deny('forbidden', 'You cannot approve this request.')\n }\n if (isExpired(job, now)) {\n return deny(\n 'expired',\n 'This request expired and can no longer be approved.',\n )\n }\n if (job.status !== 'pending') {\n return deny(\n 'not-pending',\n `This request is ${job.status} and can no longer be approved.`,\n )\n }\n if (\n job.createdBy.userId === actor.userId ||\n isSameActorEmail(job.createdBy.email, actor.email)\n ) {\n return deny(\n 'self-approval',\n 'You opened this request, so you cannot approve it.',\n )\n }\n if (\n job.acks.some(\n (ack) =>\n ack.userId === actor.userId || isSameActorEmail(ack.email, actor.email),\n )\n ) {\n return deny('already-acknowledged', 'You already approved this request.')\n }\n return ALLOWED\n}\n\n/**\n * A rejection needs the same permission as an acknowledgement, and a reason.\n *\n * The creator MAY reject their own request. That is a withdrawal with a reason,\n * and stopping it would only push the person to the withdraw button, where the\n * reason is lost.\n */\nexport function canReject(\n job: ApprovalJobDto,\n actor: ApprovalActorView,\n now: Date,\n reason: string,\n): ApprovalDecision {\n if (!hasEveryPermission(actor, job.requiredPermissions)) {\n return deny('forbidden', 'You cannot reject this request.')\n }\n if (reason.trim().length === 0) {\n return deny('no-reason', 'Give a reason to reject this request.')\n }\n if (isExpired(job, now)) {\n return deny(\n 'expired',\n 'This request expired and can no longer be rejected.',\n )\n }\n if (job.status !== 'pending') {\n return deny(\n 'not-pending',\n `This request is ${job.status} and can no longer be rejected.`,\n )\n }\n return ALLOWED\n}\n\n/** Only the person who opened a request can take it back. */\nexport function canWithdraw(\n job: ApprovalJobDto,\n actor: ApprovalActorView,\n now: Date,\n): ApprovalDecision {\n if (job.createdBy.userId !== actor.userId) {\n return deny(\n 'not-creator',\n 'Only the person who opened a request can withdraw it.',\n )\n }\n if (isExpired(job, now)) {\n return deny(\n 'expired',\n 'This request expired and can no longer be withdrawn.',\n )\n }\n if (job.status !== 'pending') {\n return deny(\n 'not-pending',\n `This request is ${job.status} and can no longer be withdrawn.`,\n )\n }\n return ALLOWED\n}\n\n/**\n * Runs a request that has its approvals.\n *\n * This is the button of the explicit mode. `failed` is executable again,\n * because a failure is often a problem of the system that is now repaired, and\n * asking for the approvals a second time helps nobody.\n *\n * ANY holder of the permission may press it, and not only the person who\n * opened the request. The decision was made when the request reached\n * `approved`: `canAcknowledge` refuses the creator, so the approvals on the\n * row came from other people, and the button only carries out what they\n * already agreed to. Holding the button to the creator would let one Camundi\n * on holiday freeze an approved action, and a use case that must not run\n * without its author has `executionMode: 'auto'` instead.\n *\n * Who PRESSED it and who ASKED for it are two different facts, and both are\n * kept: the audit trail of control-backend records the actor of the run, and\n * an action that names a person to another service names the creator. See\n * `onBehalfOf` of `extend-trial`.\n */\nexport function canExecute(\n job: ApprovalJobDto,\n actor: ApprovalActorView,\n): ApprovalDecision {\n if (!hasEveryPermission(actor, job.requiredPermissions)) {\n return deny('forbidden', 'You cannot run this request.')\n }\n if (job.status !== 'approved' && job.status !== 'failed') {\n return deny(\n 'not-executable',\n `This request is ${job.status} and cannot be run.`,\n )\n }\n return ALLOWED\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The names and the value ranges of the registered use cases.
3
+ *
4
+ * control-backend owns the registry, and a frontend reads it at run time
5
+ * (`GET /api/approvals/use-cases`). Two things cannot wait for that answer:
6
+ *
7
+ * - **The identifier**, because a caller must NAME a use case to find it in the
8
+ * registry, and to open a request of it.
9
+ * - **The range of a payload field**, because a form holds the value before any
10
+ * call goes out.
11
+ *
12
+ * Both live here, so the schema of the service and the form of the browser can
13
+ * never hold different numbers.
14
+ */
15
+ /** The identifier of each use case, on the wire and in the table. */
16
+ export declare const APPROVAL_USE_CASE_TYPES: {
17
+ readonly sendToExpiredTrial: "send-to-expired-trial";
18
+ readonly extendTrial: "extend-trial";
19
+ };
20
+ export type ApprovalUseCaseType = (typeof APPROVAL_USE_CASE_TYPES)[keyof typeof APPROVAL_USE_CASE_TYPES];
21
+ /**
22
+ * The shortest extension that accounts-backend accepts.
23
+ *
24
+ * `POST /:orgId/extendtrial` refuses a date that is less than 14 days away
25
+ * when the body carries `onBehalfOf`, and it measures those 14 days when the
26
+ * CALL arrives. The floor here is 15 and not 14, for two reasons:
27
+ *
28
+ * - The two clocks are not the same instant. Accounts builds its own "14 days
29
+ * from now" a few milliseconds after control-backend builds the date, so a
30
+ * date of exactly 14 days is already in the past for the comparison.
31
+ * - Accounts counts with `setDate`, which is calendar arithmetic. A change of
32
+ * the daylight saving time moves its limit by an hour.
33
+ */
34
+ export declare const EXTEND_TRIAL_MIN_DAYS = 15;
35
+ /**
36
+ * The longest extension that accounts-backend accepts.
37
+ *
38
+ * The same route refuses a date more than 62 days away. 61 keeps the same
39
+ * margin as the floor above.
40
+ */
41
+ export declare const EXTEND_TRIAL_MAX_DAYS = 61;
@@ -0,0 +1,12 @@
1
+ const APPROVAL_USE_CASE_TYPES = {
2
+ sendToExpiredTrial: "send-to-expired-trial",
3
+ extendTrial: "extend-trial"
4
+ };
5
+ const EXTEND_TRIAL_MIN_DAYS = 15;
6
+ const EXTEND_TRIAL_MAX_DAYS = 61;
7
+ export {
8
+ APPROVAL_USE_CASE_TYPES,
9
+ EXTEND_TRIAL_MAX_DAYS,
10
+ EXTEND_TRIAL_MIN_DAYS
11
+ };
12
+ //# sourceMappingURL=use-cases.const.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/approvals/use-cases.const.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 names and the value ranges of the registered use cases.\n *\n * control-backend owns the registry, and a frontend reads it at run time\n * (`GET /api/approvals/use-cases`). Two things cannot wait for that answer:\n *\n * - **The identifier**, because a caller must NAME a use case to find it in the\n * registry, and to open a request of it.\n * - **The range of a payload field**, because a form holds the value before any\n * call goes out.\n *\n * Both live here, so the schema of the service and the form of the browser can\n * never hold different numbers.\n */\n\n/** The identifier of each use case, on the wire and in the table. */\nexport const APPROVAL_USE_CASE_TYPES = {\n sendToExpiredTrial: 'send-to-expired-trial',\n extendTrial: 'extend-trial',\n} as const\n\nexport type ApprovalUseCaseType =\n (typeof APPROVAL_USE_CASE_TYPES)[keyof typeof APPROVAL_USE_CASE_TYPES]\n\n/**\n * The shortest extension that accounts-backend accepts.\n *\n * `POST /:orgId/extendtrial` refuses a date that is less than 14 days away\n * when the body carries `onBehalfOf`, and it measures those 14 days when the\n * CALL arrives. The floor here is 15 and not 14, for two reasons:\n *\n * - The two clocks are not the same instant. Accounts builds its own \"14 days\n * from now\" a few milliseconds after control-backend builds the date, so a\n * date of exactly 14 days is already in the past for the comparison.\n * - Accounts counts with `setDate`, which is calendar arithmetic. A change of\n * the daylight saving time moves its limit by an hour.\n */\nexport const EXTEND_TRIAL_MIN_DAYS = 15\n\n/**\n * The longest extension that accounts-backend accepts.\n *\n * The same route refuses a date more than 62 days away. 61 keeps the same\n * margin as the floor above.\n */\nexport const EXTEND_TRIAL_MAX_DAYS = 61\n"],
5
+ "mappings": "AAsBO,MAAM,0BAA0B;AAAA,EACrC,oBAAoB;AAAA,EACpB,aAAa;AACf;AAkBO,MAAM,wBAAwB;AAQ9B,MAAM,wBAAwB;",
6
+ "names": []
7
+ }
@@ -22,8 +22,14 @@ const CONFIG = {
22
22
  "accounts:trialExtender",
23
23
  "accounts:limitExtender",
24
24
  "support:agent"
25
- ]
25
+ ],
26
26
  // Pre-Sales as per https://confluence.camunda.com/spaces/HAN/pages/168660217/Adjust+C8+SaaS+permissions+and+sales+plan#AdjustC8SaaSpermissionsandsalesplan-DetailsperRole
27
+ // Enterprise Success holds the CSMs and the TAMs. A customer who has
28
+ // privacy concerns asks them to switch optional telemetry off, thus they
29
+ // carry the capability. HiBob marks no TAM, thus the division is the
30
+ // smallest group that holds all of them.
31
+ "266229124": ["console:optionalTelemetryManager"]
32
+ // Enterprise Success
27
33
  },
28
34
  subdivisionRules: {
29
35
  // The console team is the "Control plane" subdivision. The id was
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/config/camundi-permission-rules.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 { CamundiPermissionRules } from './camundi-permission-rules.types.js'\n\nconst CONFIG: CamundiPermissionRules = {\n departmentRules: {\n '205882305': ['accounts:read', 'accounts:trialExtender'], // CS\n Marketing: ['accounts:read', 'accounts:trialExtender'],\n '205882307': ['accounts:read', 'accounts:trialExtender'], // revops\n Products: ['console:admin:readonly', 'accounts:read'],\n Executive: ['accounts:read'],\n '259633605': ['accounts:read', 'accounts:trialExtender'], // Sales\n },\n divisionRules: {\n Engineering: ['console:admin:readonly'],\n '259633608': [\n 'accounts:connections:create',\n 'accounts:connections:update',\n 'support:agent',\n ], // Support\n '259633611': [\n 'accounts:trialExtender',\n 'accounts:limitExtender',\n 'support:agent',\n ], // Pre-Sales as per https://confluence.camunda.com/spaces/HAN/pages/168660217/Adjust+C8+SaaS+permissions+and+sales+plan#AdjustC8SaaSpermissionsandsalesplan-DetailsperRole\n },\n subdivisionRules: {\n // The console team is the \"Control plane\" subdivision. The id was\n // `256909487` until a HiBob restructure, and the augmenter matches a\n // subdivision exactly and stays quiet on a miss, thus the rule granted\n // nothing to anybody and nobody saw it.\n '267516827': ['accounts:admin', 'console:admin', 'consoleTeam'], // Control plane\n 'Engineering Management': [\n 'accounts:addSafeRoles',\n 'accounts:orgCreator',\n 'console:C8ReleaseManager',\n ], // Engineering Management\n // Same story: the id `258049291` is gone. The subdivision carries the name\n // `Controller` now, and a name is a valid key \u2014 `Engineering Management`\n // above is one too.\n Controller: ['console:clusterPlanManager', 'console:operationsEngineer'],\n },\n}\nexport default CONFIG\n"],
5
- "mappings": "AAQA,MAAM,SAAiC;AAAA,EACrC,iBAAiB;AAAA,IACf,aAAa,CAAC,iBAAiB,wBAAwB;AAAA;AAAA,IACvD,WAAW,CAAC,iBAAiB,wBAAwB;AAAA,IACrD,aAAa,CAAC,iBAAiB,wBAAwB;AAAA;AAAA,IACvD,UAAU,CAAC,0BAA0B,eAAe;AAAA,IACpD,WAAW,CAAC,eAAe;AAAA,IAC3B,aAAa,CAAC,iBAAiB,wBAAwB;AAAA;AAAA,EACzD;AAAA,EACA,eAAe;AAAA,IACb,aAAa,CAAC,wBAAwB;AAAA,IACtC,aAAa;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA;AAAA,IACA,aAAa;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA;AAAA,EACF;AAAA,EACA,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKhB,aAAa,CAAC,kBAAkB,iBAAiB,aAAa;AAAA;AAAA,IAC9D,0BAA0B;AAAA,MACxB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA,IAIA,YAAY,CAAC,8BAA8B,4BAA4B;AAAA,EACzE;AACF;AACA,IAAO,mCAAQ;",
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 { CamundiPermissionRules } from './camundi-permission-rules.types.js'\n\nconst CONFIG: CamundiPermissionRules = {\n departmentRules: {\n '205882305': ['accounts:read', 'accounts:trialExtender'], // CS\n Marketing: ['accounts:read', 'accounts:trialExtender'],\n '205882307': ['accounts:read', 'accounts:trialExtender'], // revops\n Products: ['console:admin:readonly', 'accounts:read'],\n Executive: ['accounts:read'],\n '259633605': ['accounts:read', 'accounts:trialExtender'], // Sales\n },\n divisionRules: {\n Engineering: ['console:admin:readonly'],\n '259633608': [\n 'accounts:connections:create',\n 'accounts:connections:update',\n 'support:agent',\n ], // Support\n '259633611': [\n 'accounts:trialExtender',\n 'accounts:limitExtender',\n 'support:agent',\n ], // Pre-Sales as per https://confluence.camunda.com/spaces/HAN/pages/168660217/Adjust+C8+SaaS+permissions+and+sales+plan#AdjustC8SaaSpermissionsandsalesplan-DetailsperRole\n // Enterprise Success holds the CSMs and the TAMs. A customer who has\n // privacy concerns asks them to switch optional telemetry off, thus they\n // carry the capability. HiBob marks no TAM, thus the division is the\n // smallest group that holds all of them.\n '266229124': ['console:optionalTelemetryManager'], // Enterprise Success\n },\n subdivisionRules: {\n // The console team is the \"Control plane\" subdivision. The id was\n // `256909487` until a HiBob restructure, and the augmenter matches a\n // subdivision exactly and stays quiet on a miss, thus the rule granted\n // nothing to anybody and nobody saw it.\n '267516827': ['accounts:admin', 'console:admin', 'consoleTeam'], // Control plane\n 'Engineering Management': [\n 'accounts:addSafeRoles',\n 'accounts:orgCreator',\n 'console:C8ReleaseManager',\n ], // Engineering Management\n // Same story: the id `258049291` is gone. The subdivision carries the name\n // `Controller` now, and a name is a valid key \u2014 `Engineering Management`\n // above is one too.\n Controller: ['console:clusterPlanManager', 'console:operationsEngineer'],\n },\n}\nexport default CONFIG\n"],
5
+ "mappings": "AAQA,MAAM,SAAiC;AAAA,EACrC,iBAAiB;AAAA,IACf,aAAa,CAAC,iBAAiB,wBAAwB;AAAA;AAAA,IACvD,WAAW,CAAC,iBAAiB,wBAAwB;AAAA,IACrD,aAAa,CAAC,iBAAiB,wBAAwB;AAAA;AAAA,IACvD,UAAU,CAAC,0BAA0B,eAAe;AAAA,IACpD,WAAW,CAAC,eAAe;AAAA,IAC3B,aAAa,CAAC,iBAAiB,wBAAwB;AAAA;AAAA,EACzD;AAAA,EACA,eAAe;AAAA,IACb,aAAa,CAAC,wBAAwB;AAAA,IACtC,aAAa;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA;AAAA,IACA,aAAa;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,aAAa,CAAC,kCAAkC;AAAA;AAAA,EAClD;AAAA,EACA,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKhB,aAAa,CAAC,kBAAkB,iBAAiB,aAAa;AAAA;AAAA,IAC9D,0BAA0B;AAAA,MACxB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA,IAIA,YAAY,CAAC,8BAA8B,4BAA4B;AAAA,EACzE;AACF;AACA,IAAO,mCAAQ;",
6
6
  "names": []
7
7
  }
@@ -338,9 +338,16 @@ export declare const APP_INTEGRATIONS_89_STREAM_MINOR = "8.9.0";
338
338
  */
339
339
  export declare const CLUSTER_SECRETS_LABEL_MIN_VERSION = "8.10.0-alpha5";
340
340
  /**
341
- * Minimum cluster (zeebe) version for the optional telemetry toggle.
341
+ * Minimum cluster (zeebe) version that runs the analytics exporter, thus the
342
+ * floor for optional telemetry.
343
+ *
344
+ * The number comes from the operator: `AnalyticsExporterSupported` in
345
+ * `pkg/features/features.go` reads `>= 8.10.0-alpha1`, and below it
346
+ * `zeebe_statefulset.go` adds no exporter env vars at all. A cluster under the
347
+ * floor sends no analytics telemetry, OPTIONAL or CONTRACTUAL, thus the row
348
+ * has no state to show and the toggle has nothing to switch.
342
349
  */
343
- export declare const OPTIONAL_TELEMETRY_MIN_VERSION = "8.10.0-alpha5";
350
+ export declare const OPTIONAL_TELEMETRY_MIN_VERSION = "8.10.0-alpha1";
344
351
  /**
345
352
  * Lowest ALPHA generation that a suspended cluster can resume from. A cluster
346
353
  * on an alpha under this floor stays suspended forever.
@@ -50,7 +50,7 @@ const APP_INTEGRATIONS_89_STREAM_ZEEBE_MIN_VERSION = "8.9.14";
50
50
  const APP_INTEGRATIONS_89_STREAM_CONNECTOR_BRIDGE_MIN_VERSION = "8.9.7";
51
51
  const APP_INTEGRATIONS_89_STREAM_MINOR = "8.9.0";
52
52
  const CLUSTER_SECRETS_LABEL_MIN_VERSION = CONNECTOR_SECRET_SYNC_MIN_VERSION;
53
- const OPTIONAL_TELEMETRY_MIN_VERSION = "8.10.0-alpha5";
53
+ const OPTIONAL_TELEMETRY_MIN_VERSION = "8.10.0-alpha1";
54
54
  const RESUME_ALPHA_MIN_VERSION = "8.10.0-alpha5";
55
55
  export {
56
56
  ADMIN_APP_RENAME_MIN_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/constants/version-gates.const.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 * Central list of the cluster version floors that gate features.\n *\n * Every `checkVersion({ versionToCompareTo })` / `checkClusterVersion()` call in\n * the apps and in `packages/c4` reads its floor from here. Put a new floor here\n * before you use it. Do not write a version literal at the call site \u2014 the same\n * feature is usually gated in more than one place (a c4 tile, a console-backend\n * guard, a CRD spec), and two literals drift. Incident #9703 was two copies of\n * one floor that disagreed.\n *\n * `packages/c3` keeps its own copy of the version helpers and is out of scope:\n * it is a public package that this repo must not change.\n *\n * The values compare with semver, so the spelling of a floor decides which\n * prerelease cohort it covers:\n * - `8.10.0-0` and `8.10.0-alpha0` sort below every `8.10.0-alphaN`.\n * - `8.10.0-alpha1` sorts above `alpha0` but below `alpha2`.\n * - `8.10.0` sorts above ALL `8.10.0-*` prereleases.\n *\n * Two constants in this file can hold the same value. Then they gate different\n * features and can move apart. When a name is an alias of another constant, the\n * two answer the same question and must never drift.\n *\n * A call site compares a floor against the zeebe version of the generation,\n * unless the comment of the constant names a different field. Name that field\n * in the comment when you add a floor for one, because the same number means a\n * different thing for `connectorBridge` or for a generation name.\n */\n\n// ---------------------------------------------------------------------------\n// Release-line floors\n//\n// Used where the code asks \"is this generation on the 8.x line?\" and no single\n// feature owns the answer. Both spell the line start as `-0`, so every\n// prerelease of that line is included.\n// ---------------------------------------------------------------------------\n\n/** Start of the 8.9 line, prereleases included. */\nexport const LINE_8_9_MIN_VERSION = '8.9.0-0'\n\n/** Start of the 8.10 line, prereleases included. */\nexport const LINE_8_10_MIN_VERSION = '8.10.0-0'\n\n// ---------------------------------------------------------------------------\n// Backups\n// ---------------------------------------------------------------------------\n\n/** Hot backups are available on the cluster (c4 hot-backups tab). */\nexport const HOT_BACKUP_MIN_VERSION = '8.2.4'\n\n// ---------------------------------------------------------------------------\n// Identity, authorizations and roles\n// ---------------------------------------------------------------------------\n\n/**\n * The \"enforce groups\" toggle is offered (c4 settings tile). The tile also\n * disappears again on 8.8 clusters that run Tasklist V2 \u2014 see\n * `ENFORCE_GROUPS_TASKLIST_V2_HIDDEN_MIN_VERSION`.\n */\nexport const ENFORCE_GROUPS_TOGGLE_MIN_VERSION = '8.5.0-alpha2'\n\n/** Resource-based authorizations exist. Replaced by identity authorizations. */\nexport const RESOURCE_BASED_AUTHORIZATIONS_MIN_VERSION = '8.6.0'\n\n/**\n * Identity-based authorizations replace the resource-based ones. Read as an\n * upper bound by the c4 settings tile: below this the cluster still shows the\n * resource-based section.\n */\nexport const IDENTITY_AUTHORIZATION_MIN_VERSION = '8.8.0-alpha1'\n\n/**\n * Upper bound of the \"your upgrade turns on identity authorizations\" notice in\n * the c4 overview tab. The notice is for 8.8-line targets only, so an 8.9\n * target does not show it.\n */\nexport const IDENTITY_AUTHORIZATION_NOTICE_MAX_VERSION = '8.9.0-alpha1'\n\n/**\n * The cluster uses the 8.8 role model, so the legacy per-cluster roles are\n * gone (c4 user-details and management widgets).\n */\nexport const NEW_CLUSTER_ROLES_MIN_VERSION = '8.8.0-alpha4'\n\n/**\n * The authorizations toggle is offered while creating a cluster (c4 create\n * cluster wizard).\n */\nexport const AUTHORIZATION_TOGGLE_MIN_VERSION = '8.8.0-alpha4'\n\n/**\n * Identity manages users, groups and tenants. The cluster shows the Identity\n * screens (c4 cluster details, user details, group details, management widget,\n * console-frontend and console-sm-frontend), and the CRD spec sends the\n * internal client IDs to Identity\n * (`K8sSpecUtils.clusterHasEnabledIdentityIntegration`).\n *\n * console-backend used `8.8.0-alpha4` until the gates moved here. That floor\n * was one alpha under the frontend. As a result, an 8.8.0-alpha4 cluster got\n * its `internalClientIDs` sent to an Identity that the UI did not show. There\n * is one floor now, on the version the UI has always used.\n */\nexport const IDENTITY_MANAGEMENT_MIN_VERSION = '8.8.0-alpha5'\n\n/** The multi-tenancy toggle is offered (c4 settings tile). */\nexport const MULTI_TENANCY_TOGGLE_MIN_VERSION = '8.8.0-alpha7'\n\n/** Business-ID uniqueness toggle is offered (c4 settings tile). */\nexport const BUSINESS_ID_UNIQUENESS_MIN_VERSION = '8.9.0-alpha5'\n\n// ---------------------------------------------------------------------------\n// Tasklist\n// ---------------------------------------------------------------------------\n\n/**\n * The current generation is good enough to upgrade to Tasklist V2 (c4 overview\n * tab).\n */\nexport const TASKLIST_V2_UPGRADE_MIN_VERSION = '8.7.0-alpha4'\n\n/**\n * The upgrade target carries Tasklist V2 (c4 overview tab). Pairs with\n * `LINE_8_10_MIN_VERSION` as the upper bound, because from 8.10 on the setting\n * is gone.\n */\nexport const TASKLIST_V2_UPGRADE_TARGET_MIN_VERSION = '8.8.0-alpha1'\n\n/**\n * A new cluster gets `tasklistV2Enabled` by default (console-backend cluster\n * creation). Reads the generation's tasklist version when it has one, else\n * zeebe.\n */\nexport const TASKLIST_V2_DEFAULT_ON_MIN_VERSION = '8.8.0'\n\n/**\n * From this version, the c4 settings tile hides the \"enforce groups\" toggle on\n * a cluster that runs Tasklist V2. The tile reads the constant with `>=`, and\n * it also needs `tasklistV2Enabled` to be true.\n *\n * The name says MIN because of that `>=`. The other MAX names in this file mark\n * the version where a gate stops, and their call sites read them with `<`.\n */\nexport const ENFORCE_GROUPS_TASKLIST_V2_HIDDEN_MIN_VERSION = '8.8.0'\n\n/** The Tasklist upgrade banner is offered (c4 settings tile). */\nexport const TASKLIST_UPGRADE_BANNER_MIN_VERSION = '8.8.0-alpha2'\n\n/**\n * The Tasklist mode toggle is offered while creating a cluster (c4 create\n * cluster wizard). Upper bound is `LINE_8_10_MIN_VERSION`.\n */\nexport const TASKLIST_MODE_TOGGLE_MIN_VERSION = '8.8.0-alpha8'\n\n/**\n * Minor-version pair that turns Tasklist V2 on during a generation update\n * (console-backend). Both are read with `VersionCheckOperator.SameMinor`: an\n * 8.7 cluster that moves to an 8.8 generation gets the setting.\n */\nexport const TASKLIST_V2_AUTO_ENABLE_FROM_MINOR = '8.7.0'\nexport const TASKLIST_V2_AUTO_ENABLE_TO_MINOR = '8.8.0'\n\n/**\n * The version where the Tasklist V2 flag stops to mean anything, because there\n * is no Tasklist V1 left to switch away from. Two readers use it. The CRD spec\n * stops to emit the Tasklist V2 mode environment variable\n * (`K8sSpecUtils.shouldSkipTasklistV2ModeEnvVar`). The accounts-frontend\n * support rows stop to show the flag.\n *\n * Spelled `8.10.0` and NOT `LINE_8_10_MIN_VERSION` (`8.10.0-0`). Cluster\n * creation sets the flag on every generation from\n * `TASKLIST_V2_DEFAULT_ON_MIN_VERSION` on. An 8.10 prerelease thus carries a\n * live value, and it must stay under this bound.\n *\n * c4 bounds its own Tasklist V2 checks with `LINE_8_10_MIN_VERSION` instead.\n * That constant answers a different question: can Console still offer the\n * Tasklist upgrade to this cluster. It correctly leaves out the 8.10\n * prereleases.\n */\nexport const TASKLIST_V2_SETTING_MAX_VERSION = '8.10.0'\n\n// ---------------------------------------------------------------------------\n// Connectors\n// ---------------------------------------------------------------------------\n\n/** The internal connectors-management page exists (c4 overview tab link). */\nexport const CONNECTORS_MANAGEMENT_LINK_MIN_VERSION = '8.7.0-alpha1'\n\n/**\n * The applications list offers \"Manage\" for connectors (c4 overview tab).\n * Matches what console-frontend has always done.\n */\nexport const INTERNAL_CONNECTORS_MANAGEMENT_MIN_VERSION = '8.8.0-alpha1'\n\n/**\n * Connector management shows the outbound connectors, the per-connector metrics\n * and the restart button. Under this floor the pages show the inbound-only\n * table.\n *\n * CAUTION: the two readers compare this floor against different version fields.\n * The c4 connector-management widgets compare\n * `generation.versions.connectorBridge`\n * (`useConnectorManagementRuntime.checkClusterVersion`). The\n * console-sm-frontend pages compare the generation name of the cluster\n * (`page.getData().generation`). Both sites had the same literal before the\n * gates moved here, so this constant keeps that. Do not read it as one\n * question until the two readers compare the same field.\n */\nexport const CONNECTOR_MANAGEMENT_METRICS_MIN_VERSION = '8.10.0-alpha3'\n\n/**\n * The secrets filter mode setting is offered on the 8.6 line (zeebe). Shared by\n * isSecretsFilterModeConfigurable (c4 settings tile + console-backend update\n * guard).\n */\nexport const SECRETS_FILTER_MODE_8_6_MIN_VERSION = '8.6.28'\n\n/** Same setting, floor for the 8.7 line. */\nexport const SECRETS_FILTER_MODE_8_7_MIN_VERSION = '8.7.25'\n\n/** Same setting, floor for the 8.8 line. */\nexport const SECRETS_FILTER_MODE_8_8_MIN_VERSION = '8.8.19'\n\n/**\n * Same setting, floor for the 8.9 line. The env var works from 8.9.7, but the\n * default stays DISABLED until 8.9.10. The floor waits for that default\n * change, so the toggle never shows an unset value with an unknown default.\n */\nexport const SECRETS_FILTER_MODE_8_9_MIN_VERSION = '8.9.10'\n\n/**\n * Same setting, floor for the 8.10 line and every line above it.\n *\n * This is the last floor: isSecretsFilterModeConfigurable applies it to all\n * versions above the 8.9 line, so 8.11 and later need no floor of their own.\n *\n * Two different reasons keep the versions below it out, and the second one is\n * why this floor is alpha6 and not alpha5:\n *\n * - alpha1 to alpha4 shipped the DISABLED default. Same reason as 8.9.7-8.9.9.\n * - alpha5 shipped the STRICT default, but not the Connectors change that this\n * setting needs. SaaS runs Connectors with legacy.mode=FALLBACK from the 8.10\n * line on, and the Connectors runtime of alpha5 refuses to start unless\n * FALLBACK is paired with STRICT\n * (ConnectorsAutoConfiguration.checkLegacyFallbackSecretFilter). A customer\n * who selects LAX or DISABLED on alpha5 crashes their own Connectors runtime\n * at startup, so the toggle must stay hidden there.\n *\n * Connectors removed that check after the alpha5 release (2026-09-03). The\n * removal is in the SNAPSHOT images and lands in the releases from alpha6 on.\n * SNAPSHOT clusters run the newest images, so they get the toggle.\n */\nexport const SECRETS_FILTER_MODE_8_10_MIN_VERSION = '8.10.0-alpha6'\n\n// ---------------------------------------------------------------------------\n// APIs and cluster URLs\n// ---------------------------------------------------------------------------\n\n/** The client details page can transform the cluster URL string (c4). */\nexport const REST_API_URL_TRANSFORM_MIN_VERSION = '8.6.0'\n\n/** The cluster publishes a Zeebe REST URL, so the client details page uses it. */\nexport const ZEEBE_REST_URL_MIN_VERSION = '8.8.0-alpha1'\n\n/** The cluster serves the v2 REST API under `<clusterUrl>/v2` (c4). */\nexport const REST_API_V2_MIN_VERSION = '8.10.0-alpha1'\n\n/** The Swagger toggle and the Swagger link are offered (c4). */\nexport const SWAGGER_MIN_VERSION = '8.8.0-alpha8'\n\n/**\n * The Swagger URL moves to its 8.9 shape (c4 `buildClusterSwaggerUrl`).\n * Spelled `8.9.0`, so 8.9 prereleases keep the older URL.\n */\nexport const SWAGGER_URL_8_9_MIN_VERSION = '8.9.0'\n\n/** The MCP toggle and the MCP client tab are offered (c4). */\nexport const MCP_MIN_VERSION = '8.9.0-alpha4'\n\n// ---------------------------------------------------------------------------\n// Apps, tabs and dashboards\n// ---------------------------------------------------------------------------\n\n/**\n * Identity is called \"Admin\" from here on (c4 navigation and overview tab,\n * console-sm-frontend). The URL and the permission still say `identity`.\n */\nexport const ADMIN_APP_RENAME_MIN_VERSION = '8.9.0-alpha5'\n\n/** The job dashboard is offered (c4 overview tab and console-sm-frontend). */\nexport const JOB_DASHBOARD_MIN_VERSION = '8.9.0-alpha5'\n\n/** The cluster serves the statistics config endpoint (c4 cluster details). */\nexport const CLUSTER_STATISTICS_CONFIG_MIN_VERSION = '8.9.0-alpha5'\n\n/** The audit log tab is offered (c4 cluster details). */\nexport const AUDIT_LOG_TAB_MIN_VERSION = '8.9.0-alpha5'\n\n// ---------------------------------------------------------------------------\n// Cluster spec and provisioning\n// ---------------------------------------------------------------------------\n\n/** Only 8.7 and newer generations are offered to AWS-only orgs (console-backend). */\nexport const AWS_ONLY_GENERATION_MIN_VERSION = '8.7.0'\n\n/**\n * The gateway spec takes its 8.9 shape (console-backend\n * `K8sSpecUtils.getGatewaySpec`). Spelled `-alpha0`, so every 8.9 prerelease\n * is included.\n */\nexport const GATEWAY_SPEC_8_9_MIN_VERSION = '8.9.0-alpha0'\n\n/**\n * The operator wires the session idle-timeout config through to Operate,\n * Tasklist and the unified app (camunda-security-library ADR-0042,\n * camunda-operator#5863). This floor matches the `SessionConfigSupported` gate\n * of the operator. The CRD spec and the c4 settings tile both read it, which is\n * why it lives here.\n */\nexport const SESSION_CONFIG_MIN_VERSION = '8.10.0-alpha5'\n\n/** A new cluster gets Optimize turned on by default (console-backend). */\nexport const OPTIMIZE_DEFAULT_ON_MIN_VERSION = '8.10.0-alpha5'\n\n// ---------------------------------------------------------------------------\n// Secrets, BYOIDP, Optimize and App Integrations\n//\n// Moved here from misc.const.ts. Same names, same values.\n// ---------------------------------------------------------------------------\n/**\n * Minimum cluster (zeebe) version for the Optimize data-filters feature. Shared by\n * the c4 settings tile, the new-cluster default, and CRD spec generation so the gate\n * stays consistent. Stricter than the operator's own gate (Camunda >= 8.9.0-alpha1,\n * where the exporter starts honoring the filters) \u2014 this is the product rollout gate.\n */\nexport const OPTIMIZE_FILTERING_MIN_VERSION = '8.10.0-alpha1'\n\n/**\n * Minimum cluster (zeebe) version at which the operator understands the new\n * cluster-secret CRDs (camunda-operator#5760) \u2014 SecretSource and SecretSync exist and\n * reconcile. Governs *provisioning the CRs*, nothing about which store they point at:\n * SecretSource provisioning (K8sClusterController.ensureSecretSource) and BYOIDP's\n * oidcProviders emission, which needs a SecretSource to deliver its clientSecretRef\n * (ByoidpSecretDeliveryController.deliverSecret refuses without one).\n *\n * BYOIDP is live up to prod, so this floor must stay at the generation BYOIDP\n * shipped on. Raising it would strand BYOIDP on every alpha below the new value.\n */\nexport const SECRET_SOURCE_MIN_VERSION = '8.10.0-alpha3'\n\n/**\n * Minimum cluster (zeebe) version for BYOIDP (external identity provider, #3190).\n * Shared by the c4 settings tile gate and the console-backend update guard so both\n * stay in lockstep.\n *\n * Aliased to SECRET_SOURCE_MIN_VERSION rather than repeating the literal: BYOIDP\n * needs the SecretSource/SecretSync CRDs to exist and nothing more (it delivers its\n * clientSecret through the SecretSource), so the two answer the same question and a\n * second literal would only let them drift. It is NOT CONNECTOR_SECRET_SYNC_MIN_VERSION\n * \u2014 that one is higher, and gating BYOIDP on it would strand BYOIDP on every alpha,\n * where it currently ships to prod.\n */\nexport const BYOIDP_MIN_VERSION = SECRET_SOURCE_MIN_VERSION\n\n/**\n * Minimum cluster (zeebe) version at which a cluster's *connector and internal*\n * secrets live in the new per-stage secret manager rather than the legacy store.\n *\n * Higher than SECRET_SOURCE_MIN_VERSION on purpose: a cluster can have a\n * SecretSource (so BYOIDP works) while its connector secrets are still in the legacy\n * store and it gets no connector SecretSync. The one thing that must never drift is\n * the pair inside THIS concern: store routing\n * (ConnectorSecretsController.resolveSecretManagerTarget), the connector SecretSync\n * and its projection into the broker's file store (K8sSpecUtils.getSecretStoresSpec),\n * the pre-8.10-backup mirror check, and the upgrade-copy crossing check\n * (GenerationController.crossesInto810) all read THIS constant. #9703 was those\n * disagreeing \u2014 where Console writes a secret must equal where the cluster reads it.\n *\n * Was the 8.10 GA line (`8.10.0`) until the connector-secret changes landed on\n * `8.10.0-alpha5` (2026-09-02). The floor moved down to that alpha because the\n * ordering requirement below was already met at that time: no\n * `8.10.0-alpha5` cluster existed yet, on any stage. So no cluster's secrets\n * were left in the legacy store under the new floor.\n *\n * Why alphas normally stay out: routing is version-based with no persisted \"which\n * store holds this cluster's secrets\" marker, so a cluster above the floor while the\n * feature gate is off has its secrets in the legacy store, and enabling the gate\n * later reroutes it to a store nobody ever populated. The copy that would fix that\n * (GenerationController.crossesInto810 -> copyConnectorSecretsToNewManager) only\n * fires on a generation CHANGE, and alpha generations have no upgrade chain at all\n * (`upgradeableFrom = []`, ReleaseTrainController.handleAlpha) \u2014 an alpha cluster is\n * created on its generation and stays there. So an alpha cohort above this floor\n * could never be migrated; the ~200 8.10.0-alpha4 clusters on prod are exactly that\n * cohort, and they stay below THIS floor (`alpha4 < alpha5`), so they are unaffected.\n *\n * The remaining ordering requirement is that a stage's gate be enabled before the\n * first at-floor cluster exists on it \u2014 which for SNAPSHOT-tagged clusters (at-floor\n * by design, since they run the newest operator) still has to be respected by hand. A\n * future move to `8.10.0-alpha6` or higher must check the same thing this move did:\n * no cluster already sitting at that version on any stage.\n */\nexport const CONNECTOR_SECRET_SYNC_MIN_VERSION = '8.10.0-alpha5'\n\n/**\n * Version floors for App Integrations (#8536). The fix landed as patches on\n * the 8.9 stream (zeebe >=8.9.14 AND connectorBridge >=8.9.7), everything\n * from 8.10.0-alpha4 on already carries both fixes. Shared by\n * isAppIntegrationsVersionSupported (c4 settings tile + console-backend\n * update guard) so the gate logic and its error message stay in lockstep.\n */\nexport const APP_INTEGRATIONS_MIN_VERSION = '8.10.0-alpha4'\nexport const APP_INTEGRATIONS_89_STREAM_ZEEBE_MIN_VERSION = '8.9.14'\nexport const APP_INTEGRATIONS_89_STREAM_CONNECTOR_BRIDGE_MIN_VERSION = '8.9.7'\n\n/**\n * Minor selector for the 8.9 stream, read with `VersionCheckOperator.SameMinor`\n * by `isAppIntegrationsVersionSupported`. It picks the stream, it is not a\n * floor: the floors are the two constants above.\n */\nexport const APP_INTEGRATIONS_89_STREAM_MINOR = '8.9.0'\n\n/**\n * The cluster details header calls the secrets \"cluster secrets\" instead of\n * the legacy name (c4).\n *\n * Aliased to CONNECTOR_SECRET_SYNC_MIN_VERSION rather than repeating the\n * literal: the label must follow the store the cluster actually uses, so the\n * two answer the same question.\n */\nexport const CLUSTER_SECRETS_LABEL_MIN_VERSION =\n CONNECTOR_SECRET_SYNC_MIN_VERSION\n\n/**\n * Minimum cluster (zeebe) version for the optional telemetry toggle.\n */\nexport const OPTIONAL_TELEMETRY_MIN_VERSION = '8.10.0-alpha5'\n\n// ---------------------------------------------------------------------------\n// Suspend and resume\n// ---------------------------------------------------------------------------\n\n/**\n * Lowest ALPHA generation that a suspended cluster can resume from. A cluster\n * on an alpha under this floor stays suspended forever.\n * `isGenerationResumable` reads this floor. The c4 resume actions and the\n * console-backend wake-up guards share that function.\n *\n * The floor applies to alphas only. A release generation under it, 8.9 or\n * 8.8 for example, resumes as before. Those lines have an upgrade chain, so a\n * customer can move off them. An alpha has no upgrade chain\n * (`upgradeableFrom = []`, `ReleaseTrainController.handleAlpha`). A cluster\n * starts on its alpha generation and stays there, so the only way off an old\n * alpha is a new cluster. A bulk operation suspends that cohort, and this floor\n * then refuses the wake-up.\n *\n * Spelled `8.10.0-alpha5` and not `-0`, because the cohort to retire is every\n * alpha under alpha5. It holds the same value as\n * `CONNECTOR_SECRET_SYNC_MIN_VERSION` today. The alphas under that floor keep\n * their connector secrets in the legacy store, and no migration can move them.\n * It stays a constant of its own, because it answers a different question and\n * the two values can move apart.\n */\nexport const RESUME_ALPHA_MIN_VERSION = '8.10.0-alpha5'\n"],
5
- "mappings": "AA4CO,MAAM,uBAAuB;AAG7B,MAAM,wBAAwB;AAO9B,MAAM,yBAAyB;AAW/B,MAAM,oCAAoC;AAG1C,MAAM,4CAA4C;AAOlD,MAAM,qCAAqC;AAO3C,MAAM,4CAA4C;AAMlD,MAAM,gCAAgC;AAMtC,MAAM,mCAAmC;AAczC,MAAM,kCAAkC;AAGxC,MAAM,mCAAmC;AAGzC,MAAM,qCAAqC;AAU3C,MAAM,kCAAkC;AAOxC,MAAM,yCAAyC;AAO/C,MAAM,qCAAqC;AAU3C,MAAM,gDAAgD;AAGtD,MAAM,sCAAsC;AAM5C,MAAM,mCAAmC;AAOzC,MAAM,qCAAqC;AAC3C,MAAM,mCAAmC;AAmBzC,MAAM,kCAAkC;AAOxC,MAAM,yCAAyC;AAM/C,MAAM,6CAA6C;AAgBnD,MAAM,2CAA2C;AAOjD,MAAM,sCAAsC;AAG5C,MAAM,sCAAsC;AAG5C,MAAM,sCAAsC;AAO5C,MAAM,sCAAsC;AAwB5C,MAAM,uCAAuC;AAO7C,MAAM,qCAAqC;AAG3C,MAAM,6BAA6B;AAGnC,MAAM,0BAA0B;AAGhC,MAAM,sBAAsB;AAM5B,MAAM,8BAA8B;AAGpC,MAAM,kBAAkB;AAUxB,MAAM,+BAA+B;AAGrC,MAAM,4BAA4B;AAGlC,MAAM,wCAAwC;AAG9C,MAAM,4BAA4B;AAOlC,MAAM,kCAAkC;AAOxC,MAAM,+BAA+B;AASrC,MAAM,6BAA6B;AAGnC,MAAM,kCAAkC;AAaxC,MAAM,iCAAiC;AAavC,MAAM,4BAA4B;AAclC,MAAM,qBAAqB;AAuC3B,MAAM,oCAAoC;AAS1C,MAAM,+BAA+B;AACrC,MAAM,+CAA+C;AACrD,MAAM,0DAA0D;AAOhE,MAAM,mCAAmC;AAUzC,MAAM,oCACX;AAKK,MAAM,iCAAiC;AA2BvC,MAAM,2BAA2B;",
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 * Central list of the cluster version floors that gate features.\n *\n * Every `checkVersion({ versionToCompareTo })` / `checkClusterVersion()` call in\n * the apps and in `packages/c4` reads its floor from here. Put a new floor here\n * before you use it. Do not write a version literal at the call site \u2014 the same\n * feature is usually gated in more than one place (a c4 tile, a console-backend\n * guard, a CRD spec), and two literals drift. Incident #9703 was two copies of\n * one floor that disagreed.\n *\n * `packages/c3` keeps its own copy of the version helpers and is out of scope:\n * it is a public package that this repo must not change.\n *\n * The values compare with semver, so the spelling of a floor decides which\n * prerelease cohort it covers:\n * - `8.10.0-0` and `8.10.0-alpha0` sort below every `8.10.0-alphaN`.\n * - `8.10.0-alpha1` sorts above `alpha0` but below `alpha2`.\n * - `8.10.0` sorts above ALL `8.10.0-*` prereleases.\n *\n * Two constants in this file can hold the same value. Then they gate different\n * features and can move apart. When a name is an alias of another constant, the\n * two answer the same question and must never drift.\n *\n * A call site compares a floor against the zeebe version of the generation,\n * unless the comment of the constant names a different field. Name that field\n * in the comment when you add a floor for one, because the same number means a\n * different thing for `connectorBridge` or for a generation name.\n */\n\n// ---------------------------------------------------------------------------\n// Release-line floors\n//\n// Used where the code asks \"is this generation on the 8.x line?\" and no single\n// feature owns the answer. Both spell the line start as `-0`, so every\n// prerelease of that line is included.\n// ---------------------------------------------------------------------------\n\n/** Start of the 8.9 line, prereleases included. */\nexport const LINE_8_9_MIN_VERSION = '8.9.0-0'\n\n/** Start of the 8.10 line, prereleases included. */\nexport const LINE_8_10_MIN_VERSION = '8.10.0-0'\n\n// ---------------------------------------------------------------------------\n// Backups\n// ---------------------------------------------------------------------------\n\n/** Hot backups are available on the cluster (c4 hot-backups tab). */\nexport const HOT_BACKUP_MIN_VERSION = '8.2.4'\n\n// ---------------------------------------------------------------------------\n// Identity, authorizations and roles\n// ---------------------------------------------------------------------------\n\n/**\n * The \"enforce groups\" toggle is offered (c4 settings tile). The tile also\n * disappears again on 8.8 clusters that run Tasklist V2 \u2014 see\n * `ENFORCE_GROUPS_TASKLIST_V2_HIDDEN_MIN_VERSION`.\n */\nexport const ENFORCE_GROUPS_TOGGLE_MIN_VERSION = '8.5.0-alpha2'\n\n/** Resource-based authorizations exist. Replaced by identity authorizations. */\nexport const RESOURCE_BASED_AUTHORIZATIONS_MIN_VERSION = '8.6.0'\n\n/**\n * Identity-based authorizations replace the resource-based ones. Read as an\n * upper bound by the c4 settings tile: below this the cluster still shows the\n * resource-based section.\n */\nexport const IDENTITY_AUTHORIZATION_MIN_VERSION = '8.8.0-alpha1'\n\n/**\n * Upper bound of the \"your upgrade turns on identity authorizations\" notice in\n * the c4 overview tab. The notice is for 8.8-line targets only, so an 8.9\n * target does not show it.\n */\nexport const IDENTITY_AUTHORIZATION_NOTICE_MAX_VERSION = '8.9.0-alpha1'\n\n/**\n * The cluster uses the 8.8 role model, so the legacy per-cluster roles are\n * gone (c4 user-details and management widgets).\n */\nexport const NEW_CLUSTER_ROLES_MIN_VERSION = '8.8.0-alpha4'\n\n/**\n * The authorizations toggle is offered while creating a cluster (c4 create\n * cluster wizard).\n */\nexport const AUTHORIZATION_TOGGLE_MIN_VERSION = '8.8.0-alpha4'\n\n/**\n * Identity manages users, groups and tenants. The cluster shows the Identity\n * screens (c4 cluster details, user details, group details, management widget,\n * console-frontend and console-sm-frontend), and the CRD spec sends the\n * internal client IDs to Identity\n * (`K8sSpecUtils.clusterHasEnabledIdentityIntegration`).\n *\n * console-backend used `8.8.0-alpha4` until the gates moved here. That floor\n * was one alpha under the frontend. As a result, an 8.8.0-alpha4 cluster got\n * its `internalClientIDs` sent to an Identity that the UI did not show. There\n * is one floor now, on the version the UI has always used.\n */\nexport const IDENTITY_MANAGEMENT_MIN_VERSION = '8.8.0-alpha5'\n\n/** The multi-tenancy toggle is offered (c4 settings tile). */\nexport const MULTI_TENANCY_TOGGLE_MIN_VERSION = '8.8.0-alpha7'\n\n/** Business-ID uniqueness toggle is offered (c4 settings tile). */\nexport const BUSINESS_ID_UNIQUENESS_MIN_VERSION = '8.9.0-alpha5'\n\n// ---------------------------------------------------------------------------\n// Tasklist\n// ---------------------------------------------------------------------------\n\n/**\n * The current generation is good enough to upgrade to Tasklist V2 (c4 overview\n * tab).\n */\nexport const TASKLIST_V2_UPGRADE_MIN_VERSION = '8.7.0-alpha4'\n\n/**\n * The upgrade target carries Tasklist V2 (c4 overview tab). Pairs with\n * `LINE_8_10_MIN_VERSION` as the upper bound, because from 8.10 on the setting\n * is gone.\n */\nexport const TASKLIST_V2_UPGRADE_TARGET_MIN_VERSION = '8.8.0-alpha1'\n\n/**\n * A new cluster gets `tasklistV2Enabled` by default (console-backend cluster\n * creation). Reads the generation's tasklist version when it has one, else\n * zeebe.\n */\nexport const TASKLIST_V2_DEFAULT_ON_MIN_VERSION = '8.8.0'\n\n/**\n * From this version, the c4 settings tile hides the \"enforce groups\" toggle on\n * a cluster that runs Tasklist V2. The tile reads the constant with `>=`, and\n * it also needs `tasklistV2Enabled` to be true.\n *\n * The name says MIN because of that `>=`. The other MAX names in this file mark\n * the version where a gate stops, and their call sites read them with `<`.\n */\nexport const ENFORCE_GROUPS_TASKLIST_V2_HIDDEN_MIN_VERSION = '8.8.0'\n\n/** The Tasklist upgrade banner is offered (c4 settings tile). */\nexport const TASKLIST_UPGRADE_BANNER_MIN_VERSION = '8.8.0-alpha2'\n\n/**\n * The Tasklist mode toggle is offered while creating a cluster (c4 create\n * cluster wizard). Upper bound is `LINE_8_10_MIN_VERSION`.\n */\nexport const TASKLIST_MODE_TOGGLE_MIN_VERSION = '8.8.0-alpha8'\n\n/**\n * Minor-version pair that turns Tasklist V2 on during a generation update\n * (console-backend). Both are read with `VersionCheckOperator.SameMinor`: an\n * 8.7 cluster that moves to an 8.8 generation gets the setting.\n */\nexport const TASKLIST_V2_AUTO_ENABLE_FROM_MINOR = '8.7.0'\nexport const TASKLIST_V2_AUTO_ENABLE_TO_MINOR = '8.8.0'\n\n/**\n * The version where the Tasklist V2 flag stops to mean anything, because there\n * is no Tasklist V1 left to switch away from. Two readers use it. The CRD spec\n * stops to emit the Tasklist V2 mode environment variable\n * (`K8sSpecUtils.shouldSkipTasklistV2ModeEnvVar`). The accounts-frontend\n * support rows stop to show the flag.\n *\n * Spelled `8.10.0` and NOT `LINE_8_10_MIN_VERSION` (`8.10.0-0`). Cluster\n * creation sets the flag on every generation from\n * `TASKLIST_V2_DEFAULT_ON_MIN_VERSION` on. An 8.10 prerelease thus carries a\n * live value, and it must stay under this bound.\n *\n * c4 bounds its own Tasklist V2 checks with `LINE_8_10_MIN_VERSION` instead.\n * That constant answers a different question: can Console still offer the\n * Tasklist upgrade to this cluster. It correctly leaves out the 8.10\n * prereleases.\n */\nexport const TASKLIST_V2_SETTING_MAX_VERSION = '8.10.0'\n\n// ---------------------------------------------------------------------------\n// Connectors\n// ---------------------------------------------------------------------------\n\n/** The internal connectors-management page exists (c4 overview tab link). */\nexport const CONNECTORS_MANAGEMENT_LINK_MIN_VERSION = '8.7.0-alpha1'\n\n/**\n * The applications list offers \"Manage\" for connectors (c4 overview tab).\n * Matches what console-frontend has always done.\n */\nexport const INTERNAL_CONNECTORS_MANAGEMENT_MIN_VERSION = '8.8.0-alpha1'\n\n/**\n * Connector management shows the outbound connectors, the per-connector metrics\n * and the restart button. Under this floor the pages show the inbound-only\n * table.\n *\n * CAUTION: the two readers compare this floor against different version fields.\n * The c4 connector-management widgets compare\n * `generation.versions.connectorBridge`\n * (`useConnectorManagementRuntime.checkClusterVersion`). The\n * console-sm-frontend pages compare the generation name of the cluster\n * (`page.getData().generation`). Both sites had the same literal before the\n * gates moved here, so this constant keeps that. Do not read it as one\n * question until the two readers compare the same field.\n */\nexport const CONNECTOR_MANAGEMENT_METRICS_MIN_VERSION = '8.10.0-alpha3'\n\n/**\n * The secrets filter mode setting is offered on the 8.6 line (zeebe). Shared by\n * isSecretsFilterModeConfigurable (c4 settings tile + console-backend update\n * guard).\n */\nexport const SECRETS_FILTER_MODE_8_6_MIN_VERSION = '8.6.28'\n\n/** Same setting, floor for the 8.7 line. */\nexport const SECRETS_FILTER_MODE_8_7_MIN_VERSION = '8.7.25'\n\n/** Same setting, floor for the 8.8 line. */\nexport const SECRETS_FILTER_MODE_8_8_MIN_VERSION = '8.8.19'\n\n/**\n * Same setting, floor for the 8.9 line. The env var works from 8.9.7, but the\n * default stays DISABLED until 8.9.10. The floor waits for that default\n * change, so the toggle never shows an unset value with an unknown default.\n */\nexport const SECRETS_FILTER_MODE_8_9_MIN_VERSION = '8.9.10'\n\n/**\n * Same setting, floor for the 8.10 line and every line above it.\n *\n * This is the last floor: isSecretsFilterModeConfigurable applies it to all\n * versions above the 8.9 line, so 8.11 and later need no floor of their own.\n *\n * Two different reasons keep the versions below it out, and the second one is\n * why this floor is alpha6 and not alpha5:\n *\n * - alpha1 to alpha4 shipped the DISABLED default. Same reason as 8.9.7-8.9.9.\n * - alpha5 shipped the STRICT default, but not the Connectors change that this\n * setting needs. SaaS runs Connectors with legacy.mode=FALLBACK from the 8.10\n * line on, and the Connectors runtime of alpha5 refuses to start unless\n * FALLBACK is paired with STRICT\n * (ConnectorsAutoConfiguration.checkLegacyFallbackSecretFilter). A customer\n * who selects LAX or DISABLED on alpha5 crashes their own Connectors runtime\n * at startup, so the toggle must stay hidden there.\n *\n * Connectors removed that check after the alpha5 release (2026-09-03). The\n * removal is in the SNAPSHOT images and lands in the releases from alpha6 on.\n * SNAPSHOT clusters run the newest images, so they get the toggle.\n */\nexport const SECRETS_FILTER_MODE_8_10_MIN_VERSION = '8.10.0-alpha6'\n\n// ---------------------------------------------------------------------------\n// APIs and cluster URLs\n// ---------------------------------------------------------------------------\n\n/** The client details page can transform the cluster URL string (c4). */\nexport const REST_API_URL_TRANSFORM_MIN_VERSION = '8.6.0'\n\n/** The cluster publishes a Zeebe REST URL, so the client details page uses it. */\nexport const ZEEBE_REST_URL_MIN_VERSION = '8.8.0-alpha1'\n\n/** The cluster serves the v2 REST API under `<clusterUrl>/v2` (c4). */\nexport const REST_API_V2_MIN_VERSION = '8.10.0-alpha1'\n\n/** The Swagger toggle and the Swagger link are offered (c4). */\nexport const SWAGGER_MIN_VERSION = '8.8.0-alpha8'\n\n/**\n * The Swagger URL moves to its 8.9 shape (c4 `buildClusterSwaggerUrl`).\n * Spelled `8.9.0`, so 8.9 prereleases keep the older URL.\n */\nexport const SWAGGER_URL_8_9_MIN_VERSION = '8.9.0'\n\n/** The MCP toggle and the MCP client tab are offered (c4). */\nexport const MCP_MIN_VERSION = '8.9.0-alpha4'\n\n// ---------------------------------------------------------------------------\n// Apps, tabs and dashboards\n// ---------------------------------------------------------------------------\n\n/**\n * Identity is called \"Admin\" from here on (c4 navigation and overview tab,\n * console-sm-frontend). The URL and the permission still say `identity`.\n */\nexport const ADMIN_APP_RENAME_MIN_VERSION = '8.9.0-alpha5'\n\n/** The job dashboard is offered (c4 overview tab and console-sm-frontend). */\nexport const JOB_DASHBOARD_MIN_VERSION = '8.9.0-alpha5'\n\n/** The cluster serves the statistics config endpoint (c4 cluster details). */\nexport const CLUSTER_STATISTICS_CONFIG_MIN_VERSION = '8.9.0-alpha5'\n\n/** The audit log tab is offered (c4 cluster details). */\nexport const AUDIT_LOG_TAB_MIN_VERSION = '8.9.0-alpha5'\n\n// ---------------------------------------------------------------------------\n// Cluster spec and provisioning\n// ---------------------------------------------------------------------------\n\n/** Only 8.7 and newer generations are offered to AWS-only orgs (console-backend). */\nexport const AWS_ONLY_GENERATION_MIN_VERSION = '8.7.0'\n\n/**\n * The gateway spec takes its 8.9 shape (console-backend\n * `K8sSpecUtils.getGatewaySpec`). Spelled `-alpha0`, so every 8.9 prerelease\n * is included.\n */\nexport const GATEWAY_SPEC_8_9_MIN_VERSION = '8.9.0-alpha0'\n\n/**\n * The operator wires the session idle-timeout config through to Operate,\n * Tasklist and the unified app (camunda-security-library ADR-0042,\n * camunda-operator#5863). This floor matches the `SessionConfigSupported` gate\n * of the operator. The CRD spec and the c4 settings tile both read it, which is\n * why it lives here.\n */\nexport const SESSION_CONFIG_MIN_VERSION = '8.10.0-alpha5'\n\n/** A new cluster gets Optimize turned on by default (console-backend). */\nexport const OPTIMIZE_DEFAULT_ON_MIN_VERSION = '8.10.0-alpha5'\n\n// ---------------------------------------------------------------------------\n// Secrets, BYOIDP, Optimize and App Integrations\n//\n// Moved here from misc.const.ts. Same names, same values.\n// ---------------------------------------------------------------------------\n/**\n * Minimum cluster (zeebe) version for the Optimize data-filters feature. Shared by\n * the c4 settings tile, the new-cluster default, and CRD spec generation so the gate\n * stays consistent. Stricter than the operator's own gate (Camunda >= 8.9.0-alpha1,\n * where the exporter starts honoring the filters) \u2014 this is the product rollout gate.\n */\nexport const OPTIMIZE_FILTERING_MIN_VERSION = '8.10.0-alpha1'\n\n/**\n * Minimum cluster (zeebe) version at which the operator understands the new\n * cluster-secret CRDs (camunda-operator#5760) \u2014 SecretSource and SecretSync exist and\n * reconcile. Governs *provisioning the CRs*, nothing about which store they point at:\n * SecretSource provisioning (K8sClusterController.ensureSecretSource) and BYOIDP's\n * oidcProviders emission, which needs a SecretSource to deliver its clientSecretRef\n * (ByoidpSecretDeliveryController.deliverSecret refuses without one).\n *\n * BYOIDP is live up to prod, so this floor must stay at the generation BYOIDP\n * shipped on. Raising it would strand BYOIDP on every alpha below the new value.\n */\nexport const SECRET_SOURCE_MIN_VERSION = '8.10.0-alpha3'\n\n/**\n * Minimum cluster (zeebe) version for BYOIDP (external identity provider, #3190).\n * Shared by the c4 settings tile gate and the console-backend update guard so both\n * stay in lockstep.\n *\n * Aliased to SECRET_SOURCE_MIN_VERSION rather than repeating the literal: BYOIDP\n * needs the SecretSource/SecretSync CRDs to exist and nothing more (it delivers its\n * clientSecret through the SecretSource), so the two answer the same question and a\n * second literal would only let them drift. It is NOT CONNECTOR_SECRET_SYNC_MIN_VERSION\n * \u2014 that one is higher, and gating BYOIDP on it would strand BYOIDP on every alpha,\n * where it currently ships to prod.\n */\nexport const BYOIDP_MIN_VERSION = SECRET_SOURCE_MIN_VERSION\n\n/**\n * Minimum cluster (zeebe) version at which a cluster's *connector and internal*\n * secrets live in the new per-stage secret manager rather than the legacy store.\n *\n * Higher than SECRET_SOURCE_MIN_VERSION on purpose: a cluster can have a\n * SecretSource (so BYOIDP works) while its connector secrets are still in the legacy\n * store and it gets no connector SecretSync. The one thing that must never drift is\n * the pair inside THIS concern: store routing\n * (ConnectorSecretsController.resolveSecretManagerTarget), the connector SecretSync\n * and its projection into the broker's file store (K8sSpecUtils.getSecretStoresSpec),\n * the pre-8.10-backup mirror check, and the upgrade-copy crossing check\n * (GenerationController.crossesInto810) all read THIS constant. #9703 was those\n * disagreeing \u2014 where Console writes a secret must equal where the cluster reads it.\n *\n * Was the 8.10 GA line (`8.10.0`) until the connector-secret changes landed on\n * `8.10.0-alpha5` (2026-09-02). The floor moved down to that alpha because the\n * ordering requirement below was already met at that time: no\n * `8.10.0-alpha5` cluster existed yet, on any stage. So no cluster's secrets\n * were left in the legacy store under the new floor.\n *\n * Why alphas normally stay out: routing is version-based with no persisted \"which\n * store holds this cluster's secrets\" marker, so a cluster above the floor while the\n * feature gate is off has its secrets in the legacy store, and enabling the gate\n * later reroutes it to a store nobody ever populated. The copy that would fix that\n * (GenerationController.crossesInto810 -> copyConnectorSecretsToNewManager) only\n * fires on a generation CHANGE, and alpha generations have no upgrade chain at all\n * (`upgradeableFrom = []`, ReleaseTrainController.handleAlpha) \u2014 an alpha cluster is\n * created on its generation and stays there. So an alpha cohort above this floor\n * could never be migrated; the ~200 8.10.0-alpha4 clusters on prod are exactly that\n * cohort, and they stay below THIS floor (`alpha4 < alpha5`), so they are unaffected.\n *\n * The remaining ordering requirement is that a stage's gate be enabled before the\n * first at-floor cluster exists on it \u2014 which for SNAPSHOT-tagged clusters (at-floor\n * by design, since they run the newest operator) still has to be respected by hand. A\n * future move to `8.10.0-alpha6` or higher must check the same thing this move did:\n * no cluster already sitting at that version on any stage.\n */\nexport const CONNECTOR_SECRET_SYNC_MIN_VERSION = '8.10.0-alpha5'\n\n/**\n * Version floors for App Integrations (#8536). The fix landed as patches on\n * the 8.9 stream (zeebe >=8.9.14 AND connectorBridge >=8.9.7), everything\n * from 8.10.0-alpha4 on already carries both fixes. Shared by\n * isAppIntegrationsVersionSupported (c4 settings tile + console-backend\n * update guard) so the gate logic and its error message stay in lockstep.\n */\nexport const APP_INTEGRATIONS_MIN_VERSION = '8.10.0-alpha4'\nexport const APP_INTEGRATIONS_89_STREAM_ZEEBE_MIN_VERSION = '8.9.14'\nexport const APP_INTEGRATIONS_89_STREAM_CONNECTOR_BRIDGE_MIN_VERSION = '8.9.7'\n\n/**\n * Minor selector for the 8.9 stream, read with `VersionCheckOperator.SameMinor`\n * by `isAppIntegrationsVersionSupported`. It picks the stream, it is not a\n * floor: the floors are the two constants above.\n */\nexport const APP_INTEGRATIONS_89_STREAM_MINOR = '8.9.0'\n\n/**\n * The cluster details header calls the secrets \"cluster secrets\" instead of\n * the legacy name (c4).\n *\n * Aliased to CONNECTOR_SECRET_SYNC_MIN_VERSION rather than repeating the\n * literal: the label must follow the store the cluster actually uses, so the\n * two answer the same question.\n */\nexport const CLUSTER_SECRETS_LABEL_MIN_VERSION =\n CONNECTOR_SECRET_SYNC_MIN_VERSION\n\n/**\n * Minimum cluster (zeebe) version that runs the analytics exporter, thus the\n * floor for optional telemetry.\n *\n * The number comes from the operator: `AnalyticsExporterSupported` in\n * `pkg/features/features.go` reads `>= 8.10.0-alpha1`, and below it\n * `zeebe_statefulset.go` adds no exporter env vars at all. A cluster under the\n * floor sends no analytics telemetry, OPTIONAL or CONTRACTUAL, thus the row\n * has no state to show and the toggle has nothing to switch.\n */\nexport const OPTIONAL_TELEMETRY_MIN_VERSION = '8.10.0-alpha1'\n\n// ---------------------------------------------------------------------------\n// Suspend and resume\n// ---------------------------------------------------------------------------\n\n/**\n * Lowest ALPHA generation that a suspended cluster can resume from. A cluster\n * on an alpha under this floor stays suspended forever.\n * `isGenerationResumable` reads this floor. The c4 resume actions and the\n * console-backend wake-up guards share that function.\n *\n * The floor applies to alphas only. A release generation under it, 8.9 or\n * 8.8 for example, resumes as before. Those lines have an upgrade chain, so a\n * customer can move off them. An alpha has no upgrade chain\n * (`upgradeableFrom = []`, `ReleaseTrainController.handleAlpha`). A cluster\n * starts on its alpha generation and stays there, so the only way off an old\n * alpha is a new cluster. A bulk operation suspends that cohort, and this floor\n * then refuses the wake-up.\n *\n * Spelled `8.10.0-alpha5` and not `-0`, because the cohort to retire is every\n * alpha under alpha5. It holds the same value as\n * `CONNECTOR_SECRET_SYNC_MIN_VERSION` today. The alphas under that floor keep\n * their connector secrets in the legacy store, and no migration can move them.\n * It stays a constant of its own, because it answers a different question and\n * the two values can move apart.\n */\nexport const RESUME_ALPHA_MIN_VERSION = '8.10.0-alpha5'\n"],
5
+ "mappings": "AA4CO,MAAM,uBAAuB;AAG7B,MAAM,wBAAwB;AAO9B,MAAM,yBAAyB;AAW/B,MAAM,oCAAoC;AAG1C,MAAM,4CAA4C;AAOlD,MAAM,qCAAqC;AAO3C,MAAM,4CAA4C;AAMlD,MAAM,gCAAgC;AAMtC,MAAM,mCAAmC;AAczC,MAAM,kCAAkC;AAGxC,MAAM,mCAAmC;AAGzC,MAAM,qCAAqC;AAU3C,MAAM,kCAAkC;AAOxC,MAAM,yCAAyC;AAO/C,MAAM,qCAAqC;AAU3C,MAAM,gDAAgD;AAGtD,MAAM,sCAAsC;AAM5C,MAAM,mCAAmC;AAOzC,MAAM,qCAAqC;AAC3C,MAAM,mCAAmC;AAmBzC,MAAM,kCAAkC;AAOxC,MAAM,yCAAyC;AAM/C,MAAM,6CAA6C;AAgBnD,MAAM,2CAA2C;AAOjD,MAAM,sCAAsC;AAG5C,MAAM,sCAAsC;AAG5C,MAAM,sCAAsC;AAO5C,MAAM,sCAAsC;AAwB5C,MAAM,uCAAuC;AAO7C,MAAM,qCAAqC;AAG3C,MAAM,6BAA6B;AAGnC,MAAM,0BAA0B;AAGhC,MAAM,sBAAsB;AAM5B,MAAM,8BAA8B;AAGpC,MAAM,kBAAkB;AAUxB,MAAM,+BAA+B;AAGrC,MAAM,4BAA4B;AAGlC,MAAM,wCAAwC;AAG9C,MAAM,4BAA4B;AAOlC,MAAM,kCAAkC;AAOxC,MAAM,+BAA+B;AASrC,MAAM,6BAA6B;AAGnC,MAAM,kCAAkC;AAaxC,MAAM,iCAAiC;AAavC,MAAM,4BAA4B;AAclC,MAAM,qBAAqB;AAuC3B,MAAM,oCAAoC;AAS1C,MAAM,+BAA+B;AACrC,MAAM,+CAA+C;AACrD,MAAM,0DAA0D;AAOhE,MAAM,mCAAmC;AAUzC,MAAM,oCACX;AAYK,MAAM,iCAAiC;AA2BvC,MAAM,2BAA2B;",
6
6
  "names": []
7
7
  }
@@ -13,6 +13,12 @@ export interface AlertSubscriptionDto {
13
13
  updated: Date;
14
14
  lastTimeTriggered?: Date;
15
15
  numberOfTimesTriggered: number;
16
+ /**
17
+ * The address of an `Email` subscription. alert-distributor adds it from the
18
+ * user id, which its list endpoint then replaces with the name of the user.
19
+ * Thus this field is the only reliable way to identify the person.
20
+ */
21
+ recepientEmail?: string;
16
22
  }
17
23
  export interface AlertSubscriptionEntryDto {
18
24
  errorMessage?: string;