@camunda/ccma-shared-types 10.7.1 → 10.9.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 (54) hide show
  1. package/dist/_tests/approvals/approval-rules.spec.d.ts +1 -0
  2. package/dist/_tests/approvals/llm-budget-top-up.spec.d.ts +1 -0
  3. package/dist/_tests/types/SalesPlanType.type.spec.d.ts +1 -0
  4. package/dist/agent-tokens/agent-tokens.d.ts +62 -3
  5. package/dist/agent-tokens/agent-tokens.js +8 -0
  6. package/dist/agent-tokens/agent-tokens.js.map +2 -2
  7. package/dist/approvals/approval-rules.d.ts +172 -0
  8. package/dist/approvals/approval-rules.js +71 -0
  9. package/dist/approvals/approval-rules.js.map +7 -0
  10. package/dist/approvals/cluster-bulk-operation.d.ts +5 -20
  11. package/dist/approvals/cluster-bulk-operation.js +3 -5
  12. package/dist/approvals/cluster-bulk-operation.js.map +2 -2
  13. package/dist/approvals/delete-cluster-corpses.d.ts +0 -15
  14. package/dist/approvals/delete-cluster-corpses.js +1 -7
  15. package/dist/approvals/delete-cluster-corpses.js.map +2 -2
  16. package/dist/approvals/edit-generation.d.ts +0 -8
  17. package/dist/approvals/edit-generation.js +0 -2
  18. package/dist/approvals/edit-generation.js.map +2 -2
  19. package/dist/approvals/force-delete-cluster-plan-type.d.ts +0 -18
  20. package/dist/approvals/force-delete-cluster-plan-type.js +1 -7
  21. package/dist/approvals/force-delete-cluster-plan-type.js.map +2 -2
  22. package/dist/approvals/policy.d.ts +94 -7
  23. package/dist/approvals/policy.js +125 -6
  24. package/dist/approvals/policy.js.map +2 -2
  25. package/dist/approvals/use-cases.const.d.ts +36 -6
  26. package/dist/approvals/use-cases.const.js +48 -16
  27. package/dist/approvals/use-cases.const.js.map +2 -2
  28. package/dist/constants/direct-route-lock.const.d.ts +36 -0
  29. package/dist/constants/direct-route-lock.const.js +21 -0
  30. package/dist/constants/direct-route-lock.const.js.map +7 -0
  31. package/dist/constants/misc.const.d.ts +38 -42
  32. package/dist/constants/misc.const.js +27 -28
  33. package/dist/constants/misc.const.js.map +2 -2
  34. package/dist/dtos/ApprovalJob.dto.d.ts +69 -5
  35. package/dist/dtos/ApprovalJob.dto.js +2 -0
  36. package/dist/dtos/ApprovalJob.dto.js.map +2 -2
  37. package/dist/dtos/ApprovalSummaryNames.dto.d.ts +37 -0
  38. package/dist/dtos/ApprovalSummaryNames.dto.js +1 -0
  39. package/dist/dtos/ApprovalSummaryNames.dto.js.map +7 -0
  40. package/dist/index.d.ts +15 -11
  41. package/dist/index.js +59 -27
  42. package/dist/index.js.map +2 -2
  43. package/dist/permissions/combo.js +3 -0
  44. package/dist/permissions/combo.js.map +2 -2
  45. package/dist/permissions/console.permissions.d.ts +17 -0
  46. package/dist/permissions/console.permissions.js +15 -1
  47. package/dist/permissions/console.permissions.js.map +2 -2
  48. package/dist/types/SalesPlanType.type.d.ts +5 -0
  49. package/dist/types/SalesPlanType.type.js +7 -0
  50. package/dist/types/SalesPlanType.type.js.map +2 -2
  51. package/dist/utils/user.utils.js +1 -1
  52. package/dist/utils/user.utils.js.map +2 -2
  53. package/dist/utils/user.utils.spec.d.ts +1 -0
  54. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -13,6 +13,21 @@ export declare const AGENT_CLAIM = "https://camunda.com/agent";
13
13
  export declare const AGENT_CLIENT_ID_PREFIX = "ccma_";
14
14
  /** AGENT_TOKENS_FEATURE_FLAG closes create and rotate while it is off. */
15
15
  export declare const AGENT_TOKENS_FEATURE_FLAG = "control.agentTokens";
16
+ /**
17
+ * AGENT_SLACK_GRANTS_FEATURE_FLAG closes the create and the extend of a
18
+ * binding, and the create of a grant, while it is off. The redeem stays open:
19
+ * a code made before the flag went off works until it expires (`AGENT_GRANT_TTL_MS`).
20
+ */
21
+ export declare const AGENT_SLACK_GRANTS_FEATURE_FLAG = "control.agentSlackGrants";
22
+ /**
23
+ * The form of a Slack channel ID: public (`C`) or private (`G`) channel.
24
+ * A binding stores the ID, never the name.
25
+ */
26
+ export declare const AGENT_SLACK_CHANNEL_PATTERN: RegExp;
27
+ /** AGENT_GRANT_TTL_MS is the time in which a grant code can be redeemed. */
28
+ export declare const AGENT_GRANT_TTL_MS = 120000;
29
+ /** AGENT_GRANTS_PER_HOUR_MAX is the limit of new grants for one binding. */
30
+ export declare const AGENT_GRANTS_PER_HOUR_MAX = 10;
16
31
  /** AGENT_TOKEN_EXPIRY_DAYS lists the lifetimes that a creator can choose. */
17
32
  export declare const AGENT_TOKEN_EXPIRY_DAYS: readonly [7, 30, 90];
18
33
  export type AgentTokenExpiryDays = (typeof AGENT_TOKEN_EXPIRY_DAYS)[number];
@@ -20,6 +35,11 @@ export type AgentTokenExpiryDays = (typeof AGENT_TOKEN_EXPIRY_DAYS)[number];
20
35
  export declare const AGENT_TOKEN_NAME_MAX = 64;
21
36
  /** AGENT_TOKENS_ACTIVE_MAX is the limit of active clients for one creator. */
22
37
  export declare const AGENT_TOKENS_ACTIVE_MAX = 10;
38
+ /**
39
+ * `secret` is a client with an id and a secret. `slack` is a binding to one
40
+ * Slack channel: it has no secret, and a grant gives it a token.
41
+ */
42
+ export type AgentTokenKind = 'secret' | 'slack';
23
43
  /** Phase 1 has read access only. Phase 2 adds `write`. */
24
44
  export type AgentAccess = 'read';
25
45
  /** The value of `AGENT_CLAIM` in a verified agent token. */
