@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.
- package/dist/_tests/approvals/approval-rules.spec.d.ts +1 -0
- package/dist/_tests/approvals/llm-budget-top-up.spec.d.ts +1 -0
- package/dist/_tests/types/SalesPlanType.type.spec.d.ts +1 -0
- package/dist/agent-tokens/agent-tokens.d.ts +62 -3
- package/dist/agent-tokens/agent-tokens.js +8 -0
- package/dist/agent-tokens/agent-tokens.js.map +2 -2
- package/dist/approvals/approval-rules.d.ts +172 -0
- package/dist/approvals/approval-rules.js +71 -0
- package/dist/approvals/approval-rules.js.map +7 -0
- package/dist/approvals/cluster-bulk-operation.d.ts +5 -20
- package/dist/approvals/cluster-bulk-operation.js +3 -5
- package/dist/approvals/cluster-bulk-operation.js.map +2 -2
- package/dist/approvals/delete-cluster-corpses.d.ts +0 -15
- package/dist/approvals/delete-cluster-corpses.js +1 -7
- package/dist/approvals/delete-cluster-corpses.js.map +2 -2
- package/dist/approvals/edit-generation.d.ts +0 -8
- package/dist/approvals/edit-generation.js +0 -2
- package/dist/approvals/edit-generation.js.map +2 -2
- package/dist/approvals/force-delete-cluster-plan-type.d.ts +0 -18
- package/dist/approvals/force-delete-cluster-plan-type.js +1 -7
- package/dist/approvals/force-delete-cluster-plan-type.js.map +2 -2
- package/dist/approvals/policy.d.ts +94 -7
- package/dist/approvals/policy.js +125 -6
- package/dist/approvals/policy.js.map +2 -2
- package/dist/approvals/use-cases.const.d.ts +36 -6
- package/dist/approvals/use-cases.const.js +48 -16
- package/dist/approvals/use-cases.const.js.map +2 -2
- package/dist/constants/direct-route-lock.const.d.ts +36 -0
- package/dist/constants/direct-route-lock.const.js +21 -0
- package/dist/constants/direct-route-lock.const.js.map +7 -0
- package/dist/constants/misc.const.d.ts +38 -42
- package/dist/constants/misc.const.js +27 -28
- package/dist/constants/misc.const.js.map +2 -2
- package/dist/dtos/ApprovalJob.dto.d.ts +69 -5
- package/dist/dtos/ApprovalJob.dto.js +2 -0
- package/dist/dtos/ApprovalJob.dto.js.map +2 -2
- package/dist/dtos/ApprovalSummaryNames.dto.d.ts +37 -0
- package/dist/dtos/ApprovalSummaryNames.dto.js +1 -0
- package/dist/dtos/ApprovalSummaryNames.dto.js.map +7 -0
- package/dist/index.d.ts +15 -11
- package/dist/index.js +59 -27
- package/dist/index.js.map +2 -2
- package/dist/permissions/combo.js +3 -0
- package/dist/permissions/combo.js.map +2 -2
- package/dist/permissions/console.permissions.d.ts +17 -0
- package/dist/permissions/console.permissions.js +15 -1
- package/dist/permissions/console.permissions.js.map +2 -2
- package/dist/types/SalesPlanType.type.d.ts +5 -0
- package/dist/types/SalesPlanType.type.js +7 -0
- package/dist/types/SalesPlanType.type.js.map +2 -2
- package/dist/utils/user.utils.js +1 -1
- package/dist/utils/user.utils.js.map +2 -2
- package/dist/utils/user.utils.spec.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
const DELETE_CLUSTER_CORPSES_FLAG = "control.deleteClusterCorpses";
|
|
2
1
|
const CLUSTER_CORPSE_KINDS = ["db", "k8s"];
|
|
3
2
|
const DELETE_CLUSTER_CORPSES_MAX = 500;
|
|
4
3
|
const CLUSTER_CORPSE_MIN_AGE_MS = 60 * 60 * 1e3;
|
|
5
|
-
const DELETE_CLUSTER_CORPSES_MIN_ACKS_PROD = 2;
|
|
6
|
-
const DELETE_CLUSTER_CORPSES_MIN_ACKS_TEST_STAGE = 1;
|
|
7
4
|
export {
|
|
8
5
|
CLUSTER_CORPSE_KINDS,
|
|
9
6
|
CLUSTER_CORPSE_MIN_AGE_MS,
|
|
10
|
-
|
|
11
|
-
DELETE_CLUSTER_CORPSES_MAX,
|
|
12
|
-
DELETE_CLUSTER_CORPSES_MIN_ACKS_PROD,
|
|
13
|
-
DELETE_CLUSTER_CORPSES_MIN_ACKS_TEST_STAGE
|
|
7
|
+
DELETE_CLUSTER_CORPSES_MAX
|
|
14
8
|
};
|
|
15
9
|
//# sourceMappingURL=delete-cluster-corpses.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/approvals/delete-cluster-corpses.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
|
|
5
|
-
"mappings": "
|
|
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 two kinds of cluster corpse.\n *\n * - `db`: a cluster row of console with no ZeebeCluster resource in\n * Kubernetes. The delete removes the row and what belongs to it.\n * - `k8s`: a ZeebeCluster resource in Kubernetes with no cluster row of\n * console. The delete removes the resource.\n */\nexport const CLUSTER_CORPSE_KINDS = ['db', 'k8s'] as const\n\nexport type ClusterCorpseKind = (typeof CLUSTER_CORPSE_KINDS)[number]\n\n/**\n * The most corpses that one request deletes.\n *\n * The request lists each corpse, so that the approvers read what they delete.\n * When there are more, the request takes the oldest ones, and a later request\n * takes the rest.\n */\nexport const DELETE_CLUSTER_CORPSES_MAX = 500\n\n/**\n * The youngest corpse that a request takes, in milliseconds.\n *\n * Console creates the Kubernetes resource of a new cluster before it commits\n * the row, and its watch of Kubernetes sees a new resource some time later.\n * So a cluster that is being created can look like either kind of corpse for\n * a short time.\n */\nexport const CLUSTER_CORPSE_MIN_AGE_MS = 60 * 60 * 1000\n\n/** One corpse, as console answers it to control-backend. */\nexport interface ClusterCorpseDto {\n /** The uuid of the cluster row, or the name of the Kubernetes resource. */\n clusterId: string\n orgId: string\n /** The name of the cluster. Only a `db` corpse has one. */\n name?: string\n /**\n * The Kubernetes context. A `k8s` corpse always has one. A `db` corpse has\n * the context of its row, when the row names one.\n */\n k8sContextUuid?: string\n /**\n * The `metadata.uid` of the resource of a `k8s` corpse. Console deletes the\n * resource only while it has this uid, so a new resource with the same name\n * stays. A `k8s` corpse without it never goes into a request.\n */\n resourceUid?: string\n /**\n * When the row or the resource was created, as an ISO 8601 string. A\n * corpse without it is never old enough for a request.\n */\n created?: string\n}\n\n/**\n * The corpses of one kind now. Console answers it on\n * `GET api/admin/approvals/cluster-corpses/:kind`.\n */\nexport interface ClusterCorpsesDto {\n kind: ClusterCorpseKind\n corpses: ClusterCorpseDto[]\n}\n\n/**\n * What console did with one corpse of a request.\n *\n * Console reads the live source again before it deletes. `not-a-corpse` means\n * that the cluster is not a corpse any more: a `db` corpse has a resource in\n * Kubernetes now, or a `k8s` corpse has a row now. `gone` means that there was\n * nothing to delete: the row or the resource does not exist, or the resource\n * of a `k8s` corpse has another uid now. Console deletes nothing for both.\n */\nexport interface DeleteClusterCorpseResult {\n outcome: 'deleted' | 'gone' | 'not-a-corpse'\n}\n"],
|
|
5
|
+
"mappings": "AAcO,MAAM,uBAAuB,CAAC,MAAM,KAAK;AAWzC,MAAM,6BAA6B;AAUnC,MAAM,4BAA4B,KAAK,KAAK;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import type { GenerationEnvVarsKey } from '../types/C8ClusterComponentVersions.type.js';
|
|
2
|
-
/**
|
|
3
|
-
* The flag of the `edit-generation` use case.
|
|
4
|
-
*
|
|
5
|
-
* control-backend opens a request only while it is on. While it is on, the
|
|
6
|
-
* Edit dialog of a generation in Control saves the name and the env vars
|
|
7
|
-
* through a request.
|
|
8
|
-
*/
|
|
9
|
-
export declare const EDIT_GENERATION_FLAG = "control.editGeneration";
|
|
10
2
|
/** The longest name that an `edit-generation` request writes. */
|
|
11
3
|
export declare const EDIT_GENERATION_NAME_MAX_LENGTH = 200;
|
|
12
4
|
/**
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
const EDIT_GENERATION_FLAG = "control.editGeneration";
|
|
2
1
|
const EDIT_GENERATION_NAME_MAX_LENGTH = 200;
|
|
3
2
|
const EDIT_GENERATION_ENV_VARS_MAX_LENGTH = 2e4;
|
|
4
3
|
const EDIT_GENERATION_EXPECTED_MAX_LENGTH = 1048576;
|
|
5
4
|
export {
|
|
6
5
|
EDIT_GENERATION_ENV_VARS_MAX_LENGTH,
|
|
7
6
|
EDIT_GENERATION_EXPECTED_MAX_LENGTH,
|
|
8
|
-
EDIT_GENERATION_FLAG,
|
|
9
7
|
EDIT_GENERATION_NAME_MAX_LENGTH
|
|
10
8
|
};
|
|
11
9
|
//# sourceMappingURL=edit-generation.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/approvals/edit-generation.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 { GenerationEnvVarsKey } from '../types/C8ClusterComponentVersions.type.js'\n\n
|
|
5
|
-
"mappings": "
|
|
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 { GenerationEnvVarsKey } from '../types/C8ClusterComponentVersions.type.js'\n\n/** The longest name that an `edit-generation` request writes. */\nexport const EDIT_GENERATION_NAME_MAX_LENGTH = 200\n\n/**\n * The longest env var text of one field in an `edit-generation` request.\n *\n * A request holds the text before and after for each of the 8 fields, thus\n * 320,000 characters at most. Plain env var text (`KEY=value` lines) stays\n * under the body limit of control-backend and of console (1 MiB each). A\n * text with many characters that JSON escapes can go over it, and the\n * backend then refuses the request.\n */\nexport const EDIT_GENERATION_ENV_VARS_MAX_LENGTH = 20_000\n\n/**\n * The longest value that an `edit-generation` request carries as the value\n * that it read (`expectedName` and `expectedEnvVars`).\n *\n * The limits of a new value do not apply to it. A generation can hold a\n * value from before those limits, and a request must still be able to\n * change that value, or another field beside it. Thus this number is the\n * body limit of control-backend and of console (1 MiB), and the body limit\n * is the true bound.\n */\nexport const EDIT_GENERATION_EXPECTED_MAX_LENGTH = 1_048_576\n\n/**\n * What console-backend answers when an approved request edited ONE\n * generation (`PATCH api/admin/approvals/generations/:generationId/edit`).\n *\n * It holds no env var text. control-backend keeps it in the result of the\n * request.\n */\nexport interface EditGenerationResult {\n generationId: string\n /** The name of the generation after the call. */\n name: string\n /**\n * The clusters of the generation that console sent the spec to. Zero when\n * the generation already held every requested value and no earlier call\n * of the request wrote it: console then wrote nothing and sent nothing.\n */\n clusterCount: number\n /**\n * The clusters of `clusterCount` that refused the spec: Kubernetes read\n * the write and said no. They run the old spec, and a second call meets\n * the same answer.\n */\n rejectedClusters: number\n /**\n * The clusters of `clusterCount` whose write failed before or after\n * Kubernetes read it. A second call sends the spec again.\n */\n failedClusters: number\n /**\n * True when the generation already held every requested value because an\n * earlier call of the same request wrote it, and console sent the spec to\n * the clusters again instead of writing.\n */\n reapplied: boolean\n /**\n * The env var fields that this call changed. Only the keys, never the\n * text. Empty when the call wrote no env var.\n */\n changedEnvVars: GenerationEnvVarsKey[]\n}\n"],
|
|
5
|
+
"mappings": "AASO,MAAM,kCAAkC;AAWxC,MAAM,sCAAsC;AAY5C,MAAM,sCAAsC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The flag of the `force-delete-cluster-plan-type` use case.
|
|
3
|
-
*
|
|
4
|
-
* Three services read it. control-backend opens a request only while it is
|
|
5
|
-
* on. Control shows the request in place of the direct force delete. Console
|
|
6
|
-
* refuses the direct force delete route to a person only while
|
|
7
|
-
* `control.lockDirectRoutes` is on too. While the lock is off, a person who
|
|
8
|
-
* holds `cptfd` can still call that route and skip the approvals.
|
|
9
|
-
*/
|
|
10
|
-
export declare const FORCE_DELETE_CLUSTER_PLAN_TYPE_FLAG = "control.forceDeleteClusterPlanType";
|
|
11
1
|
/**
|
|
12
2
|
* The most clusters that one force delete request can delete.
|
|
13
3
|
*
|
|
@@ -15,14 +5,6 @@ export declare const FORCE_DELETE_CLUSTER_PLAN_TYPE_FLAG = "control.forceDeleteC
|
|
|
15
5
|
* delete. A plan type with more clusters needs a cluster bulk deletion first.
|
|
16
6
|
*/
|
|
17
7
|
export declare const FORCE_DELETE_CLUSTER_PLAN_TYPE_MAX_CLUSTERS = 500;
|
|
18
|
-
/**
|
|
19
|
-
* The approvals of a force delete on prod: two people other than the person
|
|
20
|
-
* who asks. The request deletes customer clusters of many organizations, the
|
|
21
|
-
* same harm as a cluster bulk deletion (`CLUSTER_BULK_OPERATION_MIN_ACKS`).
|
|
22
|
-
*/
|
|
23
|
-
export declare const FORCE_DELETE_CLUSTER_PLAN_TYPE_MIN_ACKS_PROD = 2;
|
|
24
|
-
/** The approvals of a force delete on dev, int and local. */
|
|
25
|
-
export declare const FORCE_DELETE_CLUSTER_PLAN_TYPE_MIN_ACKS_TEST_STAGE = 1;
|
|
26
8
|
/** One cluster of a plan type, as console answers it to control-backend. */
|
|
27
9
|
export interface ClusterPlanTypeClusterDto {
|
|
28
10
|
id: string;
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
const FORCE_DELETE_CLUSTER_PLAN_TYPE_FLAG = "control.forceDeleteClusterPlanType";
|
|
2
1
|
const FORCE_DELETE_CLUSTER_PLAN_TYPE_MAX_CLUSTERS = 500;
|
|
3
|
-
const FORCE_DELETE_CLUSTER_PLAN_TYPE_MIN_ACKS_PROD = 2;
|
|
4
|
-
const FORCE_DELETE_CLUSTER_PLAN_TYPE_MIN_ACKS_TEST_STAGE = 1;
|
|
5
2
|
export {
|
|
6
|
-
|
|
7
|
-
FORCE_DELETE_CLUSTER_PLAN_TYPE_MAX_CLUSTERS,
|
|
8
|
-
FORCE_DELETE_CLUSTER_PLAN_TYPE_MIN_ACKS_PROD,
|
|
9
|
-
FORCE_DELETE_CLUSTER_PLAN_TYPE_MIN_ACKS_TEST_STAGE
|
|
3
|
+
FORCE_DELETE_CLUSTER_PLAN_TYPE_MAX_CLUSTERS
|
|
10
4
|
};
|
|
11
5
|
//# sourceMappingURL=force-delete-cluster-plan-type.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/approvals/force-delete-cluster-plan-type.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
|
|
5
|
-
"mappings": "
|
|
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 most clusters that one force delete request can delete.\n *\n * The request lists each cluster, so that the approvers read what they\n * delete. A plan type with more clusters needs a cluster bulk deletion first.\n */\nexport const FORCE_DELETE_CLUSTER_PLAN_TYPE_MAX_CLUSTERS = 500\n\n/** One cluster of a plan type, as console answers it to control-backend. */\nexport interface ClusterPlanTypeClusterDto {\n id: string\n name: string\n orgId: string\n}\n\n/**\n * The clusters of a plan type now. Console answers it on\n * `GET api/admin/approvals/cluster-plan-types/:planTypeId/clusters`.\n */\nexport interface ClusterPlanTypeClustersDto {\n planTypeId: string\n planTypeName: string\n clusters: ClusterPlanTypeClusterDto[]\n}\n\n/**\n * What console did with one step of a force delete.\n *\n * `gone` means that there was nothing to delete: the cluster or the plan type\n * does not exist, or the cluster moved to another plan type. A second run\n * then reads `gone` for a step that the first run finished.\n */\nexport interface ForceDeleteClusterPlanTypeStepResult {\n outcome: 'deleted' | 'gone'\n}\n"],
|
|
5
|
+
"mappings": "AAYO,MAAM,8CAA8C;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApprovalJobDto, ApprovalUseCaseDto } from '../dtos/ApprovalJob.dto.js';
|
|
1
|
+
import type { ApprovalJobDto, ApprovalOverrideGrantDto, ApprovalUseCaseDto } from '../dtos/ApprovalJob.dto.js';
|
|
2
2
|
/**
|
|
3
3
|
* The rules of the approval mechanism, as pure functions.
|
|
4
4
|
*
|
|
@@ -88,7 +88,7 @@ export declare function hasActorEmail(actor: ApprovalActorView): boolean;
|
|
|
88
88
|
* frontend can branch on it. The message is for a person, and it is shown as
|
|
89
89
|
* written.
|
|
90
90
|
*/
|
|
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';
|
|
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' | 'no-override-grant' | 'not-overridable';
|
|
92
92
|
export type ApprovalDecision = {
|
|
93
93
|
allowed: true;
|
|
94
94
|
} | {
|
|
@@ -148,6 +148,26 @@ export declare function requiredPermissionsOf(job: ApprovalJobDto): string[];
|
|
|
148
148
|
* the same page that a person is allowed to read.
|
|
149
149
|
*/
|
|
150
150
|
export declare function canView(job: ApprovalJobDto, actor: ApprovalActorView, useCase: ApprovalUseCaseDto): ApprovalDecision;
|
|
151
|
+
/**
|
|
152
|
+
* The person that a request of the two override use cases names, or
|
|
153
|
+
* `undefined` for every other request.
|
|
154
|
+
*
|
|
155
|
+
* Four rules read it. The grantee cannot approve their own grant, also not
|
|
156
|
+
* when they open it (`opensWithOwnApproval`), the grantee can read both
|
|
157
|
+
* requests, and the grantee can end their own grant before its time.
|
|
158
|
+
*/
|
|
159
|
+
export declare function overrideGranteeOf(job: Pick<ApprovalJobDto, 'type' | 'payload'>): {
|
|
160
|
+
userId: string;
|
|
161
|
+
email?: string;
|
|
162
|
+
} | undefined;
|
|
163
|
+
/**
|
|
164
|
+
* Reports whether an approval override grant lets its holder act now.
|
|
165
|
+
*
|
|
166
|
+
* It fails closed: an absent grant, a revoked one, and a date that does not
|
|
167
|
+
* parse are all inactive. control-backend reads the grant from its table at
|
|
168
|
+
* every call. A token never carries it.
|
|
169
|
+
*/
|
|
170
|
+
export declare function isOverrideGrantActive(grant: Pick<ApprovalOverrideGrantDto, 'expiresAt' | 'revokedAt'> | undefined, now: Date): boolean;
|
|
151
171
|
/**
|
|
152
172
|
* Who can OPEN a request, and with what.
|
|
153
173
|
*
|
|
@@ -171,6 +191,30 @@ export declare function canCreate(actor: ApprovalActorView, useCase: ApprovalUse
|
|
|
171
191
|
*/
|
|
172
192
|
requiredPermissions?: string[];
|
|
173
193
|
}): ApprovalDecision;
|
|
194
|
+
/**
|
|
195
|
+
* Reports whether the opener approves a new request when it is created.
|
|
196
|
+
*
|
|
197
|
+
* The opener approves when the request needs an approval and the opener holds
|
|
198
|
+
* every run permission of it. `APPROVAL_RULES` counts this approval. A request
|
|
199
|
+
* that needs no approval gets none, so its `approvedBy` stays empty.
|
|
200
|
+
*
|
|
201
|
+
* When `request` is given, the grantee of an approval override grant does not
|
|
202
|
+
* approve the grant that they open for themselves, as `canAcknowledge`
|
|
203
|
+
* refuses their approval later. Every approval of such a request then comes
|
|
204
|
+
* from another person.
|
|
205
|
+
*/
|
|
206
|
+
export declare function opensWithOwnApproval(actor: ApprovalActorView, requiredAcks: number, requiredPermissions: string[], request?: Pick<ApprovalJobDto, 'type' | 'payload'>): boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Reports whether a new request needs a reason.
|
|
209
|
+
*
|
|
210
|
+
* A request needs no reason when its opener holds every run permission and
|
|
211
|
+
* needs no second person. Nobody else reads the reason before the action
|
|
212
|
+
* runs, and the audit record names the opener.
|
|
213
|
+
*/
|
|
214
|
+
export declare function needsReason(actor: ApprovalActorView, useCase: ApprovalUseCaseDto, request?: {
|
|
215
|
+
requiredAcks?: number;
|
|
216
|
+
requiredPermissions?: string[];
|
|
217
|
+
}): boolean;
|
|
174
218
|
/**
|
|
175
219
|
* The approvals that a new request asks for.
|
|
176
220
|
*
|
|
@@ -229,9 +273,11 @@ export declare function canWithdraw(job: ApprovalJobDto, actor: ApprovalActorVie
|
|
|
229
273
|
*
|
|
230
274
|
* ANY holder of the permission may press it, and not only the person who
|
|
231
275
|
* opened the request. The decision was made when the request reached
|
|
232
|
-
* `approved
|
|
233
|
-
*
|
|
234
|
-
*
|
|
276
|
+
* `approved`, when the row got all the approvals that the request asks for.
|
|
277
|
+
* One of them can be the approval of the opener at create (see
|
|
278
|
+
* `opensWithOwnApproval`), and `canAcknowledge` keeps each person to one
|
|
279
|
+
* approval. The button only carries out what these people already agreed to.
|
|
280
|
+
* Holding the button to the creator would let one Camundi
|
|
235
281
|
* on holiday freeze an approved action, and a use case that must not run
|
|
236
282
|
* without its author has `executionMode: 'auto'` instead.
|
|
237
283
|
*
|
|
@@ -241,6 +287,45 @@ export declare function canWithdraw(job: ApprovalJobDto, actor: ApprovalActorVie
|
|
|
241
287
|
* `onBehalfOf` of `extend-trial`.
|
|
242
288
|
*/
|
|
243
289
|
export declare function canExecute(job: ApprovalJobDto, actor: ApprovalActorView, now?: Date): ApprovalDecision;
|
|
290
|
+
/**
|
|
291
|
+
* Why a request that an override approved can no longer run: the grant that
|
|
292
|
+
* the override used ended. The revoke of a grant writes it as `closedReason`
|
|
293
|
+
* on each such request, and `canRunOverridden` answers it.
|
|
294
|
+
*/
|
|
295
|
+
export declare const OVERRIDE_GRANT_ENDED_MESSAGE = "The approval override grant that ran this request ended. Open a new request.";
|
|
296
|
+
/**
|
|
297
|
+
* Reports whether a request that an override ran can still run.
|
|
298
|
+
*
|
|
299
|
+
* An override of a request in the explicit mode moves it to `approved`, and a
|
|
300
|
+
* person runs it later. A failed run can also run again. The grant is the only
|
|
301
|
+
* thing that stood in for the approvals, so each run needs it to be active
|
|
302
|
+
* still: a revoke or the end of the grant stops the request too.
|
|
303
|
+
*
|
|
304
|
+
* `grant` is the grant that the override used (`job.overrideGrantId`), which
|
|
305
|
+
* the caller reads from the grant table at the run. A later grant of the same
|
|
306
|
+
* person does not count: a revoke ends the requests that its grant ran, and a
|
|
307
|
+
* new grant must not bring them back. A request with no override is not
|
|
308
|
+
* affected. A type that joined `APPROVAL_OVERRIDE_DENYLIST` after the
|
|
309
|
+
* override needs its approvals, as `canOverride` says.
|
|
310
|
+
*/
|
|
311
|
+
export declare function canRunOverridden(job: Pick<ApprovalJobDto, 'type' | 'overriddenBy'>, grant: Pick<ApprovalOverrideGrantDto, 'expiresAt' | 'revokedAt'> | undefined, now: Date): ApprovalDecision;
|
|
312
|
+
/**
|
|
313
|
+
* Runs a `pending` request with an approval override grant, in place of the
|
|
314
|
+
* approvals that it still needs.
|
|
315
|
+
*
|
|
316
|
+
* `grant` is the active grant of the actor. The caller reads it from the
|
|
317
|
+
* grant table at every call, and never from a token: a token keeps its
|
|
318
|
+
* permissions until it expires, and a revoked grant must stop at once.
|
|
319
|
+
*
|
|
320
|
+
* The creator may override their own request, because that is what a grant
|
|
321
|
+
* is for. The actor still needs the permissions of the request: a grant
|
|
322
|
+
* takes the place of the second person, and not of the permission. A use case
|
|
323
|
+
* in `APPROVAL_OVERRIDE_DENYLIST` always needs its approvals.
|
|
324
|
+
*/
|
|
325
|
+
export declare function canOverride(job: ApprovalJobDto, actor: ApprovalActorView, now: Date, args: {
|
|
326
|
+
grant?: Pick<ApprovalOverrideGrantDto, 'userId' | 'email' | 'expiresAt' | 'revokedAt'>;
|
|
327
|
+
reason: string;
|
|
328
|
+
}): ApprovalDecision;
|
|
244
329
|
/**
|
|
245
330
|
* How many days a request can run after its last approval.
|
|
246
331
|
*
|
|
@@ -256,8 +341,10 @@ export declare const EXECUTE_WINDOW_DAYS = 7;
|
|
|
256
341
|
/**
|
|
257
342
|
* Reports the end of the run window of a request.
|
|
258
343
|
*
|
|
259
|
-
* The window starts at the last
|
|
260
|
-
*
|
|
344
|
+
* The window starts at the last decision that let the request run: the last
|
|
345
|
+
* approval, the override (`overriddenAt`), or the time of a `scheduled`
|
|
346
|
+
* request (`runAt`). A use case that needs zero approvals has none of them,
|
|
347
|
+
* so its window starts when the request was opened.
|
|
261
348
|
*/
|
|
262
349
|
export declare function runWindowEnd(job: ApprovalJobDto): Date;
|
|
263
350
|
/**
|
package/dist/approvals/policy.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { UNKNOWN_ACTOR_EMAIL } from "../constants/misc.const.js";
|
|
2
2
|
import { ConsolePermissions } from "../permissions/console.permissions.js";
|
|
3
|
+
import {
|
|
4
|
+
APPROVAL_USE_CASE_TYPES,
|
|
5
|
+
isApprovalOverridable
|
|
6
|
+
} from "./use-cases.const.js";
|
|
3
7
|
function approvalPermissionCheck(held) {
|
|
4
8
|
const all = held.has(ConsolePermissions.APPROVALS_ALL_USE_CASES);
|
|
5
9
|
return (permission) => all || held.has(permission);
|
|
@@ -41,11 +45,34 @@ function requiredPermissionsOf(job) {
|
|
|
41
45
|
return job.requiredPermissions;
|
|
42
46
|
}
|
|
43
47
|
function canView(job, actor, useCase) {
|
|
44
|
-
if (actor.has(useCase.requiredPermission) || isSameActor(job.createdBy, actor)) {
|
|
48
|
+
if (actor.has(useCase.requiredPermission) || isSameActor(job.createdBy, actor) || isOverrideGrantee(job, actor)) {
|
|
45
49
|
return ALLOWED;
|
|
46
50
|
}
|
|
47
51
|
return deny("forbidden", "You cannot see this request.");
|
|
48
52
|
}
|
|
53
|
+
function overrideGranteeOf(job) {
|
|
54
|
+
if (job.type !== APPROVAL_USE_CASE_TYPES.grantApprovalOverride && job.type !== APPROVAL_USE_CASE_TYPES.revokeApprovalOverride) {
|
|
55
|
+
return void 0;
|
|
56
|
+
}
|
|
57
|
+
const { granteeUserId, granteeEmail } = job.payload;
|
|
58
|
+
if (typeof granteeUserId !== "string" && typeof granteeEmail !== "string") {
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
userId: typeof granteeUserId === "string" ? granteeUserId : "",
|
|
63
|
+
email: typeof granteeEmail === "string" ? granteeEmail : void 0
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function isOverrideGrantee(job, actor) {
|
|
67
|
+
const grantee = overrideGranteeOf(job);
|
|
68
|
+
return grantee !== void 0 && isSameActor(grantee, actor);
|
|
69
|
+
}
|
|
70
|
+
function isOverrideGrantActive(grant, now) {
|
|
71
|
+
if (!grant || grant.revokedAt) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return new Date(grant.expiresAt).getTime() > now.getTime();
|
|
75
|
+
}
|
|
49
76
|
function canCreate(actor, useCase, request = {}) {
|
|
50
77
|
if (useCase.createPermission && !actor.has(useCase.createPermission)) {
|
|
51
78
|
return deny(
|
|
@@ -76,7 +103,10 @@ function canCreate(actor, useCase, request = {}) {
|
|
|
76
103
|
"This action needs the email address of the person who asks for it, and your session carries none. Sign in as a person to open this request."
|
|
77
104
|
);
|
|
78
105
|
}
|
|
79
|
-
if (
|
|
106
|
+
if (needsReason(actor, useCase, {
|
|
107
|
+
requiredAcks: request.requiredAcks,
|
|
108
|
+
requiredPermissions: toRun
|
|
109
|
+
}) && (request.reason ?? "").trim().length === 0) {
|
|
80
110
|
return deny("no-reason", "Give a reason to open this request.");
|
|
81
111
|
}
|
|
82
112
|
if (request.requiredAcks !== void 0 && request.requiredAcks < useCase.minAcks) {
|
|
@@ -87,6 +117,21 @@ function canCreate(actor, useCase, request = {}) {
|
|
|
87
117
|
}
|
|
88
118
|
return ALLOWED;
|
|
89
119
|
}
|
|
120
|
+
function opensWithOwnApproval(actor, requiredAcks, requiredPermissions, request) {
|
|
121
|
+
if (request !== void 0 && isOverrideGrantee(request, actor)) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return requiredAcks > 0 && hasEveryPermission(actor, requiredPermissions);
|
|
125
|
+
}
|
|
126
|
+
function needsReason(actor, useCase, request = {}) {
|
|
127
|
+
if (!useCase.reasonRequired) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return !(resolveRequiredAcks(useCase, request.requiredAcks) <= 1 && hasEveryPermission(
|
|
131
|
+
actor,
|
|
132
|
+
request.requiredPermissions ?? [useCase.requiredPermission]
|
|
133
|
+
));
|
|
134
|
+
}
|
|
90
135
|
function resolveRequiredAcks(useCase, requested) {
|
|
91
136
|
if (requested === void 0) {
|
|
92
137
|
return Math.max(useCase.defaultAcks, useCase.minAcks);
|
|
@@ -115,6 +160,12 @@ function canAcknowledge(job, actor, now) {
|
|
|
115
160
|
"You opened this request, so you cannot approve it."
|
|
116
161
|
);
|
|
117
162
|
}
|
|
163
|
+
if (isOverrideGrantee(job, actor)) {
|
|
164
|
+
return deny(
|
|
165
|
+
"self-approval",
|
|
166
|
+
"This request gives you an override grant, so you cannot approve it."
|
|
167
|
+
);
|
|
168
|
+
}
|
|
118
169
|
if (job.acks.some((ack) => isSameActor(ack, actor))) {
|
|
119
170
|
return deny("already-acknowledged", "You already approved this request.");
|
|
120
171
|
}
|
|
@@ -169,13 +220,20 @@ function canWithdraw(job, actor, now) {
|
|
|
169
220
|
return ALLOWED;
|
|
170
221
|
}
|
|
171
222
|
function canExecute(job, actor, now = /* @__PURE__ */ new Date()) {
|
|
172
|
-
|
|
223
|
+
const endsOwnGrant = (job.status === "scheduled" || job.status === "failed") && job.type === APPROVAL_USE_CASE_TYPES.revokeApprovalOverride && isOverrideGrantee(job, actor);
|
|
224
|
+
if (!endsOwnGrant && !hasEveryPermission(actor, job.requiredPermissions)) {
|
|
173
225
|
return deny("forbidden", "You cannot run this request.");
|
|
174
226
|
}
|
|
227
|
+
if (job.type === APPROVAL_USE_CASE_TYPES.grantApprovalOverride && isOverrideGrantee(job, actor)) {
|
|
228
|
+
return deny(
|
|
229
|
+
"self-approval",
|
|
230
|
+
"This request gives you an override grant, so you cannot run it."
|
|
231
|
+
);
|
|
232
|
+
}
|
|
175
233
|
if (job.status === "expired") {
|
|
176
234
|
return deny("expired", "This request is expired. Open a new request.");
|
|
177
235
|
}
|
|
178
|
-
if (job.status !== "approved" && job.status !== "failed") {
|
|
236
|
+
if (job.status !== "approved" && job.status !== "failed" && job.status !== "scheduled") {
|
|
179
237
|
return deny(
|
|
180
238
|
"not-executable",
|
|
181
239
|
`This request is ${job.status} and cannot be run.`
|
|
@@ -189,11 +247,65 @@ function canExecute(job, actor, now = /* @__PURE__ */ new Date()) {
|
|
|
189
247
|
}
|
|
190
248
|
return ALLOWED;
|
|
191
249
|
}
|
|
250
|
+
const OVERRIDE_GRANT_ENDED_MESSAGE = "The approval override grant that ran this request ended. Open a new request.";
|
|
251
|
+
function canRunOverridden(job, grant, now) {
|
|
252
|
+
if (!job.overriddenBy) {
|
|
253
|
+
return ALLOWED;
|
|
254
|
+
}
|
|
255
|
+
if (!isApprovalOverridable(job.type)) {
|
|
256
|
+
return deny(
|
|
257
|
+
"not-overridable",
|
|
258
|
+
"An override cannot run this type of request. It needs its approvals."
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (isOverrideGrantActive(grant, now)) {
|
|
262
|
+
return ALLOWED;
|
|
263
|
+
}
|
|
264
|
+
return deny("no-override-grant", OVERRIDE_GRANT_ENDED_MESSAGE);
|
|
265
|
+
}
|
|
266
|
+
function canOverride(job, actor, now, args) {
|
|
267
|
+
if (!args.grant || !isOverrideGrantActive(args.grant, now) || !isSameActor(args.grant, actor)) {
|
|
268
|
+
return deny(
|
|
269
|
+
"no-override-grant",
|
|
270
|
+
"You hold no active approval override grant."
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (!isApprovalOverridable(job.type)) {
|
|
274
|
+
return deny(
|
|
275
|
+
"not-overridable",
|
|
276
|
+
"An override cannot run this type of request. It needs its approvals."
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (!hasEveryPermission(actor, job.requiredPermissions)) {
|
|
280
|
+
return deny("forbidden", "You cannot run this request.");
|
|
281
|
+
}
|
|
282
|
+
if (isExpired(job, now)) {
|
|
283
|
+
return deny("expired", "This request expired and can no longer be run.");
|
|
284
|
+
}
|
|
285
|
+
if (job.status !== "pending") {
|
|
286
|
+
return deny(
|
|
287
|
+
"not-pending",
|
|
288
|
+
`This request is ${job.status} and cannot be run with an override.`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (args.reason.trim().length === 0) {
|
|
292
|
+
return deny(
|
|
293
|
+
"no-reason",
|
|
294
|
+
"Give a reason to run this request with an override."
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return ALLOWED;
|
|
298
|
+
}
|
|
192
299
|
const EXECUTE_WINDOW_DAYS = 7;
|
|
193
300
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
194
301
|
function runWindowEnd(job) {
|
|
195
|
-
const
|
|
196
|
-
(
|
|
302
|
+
const decisions = [
|
|
303
|
+
...job.acks.map((ack) => ack.date),
|
|
304
|
+
job.overriddenAt,
|
|
305
|
+
job.runAt
|
|
306
|
+
];
|
|
307
|
+
const start = decisions.reduce(
|
|
308
|
+
(latest, date) => date === void 0 ? latest : Math.max(latest, new Date(date).getTime()),
|
|
197
309
|
new Date(job.createdAt).getTime()
|
|
198
310
|
);
|
|
199
311
|
return new Date(start + EXECUTE_WINDOW_DAYS * DAY_MS);
|
|
@@ -209,11 +321,14 @@ function hasFailedRun(job) {
|
|
|
209
321
|
}
|
|
210
322
|
export {
|
|
211
323
|
EXECUTE_WINDOW_DAYS,
|
|
324
|
+
OVERRIDE_GRANT_ENDED_MESSAGE,
|
|
212
325
|
approvalPermissionCheck,
|
|
213
326
|
canAcknowledge,
|
|
214
327
|
canCreate,
|
|
215
328
|
canExecute,
|
|
329
|
+
canOverride,
|
|
216
330
|
canReject,
|
|
331
|
+
canRunOverridden,
|
|
217
332
|
canView,
|
|
218
333
|
canWithdraw,
|
|
219
334
|
effectiveStatus,
|
|
@@ -221,9 +336,13 @@ export {
|
|
|
221
336
|
hasEveryPermission,
|
|
222
337
|
hasFailedRun,
|
|
223
338
|
isExpired,
|
|
339
|
+
isOverrideGrantActive,
|
|
224
340
|
isRunWindowClosed,
|
|
225
341
|
isSameActor,
|
|
226
342
|
isSameActorEmail,
|
|
343
|
+
needsReason,
|
|
344
|
+
opensWithOwnApproval,
|
|
345
|
+
overrideGranteeOf,
|
|
227
346
|
remainingAcks,
|
|
228
347
|
requiredPermissionsOf,
|
|
229
348
|
resolveRequiredAcks,
|