@camunda/ccma-shared-types 3.0.0 → 5.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 (58) hide show
  1. package/dist/_tests/approvals/cluster-bulk-operation.spec.d.ts +1 -0
  2. package/dist/_tests/types/PreprodClusterTtl.type.spec.d.ts +1 -0
  3. package/dist/annotations/policy.d.ts +98 -0
  4. package/dist/annotations/policy.js +61 -0
  5. package/dist/annotations/policy.js.map +7 -0
  6. package/dist/approvals/cluster-bulk-operation.d.ts +196 -0
  7. package/dist/approvals/cluster-bulk-operation.js +127 -0
  8. package/dist/approvals/cluster-bulk-operation.js.map +7 -0
  9. package/dist/approvals/policy.d.ts +45 -5
  10. package/dist/approvals/policy.js +38 -11
  11. package/dist/approvals/policy.js.map +2 -2
  12. package/dist/approvals/use-cases.const.d.ts +7 -0
  13. package/dist/approvals/use-cases.const.js +11 -1
  14. package/dist/approvals/use-cases.const.js.map +2 -2
  15. package/dist/config/camundi-permission-rules.js +24 -3
  16. package/dist/config/camundi-permission-rules.js.map +2 -2
  17. package/dist/constants/misc.const.d.ts +2 -0
  18. package/dist/constants/misc.const.js +4 -0
  19. package/dist/constants/misc.const.js.map +2 -2
  20. package/dist/dtos/Annotation.dto.d.ts +240 -0
  21. package/dist/dtos/Annotation.dto.js +47 -0
  22. package/dist/dtos/Annotation.dto.js.map +7 -0
  23. package/dist/dtos/ApprovalJob.dto.d.ts +152 -2
  24. package/dist/dtos/ApprovalJob.dto.js +22 -0
  25. package/dist/dtos/ApprovalJob.dto.js.map +2 -2
  26. package/dist/dtos/CommonCluster.dto.d.ts +6 -0
  27. package/dist/dtos/ControlV2.dto.d.ts +65 -3
  28. package/dist/dtos/ExternalWebapp.dto.d.ts +12 -0
  29. package/dist/dtos/FeatureFlags.dto.d.ts +6 -0
  30. package/dist/dtos/Hdp.dto.d.ts +153 -0
  31. package/dist/dtos/K8sContext.dto.d.ts +13 -4
  32. package/dist/dtos/SalesPlan.dto.d.ts +25 -1
  33. package/dist/dtos/SalesPlan.dto.js +19 -6
  34. package/dist/dtos/SalesPlan.dto.js.map +2 -2
  35. package/dist/index.d.ts +16 -10
  36. package/dist/index.js +103 -7
  37. package/dist/index.js.map +2 -2
  38. package/dist/permissions/accounts.permissions.d.ts +2 -0
  39. package/dist/permissions/accounts.permissions.js +2 -1
  40. package/dist/permissions/accounts.permissions.js.map +2 -2
  41. package/dist/permissions/combo.js +4 -2
  42. package/dist/permissions/combo.js.map +2 -2
  43. package/dist/permissions/console.permissions.d.ts +32 -0
  44. package/dist/permissions/console.permissions.js +24 -2
  45. package/dist/permissions/console.permissions.js.map +2 -2
  46. package/dist/types/PreprodClusterTtl.type.d.ts +33 -0
  47. package/dist/types/PreprodClusterTtl.type.js +37 -0
  48. package/dist/types/PreprodClusterTtl.type.js.map +7 -0
  49. package/dist/types/Roles.d.ts +1 -1
  50. package/dist/types/Roles.js.map +2 -2
  51. package/dist/types/hdp.types.d.ts +6 -0
  52. package/dist/types/hdp.types.js +3 -1
  53. package/dist/types/hdp.types.js.map +2 -2
  54. package/package.json +1 -1
  55. package/dist/constants/failoverRegionPairs.const.d.ts +0 -36
  56. package/dist/constants/failoverRegionPairs.const.js +0 -34
  57. package/dist/constants/failoverRegionPairs.const.js.map +0 -7
  58. /package/dist/_tests/{constants/failoverRegionPairs.const.spec.d.ts → annotations/policy.spec.d.ts} +0 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,98 @@