@@ -39,6 +59,9 @@ export interface AgentToken {
39
59
  clientId: string;
40
60
  name: string;
41
61
  access: AgentAccess;
62
+ kind: AgentTokenKind;
63
+ /** The channel ID of a binding. It is absent when `kind` is `secret`. */
64
+ slackChannelId?: string;
42
65
  status: AgentTokenStatus;
43
66
  createdAt: string;
44
67
  expiresAt: string;
@@ -50,7 +73,8 @@ export interface AgentToken {
50
73
  * that holds `clientSecret`, and the backend cannot show the secret again.
51
74
  */
52
75
  export interface AgentTokenCreated extends AgentToken {
53
- clientSecret: string;
76
+ /** Absent for a binding, which has no secret. */
77
+ clientSecret?: string;
54
78
  }
55
79
  /** One agent client, as the admin list shows it. */
56
80
  export interface AgentTokenAdmin extends AgentToken {
@@ -64,6 +88,41 @@ export interface AgentTokenAdmin extends AgentToken {
64
88
  export interface CreateAgentTokenDto {
65
89
  name: string;
66
90
  expiresInDays: AgentTokenExpiryDays;
91
+ /** Set it to make a binding to this channel. Then there is no secret. */
92
+ slackChannelId?: string;
93
+ }
94
+ /** The body of `POST /api/agent-tokens/:uuid/extend`. */
95
+ export interface ExtendAgentTokenDto {
96
+ expiresInDays: AgentTokenExpiryDays;
97
+ }
98
+ /** The body of `POST /internal/agent-grants`. CloudBot sends it. */
99
+ export interface CreateAgentGrantDto {
100
+ slackChannelId: string;
101
+ slackThreadTs: string;
102
+ requestedBySlackUserId: string;
103
+ requestedByEmail: string;
104
+ /** The S256 challenge of the PKCE verifier that only ccmacli knows. */
105
+ codeChallenge: string;
106
+ }
107
+ /** The answer of `POST /internal/agent-grants`. */
108
+ export interface AgentGrantCreated {
109
+ code: string;
110
+ expiresAt: string;
111
+ }
112
+ /** Why control-backend refuses a grant. CloudBot words the reply from it. */
113
+ export type AgentGrantRefusalReason = 'no_binding' | 'expired' | 'revoked' | 'inactive' | 'requester_unknown' | 'rate_limited' | 'thread_busy' | 'feature_off';
114
+ /** The body of the 409 answer of `POST /internal/agent-grants`. */
115
+ export interface AgentGrantRefusal {
116
+ code: string;
117
+ message: string;
118
+ reason: AgentGrantRefusalReason;
119
+ expiredAt?: string;
120
+ ownerEmail?: string;
121
+ }
122
+ /** The body of `POST /internal/agent-grants/redeem`. token-issuer sends it. */
123
+ export interface RedeemAgentGrantDto {
124
+ code: string;
125
+ codeVerifier: string;
67
126
  }
68
127
  /** The answer of `POST /internal/agent-clients/verify`. */
69
128
  export interface AgentVerifyResponse {
@@ -75,8 +134,8 @@ export interface AgentVerifyResponse {
75
134
  * The answer of `GET /internal/agent-clients/:uuid/status`.
76
135
  *
77
136
  * `permissions` is `AGENT_READ_PERMISSIONS` of all backends, intersected with
78
- * the permissions of the creator at create time. It is empty when `active` is
79
- * false.
137
+ * the permissions of the creator at the create or the last extend. It is empty
138
+ * when `active` is false.
80
139
  */
81
140
  export interface AgentStatusResponse {
82
141
  active: boolean;
@@ -3,6 +3,10 @@ import { ConsolePermissions } from "../permissions/console.permissions.js";
3
3
  const AGENT_CLAIM = "https://camunda.com/agent";
4
4
  const AGENT_CLIENT_ID_PREFIX = "ccma_";
5
5
  const AGENT_TOKENS_FEATURE_FLAG = "control.agentTokens";
6
+ const AGENT_SLACK_GRANTS_FEATURE_FLAG = "control.agentSlackGrants";
7
+ const AGENT_SLACK_CHANNEL_PATTERN = /^[CG][A-Z0-9]{8,}$/;
8
+ const AGENT_GRANT_TTL_MS = 12e4;
9
+ const AGENT_GRANTS_PER_HOUR_MAX = 10;
6
10
  const AGENT_TOKEN_EXPIRY_DAYS = [7, 30, 90];
7
11
  const AGENT_TOKEN_NAME_MAX = 64;
8
12
  const AGENT_TOKENS_ACTIVE_MAX = 10;
@@ -51,7 +55,11 @@ export {
51
55
  AGENT_CLAIM,
52
56
  AGENT_CLIENT_ID_PREFIX,
53
57
  AGENT_ERROR,
58
+ AGENT_GRANTS_PER_HOUR_MAX,
59
+ AGENT_GRANT_TTL_MS,
54
60
  AGENT_READ_PERMISSIONS,
61
+ AGENT_SLACK_CHANNEL_PATTERN,
62
+ AGENT_SLACK_GRANTS_FEATURE_FLAG,
55
63
  AGENT_TOKENS_ACTIVE_MAX,
56
64
  AGENT_TOKENS_FEATURE_FLAG,
57
65
  AGENT_TOKEN_EXPIRY_DAYS,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/agent-tokens/agent-tokens.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 { AccountsPermissions } from '../permissions/accounts.permissions.js'\nimport { ConsolePermissions } from '../permissions/console.permissions.js'\n\n/**\n * The agent tokens of ccmacli: machine credentials that a Camundi creates in\n * Camunda Control for an agent, with read access only.\n *\n * control-backend stores the clients. token-issuer exchanges the credentials\n * for an Auth0 token of the agent audience. control-backend, console-backend\n * and accounts-backend accept that token on their read routes. Every type and\n * rule that two of these services share is in this file.\n */\n\n/** AGENT_CLAIM is the claim of an agent token that names the client. */\nexport const AGENT_CLAIM = 'https://camunda.com/agent'\n\n/** AGENT_CLIENT_ID_PREFIX starts every client id of an agent client. */\nexport const AGENT_CLIENT_ID_PREFIX = 'ccma_'\n\n/** AGENT_TOKENS_FEATURE_FLAG closes create and rotate while it is off. */\nexport const AGENT_TOKENS_FEATURE_FLAG = 'control.agentTokens'\n\n/** AGENT_TOKEN_EXPIRY_DAYS lists the lifetimes that a creator can choose. */\nexport const AGENT_TOKEN_EXPIRY_DAYS = [7, 30, 90] as const\n\nexport type AgentTokenExpiryDays = (typeof AGENT_TOKEN_EXPIRY_DAYS)[number]\n\n/** AGENT_TOKEN_NAME_MAX is the length limit of a name, after the trim. */\nexport const AGENT_TOKEN_NAME_MAX = 64\n\n/** AGENT_TOKENS_ACTIVE_MAX is the limit of active clients for one creator. */\nexport const AGENT_TOKENS_ACTIVE_MAX = 10\n\n/** Phase 1 has read access only. Phase 2 adds `write`. */\nexport type AgentAccess = 'read'\n\n/** The value of `AGENT_CLAIM` in a verified agent token. */\nexport interface AgentClaim {\n uuid: string\n access: AgentAccess\n /** The `sub` of the Camundi who created the client. */\n createdByUserId: string\n}\n\n/** `inactive` means that the creator is no longer a Camundi. */\nexport type AgentTokenStatus = 'active' | 'expired' | 'revoked' | 'inactive'\n\n/** AGENT_TOKEN_STATUSES lists every value of `AgentTokenStatus`. */\nexport const AGENT_TOKEN_STATUSES = [\n 'active',\n 'expired',\n 'revoked',\n 'inactive',\n] as const satisfies readonly AgentTokenStatus[]\n\n/** One agent client, as its creator sees it. It never holds the secret. */\nexport interface AgentToken {\n uuid: string\n clientId: string\n name: string\n access: AgentAccess\n status: AgentTokenStatus\n createdAt: string\n expiresAt: string\n lastUsedAt?: string\n revokedAt?: string\n}\n\n/**\n * AgentTokenCreated is the answer of create and rotate. It is the only answer\n * that holds `clientSecret`, and the backend cannot show the secret again.\n */\nexport interface AgentTokenCreated extends AgentToken {\n clientSecret: string\n}\n\n/** One agent client, as the admin list shows it. */\nexport interface AgentTokenAdmin extends AgentToken {\n createdBy: { userId: string; email: string }\n revokedByUserId?: string\n}\n\n/** The body of `POST /api/agent-tokens`. */\nexport interface CreateAgentTokenDto {\n name: string\n expiresInDays: AgentTokenExpiryDays\n}\n\n/** The answer of `POST /internal/agent-clients/verify`. */\nexport interface AgentVerifyResponse {\n uuid: string\n access: AgentAccess\n createdByUserId: string\n}\n\n/**\n * The answer of `GET /internal/agent-clients/:uuid/status`.\n *\n * `permissions` is `AGENT_READ_PERMISSIONS` of all backends, intersected with\n * the permissions of the creator at create time. It is empty when `active` is\n * false.\n */\nexport interface AgentStatusResponse {\n active: boolean\n permissions: string[]\n}\n\n/**\n * AGENT_READ_PERMISSIONS is the most that an agent token can do in each\n * backend. A backend intersects its set with the `permissions` of the status\n * answer, so an agent never holds a permission that its creator did not hold.\n *\n * `crtp` is a publish permission. It is here for one read only, the\n * release-train sanity check. The agent allowlist of console-backend, which a\n * later change of epic #10804 adds, must be the bound. Never add a route to\n * that allowlist that `crtp` guards for a write.\n */\nexport const AGENT_READ_PERMISSIONS: Readonly<{\n control: readonly string[]\n console: readonly string[]\n accounts: readonly string[]\n}> = {\n control: [AccountsPermissions.READ_ALL],\n console: [\n ConsolePermissions.CHANNELS_AND_GENERATIONS_ADMIN_SEE,\n ConsolePermissions.CLUSTERS_READALLCLUSTERINFO,\n ConsolePermissions.CLUSTER_PLAN_SEE_ALL,\n ConsolePermissions.RELEASE_TRAIN_PUBLISH,\n ],\n accounts: [AccountsPermissions.READ_ALL],\n}\n\n/** agentReadPermissionUnion is the union of the three backend sets. */\nexport function agentReadPermissionUnion(): string[] {\n return [\n ...new Set([\n ...AGENT_READ_PERMISSIONS.control,\n ...AGENT_READ_PERMISSIONS.console,\n ...AGENT_READ_PERMISSIONS.accounts,\n ]),\n ]\n}\n\n/** The codes of the two denials that only an agent token can get. */\nexport const AGENT_ERROR = {\n readOnly: 'auth.agent_read_only',\n inactive: 'auth.agent_inactive',\n} as const\n\nconst UUID_SHAPE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/**\n * parseAgentClaim reads `AGENT_CLAIM` from the payload of a VERIFIED token.\n * It returns `undefined` when the claim is absent or has a wrong shape, and\n * the caller must then refuse the token.\n */\nexport function parseAgentClaim(\n payload: Record<string, unknown>,\n): AgentClaim | undefined {\n const claim = payload[AGENT_CLAIM]\n if (typeof claim !== 'object' || claim === null || Array.isArray(claim)) {\n return undefined\n }\n const { uuid, access, createdByUserId } = claim as Record<string, unknown>\n if (\n typeof uuid !== 'string' ||\n !UUID_SHAPE.test(uuid) ||\n access !== 'read' ||\n typeof createdByUserId !== 'string' ||\n createdByUserId.length === 0\n ) {\n return undefined\n }\n return { uuid, access, createdByUserId }\n}\n"],
5
- "mappings": "AAMA,SAAS,2BAA2B;AACpC,SAAS,0BAA0B;AAa5B,MAAM,cAAc;AAGpB,MAAM,yBAAyB;AAG/B,MAAM,4BAA4B;AAGlC,MAAM,0BAA0B,CAAC,GAAG,IAAI,EAAE;AAK1C,MAAM,uBAAuB;AAG7B,MAAM,0BAA0B;AAiBhC,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAgEO,MAAM,yBAIR;AAAA,EACH,SAAS,CAAC,oBAAoB,QAAQ;AAAA,EACtC,SAAS;AAAA,IACP,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,EACrB;AAAA,EACA,UAAU,CAAC,oBAAoB,QAAQ;AACzC;AAGO,SAAS,2BAAqC;AACnD,SAAO;AAAA,IACL,GAAG,oBAAI,IAAI;AAAA,MACT,GAAG,uBAAuB;AAAA,MAC1B,GAAG,uBAAuB;AAAA,MAC1B,GAAG,uBAAuB;AAAA,IAC5B,CAAC;AAAA,EACH;AACF;AAGO,MAAM,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,UAAU;AACZ;AAEA,MAAM,aACJ;AAOK,SAAS,gBACd,SACwB;AACxB,QAAM,QAAQ,QAAQ,WAAW;AACjC,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,WAAO;AAAA,EACT;AACA,QAAM,EAAE,MAAM,QAAQ,gBAAgB,IAAI;AAC1C,MACE,OAAO,SAAS,YAChB,CAAC,WAAW,KAAK,IAAI,KACrB,WAAW,UACX,OAAO,oBAAoB,YAC3B,gBAAgB,WAAW,GAC3B;AACA,WAAO;AAAA,EACT;AACA,SAAO,EAAE,MAAM,QAAQ,gBAAgB;AACzC;",
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 { AccountsPermissions } from '../permissions/accounts.permissions.js'\nimport { ConsolePermissions } from '../permissions/console.permissions.js'\n\n/**\n * The agent tokens of ccmacli: machine credentials that a Camundi creates in\n * Camunda Control for an agent, with read access only.\n *\n * control-backend stores the clients. token-issuer exchanges the credentials\n * for an Auth0 token of the agent audience. control-backend, console-backend\n * and accounts-backend accept that token on their read routes. Every type and\n * rule that two of these services share is in this file.\n */\n\n/** AGENT_CLAIM is the claim of an agent token that names the client. */\nexport const AGENT_CLAIM = 'https://camunda.com/agent'\n\n/** AGENT_CLIENT_ID_PREFIX starts every client id of an agent client. */\nexport const AGENT_CLIENT_ID_PREFIX = 'ccma_'\n\n/** AGENT_TOKENS_FEATURE_FLAG closes create and rotate while it is off. */\nexport const AGENT_TOKENS_FEATURE_FLAG = 'control.agentTokens'\n\n/**\n * AGENT_SLACK_GRANTS_FEATURE_FLAG closes the create and the extend of a\n * binding, and the create of a grant, while it is off. The redeem stays open:\n * a code made before the flag went off works until it expires (`AGENT_GRANT_TTL_MS`).\n */\nexport const AGENT_SLACK_GRANTS_FEATURE_FLAG = 'control.agentSlackGrants'\n\n/**\n * The form of a Slack channel ID: public (`C`) or private (`G`) channel.\n * A binding stores the ID, never the name.\n */\nexport const AGENT_SLACK_CHANNEL_PATTERN = /^[CG][A-Z0-9]{8,}$/\n\n/** AGENT_GRANT_TTL_MS is the time in which a grant code can be redeemed. */\nexport const AGENT_GRANT_TTL_MS = 120_000\n\n/** AGENT_GRANTS_PER_HOUR_MAX is the limit of new grants for one binding. */\nexport const AGENT_GRANTS_PER_HOUR_MAX = 10\n\n/** AGENT_TOKEN_EXPIRY_DAYS lists the lifetimes that a creator can choose. */\nexport const AGENT_TOKEN_EXPIRY_DAYS = [7, 30, 90] as const\n\nexport type AgentTokenExpiryDays = (typeof AGENT_TOKEN_EXPIRY_DAYS)[number]\n\n/** AGENT_TOKEN_NAME_MAX is the length limit of a name, after the trim. */\nexport const AGENT_TOKEN_NAME_MAX = 64\n\n/** AGENT_TOKENS_ACTIVE_MAX is the limit of active clients for one creator. */\nexport const AGENT_TOKENS_ACTIVE_MAX = 10\n\n/**\n * `secret` is a client with an id and a secret. `slack` is a binding to one\n * Slack channel: it has no secret, and a grant gives it a token.\n */\nexport type AgentTokenKind = 'secret' | 'slack'\n\n/** Phase 1 has read access only. Phase 2 adds `write`. */\nexport type AgentAccess = 'read'\n\n/** The value of `AGENT_CLAIM` in a verified agent token. */\nexport interface AgentClaim {\n uuid: string\n access: AgentAccess\n /** The `sub` of the Camundi who created the client. */\n createdByUserId: string\n}\n\n/** `inactive` means that the creator is no longer a Camundi. */\nexport type AgentTokenStatus = 'active' | 'expired' | 'revoked' | 'inactive'\n\n/** AGENT_TOKEN_STATUSES lists every value of `AgentTokenStatus`. */\nexport const AGENT_TOKEN_STATUSES = [\n 'active',\n 'expired',\n 'revoked',\n 'inactive',\n] as const satisfies readonly AgentTokenStatus[]\n\n/** One agent client, as its creator sees it. It never holds the secret. */\nexport interface AgentToken {\n uuid: string\n clientId: string\n name: string\n access: AgentAccess\n kind: AgentTokenKind\n /** The channel ID of a binding. It is absent when `kind` is `secret`. */\n slackChannelId?: string\n status: AgentTokenStatus\n createdAt: string\n expiresAt: string\n lastUsedAt?: string\n revokedAt?: string\n}\n\n/**\n * AgentTokenCreated is the answer of create and rotate. It is the only answer\n * that holds `clientSecret`, and the backend cannot show the secret again.\n */\nexport interface AgentTokenCreated extends AgentToken {\n /** Absent for a binding, which has no secret. */\n clientSecret?: string\n}\n\n/** One agent client, as the admin list shows it. */\nexport interface AgentTokenAdmin extends AgentToken {\n createdBy: { userId: string; email: string }\n revokedByUserId?: string\n}\n\n/** The body of `POST /api/agent-tokens`. */\nexport interface CreateAgentTokenDto {\n name: string\n expiresInDays: AgentTokenExpiryDays\n /** Set it to make a binding to this channel. Then there is no secret. */\n slackChannelId?: string\n}\n\n/** The body of `POST /api/agent-tokens/:uuid/extend`. */\nexport interface ExtendAgentTokenDto {\n expiresInDays: AgentTokenExpiryDays\n}\n\n/** The body of `POST /internal/agent-grants`. CloudBot sends it. */\nexport interface CreateAgentGrantDto {\n slackChannelId: string\n slackThreadTs: string\n requestedBySlackUserId: string\n requestedByEmail: string\n /** The S256 challenge of the PKCE verifier that only ccmacli knows. */\n codeChallenge: string\n}\n\n/** The answer of `POST /internal/agent-grants`. */\nexport interface AgentGrantCreated {\n code: string\n expiresAt: string\n}\n\n/** Why control-backend refuses a grant. CloudBot words the reply from it. */\nexport type AgentGrantRefusalReason =\n | 'no_binding'\n | 'expired'\n | 'revoked'\n | 'inactive'\n | 'requester_unknown'\n | 'rate_limited'\n | 'thread_busy'\n | 'feature_off'\n\n/** The body of the 409 answer of `POST /internal/agent-grants`. */\nexport interface AgentGrantRefusal {\n code: string\n message: string\n reason: AgentGrantRefusalReason\n expiredAt?: string\n ownerEmail?: string\n}\n\n/** The body of `POST /internal/agent-grants/redeem`. token-issuer sends it. */\nexport interface RedeemAgentGrantDto {\n code: string\n codeVerifier: string\n}\n\n/** The answer of `POST /internal/agent-clients/verify`. */\nexport interface AgentVerifyResponse {\n uuid: string\n access: AgentAccess\n createdByUserId: string\n}\n\n/**\n * The answer of `GET /internal/agent-clients/:uuid/status`.\n *\n * `permissions` is `AGENT_READ_PERMISSIONS` of all backends, intersected with\n * the permissions of the creator at the create or the last extend. It is empty\n * when `active` is false.\n */\nexport interface AgentStatusResponse {\n active: boolean\n permissions: string[]\n}\n\n/**\n * AGENT_READ_PERMISSIONS is the most that an agent token can do in each\n * backend. A backend intersects its set with the `permissions` of the status\n * answer, so an agent never holds a permission that its creator did not hold.\n *\n * `crtp` is a publish permission. It is here for one read only, the\n * release-train sanity check. The agent allowlist of console-backend, which a\n * later change of epic #10804 adds, must be the bound. Never add a route to\n * that allowlist that `crtp` guards for a write.\n */\nexport const AGENT_READ_PERMISSIONS: Readonly<{\n control: readonly string[]\n console: readonly string[]\n accounts: readonly string[]\n}> = {\n control: [AccountsPermissions.READ_ALL],\n console: [\n ConsolePermissions.CHANNELS_AND_GENERATIONS_ADMIN_SEE,\n ConsolePermissions.CLUSTERS_READALLCLUSTERINFO,\n ConsolePermissions.CLUSTER_PLAN_SEE_ALL,\n ConsolePermissions.RELEASE_TRAIN_PUBLISH,\n ],\n accounts: [AccountsPermissions.READ_ALL],\n}\n\n/** agentReadPermissionUnion is the union of the three backend sets. */\nexport function agentReadPermissionUnion(): string[] {\n return [\n ...new Set([\n ...AGENT_READ_PERMISSIONS.control,\n ...AGENT_READ_PERMISSIONS.console,\n ...AGENT_READ_PERMISSIONS.accounts,\n ]),\n ]\n}\n\n/** The codes of the two denials that only an agent token can get. */\nexport const AGENT_ERROR = {\n readOnly: 'auth.agent_read_only',\n inactive: 'auth.agent_inactive',\n} as const\n\nconst UUID_SHAPE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/**\n * parseAgentClaim reads `AGENT_CLAIM` from the payload of a VERIFIED token.\n * It returns `undefined` when the claim is absent or has a wrong shape, and\n * the caller must then refuse the token.\n */\nexport function parseAgentClaim(\n payload: Record<string, unknown>,\n): AgentClaim | undefined {\n const claim = payload[AGENT_CLAIM]\n if (typeof claim !== 'object' || claim === null || Array.isArray(claim)) {\n return undefined\n }\n const { uuid, access, createdByUserId } = claim as Record<string, unknown>\n if (\n typeof uuid !== 'string' ||\n !UUID_SHAPE.test(uuid) ||\n access !== 'read' ||\n typeof createdByUserId !== 'string' ||\n createdByUserId.length === 0\n ) {\n return undefined\n }\n return { uuid, access, createdByUserId }\n}\n"],
5
+ "mappings": "AAMA,SAAS,2BAA2B;AACpC,SAAS,0BAA0B;AAa5B,MAAM,cAAc;AAGpB,MAAM,yBAAyB;AAG/B,MAAM,4BAA4B;AAOlC,MAAM,kCAAkC;AAMxC,MAAM,8BAA8B;AAGpC,MAAM,qBAAqB;AAG3B,MAAM,4BAA4B;AAGlC,MAAM,0BAA0B,CAAC,GAAG,IAAI,EAAE;AAK1C,MAAM,uBAAuB;AAG7B,MAAM,0BAA0B;AAuBhC,MAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAqHO,MAAM,yBAIR;AAAA,EACH,SAAS,CAAC,oBAAoB,QAAQ;AAAA,EACtC,SAAS;AAAA,IACP,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,EACrB;AAAA,EACA,UAAU,CAAC,oBAAoB,QAAQ;AACzC;AAGO,SAAS,2BAAqC;AACnD,SAAO;AAAA,IACL,GAAG,oBAAI,IAAI;AAAA,MACT,GAAG,uBAAuB;AAAA,MAC1B,GAAG,uBAAuB;AAAA,MAC1B,GAAG,uBAAuB;AAAA,IAC5B,CAAC;AAAA,EACH;AACF;AAGO,MAAM,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,UAAU;AACZ;AAEA,MAAM,aACJ;AAOK,SAAS,gBACd,SACwB;AACxB,QAAM,QAAQ,QAAQ,WAAW;AACjC,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,WAAO;AAAA,EACT;AACA,QAAM,EAAE,MAAM,QAAQ,gBAAgB,IAAI;AAC1C,MACE,OAAO,SAAS,YAChB,CAAC,WAAW,KAAK,IAAI,KACrB,WAAW,UACX,OAAO,oBAAoB,YAC3B,gBAAgB,WAAW,GAC3B;AACA,WAAO;AAAA,EACT;AACA,SAAO,EAAE,MAAM,QAAQ,gBAAgB;AACzC;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,172 @@
1
+ import type { Stage } from '../constants/misc.const.js';
2
+ import { type ApprovalUseCaseType } from './use-cases.const.js';
3
+ /**
4
+ * The approvals of one type of request.
5
+ *
6
+ * The count is the number of approvals from holders of the run permissions of
7
+ * the request (`requiredPermissions`). The opener counts too: when the opener
8
+ * holds every run permission, the request gets the approval of the opener
9
+ * when it is created. The opener cannot approve the request by hand
10
+ * (`canAcknowledge`).
11
+ */
12
+ export interface ApprovalRule {
13
+ /** The approvals on prod. */
14
+ prod: number;
15
+ /** The approvals on dev, int and local. When it is not set, `prod` applies. */
16
+ testStages?: number;
17
+ }
18
+ /**
19
+ * How many approvals each type of request needs. This table is the one source
20
+ * of that number, for control-backend, control-frontend, and the routes of
21
+ * accounts and console that run an approved action.
22
+ *
23
+ * - **0**: no approval. Only a holder, or a person whom the use case opens the
24
+ * request to, can open it.
25
+ * - **1**: a holder does the action alone. The request of a holder runs when
26
+ * it is created. The request of a person who is not a holder waits for one
27
+ * holder.
28
+ * - **2**: four eyes. The request of a holder waits for one other holder. The
29
+ * request of a person who is not a holder waits for two holders.
30
+ * - **3**: six eyes on prod.
31
+ *
32
+ * Before #10483 a use case counted only the approvals of people other than
33
+ * the opener. Thus an old count and a count of this table are not the same
34
+ * unit.
35
+ */
36
+ export declare const APPROVAL_RULES: {
37
+ readonly "cluster-ttl-deletion": {
38
+ readonly prod: 0;
39
+ };
40
+ readonly "create-personal-organisation": {
41
+ readonly prod: 0;
42
+ };
43
+ readonly "create-custom-hardware-sizing": {
44
+ readonly prod: 0;
45
+ };
46
+ readonly "remove-custom-hardware-sizing": {
47
+ readonly prod: 0;
48
+ };
49
+ readonly "create-custom-generation": {
50
+ readonly prod: 0;
51
+ };
52
+ readonly "remove-custom-generation": {
53
+ readonly prod: 0;
54
+ };
55
+ readonly "self-service-cluster-modification": {
56
+ readonly prod: 0;
57
+ };
58
+ readonly "revoke-approval-override": {
59
+ readonly prod: 0;
60
+ };
61
+ readonly "extend-trial": {
62
+ readonly prod: 1;
63
+ };
64
+ readonly "change-org-limits": {
65
+ readonly prod: 1;
66
+ };
67
+ readonly "llm-budget-top-up": {
68
+ readonly prod: 1;
69
+ };
70
+ readonly "change-sales-plan": {
71
+ readonly prod: 1;
72
+ };
73
+ readonly "request-in-negotiation": {
74
+ readonly prod: 1;
75
+ };
76
+ readonly "onboard-to-enterprise": {
77
+ readonly prod: 1;
78
+ };
79
+ readonly "automated-enterprise-onboarding": {
80
+ readonly prod: 1;
81
+ };
82
+ readonly "change-cluster-plan-type": {
83
+ readonly prod: 1;
84
+ };
85
+ readonly "delete-internal-backup": {
86
+ readonly prod: 1;
87
+ };
88
+ readonly "release-train-publish": {
89
+ readonly prod: 1;
90
+ };
91
+ readonly "release-train-rollback": {
92
+ readonly prod: 1;
93
+ };
94
+ readonly "change-platform-roles": {
95
+ readonly prod: 2;
96
+ };
97
+ readonly "change-user-email": {
98
+ readonly prod: 2;
99
+ };
100
+ readonly "delete-user": {
101
+ readonly prod: 2;
102
+ };
103
+ readonly "purge-users": {
104
+ readonly prod: 2;
105
+ };
106
+ readonly "update-user-metadata": {
107
+ readonly prod: 2;
108
+ };
109
+ readonly "delete-organization": {
110
+ readonly prod: 2;
111
+ };
112
+ readonly "send-to-expired-trial": {
113
+ readonly prod: 2;
114
+ };
115
+ readonly "delete-sso-connection": {
116
+ readonly prod: 2;
117
+ };
118
+ readonly "delete-api-client": {
119
+ readonly prod: 2;
120
+ };
121
+ readonly "move-cluster-org": {
122
+ readonly prod: 2;
123
+ };
124
+ readonly "edit-generation": {
125
+ readonly prod: 2;
126
+ };
127
+ readonly "change-generation-versions": {
128
+ readonly prod: 2;
129
+ };
130
+ readonly "cluster-bulk-generation-update": {
131
+ readonly prod: 2;
132
+ };
133
+ readonly "cluster-bulk-plan-update": {
134
+ readonly prod: 2;
135
+ };
136
+ readonly "cluster-bulk-plan-type-update": {
137
+ readonly prod: 2;
138
+ };
139
+ readonly "cluster-bulk-sleep-wakeup": {
140
+ readonly prod: 2;
141
+ };
142
+ readonly "cluster-bulk-spec-update": {
143
+ readonly prod: 2;
144
+ };
145
+ readonly "cluster-bulk-deletion": {
146
+ readonly prod: 3;
147
+ readonly testStages: 2;
148
+ };
149
+ readonly "delete-cluster-corpses": {
150
+ readonly prod: 3;
151
+ readonly testStages: 2;
152
+ };
153
+ readonly "force-delete-cluster-plan-type": {
154
+ readonly prod: 3;
155
+ readonly testStages: 2;
156
+ };
157
+ readonly "change-feature-flag": {
158
+ readonly prod: 3;
159
+ readonly testStages: 2;
160
+ };
161
+ readonly "grant-approval-override": {
162
+ readonly prod: 2;
163
+ };
164
+ };
165
+ /** The types whose count on the test stages is not the count on prod. */
166
+ export type StageDependentApprovalType = {
167
+ [K in ApprovalUseCaseType]: (typeof APPROVAL_RULES)[K] extends {
168
+ testStages: number;
169
+ } ? K : never;
170
+ }[ApprovalUseCaseType];
171
+ /** The approvals that a request of `type` needs on `stage`. */
172
+ export declare function approvalsRequiredFor(type: ApprovalUseCaseType, stage: Stage): number;
@@ -0,0 +1,71 @@
1
+ import {
2
+ APPROVAL_USE_CASE_TYPES as T
3
+ } from "./use-cases.const.js";
4
+ const APPROVAL_RULES = {
5
+ // No approval. A use case that is open to every Camundi, or that only a
6
+ // system opens.
7
+ [T.clusterTtlDeletion]: { prod: 0 },
8
+ [T.createPersonalOrganisation]: { prod: 0 },
9
+ [T.createCustomHardwareSizing]: { prod: 0 },
10
+ [T.removeCustomHardwareSizing]: { prod: 0 },
11
+ [T.createCustomGeneration]: { prod: 0 },
12
+ [T.removeCustomGeneration]: { prod: 0 },
13
+ [T.selfServiceClusterModification]: { prod: 0 },
14
+ // Only the run of a grant opens it, as the system, and it runs by itself at
15
+ // the end of the grant. Ending a grant takes power away.
16
+ [T.revokeApprovalOverride]: { prod: 0 },
17
+ // A holder does it alone. Before #10483 a holder did these actions in
18
+ // accounts-frontend or console-frontend with no second person.
19
+ [T.extendTrial]: { prod: 1 },
20
+ [T.changeOrgLimits]: { prod: 1 },
21
+ [T.llmBudgetTopUp]: { prod: 1 },
22
+ [T.changeSalesPlan]: { prod: 1 },
23
+ [T.requestInNegotiation]: { prod: 1 },
24
+ [T.onboardToEnterprise]: { prod: 1 },
25
+ [T.automatedEnterpriseOnboarding]: { prod: 1 },
26
+ [T.changeClusterPlanType]: { prod: 1 },
27
+ [T.deleteInternalBackup]: { prod: 1 },
28
+ [T.releaseTrainPublish]: { prod: 1 },
29
+ [T.releaseTrainRollback]: { prod: 1 },
30
+ // Four eyes. The action removes data or access, changes a person, moves a
31
+ // cluster to another customer, or restarts every cluster of a generation.
32
+ [T.changePlatformRoles]: { prod: 2 },
33
+ [T.changeUserEmail]: { prod: 2 },
34
+ [T.deleteUser]: { prod: 2 },
35
+ [T.purgeUsers]: { prod: 2 },
36
+ [T.updateUserMetadata]: { prod: 2 },
37
+ [T.deleteOrganization]: { prod: 2 },
38
+ [T.sendToExpiredTrial]: { prod: 2 },
39
+ [T.deleteSsoConnection]: { prod: 2 },
40
+ [T.deleteApiClient]: { prod: 2 },
41
+ [T.moveClusterOrg]: { prod: 2 },
42
+ [T.editGeneration]: { prod: 2 },
43
+ [T.changeGenerationVersions]: { prod: 2 },
44
+ // One decision changes many clusters. A deletion cannot be undone, so it
45
+ // needs six eyes on prod.
46
+ [T.clusterBulkGenerationUpdate]: { prod: 2 },
47
+ [T.clusterBulkPlanUpdate]: { prod: 2 },
48
+ [T.clusterBulkPlanTypeUpdate]: { prod: 2 },
49
+ [T.clusterBulkSleepWakeup]: { prod: 2 },
50
+ [T.clusterBulkSpecUpdate]: { prod: 2 },
51
+ [T.clusterBulkDeletion]: { prod: 3, testStages: 2 },
52
+ [T.deleteClusterCorpses]: { prod: 3, testStages: 2 },
53
+ [T.forceDeleteClusterPlanType]: { prod: 3, testStages: 2 },
54
+ // A flag of prod turns a feature on or off for every customer.
55
+ [T.changeFeatureFlag]: { prod: 3, testStages: 2 },
56
+ // A grant lets one Camundi run many requests with no second person. The
57
+ // opener and one other holder, on every stage. The grantee approves
58
+ // neither by hand nor when they open it, thus a grant for oneself needs
59
+ // two other holders. The console team has three people, so a third
60
+ // approval would block a grant for oneself.
61
+ [T.grantApprovalOverride]: { prod: 2 }
62
+ };
63
+ function approvalsRequiredFor(type, stage) {
64
+ const rule = APPROVAL_RULES[type];
65
+ return stage === "prod" ? rule.prod : rule.testStages ?? rule.prod;
66
+ }
67
+ export {
68
+ APPROVAL_RULES,
69
+ approvalsRequiredFor
70
+ };
71
+ //# sourceMappingURL=approval-rules.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/approvals/approval-rules.ts"],
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport type { Stage } from '../constants/misc.const.js'\nimport {\n type ApprovalUseCaseType,\n APPROVAL_USE_CASE_TYPES as T,\n} from './use-cases.const.js'\n\n/**\n * The approvals of one type of request.\n *\n * The count is the number of approvals from holders of the run permissions of\n * the request (`requiredPermissions`). The opener counts too: when the opener\n * holds every run permission, the request gets the approval of the opener\n * when it is created. The opener cannot approve the request by hand\n * (`canAcknowledge`).\n */\nexport interface ApprovalRule {\n /** The approvals on prod. */\n prod: number\n /** The approvals on dev, int and local. When it is not set, `prod` applies. */\n testStages?: number\n}\n\n/**\n * How many approvals each type of request needs. This table is the one source\n * of that number, for control-backend, control-frontend, and the routes of\n * accounts and console that run an approved action.\n *\n * - **0**: no approval. Only a holder, or a person whom the use case opens the\n * request to, can open it.\n * - **1**: a holder does the action alone. The request of a holder runs when\n * it is created. The request of a person who is not a holder waits for one\n * holder.\n * - **2**: four eyes. The request of a holder waits for one other holder. The\n * request of a person who is not a holder waits for two holders.\n * - **3**: six eyes on prod.\n *\n * Before #10483 a use case counted only the approvals of people other than\n * the opener. Thus an old count and a count of this table are not the same\n * unit.\n */\nexport const APPROVAL_RULES = {\n // No approval. A use case that is open to every Camundi, or that only a\n // system opens.\n [T.clusterTtlDeletion]: { prod: 0 },\n [T.createPersonalOrganisation]: { prod: 0 },\n [T.createCustomHardwareSizing]: { prod: 0 },\n [T.removeCustomHardwareSizing]: { prod: 0 },\n [T.createCustomGeneration]: { prod: 0 },\n [T.removeCustomGeneration]: { prod: 0 },\n [T.selfServiceClusterModification]: { prod: 0 },\n // Only the run of a grant opens it, as the system, and it runs by itself at\n // the end of the grant. Ending a grant takes power away.\n [T.revokeApprovalOverride]: { prod: 0 },\n\n // A holder does it alone. Before #10483 a holder did these actions in\n // accounts-frontend or console-frontend with no second person.\n [T.extendTrial]: { prod: 1 },\n [T.changeOrgLimits]: { prod: 1 },\n [T.llmBudgetTopUp]: { prod: 1 },\n [T.changeSalesPlan]: { prod: 1 },\n [T.requestInNegotiation]: { prod: 1 },\n [T.onboardToEnterprise]: { prod: 1 },\n [T.automatedEnterpriseOnboarding]: { prod: 1 },\n [T.changeClusterPlanType]: { prod: 1 },\n [T.deleteInternalBackup]: { prod: 1 },\n [T.releaseTrainPublish]: { prod: 1 },\n [T.releaseTrainRollback]: { prod: 1 },\n\n // Four eyes. The action removes data or access, changes a person, moves a\n // cluster to another customer, or restarts every cluster of a generation.\n [T.changePlatformRoles]: { prod: 2 },\n [T.changeUserEmail]: { prod: 2 },\n [T.deleteUser]: { prod: 2 },\n [T.purgeUsers]: { prod: 2 },\n [T.updateUserMetadata]: { prod: 2 },\n [T.deleteOrganization]: { prod: 2 },\n [T.sendToExpiredTrial]: { prod: 2 },\n [T.deleteSsoConnection]: { prod: 2 },\n [T.deleteApiClient]: { prod: 2 },\n [T.moveClusterOrg]: { prod: 2 },\n [T.editGeneration]: { prod: 2 },\n [T.changeGenerationVersions]: { prod: 2 },\n\n // One decision changes many clusters. A deletion cannot be undone, so it\n // needs six eyes on prod.\n [T.clusterBulkGenerationUpdate]: { prod: 2 },\n [T.clusterBulkPlanUpdate]: { prod: 2 },\n [T.clusterBulkPlanTypeUpdate]: { prod: 2 },\n [T.clusterBulkSleepWakeup]: { prod: 2 },\n [T.clusterBulkSpecUpdate]: { prod: 2 },\n [T.clusterBulkDeletion]: { prod: 3, testStages: 2 },\n [T.deleteClusterCorpses]: { prod: 3, testStages: 2 },\n [T.forceDeleteClusterPlanType]: { prod: 3, testStages: 2 },\n\n // A flag of prod turns a feature on or off for every customer.\n [T.changeFeatureFlag]: { prod: 3, testStages: 2 },\n\n // A grant lets one Camundi run many requests with no second person. The\n // opener and one other holder, on every stage. The grantee approves\n // neither by hand nor when they open it, thus a grant for oneself needs\n // two other holders. The console team has three people, so a third\n // approval would block a grant for oneself.\n [T.grantApprovalOverride]: { prod: 2 },\n} as const satisfies Record<ApprovalUseCaseType, ApprovalRule>\n\n/** The types whose count on the test stages is not the count on prod. */\nexport type StageDependentApprovalType = {\n [K in ApprovalUseCaseType]: (typeof APPROVAL_RULES)[K] extends {\n testStages: number\n }\n ? K\n : never\n}[ApprovalUseCaseType]\n\n/** The approvals that a request of `type` needs on `stage`. */\nexport function approvalsRequiredFor(\n type: ApprovalUseCaseType,\n stage: Stage,\n): number {\n const rule: ApprovalRule = APPROVAL_RULES[type]\n return stage === 'prod' ? rule.prod : (rule.testStages ?? rule.prod)\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { ClusterBulkOperationStatus, ClusterBulkOperationType } from '../enums/ClusterBulkOperation.enum.js';
15
15
  import type { ClusterBulkOperationParameter } from '../types/ClusterBulkOperation.type.js';
16
+ import { type ApprovalUseCaseType } from './use-cases.const.js';
16
17
  /**
17
18
  * The most clusters that one request holds.
18
19
  *
@@ -41,30 +42,11 @@ export declare const CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH = 100;
41
42
  export declare const CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS = 60000;
42
43
  /** What one operation type needs, and which use case carries it. */
43
44
  export interface ClusterBulkOperationRule {
44
- useCase: string;
45
+ useCase: ApprovalUseCaseType;
45
46
  label: string;
46
47
  /** The permission that opens, approves and runs a request of this type. */
47
48
  permission: string;
48
49
  }
49
- /**
50
- * The floor of approvals of a cluster bulk operation on prod, for every type.
51
- * Dev, int and local use `CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS`.
52
- *
53
- * A bulk operation changes many clusters with one decision, so it needs two
54
- * people other than the requester. Every other use case needs one. This
55
- * floor is separate from `MINIMUM_EYEPAIRS` and is not lowered with it: the
56
- * approval flow needs 2 other people, and the old flow of console-backend
57
- * needs the count of `MINIMUM_EYEPAIRS`. That flow reads
58
- * `requiredBulkOperationAcks`.
59
- */
60
- export declare const CLUSTER_BULK_OPERATION_MIN_ACKS = 2;
61
- /**
62
- * The floor of approvals of a cluster bulk operation on dev, int and local.
63
- *
64
- * A test stage often has one other Camundi who can approve. One approval
65
- * there still tests the whole flow.
66
- */
67
- export declare const CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS = 1;
68
50
  /**
69
51
  * The rules of each operation type.
70
52
  *
@@ -109,6 +91,9 @@ export interface ClusterBulkPlanUpdateSettings {
109
91
  /**
110
92
  * No `keepModification`: the parameter of a plan type update in the old flow
111
93
  * has none, and its dialog never sends it.
94
+ *
95
+ * No packages either: each cluster keeps its packages, and console checks
96
+ * them against the free reservations of the organization.
112
97
  */
113
98
  export interface ClusterBulkPlanTypeUpdateSettings {
114
99
  targetPlanTypeId: string;
@@ -3,13 +3,13 @@ import {
3
3
  ClusterBulkOperationType
4
4
  } from "../enums/ClusterBulkOperation.enum.js";
5
5
  import { ConsolePermissions } from "../permissions/console.permissions.js";
6
- import { APPROVAL_USE_CASE_TYPES } from "./use-cases.const.js";
6
+ import {
7
+ APPROVAL_USE_CASE_TYPES
8
+ } from "./use-cases.const.js";
7
9
  const CLUSTER_BULK_OPERATION_MAX_CLUSTERS = 3e3;
8
10
  const CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH = 200;
9
11
  const CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH = 100;
10
12
  const CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS = 6e4;
11
- const CLUSTER_BULK_OPERATION_MIN_ACKS = 2;
12
- const CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS = 1;
13
13
  const CLUSTER_BULK_OPERATION_RULES = {
14
14
  [ClusterBulkOperationType.GENERATION_UPDATE]: {
15
15
  useCase: APPROVAL_USE_CASE_TYPES.clusterBulkGenerationUpdate,
@@ -114,10 +114,8 @@ function clusterBulkResultOf(executionResult, clusterId) {
114
114
  export {
115
115
  CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH,
116
116
  CLUSTER_BULK_OPERATION_MAX_CLUSTERS,
117
- CLUSTER_BULK_OPERATION_MIN_ACKS,
118
117
  CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH,
119
118
  CLUSTER_BULK_OPERATION_RULES,
120
- CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS,
121
119
  CLUSTER_BULK_OPERATION_TYPES,
122
120
  CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS,
123
121
  clusterBulkOperationTypeOf,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/approvals/cluster-bulk-operation.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 cluster bulk operation on the approval mechanism of control-backend.\n *\n * control-backend and control-frontend both read this file. The backend\n * builds its six use cases from `CLUSTER_BULK_OPERATION_RULES`, and the form\n * of the frontend reads the same table to run `canCreate` before the request\n * goes out. A permission that only one side knew would be a button that the\n * server then refuses.\n *\n * console-backend reads the step types at the end. control-backend calls one\n * step for each cluster, and console-backend does the work of that step with\n * the same code that its own bulk operation runs.\n */\n\nimport {\n ClusterBulkOperationStatus,\n ClusterBulkOperationType,\n} from '../enums/ClusterBulkOperation.enum.js'\nimport { ConsolePermissions } from '../permissions/console.permissions.js'\nimport type { ClusterBulkOperationParameter } from '../types/ClusterBulkOperation.type.js'\nimport { APPROVAL_USE_CASE_TYPES } from './use-cases.const.js'\n\n/**\n * The most clusters that one request holds.\n *\n * The payload is a json column, and the create route reads it in one body of\n * at most 1 MiB. 3000 clusters with the longest names stay far below that.\n * A larger set is two requests, which is also two decisions for the people\n * who approve.\n */\nexport const CLUSTER_BULK_OPERATION_MAX_CLUSTERS = 3000\n\n/** The longest name of an operation. The name is what the queue shows. */\nexport const CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH = 200\n\n/**\n * The longest cluster name that the payload keeps.\n *\n * The name is a copy for a reader. The id alone decides what the step\n * changes.\n */\nexport const CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH = 100\n\n/**\n * The wait between two sleeping clusters of one datacenter.\n *\n * The same number as the bulk operation of console-backend. A wake-up loads\n * the datacenter, and many wake-ups together overload it. Datacenters do not\n * wait for each other.\n */\nexport const CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS = 60_000\n\n/** What one operation type needs, and which use case carries it. */\nexport interface ClusterBulkOperationRule {\n useCase: string\n label: string\n /** The permission that opens, approves and runs a request of this type. */\n permission: string\n}\n\n/**\n * The floor of approvals of a cluster bulk operation on prod, for every type.\n * Dev, int and local use `CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS`.\n *\n * A bulk operation changes many clusters with one decision, so it needs two\n * people other than the requester. Every other use case needs one. This\n * floor is separate from `MINIMUM_EYEPAIRS` and is not lowered with it: the\n * approval flow needs 2 other people, and the old flow of console-backend\n * needs the count of `MINIMUM_EYEPAIRS`. That flow reads\n * `requiredBulkOperationAcks`.\n */\nexport const CLUSTER_BULK_OPERATION_MIN_ACKS = 2\n\n/**\n * The floor of approvals of a cluster bulk operation on dev, int and local.\n *\n * A test stage often has one other Camundi who can approve. One approval\n * there still tests the whole flow.\n */\nexport const CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS = 1\n\n/**\n * The rules of each operation type.\n *\n * The permissions are the ones that the bulk operation of console-backend\n * checks today (`getBulkOpRequiredPermission`), so a Camundi who can run one\n * flow can run the other. `CLUSTERS_READALLCLUSTERINFO` is part of every\n * type there, and `clusterBulkRequiredPermissions` adds it here.\n */\nexport const CLUSTER_BULK_OPERATION_RULES: Record<\n ClusterBulkOperationType,\n ClusterBulkOperationRule\n> = {\n [ClusterBulkOperationType.GENERATION_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkGenerationUpdate,\n label: 'Generation update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_GENERATION,\n },\n [ClusterBulkOperationType.PLAN_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkPlanUpdate,\n label: 'Plan update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE,\n },\n [ClusterBulkOperationType.PLAN_TYPE_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkPlanTypeUpdate,\n label: 'Plan type update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE,\n },\n [ClusterBulkOperationType.SLEEP_WAKEUP]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkSleepWakeup,\n label: 'Sleep / wake up',\n permission: ConsolePermissions.CLUSTERS_CAN_SEND_TO_SLEEP,\n },\n [ClusterBulkOperationType.DELETION]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkDeletion,\n label: 'Deletion',\n permission: ConsolePermissions.CLUSTERS_DELETE_CLUSTER,\n },\n [ClusterBulkOperationType.SPEC_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkSpecUpdate,\n label: 'Spec update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE,\n },\n}\n\n/** The operation types in the order that a form offers them. */\nexport const CLUSTER_BULK_OPERATION_TYPES: ClusterBulkOperationType[] = [\n ClusterBulkOperationType.GENERATION_UPDATE,\n ClusterBulkOperationType.PLAN_UPDATE,\n ClusterBulkOperationType.PLAN_TYPE_UPDATE,\n ClusterBulkOperationType.SLEEP_WAKEUP,\n ClusterBulkOperationType.SPEC_UPDATE,\n ClusterBulkOperationType.DELETION,\n]\n\n/** The operation type of a use case, or `undefined` for any other use case. */\nexport function clusterBulkOperationTypeOf(\n useCase: string,\n): ClusterBulkOperationType | undefined {\n return CLUSTER_BULK_OPERATION_TYPES.find(\n (type) => CLUSTER_BULK_OPERATION_RULES[type].useCase === useCase,\n )\n}\n\n/** One cluster of a request. */\nexport interface ClusterBulkTargetDto {\n id: string\n /** A copy for a reader. The step reads the id alone. */\n name?: string\n orgId?: string\n}\n\nexport interface ClusterBulkGenerationUpdateSettings {\n targetGenerationId: string\n /** A copy for a reader. */\n targetGenerationName?: string\n /** See `ClusterBulkOperationGenerationUpdateParameter.yoloAllClusters`. */\n yoloAllClusters?: boolean\n /** See `ClusterBulkOperationGenerationUpdateParameter.yoloForceUpdate`. */\n yoloForceUpdate?: boolean\n /**\n * Changes only a cluster with auto-update on. console-backend reads the\n * cluster when its step runs, because the customer can turn auto-update off\n * while the request waits.\n */\n onlyAutoUpgrade?: boolean\n}\n\nexport interface ClusterBulkPlanUpdateSettings {\n targetPlanId: string\n targetPlanName?: string\n /** Keeps the active modification of a cluster through the change. */\n keepModification?: boolean\n}\n\n/**\n * No `keepModification`: the parameter of a plan type update in the old flow\n * has none, and its dialog never sends it.\n */\nexport interface ClusterBulkPlanTypeUpdateSettings {\n targetPlanTypeId: string\n targetPlanTypeName?: string\n}\n\nexport interface ClusterBulkSleepWakeupSettings {\n action: 'sleep' | 'wakeup'\n}\n\n/** The settings of each type. Deletion and spec update have none. */\nexport interface ClusterBulkSettingsByType {\n [ClusterBulkOperationType.GENERATION_UPDATE]: ClusterBulkGenerationUpdateSettings\n [ClusterBulkOperationType.PLAN_UPDATE]: ClusterBulkPlanUpdateSettings\n [ClusterBulkOperationType.PLAN_TYPE_UPDATE]: ClusterBulkPlanTypeUpdateSettings\n [ClusterBulkOperationType.SLEEP_WAKEUP]: ClusterBulkSleepWakeupSettings\n [ClusterBulkOperationType.DELETION]: Record<string, never>\n [ClusterBulkOperationType.SPEC_UPDATE]: Record<string, never>\n}\n\n/** The payload of a request, for one operation type. */\nexport interface ClusterBulkOperationPayloadDto<\n T extends ClusterBulkOperationType = ClusterBulkOperationType,\n> {\n name: string\n clusters: ClusterBulkTargetDto[]\n settings: ClusterBulkSettingsByType[T]\n}\n\n/** True when the settings of a generation update go past a safety check. */\nexport function isClusterBulkYolo(\n type: ClusterBulkOperationType,\n settings: unknown,\n): boolean {\n if (\n type !== ClusterBulkOperationType.GENERATION_UPDATE ||\n typeof settings !== 'object' ||\n settings === null\n ) {\n return false\n }\n const typed = settings as ClusterBulkGenerationUpdateSettings\n return typed.yoloAllClusters === true || typed.yoloForceUpdate === true\n}\n\n/**\n * Every permission that a request of this type and these settings needs.\n *\n * control-backend stores the answer on the request as\n * `requiredPermissions`, and the form passes the same answer to `canCreate`.\n * A yolo flag adds `CLUSTERS_CAN_UPDATE_GENERATION_YOLO`, which is the rule\n * of `needsGenerationYolo` in console-backend.\n */\nexport function clusterBulkRequiredPermissions(\n type: ClusterBulkOperationType,\n settings: unknown,\n): string[] {\n const permissions = [\n CLUSTER_BULK_OPERATION_RULES[type].permission,\n ConsolePermissions.CLUSTERS_READALLCLUSTERINFO,\n ]\n if (isClusterBulkYolo(type, settings)) {\n permissions.push(ConsolePermissions.CLUSTERS_CAN_UPDATE_GENERATION_YOLO)\n }\n return permissions\n}\n\n/**\n * Turns the settings of a request into the parameter of console-backend.\n *\n * The step route of console-backend reads the parameter shape of its own\n * bulk operation, so one function in console-backend serves both flows.\n * `nop` of the sleep action is not reachable from here on purpose: a request\n * that changes nothing is not a request that people approve.\n */\nexport function clusterBulkParameterOf(\n type: ClusterBulkOperationType,\n settings: unknown,\n): ClusterBulkOperationParameter {\n const values = (settings ?? {}) as Record<string, unknown>\n switch (type) {\n case ClusterBulkOperationType.GENERATION_UPDATE:\n return {\n toGenerationUuid: String(values.targetGenerationId),\n yoloAllClusters: values.yoloAllClusters === true,\n yoloForceUpdate: values.yoloForceUpdate === true,\n // Only when set, so the parameter of a plain update stays as it was.\n ...(values.onlyAutoUpgrade === true && { onlyAutoUpgrade: true }),\n }\n case ClusterBulkOperationType.PLAN_UPDATE:\n return {\n toPlanUuid: String(values.targetPlanId),\n yolo: false,\n keepModification: values.keepModification === true,\n }\n case ClusterBulkOperationType.PLAN_TYPE_UPDATE:\n return {\n toPlanTypeUuid: String(values.targetPlanTypeId),\n yolo: false,\n }\n case ClusterBulkOperationType.SLEEP_WAKEUP:\n return { toSleep: values.action === 'wakeup' ? 'wakeup' : 'sleep' }\n default:\n return undefined\n }\n}\n\n/** The outcome of one cluster, once its step finished. */\nexport type ClusterBulkFinalStatus =\n | ClusterBulkOperationStatus.DONE\n | ClusterBulkOperationStatus.ERRORED\n | ClusterBulkOperationStatus.SKIPPED\n\n/**\n * What a request records for one cluster, in `executionResult`.\n *\n * `executionResult` of a bulk request is an object with ONE key for each\n * cluster that finished, keyed by the id of the cluster. A cluster with no\n * key has not run yet. That is what lets an interrupted run skip the\n * clusters that are finished, and what a reader shows as \"not reached\".\n */\nexport interface ClusterBulkClusterResultDto {\n result: ClusterBulkFinalStatus\n /** Why a cluster errored or was skipped, or a note on a done cluster. */\n error?: string\n /** An ISO timestamp: when the step answered. */\n at: string\n}\n\n/** Reads the result of one cluster out of `executionResult`. */\nexport function clusterBulkResultOf(\n executionResult: Record<string, unknown> | undefined,\n clusterId: string,\n): ClusterBulkClusterResultDto | undefined {\n const entry = executionResult?.[clusterId]\n if (typeof entry !== 'object' || entry === null) {\n return undefined\n }\n const result = (entry as { result?: unknown }).result\n if (\n result !== ClusterBulkOperationStatus.DONE &&\n result !== ClusterBulkOperationStatus.ERRORED &&\n result !== ClusterBulkOperationStatus.SKIPPED\n ) {\n return undefined\n }\n return entry as ClusterBulkClusterResultDto\n}\n\n/**\n * The body of the step route of console-backend.\n *\n * `api/admin/approvals/clusters/:clusterId/bulk-step`. One call does the work\n * of one operation type on one cluster.\n */\nexport interface ClusterBulkStepRequestDto {\n type: ClusterBulkOperationType\n parameter: ClusterBulkOperationParameter\n /**\n * When true, a sleeping cluster is NOT changed. The answer is `deferred`,\n * with the datacenter of the cluster.\n *\n * This is how control-backend keeps the stagger of the old run: the first\n * pass changes the awake clusters and learns which ones sleep, and a second\n * pass changes the sleeping ones: one lane for each datacenter, the lanes\n * side by side, and the clusters of one lane\n * `CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS` apart.\n */\n deferIfSleeping: boolean\n}\n\n/** The answer of the step route. */\nexport type ClusterBulkStepResultDto =\n | {\n outcome: 'finished'\n result: ClusterBulkFinalStatus\n error?: string\n }\n | {\n outcome: 'deferred'\n /** The k8s context of the cluster, or `unknown`. */\n datacenter: string\n }\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\n/**\n * The cluster bulk operation on the approval mechanism of control-backend.\n *\n * control-backend and control-frontend both read this file. The backend\n * builds its six use cases from `CLUSTER_BULK_OPERATION_RULES`, and the form\n * of the frontend reads the same table to run `canCreate` before the request\n * goes out. A permission that only one side knew would be a button that the\n * server then refuses.\n *\n * console-backend reads the step types at the end. control-backend calls one\n * step for each cluster, and console-backend does the work of that step with\n * the same code that its own bulk operation runs.\n */\n\nimport {\n ClusterBulkOperationStatus,\n ClusterBulkOperationType,\n} from '../enums/ClusterBulkOperation.enum.js'\nimport { ConsolePermissions } from '../permissions/console.permissions.js'\nimport type { ClusterBulkOperationParameter } from '../types/ClusterBulkOperation.type.js'\nimport {\n APPROVAL_USE_CASE_TYPES,\n type ApprovalUseCaseType,\n} from './use-cases.const.js'\n\n/**\n * The most clusters that one request holds.\n *\n * The payload is a json column, and the create route reads it in one body of\n * at most 1 MiB. 3000 clusters with the longest names stay far below that.\n * A larger set is two requests, which is also two decisions for the people\n * who approve.\n */\nexport const CLUSTER_BULK_OPERATION_MAX_CLUSTERS = 3000\n\n/** The longest name of an operation. The name is what the queue shows. */\nexport const CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH = 200\n\n/**\n * The longest cluster name that the payload keeps.\n *\n * The name is a copy for a reader. The id alone decides what the step\n * changes.\n */\nexport const CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH = 100\n\n/**\n * The wait between two sleeping clusters of one datacenter.\n *\n * The same number as the bulk operation of console-backend. A wake-up loads\n * the datacenter, and many wake-ups together overload it. Datacenters do not\n * wait for each other.\n */\nexport const CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS = 60_000\n\n/** What one operation type needs, and which use case carries it. */\nexport interface ClusterBulkOperationRule {\n useCase: ApprovalUseCaseType\n label: string\n /** The permission that opens, approves and runs a request of this type. */\n permission: string\n}\n\n/**\n * The rules of each operation type.\n *\n * The permissions are the ones that the bulk operation of console-backend\n * checks today (`getBulkOpRequiredPermission`), so a Camundi who can run one\n * flow can run the other. `CLUSTERS_READALLCLUSTERINFO` is part of every\n * type there, and `clusterBulkRequiredPermissions` adds it here.\n */\nexport const CLUSTER_BULK_OPERATION_RULES: Record<\n ClusterBulkOperationType,\n ClusterBulkOperationRule\n> = {\n [ClusterBulkOperationType.GENERATION_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkGenerationUpdate,\n label: 'Generation update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_GENERATION,\n },\n [ClusterBulkOperationType.PLAN_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkPlanUpdate,\n label: 'Plan update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE,\n },\n [ClusterBulkOperationType.PLAN_TYPE_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkPlanTypeUpdate,\n label: 'Plan type update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE,\n },\n [ClusterBulkOperationType.SLEEP_WAKEUP]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkSleepWakeup,\n label: 'Sleep / wake up',\n permission: ConsolePermissions.CLUSTERS_CAN_SEND_TO_SLEEP,\n },\n [ClusterBulkOperationType.DELETION]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkDeletion,\n label: 'Deletion',\n permission: ConsolePermissions.CLUSTERS_DELETE_CLUSTER,\n },\n [ClusterBulkOperationType.SPEC_UPDATE]: {\n useCase: APPROVAL_USE_CASE_TYPES.clusterBulkSpecUpdate,\n label: 'Spec update',\n permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE,\n },\n}\n\n/** The operation types in the order that a form offers them. */\nexport const CLUSTER_BULK_OPERATION_TYPES: ClusterBulkOperationType[] = [\n ClusterBulkOperationType.GENERATION_UPDATE,\n ClusterBulkOperationType.PLAN_UPDATE,\n ClusterBulkOperationType.PLAN_TYPE_UPDATE,\n ClusterBulkOperationType.SLEEP_WAKEUP,\n ClusterBulkOperationType.SPEC_UPDATE,\n ClusterBulkOperationType.DELETION,\n]\n\n/** The operation type of a use case, or `undefined` for any other use case. */\nexport function clusterBulkOperationTypeOf(\n useCase: string,\n): ClusterBulkOperationType | undefined {\n return CLUSTER_BULK_OPERATION_TYPES.find(\n (type) => CLUSTER_BULK_OPERATION_RULES[type].useCase === useCase,\n )\n}\n\n/** One cluster of a request. */\nexport interface ClusterBulkTargetDto {\n id: string\n /** A copy for a reader. The step reads the id alone. */\n name?: string\n orgId?: string\n}\n\nexport interface ClusterBulkGenerationUpdateSettings {\n targetGenerationId: string\n /** A copy for a reader. */\n targetGenerationName?: string\n /** See `ClusterBulkOperationGenerationUpdateParameter.yoloAllClusters`. */\n yoloAllClusters?: boolean\n /** See `ClusterBulkOperationGenerationUpdateParameter.yoloForceUpdate`. */\n yoloForceUpdate?: boolean\n /**\n * Changes only a cluster with auto-update on. console-backend reads the\n * cluster when its step runs, because the customer can turn auto-update off\n * while the request waits.\n */\n onlyAutoUpgrade?: boolean\n}\n\nexport interface ClusterBulkPlanUpdateSettings {\n targetPlanId: string\n targetPlanName?: string\n /** Keeps the active modification of a cluster through the change. */\n keepModification?: boolean\n}\n\n/**\n * No `keepModification`: the parameter of a plan type update in the old flow\n * has none, and its dialog never sends it.\n *\n * No packages either: each cluster keeps its packages, and console checks\n * them against the free reservations of the organization.\n */\nexport interface ClusterBulkPlanTypeUpdateSettings {\n targetPlanTypeId: string\n targetPlanTypeName?: string\n}\n\nexport interface ClusterBulkSleepWakeupSettings {\n action: 'sleep' | 'wakeup'\n}\n\n/** The settings of each type. Deletion and spec update have none. */\nexport interface ClusterBulkSettingsByType {\n [ClusterBulkOperationType.GENERATION_UPDATE]: ClusterBulkGenerationUpdateSettings\n [ClusterBulkOperationType.PLAN_UPDATE]: ClusterBulkPlanUpdateSettings\n [ClusterBulkOperationType.PLAN_TYPE_UPDATE]: ClusterBulkPlanTypeUpdateSettings\n [ClusterBulkOperationType.SLEEP_WAKEUP]: ClusterBulkSleepWakeupSettings\n [ClusterBulkOperationType.DELETION]: Record<string, never>\n [ClusterBulkOperationType.SPEC_UPDATE]: Record<string, never>\n}\n\n/** The payload of a request, for one operation type. */\nexport interface ClusterBulkOperationPayloadDto<\n T extends ClusterBulkOperationType = ClusterBulkOperationType,\n> {\n name: string\n clusters: ClusterBulkTargetDto[]\n settings: ClusterBulkSettingsByType[T]\n}\n\n/** True when the settings of a generation update go past a safety check. */\nexport function isClusterBulkYolo(\n type: ClusterBulkOperationType,\n settings: unknown,\n): boolean {\n if (\n type !== ClusterBulkOperationType.GENERATION_UPDATE ||\n typeof settings !== 'object' ||\n settings === null\n ) {\n return false\n }\n const typed = settings as ClusterBulkGenerationUpdateSettings\n return typed.yoloAllClusters === true || typed.yoloForceUpdate === true\n}\n\n/**\n * Every permission that a request of this type and these settings needs.\n *\n * control-backend stores the answer on the request as\n * `requiredPermissions`, and the form passes the same answer to `canCreate`.\n * A yolo flag adds `CLUSTERS_CAN_UPDATE_GENERATION_YOLO`, which is the rule\n * of `needsGenerationYolo` in console-backend.\n */\nexport function clusterBulkRequiredPermissions(\n type: ClusterBulkOperationType,\n settings: unknown,\n): string[] {\n const permissions = [\n CLUSTER_BULK_OPERATION_RULES[type].permission,\n ConsolePermissions.CLUSTERS_READALLCLUSTERINFO,\n ]\n if (isClusterBulkYolo(type, settings)) {\n permissions.push(ConsolePermissions.CLUSTERS_CAN_UPDATE_GENERATION_YOLO)\n }\n return permissions\n}\n\n/**\n * Turns the settings of a request into the parameter of console-backend.\n *\n * The step route of console-backend reads the parameter shape of its own\n * bulk operation, so one function in console-backend serves both flows.\n * `nop` of the sleep action is not reachable from here on purpose: a request\n * that changes nothing is not a request that people approve.\n */\nexport function clusterBulkParameterOf(\n type: ClusterBulkOperationType,\n settings: unknown,\n): ClusterBulkOperationParameter {\n const values = (settings ?? {}) as Record<string, unknown>\n switch (type) {\n case ClusterBulkOperationType.GENERATION_UPDATE:\n return {\n toGenerationUuid: String(values.targetGenerationId),\n yoloAllClusters: values.yoloAllClusters === true,\n yoloForceUpdate: values.yoloForceUpdate === true,\n // Only when set, so the parameter of a plain update stays as it was.\n ...(values.onlyAutoUpgrade === true && { onlyAutoUpgrade: true }),\n }\n case ClusterBulkOperationType.PLAN_UPDATE:\n return {\n toPlanUuid: String(values.targetPlanId),\n yolo: false,\n keepModification: values.keepModification === true,\n }\n case ClusterBulkOperationType.PLAN_TYPE_UPDATE:\n return {\n toPlanTypeUuid: String(values.targetPlanTypeId),\n yolo: false,\n }\n case ClusterBulkOperationType.SLEEP_WAKEUP:\n return { toSleep: values.action === 'wakeup' ? 'wakeup' : 'sleep' }\n default:\n return undefined\n }\n}\n\n/** The outcome of one cluster, once its step finished. */\nexport type ClusterBulkFinalStatus =\n | ClusterBulkOperationStatus.DONE\n | ClusterBulkOperationStatus.ERRORED\n | ClusterBulkOperationStatus.SKIPPED\n\n/**\n * What a request records for one cluster, in `executionResult`.\n *\n * `executionResult` of a bulk request is an object with ONE key for each\n * cluster that finished, keyed by the id of the cluster. A cluster with no\n * key has not run yet. That is what lets an interrupted run skip the\n * clusters that are finished, and what a reader shows as \"not reached\".\n */\nexport interface ClusterBulkClusterResultDto {\n result: ClusterBulkFinalStatus\n /** Why a cluster errored or was skipped, or a note on a done cluster. */\n error?: string\n /** An ISO timestamp: when the step answered. */\n at: string\n}\n\n/** Reads the result of one cluster out of `executionResult`. */\nexport function clusterBulkResultOf(\n executionResult: Record<string, unknown> | undefined,\n clusterId: string,\n): ClusterBulkClusterResultDto | undefined {\n const entry = executionResult?.[clusterId]\n if (typeof entry !== 'object' || entry === null) {\n return undefined\n }\n const result = (entry as { result?: unknown }).result\n if (\n result !== ClusterBulkOperationStatus.DONE &&\n result !== ClusterBulkOperationStatus.ERRORED &&\n result !== ClusterBulkOperationStatus.SKIPPED\n ) {\n return undefined\n }\n return entry as ClusterBulkClusterResultDto\n}\n\n/**\n * The body of the step route of console-backend.\n *\n * `api/admin/approvals/clusters/:clusterId/bulk-step`. One call does the work\n * of one operation type on one cluster.\n */\nexport interface ClusterBulkStepRequestDto {\n type: ClusterBulkOperationType\n parameter: ClusterBulkOperationParameter\n /**\n * When true, a sleeping cluster is NOT changed. The answer is `deferred`,\n * with the datacenter of the cluster.\n *\n * This is how control-backend keeps the stagger of the old run: the first\n * pass changes the awake clusters and learns which ones sleep, and a second\n * pass changes the sleeping ones: one lane for each datacenter, the lanes\n * side by side, and the clusters of one lane\n * `CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS` apart.\n */\n deferIfSleeping: boolean\n}\n\n/** The answer of the step route. */\nexport type ClusterBulkStepResultDto =\n | {\n outcome: 'finished'\n result: ClusterBulkFinalStatus\n error?: string\n }\n | {\n outcome: 'deferred'\n /** The k8s context of the cluster, or `unknown`. */\n datacenter: string\n }\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,10 +1,3 @@
1
- /**
2
- * The flag of the `delete-cluster-corpses` use case.
3
- *
4
- * control-backend opens a request only while it is on. Control shows the
5
- * request in place of the direct "Delete all corpses" while it is on.
6
- */
7
- export declare const DELETE_CLUSTER_CORPSES_FLAG = "control.deleteClusterCorpses";
8
1
  /**
9
2
  * The two kinds of cluster corpse.
10
3
  *
@@ -32,14 +25,6 @@ export declare const DELETE_CLUSTER_CORPSES_MAX = 500;
32
25
  * a short time.
33
26
  */
34
27
  export declare const CLUSTER_CORPSE_MIN_AGE_MS: number;
35
- /**
36
- * The approvals of a corpse deletion on prod: two people other than the
37
- * person who asks, the same as a cluster bulk deletion
38
- * (`CLUSTER_BULK_OPERATION_MIN_ACKS`).
39
- */
40
- export declare const DELETE_CLUSTER_CORPSES_MIN_ACKS_PROD = 2;
41
- /** The approvals of a corpse deletion on dev, int and local. */
42
- export declare const DELETE_CLUSTER_CORPSES_MIN_ACKS_TEST_STAGE = 1;
43
28
  /** One corpse, as console answers it to control-backend. */
44
29
  export interface ClusterCorpseDto {
45
30
  /** The uuid of the cluster row, or the name of the Kubernetes resource. */