@camunda/ccma-shared-types 6.0.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/_tests/constants/customer-hardware-sizing.const.spec.d.ts +1 -0
  2. package/dist/_tests/dtos/ClusterPlanWhiteList.spec.d.ts +1 -0
  3. package/dist/_tests/utils/upgradeGateReason.utils.spec.d.ts +1 -0
  4. package/dist/approvals/policy.js +1 -1
  5. package/dist/approvals/policy.js.map +2 -2
  6. package/dist/approvals/use-cases.const.d.ts +16 -0
  7. package/dist/approvals/use-cases.const.js +18 -1
  8. package/dist/approvals/use-cases.const.js.map +2 -2
  9. package/dist/constants/customer-hardware-sizing.const.d.ts +124 -0
  10. package/dist/constants/customer-hardware-sizing.const.js +266 -0
  11. package/dist/constants/customer-hardware-sizing.const.js.map +7 -0
  12. package/dist/constants/version-gates.const.d.ts +12 -0
  13. package/dist/constants/version-gates.const.js +2 -0
  14. package/dist/constants/version-gates.const.js.map +2 -2
  15. package/dist/dtos/ApprovalJob.dto.d.ts +33 -3
  16. package/dist/dtos/ApprovalJob.dto.js.map +2 -2
  17. package/dist/dtos/ClusterPlanTypeChange.dto.d.ts +23 -0
  18. package/dist/dtos/ClusterPlanTypeChange.dto.js +1 -0
  19. package/dist/dtos/ClusterPlanTypeChange.dto.js.map +7 -0
  20. package/dist/dtos/CommonCluster.dto.d.ts +5 -0
  21. package/dist/dtos/EarlyAccessFeature.dto.d.ts +1 -0
  22. package/dist/dtos/FeatureFlags.dto.d.ts +9 -1
  23. package/dist/dtos/Organization.dto.d.ts +11 -0
  24. package/dist/dtos/Organization.dto.js.map +1 -1
  25. package/dist/dtos/SalesPlan.dto.d.ts +18 -0
  26. package/dist/dtos/SalesPlan.dto.js +18 -0
  27. package/dist/dtos/SalesPlan.dto.js.map +2 -2
  28. package/dist/dtos/UpgradeReadiness.dto.d.ts +32 -0
  29. package/dist/dtos/UpgradeReadiness.dto.js +1 -0
  30. package/dist/dtos/UpgradeReadiness.dto.js.map +7 -0
  31. package/dist/enums/SystemAuditEntity.enum.d.ts +1 -0
  32. package/dist/enums/SystemAuditEntity.enum.js +1 -0
  33. package/dist/enums/SystemAuditEntity.enum.js.map +2 -2
  34. package/dist/index.d.ts +7 -3
  35. package/dist/index.js +42 -0
  36. package/dist/index.js.map +2 -2
  37. package/dist/search/organization-search-fields.d.ts +5 -2
  38. package/dist/search/organization-search-fields.js +9 -1
  39. package/dist/search/organization-search-fields.js.map +2 -2
  40. package/dist/types/ClusterBulkOperation.type.d.ts +14 -0
  41. package/dist/utils/elasticsearchPlanPath.utils.d.ts +3 -1
  42. package/dist/utils/elasticsearchPlanPath.utils.js +14 -5
  43. package/dist/utils/elasticsearchPlanPath.utils.js.map +2 -2
  44. package/dist/utils/upgradeGateReason.utils.d.ts +8 -0
  45. package/dist/utils/upgradeGateReason.utils.js +23 -0
  46. package/dist/utils/upgradeGateReason.utils.js.map +7 -0
  47. package/package.json +1 -1
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/constants/customer-hardware-sizing.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\nimport { canonicalPlan } from '../utils/elasticsearchPlanPath.utils.js'\n\n/**\n * Customer hardware sizing: a cluster plan that a Camundi creates for an\n * organization of their own, with no admin permission. The approval use cases\n * `create-customer-hardware-sizing` and `remove-customer-hardware-sizing` run\n * on dev only, and the limits in this file keep one plan from asking for a\n * machine park.\n *\n * Every cap below is 10 times the largest value in the cluster plans of prod\n * (dump of 2026-10-01, 27 plans). The cap for storage is 10 times the largest\n * value of a plan that is NOT a load test: `Cisco XXL Year 1` and\n * `G3-M (adapted for Cisco load test)` ask for 10 TB on purpose, and a cap\n * from them would be 100 TB.\n */\n\n/** The stage where customer hardware sizing runs. */\nexport const isCustomerHardwareSizingStage = (stage: string): boolean =>\n stage === 'dev'\n\n/**\n * The most plans that one Camundi can own through customer hardware sizing.\n *\n * The count is the number of plans the Camundi created. A plan that the\n * Camundi deleted does not count.\n */\nexport const CUSTOMER_HARDWARE_SIZING_MAX_PLANS_PER_REQUESTER = 10\n\n/** The shortest and longest name of a customer hardware sizing plan. */\nexport const CUSTOMER_HARDWARE_SIZING_NAME_MIN_LENGTH = 3\nexport const CUSTOMER_HARDWARE_SIZING_NAME_MAX_LENGTH = 100\n\n/**\n * The longest plan, as JSON text. The largest plan of prod is about 6000\n * characters. The cap stops a request that carries a huge object.\n */\nexport const CUSTOMER_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH = 50_000\n\n/** One cap for a resource of one kind of component. `gi` is gibibytes. */\ninterface ResourceCaps {\n cpu: number\n memoryGi: number\n storageGi: number\n}\n\n/**\n * Caps for the requests and the limits of one component.\n *\n * `other` is every component that is not the Zeebe broker or Elasticsearch:\n * Operate, Tasklist, Optimize, the gateways, Identity and the others. The\n * largest of them in prod is Operate, with 6 CPU and 30 GiB.\n *\n * Prod maximum, before the factor of 10:\n * - broker: 16 CPU, 50 GiB, 500 GiB of storage (no load test)\n * - Elasticsearch: 30 CPU, 100 GiB, 3000 GiB of storage (no load test)\n * - other: 6 CPU, 30 GiB, no storage\n */\nexport const CUSTOMER_HARDWARE_SIZING_RESOURCE_CAPS = {\n broker: { cpu: 160, memoryGi: 500, storageGi: 5000 },\n elasticsearch: { cpu: 300, memoryGi: 1000, storageGi: 30_000 },\n other: { cpu: 60, memoryGi: 300, storageGi: 5000 },\n} as const satisfies Record<string, ResourceCaps>\n\n/** Caps for the count fields of a plan. Prod maximum is in the comments. */\nexport const CUSTOMER_HARDWARE_SIZING_COUNT_CAPS = {\n // 7\n clusterSize: 70,\n // 50\n partitionsCount: 500,\n // 3\n replicationFactor: 30,\n // 5\n nodesCount: 50,\n // `replicas` of any component. 3.\n replicas: 30,\n} as const\n\n/** `zeebe.broker.storage.autoResizing.increase`. Prod maximum is 18 GiB. */\nexport const CUSTOMER_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI = 180\n\n/**\n * The caps over the whole broker set, which the caps for one broker do not\n * give. A plan with 70 brokers of 160 CPU passes every cap above, and it asks\n * for 11200 CPU. The prod maximum is 84 CPU (7 brokers, `Cisco XXL Year 1`),\n * 250 GiB of memory (5 brokers, `GLS HA`) and 2500 GiB of storage (5 brokers,\n * `GLS HA`, no load test).\n */\nexport const CUSTOMER_HARDWARE_SIZING_BROKER_TOTAL_CAPS = {\n cpu: 840,\n memoryGi: 2500,\n storageGi: 25_000,\n} as const\n\n/**\n * The same, for the whole Elasticsearch node set. Ten times the prod maximum:\n * 90 CPU and 300 GiB of memory (3 nodes, `GLS HA`), and 10000 GiB of storage\n * (5 nodes, `CCL G3L HA`). The storage of one node is a claim of one volume,\n * so the total is the claim times the number of nodes.\n */\nexport const CUSTOMER_HARDWARE_SIZING_ELASTICSEARCH_TOTAL_CAPS = {\n cpu: 900,\n memoryGi: 3000,\n storageGi: 100_000,\n} as const\n\n/**\n * The same, for the pods of one component that has `replicas`, such as\n * Operate or the gateway. Ten times the prod maximum: 12 CPU (the gateway, 3\n * replicas of 4 CPU) and 60 GiB of memory (Operate, 2 replicas of 30 GiB). No\n * component of prod has storage in its pod resources, so the storage total is\n * the cap for one pod, 10 times over. The check counts a storage request of a\n * pod in `resources` against it.\n */\nexport const CUSTOMER_HARDWARE_SIZING_REPLICA_SET_TOTAL_CAPS = {\n cpu: 120,\n memoryGi: 600,\n storageGi: 50_000,\n} as const\n\nconst GIBIBYTE = 1024 ** 3\n\nconst QUANTITY_MULTIPLIERS: Record<string, number> = {\n '': 1,\n m: 1e-3,\n k: 1e3,\n M: 1e6,\n G: 1e9,\n T: 1e12,\n P: 1e15,\n Ki: 1024,\n Mi: 1024 ** 2,\n Gi: 1024 ** 3,\n Ti: 1024 ** 4,\n Pi: 1024 ** 5,\n}\n\nconst QUANTITY_PATTERN = /^([0-9]+(?:\\.[0-9]+)?)(m|k|M|G|T|P|Ki|Mi|Gi|Ti|Pi)?$/u\n\n/**\n * Reads a Kubernetes quantity such as `500m`, `2`, `4Gi` or `1G`, and returns\n * its value in base units: cores for a CPU, bytes for memory and storage.\n *\n * Returns `undefined` for a text that this function cannot read, so the\n * caller can refuse it. The exponent form (`1e3`) is not read on purpose.\n */\nexport function parseKubernetesQuantity(value: unknown): number | undefined {\n if (typeof value === 'number') {\n return Number.isFinite(value) && value >= 0 ? value : undefined\n }\n if (typeof value !== 'string') {\n return undefined\n }\n const match = QUANTITY_PATTERN.exec(value)\n if (!match) {\n return undefined\n }\n return Number(match[1]) * (QUANTITY_MULTIPLIERS[match[2] ?? ''] ?? 1)\n}\n\ntype ComponentKind = keyof typeof CUSTOMER_HARDWARE_SIZING_RESOURCE_CAPS\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nfunction componentOf(path: string[]): ComponentKind {\n if (path[0] === 'zeebe' && path[1] === 'broker') {\n return 'broker'\n }\n if (path[0] === 'elasticsearch') {\n return 'elasticsearch'\n }\n return 'other'\n}\n\nfunction checkNumberCap(\n violations: string[],\n path: string[],\n value: unknown,\n cap: number,\n): void {\n const name = path.join('.')\n const parsed = parseKubernetesQuantity(value)\n if (parsed === undefined) {\n violations.push(`${name} is not a number that can be checked`)\n } else if (parsed > cap) {\n violations.push(`${name} is ${parsed}, and the most is ${cap}`)\n }\n}\n\nfunction checkResource(\n violations: string[],\n path: string[],\n value: unknown,\n kind: 'cpu' | 'memory' | 'storage',\n): void {\n const name = path.join('.')\n const caps = CUSTOMER_HARDWARE_SIZING_RESOURCE_CAPS[componentOf(path)]\n const parsed = parseKubernetesQuantity(value)\n if (parsed === undefined) {\n violations.push(`${name} is not a quantity that can be checked`)\n return\n }\n if (kind === 'cpu') {\n if (parsed > caps.cpu) {\n violations.push(`${name} is ${parsed} CPU, and the most is ${caps.cpu}`)\n }\n return\n }\n const cap = kind === 'memory' ? caps.memoryGi : caps.storageGi\n if (parsed / GIBIBYTE > cap) {\n violations.push(`${name} is over ${cap} GiB`)\n }\n}\n\nfunction walk(violations: string[], node: unknown, path: string[]): void {\n if (!isRecord(node)) {\n return\n }\n for (const [key, value] of Object.entries(node)) {\n // The shape check of console treats `null` like an absent key, so this\n // check does too. A `null` has nothing to cap.\n if (value === null || value === undefined) {\n continue\n }\n const next = [...path, key]\n const parent = path.at(-1)\n const grandParent = path.at(-2)\n if (\n Object.hasOwn(CUSTOMER_HARDWARE_SIZING_COUNT_CAPS, key) &&\n !isRecord(value)\n ) {\n checkNumberCap(\n violations,\n next,\n value,\n CUSTOMER_HARDWARE_SIZING_COUNT_CAPS[\n key as keyof typeof CUSTOMER_HARDWARE_SIZING_COUNT_CAPS\n ],\n )\n } else if (\n (parent === 'limits' || parent === 'requests') &&\n grandParent === 'resources' &&\n (key === 'cpu' || key === 'memory' || key === 'storage')\n ) {\n checkResource(violations, next, value, key)\n } else if (\n (parent === 'limits' || parent === 'requests') &&\n grandParent === 'resources'\n ) {\n // Any other resource name (`ephemeral-storage`, a GPU) has no cap.\n violations.push(`${next.join('.')} is not a resource that has a cap`)\n } else if (key === 'persistentStorageCapacity') {\n // The legacy size of the volume. The operator can still read it.\n checkResource(violations, next, value, 'storage')\n } else if (parent === 'autoResizing' && key === 'increase') {\n // Only the step is capped. The disk itself stops at\n // `storage.resources.limits.storage`: `Plan.type.ts` calls it \"the upper\n // limit the pvc autoscaler can scale up to\", and the operator hands it\n // to the autoscaler as the storage limit of the PVC. The caps for that\n // field, for one node and for all nodes together, bound the size that a\n // disk can reach. `threshold` has no effect on that size.\n const parsed = parseKubernetesQuantity(value)\n if (\n parsed === undefined ||\n parsed / GIBIBYTE > CUSTOMER_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI\n ) {\n violations.push(\n `${next.join('.')} is not a quantity of at most ${CUSTOMER_HARDWARE_SIZING_MAX_AUTO_RESIZE_INCREASE_GI} GiB`,\n )\n }\n } else {\n walk(violations, value, next)\n }\n }\n}\n\nconst larger = (\n block: Record<string, unknown> | undefined,\n key: string,\n): number | undefined => {\n const values = [block?.limits, block?.requests].map((entry) =>\n isRecord(entry) ? parseKubernetesQuantity(entry[key]) : undefined,\n )\n const known = values.filter((value) => value !== undefined)\n return known.length > 0 ? Math.max(...known) : undefined\n}\n\n/**\n * Checks the whole node set of one component against its totals. The caps for\n * one node do not bound `count` nodes together.\n *\n * Kubernetes schedules on `requests`, and a plan can set only `requests`. The\n * total reads the larger of the two, so that neither one skips it. The legacy\n * `persistentStorageCapacity` counts as storage too.\n */\nfunction checkSetTotals(\n violations: string[],\n who: string,\n count: unknown,\n parts: {\n resources: unknown\n storageResources: unknown\n legacyStorage: unknown\n },\n caps: { cpu: number; memoryGi: number; storageGi: number },\n): void {\n const nodes = parseKubernetesQuantity(count)\n if (nodes === undefined) {\n return\n }\n const resources = isRecord(parts.resources) ? parts.resources : undefined\n const storageResources = isRecord(parts.storageResources)\n ? parts.storageResources\n : undefined\n const cpu = larger(resources, 'cpu')\n if (cpu !== undefined && cpu * nodes > caps.cpu) {\n violations.push(`${who} together ask for more than ${caps.cpu} CPU`)\n }\n const memory = larger(resources, 'memory')\n if (memory !== undefined && (memory * nodes) / GIBIBYTE > caps.memoryGi) {\n violations.push(\n `${who} together ask for more than ${caps.memoryGi} GiB of memory`,\n )\n }\n const disks = [\n larger(storageResources, 'storage'),\n parseKubernetesQuantity(parts.legacyStorage),\n ].filter((value) => value !== undefined)\n if (\n disks.length > 0 &&\n (Math.max(...disks) * nodes) / GIBIBYTE > caps.storageGi\n ) {\n violations.push(\n `${who} together ask for more than ${caps.storageGi} GiB of storage`,\n )\n }\n}\n\n/**\n * Walks the plan for every component that has `replicas`, and checks its pods\n * together. The pod resources are in `resources`, or in `backend.resources`.\n * The brokers (`clusterSize`) and Elasticsearch (`nodesCount`) have their own\n * count and their own totals.\n */\nfunction checkReplicaSets(\n violations: string[],\n node: unknown,\n path: string[],\n): void {\n if (!isRecord(node)) {\n return\n }\n if (\n node.replicas !== undefined &&\n node.replicas !== null &&\n !isRecord(node.replicas)\n ) {\n const backend = isRecord(node.backend) ? node.backend : undefined\n const podResources = node.resources ?? backend?.resources\n checkSetTotals(\n violations,\n `the replicas of ${path.join('.') || 'the plan'}`,\n node.replicas,\n {\n resources: podResources,\n // The same block: a pod can ask for storage in its resources too.\n storageResources: podResources,\n legacyStorage: undefined,\n },\n CUSTOMER_HARDWARE_SIZING_REPLICA_SET_TOTAL_CAPS,\n )\n }\n for (const [key, value] of Object.entries(node)) {\n checkReplicaSets(violations, value, [...path, key])\n }\n}\n\nfunction checkNodeSetTotals(\n violations: string[],\n plan: Record<string, unknown>,\n): void {\n const zeebe = isRecord(plan.zeebe) ? plan.zeebe : undefined\n const broker = isRecord(zeebe?.broker) ? zeebe.broker : undefined\n if (broker) {\n const storage = isRecord(broker.storage) ? broker.storage : undefined\n checkSetTotals(\n violations,\n 'the brokers',\n broker.clusterSize,\n {\n resources: broker.resources,\n storageResources: storage?.resources,\n legacyStorage: broker.persistentStorageCapacity,\n },\n CUSTOMER_HARDWARE_SIZING_BROKER_TOTAL_CAPS,\n )\n }\n checkReplicaSets(violations, plan, [])\n const elasticsearch = isRecord(plan.elasticsearch)\n ? plan.elasticsearch\n : undefined\n if (elasticsearch) {\n const config = isRecord(elasticsearch.config)\n ? elasticsearch.config\n : undefined\n const storage = isRecord(config?.storage) ? config.storage : undefined\n checkSetTotals(\n violations,\n 'the Elasticsearch nodes',\n config?.nodesCount,\n {\n resources: elasticsearch.resources,\n storageResources: storage?.resources,\n legacyStorage: elasticsearch.persistentStorageCapacity,\n },\n CUSTOMER_HARDWARE_SIZING_ELASTICSEARCH_TOTAL_CAPS,\n )\n }\n}\n\n/**\n * Checks a cluster plan against the caps of customer hardware sizing.\n *\n * Returns the list of violations, and an empty list when the plan is within\n * the caps. It checks the size only. The shape of the plan is the job of the\n * plan validator of console-backend.\n *\n * A value that this function cannot read is a violation too: a cap that the\n * plan can skip with a strange spelling is no cap.\n */\nexport function checkCustomerHardwareSizing(plan: unknown): string[] {\n if (!isRecord(plan)) {\n return ['the plan is not an object']\n }\n if (\n JSON.stringify(plan).length > CUSTOMER_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH\n ) {\n return [\n `the plan is longer than ${CUSTOMER_HARDWARE_SIZING_MAX_PLAN_JSON_LENGTH} characters`,\n ]\n }\n // `operate.elasticsearch` is the old place of the Elasticsearch block. The\n // caps for Elasticsearch must read it as one place, so the plan is moved\n // to the canonical form first.\n const canonical = canonicalPlan(plan)\n const violations: string[] = []\n walk(violations, canonical, [])\n checkNodeSetTotals(violations, canonical)\n return violations\n}\n"],
5
+ "mappings": "AAMA,SAAS,qBAAqB;AAiBvB,MAAM,gCAAgC,CAAC,UAC5C,UAAU;AAQL,MAAM,mDAAmD;AAGzD,MAAM,2CAA2C;AACjD,MAAM,2CAA2C;AAMjD,MAAM,gDAAgD;AAqBtD,MAAM,yCAAyC;AAAA,EACpD,QAAQ,EAAE,KAAK,KAAK,UAAU,KAAK,WAAW,IAAK;AAAA,EACnD,eAAe,EAAE,KAAK,KAAK,UAAU,KAAM,WAAW,IAAO;AAAA,EAC7D,OAAO,EAAE,KAAK,IAAI,UAAU,KAAK,WAAW,IAAK;AACnD;AAGO,MAAM,sCAAsC;AAAA;AAAA,EAEjD,aAAa;AAAA;AAAA,EAEb,iBAAiB;AAAA;AAAA,EAEjB,mBAAmB;AAAA;AAAA,EAEnB,YAAY;AAAA;AAAA,EAEZ,UAAU;AACZ;AAGO,MAAM,uDAAuD;AAS7D,MAAM,6CAA6C;AAAA,EACxD,KAAK;AAAA,EACL,UAAU;AAAA,EACV,WAAW;AACb;AAQO,MAAM,oDAAoD;AAAA,EAC/D,KAAK;AAAA,EACL,UAAU;AAAA,EACV,WAAW;AACb;AAUO,MAAM,kDAAkD;AAAA,EAC7D,KAAK;AAAA,EACL,UAAU;AAAA,EACV,WAAW;AACb;AAEA,MAAM,WAAW,QAAQ;AAEzB,MAAM,uBAA+C;AAAA,EACnD,IAAI;AAAA,EACJ,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,IAAI;AAAA,EACJ,IAAI,QAAQ;AAAA,EACZ,IAAI,QAAQ;AAAA,EACZ,IAAI,QAAQ;AAAA,EACZ,IAAI,QAAQ;AACd;AAEA,MAAM,mBAAmB;AASlB,SAAS,wBAAwB,OAAoC;AAC1E,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO,SAAS,KAAK,KAAK,SAAS,IAAI,QAAQ;AAAA,EACxD;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,iBAAiB,KAAK,KAAK;AACzC,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,SAAO,OAAO,MAAM,CAAC,CAAC,KAAK,qBAAqB,MAAM,CAAC,KAAK,EAAE,KAAK;AACrE;AAIA,MAAM,WAAW,CAAC,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAErE,SAAS,YAAY,MAA+B;AAClD,MAAI,KAAK,CAAC,MAAM,WAAW,KAAK,CAAC,MAAM,UAAU;AAC/C,WAAO;AAAA,EACT;AACA,MAAI,KAAK,CAAC,MAAM,iBAAiB;AAC/B,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,eACP,YACA,MACA,OACA,KACM;AACN,QAAM,OAAO,KAAK,KAAK,GAAG;AAC1B,QAAM,SAAS,wBAAwB,KAAK;AAC5C,MAAI,WAAW,QAAW;AACxB,eAAW,KAAK,GAAG,IAAI,sCAAsC;AAAA,EAC/D,WAAW,SAAS,KAAK;AACvB,eAAW,KAAK,GAAG,IAAI,OAAO,MAAM,qBAAqB,GAAG,EAAE;AAAA,EAChE;AACF;AAEA,SAAS,cACP,YACA,MACA,OACA,MACM;AACN,QAAM,OAAO,KAAK,KAAK,GAAG;AAC1B,QAAM,OAAO,uCAAuC,YAAY,IAAI,CAAC;AACrE,QAAM,SAAS,wBAAwB,KAAK;AAC5C,MAAI,WAAW,QAAW;AACxB,eAAW,KAAK,GAAG,IAAI,wCAAwC;AAC/D;AAAA,EACF;AACA,MAAI,SAAS,OAAO;AAClB,QAAI,SAAS,KAAK,KAAK;AACrB,iBAAW,KAAK,GAAG,IAAI,OAAO,MAAM,yBAAyB,KAAK,GAAG,EAAE;AAAA,IACzE;AACA;AAAA,EACF;AACA,QAAM,MAAM,SAAS,WAAW,KAAK,WAAW,KAAK;AACrD,MAAI,SAAS,WAAW,KAAK;AAC3B,eAAW,KAAK,GAAG,IAAI,YAAY,GAAG,MAAM;AAAA,EAC9C;AACF;AAEA,SAAS,KAAK,YAAsB,MAAe,MAAsB;AACvE,MAAI,CAAC,SAAS,IAAI,GAAG;AACnB;AAAA,EACF;AACA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAG/C,QAAI,UAAU,QAAQ,UAAU,QAAW;AACzC;AAAA,IACF;AACA,UAAM,OAAO,CAAC,GAAG,MAAM,GAAG;AAC1B,UAAM,SAAS,KAAK,GAAG,EAAE;AACzB,UAAM,cAAc,KAAK,GAAG,EAAE;AAC9B,QACE,OAAO,OAAO,qCAAqC,GAAG,KACtD,CAAC,SAAS,KAAK,GACf;AACA;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA,oCACE,GACF;AAAA,MACF;AAAA,IACF,YACG,WAAW,YAAY,WAAW,eACnC,gBAAgB,gBACf,QAAQ,SAAS,QAAQ,YAAY,QAAQ,YAC9C;AACA,oBAAc,YAAY,MAAM,OAAO,GAAG;AAAA,IAC5C,YACG,WAAW,YAAY,WAAW,eACnC,gBAAgB,aAChB;AAEA,iBAAW,KAAK,GAAG,KAAK,KAAK,GAAG,CAAC,mCAAmC;AAAA,IACtE,WAAW,QAAQ,6BAA6B;AAE9C,oBAAc,YAAY,MAAM,OAAO,SAAS;AAAA,IAClD,WAAW,WAAW,kBAAkB,QAAQ,YAAY;AAO1D,YAAM,SAAS,wBAAwB,KAAK;AAC5C,UACE,WAAW,UACX,SAAS,WAAW,sDACpB;AACA,mBAAW;AAAA,UACT,GAAG,KAAK,KAAK,GAAG,CAAC,iCAAiC,oDAAoD;AAAA,QACxG;AAAA,MACF;AAAA,IACF,OAAO;AACL,WAAK,YAAY,OAAO,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;AAEA,MAAM,SAAS,CACb,OACA,QACuB;AACvB,QAAM,SAAS,CAAC,OAAO,QAAQ,OAAO,QAAQ,EAAE;AAAA,IAAI,CAAC,UACnD,SAAS,KAAK,IAAI,wBAAwB,MAAM,GAAG,CAAC,IAAI;AAAA,EAC1D;AACA,QAAM,QAAQ,OAAO,OAAO,CAAC,UAAU,UAAU,MAAS;AAC1D,SAAO,MAAM,SAAS,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI;AACjD;AAUA,SAAS,eACP,YACA,KACA,OACA,OAKA,MACM;AACN,QAAM,QAAQ,wBAAwB,KAAK;AAC3C,MAAI,UAAU,QAAW;AACvB;AAAA,EACF;AACA,QAAM,YAAY,SAAS,MAAM,SAAS,IAAI,MAAM,YAAY;AAChE,QAAM,mBAAmB,SAAS,MAAM,gBAAgB,IACpD,MAAM,mBACN;AACJ,QAAM,MAAM,OAAO,WAAW,KAAK;AACnC,MAAI,QAAQ,UAAa,MAAM,QAAQ,KAAK,KAAK;AAC/C,eAAW,KAAK,GAAG,GAAG,+BAA+B,KAAK,GAAG,MAAM;AAAA,EACrE;AACA,QAAM,SAAS,OAAO,WAAW,QAAQ;AACzC,MAAI,WAAW,UAAc,SAAS,QAAS,WAAW,KAAK,UAAU;AACvE,eAAW;AAAA,MACT,GAAG,GAAG,+BAA+B,KAAK,QAAQ;AAAA,IACpD;AAAA,EACF;AACA,QAAM,QAAQ;AAAA,IACZ,OAAO,kBAAkB,SAAS;AAAA,IAClC,wBAAwB,MAAM,aAAa;AAAA,EAC7C,EAAE,OAAO,CAAC,UAAU,UAAU,MAAS;AACvC,MACE,MAAM,SAAS,KACd,KAAK,IAAI,GAAG,KAAK,IAAI,QAAS,WAAW,KAAK,WAC/C;AACA,eAAW;AAAA,MACT,GAAG,GAAG,+BAA+B,KAAK,SAAS;AAAA,IACrD;AAAA,EACF;AACF;AAQA,SAAS,iBACP,YACA,MACA,MACM;AACN,MAAI,CAAC,SAAS,IAAI,GAAG;AACnB;AAAA,EACF;AACA,MACE,KAAK,aAAa,UAClB,KAAK,aAAa,QAClB,CAAC,SAAS,KAAK,QAAQ,GACvB;AACA,UAAM,UAAU,SAAS,KAAK,OAAO,IAAI,KAAK,UAAU;AACxD,UAAM,eAAe,KAAK,aAAa,SAAS;AAChD;AAAA,MACE;AAAA,MACA,mBAAmB,KAAK,KAAK,GAAG,KAAK,UAAU;AAAA,MAC/C,KAAK;AAAA,MACL;AAAA,QACE,WAAW;AAAA;AAAA,QAEX,kBAAkB;AAAA,QAClB,eAAe;AAAA,MACjB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,qBAAiB,YAAY,OAAO,CAAC,GAAG,MAAM,GAAG,CAAC;AAAA,EACpD;AACF;AAEA,SAAS,mBACP,YACA,MACM;AACN,QAAM,QAAQ,SAAS,KAAK,KAAK,IAAI,KAAK,QAAQ;AAClD,QAAM,SAAS,SAAS,OAAO,MAAM,IAAI,MAAM,SAAS;AACxD,MAAI,QAAQ;AACV,UAAM,UAAU,SAAS,OAAO,OAAO,IAAI,OAAO,UAAU;AAC5D;AAAA,MACE;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP;AAAA,QACE,WAAW,OAAO;AAAA,QAClB,kBAAkB,SAAS;AAAA,QAC3B,eAAe,OAAO;AAAA,MACxB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,mBAAiB,YAAY,MAAM,CAAC,CAAC;AACrC,QAAM,gBAAgB,SAAS,KAAK,aAAa,IAC7C,KAAK,gBACL;AACJ,MAAI,eAAe;AACjB,UAAM,SAAS,SAAS,cAAc,MAAM,IACxC,cAAc,SACd;AACJ,UAAM,UAAU,SAAS,QAAQ,OAAO,IAAI,OAAO,UAAU;AAC7D;AAAA,MACE;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR;AAAA,QACE,WAAW,cAAc;AAAA,QACzB,kBAAkB,SAAS;AAAA,QAC3B,eAAe,cAAc;AAAA,MAC/B;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAYO,SAAS,4BAA4B,MAAyB;AACnE,MAAI,CAAC,SAAS,IAAI,GAAG;AACnB,WAAO,CAAC,2BAA2B;AAAA,EACrC;AACA,MACE,KAAK,UAAU,IAAI,EAAE,SAAS,+CAC9B;AACA,WAAO;AAAA,MACL,2BAA2B,6CAA6C;AAAA,IAC1E;AAAA,EACF;AAIA,QAAM,YAAY,cAAc,IAAI;AACpC,QAAM,aAAuB,CAAC;AAC9B,OAAK,YAAY,WAAW,CAAC,CAAC;AAC9B,qBAAmB,YAAY,SAAS;AACxC,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -370,3 +370,15 @@ export declare const OPTIONAL_TELEMETRY_MIN_VERSION = "8.10.0-alpha1";
370
370
  * the two values can move apart.
371
371
  */
372
372
  export declare const RESUME_ALPHA_MIN_VERSION = "8.10.0-alpha5";
373
+ /**
374
+ * The cluster serves the public upgrade status endpoint
375
+ * `cluster/v2/status/upgrade` (PDP-3067, camunda/camunda#61876). From this
376
+ * floor the upgrade gate of console-backend asks the cluster if it finished
377
+ * the migrations of its last upgrade. Under it, the 24-hour grace period
378
+ * decides.
379
+ *
380
+ * Spelled `8.10.0`, which sorts over all `8.10.0-*` prereleases. The endpoint
381
+ * lands in the first release of the 8.10 line. The alphas do not have it, and
382
+ * they stay on the grace period.
383
+ */
384
+ export declare const UPGRADE_READINESS_MIN_VERSION = "8.10.0";
@@ -52,6 +52,7 @@ const APP_INTEGRATIONS_89_STREAM_MINOR = "8.9.0";
52
52
  const CLUSTER_SECRETS_LABEL_MIN_VERSION = CONNECTOR_SECRET_SYNC_MIN_VERSION;
53
53
  const OPTIONAL_TELEMETRY_MIN_VERSION = "8.10.0-alpha1";
54
54
  const RESUME_ALPHA_MIN_VERSION = "8.10.0-alpha5";
55
+ const UPGRADE_READINESS_MIN_VERSION = "8.10.0";
55
56
  export {
56
57
  ADMIN_APP_RENAME_MIN_VERSION,
57
58
  APP_INTEGRATIONS_89_STREAM_CONNECTOR_BRIDGE_MIN_VERSION,
@@ -106,6 +107,7 @@ export {
106
107
  TASKLIST_V2_SETTING_MAX_VERSION,
107
108
  TASKLIST_V2_UPGRADE_MIN_VERSION,
108
109
  TASKLIST_V2_UPGRADE_TARGET_MIN_VERSION,
110
+ UPGRADE_READINESS_MIN_VERSION,
109
111
  ZEEBE_REST_URL_MIN_VERSION
110
112
  };
111
113
  //# sourceMappingURL=version-gates.const.js.map
@@ -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 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;",
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\n// ---------------------------------------------------------------------------\n// Generation upgrades\n// ---------------------------------------------------------------------------\n\n/**\n * The cluster serves the public upgrade status endpoint\n * `cluster/v2/status/upgrade` (PDP-3067, camunda/camunda#61876). From this\n * floor the upgrade gate of console-backend asks the cluster if it finished\n * the migrations of its last upgrade. Under it, the 24-hour grace period\n * decides.\n *\n * Spelled `8.10.0`, which sorts over all `8.10.0-*` prereleases. The endpoint\n * lands in the first release of the 8.10 line. The alphas do not have it, and\n * they stay on the grace period.\n */\nexport const UPGRADE_READINESS_MIN_VERSION = '8.10.0'\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;AAiBjC,MAAM,gCAAgC;",
6
6
  "names": []
7
7
  }
@@ -191,9 +191,11 @@ export interface ApprovalAck extends ApprovalActor {
191
191
  * holds, because every other record of the fleet holds an Auth0 `sub`.
192
192
  *
193
193
  * CAUTION: The envelope is a CLAIM of the caller, and not proof. A route that
194
- * acts on it must ask Auth0 again whether `requestedBy` holds the permission
195
- * of the action. `api/admin/approvals` in accounts-backend does that. See the
196
- * note on `M2M_CONTROL_APPROVALS`.
194
+ * acts on it must ask Auth0 again whether `requestedBy` may do the action.
195
+ * That means the permission of the action. For a use case with
196
+ * `openToEveryone`, it means that the person is a Camundi, because no
197
+ * permission runs that action. `api/admin/approvals` in accounts-backend does
198
+ * that. See the note on `M2M_CONTROL_APPROVALS`.
197
199
  */
198
200
  export interface ApprovalM2mActor {
199
201
  /** The id of the approval request, to join the records of both services. */
@@ -454,6 +456,10 @@ export interface ApprovalUseCaseDto {
454
456
  * It is also the permission to SEE a request of this use case, because a
455
457
  * person who cannot act on one has no reason to read it. The person who
456
458
  * opened a request always sees that request.
459
+ *
460
+ * The one exception to "or run" is a use case with `openToEveryone`. It has
461
+ * no approval, so this permission only decides who sees the requests of
462
+ * other people. Any signed-in person can open and run such a request.
457
463
  */
458
464
  requiredPermission: string;
459
465
  /**
@@ -484,4 +490,28 @@ export interface ApprovalUseCaseDto {
484
490
  * reads the user id alone and needs no address.
485
491
  */
486
492
  creatorEmailRequired?: boolean;
493
+ /**
494
+ * A use case with no approval that any signed-in person can open AND run.
495
+ *
496
+ * A request with a floor of zero runs when it is opened, so `canCreate`
497
+ * asks the person to hold `requiredPermission`. This flag lifts that one
498
+ * rule, for an action whose run needs no permission of the person. Set it
499
+ * only when two things hold:
500
+ *
501
+ * - Something else bounds how often a person can ask.
502
+ * - The service that runs the action authorizes the person again at its
503
+ * source, so that nobody can change what is not theirs. The action can
504
+ * touch an organization only when that check lets the person change it.
505
+ *
506
+ * `create-personal-organisation` and the two use cases of customer hardware
507
+ * sizing set it. The two use cases of customer hardware sizing do the second
508
+ * check in two ways:
509
+ *
510
+ * - Creating a plan, and unlinking a plan from one organization, read the
511
+ * role of the person in that organization. Only an OWNER passes.
512
+ * - Deleting a plan reads no role. Console checks that the person created
513
+ * the plan. Accounts reads the organizations that the person is a member
514
+ * of, in any role, and removes the plan from their lists.
515
+ */
516
+ openToEveryone?: boolean;
487
517
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/dtos/ApprovalJob.dto.ts"],
4
- "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\n/**\n * The wire types of the generic approval mechanism.\n *\n * This file holds no logic and no dependency, because three consumers read it:\n * control-backend, control-frontend and cloudbot. The rules that read these\n * types are in `../approvals/policy.js`, and they are pure for the same reason.\n */\n\n/**\n * Every state that an approval request can hold.\n *\n * A `const` tuple, not an enum, so it is both the union type and the list that\n * validates a status filter at run time.\n *\n * The path of a request: `pending` until it reaches the threshold. Then\n * `executing` (automatic mode) or `approved` (explicit mode, which waits for a\n * person to press the button). `executed` or `failed` after the action ran. A\n * request that never reaches the threshold ends `rejected`, `withdrawn` or\n * `expired`.\n */\nexport const APPROVAL_JOB_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n 'executed',\n 'failed',\n 'rejected',\n 'withdrawn',\n 'expired',\n] as const\n\nexport type ApprovalJobStatus = (typeof APPROVAL_JOB_STATUSES)[number]\n\n/**\n * The states in which the action of a request is still COMING.\n *\n * `pending` waits for approvals, `approved` waits for a person to press the\n * button, and `executing` is running now. Each of them can still change the\n * world, so two requests for one target must not be in these states together.\n * `dedupeKey` reads this set for that reason.\n *\n * `failed` is NOT here, although a person can run a failed request again. A\n * failed request runs only when a person presses the button, and that person\n * sees the row. Blocking a new request on a failed one would also trap the\n * key: `canWithdraw` accepts `pending` alone, so nobody could close it.\n *\n * `executed`, `rejected`, `withdrawn` and `expired` are the end of a request.\n */\nexport const APPROVAL_LIVE_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n] as const satisfies readonly ApprovalJobStatus[]\n\n/**\n * How many requests one page of the list holds when the caller asks for no\n * number.\n *\n * The list is a work queue that a Camundi reads, so the page is large: a\n * person wants the whole queue and not ten rows at a time. What the number is\n * for is the ceiling below.\n */\nexport const APPROVAL_PAGE_SIZE_DEFAULT = 500\n\n/**\n * The largest page that the service builds, and the largest that a caller may\n * ask for. The API answers 400 above it.\n *\n * The list had no limit at all. One authenticated caller could then make the\n * service read the whole table into memory, serialize it, and do it again, so\n * the cost of a request grew with the age of the service and with nothing that\n * the caller had to pay for. A ceiling turns that into a fixed cost.\n *\n * It is HERE and not only in the service, because the view must know it: a\n * page that asked for more would get a 400 instead of rows.\n */\nexport const APPROVAL_PAGE_SIZE_MAX = 2000\n\n/**\n * The columns that the queue can be ordered by.\n *\n * A NAME and not an SQL expression: the repository maps each one, so a caller\n * can never name a column that this list does not hold. `status` orders by the\n * EFFECTIVE status, because a `pending` row whose time ran out reads as\n * `expired` everywhere else.\n */\nexport const APPROVAL_SORT_FIELDS = [\n 'createdAt',\n 'expiresAt',\n 'status',\n 'type',\n 'createdBy',\n] as const\n\nexport type ApprovalSortField = (typeof APPROVAL_SORT_FIELDS)[number]\n\nexport type ApprovalSortOrder = 'asc' | 'desc'\n\n/** The default order of the queue: the newest request first. */\nexport const APPROVAL_SORT_DEFAULT: {\n sort: ApprovalSortField\n order: ApprovalSortOrder\n} = { sort: 'createdAt', order: 'desc' }\n\n/**\n * The longest reason that a request, a rejection or a withdrawal accepts.\n *\n * ONE number, because three routes of control-backend hold it and three fields\n * of control-frontend must stop there. A field that let a person write more\n * than the schema accepts turns their sentence into a 400 after they wrote it.\n */\nexport const APPROVAL_REASON_MAX_LENGTH = 2000\n\n/**\n * The largest number of approvals that a caller can ask for.\n *\n * A request that asks for 400 approvals can never reach them, so it is a\n * request that only fills the table and expires. The ceiling turns that into a\n * 400 at the moment a person types it. It is HERE because the form of the\n * bulk operation must stop at the same number as the route.\n */\nexport const APPROVAL_ACKS_MAX = 10\n\n/**\n * The longest search text that the list accepts.\n *\n * A search runs `ILIKE` over the payload of every visible row, so the cost of\n * one call follows the length of the text. Nothing a person types to find a\n * request is near this.\n */\nexport const APPROVAL_SEARCH_MAX_LENGTH = 200\n\n/**\n * The payload field that names the organization a request acts on.\n *\n * It is a CONVENTION, and this constant is what makes the convention readable\n * from both sides. A use case whose action hits one organization puts the id of\n * that organization in this field, and `GET /api/approvals?orgId=\u2026` then finds\n * the request with no knowledge of the use case. The filter reads this one\n * field of the payload, so it matches the whole id and nothing else. The free\n * text search reads the payload as TEXT, which also matches an id that only\n * appears inside another field, and it cannot answer \"the requests of this\n * organization\".\n *\n * The organization page of control-frontend does NOT read this field. It asks\n * the entity-link filter (`entityType=org`), which reads a row that\n * `relatedEntities` of a use case writes \u2014 see `ApprovalEntityRef`. The summary\n * is separate again: the `org` field of a `summarize` result is what a READER\n * follows.\n */\nexport const APPROVAL_PAYLOAD_ORG_FIELD = 'orgId'\n\n/**\n * How many requests each status holds, for the filters of the queue.\n *\n * Every number answers the SAME question as the list beside it: the counts are\n * built with the visibility rule, the action filter and the search text of the\n * caller, and with the lazy expiry. A count that ignored one of those would\n * send a person to a filter that then shows nothing.\n */\nexport interface ApprovalStatusCountsDto {\n /** Every request that matches, whatever its status. */\n all: number\n /** One number for each status. A status with no request is 0, never absent. */\n byStatus: Record<ApprovalJobStatus, number>\n}\n\n/**\n * What happens when a request reaches its threshold.\n *\n * `auto` runs the action immediately. `explicit` holds the request at\n * `approved` and waits for a person to run it.\n */\nexport type ApprovalExecutionMode = 'auto' | 'explicit'\n\n/**\n * WHERE the action runs. It is a separate question from who starts it.\n *\n * - `inline` runs the action inside the call that started it, and the answer\n * already carries the outcome. Right for one call to another service.\n * - `background` answers as soon as the request is claimed, and the action\n * runs on. It reports into `ApprovalJobDto.progress`, and the row is the\n * only place that holds the progress, so a reload and a second person read\n * the same numbers. A pod that stops loses the run, and the reconciler of\n * control-backend recovers the row.\n *\n * An action over thousands of targets MUST be `background`. `inline` would\n * hold one HTTP request for the whole run, and the ingress, the load balancer\n * and the browser each have their own deadline before it finishes.\n */\nexport type ApprovalActionMode = 'inline' | 'background'\n\n/**\n * A person, as an approval request records them.\n *\n * Both keys are kept. The machine then never has to choose one identity key,\n * which is what split the earlier acknowledgement flows: some held an email and\n * some held a user id, and the two could not be compared.\n */\nexport interface ApprovalActor {\n userId: string\n email: string\n}\n\nexport interface ApprovalAck extends ApprovalActor {\n /** An ISO timestamp. It is in a json column, so it is a string, not a Date. */\n date: string\n}\n\n/**\n * Who asked for an action, and who agreed to it.\n *\n * Every route of `api/admin/approvals` takes this envelope, and control-backend\n * builds it from the request that it runs. The envelope exists because the\n * token cannot carry it: the call is machine to machine, so `sub` holds the id\n * of the Auth0 application and not a person. A service that reads the token\n * alone therefore writes \"control-backend\" into its audit record, which hides\n * the two Camundis that the whole mechanism exists to record.\n *\n * `approvedBy` is a list, because a use case can need more than one approval,\n * and the record must name all of them. It can also be EMPTY: how many\n * approvals an action needs is a number of the use case, and `move-cluster-org`\n * holds zero on purpose. The person in `requestedBy` is never in the list: the\n * four-eyes rule is that the person who asks is not the person who agrees, and\n * control-backend holds that rule.\n *\n * Each person carries both keys of `ApprovalActor`. The email is what a route\n * uses to ask Auth0 about the person, and the user id is what an audit record\n * holds, because every other record of the fleet holds an Auth0 `sub`.\n *\n * CAUTION: The envelope is a CLAIM of the caller, and not proof. A route that\n * acts on it must ask Auth0 again whether `requestedBy` holds the permission\n * of the action. `api/admin/approvals` in accounts-backend does that. See the\n * note on `M2M_CONTROL_APPROVALS`.\n */\nexport interface ApprovalM2mActor {\n /** The id of the approval request, to join the records of both services. */\n requestId: string\n /** The Camundi who opened the request. */\n requestedBy: ApprovalActor\n /** The Camundis who approved it. Empty when the use case needs none. */\n approvedBy: ApprovalActor[]\n}\n\n/**\n * What one value of a summary field is.\n *\n * `text` is what a person reads. `id` is what a link points at, and it is\n * absent when the kind names no entity. The two are separate because the text\n * is a COPY that the request keeps: an organization that gets a new name after\n * the request was opened must still read as the organization that the person\n * chose.\n */\nexport interface ApprovalSummaryValue {\n text: string\n id?: string\n}\n\n/**\n * How a reader must render one field of a summary.\n *\n * `org`, `cluster` and `user` name an entity, so a reader can link to it and\n * can resolve a fresh name. The others are plain: `date` is an ISO timestamp,\n * and `code` is a value that must keep its characters, such as a json fragment.\n */\nexport const APPROVAL_SUMMARY_FIELD_KINDS = [\n 'text',\n 'number',\n 'boolean',\n 'date',\n 'org',\n 'cluster',\n 'user',\n 'code',\n] as const\n\nexport type ApprovalSummaryFieldKind =\n (typeof APPROVAL_SUMMARY_FIELD_KINDS)[number]\n\n/**\n * One labelled field of the payload of a request.\n *\n * This is what makes ONE view able to show EVERY use case. The payload of a\n * request is an object of anything, so a generic view can only print it as\n * json. The use case turns it into labelled fields here, and the view renders\n * a field by its kind: it links an `org`, it formats a `date`, and it lists\n * 300 clusters as 300 links.\n *\n * `values` is always an array. A field that holds one value holds an array of\n * one, so a reader needs no branch for \"one or many\".\n */\nexport interface ApprovalSummaryField {\n label: string\n kind: ApprovalSummaryFieldKind\n values: ApprovalSummaryValue[]\n /**\n * How many values the field holds, when `values` holds only the first of\n * them. The list route cuts every field to `APPROVAL_LIST_SUMMARY_VALUES_MAX`\n * values. The detail route sends every value and no `total`.\n */\n total?: number\n}\n\n/**\n * How many values of one summary field a row of the list carries.\n *\n * A bulk operation names up to 3000 clusters in one field. A page of 100 such\n * rows would carry 300,000 values that no row shows. The view shows at most\n * 12 values of a field, so the list stops there, and `total` holds the count.\n */\nexport const APPROVAL_LIST_SUMMARY_VALUES_MAX = 12\n\n/**\n * The kinds of entity that an approval request can concern.\n *\n * These are the entities that a request is findable by: the org and cluster\n * detail pages ask \"which requests touch me\", and the answer reads a row for\n * each entity. It is a subset of `APPROVAL_SUMMARY_FIELD_KINDS` \u2014 the kinds that\n * name an entity \u2014 because only those carry an id that a page can match.\n */\nexport const APPROVAL_ENTITY_TYPES = ['cluster', 'org', 'user'] as const\n\nexport type ApprovalEntityType = (typeof APPROVAL_ENTITY_TYPES)[number]\n\n/**\n * One entity that an approval request concerns.\n *\n * A use case reports these from its payload, and the service writes one row for\n * each into `approval_job_entity`. That row is what makes a request findable by\n * an entity it touches, without a text search over the payload: a cluster-move\n * request names its cluster, its target organization AND its source\n * organization, so it shows on all three detail pages. The `summary` still\n * carries the words a reader sees; this carries only the ids a page matches.\n */\nexport interface ApprovalEntityRef {\n type: ApprovalEntityType\n id: string\n}\n\n/**\n * How long a run may go quiet before a reader calls it stalled.\n *\n * The pod that runs an action writes its progress and renews its lease on one\n * timer, so a healthy run touches the row well inside this window. A row that\n * is `executing` and older than this held a pod that stopped, and the\n * reconciler of control-backend is on its way to it.\n *\n * It lives here, and not in the config of the service, because a READER needs\n * it: the browser must decide what to show without a second call, and a number\n * that only the backend knew would make the two disagree. `ACTION_LEASE_MS` in\n * `config/index.ts` is held below this on purpose.\n */\nexport const APPROVAL_RUN_STALE_MS = 120_000\n\n/**\n * What an action reports while it runs.\n *\n * This is what makes a long action watchable. Every field is in the DATABASE\n * and not in the answer to the call that started the run, so three readers see\n * the same thing: the person who pressed the button, the same person after a\n * reload, and a second person who opens the request.\n *\n * `total` can be 0 while an action counts its targets, and it stays 0 for an\n * action that cannot count them. A reader must then show that it runs, and no\n * percentage.\n */\nexport interface ApprovalProgress {\n /** Targets in total. 0 means the action did not say. */\n total: number\n /** Targets finished, whatever the outcome of each one. */\n done: number\n /** Targets that finished with an error. Part of `done`. */\n failed: number\n /** One short line for a person, such as the step that runs now. */\n note?: string\n /** An ISO timestamp: when the run started. */\n startedAt: string\n /** An ISO timestamp: when the run last wrote anything. */\n updatedAt: string\n}\n\n/** The share of the work that is finished, or `undefined` with no total. */\nexport function progressFraction(\n progress: ApprovalProgress | undefined,\n): number | undefined {\n if (!progress || progress.total <= 0) {\n return undefined\n }\n return Math.min(1, progress.done / progress.total)\n}\n\n/**\n * Reports whether a running request went quiet.\n *\n * True means that the pod which held the run stopped, or lost the row. It does\n * NOT mean that the action failed: the reconciler either runs it again, or it\n * writes `failed` so that a person can. See `APPROVAL_RUN_STALE_MS`.\n */\nexport function isRunStalled(job: ApprovalJobDto, now: Date): boolean {\n if (job.status !== 'executing') {\n return false\n }\n const lastSign = job.progress?.updatedAt ?? job.executingSince\n if (!lastSign) {\n return false\n }\n return now.getTime() - new Date(lastSign).getTime() > APPROVAL_RUN_STALE_MS\n}\n\n/** One approval request, as the API returns it. */\nexport interface ApprovalJobDto {\n id: string\n type: string\n dedupeKey?: string\n payload: Record<string, unknown>\n /**\n * The payload as labelled fields. See `ApprovalSummaryField`.\n *\n * The use case builds it when the request is opened AND every time the\n * request is read, so a field that a later version of the use case adds\n * appears on a request that is already open.\n */\n summary: ApprovalSummaryField[]\n /** Why the person opened the request. Some use cases demand one. */\n reason?: string\n createdBy: ApprovalActor\n acks: ApprovalAck[]\n /**\n * How many approvals THIS request needs.\n *\n * It is on the request and not only on the use case, because the person who\n * opens one can ask for more than the floor of the use case. A bulk\n * operation over 3000 clusters is the same action as one over 3, and it can\n * still deserve a third pair of eyes. `ApprovalUseCaseDto.minAcks` is the\n * floor that a caller cannot go below.\n */\n requiredAcks: number\n /**\n * What a person must hold to approve, reject or run THIS request.\n *\n * It is on the request and not only on the use case, because a payload can\n * raise the guard: a generation update with `yoloForceUpdate` needs the yolo\n * permission on top of the base one, and the same use case without the flag\n * does not. The set is computed ONE time, when the request is opened, so it\n * is also the record of what the request was guarded by.\n *\n * A person needs EVERY permission in the set, not one of them.\n */\n requiredPermissions: string[]\n executionMode: ApprovalExecutionMode\n actionMode: ApprovalActionMode\n status: ApprovalJobStatus\n closedBy?: ApprovalActor\n closedReason?: string\n /** An ISO timestamp. */\n expiresAt: string\n /**\n * An ISO timestamp: when the current run took the request.\n *\n * It is set every time a run starts, so a run that the reconciler starts\n * again moves it. `attempts` says how many runs there were.\n */\n executingSince?: string\n /** How many times a run started. A recovery raises it. */\n attempts: number\n /**\n * What the action reports while it runs. See `ApprovalProgress`.\n *\n * It stays on the row after the run, so the record of a finished run says\n * how many targets it touched and how many refused.\n */\n progress?: ApprovalProgress\n executedAt?: string\n executionError?: string\n /**\n * What the action reported when it ran.\n *\n * The shape belongs to the use case. An action over many targets puts the\n * result of each target here, which is the difference between \"it failed\"\n * and \"it did 2998 of 3000, and these two refused\".\n */\n executionResult?: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/**\n * What a use case is, as the API reports it.\n *\n * The frontend needs `requiredPermission` to run the same rule as the backend.\n * PR #9301 kept this registry a second time, by hand, in the frontend. A\n * registry that the server owns means a new use case needs no frontend deploy.\n */\nexport interface ApprovalUseCaseDto {\n type: string\n /** A short name for a person to read. */\n label: string\n /** One sentence: what the action does, and what it cannot undo. */\n description: string\n /** The approvals a new request asks for when the caller names no number. */\n defaultAcks: number\n /**\n * The floor. A caller can ask for more approvals, never for fewer.\n *\n * It is a separate number from `defaultAcks` because the two answer\n * different questions: the default is what the form offers, and the floor is\n * what the service refuses to go below.\n */\n minAcks: number\n ttlDays: number\n executionMode: ApprovalExecutionMode\n /** See `ApprovalActionMode`. An action over many targets is `background`. */\n actionMode: ApprovalActionMode\n /**\n * True when a run that was interrupted can start again by itself.\n *\n * It is a promise about the ACTION, and the reconciler reads it. Set it only\n * when the action can run a second time over the same payload and do no harm\n * twice: it must read what the first run already finished, out of\n * `ApprovalJobDto.executionResult`, and skip it.\n *\n * False, the default, is the safe answer. An interrupted run then lands in\n * `failed`, which a person can run again from the detail page, and a person\n * decides whether running it twice is safe.\n */\n resumable: boolean\n /**\n * The permission that every request of this use case needs to approve,\n * reject or run. A payload can add more. See\n * `ApprovalJobDto.requiredPermissions`, which is the effective set.\n *\n * It is also the permission to SEE a request of this use case, because a\n * person who cannot act on one has no reason to read it. The person who\n * opened a request always sees that request.\n */\n requiredPermission: string\n /**\n * The permission to open a request. Absent means any signed-in Camundi.\n *\n * Set it when the pool that can ASK must be as small as the pool that can\n * approve. Leave it absent when the approvals alone are the guard, and the\n * proposal is not.\n */\n createPermission?: string\n /** True when a request cannot be opened without a reason. */\n reasonRequired: boolean\n /**\n * True when the ACTION needs the email address of the person who asked.\n *\n * A token can carry no address, and the row then stores\n * `UNKNOWN_ACTOR_EMAIL`. An action that sends that placeholder to another\n * service asks it to find a user that does not exist, so the request can\n * only fail \u2014 and it fails at the END, after other people spent their\n * approvals on it. A use case that carries the creator into its call sets\n * this, and `canCreate` then refuses such a caller at the start.\n *\n * `extend-trial` and `send-to-expired-trial` are two: both send the creator\n * to accounts in an `ApprovalM2mActor`, and accounts reads the permissions\n * of that person again before it acts.\n *\n * `move-cluster-org` does NOT set this. Its envelope reaches console, which\n * reads the user id alone and needs no address.\n */\n creatorEmailRequired?: boolean\n}\n"],
5
- "mappings": "AA0BO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAmBO,MAAM,yBAAyB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF;AAUO,MAAM,6BAA6B;AAcnC,MAAM,yBAAyB;AAU/B,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,MAAM,wBAGT,EAAE,MAAM,aAAa,OAAO,OAAO;AAShC,MAAM,6BAA6B;AAUnC,MAAM,oBAAoB;AAS1B,MAAM,6BAA6B;AAoBnC,MAAM,6BAA6B;AAmHnC,MAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAoCO,MAAM,mCAAmC;AAUzC,MAAM,wBAAwB,CAAC,WAAW,OAAO,MAAM;AAgCvD,MAAM,wBAAwB;AA8B9B,SAAS,iBACd,UACoB;AACpB,MAAI,CAAC,YAAY,SAAS,SAAS,GAAG;AACpC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,GAAG,SAAS,OAAO,SAAS,KAAK;AACnD;AASO,SAAS,aAAa,KAAqB,KAAoB;AACpE,MAAI,IAAI,WAAW,aAAa;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,WAAW,IAAI,UAAU,aAAa,IAAI;AAChD,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AACA,SAAO,IAAI,QAAQ,IAAI,IAAI,KAAK,QAAQ,EAAE,QAAQ,IAAI;AACxD;",
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\n/**\n * The wire types of the generic approval mechanism.\n *\n * This file holds no logic and no dependency, because three consumers read it:\n * control-backend, control-frontend and cloudbot. The rules that read these\n * types are in `../approvals/policy.js`, and they are pure for the same reason.\n */\n\n/**\n * Every state that an approval request can hold.\n *\n * A `const` tuple, not an enum, so it is both the union type and the list that\n * validates a status filter at run time.\n *\n * The path of a request: `pending` until it reaches the threshold. Then\n * `executing` (automatic mode) or `approved` (explicit mode, which waits for a\n * person to press the button). `executed` or `failed` after the action ran. A\n * request that never reaches the threshold ends `rejected`, `withdrawn` or\n * `expired`.\n */\nexport const APPROVAL_JOB_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n 'executed',\n 'failed',\n 'rejected',\n 'withdrawn',\n 'expired',\n] as const\n\nexport type ApprovalJobStatus = (typeof APPROVAL_JOB_STATUSES)[number]\n\n/**\n * The states in which the action of a request is still COMING.\n *\n * `pending` waits for approvals, `approved` waits for a person to press the\n * button, and `executing` is running now. Each of them can still change the\n * world, so two requests for one target must not be in these states together.\n * `dedupeKey` reads this set for that reason.\n *\n * `failed` is NOT here, although a person can run a failed request again. A\n * failed request runs only when a person presses the button, and that person\n * sees the row. Blocking a new request on a failed one would also trap the\n * key: `canWithdraw` accepts `pending` alone, so nobody could close it.\n *\n * `executed`, `rejected`, `withdrawn` and `expired` are the end of a request.\n */\nexport const APPROVAL_LIVE_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n] as const satisfies readonly ApprovalJobStatus[]\n\n/**\n * How many requests one page of the list holds when the caller asks for no\n * number.\n *\n * The list is a work queue that a Camundi reads, so the page is large: a\n * person wants the whole queue and not ten rows at a time. What the number is\n * for is the ceiling below.\n */\nexport const APPROVAL_PAGE_SIZE_DEFAULT = 500\n\n/**\n * The largest page that the service builds, and the largest that a caller may\n * ask for. The API answers 400 above it.\n *\n * The list had no limit at all. One authenticated caller could then make the\n * service read the whole table into memory, serialize it, and do it again, so\n * the cost of a request grew with the age of the service and with nothing that\n * the caller had to pay for. A ceiling turns that into a fixed cost.\n *\n * It is HERE and not only in the service, because the view must know it: a\n * page that asked for more would get a 400 instead of rows.\n */\nexport const APPROVAL_PAGE_SIZE_MAX = 2000\n\n/**\n * The columns that the queue can be ordered by.\n *\n * A NAME and not an SQL expression: the repository maps each one, so a caller\n * can never name a column that this list does not hold. `status` orders by the\n * EFFECTIVE status, because a `pending` row whose time ran out reads as\n * `expired` everywhere else.\n */\nexport const APPROVAL_SORT_FIELDS = [\n 'createdAt',\n 'expiresAt',\n 'status',\n 'type',\n 'createdBy',\n] as const\n\nexport type ApprovalSortField = (typeof APPROVAL_SORT_FIELDS)[number]\n\nexport type ApprovalSortOrder = 'asc' | 'desc'\n\n/** The default order of the queue: the newest request first. */\nexport const APPROVAL_SORT_DEFAULT: {\n sort: ApprovalSortField\n order: ApprovalSortOrder\n} = { sort: 'createdAt', order: 'desc' }\n\n/**\n * The longest reason that a request, a rejection or a withdrawal accepts.\n *\n * ONE number, because three routes of control-backend hold it and three fields\n * of control-frontend must stop there. A field that let a person write more\n * than the schema accepts turns their sentence into a 400 after they wrote it.\n */\nexport const APPROVAL_REASON_MAX_LENGTH = 2000\n\n/**\n * The largest number of approvals that a caller can ask for.\n *\n * A request that asks for 400 approvals can never reach them, so it is a\n * request that only fills the table and expires. The ceiling turns that into a\n * 400 at the moment a person types it. It is HERE because the form of the\n * bulk operation must stop at the same number as the route.\n */\nexport const APPROVAL_ACKS_MAX = 10\n\n/**\n * The longest search text that the list accepts.\n *\n * A search runs `ILIKE` over the payload of every visible row, so the cost of\n * one call follows the length of the text. Nothing a person types to find a\n * request is near this.\n */\nexport const APPROVAL_SEARCH_MAX_LENGTH = 200\n\n/**\n * The payload field that names the organization a request acts on.\n *\n * It is a CONVENTION, and this constant is what makes the convention readable\n * from both sides. A use case whose action hits one organization puts the id of\n * that organization in this field, and `GET /api/approvals?orgId=\u2026` then finds\n * the request with no knowledge of the use case. The filter reads this one\n * field of the payload, so it matches the whole id and nothing else. The free\n * text search reads the payload as TEXT, which also matches an id that only\n * appears inside another field, and it cannot answer \"the requests of this\n * organization\".\n *\n * The organization page of control-frontend does NOT read this field. It asks\n * the entity-link filter (`entityType=org`), which reads a row that\n * `relatedEntities` of a use case writes \u2014 see `ApprovalEntityRef`. The summary\n * is separate again: the `org` field of a `summarize` result is what a READER\n * follows.\n */\nexport const APPROVAL_PAYLOAD_ORG_FIELD = 'orgId'\n\n/**\n * How many requests each status holds, for the filters of the queue.\n *\n * Every number answers the SAME question as the list beside it: the counts are\n * built with the visibility rule, the action filter and the search text of the\n * caller, and with the lazy expiry. A count that ignored one of those would\n * send a person to a filter that then shows nothing.\n */\nexport interface ApprovalStatusCountsDto {\n /** Every request that matches, whatever its status. */\n all: number\n /** One number for each status. A status with no request is 0, never absent. */\n byStatus: Record<ApprovalJobStatus, number>\n}\n\n/**\n * What happens when a request reaches its threshold.\n *\n * `auto` runs the action immediately. `explicit` holds the request at\n * `approved` and waits for a person to run it.\n */\nexport type ApprovalExecutionMode = 'auto' | 'explicit'\n\n/**\n * WHERE the action runs. It is a separate question from who starts it.\n *\n * - `inline` runs the action inside the call that started it, and the answer\n * already carries the outcome. Right for one call to another service.\n * - `background` answers as soon as the request is claimed, and the action\n * runs on. It reports into `ApprovalJobDto.progress`, and the row is the\n * only place that holds the progress, so a reload and a second person read\n * the same numbers. A pod that stops loses the run, and the reconciler of\n * control-backend recovers the row.\n *\n * An action over thousands of targets MUST be `background`. `inline` would\n * hold one HTTP request for the whole run, and the ingress, the load balancer\n * and the browser each have their own deadline before it finishes.\n */\nexport type ApprovalActionMode = 'inline' | 'background'\n\n/**\n * A person, as an approval request records them.\n *\n * Both keys are kept. The machine then never has to choose one identity key,\n * which is what split the earlier acknowledgement flows: some held an email and\n * some held a user id, and the two could not be compared.\n */\nexport interface ApprovalActor {\n userId: string\n email: string\n}\n\nexport interface ApprovalAck extends ApprovalActor {\n /** An ISO timestamp. It is in a json column, so it is a string, not a Date. */\n date: string\n}\n\n/**\n * Who asked for an action, and who agreed to it.\n *\n * Every route of `api/admin/approvals` takes this envelope, and control-backend\n * builds it from the request that it runs. The envelope exists because the\n * token cannot carry it: the call is machine to machine, so `sub` holds the id\n * of the Auth0 application and not a person. A service that reads the token\n * alone therefore writes \"control-backend\" into its audit record, which hides\n * the two Camundis that the whole mechanism exists to record.\n *\n * `approvedBy` is a list, because a use case can need more than one approval,\n * and the record must name all of them. It can also be EMPTY: how many\n * approvals an action needs is a number of the use case, and `move-cluster-org`\n * holds zero on purpose. The person in `requestedBy` is never in the list: the\n * four-eyes rule is that the person who asks is not the person who agrees, and\n * control-backend holds that rule.\n *\n * Each person carries both keys of `ApprovalActor`. The email is what a route\n * uses to ask Auth0 about the person, and the user id is what an audit record\n * holds, because every other record of the fleet holds an Auth0 `sub`.\n *\n * CAUTION: The envelope is a CLAIM of the caller, and not proof. A route that\n * acts on it must ask Auth0 again whether `requestedBy` may do the action.\n * That means the permission of the action. For a use case with\n * `openToEveryone`, it means that the person is a Camundi, because no\n * permission runs that action. `api/admin/approvals` in accounts-backend does\n * that. See the note on `M2M_CONTROL_APPROVALS`.\n */\nexport interface ApprovalM2mActor {\n /** The id of the approval request, to join the records of both services. */\n requestId: string\n /** The Camundi who opened the request. */\n requestedBy: ApprovalActor\n /** The Camundis who approved it. Empty when the use case needs none. */\n approvedBy: ApprovalActor[]\n}\n\n/**\n * What one value of a summary field is.\n *\n * `text` is what a person reads. `id` is what a link points at, and it is\n * absent when the kind names no entity. The two are separate because the text\n * is a COPY that the request keeps: an organization that gets a new name after\n * the request was opened must still read as the organization that the person\n * chose.\n */\nexport interface ApprovalSummaryValue {\n text: string\n id?: string\n}\n\n/**\n * How a reader must render one field of a summary.\n *\n * `org`, `cluster` and `user` name an entity, so a reader can link to it and\n * can resolve a fresh name. The others are plain: `date` is an ISO timestamp,\n * and `code` is a value that must keep its characters, such as a json fragment.\n */\nexport const APPROVAL_SUMMARY_FIELD_KINDS = [\n 'text',\n 'number',\n 'boolean',\n 'date',\n 'org',\n 'cluster',\n 'user',\n 'code',\n] as const\n\nexport type ApprovalSummaryFieldKind =\n (typeof APPROVAL_SUMMARY_FIELD_KINDS)[number]\n\n/**\n * One labelled field of the payload of a request.\n *\n * This is what makes ONE view able to show EVERY use case. The payload of a\n * request is an object of anything, so a generic view can only print it as\n * json. The use case turns it into labelled fields here, and the view renders\n * a field by its kind: it links an `org`, it formats a `date`, and it lists\n * 300 clusters as 300 links.\n *\n * `values` is always an array. A field that holds one value holds an array of\n * one, so a reader needs no branch for \"one or many\".\n */\nexport interface ApprovalSummaryField {\n label: string\n kind: ApprovalSummaryFieldKind\n values: ApprovalSummaryValue[]\n /**\n * How many values the field holds, when `values` holds only the first of\n * them. The list route cuts every field to `APPROVAL_LIST_SUMMARY_VALUES_MAX`\n * values. The detail route sends every value and no `total`.\n */\n total?: number\n}\n\n/**\n * How many values of one summary field a row of the list carries.\n *\n * A bulk operation names up to 3000 clusters in one field. A page of 100 such\n * rows would carry 300,000 values that no row shows. The view shows at most\n * 12 values of a field, so the list stops there, and `total` holds the count.\n */\nexport const APPROVAL_LIST_SUMMARY_VALUES_MAX = 12\n\n/**\n * The kinds of entity that an approval request can concern.\n *\n * These are the entities that a request is findable by: the org and cluster\n * detail pages ask \"which requests touch me\", and the answer reads a row for\n * each entity. It is a subset of `APPROVAL_SUMMARY_FIELD_KINDS` \u2014 the kinds that\n * name an entity \u2014 because only those carry an id that a page can match.\n */\nexport const APPROVAL_ENTITY_TYPES = ['cluster', 'org', 'user'] as const\n\nexport type ApprovalEntityType = (typeof APPROVAL_ENTITY_TYPES)[number]\n\n/**\n * One entity that an approval request concerns.\n *\n * A use case reports these from its payload, and the service writes one row for\n * each into `approval_job_entity`. That row is what makes a request findable by\n * an entity it touches, without a text search over the payload: a cluster-move\n * request names its cluster, its target organization AND its source\n * organization, so it shows on all three detail pages. The `summary` still\n * carries the words a reader sees; this carries only the ids a page matches.\n */\nexport interface ApprovalEntityRef {\n type: ApprovalEntityType\n id: string\n}\n\n/**\n * How long a run may go quiet before a reader calls it stalled.\n *\n * The pod that runs an action writes its progress and renews its lease on one\n * timer, so a healthy run touches the row well inside this window. A row that\n * is `executing` and older than this held a pod that stopped, and the\n * reconciler of control-backend is on its way to it.\n *\n * It lives here, and not in the config of the service, because a READER needs\n * it: the browser must decide what to show without a second call, and a number\n * that only the backend knew would make the two disagree. `ACTION_LEASE_MS` in\n * `config/index.ts` is held below this on purpose.\n */\nexport const APPROVAL_RUN_STALE_MS = 120_000\n\n/**\n * What an action reports while it runs.\n *\n * This is what makes a long action watchable. Every field is in the DATABASE\n * and not in the answer to the call that started the run, so three readers see\n * the same thing: the person who pressed the button, the same person after a\n * reload, and a second person who opens the request.\n *\n * `total` can be 0 while an action counts its targets, and it stays 0 for an\n * action that cannot count them. A reader must then show that it runs, and no\n * percentage.\n */\nexport interface ApprovalProgress {\n /** Targets in total. 0 means the action did not say. */\n total: number\n /** Targets finished, whatever the outcome of each one. */\n done: number\n /** Targets that finished with an error. Part of `done`. */\n failed: number\n /** One short line for a person, such as the step that runs now. */\n note?: string\n /** An ISO timestamp: when the run started. */\n startedAt: string\n /** An ISO timestamp: when the run last wrote anything. */\n updatedAt: string\n}\n\n/** The share of the work that is finished, or `undefined` with no total. */\nexport function progressFraction(\n progress: ApprovalProgress | undefined,\n): number | undefined {\n if (!progress || progress.total <= 0) {\n return undefined\n }\n return Math.min(1, progress.done / progress.total)\n}\n\n/**\n * Reports whether a running request went quiet.\n *\n * True means that the pod which held the run stopped, or lost the row. It does\n * NOT mean that the action failed: the reconciler either runs it again, or it\n * writes `failed` so that a person can. See `APPROVAL_RUN_STALE_MS`.\n */\nexport function isRunStalled(job: ApprovalJobDto, now: Date): boolean {\n if (job.status !== 'executing') {\n return false\n }\n const lastSign = job.progress?.updatedAt ?? job.executingSince\n if (!lastSign) {\n return false\n }\n return now.getTime() - new Date(lastSign).getTime() > APPROVAL_RUN_STALE_MS\n}\n\n/** One approval request, as the API returns it. */\nexport interface ApprovalJobDto {\n id: string\n type: string\n dedupeKey?: string\n payload: Record<string, unknown>\n /**\n * The payload as labelled fields. See `ApprovalSummaryField`.\n *\n * The use case builds it when the request is opened AND every time the\n * request is read, so a field that a later version of the use case adds\n * appears on a request that is already open.\n */\n summary: ApprovalSummaryField[]\n /** Why the person opened the request. Some use cases demand one. */\n reason?: string\n createdBy: ApprovalActor\n acks: ApprovalAck[]\n /**\n * How many approvals THIS request needs.\n *\n * It is on the request and not only on the use case, because the person who\n * opens one can ask for more than the floor of the use case. A bulk\n * operation over 3000 clusters is the same action as one over 3, and it can\n * still deserve a third pair of eyes. `ApprovalUseCaseDto.minAcks` is the\n * floor that a caller cannot go below.\n */\n requiredAcks: number\n /**\n * What a person must hold to approve, reject or run THIS request.\n *\n * It is on the request and not only on the use case, because a payload can\n * raise the guard: a generation update with `yoloForceUpdate` needs the yolo\n * permission on top of the base one, and the same use case without the flag\n * does not. The set is computed ONE time, when the request is opened, so it\n * is also the record of what the request was guarded by.\n *\n * A person needs EVERY permission in the set, not one of them.\n */\n requiredPermissions: string[]\n executionMode: ApprovalExecutionMode\n actionMode: ApprovalActionMode\n status: ApprovalJobStatus\n closedBy?: ApprovalActor\n closedReason?: string\n /** An ISO timestamp. */\n expiresAt: string\n /**\n * An ISO timestamp: when the current run took the request.\n *\n * It is set every time a run starts, so a run that the reconciler starts\n * again moves it. `attempts` says how many runs there were.\n */\n executingSince?: string\n /** How many times a run started. A recovery raises it. */\n attempts: number\n /**\n * What the action reports while it runs. See `ApprovalProgress`.\n *\n * It stays on the row after the run, so the record of a finished run says\n * how many targets it touched and how many refused.\n */\n progress?: ApprovalProgress\n executedAt?: string\n executionError?: string\n /**\n * What the action reported when it ran.\n *\n * The shape belongs to the use case. An action over many targets puts the\n * result of each target here, which is the difference between \"it failed\"\n * and \"it did 2998 of 3000, and these two refused\".\n */\n executionResult?: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/**\n * What a use case is, as the API reports it.\n *\n * The frontend needs `requiredPermission` to run the same rule as the backend.\n * PR #9301 kept this registry a second time, by hand, in the frontend. A\n * registry that the server owns means a new use case needs no frontend deploy.\n */\nexport interface ApprovalUseCaseDto {\n type: string\n /** A short name for a person to read. */\n label: string\n /** One sentence: what the action does, and what it cannot undo. */\n description: string\n /** The approvals a new request asks for when the caller names no number. */\n defaultAcks: number\n /**\n * The floor. A caller can ask for more approvals, never for fewer.\n *\n * It is a separate number from `defaultAcks` because the two answer\n * different questions: the default is what the form offers, and the floor is\n * what the service refuses to go below.\n */\n minAcks: number\n ttlDays: number\n executionMode: ApprovalExecutionMode\n /** See `ApprovalActionMode`. An action over many targets is `background`. */\n actionMode: ApprovalActionMode\n /**\n * True when a run that was interrupted can start again by itself.\n *\n * It is a promise about the ACTION, and the reconciler reads it. Set it only\n * when the action can run a second time over the same payload and do no harm\n * twice: it must read what the first run already finished, out of\n * `ApprovalJobDto.executionResult`, and skip it.\n *\n * False, the default, is the safe answer. An interrupted run then lands in\n * `failed`, which a person can run again from the detail page, and a person\n * decides whether running it twice is safe.\n */\n resumable: boolean\n /**\n * The permission that every request of this use case needs to approve,\n * reject or run. A payload can add more. See\n * `ApprovalJobDto.requiredPermissions`, which is the effective set.\n *\n * It is also the permission to SEE a request of this use case, because a\n * person who cannot act on one has no reason to read it. The person who\n * opened a request always sees that request.\n *\n * The one exception to \"or run\" is a use case with `openToEveryone`. It has\n * no approval, so this permission only decides who sees the requests of\n * other people. Any signed-in person can open and run such a request.\n */\n requiredPermission: string\n /**\n * The permission to open a request. Absent means any signed-in Camundi.\n *\n * Set it when the pool that can ASK must be as small as the pool that can\n * approve. Leave it absent when the approvals alone are the guard, and the\n * proposal is not.\n */\n createPermission?: string\n /** True when a request cannot be opened without a reason. */\n reasonRequired: boolean\n /**\n * True when the ACTION needs the email address of the person who asked.\n *\n * A token can carry no address, and the row then stores\n * `UNKNOWN_ACTOR_EMAIL`. An action that sends that placeholder to another\n * service asks it to find a user that does not exist, so the request can\n * only fail \u2014 and it fails at the END, after other people spent their\n * approvals on it. A use case that carries the creator into its call sets\n * this, and `canCreate` then refuses such a caller at the start.\n *\n * `extend-trial` and `send-to-expired-trial` are two: both send the creator\n * to accounts in an `ApprovalM2mActor`, and accounts reads the permissions\n * of that person again before it acts.\n *\n * `move-cluster-org` does NOT set this. Its envelope reaches console, which\n * reads the user id alone and needs no address.\n */\n creatorEmailRequired?: boolean\n /**\n * A use case with no approval that any signed-in person can open AND run.\n *\n * A request with a floor of zero runs when it is opened, so `canCreate`\n * asks the person to hold `requiredPermission`. This flag lifts that one\n * rule, for an action whose run needs no permission of the person. Set it\n * only when two things hold:\n *\n * - Something else bounds how often a person can ask.\n * - The service that runs the action authorizes the person again at its\n * source, so that nobody can change what is not theirs. The action can\n * touch an organization only when that check lets the person change it.\n *\n * `create-personal-organisation` and the two use cases of customer hardware\n * sizing set it. The two use cases of customer hardware sizing do the second\n * check in two ways:\n *\n * - Creating a plan, and unlinking a plan from one organization, read the\n * role of the person in that organization. Only an OWNER passes.\n * - Deleting a plan reads no role. Console checks that the person created\n * the plan. Accounts reads the organizations that the person is a member\n * of, in any role, and removes the plan from their lists.\n */\n openToEveryone?: boolean\n}\n"],
5
+ "mappings": "AA0BO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAmBO,MAAM,yBAAyB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF;AAUO,MAAM,6BAA6B;AAcnC,MAAM,yBAAyB;AAU/B,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,MAAM,wBAGT,EAAE,MAAM,aAAa,OAAO,OAAO;AAShC,MAAM,6BAA6B;AAUnC,MAAM,oBAAoB;AAS1B,MAAM,6BAA6B;AAoBnC,MAAM,6BAA6B;AAqHnC,MAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAoCO,MAAM,mCAAmC;AAUzC,MAAM,wBAAwB,CAAC,WAAW,OAAO,MAAM;AAgCvD,MAAM,wBAAwB;AA8B9B,SAAS,iBACd,UACoB;AACpB,MAAI,CAAC,YAAY,SAAS,SAAS,GAAG;AACpC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,GAAG,SAAS,OAAO,SAAS,KAAK;AACnD;AASO,SAAS,aAAa,KAAqB,KAAoB;AACpE,MAAI,IAAI,WAAW,aAAa;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,WAAW,IAAI,UAAU,aAAa,IAAI;AAChD,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AACA,SAAO,IAAI,QAAQ,IAAI,IAAI,KAAK,QAAQ,EAAE,QAAQ,IAAI;AACxD;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * What console-backend answers when an approved request changed the plan type
3
+ * of one cluster (`PATCH api/admin/approvals/clusters/:clusterId/plan-type`).
4
+ *
5
+ * control-backend keeps it as the result of the request, so the person who
6
+ * asked can read whether the cluster plan moved too.
7
+ */
8
+ export interface ChangeClusterPlanTypeResult {
9
+ clusterId: string;
10
+ orgId: string;
11
+ fromPlanTypeUuid: string;
12
+ toPlanTypeUuid: string;
13
+ /**
14
+ * True when the cluster plan moved to the active plan of the new plan type.
15
+ * False when the cluster already ran that plan, or when the cluster stayed
16
+ * on its plan type for a change of the packages and its plan is not
17
+ * migratable to the active plan of that type. The approval route refuses a
18
+ * NEW plan type whose active plan the cluster cannot run, so that case is
19
+ * a failed request and never a result.
20
+ */
21
+ clusterPlanUpdated: boolean;
22
+ numberOfAllocatedHwPackages: number;
23
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=ClusterPlanTypeChange.dto.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -58,6 +58,11 @@ export interface ExternalIdpConfig {
58
58
  scopes?: string[];
59
59
  usernameClaim?: string;
60
60
  clientIdClaim?: string;
61
+ /** True resolves a token that carries both `usernameClaim` and `clientIdClaim` as a
62
+ * user. Absent or false keeps the engine default, which resolves it as a client.
63
+ * Entra puts `azp` in user tokens too, so with `clientIdClaim: 'azp'` an Entra user
64
+ * signs in as a client unless this is true (camunda/camunda#39049). */
65
+ preferUsernameClaim?: boolean;
61
66
  /** Token audiences (`aud` values) the cluster accepts from this provider. A list,
62
67
  * and named for the CRD field (`oidcProviders[].audiences`,
63
68
  * camunda-operator#5961) — an `audience` string would be pruned on write.
@@ -9,6 +9,7 @@ export interface EarlyAccessFeatureDto {
9
9
  actionLabel?: string;
10
10
  actionUrl?: string;
11
11
  clickwrapId?: string;
12
+ featureFlagKey?: string;
12
13
  salesPlanTypeVisibilities?: EarlyAccessFeatureSalesplanConfigDto[];
13
14
  }
14
15
  export interface EarlyAccessFeatureSalesplanConfigDto {
@@ -44,6 +44,11 @@ export interface FeatureFlagDto {
44
44
  * from that day on. Absent for a flag that has no end, such as a permission.
45
45
  */
46
46
  expiresAt?: string;
47
+ /**
48
+ * The issue that tracks the flag, as an `http` or `https` URL. Absent on an
49
+ * older row. A write of a row without one must give one.
50
+ */
51
+ issueUrl?: string;
47
52
  createdAt: string;
48
53
  /**
49
54
  * The email of the person who made the row, or the name of the seed
@@ -81,11 +86,14 @@ export interface CreateFeatureFlagDto {
81
86
  owner: string;
82
87
  /** `YYYY-MM-DD`. Give it for a flag that gates a rollout. */
83
88
  expiresAt?: string;
89
+ /** Required for a new flag: the issue that tracks the flag. */
90
+ issueUrl: string;
84
91
  }
85
92
  type EditableFeatureFlagFields = Omit<CreateFeatureFlagDto, 'key'>;
86
93
  /**
87
94
  * What an edit sends: at least one field. A field that is absent stays as it
88
- * is. The route answers 400 to an empty body.
95
+ * is. The route answers 400 to an empty body, and to an edit of a row with no
96
+ * `issueUrl` that does not give one.
89
97
  */
90
98
  export type UpdateFeatureFlagDto = {
91
99
  [Field in keyof EditableFeatureFlagFields]: Pick<EditableFeatureFlagFields, Field> & Partial<EditableFeatureFlagFields>;
@@ -67,6 +67,17 @@ export interface OrganizationMetadata {
67
67
  * to look in the right workspace after the configured one changed.
68
68
  */
69
69
  workspaceId?: string;
70
+ /**
71
+ * True while the enterprise org has the `camunda-provided-llm` early access
72
+ * feature off, or a revoke of its key is not finished. Console then writes
73
+ * no connector secrets for this key, and a sweep removes the ones that
74
+ * exist.
75
+ *
76
+ * It does not prove that the upstream blocked the key. A block can still be
77
+ * pending in the operation queue (`litellm.blockKey`). Absent means not
78
+ * suspended.
79
+ */
80
+ suspended?: boolean;
70
81
  /**
71
82
  * The key that a provider migration replaced, kept until the delete of
72
83
  * that key is confirmed.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/dtos/Organization.dto.ts"],
4
- "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport type { OrganizationPermissions } from '../types/OrganizationPermission.type.js'\nimport type { SalesPlanType } from '../types/SalesPlanType.type.js'\nimport type { ClusterCreationDefaults } from './ClusterCreationDefaults.dto.js'\nimport type { OrganizationToSalesPlanDto } from './OrganizationToSalesPlan.dto.js'\n\nexport type LicensingModelType = 'legacy' | '2025'\n\nexport const VALID_LICENSING_MODELS: readonly LicensingModelType[] = [\n 'legacy',\n '2025',\n] as const\n\nexport const LICENSING_MODEL_DISPLAY_NAMES: Record<LicensingModelType, string> =\n {\n legacy: 'Legacy (PI, DI, TU)',\n '2025': '2025 (PI+Tenant)',\n } as const\n\n/**\n * Upstream that holds a managed LLM key.\n *\n * `@camunda-cloud/featureflags` owns the `LlmProvider` type and its rollout\n * rules. This package must not depend on that one, so the union is repeated\n * here. Keep the two in step.\n */\nexport type LlmProviderName = 'litellm' | 'openrouter'\n\nexport interface OrganizationMetadata {\n clickedRequestPaidPlan?: boolean\n /**\n * Default values for the settings of a new cluster of this organization\n * (#9732). They seed a cluster at creation and are never applied again.\n *\n * A field that is absent takes the value that the console backend writes.\n * The whole key is absent for almost every organization.\n */\n clusterDefaults?: ClusterCreationDefaults\n salesForceId?: string\n enterpriseDevOnly?: boolean\n awsOnly?: boolean\n licensingModel?: LicensingModelType\n autoJoin?: AutoJoinAssignment\n llm?: {\n tenantKey: string\n /**\n * Team that holds the key on LiteLLM.\n *\n * Optional, and every call site already treats it as such. Old LiteLLM\n * orgs got a key without a team. OpenRouter has no teams at all.\n */\n teamId?: string\n created: number\n /** Epoch ms of the last key rotation \u2014 set when the tenant key is regenerated. */\n updated?: number\n /**\n * Upstream that holds this key. Absent means `litellm`, because every key\n * from before the OpenRouter rollout is on LiteLLM.\n *\n * This value, and not the OpenRouter feature flag, decides where the\n * read, block, top-up and delete paths look for the key. A flag change\n * must never move a live key to a provider that does not hold it.\n */\n provider?: LlmProviderName\n /**\n * Handle for the key on OpenRouter, which addresses a key by hash and not\n * by its value. Absent for a LiteLLM key.\n *\n * Without it, an OpenRouter key can be found again only by a list request\n * that matches on the name of the key.\n */\n keyHash?: string\n /**\n * UUID of the OpenRouter workspace that holds this key. Absent for a\n * LiteLLM key, and for an OpenRouter key that lives in the default\n * workspace.\n *\n * A key stays in its workspace for its whole life, and a list request\n * covers one workspace. So the lost-hash recovery by name needs this value\n * to look in the right workspace after the configured one changed.\n */\n workspaceId?: string\n /**\n * The key that a provider migration replaced, kept until the delete of\n * that key is confirmed.\n *\n * The migration to OpenRouter writes the new key over `tenantKey` before\n * it deletes the old one. Without this record the old key stays alive on\n * the old upstream and nothing points at it. The migration removes this\n * field when the old upstream confirms the delete, and it uses the field\n * to find the orgs that a re-run must finish.\n *\n * Absent for every org that never moved upstream.\n */\n migratedFrom?: {\n /** Upstream that holds the replaced key. */\n provider: LlmProviderName\n /** The replaced key itself. It is a credential. Never log it. */\n tenantKey: string\n /** Team that held the replaced key on LiteLLM. */\n teamId?: string\n }\n }\n}\n\nexport interface AutoJoinAssignment {\n departments: Array<{ hibobId: string; roles: string[] }>\n divisions: Array<{ hibobId: string; roles: string[] }>\n subdivisions: Array<{ hibobId: string; roles: string[] }>\n}\nexport interface OrganizationDto {\n uuid: string\n name: string\n organizationToSalesPlan?: OrganizationToSalesPlanDto\n permissions?: OrganizationPermissions\n metadata?: OrganizationMetadata\n created: Date\n internal: boolean\n}\n\nexport interface OrgOwner {\n owner: string\n email: string\n first: string\n last: string\n}\n\nexport interface OrgWithNameAndSalesPlan {\n uuid: string\n name: string | undefined\n salesPlanName: string | undefined\n salesPlanUuid: string | undefined\n salesPlanType: SalesPlanType | undefined\n}\n"],
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 { OrganizationPermissions } from '../types/OrganizationPermission.type.js'\nimport type { SalesPlanType } from '../types/SalesPlanType.type.js'\nimport type { ClusterCreationDefaults } from './ClusterCreationDefaults.dto.js'\nimport type { OrganizationToSalesPlanDto } from './OrganizationToSalesPlan.dto.js'\n\nexport type LicensingModelType = 'legacy' | '2025'\n\nexport const VALID_LICENSING_MODELS: readonly LicensingModelType[] = [\n 'legacy',\n '2025',\n] as const\n\nexport const LICENSING_MODEL_DISPLAY_NAMES: Record<LicensingModelType, string> =\n {\n legacy: 'Legacy (PI, DI, TU)',\n '2025': '2025 (PI+Tenant)',\n } as const\n\n/**\n * Upstream that holds a managed LLM key.\n *\n * `@camunda-cloud/featureflags` owns the `LlmProvider` type and its rollout\n * rules. This package must not depend on that one, so the union is repeated\n * here. Keep the two in step.\n */\nexport type LlmProviderName = 'litellm' | 'openrouter'\n\nexport interface OrganizationMetadata {\n clickedRequestPaidPlan?: boolean\n /**\n * Default values for the settings of a new cluster of this organization\n * (#9732). They seed a cluster at creation and are never applied again.\n *\n * A field that is absent takes the value that the console backend writes.\n * The whole key is absent for almost every organization.\n */\n clusterDefaults?: ClusterCreationDefaults\n salesForceId?: string\n enterpriseDevOnly?: boolean\n awsOnly?: boolean\n licensingModel?: LicensingModelType\n autoJoin?: AutoJoinAssignment\n llm?: {\n tenantKey: string\n /**\n * Team that holds the key on LiteLLM.\n *\n * Optional, and every call site already treats it as such. Old LiteLLM\n * orgs got a key without a team. OpenRouter has no teams at all.\n */\n teamId?: string\n created: number\n /** Epoch ms of the last key rotation \u2014 set when the tenant key is regenerated. */\n updated?: number\n /**\n * Upstream that holds this key. Absent means `litellm`, because every key\n * from before the OpenRouter rollout is on LiteLLM.\n *\n * This value, and not the OpenRouter feature flag, decides where the\n * read, block, top-up and delete paths look for the key. A flag change\n * must never move a live key to a provider that does not hold it.\n */\n provider?: LlmProviderName\n /**\n * Handle for the key on OpenRouter, which addresses a key by hash and not\n * by its value. Absent for a LiteLLM key.\n *\n * Without it, an OpenRouter key can be found again only by a list request\n * that matches on the name of the key.\n */\n keyHash?: string\n /**\n * UUID of the OpenRouter workspace that holds this key. Absent for a\n * LiteLLM key, and for an OpenRouter key that lives in the default\n * workspace.\n *\n * A key stays in its workspace for its whole life, and a list request\n * covers one workspace. So the lost-hash recovery by name needs this value\n * to look in the right workspace after the configured one changed.\n */\n workspaceId?: string\n /**\n * True while the enterprise org has the `camunda-provided-llm` early access\n * feature off, or a revoke of its key is not finished. Console then writes\n * no connector secrets for this key, and a sweep removes the ones that\n * exist.\n *\n * It does not prove that the upstream blocked the key. A block can still be\n * pending in the operation queue (`litellm.blockKey`). Absent means not\n * suspended.\n */\n suspended?: boolean\n /**\n * The key that a provider migration replaced, kept until the delete of\n * that key is confirmed.\n *\n * The migration to OpenRouter writes the new key over `tenantKey` before\n * it deletes the old one. Without this record the old key stays alive on\n * the old upstream and nothing points at it. The migration removes this\n * field when the old upstream confirms the delete, and it uses the field\n * to find the orgs that a re-run must finish.\n *\n * Absent for every org that never moved upstream.\n */\n migratedFrom?: {\n /** Upstream that holds the replaced key. */\n provider: LlmProviderName\n /** The replaced key itself. It is a credential. Never log it. */\n tenantKey: string\n /** Team that held the replaced key on LiteLLM. */\n teamId?: string\n }\n }\n}\n\nexport interface AutoJoinAssignment {\n departments: Array<{ hibobId: string; roles: string[] }>\n divisions: Array<{ hibobId: string; roles: string[] }>\n subdivisions: Array<{ hibobId: string; roles: string[] }>\n}\nexport interface OrganizationDto {\n uuid: string\n name: string\n organizationToSalesPlan?: OrganizationToSalesPlanDto\n permissions?: OrganizationPermissions\n metadata?: OrganizationMetadata\n created: Date\n internal: boolean\n}\n\nexport interface OrgOwner {\n owner: string\n email: string\n first: string\n last: string\n}\n\nexport interface OrgWithNameAndSalesPlan {\n uuid: string\n name: string | undefined\n salesPlanName: string | undefined\n salesPlanUuid: string | undefined\n salesPlanType: SalesPlanType | undefined\n}\n"],
5
5
  "mappings": "AAaO,MAAM,yBAAwD;AAAA,EACnE;AAAA,EACA;AACF;AAEO,MAAM,gCACX;AAAA,EACE,QAAQ;AAAA,EACR,QAAQ;AACV;",
6
6
  "names": []
7
7
  }
@@ -59,6 +59,13 @@ export interface Limits extends ClusterLimits {
59
59
  * the Stable channel with classification 'orgUnlocked'.
60
60
  */
61
61
  generationWhiteList?: string;
62
+ /**
63
+ * Comma-separated list of cluster plan UUIDs that are unlocked for this org.
64
+ * On create, the caller can name one of them in `clusterPlanId`. The cluster
65
+ * then gets that plan instead of the active plan of its plan type. Read
66
+ * only when the flag `shared.unlockedClusterPlans` is on.
67
+ */
68
+ clusterPlanWhiteList?: string;
62
69
  /**
63
70
  * When true, users with only the Developer role cannot create clusters in this org.
64
71
  * Users who also hold Member, Admin, Owner, or Operations Engineer are unaffected.
@@ -142,3 +149,14 @@ export declare function unusedSalesPlanLimits(limits: Limits, stage?: string): {
142
149
  description: string;
143
150
  isClusterBasedLimit: boolean;
144
151
  }[];
152
+ /**
153
+ * Returns the cluster plan uuids of the `clusterPlanWhiteList` limit. A value
154
+ * that is not a string gives no ids, because an old write path did not check
155
+ * the type.
156
+ */
157
+ export declare function clusterPlanWhiteListIds(limits: Limits | undefined): string[];
158
+ /**
159
+ * Says if the feature flag state offers the limit key. The flag
160
+ * `shared.unlockedClusterPlans` hides `clusterPlanWhiteList`.
161
+ */
162
+ export declare function isLimitKeyEnabled(limitKey: string, unlockedClusterPlans: boolean): boolean;
@@ -147,6 +147,12 @@ function allSalesPlanLimits() {
147
147
  description: "unlocked generation UUIDs - comma separated. These generations will be available for cluster creation regardless of channel membership. Attached to the Stable channel.",
148
148
  isClusterBasedLimit: false
149
149
  },
150
+ {
151
+ key: "clusterPlanWhiteList",
152
+ type: "string",
153
+ description: "unlocked cluster plan UUIDs - comma separated. On create, a cluster can use one of these plans instead of the active plan of its plan type. The reservation still counts against the plan type. The limit has an effect only when the flag shared.unlockedClusterPlans is on.",
154
+ isClusterBasedLimit: false
155
+ },
150
156
  {
151
157
  key: "developerNoClusterCreate",
152
158
  type: "boolean",
@@ -177,6 +183,16 @@ function unusedSalesPlanLimits(limits, stage) {
177
183
  (current) => !(Object.keys(limits).includes(current.key) && limits[current.key] !== null) && (stage === void 0 || isLimitOfferedOnStage(current, stage))
178
184
  );
179
185
  }
186
+ function clusterPlanWhiteListIds(limits) {
187
+ const value = limits?.clusterPlanWhiteList;
188
+ if (typeof value !== "string") {
189
+ return [];
190
+ }
191
+ return value.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
192
+ }
193
+ function isLimitKeyEnabled(limitKey, unlockedClusterPlans) {
194
+ return limitKey !== "clusterPlanWhiteList" || unlockedClusterPlans;
195
+ }
180
196
  export {
181
197
  MAX_CLUSTER_CLIENTS_DEFAULT,
182
198
  MAX_CLUSTER_CLIENTS_DEV,
@@ -185,7 +201,9 @@ export {
185
201
  RETENTION_LIMIT_MINIMUM,
186
202
  allClusterBasedLimits,
187
203
  allSalesPlanLimits,
204
+ clusterPlanWhiteListIds,
188
205
  deprecatedLimit,
206
+ isLimitKeyEnabled,
189
207
  isLimitOfferedOnStage,
190
208
  unusedClusterBasedLimits,
191
209
  unusedSalesPlanLimits