1
+ import type { AnnotationDto } from '../dtos/Annotation.dto.js';
2
+ /**
3
+ * The rules of the annotations, as pure functions.
4
+ *
5
+ * Two consumers run this file: control-backend decides whether to refuse a
6
+ * write, and control-frontend decides whether to show the button. One copy
7
+ * means the button and the route can never disagree.
8
+ *
9
+ * No function here does I/O or returns a promise, which is what keeps it safe
10
+ * for a browser bundle and for a list of 200 rows.
11
+ */
12
+ /** Who is asking. The frontend has this from `useSession`. */
13
+ export interface AnnotationActorView {
14
+ userId: string;
15
+ /**
16
+ * The address of the actor. It is the SECOND identity of the author rule,
17
+ * for the same reason as in the approvals: one person can hold two Auth0
18
+ * identities with two different `sub` values, and the author of a note must
19
+ * still be able to edit it after signing in through the other one.
20
+ */
21
+ email?: string;
22
+ }
23
+ /**
24
+ * Why a write was refused.
25
+ *
26
+ * The backend maps each code to a status, and the frontend shows the same
27
+ * words. See `ANNOTATION_DENIAL_MESSAGES`.
28
+ */
29
+ export type AnnotationDenialCode = 'not-author' | 'empty-body' | 'body-too-long' | 'empty-tag' | 'tag-too-long';
30
+ /**
31
+ * What a person reads for each denial.
32
+ *
33
+ * The MESSAGE is here, and not in the error catalogue of the backend, because
34
+ * the frontend shows it beside a disabled button before any request is sent.
35
+ * Two spellings of one rule is the drift that this file exists to stop.
36
+ */
37
+ export declare const ANNOTATION_DENIAL_MESSAGES: Record<AnnotationDenialCode, string>;
38
+ /** A rule that refused, with the code and the words for it. */
39
+ export interface AnnotationDenial {
40
+ ok: false;
41
+ code: AnnotationDenialCode;
42
+ message: string;
43
+ }
44
+ /** A rule that let the write through. */
45
+ export interface AnnotationAllowance {
46
+ ok: true;
47
+ }
48
+ export type AnnotationDecision = AnnotationAllowance | AnnotationDenial;
49
+ /**
50
+ * Reports whether the actor wrote this annotation.
51
+ *
52
+ * The id is read first, and the address second. See `AnnotationActorView.email`
53
+ * for why one identity is not enough.
54
+ */
55
+ export declare function isAuthor(annotation: AnnotationDto, actor: AnnotationActorView): boolean;
56
+ /**
57
+ * Whether the actor can change the text of a note.
58
+ *
59
+ * Only the author. A note carries an opinion with a name on it, so a second
60
+ * person who edits it would put words in that person's mouth. Somebody who
61
+ * disagrees writes a note of their own, and both stay visible.
62
+ */
63
+ export declare function canEditNote(annotation: AnnotationDto, actor: AnnotationActorView): AnnotationDecision;
64
+ /**
65
+ * Whether the actor can remove an annotation.
66
+ *
67
+ * A NOTE follows `canEditNote`: the author, and nobody else.
68
+ *
69
+ * A TAG is different, and anybody can remove one. A tag is a grouping and not
70
+ * a statement, a wrong tag makes a search answer wrongly for everybody, and
71
+ * the person who can see the mistake is rarely the person who made it. The
72
+ * removal keeps the row, so who added it and who took it off both stay
73
+ * readable.
74
+ */
75
+ export declare function canRemove(annotation: AnnotationDto, actor: AnnotationActorView): AnnotationDecision;
76
+ /**
77
+ * Whether this text can be a note.
78
+ *
79
+ * The frontend runs it on every keystroke to hold the button, and the route
80
+ * runs it on the body that arrives. The Zod schema of the route holds the same
81
+ * two numbers, so a caller that skips the frontend still meets them.
82
+ */
83
+ export declare function checkNoteBody(body: string): AnnotationDecision;
84
+ /**
85
+ * Whether this text can be a tag name.
86
+ *
87
+ * The dictionary stores TWO values for one tag, so both are measured. The
88
+ * identity is `normalizeTagName(name)`, which the unique index holds, and the
89
+ * displayed name is `name.trim()`, which is what a reader sees on every badge.
90
+ * They are not the same length: normalizing turns a run of inner spaces into
91
+ * one hyphen, so `a<many spaces>b` is shorter after it than before. Measuring
92
+ * only the normalized value therefore let a display name through that is
93
+ * longer than the ceiling this function exists to hold.
94
+ *
95
+ * Emptiness is asked of the normalized value alone: a name of nothing but
96
+ * whitespace has no identity, whatever it looks like.
97
+ */
98
+ export declare function checkTagName(name: string): AnnotationDecision;
@@ -0,0 +1,61 @@
1
+ import { isSameActorEmail } from "../approvals/policy.js";
2
+ import {
3
+ ANNOTATION_BODY_MAX,
4
+ ANNOTATION_TAG_NAME_MAX,
5
+ normalizeTagName
6
+ } from "../dtos/Annotation.dto.js";
7
+ const ANNOTATION_DENIAL_MESSAGES = {
8
+ "not-author": "Only the person who wrote a note can change or remove it.",
9
+ "empty-body": "A note needs some text.",
10
+ "body-too-long": `A note holds at most ${ANNOTATION_BODY_MAX} characters.`,
11
+ "empty-tag": "A tag needs a name.",
12
+ "tag-too-long": `A tag name holds at most ${ANNOTATION_TAG_NAME_MAX} characters.`
13
+ };
14
+ const ALLOW = { ok: true };
15
+ function deny(code) {
16
+ return { ok: false, code, message: ANNOTATION_DENIAL_MESSAGES[code] };
17
+ }
18
+ function isAuthor(annotation, actor) {
19
+ if (annotation.createdBy.userId === actor.userId) {
20
+ return true;
21
+ }
22
+ return isSameActorEmail(annotation.createdBy.email, actor.email);
23
+ }
24
+ function canEditNote(annotation, actor) {
25
+ return isAuthor(annotation, actor) ? ALLOW : deny("not-author");
26
+ }
27
+ function canRemove(annotation, actor) {
28
+ if (annotation.kind === "tag") {
29
+ return ALLOW;
30
+ }
31
+ return canEditNote(annotation, actor);
32
+ }
33
+ function checkNoteBody(body) {
34
+ const trimmed = body.trim();
35
+ if (trimmed.length === 0) {
36
+ return deny("empty-body");
37
+ }
38
+ if (trimmed.length > ANNOTATION_BODY_MAX) {
39
+ return deny("body-too-long");
40
+ }
41
+ return ALLOW;
42
+ }
43
+ function checkTagName(name) {
44
+ const normalized = normalizeTagName(name);
45
+ if (normalized.length === 0) {
46
+ return deny("empty-tag");
47
+ }
48
+ if (normalized.length > ANNOTATION_TAG_NAME_MAX || name.trim().length > ANNOTATION_TAG_NAME_MAX) {
49
+ return deny("tag-too-long");
50
+ }
51
+ return ALLOW;
52
+ }
53
+ export {
54
+ ANNOTATION_DENIAL_MESSAGES,
55
+ canEditNote,
56
+ canRemove,
57
+ checkNoteBody,
58
+ checkTagName,
59
+ isAuthor
60
+ };
61
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/annotations/policy.ts"],
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport { isSameActorEmail } from '../approvals/policy.js'\nimport type { AnnotationDto } from '../dtos/Annotation.dto.js'\nimport {\n ANNOTATION_BODY_MAX,\n ANNOTATION_TAG_NAME_MAX,\n normalizeTagName,\n} from '../dtos/Annotation.dto.js'\n\n/**\n * The rules of the annotations, as pure functions.\n *\n * Two consumers run this file: control-backend decides whether to refuse a\n * write, and control-frontend decides whether to show the button. One copy\n * means the button and the route can never disagree.\n *\n * No function here does I/O or returns a promise, which is what keeps it safe\n * for a browser bundle and for a list of 200 rows.\n */\n\n/** Who is asking. The frontend has this from `useSession`. */\nexport interface AnnotationActorView {\n userId: string\n /**\n * The address of the actor. It is the SECOND identity of the author rule,\n * for the same reason as in the approvals: one person can hold two Auth0\n * identities with two different `sub` values, and the author of a note must\n * still be able to edit it after signing in through the other one.\n */\n email?: string\n}\n\n/**\n * Why a write was refused.\n *\n * The backend maps each code to a status, and the frontend shows the same\n * words. See `ANNOTATION_DENIAL_MESSAGES`.\n */\nexport type AnnotationDenialCode =\n | 'not-author'\n | 'empty-body'\n | 'body-too-long'\n | 'empty-tag'\n | 'tag-too-long'\n\n/**\n * What a person reads for each denial.\n *\n * The MESSAGE is here, and not in the error catalogue of the backend, because\n * the frontend shows it beside a disabled button before any request is sent.\n * Two spellings of one rule is the drift that this file exists to stop.\n */\nexport const ANNOTATION_DENIAL_MESSAGES: Record<AnnotationDenialCode, string> =\n {\n 'not-author': 'Only the person who wrote a note can change or remove it.',\n 'empty-body': 'A note needs some text.',\n 'body-too-long': `A note holds at most ${ANNOTATION_BODY_MAX} characters.`,\n 'empty-tag': 'A tag needs a name.',\n 'tag-too-long': `A tag name holds at most ${ANNOTATION_TAG_NAME_MAX} characters.`,\n }\n\n/** A rule that refused, with the code and the words for it. */\nexport interface AnnotationDenial {\n ok: false\n code: AnnotationDenialCode\n message: string\n}\n\n/** A rule that let the write through. */\nexport interface AnnotationAllowance {\n ok: true\n}\n\nexport type AnnotationDecision = AnnotationAllowance | AnnotationDenial\n\nconst ALLOW: AnnotationAllowance = { ok: true }\n\nfunction deny(code: AnnotationDenialCode): AnnotationDenial {\n return { ok: false, code, message: ANNOTATION_DENIAL_MESSAGES[code] }\n}\n\n/**\n * Reports whether the actor wrote this annotation.\n *\n * The id is read first, and the address second. See `AnnotationActorView.email`\n * for why one identity is not enough.\n */\nexport function isAuthor(\n annotation: AnnotationDto,\n actor: AnnotationActorView,\n): boolean {\n if (annotation.createdBy.userId === actor.userId) {\n return true\n }\n return isSameActorEmail(annotation.createdBy.email, actor.email)\n}\n\n/**\n * Whether the actor can change the text of a note.\n *\n * Only the author. A note carries an opinion with a name on it, so a second\n * person who edits it would put words in that person's mouth. Somebody who\n * disagrees writes a note of their own, and both stay visible.\n */\nexport function canEditNote(\n annotation: AnnotationDto,\n actor: AnnotationActorView,\n): AnnotationDecision {\n return isAuthor(annotation, actor) ? ALLOW : deny('not-author')\n}\n\n/**\n * Whether the actor can remove an annotation.\n *\n * A NOTE follows `canEditNote`: the author, and nobody else.\n *\n * A TAG is different, and anybody can remove one. A tag is a grouping and not\n * a statement, a wrong tag makes a search answer wrongly for everybody, and\n * the person who can see the mistake is rarely the person who made it. The\n * removal keeps the row, so who added it and who took it off both stay\n * readable.\n */\nexport function canRemove(\n annotation: AnnotationDto,\n actor: AnnotationActorView,\n): AnnotationDecision {\n if (annotation.kind === 'tag') {\n return ALLOW\n }\n return canEditNote(annotation, actor)\n}\n\n/**\n * Whether this text can be a note.\n *\n * The frontend runs it on every keystroke to hold the button, and the route\n * runs it on the body that arrives. The Zod schema of the route holds the same\n * two numbers, so a caller that skips the frontend still meets them.\n */\nexport function checkNoteBody(body: string): AnnotationDecision {\n const trimmed = body.trim()\n if (trimmed.length === 0) {\n return deny('empty-body')\n }\n if (trimmed.length > ANNOTATION_BODY_MAX) {\n return deny('body-too-long')\n }\n return ALLOW\n}\n\n/**\n * Whether this text can be a tag name.\n *\n * The dictionary stores TWO values for one tag, so both are measured. The\n * identity is `normalizeTagName(name)`, which the unique index holds, and the\n * displayed name is `name.trim()`, which is what a reader sees on every badge.\n * They are not the same length: normalizing turns a run of inner spaces into\n * one hyphen, so `a<many spaces>b` is shorter after it than before. Measuring\n * only the normalized value therefore let a display name through that is\n * longer than the ceiling this function exists to hold.\n *\n * Emptiness is asked of the normalized value alone: a name of nothing but\n * whitespace has no identity, whatever it looks like.\n */\nexport function checkTagName(name: string): AnnotationDecision {\n const normalized = normalizeTagName(name)\n if (normalized.length === 0) {\n return deny('empty-tag')\n }\n if (\n normalized.length > ANNOTATION_TAG_NAME_MAX ||\n name.trim().length > ANNOTATION_TAG_NAME_MAX\n ) {\n return deny('tag-too-long')\n }\n return ALLOW\n}\n"],
5
+ "mappings": "AAMA,SAAS,wBAAwB;AAEjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA6CA,MAAM,6BACX;AAAA,EACE,cAAc;AAAA,EACd,cAAc;AAAA,EACd,iBAAiB,wBAAwB,mBAAmB;AAAA,EAC5D,aAAa;AAAA,EACb,gBAAgB,4BAA4B,uBAAuB;AACrE;AAgBF,MAAM,QAA6B,EAAE,IAAI,KAAK;AAE9C,SAAS,KAAK,MAA8C;AAC1D,SAAO,EAAE,IAAI,OAAO,MAAM,SAAS,2BAA2B,IAAI,EAAE;AACtE;AAQO,SAAS,SACd,YACA,OACS;AACT,MAAI,WAAW,UAAU,WAAW,MAAM,QAAQ;AAChD,WAAO;AAAA,EACT;AACA,SAAO,iBAAiB,WAAW,UAAU,OAAO,MAAM,KAAK;AACjE;AASO,SAAS,YACd,YACA,OACoB;AACpB,SAAO,SAAS,YAAY,KAAK,IAAI,QAAQ,KAAK,YAAY;AAChE;AAaO,SAAS,UACd,YACA,OACoB;AACpB,MAAI,WAAW,SAAS,OAAO;AAC7B,WAAO;AAAA,EACT;AACA,SAAO,YAAY,YAAY,KAAK;AACtC;AASO,SAAS,cAAc,MAAkC;AAC9D,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,KAAK,YAAY;AAAA,EAC1B;AACA,MAAI,QAAQ,SAAS,qBAAqB;AACxC,WAAO,KAAK,eAAe;AAAA,EAC7B;AACA,SAAO;AACT;AAgBO,SAAS,aAAa,MAAkC;AAC7D,QAAM,aAAa,iBAAiB,IAAI;AACxC,MAAI,WAAW,WAAW,GAAG;AAC3B,WAAO,KAAK,WAAW;AAAA,EACzB;AACA,MACE,WAAW,SAAS,2BACpB,KAAK,KAAK,EAAE,SAAS,yBACrB;AACA,WAAO,KAAK,cAAc;AAAA,EAC5B;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The cluster bulk operation on the approval mechanism of control-backend.
3
+ *
4
+ * control-backend and control-frontend both read this file. The backend
5
+ * builds its six use cases from `CLUSTER_BULK_OPERATION_RULES`, and the form
6
+ * of the frontend reads the same table to run `canCreate` before the request
7
+ * goes out. A permission that only one side knew would be a button that the
8
+ * server then refuses.
9
+ *
10
+ * console-backend reads the step types at the end. control-backend calls one
11
+ * step for each cluster, and console-backend does the work of that step with
12
+ * the same code that its own bulk operation runs.
13
+ */
14
+ import { ClusterBulkOperationStatus, ClusterBulkOperationType } from '../enums/ClusterBulkOperation.enum.js';
15
+ import type { ClusterBulkOperationParameter } from '../types/ClusterBulkOperation.type.js';
16
+ /**
17
+ * The most clusters that one request holds.
18
+ *
19
+ * The payload is a json column, and the create route reads it in one body of
20
+ * at most 1 MiB. 3000 clusters with the longest names stay far below that.
21
+ * A larger set is two requests, which is also two decisions for the people
22
+ * who approve.
23
+ */
24
+ export declare const CLUSTER_BULK_OPERATION_MAX_CLUSTERS = 3000;
25
+ /** The longest name of an operation. The name is what the queue shows. */
26
+ export declare const CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH = 200;
27
+ /**
28
+ * The longest cluster name that the payload keeps.
29
+ *
30
+ * The name is a copy for a reader. The id alone decides what the step
31
+ * changes.
32
+ */
33
+ export declare const CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH = 100;
34
+ /**
35
+ * The wait between two sleeping clusters of one datacenter.
36
+ *
37
+ * The same number as the bulk operation of console-backend. A wake-up loads
38
+ * the datacenter, and many wake-ups together overload it. Datacenters do not
39
+ * wait for each other.
40
+ */
41
+ export declare const CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS = 60000;
42
+ /** What one operation type needs, and which use case carries it. */
43
+ export interface ClusterBulkOperationRule {
44
+ useCase: string;
45
+ label: string;
46
+ /** The permission that opens, approves and runs a request of this type. */
47
+ permission: string;
48
+ }
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 replaces `MINIMUM_EYEPAIRS` for the approval flow only. The old flow
56
+ * of console-backend still reads `MINIMUM_EYEPAIRS`.
57
+ */
58
+ export declare const CLUSTER_BULK_OPERATION_MIN_ACKS = 2;
59
+ /**
60
+ * The floor of approvals of a cluster bulk operation on dev, int and local.
61
+ *
62
+ * A test stage often has one other Camundi who can approve. One approval
63
+ * there still tests the whole flow.
64
+ */
65
+ export declare const CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS = 1;
66
+ /**
67
+ * The rules of each operation type.
68
+ *
69
+ * The permissions are the ones that the bulk operation of console-backend
70
+ * checks today (`getBulkOpRequiredPermission`), so a Camundi who can run one
71
+ * flow can run the other. `CLUSTERS_READALLCLUSTERINFO` is part of every
72
+ * type there, and `clusterBulkRequiredPermissions` adds it here.
73
+ */
74
+ export declare const CLUSTER_BULK_OPERATION_RULES: Record<ClusterBulkOperationType, ClusterBulkOperationRule>;
75
+ /** The operation types in the order that a form offers them. */
76
+ export declare const CLUSTER_BULK_OPERATION_TYPES: ClusterBulkOperationType[];
77
+ /** The operation type of a use case, or `undefined` for any other use case. */
78
+ export declare function clusterBulkOperationTypeOf(useCase: string): ClusterBulkOperationType | undefined;
79
+ /** One cluster of a request. */
80
+ export interface ClusterBulkTargetDto {
81
+ id: string;
82
+ /** A copy for a reader. The step reads the id alone. */
83
+ name?: string;
84
+ orgId?: string;
85
+ }
86
+ export interface ClusterBulkGenerationUpdateSettings {
87
+ targetGenerationId: string;
88
+ /** A copy for a reader. */
89
+ targetGenerationName?: string;
90
+ /** See `ClusterBulkOperationGenerationUpdateParameter.yoloAllClusters`. */
91
+ yoloAllClusters?: boolean;
92
+ /** See `ClusterBulkOperationGenerationUpdateParameter.yoloForceUpdate`. */
93
+ yoloForceUpdate?: boolean;
94
+ }
95
+ export interface ClusterBulkPlanUpdateSettings {
96
+ targetPlanId: string;
97
+ targetPlanName?: string;
98
+ /** Keeps the active modification of a cluster through the change. */
99
+ keepModification?: boolean;
100
+ }
101
+ /**
102
+ * No `keepModification`: the parameter of a plan type update in the old flow
103
+ * has none, and its dialog never sends it.
104
+ */
105
+ export interface ClusterBulkPlanTypeUpdateSettings {
106
+ targetPlanTypeId: string;
107
+ targetPlanTypeName?: string;
108
+ }
109
+ export interface ClusterBulkSleepWakeupSettings {
110
+ action: 'sleep' | 'wakeup';
111
+ }
112
+ /** The settings of each type. Deletion and spec update have none. */
113
+ export interface ClusterBulkSettingsByType {
114
+ [ClusterBulkOperationType.GENERATION_UPDATE]: ClusterBulkGenerationUpdateSettings;
115
+ [ClusterBulkOperationType.PLAN_UPDATE]: ClusterBulkPlanUpdateSettings;
116
+ [ClusterBulkOperationType.PLAN_TYPE_UPDATE]: ClusterBulkPlanTypeUpdateSettings;
117
+ [ClusterBulkOperationType.SLEEP_WAKEUP]: ClusterBulkSleepWakeupSettings;
118
+ [ClusterBulkOperationType.DELETION]: Record<string, never>;
119
+ [ClusterBulkOperationType.SPEC_UPDATE]: Record<string, never>;
120
+ }
121
+ /** The payload of a request, for one operation type. */
122
+ export interface ClusterBulkOperationPayloadDto<T extends ClusterBulkOperationType = ClusterBulkOperationType> {
123
+ name: string;
124
+ clusters: ClusterBulkTargetDto[];
125
+ settings: ClusterBulkSettingsByType[T];
126
+ }
127
+ /** True when the settings of a generation update go past a safety check. */
128
+ export declare function isClusterBulkYolo(type: ClusterBulkOperationType, settings: unknown): boolean;
129
+ /**
130
+ * Every permission that a request of this type and these settings needs.
131
+ *
132
+ * control-backend stores the answer on the request as
133
+ * `requiredPermissions`, and the form passes the same answer to `canCreate`.
134
+ * A yolo flag adds `CLUSTERS_CAN_UPDATE_GENERATION_YOLO`, which is the rule
135
+ * of `needsGenerationYolo` in console-backend.
136
+ */
137
+ export declare function clusterBulkRequiredPermissions(type: ClusterBulkOperationType, settings: unknown): string[];
138
+ /**
139
+ * Turns the settings of a request into the parameter of console-backend.
140
+ *
141
+ * The step route of console-backend reads the parameter shape of its own
142
+ * bulk operation, so one function in console-backend serves both flows.
143
+ * `nop` of the sleep action is not reachable from here on purpose: a request
144
+ * that changes nothing is not a request that people approve.
145
+ */
146
+ export declare function clusterBulkParameterOf(type: ClusterBulkOperationType, settings: unknown): ClusterBulkOperationParameter;
147
+ /** The outcome of one cluster, once its step finished. */
148
+ export type ClusterBulkFinalStatus = ClusterBulkOperationStatus.DONE | ClusterBulkOperationStatus.ERRORED | ClusterBulkOperationStatus.SKIPPED;
149
+ /**
150
+ * What a request records for one cluster, in `executionResult`.
151
+ *
152
+ * `executionResult` of a bulk request is an object with ONE key for each
153
+ * cluster that finished, keyed by the id of the cluster. A cluster with no
154
+ * key has not run yet. That is what lets an interrupted run skip the
155
+ * clusters that are finished, and what a reader shows as "not reached".
156
+ */
157
+ export interface ClusterBulkClusterResultDto {
158
+ result: ClusterBulkFinalStatus;
159
+ /** Why a cluster errored or was skipped, or a note on a done cluster. */
160
+ error?: string;
161
+ /** An ISO timestamp: when the step answered. */
162
+ at: string;
163
+ }
164
+ /** Reads the result of one cluster out of `executionResult`. */
165
+ export declare function clusterBulkResultOf(executionResult: Record<string, unknown> | undefined, clusterId: string): ClusterBulkClusterResultDto | undefined;
166
+ /**
167
+ * The body of the step route of console-backend.
168
+ *
169
+ * `api/admin/approvals/clusters/:clusterId/bulk-step`. One call does the work
170
+ * of one operation type on one cluster.
171
+ */
172
+ export interface ClusterBulkStepRequestDto {
173
+ type: ClusterBulkOperationType;
174
+ parameter: ClusterBulkOperationParameter;
175
+ /**
176
+ * When true, a sleeping cluster is NOT changed. The answer is `deferred`,
177
+ * with the datacenter of the cluster.
178
+ *
179
+ * This is how control-backend keeps the stagger of the old run: the first
180
+ * pass changes the awake clusters and learns which ones sleep, and a second
181
+ * pass changes the sleeping ones: one lane for each datacenter, the lanes
182
+ * side by side, and the clusters of one lane
183
+ * `CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS` apart.
184
+ */
185
+ deferIfSleeping: boolean;
186
+ }
187
+ /** The answer of the step route. */
188
+ export type ClusterBulkStepResultDto = {
189
+ outcome: 'finished';
190
+ result: ClusterBulkFinalStatus;
191
+ error?: string;
192
+ } | {
193
+ outcome: 'deferred';
194
+ /** The k8s context of the cluster, or `unknown`. */
195
+ datacenter: string;
196
+ };
@@ -0,0 +1,127 @@
1
+ import {
2
+ ClusterBulkOperationStatus,
3
+ ClusterBulkOperationType
4
+ } from "../enums/ClusterBulkOperation.enum.js";
5
+ import { ConsolePermissions } from "../permissions/console.permissions.js";
6
+ import { APPROVAL_USE_CASE_TYPES } from "./use-cases.const.js";
7
+ const CLUSTER_BULK_OPERATION_MAX_CLUSTERS = 3e3;
8
+ const CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH = 200;
9
+ const CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH = 100;
10
+ 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
+ const CLUSTER_BULK_OPERATION_RULES = {
14
+ [ClusterBulkOperationType.GENERATION_UPDATE]: {
15
+ useCase: APPROVAL_USE_CASE_TYPES.clusterBulkGenerationUpdate,
16
+ label: "Generation update",
17
+ permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_GENERATION
18
+ },
19
+ [ClusterBulkOperationType.PLAN_UPDATE]: {
20
+ useCase: APPROVAL_USE_CASE_TYPES.clusterBulkPlanUpdate,
21
+ label: "Plan update",
22
+ permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE
23
+ },
24
+ [ClusterBulkOperationType.PLAN_TYPE_UPDATE]: {
25
+ useCase: APPROVAL_USE_CASE_TYPES.clusterBulkPlanTypeUpdate,
26
+ label: "Plan type update",
27
+ permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE
28
+ },
29
+ [ClusterBulkOperationType.SLEEP_WAKEUP]: {
30
+ useCase: APPROVAL_USE_CASE_TYPES.clusterBulkSleepWakeup,
31
+ label: "Sleep / wake up",
32
+ permission: ConsolePermissions.CLUSTERS_CAN_SEND_TO_SLEEP
33
+ },
34
+ [ClusterBulkOperationType.DELETION]: {
35
+ useCase: APPROVAL_USE_CASE_TYPES.clusterBulkDeletion,
36
+ label: "Deletion",
37
+ permission: ConsolePermissions.CLUSTERS_DELETE_CLUSTER
38
+ },
39
+ [ClusterBulkOperationType.SPEC_UPDATE]: {
40
+ useCase: APPROVAL_USE_CASE_TYPES.clusterBulkSpecUpdate,
41
+ label: "Spec update",
42
+ permission: ConsolePermissions.CLUSTERS_CAN_UPDATE_CLUSTERPLANTYPE
43
+ }
44
+ };
45
+ const CLUSTER_BULK_OPERATION_TYPES = [
46
+ ClusterBulkOperationType.GENERATION_UPDATE,
47
+ ClusterBulkOperationType.PLAN_UPDATE,
48
+ ClusterBulkOperationType.PLAN_TYPE_UPDATE,
49
+ ClusterBulkOperationType.SLEEP_WAKEUP,
50
+ ClusterBulkOperationType.SPEC_UPDATE,
51
+ ClusterBulkOperationType.DELETION
52
+ ];
53
+ function clusterBulkOperationTypeOf(useCase) {
54
+ return CLUSTER_BULK_OPERATION_TYPES.find(
55
+ (type) => CLUSTER_BULK_OPERATION_RULES[type].useCase === useCase
56
+ );
57
+ }
58
+ function isClusterBulkYolo(type, settings) {
59
+ if (type !== ClusterBulkOperationType.GENERATION_UPDATE || typeof settings !== "object" || settings === null) {
60
+ return false;
61
+ }
62
+ const typed = settings;
63
+ return typed.yoloAllClusters === true || typed.yoloForceUpdate === true;
64
+ }
65
+ function clusterBulkRequiredPermissions(type, settings) {
66
+ const permissions = [
67
+ CLUSTER_BULK_OPERATION_RULES[type].permission,
68
+ ConsolePermissions.CLUSTERS_READALLCLUSTERINFO
69
+ ];
70
+ if (isClusterBulkYolo(type, settings)) {
71
+ permissions.push(ConsolePermissions.CLUSTERS_CAN_UPDATE_GENERATION_YOLO);
72
+ }
73
+ return permissions;
74
+ }
75
+ function clusterBulkParameterOf(type, settings) {
76
+ const values = settings ?? {};
77
+ switch (type) {
78
+ case ClusterBulkOperationType.GENERATION_UPDATE:
79
+ return {
80
+ toGenerationUuid: String(values.targetGenerationId),
81
+ yoloAllClusters: values.yoloAllClusters === true,
82
+ yoloForceUpdate: values.yoloForceUpdate === true
83
+ };
84
+ case ClusterBulkOperationType.PLAN_UPDATE:
85
+ return {
86
+ toPlanUuid: String(values.targetPlanId),
87
+ yolo: false,
88
+ keepModification: values.keepModification === true
89
+ };
90
+ case ClusterBulkOperationType.PLAN_TYPE_UPDATE:
91
+ return {
92
+ toPlanTypeUuid: String(values.targetPlanTypeId),
93
+ yolo: false
94
+ };
95
+ case ClusterBulkOperationType.SLEEP_WAKEUP:
96
+ return { toSleep: values.action === "wakeup" ? "wakeup" : "sleep" };
97
+ default:
98
+ return void 0;
99
+ }
100
+ }
101
+ function clusterBulkResultOf(executionResult, clusterId) {
102
+ const entry = executionResult?.[clusterId];
103
+ if (typeof entry !== "object" || entry === null) {
104
+ return void 0;
105
+ }
106
+ const result = entry.result;
107
+ if (result !== ClusterBulkOperationStatus.DONE && result !== ClusterBulkOperationStatus.ERRORED && result !== ClusterBulkOperationStatus.SKIPPED) {
108
+ return void 0;
109
+ }
110
+ return entry;
111
+ }
112
+ export {
113
+ CLUSTER_BULK_OPERATION_CLUSTER_NAME_MAX_LENGTH,
114
+ CLUSTER_BULK_OPERATION_MAX_CLUSTERS,
115
+ CLUSTER_BULK_OPERATION_MIN_ACKS,
116
+ CLUSTER_BULK_OPERATION_NAME_MAX_LENGTH,
117
+ CLUSTER_BULK_OPERATION_RULES,
118
+ CLUSTER_BULK_OPERATION_TEST_STAGE_MIN_ACKS,
119
+ CLUSTER_BULK_OPERATION_TYPES,
120
+ CLUSTER_BULK_SLEEPING_CLUSTER_DELAY_MS,
121
+ clusterBulkOperationTypeOf,
122
+ clusterBulkParameterOf,
123
+ clusterBulkRequiredPermissions,
124
+ clusterBulkResultOf,
125
+ isClusterBulkYolo
126
+ };
127
+ //# sourceMappingURL=cluster-bulk-operation.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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 replaces `MINIMUM_EYEPAIRS` for the approval flow only. The old flow\n * of console-backend still reads `MINIMUM_EYEPAIRS`.\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\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 }\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
+ "names": []
7
+ }
@@ -38,6 +38,16 @@ export interface ApprovalActorView {
38
38
  email?: string;
39
39
  has(permission: string): boolean;
40
40
  }
41
+ /**
42
+ * Builds the `has` of an actor from the permissions that the actor holds.
43
+ *
44
+ * `ConsolePermissions.APPROVALS_ALL_USE_CASES` stands in for every permission
45
+ * that a use case asks for. Both actor builders call this function, the one of
46
+ * control-backend and the one of the browser, so the button and the route
47
+ * agree on it. A rule that reads `actor.has` needs no change for it, and nor
48
+ * does a use case that a later change adds.
49
+ */
50
+ export declare function approvalPermissionCheck(held: ReadonlySet<string>): (permission: string) => boolean;
41
51
  /**
42
52
  * Reports whether two email addresses name one person.
43
53
  *
@@ -50,6 +60,19 @@ export interface ApprovalActorView {
50
60
  * are one mailbox and must not pass as two people.
51
61
  */
52
62
  export declare function isSameActorEmail(one?: string, other?: string): boolean;
63
+ /**
64
+ * Reports whether one identity of a request names the actor.
65
+ *
66
+ * The `sub` of the token OR the email, because one person can hold more than
67
+ * one Auth0 identity. Every rule that asks "is this the same human?" reads both
68
+ * through this function, so a rule cannot forget the second half: `canWithdraw`
69
+ * did, and it refused a creator whose browser and whose service resolved the
70
+ * id from two different tokens.
71
+ */
72
+ export declare function isSameActor(identity: {
73
+ userId: string;
74
+ email?: string;
75
+ }, actor: ApprovalActorView): boolean;
53
76
  /**
54
77
  * Reports whether this actor carries an address that names a person.
55
78
  *
@@ -65,7 +88,7 @@ export declare function hasActorEmail(actor: ApprovalActorView): boolean;
65
88
  * frontend can branch on it. The message is for a person, and it is shown as
66
89
  * written.
67
90
  */
68
- export type ApprovalDenialCode = 'forbidden' | 'not-pending' | 'self-approval' | 'already-acknowledged' | 'expired' | 'not-creator' | 'not-executable' | 'no-reason' | 'too-few-acks' | 'no-creator-email';
91
+ export type ApprovalDenialCode = 'forbidden' | 'not-pending' | 'self-approval' | 'self-rejection' | 'already-acknowledged' | 'expired' | 'not-creator' | 'not-executable' | 'no-reason' | 'too-few-acks' | 'no-creator-email';
69
92
  export type ApprovalDecision = {
70
93
  allowed: true;
71
94
  } | {
@@ -168,12 +191,29 @@ export declare function canAcknowledge(job: ApprovalJobDto, actor: ApprovalActor
168
191
  /**
169
192
  * A rejection needs the same permission as an acknowledgement, and a reason.
170
193
  *
171
- * The creator MAY reject their own request. That is a withdrawal with a reason,
172
- * and stopping it would only push the person to the withdraw button, where the
173
- * reason is lost.
194
+ * Two people answer a request, and each one has ONE verb. Somebody else
195
+ * approves or rejects it; the person who opened it withdraws it. The creator is
196
+ * refused here for the same reason `canAcknowledge` refuses them: one person
197
+ * must not be both sides of a four-eyes decision. The message names the button
198
+ * that they do have, and the trail loses nothing, because a withdrawal writes
199
+ * its reason into the same field.
200
+ *
201
+ * The ORDER of the checks is the order that a person meets them, and the state
202
+ * of the request comes before the reason. A closed request must answer
203
+ * "already closed" whatever the caller typed: with the reason first, a page
204
+ * that had no reason yet read `no-reason` from a rejected request, and the
205
+ * Reject button of control-frontend stayed enabled on a request that nothing
206
+ * could reject any more.
174
207
  */
175
208
  export declare function canReject(job: ApprovalJobDto, actor: ApprovalActorView, now: Date, reason: string): ApprovalDecision;
176
- /** Only the person who opened a request can take it back. */
209
+ /**
210
+ * Only the person who opened a request can take it back.
211
+ *
212
+ * BOTH identities, as every other rule of this file reads them. One person can
213
+ * hold two Auth0 identities with two different `sub` values, and a rule that
214
+ * read the id alone refused the creator their own withdrawal as soon as the
215
+ * browser and the service resolved that id from two different tokens.
216
+ */
177
217
  export declare function canWithdraw(job: ApprovalJobDto, actor: ApprovalActorView, now: Date): ApprovalDecision;
178
218
  /**
179
219
  * Runs a request that has its approvals.